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