• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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