• 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;
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