|  | // 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<HighPrecisionTime> { | 
|  | 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<HighPrecisionTime>): boolean { | 
|  | return this.start.eq(other.start) && this.end.eq(other.end); | 
|  | } | 
|  |  | 
|  | contains(x: HighPrecisionTime|Span<HighPrecisionTime>): 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<HighPrecisionTime>): 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<HighPrecisionTime> { | 
|  | 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<HighPrecisionTime> { | 
|  | return new HighPrecisionTimeSpan( | 
|  | this.start.sub(time), | 
|  | this.end.add(time), | 
|  | ); | 
|  | } | 
|  | } |