xref: /aosp_15_r20/development/tools/winscope/src/common/timestamp_converter.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
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