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