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