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