xref: /aosp_15_r20/external/perfetto/ui/src/base/high_precision_time.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2024 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 {assertUnreachable} from './logging';
16import {Time, time} from './time';
17
18export type RoundMode = 'round' | 'floor' | 'ceil';
19
20/**
21 * Represents a time value in trace processor's time units, which is capable of
22 * representing a time with at least 64 bit integer precision and 53 bits of
23 * fractional precision.
24 *
25 * This class is immutable - any methods that modify this time will return a new
26 * copy containing instead.
27 */
28export class HighPrecisionTime {
29  // This is the high precision time representing 0
30  static readonly ZERO = new HighPrecisionTime(Time.fromRaw(0n));
31
32  // time value == |integral| + |fractional|
33  // |fractional| is kept in the range 0 <= x < 1 to avoid losing precision
34  readonly integral: time;
35  readonly fractional: number;
36
37  /**
38   * Constructs a HighPrecisionTime object.
39   *
40   * @param integral The integer part of the time value.
41   * @param fractional The fractional part of the time value.
42   */
43  constructor(integral: time, fractional: number = 0) {
44    // Normalize |fractional| to the range 0.0 <= x < 1.0
45    const fractionalFloor = Math.floor(fractional);
46    this.integral = (integral + BigInt(fractionalFloor)) as time;
47    this.fractional = fractional - fractionalFloor;
48  }
49
50  /**
51   * Converts to an integer time value.
52   *
53   * @param round How to round ('round', 'floor', or 'ceil').
54   */
55  toTime(round: RoundMode = 'floor'): time {
56    switch (round) {
57      case 'round':
58        return Time.fromRaw(
59          this.integral + BigInt(Math.round(this.fractional)),
60        );
61      case 'floor':
62        return Time.fromRaw(this.integral);
63      case 'ceil':
64        return Time.fromRaw(this.integral + BigInt(Math.ceil(this.fractional)));
65      default:
66        assertUnreachable(round);
67    }
68  }
69
70  /**
71   * Converts to a JavaScript number. Precision loss should be expected when
72   * integral values are large.
73   */
74  toNumber(): number {
75    return Number(this.integral) + this.fractional;
76  }
77
78  /**
79   * Adds another HighPrecisionTime to this one and returns the result.
80   *
81   * @param time A HighPrecisionTime object to add.
82   */
83  add(time: HighPrecisionTime): HighPrecisionTime {
84    return new HighPrecisionTime(
85      Time.add(this.integral, time.integral),
86      this.fractional + time.fractional,
87    );
88  }
89
90  /**
91   * Adds an integer time value to this HighPrecisionTime and returns the result.
92   *
93   * @param t A time value to add.
94   */
95  addTime(t: time): HighPrecisionTime {
96    return new HighPrecisionTime(Time.add(this.integral, t), this.fractional);
97  }
98
99  /**
100   * Adds a floating point time value to this one and returns the result.
101   *
102   * @param n A floating point value to add.
103   */
104  addNumber(n: number): HighPrecisionTime {
105    return new HighPrecisionTime(this.integral, this.fractional + n);
106  }
107
108  /**
109   * Subtracts another HighPrecisionTime from this one and returns the result.
110   *
111   * @param time A HighPrecisionTime object to subtract.
112   */
113  sub(time: HighPrecisionTime): HighPrecisionTime {
114    return new HighPrecisionTime(
115      Time.sub(this.integral, time.integral),
116      this.fractional - time.fractional,
117    );
118  }
119
120  /**
121   * Subtract an integer time value from this HighPrecisionTime and returns the
122   * result.
123   *
124   * @param t A time value to subtract.
125   */
126  subTime(t: time): HighPrecisionTime {
127    return new HighPrecisionTime(Time.sub(this.integral, t), this.fractional);
128  }
129
130  /**
131   * Subtracts a floating point time value from this one and returns the result.
132   *
133   * @param n A floating point value to subtract.
134   */
135  subNumber(n: number): HighPrecisionTime {
136    return new HighPrecisionTime(this.integral, this.fractional - n);
137  }
138
139  /**
140   * Checks if this HighPrecisionTime is approximately equal to another, within
141   * a given epsilon.
142   *
143   * @param other A HighPrecisionTime object to compare.
144   * @param epsilon The tolerance for equality check.
145   */
146  equals(other: HighPrecisionTime, epsilon: number = 1e-6): boolean {
147    return Math.abs(this.sub(other).toNumber()) < epsilon;
148  }
149
150  /**
151   * Checks if this time value is within the range defined by [start, end).
152   *
153   * @param start The start of the time range (inclusive).
154   * @param end The end of the time range (exclusive).
155   */
156  containedWithin(start: time, end: time): boolean {
157    return this.integral >= start && this.integral < end;
158  }
159
160  /**
161   * Checks if this HighPrecisionTime is less than a given time.
162   *
163   * @param t A time value.
164   */
165  lt(t: time): boolean {
166    return this.integral < t;
167  }
168
169  /**
170   * Checks if this HighPrecisionTime is less than or equal to a given time.
171   *
172   * @param t A time value.
173   */
174  lte(t: time): boolean {
175    return (
176      this.integral < t ||
177      (this.integral === t && Math.abs(this.fractional - 0.0) < Number.EPSILON)
178    );
179  }
180
181  /**
182   * Checks if this HighPrecisionTime is greater than a given time.
183   *
184   * @param t A time value.
185   */
186  gt(t: time): boolean {
187    return (
188      this.integral > t ||
189      (this.integral === t && Math.abs(this.fractional - 0.0) > Number.EPSILON)
190    );
191  }
192
193  /**
194   * Checks if this HighPrecisionTime is greater than or equal to a given time.
195   *
196   * @param t A time value.
197   */
198  gte(t: time): boolean {
199    return this.integral >= t;
200  }
201
202  /**
203   * Clamps this HighPrecisionTime to be within the specified range.
204   *
205   * @param lower The lower bound of the range.
206   * @param upper The upper bound of the range.
207   */
208  clamp(lower: time, upper: time): HighPrecisionTime {
209    if (this.integral < lower) {
210      return new HighPrecisionTime(lower);
211    } else if (this.integral >= upper) {
212      return new HighPrecisionTime(upper);
213    } else {
214      return this;
215    }
216  }
217
218  /**
219   * Returns the absolute value of this HighPrecisionTime.
220   */
221  abs(): HighPrecisionTime {
222    if (this.integral >= 0n) {
223      return this;
224    }
225    const newIntegral = Time.fromRaw(-this.integral);
226    const newFractional = -this.fractional;
227    return new HighPrecisionTime(newIntegral, newFractional);
228  }
229
230  /**
231   * Converts this HighPrecisionTime to a string representation.
232   */
233  toString(): string {
234    const fractionalAsString = this.fractional.toString();
235    if (fractionalAsString === '0') {
236      return this.integral.toString();
237    } else {
238      return `${this.integral}${fractionalAsString.substring(1)}`;
239    }
240  }
241}
242