blob: d913e6425ff4c25021b6a9993b48fdcd1ef80e6a [file] [log] [blame]
// 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, TPTime} from './time';
export type RoundMode = 'round'|'floor'|'ceil';
export type Timeish = HighPrecisionTime|TPTime;
// Stores a time as a bigint and an offset which is capable of:
// - Storing and reproducing "TPTime"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 fromTPTime(timestamp: TPTime): 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;
}
toTPTime(roundMode: RoundMode = 'floor'): TPTime {
switch (roundMode) {
case 'round':
return this.base + BigInt(Math.round(this.offset));
case 'floor':
return this.base;
case 'ceil':
return 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);
}
addTPTime(ts: TPTime): HighPrecisionTime {
return new HighPrecisionTime(this.base + ts, this.offset);
}
sub(other: HighPrecisionTime): HighPrecisionTime {
return new HighPrecisionTime(
this.base - other.base, this.offset - other.offset);
}
subTPTime(ts: TPTime): HighPrecisionTime {
return this.addTPTime(-ts);
}
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.fromHPTimeOrTPTime(other);
return Math.abs(this.sub(x).nanos) < epsilon;
}
private static fromHPTimeOrTPTime(x: HighPrecisionTime|
TPTime): HighPrecisionTime {
if (x instanceof HighPrecisionTime) {
return x;
} else if (typeof x === 'bigint') {
return HighPrecisionTime.fromTPTime(x);
} else {
const y: never = x;
throw new Error(`Invalid type ${y}`);
}
}
lt(other: Timeish): boolean {
const x = HighPrecisionTime.fromHPTimeOrTPTime(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: TPTime|HighPrecisionTime, end: TPTime|HighPrecisionTime) {
this.start = (start instanceof HighPrecisionTime) ?
start :
HighPrecisionTime.fromTPTime(start);
this.end = (end instanceof HighPrecisionTime) ?
end :
HighPrecisionTime.fromTPTime(end);
assertTrue(
this.start.lte(this.end),
`TimeSpan start [${this.start}] cannot be greater than end [${
this.end}]`);
}
static fromTpTime(start: TPTime, end: TPTime): HighPrecisionTimeSpan {
return new HighPrecisionTimeSpan(
HighPrecisionTime.fromTPTime(start),
HighPrecisionTime.fromTPTime(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);
}
}
intersectsSpan(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),
);
}
}