1/* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import {assertDefined, assertTrue} from './assert_utils'; 18import {BigintMath} from './bigint_math'; 19import { 20 INVALID_TIME_NS, 21 Timestamp, 22 TimestampFormatter, 23 TimestampFormatType, 24 TimezoneInfo, 25} from './time'; 26import {TimestampUtils} from './timestamp_utils'; 27import {TIME_UNITS, TIME_UNIT_TO_NANO} from './time_units'; 28import {UTCOffset} from './utc_offset'; 29 30// Pre-T traces do not provide real-to-boottime or real-to-monotonic offsets,so 31// we group their timestamps under the "ELAPSED" umbrella term, and hope that 32// the CPU was not suspended before the tracing session, causing them to diverge. 33enum TimestampType { 34 ELAPSED, 35 REAL, 36} 37 38class RealTimestampFormatter implements TimestampFormatter { 39 constructor(private utcOffset: UTCOffset) {} 40 41 setUTCOffset(value: UTCOffset) { 42 this.utcOffset = value; 43 } 44 45 format(timestamp: Timestamp, type: TimestampFormatType): string { 46 const timestampNanos = 47 timestamp.getValueNs() + (this.utcOffset.getValueNs() ?? 0n); 48 const ms = BigintMath.divideAndRound( 49 timestampNanos, 50 BigInt(TIME_UNIT_TO_NANO.ms), 51 ); 52 const formattedTimestamp = new Date(Number(ms)) 53 .toISOString() 54 .replace('Z', '') 55 .replace('T', ', '); 56 if (type === TimestampFormatType.DROP_DATE) { 57 return assertDefined( 58 TimestampUtils.extractTimeFromHumanTimestamp(formattedTimestamp), 59 ); 60 } 61 return formattedTimestamp; 62 } 63} 64const REAL_TIMESTAMP_FORMATTER_UTC = new RealTimestampFormatter( 65 new UTCOffset(), 66); 67 68class ElapsedTimestampFormatter { 69 format(timestamp: Timestamp): string { 70 let leftNanos = timestamp.getValueNs(); 71 const parts: Array<{value: bigint; unit: string}> = TIME_UNITS.slice() 72 .reverse() 73 .map(({nanosInUnit, unit}) => { 74 let amountOfUnit = BigInt(0); 75 if (leftNanos >= nanosInUnit) { 76 amountOfUnit = leftNanos / BigInt(nanosInUnit); 77 } 78 leftNanos = leftNanos % BigInt(nanosInUnit); 79 return {value: amountOfUnit, unit}; 80 }); 81 82 // Remove all 0ed units at start 83 while (parts.length > 1 && parts[0].value === 0n) { 84 parts.shift(); 85 } 86 87 return parts.map((part) => `${part.value}${part.unit}`).join(''); 88 } 89} 90const ELAPSED_TIMESTAMP_FORMATTER = new ElapsedTimestampFormatter(); 91 92export interface ParserTimestampConverter { 93 makeTimestampFromRealNs(valueNs: bigint): Timestamp; 94 makeTimestampFromMonotonicNs(valueNs: bigint): Timestamp; 95 makeTimestampFromBootTimeNs(valueNs: bigint): Timestamp; 96 makeZeroTimestamp(): Timestamp; 97} 98 99export interface ComponentTimestampConverter { 100 makeTimestampFromHuman(timestampHuman: string): Timestamp; 101 getUTCOffset(): string; 102 makeTimestampFromNs(valueNs: bigint): Timestamp; 103 validateHumanInput(timestampHuman: string): boolean; 104} 105 106export interface RemoteToolTimestampConverter { 107 makeTimestampFromBootTimeNs(valueNs: bigint): Timestamp; 108 makeTimestampFromRealNs(valueNs: bigint): Timestamp; 109 tryGetBootTimeNs(timestamp: Timestamp): bigint | undefined; 110 tryGetRealTimeNs(timestamp: Timestamp): bigint | undefined; 111} 112 113export class TimestampConverter 114 implements 115 ParserTimestampConverter, 116 ComponentTimestampConverter, 117 RemoteToolTimestampConverter 118{ 119 private readonly utcOffset = new UTCOffset(); 120 private readonly realTimestampFormatter = new RealTimestampFormatter( 121 this.utcOffset, 122 ); 123 private createdTimestampType: TimestampType | undefined; 124 125 constructor( 126 private timezoneInfo: TimezoneInfo, 127 private realToMonotonicTimeOffsetNs?: bigint, 128 private realToBootTimeOffsetNs?: bigint, 129 ) {} 130 131 initializeUTCOffset(timestamp: Timestamp) { 132 if ( 133 this.utcOffset.getValueNs() !== undefined || 134 !this.canMakeRealTimestamps() 135 ) { 136 return; 137 } 138 const utcValueNs = timestamp.getValueNs(); 139 const localNs = 140 this.timezoneInfo.timezone !== 'UTC' 141 ? this.addTimezoneOffset(this.timezoneInfo.timezone, utcValueNs) 142 : utcValueNs; 143 const utcOffsetNs = localNs - utcValueNs; 144 this.utcOffset.initialize(utcOffsetNs); 145 } 146 147 setRealToMonotonicTimeOffsetNs(ns: bigint) { 148 if (this.realToMonotonicTimeOffsetNs !== undefined) { 149 return; 150 } 151 this.realToMonotonicTimeOffsetNs = ns; 152 } 153 154 setRealToBootTimeOffsetNs(ns: bigint) { 155 if (this.realToBootTimeOffsetNs !== undefined) { 156 return; 157 } 158 this.realToBootTimeOffsetNs = ns; 159 } 160 161 getUTCOffset(): string { 162 return this.utcOffset.format(); 163 } 164 165 makeTimestampFromMonotonicNs(valueNs: bigint): Timestamp { 166 if (this.realToMonotonicTimeOffsetNs !== undefined) { 167 return this.makeRealTimestamp(valueNs + this.realToMonotonicTimeOffsetNs); 168 } 169 return this.makeElapsedTimestamp(valueNs); 170 } 171 172 makeTimestampFromBootTimeNs(valueNs: bigint): Timestamp { 173 if (this.realToBootTimeOffsetNs !== undefined) { 174 return this.makeRealTimestamp(valueNs + this.realToBootTimeOffsetNs); 175 } 176 return this.makeElapsedTimestamp(valueNs); 177 } 178 179 makeTimestampFromRealNs(valueNs: bigint): Timestamp { 180 return this.makeRealTimestamp(valueNs); 181 } 182 183 makeTimestampFromHuman(timestampHuman: string): Timestamp { 184 if (TimestampUtils.isHumanElapsedTimeFormat(timestampHuman)) { 185 return this.makeTimestampfromHumanElapsed(timestampHuman); 186 } 187 188 if ( 189 TimestampUtils.isISOFormat(timestampHuman) || 190 TimestampUtils.isRealDateTimeFormat(timestampHuman) 191 ) { 192 return this.makeTimestampFromHumanReal(timestampHuman); 193 } 194 195 throw new Error('Invalid timestamp format'); 196 } 197 198 makeTimestampFromNs(valueNs: bigint): Timestamp { 199 return new Timestamp( 200 valueNs, 201 this.canMakeRealTimestamps() 202 ? this.realTimestampFormatter 203 : ELAPSED_TIMESTAMP_FORMATTER, 204 ); 205 } 206 207 makeZeroTimestamp(): Timestamp { 208 if (this.canMakeRealTimestamps()) { 209 return new Timestamp(INVALID_TIME_NS, REAL_TIMESTAMP_FORMATTER_UTC); 210 } else { 211 return new Timestamp(INVALID_TIME_NS, ELAPSED_TIMESTAMP_FORMATTER); 212 } 213 } 214 215 tryGetBootTimeNs(timestamp: Timestamp): bigint | undefined { 216 if ( 217 this.createdTimestampType !== TimestampType.REAL || 218 this.realToBootTimeOffsetNs === undefined 219 ) { 220 return undefined; 221 } 222 return timestamp.getValueNs() - this.realToBootTimeOffsetNs; 223 } 224 225 tryGetRealTimeNs(timestamp: Timestamp): bigint | undefined { 226 if (this.createdTimestampType !== TimestampType.REAL) { 227 return undefined; 228 } 229 return timestamp.getValueNs(); 230 } 231 232 validateHumanInput(timestampHuman: string, context = this): boolean { 233 if (context.canMakeRealTimestamps()) { 234 return TimestampUtils.isHumanRealTimestampFormat(timestampHuman); 235 } 236 return TimestampUtils.isHumanElapsedTimeFormat(timestampHuman); 237 } 238 239 clear() { 240 this.createdTimestampType = undefined; 241 this.realToBootTimeOffsetNs = undefined; 242 this.realToMonotonicTimeOffsetNs = undefined; 243 this.utcOffset.clear(); 244 } 245 246 private canMakeRealTimestamps(): boolean { 247 return this.createdTimestampType === TimestampType.REAL; 248 } 249 250 private makeRealTimestamp(valueNs: bigint): Timestamp { 251 assertTrue( 252 this.createdTimestampType === undefined || 253 this.createdTimestampType === TimestampType.REAL, 254 ); 255 this.createdTimestampType = TimestampType.REAL; 256 return new Timestamp(valueNs, this.realTimestampFormatter); 257 } 258 259 private makeElapsedTimestamp(valueNs: bigint): Timestamp { 260 assertTrue( 261 this.createdTimestampType === undefined || 262 this.createdTimestampType === TimestampType.ELAPSED, 263 ); 264 this.createdTimestampType = TimestampType.ELAPSED; 265 return new Timestamp(valueNs, ELAPSED_TIMESTAMP_FORMATTER); 266 } 267 268 private makeTimestampFromHumanReal(timestampHuman: string): Timestamp { 269 // Remove trailing Z if present 270 timestampHuman = timestampHuman.replace('Z', ''); 271 272 // Convert to ISO format if required 273 if (TimestampUtils.isRealDateTimeFormat(timestampHuman)) { 274 timestampHuman = timestampHuman.replace(', ', 'T'); 275 } 276 277 // Date.parse only considers up to millisecond precision, 278 // so only pass in YYYY-MM-DDThh:mm:ss 279 let nanos = 0n; 280 if (timestampHuman.includes('.')) { 281 const [datetime, ns] = timestampHuman.split('.'); 282 nanos += BigInt(Math.floor(Number(ns.padEnd(9, '0')))); 283 timestampHuman = datetime; 284 } 285 286 timestampHuman += this.utcOffset.format().slice(3); 287 288 return this.makeTimestampFromRealNs( 289 BigInt(Date.parse(timestampHuman)) * BigInt(TIME_UNIT_TO_NANO['ms']) + 290 BigInt(nanos), 291 ); 292 } 293 294 private makeTimestampfromHumanElapsed(timestampHuman: string): Timestamp { 295 const usedUnits = timestampHuman.split(/[0-9]+/).filter((it) => it !== ''); 296 const usedValues = timestampHuman 297 .split(/[a-z]+/) 298 .filter((it) => it !== '') 299 .map((it) => Math.floor(Number(it))); 300 301 let ns = BigInt(0); 302 303 for (let i = 0; i < usedUnits.length; i++) { 304 const unit = usedUnits[i]; 305 const value = usedValues[i]; 306 const unitData = assertDefined(TIME_UNITS.find((it) => it.unit === unit)); 307 ns += BigInt(unitData.nanosInUnit) * BigInt(value); 308 } 309 310 return this.makeElapsedTimestamp(ns); 311 } 312 313 private addTimezoneOffset(timezone: string, timestampNs: bigint): bigint { 314 const utcDate = new Date(Number(timestampNs / 1000000n)); 315 const timezoneDateFormatted = utcDate.toLocaleString('en-US', { 316 timeZone: timezone, 317 }); 318 const timezoneDate = new Date(timezoneDateFormatted); 319 320 let daysDiff = timezoneDate.getDay() - utcDate.getDay(); // day of the week 321 if (daysDiff > 1) { 322 // Saturday in timezone, Sunday in UTC 323 daysDiff = -1; 324 } else if (daysDiff < -1) { 325 // Sunday in timezone, Saturday in UTC 326 daysDiff = 1; 327 } 328 329 const hoursDiff = 330 timezoneDate.getHours() - utcDate.getHours() + daysDiff * 24; 331 const minutesDiff = timezoneDate.getMinutes() - utcDate.getMinutes(); 332 const localTimezoneOffsetMinutes = utcDate.getTimezoneOffset(); 333 334 return ( 335 timestampNs + 336 BigInt(hoursDiff * 3.6e12) + 337 BigInt(minutesDiff * 6e10) - 338 BigInt(localTimezoneOffsetMinutes * 6e10) 339 ); 340 } 341} 342 343export const UTC_TIMEZONE_INFO = { 344 timezone: 'UTC', 345 locale: 'en-US', 346}; 347