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; 31 32export class Time { 33 // Negative time is never found in a trace - so -1 is commonly used as a flag 34 // to represent a value is undefined or unset, without having to use a 35 // nullable or union type. 36 static readonly INVALID = Time.fromRaw(-1n); 37 38 // The min and max possible values, considering times cannot be negative. 39 static readonly MIN = Time.fromRaw(0n); 40 static readonly MAX = Time.fromRaw(BigintMath.INT64_MAX); 41 42 static readonly ZERO = Time.fromRaw(0n); 43 44 // Cast a bigint to a |time|. Supports potentially |undefined| values. 45 // I.e. it performs the following conversions: 46 // - `bigint` -> `time` 47 // - `bigint|undefined` -> `time|undefined` 48 // 49 // Use this function with caution. The function is effectively a no-op in JS, 50 // but using it tells TypeScript that "this value is a time value". It's up to 51 // the caller to ensure the value is in the correct units and time domain. 52 // 53 // If you're reaching for this function after doing some maths on a |time| 54 // value and it's decayed to a |bigint| consider using the static math methods 55 // in |Time| instead, as they will do the appropriate casting for you. 56 static fromRaw(v: bigint): time; 57 static fromRaw(v?: bigint): time | undefined; 58 static fromRaw(v?: bigint): time | undefined { 59 return v as time | undefined; 60 } 61 62 // Convert seconds (number) to a time value. 63 // Note: number -> BigInt conversion is relatively slow. 64 static fromSeconds(seconds: number): time { 65 return Time.fromRaw(BigInt(Math.floor(seconds * TIME_UNITS_PER_SEC))); 66 } 67 68 // Convert time value to seconds and return as a number (i.e. float). 69 // Warning: This function is lossy, i.e. precision is lost when converting 70 // BigInt -> number. 71 // Note: BigInt -> number conversion is relatively slow. 72 static toSeconds(t: time): number { 73 return Number(t) / TIME_UNITS_PER_SEC; 74 } 75 76 // Convert milliseconds (number) to a time value. 77 // Note: number -> BigInt conversion is relatively slow. 78 static fromMillis(millis: number): time { 79 return Time.fromRaw(BigInt(Math.floor(millis * TIME_UNITS_PER_MILLISEC))); 80 } 81 82 // Convert time value to milliseconds and return as a number (i.e. float). 83 // Warning: This function is lossy, i.e. precision is lost when converting 84 // BigInt -> number. 85 // Note: BigInt -> number conversion is relatively slow. 86 static toMillis(t: time): number { 87 return Number(t) / TIME_UNITS_PER_MILLISEC; 88 } 89 90 // Convert a Date object to a time value, given an offset from the unix epoch. 91 // Note: number -> BigInt conversion is relatively slow. 92 static fromDate(d: Date, offset: duration): time { 93 const millis = d.getTime(); 94 const t = Time.fromMillis(millis); 95 return Time.add(t, offset); 96 } 97 98 // Convert time value to a Date object, given an offset from the unix epoch. 99 // Warning: This function is lossy, i.e. precision is lost when converting 100 // BigInt -> number. 101 // Note: BigInt -> number conversion is relatively slow. 102 static toDate(t: time, offset: duration): Date { 103 const timeSinceEpoch = Time.sub(t, offset); 104 const millis = Time.toMillis(timeSinceEpoch); 105 return new Date(millis); 106 } 107 108 // Find the closest previous midnight for a given time value. 109 static getLatestMidnight(time: time, offset: duration): time { 110 const date = Time.toDate(time, offset); 111 const floorDay = new Date( 112 Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()), 113 ); 114 115 return Time.fromDate(floorDay, offset); 116 } 117 118 static add(t: time, d: duration): time { 119 return Time.fromRaw(t + d); 120 } 121 122 static sub(t: time, d: duration): time { 123 return Time.fromRaw(t - d); 124 } 125 126 static diff(a: time, b: time): duration { 127 return a - b; 128 } 129 130 static min(a: time, b: time): time { 131 return Time.fromRaw(BigintMath.min(a, b)); 132 } 133 134 static max(a: time, b: time): time { 135 return Time.fromRaw(BigintMath.max(a, b)); 136 } 137 138 static quantFloor(a: time, b: duration): time { 139 return Time.fromRaw(BigintMath.quantFloor(a, b)); 140 } 141 142 static quantCeil(a: time, b: duration): time { 143 return Time.fromRaw(BigintMath.quantCeil(a, b)); 144 } 145 146 static quant(a: time, b: duration): time { 147 return Time.fromRaw(BigintMath.quant(a, b)); 148 } 149 150 // Format time as seconds. 151 static formatSeconds(time: time): string { 152 return Time.toSeconds(time).toString() + ' s'; 153 } 154 155 static toTimecode(time: time): Timecode { 156 return new Timecode(time); 157 } 158} 159 160export class Duration { 161 // The min and max possible duration values - durations can be negative. 162 static MIN = BigintMath.INT64_MIN; 163 static MAX = BigintMath.INT64_MAX; 164 static ZERO = 0n; 165 166 // Cast a bigint to a |duration|. Supports potentially |undefined| values. 167 // I.e. it performs the following conversions: 168 // - `bigint` -> `duration` 169 // - `bigint|undefined` -> `duration|undefined` 170 // 171 // Use this function with caution. The function is effectively a no-op in JS, 172 // but using it tells TypeScript that "this value is a duration value". It's 173 // up to the caller to ensure the value is in the correct units. 174 // 175 // If you're reaching for this function after doing some maths on a |duration| 176 // value and it's decayed to a |bigint| consider using the static math methods 177 // in |duration| instead, as they will do the appropriate casting for you. 178 static fromRaw(v: bigint): duration; 179 static fromRaw(v?: bigint): duration | undefined; 180 static fromRaw(v?: bigint): duration | undefined { 181 return v as duration | undefined; 182 } 183 184 static min(a: duration, b: duration): duration { 185 return BigintMath.min(a, b); 186 } 187 188 static max(a: duration, b: duration): duration { 189 return BigintMath.max(a, b); 190 } 191 192 static fromMillis(millis: number) { 193 return BigInt(Math.floor((millis / 1e3) * TIME_UNITS_PER_SEC)); 194 } 195 196 // Convert time to seconds as a number. 197 // Use this function with caution. It loses precision and is slow. 198 static toSeconds(d: duration) { 199 return Number(d) / TIME_UNITS_PER_SEC; 200 } 201 202 // Print duration as as human readable string - i.e. to only a handful of 203 // significant figues. 204 // Use this when readability is more desireable than precision. 205 // Examples: 1234 -> 1.23ns 206 // 123456789 -> 123ms 207 // 123,123,123,123,123 -> 34h 12m 208 // 1,000,000,023 -> 1 s 209 // 1,230,000,023 -> 1.2 s 210 static humanise(dur: duration): string { 211 const sec = Duration.toSeconds(dur); 212 const units = ['s', 'ms', 'us', 'ns']; 213 const sign = Math.sign(sec); 214 let n = Math.abs(sec); 215 let u = 0; 216 while (n < 1 && n !== 0 && u < units.length - 1) { 217 n *= 1000; 218 u++; 219 } 220 return `${sign < 0 ? '-' : ''}${Math.round(n * 10) / 10}${units[u]}`; 221 } 222 223 // Print duration with absolute precision. 224 static format(duration: duration): string { 225 let result = ''; 226 if (duration < 1) return '0s'; 227 const unitAndValue: [string, bigint][] = [ 228 ['h', 3_600_000_000_000n], 229 ['m', 60_000_000_000n], 230 ['s', 1_000_000_000n], 231 ['ms', 1_000_000n], 232 ['us', 1_000n], 233 ['ns', 1n], 234 ]; 235 unitAndValue.forEach(([unit, unitSize]) => { 236 if (duration >= unitSize) { 237 const unitCount = duration / unitSize; 238 result += unitCount.toLocaleString() + unit + ' '; 239 duration = duration % unitSize; 240 } 241 }); 242 return result.slice(0, -1); 243 } 244 245 static formatSeconds(dur: duration): string { 246 return Duration.toSeconds(dur).toString() + ' s'; 247 } 248} 249 250// This class takes a time and converts it to a set of strings representing a 251// time code where each string represents a group of time units formatted with 252// an appropriate number of leading zeros. 253export class Timecode { 254 public readonly sign: string; 255 public readonly days: string; 256 public readonly hours: string; 257 public readonly minutes: string; 258 public readonly seconds: string; 259 public readonly millis: string; 260 public readonly micros: string; 261 public readonly nanos: string; 262 263 constructor(time: time) { 264 this.sign = time < 0 ? '-' : ''; 265 266 const absTime = BigintMath.abs(time); 267 268 const days = absTime / 86_400_000_000_000n; 269 const hours = (absTime / 3_600_000_000_000n) % 24n; 270 const minutes = (absTime / 60_000_000_000n) % 60n; 271 const seconds = (absTime / 1_000_000_000n) % 60n; 272 const millis = (absTime / 1_000_000n) % 1_000n; 273 const micros = (absTime / 1_000n) % 1_000n; 274 const nanos = absTime % 1_000n; 275 276 this.days = days.toString(); 277 this.hours = hours.toString().padStart(2, '0'); 278 this.minutes = minutes.toString().padStart(2, '0'); 279 this.seconds = seconds.toString().padStart(2, '0'); 280 this.millis = millis.toString().padStart(3, '0'); 281 this.micros = micros.toString().padStart(3, '0'); 282 this.nanos = nanos.toString().padStart(3, '0'); 283 } 284 285 // Get the upper part of the timecode formatted as: [-]DdHH:MM:SS. 286 get dhhmmss(): string { 287 const days = this.days === '0' ? '' : `${this.days}d`; 288 return `${this.sign}${days}${this.hours}:${this.minutes}:${this.seconds}`; 289 } 290 291 // Get the subsecond part of the timecode formatted as: mmm uuu nnn. 292 // The "space" char is configurable but defaults to a normal space. 293 subsec(spaceChar: string = ' '): string { 294 return `${this.millis}${spaceChar}${this.micros}${spaceChar}${this.nanos}`; 295 } 296 297 // Formats the entire timecode to a string. 298 toString(spaceChar: string = ' '): string { 299 return `${this.dhhmmss}.${this.subsec(spaceChar)}`; 300 } 301} 302 303export function currentDateHourAndMinute(): string { 304 const date = new Date(); 305 return `${date 306 .toISOString() 307 .substr(0, 10)}-${date.getHours()}-${date.getMinutes()}`; 308} 309 310// Create a Time value from an arbitrary SQL value. 311 312// Represents a half-open interval of time in the form [start, end). 313// E.g. interval contains all time values which are >= start and < end. 314export interface Span<TimeT, DurationT = TimeT> { 315 get start(): TimeT; 316 get end(): TimeT; 317 get duration(): DurationT; 318 get midpoint(): TimeT; 319 contains(span: TimeT | Span<TimeT, DurationT>): boolean; 320 intersectsInterval(span: Span<TimeT, DurationT>): boolean; 321 intersects(a: TimeT, b: TimeT): boolean; 322 equals(span: Span<TimeT, DurationT>): boolean; 323 add(offset: DurationT): Span<TimeT, DurationT>; 324 pad(padding: DurationT): Span<TimeT, DurationT>; 325} 326 327export class TimeSpan implements Span<time, duration> { 328 static readonly ZERO = new TimeSpan(Time.ZERO, Time.ZERO); 329 readonly start: time; 330 readonly end: time; 331 332 constructor(start: time, end: time) { 333 assertTrue( 334 start <= end, 335 `Span start [${start}] cannot be greater than end [${end}]`, 336 ); 337 this.start = start; 338 this.end = end; 339 } 340 341 static fromTimeAndDuration(start: time, duration: duration): TimeSpan { 342 return new TimeSpan(start, Time.add(start, duration)); 343 } 344 345 get duration(): duration { 346 return this.end - this.start; 347 } 348 349 get midpoint(): time { 350 return Time.fromRaw((this.start + this.end) / 2n); 351 } 352 353 contains(x: time | Span<time, duration>): boolean { 354 if (typeof x === 'bigint') { 355 return this.start <= x && x < this.end; 356 } else { 357 return this.start <= x.start && x.end <= this.end; 358 } 359 } 360 361 intersectsInterval(span: Span<time, duration>): boolean { 362 return !(span.end <= this.start || span.start >= this.end); 363 } 364 365 intersects(start: time, end: time): boolean { 366 return !(end <= this.start || start >= this.end); 367 } 368 369 equals(span: Span<time, duration>): boolean { 370 return this.start === span.start && this.end === span.end; 371 } 372 373 add(x: duration): Span<time, duration> { 374 return new TimeSpan(Time.add(this.start, x), Time.add(this.end, x)); 375 } 376 377 pad(padding: duration): Span<time, duration> { 378 return new TimeSpan( 379 Time.sub(this.start, padding), 380 Time.add(this.end, padding), 381 ); 382 } 383} 384 385// Print the date only for a given date in ISO format. 386export function toISODateOnly(date: Date) { 387 const year = date.getUTCFullYear(); 388 const month = String(date.getUTCMonth() + 1).padStart(2, '0'); 389 const day = String(date.getUTCDate()).padStart(2, '0'); 390 391 return `${year}-${month}-${day}`; 392} 393