• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2023 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 {assertTrue} from '../base/logging';
16import {Span, Time, time} from '../base/time';
17
18export type RoundMode = 'round' | 'floor' | 'ceil';
19export type Timeish = HighPrecisionTime | time;
20
21// Stores a time as a bigint and an offset which is capable of:
22// - Storing and reproducing "Time"s without losing precision.
23// - Storing time with sub-nanosecond precision.
24// This class is immutable - each operation returns a new object.
25export class HighPrecisionTime {
26  // Time in nanoseconds == base + offset
27  // offset is kept in the range 0 <= x < 1 to avoid losing precision
28  readonly base: bigint;
29  readonly offset: number;
30
31  static get ZERO(): HighPrecisionTime {
32    return new HighPrecisionTime(0n);
33  }
34
35  constructor(base: bigint = 0n, offset: number = 0) {
36    // Normalize offset to sit in the range 0.0 <= x < 1.0
37    const offsetFloor = Math.floor(offset);
38    this.base = base + BigInt(offsetFloor);
39    this.offset = offset - offsetFloor;
40  }
41
42  static fromTime(timestamp: time): HighPrecisionTime {
43    return new HighPrecisionTime(timestamp, 0);
44  }
45
46  static fromNanos(nanos: number | bigint) {
47    if (typeof nanos === 'number') {
48      return new HighPrecisionTime(0n, nanos);
49    } else if (typeof nanos === 'bigint') {
50      return new HighPrecisionTime(nanos);
51    } else {
52      const value: never = nanos;
53      throw new Error(`Value ${value} is neither a number nor a bigint`);
54    }
55  }
56
57  static fromSeconds(seconds: number) {
58    const nanos = seconds * 1e9;
59    const offset = nanos - Math.floor(nanos);
60    return new HighPrecisionTime(BigInt(Math.floor(nanos)), offset);
61  }
62
63  static max(a: HighPrecisionTime, b: HighPrecisionTime): HighPrecisionTime {
64    return a.gt(b) ? a : b;
65  }
66
67  static min(a: HighPrecisionTime, b: HighPrecisionTime): HighPrecisionTime {
68    return a.lt(b) ? a : b;
69  }
70
71  toTime(roundMode: RoundMode = 'floor'): time {
72    switch (roundMode) {
73      case 'round':
74        return Time.fromRaw(this.base + BigInt(Math.round(this.offset)));
75      case 'floor':
76        return Time.fromRaw(this.base);
77      case 'ceil':
78        return Time.fromRaw(this.base + BigInt(Math.ceil(this.offset)));
79      default:
80        const exhaustiveCheck: never = roundMode;
81        throw new Error(`Unhandled roundMode case: ${exhaustiveCheck}`);
82    }
83  }
84
85  get nanos(): number {
86    // WARNING: Number(bigint) can be surprisingly slow.
87    // WARNING: Precision may be lost here.
88    return Number(this.base) + this.offset;
89  }
90
91  get seconds(): number {
92    // WARNING: Number(bigint) can be surprisingly slow.
93    // WARNING: Precision may be lost here.
94    return (Number(this.base) + this.offset) / 1e9;
95  }
96
97  add(other: HighPrecisionTime): HighPrecisionTime {
98    return new HighPrecisionTime(
99      this.base + other.base,
100      this.offset + other.offset,
101    );
102  }
103
104  addNanos(nanos: number | bigint): HighPrecisionTime {
105    return this.add(HighPrecisionTime.fromNanos(nanos));
106  }
107
108  addSeconds(seconds: number): HighPrecisionTime {
109    return new HighPrecisionTime(this.base, this.offset + seconds * 1e9);
110  }
111
112  addTime(ts: time): HighPrecisionTime {
113    return new HighPrecisionTime(this.base + ts, this.offset);
114  }
115
116  sub(other: HighPrecisionTime): HighPrecisionTime {
117    return new HighPrecisionTime(
118      this.base - other.base,
119      this.offset - other.offset,
120    );
121  }
122
123  subTime(ts: time): HighPrecisionTime {
124    return new HighPrecisionTime(this.base - ts, this.offset);
125  }
126
127  subNanos(nanos: number | bigint): HighPrecisionTime {
128    return this.add(HighPrecisionTime.fromNanos(-nanos));
129  }
130
131  divide(divisor: number): HighPrecisionTime {
132    return this.multiply(1 / divisor);
133  }
134
135  multiply(factor: number): HighPrecisionTime {
136    const factorFloor = Math.floor(factor);
137    const newBase = this.base * BigInt(factorFloor);
138    const additionalBit = Number(this.base) * (factor - factorFloor);
139    const newOffset = factor * this.offset + additionalBit;
140    return new HighPrecisionTime(newBase, newOffset);
141  }
142
143  // Return true if other time is within some epsilon, default 1 femtosecond
144  eq(other: Timeish, epsilon: number = 1e-6): boolean {
145    const x = HighPrecisionTime.fromHPTimeOrTime(other);
146    return Math.abs(this.sub(x).nanos) < epsilon;
147  }
148
149  private static fromHPTimeOrTime(
150    x: HighPrecisionTime | time,
151  ): HighPrecisionTime {
152    if (x instanceof HighPrecisionTime) {
153      return x;
154    } else if (typeof x === 'bigint') {
155      return HighPrecisionTime.fromTime(x);
156    } else {
157      const y: never = x;
158      throw new Error(`Invalid type ${y}`);
159    }
160  }
161
162  lt(other: Timeish): boolean {
163    const x = HighPrecisionTime.fromHPTimeOrTime(other);
164    if (this.base < x.base) {
165      return true;
166    } else if (this.base === x.base) {
167      return this.offset < x.offset;
168    } else {
169      return false;
170    }
171  }
172
173  lte(other: Timeish): boolean {
174    if (this.eq(other)) {
175      return true;
176    } else {
177      return this.lt(other);
178    }
179  }
180
181  gt(other: Timeish): boolean {
182    return !this.lte(other);
183  }
184
185  gte(other: Timeish): boolean {
186    return !this.lt(other);
187  }
188
189  clamp(lower: HighPrecisionTime, upper: HighPrecisionTime): HighPrecisionTime {
190    if (this.lt(lower)) {
191      return lower;
192    } else if (this.gt(upper)) {
193      return upper;
194    } else {
195      return this;
196    }
197  }
198
199  toString(): string {
200    const offsetAsString = this.offset.toString();
201    if (offsetAsString === '0') {
202      return this.base.toString();
203    } else {
204      return `${this.base}${offsetAsString.substring(1)}`;
205    }
206  }
207
208  abs(): HighPrecisionTime {
209    if (this.base >= 0n) {
210      return this;
211    }
212    const newBase = -this.base;
213    const newOffset = -this.offset;
214    return new HighPrecisionTime(newBase, newOffset);
215  }
216}
217
218export class HighPrecisionTimeSpan implements Span<HighPrecisionTime> {
219  readonly start: HighPrecisionTime;
220  readonly end: HighPrecisionTime;
221
222  static readonly ZERO = new HighPrecisionTimeSpan(
223    HighPrecisionTime.ZERO,
224    HighPrecisionTime.ZERO,
225  );
226
227  constructor(start: time | HighPrecisionTime, end: time | HighPrecisionTime) {
228    this.start =
229      start instanceof HighPrecisionTime
230        ? start
231        : HighPrecisionTime.fromTime(start);
232    this.end =
233      end instanceof HighPrecisionTime ? end : HighPrecisionTime.fromTime(end);
234    assertTrue(
235      this.start.lte(this.end),
236      `TimeSpan start [${this.start}] cannot be greater than end [${this.end}]`,
237    );
238  }
239
240  static fromTime(start: time, end: time): HighPrecisionTimeSpan {
241    return new HighPrecisionTimeSpan(
242      HighPrecisionTime.fromTime(start),
243      HighPrecisionTime.fromTime(end),
244    );
245  }
246
247  get duration(): HighPrecisionTime {
248    return this.end.sub(this.start);
249  }
250
251  get midpoint(): HighPrecisionTime {
252    return this.start.add(this.end).divide(2);
253  }
254
255  equals(other: Span<HighPrecisionTime>): boolean {
256    return this.start.eq(other.start) && this.end.eq(other.end);
257  }
258
259  contains(x: HighPrecisionTime | Span<HighPrecisionTime>): boolean {
260    if (x instanceof HighPrecisionTime) {
261      return this.start.lte(x) && x.lt(this.end);
262    } else {
263      return this.start.lte(x.start) && x.end.lte(this.end);
264    }
265  }
266
267  intersectsInterval(x: Span<HighPrecisionTime>): boolean {
268    return !(x.end.lte(this.start) || x.start.gte(this.end));
269  }
270
271  intersects(start: HighPrecisionTime, end: HighPrecisionTime): boolean {
272    return !(end.lte(this.start) || start.gte(this.end));
273  }
274
275  add(time: HighPrecisionTime): Span<HighPrecisionTime> {
276    return new HighPrecisionTimeSpan(this.start.add(time), this.end.add(time));
277  }
278
279  // Move the start and end away from each other a certain amount
280  pad(time: HighPrecisionTime): Span<HighPrecisionTime> {
281    return new HighPrecisionTimeSpan(this.start.sub(time), this.end.add(time));
282  }
283}
284