1// Copyright (C) 2018 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {BigintMath} from './bigint_math'; 16import {Brand} from './brand'; 17import {assertTrue} from './logging'; 18 19// The |time| type represents trace time in the same units and domain as trace 20// processor (i.e. typically boot time in nanoseconds, but most of the UI should 21// be completely agnostic to this). 22export type time = Brand<bigint, 'time'>; 23 24// The |duration| type is used to represent the duration of time between two 25// |time|s. The domain is irrelevant because a duration is relative. 26export type duration = bigint; 27 28// The conversion factor for converting between different time units. 29const TIME_UNITS_PER_SEC = 1e9; 30const TIME_UNITS_PER_MILLISEC = 1e6; 31const TIME_UNITS_PER_MICROSEC = 1e3; 32 33export class Time { 34 // Negative time is never found in a trace - so -1 is commonly used as a flag 35 // to represent a value is undefined or unset, without having to use a 36 // nullable or union type. 37 static readonly INVALID = Time.fromRaw(-1n); 38 39 // The min and max possible values, considering times cannot be negative. 40 static readonly MIN = Time.fromRaw(0n); 41 static readonly MAX = Time.fromRaw(BigintMath.INT64_MAX); 42 43 static readonly ZERO = Time.fromRaw(0n); 44 45 // Cast a bigint to a |time|. Supports potentially |undefined| values. 46 // I.e. it performs the following conversions: 47 // - `bigint` -> `time` 48 // - `bigint|undefined` -> `time|undefined` 49 // 50 // Use this function with caution. The function is effectively a no-op in JS, 51 // but using it tells TypeScript that "this value is a time value". It's up to 52 // the caller to ensure the value is in the correct units and time domain. 53 // 54 // If you're reaching for this function after doing some maths on a |time| 55 // value and it's decayed to a |bigint| consider using the static math methods 56 // in |Time| instead, as they will do the appropriate casting for you. 57 static fromRaw(v: bigint): time; 58 static fromRaw(v?: bigint): time | undefined; 59 static fromRaw(v?: bigint): time | undefined { 60 return v as time | undefined; 61 } 62 63 // Convert seconds (number) to a time value. 64 // Note: number -> BigInt conversion is relatively slow. 65 static fromSeconds(seconds: number): time { 66 return Time.fromRaw(BigInt(Math.floor(seconds * TIME_UNITS_PER_SEC))); 67 } 68 69 // Convert time value to seconds and return as a number (i.e. float). 70 // Warning: This function is lossy, i.e. precision is lost when converting 71 // BigInt -> number. 72 // Note: BigInt -> number conversion is relatively slow. 73 static toSeconds(t: time): number { 74 return Number(t) / TIME_UNITS_PER_SEC; 75 } 76 77 // Convert milliseconds (number) to a time value. 78 // Note: number -> BigInt conversion is relatively slow. 79 static fromMillis(millis: number): time { 80 return Time.fromRaw(BigInt(Math.floor(millis * TIME_UNITS_PER_MILLISEC))); 81 } 82 83 // Convert time value to milliseconds and return as a number (i.e. float). 84 // Warning: This function is lossy, i.e. precision is lost when converting 85 // BigInt -> number. 86 // Note: BigInt -> number conversion is relatively slow. 87 static toMillis(t: time): number { 88 return Number(t) / TIME_UNITS_PER_MILLISEC; 89 } 90 91 // Convert microseconds (number) to a time value. 92 // Note: number -> BigInt conversion is relatively slow. 93 static fromMicros(millis: number): time { 94 return Time.fromRaw(BigInt(Math.floor(millis * TIME_UNITS_PER_MICROSEC))); 95 } 96 97 // Convert time value to microseconds and return as a number (i.e. float). 98 // Warning: This function is lossy, i.e. precision is lost when converting 99 // BigInt -> number. 100 // Note: BigInt -> number conversion is relatively slow. 101 static toMicros(t: time): number { 102 return Number(t) / TIME_UNITS_PER_MICROSEC; 103 } 104 105 // Convert a Date object to a time value, given an offset from the unix epoch. 106 // Note: number -> BigInt conversion is relatively slow. 107 static fromDate(d: Date, offset: duration): time { 108 const millis = d.getTime(); 109 const t = Time.fromMillis(millis); 110 return Time.add(t, offset); 111 } 112 113 // Convert time value to a Date object, given an offset from the unix epoch. 114 // Warning: This function is lossy, i.e. precision is lost when converting 115 // BigInt -> number. 116 // Note: BigInt -> number conversion is relatively slow. 117 static toDate(t: time, offset: duration): Date { 118 const timeSinceEpoch = Time.sub(t, offset); 119 const millis = Time.toMillis(timeSinceEpoch); 120 return new Date(millis); 121 } 122 123 // Find the closest previous midnight for a given time value. 124 static getLatestMidnight(time: time, offset: duration): time { 125 const date = Time.toDate(time, offset); 126 const floorDay = new Date( 127 Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()), 128 ); 129 130 return Time.fromDate(floorDay, offset); 131 } 132 133 static add(t: time, d: duration): time { 134 return Time.fromRaw(t + d); 135 } 136 137 static sub(t: time, d: duration): time { 138 return Time.fromRaw(t - d); 139 } 140 141 static diff(a: time, b: time): duration { 142 return a - b; 143 } 144 145 static min(a: time, b: time): time { 146 return Time.fromRaw(BigintMath.min(a, b)); 147 } 148 149 static max(a: time, b: time): time { 150 return Time.fromRaw(BigintMath.max(a, b)); 151 } 152 153 static quantFloor(a: time, b: duration): time { 154 return Time.fromRaw(BigintMath.quantFloor(a, b)); 155 } 156 157 static quantCeil(a: time, b: duration): time { 158 return Time.fromRaw(BigintMath.quantCeil(a, b)); 159 } 160 161 static quant(a: time, b: duration): time { 162 return Time.fromRaw(BigintMath.quant(a, b)); 163 } 164 165 static formatSeconds(time: time): string { 166 return Time.toSeconds(time).toString() + ' s'; 167 } 168 169 static formatMilliseconds(time: time): string { 170 return Time.toMillis(time).toString() + ' ms'; 171 } 172 173 static formatMicroseconds(time: time): string { 174 return Time.toMicros(time).toString() + ' us'; 175 } 176 177 static toTimecode(time: time): Timecode { 178 return new Timecode(time); 179 } 180} 181 182// Format `value` to `n` significant digits. 183// Examples: (1234, 2) -> 1234. 184// (12.34, 2) -> 12. 185// (0.1234, 2) -> 0.12. 186function toSignificantDigits(value: number, n: number): string { 187 const sign = Math.sign(value); 188 value = Math.abs(value); 189 // For each of (1, 10, 100, ..., 10^n) we need to render an additional digit 190 // after comma. 191 let pow = 1; 192 let digitsAfterComma = 0; 193 for (let i = 0; i < n; i++, pow *= 10) { 194 if (value < pow) { 195 digitsAfterComma++; 196 } 197 } 198 // Print precisely `digitsAfterComma` digits after comma, unless the number is an integer. 199 const formatted = 200 value === Math.round(value) ? value : value.toFixed(digitsAfterComma); 201 return `${sign < 0 ? '-' : ''}${formatted}`; 202} 203 204export class Duration { 205 // The min and max possible duration values - durations can be negative. 206 static MIN = BigintMath.INT64_MIN; 207 static MAX = BigintMath.INT64_MAX; 208 static ZERO = 0n; 209 210 // Cast a bigint to a |duration|. Supports potentially |undefined| values. 211 // I.e. it performs the following conversions: 212 // - `bigint` -> `duration` 213 // - `bigint|undefined` -> `duration|undefined` 214 // 215 // Use this function with caution. The function is effectively a no-op in JS, 216 // but using it tells TypeScript that "this value is a duration value". It's 217 // up to the caller to ensure the value is in the correct units. 218 // 219 // If you're reaching for this function after doing some maths on a |duration| 220 // value and it's decayed to a |bigint| consider using the static math methods 221 // in |duration| instead, as they will do the appropriate casting for you. 222 static fromRaw(v: bigint): duration; 223 static fromRaw(v?: bigint): duration | undefined; 224 static fromRaw(v?: bigint): duration | undefined { 225 return v as duration | undefined; 226 } 227 228 static min(a: duration, b: duration): duration { 229 return BigintMath.min(a, b); 230 } 231 232 static max(a: duration, b: duration): duration { 233 return BigintMath.max(a, b); 234 } 235 236 static fromMillis(millis: number) { 237 return BigInt(Math.floor((millis / 1e3) * TIME_UNITS_PER_SEC)); 238 } 239 240 // Convert time to seconds as a number. 241 // Use this function with caution. It loses precision and is slow. 242 static toSeconds(d: duration) { 243 return Number(d) / TIME_UNITS_PER_SEC; 244 } 245 246 // Convert time to seconds as a number. 247 // Use this function with caution. It loses precision and is slow. 248 static toMilliseconds(d: duration) { 249 return Number(d) / TIME_UNITS_PER_MILLISEC; 250 } 251 252 // Convert time to seconds as a number. 253 // Use this function with caution. It loses precision and is slow. 254 static toMicroSeconds(d: duration) { 255 return Number(d) / TIME_UNITS_PER_MICROSEC; 256 } 257 258 // Print duration as as human readable string - i.e. to only a handful of 259 // significant figues. 260 // Use this when readability is more desireable than precision. 261 // Examples: 1234 -> 1.234us 262 // 123456789 -> 123.5ms 263 // 123,123,123,123,123 -> 123123s 264 // 1,000,000,000 -> 1s 265 // 1,000,000,023 -> 1.000s 266 // 1,230,000,023 -> 1.230s 267 static humanise(dur: duration): string { 268 if (dur < 1) return '0s'; 269 const units = ['ns', 'us', 'ms', 's']; 270 let n = Math.abs(Number(dur)); 271 let u = 0; 272 while (n >= 1000 && u + 1 < units.length) { 273 n /= 1000; 274 ++u; 275 } 276 return `${toSignificantDigits(Math.sign(Number(dur)) * n, 4)}${units[u]}`; 277 } 278 279 // Print duration with absolute precision. 280 static format(duration: duration): string { 281 let result = ''; 282 if (duration < 1) return '0s'; 283 const unitAndValue: [string, bigint][] = [ 284 ['h', 3_600_000_000_000n], 285 ['m', 60_000_000_000n], 286 ['s', 1_000_000_000n], 287 ['ms', 1_000_000n], 288 ['us', 1_000n], 289 ['ns', 1n], 290 ]; 291 unitAndValue.forEach(([unit, unitSize]) => { 292 if (duration >= unitSize) { 293 const unitCount = duration / unitSize; 294 result += unitCount.toLocaleString() + unit + ' '; 295 duration = duration % unitSize; 296 } 297 }); 298 return result.slice(0, -1); 299 } 300 301 static formatSeconds(dur: duration): string { 302 return Duration.toSeconds(dur).toString() + ' s'; 303 } 304 305 static formatMilliseconds(dur: duration): string { 306 return Duration.toMilliseconds(dur).toString() + ' ms'; 307 } 308 309 static formatMicroseconds(dur: duration): string { 310 return Duration.toMicroSeconds(dur).toString() + ' us'; 311 } 312} 313 314// This class takes a time and converts it to a set of strings representing a 315// time code where each string represents a group of time units formatted with 316// an appropriate number of leading zeros. 317export class Timecode { 318 public readonly sign: string; 319 public readonly days: string; 320 public readonly hours: string; 321 public readonly minutes: string; 322 public readonly seconds: string; 323 public readonly millis: string; 324 public readonly micros: string; 325 public readonly nanos: string; 326 327 constructor(time: time) { 328 this.sign = time < 0 ? '-' : ''; 329 330 const absTime = BigintMath.abs(time); 331 332 const days = absTime / 86_400_000_000_000n; 333 const hours = (absTime / 3_600_000_000_000n) % 24n; 334 const minutes = (absTime / 60_000_000_000n) % 60n; 335 const seconds = (absTime / 1_000_000_000n) % 60n; 336 const millis = (absTime / 1_000_000n) % 1_000n; 337 const micros = (absTime / 1_000n) % 1_000n; 338 const nanos = absTime % 1_000n; 339 340 this.days = days.toString(); 341 this.hours = hours.toString().padStart(2, '0'); 342 this.minutes = minutes.toString().padStart(2, '0'); 343 this.seconds = seconds.toString().padStart(2, '0'); 344 this.millis = millis.toString().padStart(3, '0'); 345 this.micros = micros.toString().padStart(3, '0'); 346 this.nanos = nanos.toString().padStart(3, '0'); 347 } 348 349 // Get the upper part of the timecode formatted as: [-]DdHH:MM:SS. 350 get dhhmmss(): string { 351 const days = this.days === '0' ? '' : `${this.days}d`; 352 return `${this.sign}${days}${this.hours}:${this.minutes}:${this.seconds}`; 353 } 354 355 // Get the subsecond part of the timecode formatted as: mmm uuu nnn. 356 // The "space" char is configurable but defaults to a normal space. 357 subsec(spaceChar: string = ' '): string { 358 return `${this.millis}${spaceChar}${this.micros}${spaceChar}${this.nanos}`; 359 } 360 361 // Formats the entire timecode to a string. 362 toString(spaceChar: string = ' '): string { 363 return `${this.dhhmmss}.${this.subsec(spaceChar)}`; 364 } 365} 366 367export function currentDateHourAndMinute(): string { 368 const date = new Date(); 369 return `${date 370 .toISOString() 371 .substr(0, 10)}-${date.getHours()}-${date.getMinutes()}`; 372} 373 374export class TimeSpan { 375 static readonly ZERO = new TimeSpan(Time.ZERO, Time.ZERO); 376 377 readonly start: time; 378 readonly end: time; 379 380 constructor(start: time, end: time) { 381 assertTrue( 382 start <= end, 383 `Span start [${start}] cannot be greater than end [${end}]`, 384 ); 385 this.start = start; 386 this.end = end; 387 } 388 389 static fromTimeAndDuration(start: time, duration: duration): TimeSpan { 390 return new TimeSpan(start, Time.add(start, duration)); 391 } 392 393 get duration(): duration { 394 return this.end - this.start; 395 } 396 397 get midpoint(): time { 398 return Time.fromRaw((this.start + this.end) / 2n); 399 } 400 401 contains(t: time): boolean { 402 return this.start <= t && t < this.end; 403 } 404 405 containsSpan(start: time, end: time): boolean { 406 return this.start <= start && end <= this.end; 407 } 408 409 overlaps(start: time, end: time): boolean { 410 return !(end <= this.start || start >= this.end); 411 } 412 413 equals(span: TimeSpan): boolean { 414 return this.start === span.start && this.end === span.end; 415 } 416 417 translate(x: duration): TimeSpan { 418 return new TimeSpan(Time.add(this.start, x), Time.add(this.end, x)); 419 } 420 421 pad(padding: duration): TimeSpan { 422 return new TimeSpan( 423 Time.sub(this.start, padding), 424 Time.add(this.end, padding), 425 ); 426 } 427} 428 429// Print the date only for a given date in ISO format. 430export function toISODateOnly(date: Date) { 431 const year = date.getUTCFullYear(); 432 const month = String(date.getUTCMonth() + 1).padStart(2, '0'); 433 const day = String(date.getUTCDate()).padStart(2, '0'); 434 435 return `${year}-${month}-${day}`; 436} 437