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