| // Copyright (C) 2018 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import {BigintMath} from './bigint_math'; |
| import {Brand} from './brand'; |
| import {assertTrue} from './logging'; |
| |
| // The |time| type represents trace time in the same units and domain as trace |
| // processor (i.e. typically boot time in nanoseconds, but most of the UI should |
| // be completely agnostic to this). |
| export type time = Brand<bigint, 'time'>; |
| |
| // The |duration| type is used to represent the duration of time between two |
| // |time|s. The domain is irrelevant because a duration is relative. |
| export type duration = bigint; |
| |
| // The conversion factor for converting between different time units. |
| const TIME_UNITS_PER_SEC = 1e9; |
| const TIME_UNITS_PER_MILLISEC = 1e6; |
| const TIME_UNITS_PER_MICROSEC = 1e3; |
| |
| export class Time { |
| // Negative time is never found in a trace - so -1 is commonly used as a flag |
| // to represent a value is undefined or unset, without having to use a |
| // nullable or union type. |
| static readonly INVALID = Time.fromRaw(-1n); |
| |
| // The min and max possible values, considering times cannot be negative. |
| static readonly MIN = Time.fromRaw(0n); |
| static readonly MAX = Time.fromRaw(BigintMath.INT64_MAX); |
| |
| static readonly ZERO = Time.fromRaw(0n); |
| |
| // Cast a bigint to a |time|. Supports potentially |undefined| values. |
| // I.e. it performs the following conversions: |
| // - `bigint` -> `time` |
| // - `bigint|undefined` -> `time|undefined` |
| // |
| // Use this function with caution. The function is effectively a no-op in JS, |
| // but using it tells TypeScript that "this value is a time value". It's up to |
| // the caller to ensure the value is in the correct units and time domain. |
| // |
| // If you're reaching for this function after doing some maths on a |time| |
| // value and it's decayed to a |bigint| consider using the static math methods |
| // in |Time| instead, as they will do the appropriate casting for you. |
| static fromRaw(v: bigint): time; |
| static fromRaw(v?: bigint): time | undefined; |
| static fromRaw(v?: bigint): time | undefined { |
| return v as time | undefined; |
| } |
| |
| // Convert seconds (number) to a time value. |
| // Note: number -> BigInt conversion is relatively slow. |
| static fromSeconds(seconds: number): time { |
| return Time.fromRaw(BigInt(Math.floor(seconds * TIME_UNITS_PER_SEC))); |
| } |
| |
| // Convert time value to seconds and return as a number (i.e. float). |
| // Warning: This function is lossy, i.e. precision is lost when converting |
| // BigInt -> number. |
| // Note: BigInt -> number conversion is relatively slow. |
| static toSeconds(t: time): number { |
| return Number(t) / TIME_UNITS_PER_SEC; |
| } |
| |
| // Convert milliseconds (number) to a time value. |
| // Note: number -> BigInt conversion is relatively slow. |
| static fromMillis(millis: number): time { |
| return Time.fromRaw(BigInt(Math.floor(millis * TIME_UNITS_PER_MILLISEC))); |
| } |
| |
| // Convert time value to milliseconds and return as a number (i.e. float). |
| // Warning: This function is lossy, i.e. precision is lost when converting |
| // BigInt -> number. |
| // Note: BigInt -> number conversion is relatively slow. |
| static toMillis(t: time): number { |
| return Number(t) / TIME_UNITS_PER_MILLISEC; |
| } |
| |
| // Convert microseconds (number) to a time value. |
| // Note: number -> BigInt conversion is relatively slow. |
| static fromMicros(millis: number): time { |
| return Time.fromRaw(BigInt(Math.floor(millis * TIME_UNITS_PER_MICROSEC))); |
| } |
| |
| // Convert time value to microseconds and return as a number (i.e. float). |
| // Warning: This function is lossy, i.e. precision is lost when converting |
| // BigInt -> number. |
| // Note: BigInt -> number conversion is relatively slow. |
| static toMicros(t: time): number { |
| return Number(t) / TIME_UNITS_PER_MICROSEC; |
| } |
| |
| /** |
| * Convert a JavaScript Date to trace time. |
| * |
| * @param date - The date. Note that Date objects are time zone agnostic, they |
| * are a simple wrapper around a unix timestamp. A time zone is applied only |
| * when extracting data from it such as printing it to a string. |
| * @param unixOffset - This represents the trace time at the unix epoch |
| * (usually some large negative number). |
| * @returns The time represented in trace time. |
| */ |
| static fromDate(date: Date, unixOffset: duration): time { |
| const unixTimeMillis = date.getTime(); |
| const traceTime = Time.add(Time.fromMillis(unixTimeMillis), unixOffset); |
| return traceTime; |
| } |
| |
| // Convert time value to a Date object, given an offset from the unix epoch. |
| // Warning: This function is lossy, i.e. precision is lost when converting |
| // BigInt -> number. |
| // Note: BigInt -> number conversion is relatively slow. |
| |
| /** |
| * Converts trace time to a JavaScript Date object. |
| * |
| * @param time - A trace time. |
| * @param unixOffset - This represents the trace time at the unix epoch |
| * (usually some large negative number). |
| * @returns A JavaScript Date object. Remember Date objects don't contain any |
| * timezone information, they are a simple wrapper around unix time. |
| */ |
| static toDate(time: time, unixOffset: duration): Date { |
| const unixTimeMillis = Time.toMillis(Time.sub(time, unixOffset)); |
| return new Date(unixTimeMillis); |
| } |
| |
| static add(t: time, d: duration): time { |
| return Time.fromRaw(t + d); |
| } |
| |
| static sub(t: time, d: duration): time { |
| return Time.fromRaw(t - d); |
| } |
| |
| static diff(a: time, b: time): duration { |
| return a - b; |
| } |
| |
| static durationBetween(a: time, b: time): duration { |
| return a >= b ? a - b : b - a; |
| } |
| |
| static min(a: time, b: time): time { |
| return Time.fromRaw(BigintMath.min(a, b)); |
| } |
| |
| static max(a: time, b: time): time { |
| return Time.fromRaw(BigintMath.max(a, b)); |
| } |
| |
| static quantFloor(a: time, b: duration): time { |
| return Time.fromRaw(BigintMath.quantFloor(a, b)); |
| } |
| |
| static quantCeil(a: time, b: duration): time { |
| return Time.fromRaw(BigintMath.quantCeil(a, b)); |
| } |
| |
| static quant(a: time, b: duration): time { |
| return Time.fromRaw(BigintMath.quant(a, b)); |
| } |
| |
| static formatSeconds(time: time): string { |
| return Time.toSeconds(time).toString() + ' s'; |
| } |
| |
| static formatMilliseconds(time: time): string { |
| return Time.toMillis(time).toString() + ' ms'; |
| } |
| |
| static formatMicroseconds(time: time): string { |
| return Time.toMicros(time).toString() + ' us'; |
| } |
| |
| static toTimecode(time: time): Timecode { |
| return new Timecode(time); |
| } |
| } |
| |
| // Format `value` to `n` significant digits. |
| // Examples: (1234, 2) -> 1234. |
| // (12.34, 2) -> 12. |
| // (0.1234, 2) -> 0.12. |
| function toSignificantDigits(value: number, n: number): string { |
| const sign = Math.sign(value); |
| value = Math.abs(value); |
| // For each of (1, 10, 100, ..., 10^n) we need to render an additional digit |
| // after comma. |
| let pow = 1; |
| let digitsAfterComma = 0; |
| for (let i = 0; i < n; i++, pow *= 10) { |
| if (value < pow) { |
| digitsAfterComma++; |
| } |
| } |
| // Print precisely `digitsAfterComma` digits after comma, unless the number is an integer. |
| const formatted = |
| value === Math.round(value) ? value : value.toFixed(digitsAfterComma); |
| return `${sign < 0 ? '-' : ''}${formatted}`; |
| } |
| |
| export class Duration { |
| // The min and max possible duration values - durations can be negative. |
| static MIN = BigintMath.INT64_MIN; |
| static MAX = BigintMath.INT64_MAX; |
| static ZERO = 0n; |
| |
| // Cast a bigint to a |duration|. Supports potentially |undefined| values. |
| // I.e. it performs the following conversions: |
| // - `bigint` -> `duration` |
| // - `bigint|undefined` -> `duration|undefined` |
| // |
| // Use this function with caution. The function is effectively a no-op in JS, |
| // but using it tells TypeScript that "this value is a duration value". It's |
| // up to the caller to ensure the value is in the correct units. |
| // |
| // If you're reaching for this function after doing some maths on a |duration| |
| // value and it's decayed to a |bigint| consider using the static math methods |
| // in |duration| instead, as they will do the appropriate casting for you. |
| static fromRaw(v: bigint): duration; |
| static fromRaw(v?: bigint): duration | undefined; |
| static fromRaw(v?: bigint): duration | undefined { |
| return v as duration | undefined; |
| } |
| |
| static min(a: duration, b: duration): duration { |
| return BigintMath.min(a, b); |
| } |
| |
| static max(a: duration, b: duration): duration { |
| return BigintMath.max(a, b); |
| } |
| |
| static fromMillis(millis: number) { |
| return BigInt(Math.floor((millis / 1e3) * TIME_UNITS_PER_SEC)); |
| } |
| |
| // Convert time to seconds as a number. |
| // Use this function with caution. It loses precision and is slow. |
| static toSeconds(d: duration) { |
| return Number(d) / TIME_UNITS_PER_SEC; |
| } |
| |
| // Convert time to seconds as a number. |
| // Use this function with caution. It loses precision and is slow. |
| static toMilliseconds(d: duration) { |
| return Number(d) / TIME_UNITS_PER_MILLISEC; |
| } |
| |
| // Convert time to seconds as a number. |
| // Use this function with caution. It loses precision and is slow. |
| static toMicroSeconds(d: duration) { |
| return Number(d) / TIME_UNITS_PER_MICROSEC; |
| } |
| |
| // Print duration as as human readable string - i.e. to only a handful of |
| // significant figues. |
| // Use this when readability is more desireable than precision. |
| // Examples: 1234 -> 1.234us |
| // 123456789 -> 123.5ms |
| // 123,123,123,123,123 -> 123123s |
| // 1,000,000,000 -> 1s |
| // 1,000,000,023 -> 1.000s |
| // 1,230,000,023 -> 1.230s |
| static humanise(dur: duration): string { |
| if (dur < 1) return '0s'; |
| const units = ['ns', 'us', 'ms', 's']; |
| let n = Math.abs(Number(dur)); |
| let u = 0; |
| while (n >= 1000 && u + 1 < units.length) { |
| n /= 1000; |
| ++u; |
| } |
| return `${toSignificantDigits(Math.sign(Number(dur)) * n, 4)}${units[u]}`; |
| } |
| |
| // Print duration with absolute precision. |
| static format(duration: duration): string { |
| let result = ''; |
| if (duration < 1) return '0s'; |
| const unitAndValue: [string, bigint][] = [ |
| ['h', 3_600_000_000_000n], |
| ['m', 60_000_000_000n], |
| ['s', 1_000_000_000n], |
| ['ms', 1_000_000n], |
| ['us', 1_000n], |
| ['ns', 1n], |
| ]; |
| unitAndValue.forEach(([unit, unitSize]) => { |
| if (duration >= unitSize) { |
| const unitCount = duration / unitSize; |
| result += unitCount.toLocaleString() + unit + ' '; |
| duration = duration % unitSize; |
| } |
| }); |
| return result.slice(0, -1); |
| } |
| |
| static formatSeconds(dur: duration): string { |
| return Duration.toSeconds(dur).toString() + ' s'; |
| } |
| |
| static formatMilliseconds(dur: duration): string { |
| return Duration.toMilliseconds(dur).toString() + ' ms'; |
| } |
| |
| static formatMicroseconds(dur: duration): string { |
| return Duration.toMicroSeconds(dur).toString() + ' us'; |
| } |
| } |
| |
| // This class takes a time and converts it to a set of strings representing a |
| // time code where each string represents a group of time units formatted with |
| // an appropriate number of leading zeros. |
| export class Timecode { |
| public readonly sign: string; |
| public readonly days: string; |
| public readonly hours: string; |
| public readonly minutes: string; |
| public readonly seconds: string; |
| public readonly millis: string; |
| public readonly micros: string; |
| public readonly nanos: string; |
| |
| constructor(time: time) { |
| this.sign = time < 0 ? '-' : ''; |
| |
| const absTime = BigintMath.abs(time); |
| |
| const days = absTime / 86_400_000_000_000n; |
| const hours = (absTime / 3_600_000_000_000n) % 24n; |
| const minutes = (absTime / 60_000_000_000n) % 60n; |
| const seconds = (absTime / 1_000_000_000n) % 60n; |
| const millis = (absTime / 1_000_000n) % 1_000n; |
| const micros = (absTime / 1_000n) % 1_000n; |
| const nanos = absTime % 1_000n; |
| |
| this.days = days.toString(); |
| this.hours = hours.toString().padStart(2, '0'); |
| this.minutes = minutes.toString().padStart(2, '0'); |
| this.seconds = seconds.toString().padStart(2, '0'); |
| this.millis = millis.toString().padStart(3, '0'); |
| this.micros = micros.toString().padStart(3, '0'); |
| this.nanos = nanos.toString().padStart(3, '0'); |
| } |
| |
| // Get the upper part of the timecode formatted as: [-]DdHH:MM:SS. |
| get dhhmmss(): string { |
| const days = this.days === '0' ? '' : `${this.days}d`; |
| return `${this.sign}${days}${this.hours}:${this.minutes}:${this.seconds}`; |
| } |
| |
| // Get the subsecond part of the timecode formatted as: mmm uuu nnn. |
| // The "space" char is configurable but defaults to a normal space. |
| subsec(spaceChar: string = ' '): string { |
| return `${this.millis}${spaceChar}${this.micros}${spaceChar}${this.nanos}`; |
| } |
| |
| // Formats the entire timecode to a string. |
| toString(spaceChar: string = ' '): string { |
| return `${this.dhhmmss}.${this.subsec(spaceChar)}`; |
| } |
| } |
| |
| export function currentDateHourAndMinute(): string { |
| const date = new Date(); |
| return `${date |
| .toISOString() |
| .substr(0, 10)}-${date.getHours()}-${date.getMinutes()}`; |
| } |
| |
| export class TimeSpan { |
| static readonly ZERO = new TimeSpan(Time.ZERO, Time.ZERO); |
| |
| readonly start: time; |
| readonly end: time; |
| |
| constructor(start: time, end: time) { |
| assertTrue( |
| start <= end, |
| `Span start [${start}] cannot be greater than end [${end}]`, |
| ); |
| this.start = start; |
| this.end = end; |
| } |
| |
| static fromTimeAndDuration(start: time, duration: duration): TimeSpan { |
| return new TimeSpan(start, Time.add(start, duration)); |
| } |
| |
| get duration(): duration { |
| return this.end - this.start; |
| } |
| |
| get midpoint(): time { |
| return Time.fromRaw((this.start + this.end) / 2n); |
| } |
| |
| contains(t: time): boolean { |
| return this.start <= t && t < this.end; |
| } |
| |
| containsSpan(start: time, end: time): boolean { |
| return this.start <= start && end <= this.end; |
| } |
| |
| overlaps(start: time, end: time): boolean { |
| return !(end <= this.start || start >= this.end); |
| } |
| |
| equals(span: TimeSpan): boolean { |
| return this.start === span.start && this.end === span.end; |
| } |
| |
| translate(x: duration): TimeSpan { |
| return new TimeSpan(Time.add(this.start, x), Time.add(this.end, x)); |
| } |
| |
| pad(padding: duration): TimeSpan { |
| return new TimeSpan( |
| Time.sub(this.start, padding), |
| Time.add(this.end, padding), |
| ); |
| } |
| } |
| |
| /** |
| * Formats a Date object (unix timestamp) to a string in a specified timezone. |
| * |
| * @description |
| * The output format is a customizable combination of `YYYY-MM-DD`, |
| * `HH:mm:ss.SSS`, and the timezone offset `(±HH:MM)`. |
| * |
| * @param date The original JavaScript `Date` object to format. |
| * @param options An optional configuration object. |
| * @param {number} [options.tzOffsetMins=0] - The timezone offset in minutes |
| * from UTC. For example, -420 for UTC-7 or 330 for UTC+5:30. Defaults to 0 |
| * (UTC). |
| * @param {boolean} [options.printDate=true] - Whether to include the date part |
| * (`YYYY-MM-DD`). |
| * @param {boolean} [options.printTime=true] - Whether to include the time part |
| * (`HH:mm:ss.SSS`). |
| * @param {boolean} [options.printTimezone=true] - Whether to include the |
| * timezone offset. |
| * |
| * @returns A formatted string representing the date and time in the specified |
| * timezone. |
| * |
| * @example |
| * const myDate = new Date('2025-06-15T10:00:00.000Z'); |
| * |
| * // Format for Indian Standard Time (UTC+5:30) |
| * // tzOffsetMins = 5 * 60 + 30 = 330 |
| * const istString = formatDate(myDate, { tzOffsetMins: 330 }); |
| * console.log(istString); // "2025-06-15 15:30:00.000 UTC+05:30" |
| * |
| * // Format for Pacific Daylight Time (UTC-7) |
| * // tzOffsetMins = -7 * 60 = -420 |
| * const pdtString = formatDate(myDate, { tzOffsetMins: -420 }); |
| * console.log(pdtString); // "2025-06-15 03:00:00.000 UTC-07:00" |
| * |
| * // Format with only the date and timezone |
| * const dateOnly = formatDate(myDate, { tzOffsetMins: -420, printTime: false }); |
| * console.log(dateOnly); // "2025-06-15 UTC-07:00" |
| * |
| * // Format as UTC with no timezone part |
| * const utcOnly = formatDate(myDate, { printTimezone: false }); |
| * console.log(utcOnly); // "2025-06-15 10:00:00.000" |
| */ |
| export function formatDate( |
| date: Date, |
| { |
| tzOffsetMins = 0, |
| printDate = true, |
| printTime = true, |
| printTimezone = true, |
| }: { |
| printDate?: boolean; |
| printTime?: boolean; |
| printTimezone?: boolean; |
| tzOffsetMins?: number; |
| } = {}, |
| ) { |
| const originalTimestamp = date.getTime(); |
| const timezoneOffsetInMilliseconds = tzOffsetMins * 60 * 1000; |
| const dateInTimezone = new Date( |
| originalTimestamp + timezoneOffsetInMilliseconds, |
| ); |
| |
| const dateStringParts: string[] = []; |
| |
| if (printDate) { |
| const year = dateInTimezone.getUTCFullYear(); |
| const month = String(dateInTimezone.getUTCMonth() + 1).padStart(2, '0'); |
| const day = String(dateInTimezone.getUTCDate()).padStart(2, '0'); |
| dateStringParts.push(`${year}-${month}-${day}`); |
| } |
| |
| if (printTime) { |
| const hours = String(dateInTimezone.getUTCHours()).padStart(2, '0'); |
| const mins = String(dateInTimezone.getUTCMinutes()).padStart(2, '0'); |
| const sec = String(dateInTimezone.getUTCSeconds()).padStart(2, '0'); |
| const ms = String(dateInTimezone.getUTCMilliseconds()).padStart(3, '0'); |
| dateStringParts.push(`${hours}:${mins}:${sec}.${ms}`); |
| } |
| |
| if (printTimezone) { |
| dateStringParts.push(formatTimezone(tzOffsetMins)); |
| } |
| |
| return dateStringParts.join(' '); |
| } |
| |
| /** |
| * Given a timezone as an offset from UTC in minutes, format it in the usual ISO |
| * standard way. E.g.: UTC+01:00 |
| * |
| * @param tzOffsetMins - The timezone offset in minutes. |
| * @returns A string representing the timezone in ISO format. |
| */ |
| export function formatTimezone(tzOffsetMins: number): string { |
| const sign = tzOffsetMins >= 0 ? '+' : '-'; |
| const absMins = Math.abs(tzOffsetMins); |
| const hours = Math.floor(absMins / 60); |
| const mins = absMins % 60; |
| const hoursStr = String(hours).padStart(2, '0'); |
| const minsStr = String(mins).padStart(2, '0'); |
| return `UTC${sign}${hoursStr}:${minsStr}`; |
| } |