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 182export class Duration { 183 // The min and max possible duration values - durations can be negative. 184 static MIN = BigintMath.INT64_MIN; 185 static MAX = BigintMath.INT64_MAX; 186 static ZERO = 0n; 187 188 // Cast a bigint to a |duration|. Supports potentially |undefined| values. 189 // I.e. it performs the following conversions: 190 // - `bigint` -> `duration` 191 // - `bigint|undefined` -> `duration|undefined` 192 // 193 // Use this function with caution. The function is effectively a no-op in JS, 194 // but using it tells TypeScript that "this value is a duration value". It's 195 // up to the caller to ensure the value is in the correct units. 196 // 197 // If you're reaching for this function after doing some maths on a |duration| 198 // value and it's decayed to a |bigint| consider using the static math methods 199 // in |duration| instead, as they will do the appropriate casting for you. 200 static fromRaw(v: bigint): duration; 201 static fromRaw(v?: bigint): duration | undefined; 202 static fromRaw(v?: bigint): duration | undefined { 203 return v as duration | undefined; 204 } 205 206 static min(a: duration, b: duration): duration { 207 return BigintMath.min(a, b); 208 } 209 210 static max(a: duration, b: duration): duration { 211 return BigintMath.max(a, b); 212 } 213 214 static fromMillis(millis: number) { 215 return BigInt(Math.floor((millis / 1e3) * TIME_UNITS_PER_SEC)); 216 } 217 218 // Convert time to seconds as a number. 219 // Use this function with caution. It loses precision and is slow. 220 static toSeconds(d: duration) { 221 return Number(d) / TIME_UNITS_PER_SEC; 222 } 223 224 // Convert time to seconds as a number. 225 // Use this function with caution. It loses precision and is slow. 226 static toMilliseconds(d: duration) { 227 return Number(d) / TIME_UNITS_PER_MILLISEC; 228 } 229 230 // Convert time to seconds as a number. 231 // Use this function with caution. It loses precision and is slow. 232 static toMicroSeconds(d: duration) { 233 return Number(d) / TIME_UNITS_PER_MICROSEC; 234 } 235 236 // Print duration as as human readable string - i.e. to only a handful of 237 // significant figues. 238 // Use this when readability is more desireable than precision. 239 // Examples: 1234 -> 1.23ns 240 // 123456789 -> 123ms 241 // 123,123,123,123,123 -> 34h 12m 242 // 1,000,000,023 -> 1 s 243 // 1,230,000,023 -> 1.2 s 244 static humanise(dur: duration): string { 245 const sec = Duration.toSeconds(dur); 246 const units = ['s', 'ms', 'us', 'ns']; 247 const sign = Math.sign(sec); 248 let n = Math.abs(sec); 249 let u = 0; 250 while (n < 1 && n !== 0 && u < units.length - 1) { 251 n *= 1000; 252 u++; 253 } 254 return `${sign < 0 ? '-' : ''}${Math.round(n * 10) / 10}${units[u]}`; 255 } 256 257 // Print duration with absolute precision. 258 static format(duration: duration): string { 259 let result = ''; 260 if (duration < 1) return '0s'; 261 const unitAndValue: [string, bigint][] = [ 262 ['h', 3_600_000_000_000n], 263 ['m', 60_000_000_000n], 264 ['s', 1_000_000_000n], 265 ['ms', 1_000_000n], 266 ['us', 1_000n], 267 ['ns', 1n], 268 ]; 269 unitAndValue.forEach(([unit, unitSize]) => { 270 if (duration >= unitSize) { 271 const unitCount = duration / unitSize; 272 result += unitCount.toLocaleString() + unit + ' '; 273 duration = duration % unitSize; 274 } 275 }); 276 return result.slice(0, -1); 277 } 278 279 static formatSeconds(dur: duration): string { 280 return Duration.toSeconds(dur).toString() + ' s'; 281 } 282 283 static formatMilliseconds(dur: duration): string { 284 return Duration.toMilliseconds(dur).toString() + ' ms'; 285 } 286 287 static formatMicroseconds(dur: duration): string { 288 return Duration.toMicroSeconds(dur).toString() + ' us'; 289 } 290} 291 292// This class takes a time and converts it to a set of strings representing a 293// time code where each string represents a group of time units formatted with 294// an appropriate number of leading zeros. 295export class Timecode { 296 public readonly sign: string; 297 public readonly days: string; 298 public readonly hours: string; 299 public readonly minutes: string; 300 public readonly seconds: string; 301 public readonly millis: string; 302 public readonly micros: string; 303 public readonly nanos: string; 304 305 constructor(time: time) { 306 this.sign = time < 0 ? '-' : ''; 307 308 const absTime = BigintMath.abs(time); 309 310 const days = absTime / 86_400_000_000_000n; 311 const hours = (absTime / 3_600_000_000_000n) % 24n; 312 const minutes = (absTime / 60_000_000_000n) % 60n; 313 const seconds = (absTime / 1_000_000_000n) % 60n; 314 const millis = (absTime / 1_000_000n) % 1_000n; 315 const micros = (absTime / 1_000n) % 1_000n; 316 const nanos = absTime % 1_000n; 317 318 this.days = days.toString(); 319 this.hours = hours.toString().padStart(2, '0'); 320 this.minutes = minutes.toString().padStart(2, '0'); 321 this.seconds = seconds.toString().padStart(2, '0'); 322 this.millis = millis.toString().padStart(3, '0'); 323 this.micros = micros.toString().padStart(3, '0'); 324 this.nanos = nanos.toString().padStart(3, '0'); 325 } 326 327 // Get the upper part of the timecode formatted as: [-]DdHH:MM:SS. 328 get dhhmmss(): string { 329 const days = this.days === '0' ? '' : `${this.days}d`; 330 return `${this.sign}${days}${this.hours}:${this.minutes}:${this.seconds}`; 331 } 332 333 // Get the subsecond part of the timecode formatted as: mmm uuu nnn. 334 // The "space" char is configurable but defaults to a normal space. 335 subsec(spaceChar: string = ' '): string { 336 return `${this.millis}${spaceChar}${this.micros}${spaceChar}${this.nanos}`; 337 } 338 339 // Formats the entire timecode to a string. 340 toString(spaceChar: string = ' '): string { 341 return `${this.dhhmmss}.${this.subsec(spaceChar)}`; 342 } 343} 344 345export function currentDateHourAndMinute(): string { 346 const date = new Date(); 347 return `${date 348 .toISOString() 349 .substr(0, 10)}-${date.getHours()}-${date.getMinutes()}`; 350} 351 352export class TimeSpan { 353 static readonly ZERO = new TimeSpan(Time.ZERO, Time.ZERO); 354 355 readonly start: time; 356 readonly end: time; 357 358 constructor(start: time, end: time) { 359 assertTrue( 360 start <= end, 361 `Span start [${start}] cannot be greater than end [${end}]`, 362 ); 363 this.start = start; 364 this.end = end; 365 } 366 367 static fromTimeAndDuration(start: time, duration: duration): TimeSpan { 368 return new TimeSpan(start, Time.add(start, duration)); 369 } 370 371 get duration(): duration { 372 return this.end - this.start; 373 } 374 375 get midpoint(): time { 376 return Time.fromRaw((this.start + this.end) / 2n); 377 } 378 379 contains(t: time): boolean { 380 return this.start <= t && t < this.end; 381 } 382 383 containsSpan(start: time, end: time): boolean { 384 return this.start <= start && end <= this.end; 385 } 386 387 overlaps(start: time, end: time): boolean { 388 return !(end <= this.start || start >= this.end); 389 } 390 391 equals(span: TimeSpan): boolean { 392 return this.start === span.start && this.end === span.end; 393 } 394 395 translate(x: duration): TimeSpan { 396 return new TimeSpan(Time.add(this.start, x), Time.add(this.end, x)); 397 } 398 399 pad(padding: duration): TimeSpan { 400 return new TimeSpan( 401 Time.sub(this.start, padding), 402 Time.add(this.end, padding), 403 ); 404 } 405} 406 407// Print the date only for a given date in ISO format. 408export function toISODateOnly(date: Date) { 409 const year = date.getUTCFullYear(); 410 const month = String(date.getUTCMonth() + 1).padStart(2, '0'); 411 const day = String(date.getUTCDate()).padStart(2, '0'); 412 413 return `${year}-${month}-${day}`; 414} 415