// Copyright (C) 2023 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 {assertTrue} from '../base/logging'; import {Span, Time, time} from '../base/time'; export type RoundMode = 'round' | 'floor' | 'ceil'; export type Timeish = HighPrecisionTime | time; // Stores a time as a bigint and an offset which is capable of: // - Storing and reproducing "Time"s without losing precision. // - Storing time with sub-nanosecond precision. // This class is immutable - each operation returns a new object. export class HighPrecisionTime { // Time in nanoseconds == base + offset // offset is kept in the range 0 <= x < 1 to avoid losing precision readonly base: bigint; readonly offset: number; static get ZERO(): HighPrecisionTime { return new HighPrecisionTime(0n); } constructor(base: bigint = 0n, offset: number = 0) { // Normalize offset to sit in the range 0.0 <= x < 1.0 const offsetFloor = Math.floor(offset); this.base = base + BigInt(offsetFloor); this.offset = offset - offsetFloor; } static fromTime(timestamp: time): HighPrecisionTime { return new HighPrecisionTime(timestamp, 0); } static fromNanos(nanos: number | bigint) { if (typeof nanos === 'number') { return new HighPrecisionTime(0n, nanos); } else if (typeof nanos === 'bigint') { return new HighPrecisionTime(nanos); } else { const value: never = nanos; throw new Error(`Value ${value} is neither a number nor a bigint`); } } static fromSeconds(seconds: number) { const nanos = seconds * 1e9; const offset = nanos - Math.floor(nanos); return new HighPrecisionTime(BigInt(Math.floor(nanos)), offset); } static max(a: HighPrecisionTime, b: HighPrecisionTime): HighPrecisionTime { return a.gt(b) ? a : b; } static min(a: HighPrecisionTime, b: HighPrecisionTime): HighPrecisionTime { return a.lt(b) ? a : b; } toTime(roundMode: RoundMode = 'floor'): time { switch (roundMode) { case 'round': return Time.fromRaw(this.base + BigInt(Math.round(this.offset))); case 'floor': return Time.fromRaw(this.base); case 'ceil': return Time.fromRaw(this.base + BigInt(Math.ceil(this.offset))); default: const exhaustiveCheck: never = roundMode; throw new Error(`Unhandled roundMode case: ${exhaustiveCheck}`); } } get nanos(): number { // WARNING: Number(bigint) can be surprisingly slow. // WARNING: Precision may be lost here. return Number(this.base) + this.offset; } get seconds(): number { // WARNING: Number(bigint) can be surprisingly slow. // WARNING: Precision may be lost here. return (Number(this.base) + this.offset) / 1e9; } add(other: HighPrecisionTime): HighPrecisionTime { return new HighPrecisionTime( this.base + other.base, this.offset + other.offset, ); } addNanos(nanos: number | bigint): HighPrecisionTime { return this.add(HighPrecisionTime.fromNanos(nanos)); } addSeconds(seconds: number): HighPrecisionTime { return new HighPrecisionTime(this.base, this.offset + seconds * 1e9); } addTime(ts: time): HighPrecisionTime { return new HighPrecisionTime(this.base + ts, this.offset); } sub(other: HighPrecisionTime): HighPrecisionTime { return new HighPrecisionTime( this.base - other.base, this.offset - other.offset, ); } subTime(ts: time): HighPrecisionTime { return new HighPrecisionTime(this.base - ts, this.offset); } subNanos(nanos: number | bigint): HighPrecisionTime { return this.add(HighPrecisionTime.fromNanos(-nanos)); } divide(divisor: number): HighPrecisionTime { return this.multiply(1 / divisor); } multiply(factor: number): HighPrecisionTime { const factorFloor = Math.floor(factor); const newBase = this.base * BigInt(factorFloor); const additionalBit = Number(this.base) * (factor - factorFloor); const newOffset = factor * this.offset + additionalBit; return new HighPrecisionTime(newBase, newOffset); } // Return true if other time is within some epsilon, default 1 femtosecond eq(other: Timeish, epsilon: number = 1e-6): boolean { const x = HighPrecisionTime.fromHPTimeOrTime(other); return Math.abs(this.sub(x).nanos) < epsilon; } private static fromHPTimeOrTime( x: HighPrecisionTime | time, ): HighPrecisionTime { if (x instanceof HighPrecisionTime) { return x; } else if (typeof x === 'bigint') { return HighPrecisionTime.fromTime(x); } else { const y: never = x; throw new Error(`Invalid type ${y}`); } } lt(other: Timeish): boolean { const x = HighPrecisionTime.fromHPTimeOrTime(other); if (this.base < x.base) { return true; } else if (this.base === x.base) { return this.offset < x.offset; } else { return false; } } lte(other: Timeish): boolean { if (this.eq(other)) { return true; } else { return this.lt(other); } } gt(other: Timeish): boolean { return !this.lte(other); } gte(other: Timeish): boolean { return !this.lt(other); } clamp(lower: HighPrecisionTime, upper: HighPrecisionTime): HighPrecisionTime { if (this.lt(lower)) { return lower; } else if (this.gt(upper)) { return upper; } else { return this; } } toString(): string { const offsetAsString = this.offset.toString(); if (offsetAsString === '0') { return this.base.toString(); } else { return `${this.base}${offsetAsString.substring(1)}`; } } abs(): HighPrecisionTime { if (this.base >= 0n) { return this; } const newBase = -this.base; const newOffset = -this.offset; return new HighPrecisionTime(newBase, newOffset); } } export class HighPrecisionTimeSpan implements Span { readonly start: HighPrecisionTime; readonly end: HighPrecisionTime; static readonly ZERO = new HighPrecisionTimeSpan( HighPrecisionTime.ZERO, HighPrecisionTime.ZERO, ); constructor(start: time | HighPrecisionTime, end: time | HighPrecisionTime) { this.start = start instanceof HighPrecisionTime ? start : HighPrecisionTime.fromTime(start); this.end = end instanceof HighPrecisionTime ? end : HighPrecisionTime.fromTime(end); assertTrue( this.start.lte(this.end), `TimeSpan start [${this.start}] cannot be greater than end [${this.end}]`, ); } static fromTime(start: time, end: time): HighPrecisionTimeSpan { return new HighPrecisionTimeSpan( HighPrecisionTime.fromTime(start), HighPrecisionTime.fromTime(end), ); } get duration(): HighPrecisionTime { return this.end.sub(this.start); } get midpoint(): HighPrecisionTime { return this.start.add(this.end).divide(2); } equals(other: Span): boolean { return this.start.eq(other.start) && this.end.eq(other.end); } contains(x: HighPrecisionTime | Span): boolean { if (x instanceof HighPrecisionTime) { return this.start.lte(x) && x.lt(this.end); } else { return this.start.lte(x.start) && x.end.lte(this.end); } } intersectsInterval(x: Span): boolean { return !(x.end.lte(this.start) || x.start.gte(this.end)); } intersects(start: HighPrecisionTime, end: HighPrecisionTime): boolean { return !(end.lte(this.start) || start.gte(this.end)); } add(time: HighPrecisionTime): Span { return new HighPrecisionTimeSpan(this.start.add(time), this.end.add(time)); } // Move the start and end away from each other a certain amount pad(time: HighPrecisionTime): Span { return new HighPrecisionTimeSpan(this.start.sub(time), this.end.add(time)); } }