xref: /aosp_15_r20/development/tools/winscope/src/app/loaded_parsers.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
1/*
2 * Copyright (C) 2023 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} from 'common/assert_utils';
18import {FileUtils} from 'common/file_utils';
19import {OnProgressUpdateType} from 'common/function_utils';
20import {INVALID_TIME_NS, TimeRange, Timestamp} from 'common/time';
21import {TIME_UNIT_TO_NANO} from 'common/time_units';
22import {UserNotifier} from 'common/user_notifier';
23import {TraceHasOldData, TraceOverridden} from 'messaging/user_warnings';
24import {FileAndParser} from 'parsers/file_and_parser';
25import {FileAndParsers} from 'parsers/file_and_parsers';
26import {Parser} from 'trace/parser';
27import {TraceFile} from 'trace/trace_file';
28import {TRACE_INFO} from 'trace/trace_info';
29import {TraceEntryTypeMap, TraceType} from 'trace/trace_type';
30
31export class LoadedParsers {
32  static readonly MAX_ALLOWED_TIME_GAP_BETWEEN_TRACES_NS = BigInt(
33    5 * TIME_UNIT_TO_NANO.m,
34  ); // 5m
35  static readonly MAX_ALLOWED_TIME_GAP_BETWEEN_RTE_OFFSET = BigInt(
36    5 * TIME_UNIT_TO_NANO.s,
37  ); // 5s
38  static readonly REAL_TIME_TRACES_WITHOUT_RTE_OFFSET = [
39    TraceType.CUJS,
40    TraceType.EVENT_LOG,
41  ];
42
43  private legacyParsers = new Array<FileAndParser>();
44  private perfettoParsers = new Array<FileAndParser>();
45  private legacyParsersKeptForDownload = new Array<FileAndParser>();
46  private perfettoParsersKeptForDownload = new Array<FileAndParser>();
47
48  addParsers(
49    legacyParsers: FileAndParser[],
50    perfettoParsers: FileAndParsers | undefined,
51  ) {
52    if (perfettoParsers) {
53      this.addPerfettoParsers(perfettoParsers);
54    }
55    // Traces were simultaneously upgraded to contain real-to-boottime or real-to-monotonic offsets.
56    // If we have a mix of parsers with and without offsets, the ones without must be dangling
57    // trace files with old data, and should be filtered out.
58    legacyParsers = this.filterOutParsersWithoutOffsetsIfRequired(
59      legacyParsers,
60      perfettoParsers,
61    );
62    legacyParsers = this.filterOutLegacyParsersWithOldData(legacyParsers);
63    legacyParsers = this.filterScreenshotParsersIfRequired(legacyParsers);
64
65    this.addLegacyParsers(legacyParsers);
66  }
67
68  getParsers(): Array<Parser<object>> {
69    const fileAndParsers = [
70      ...this.legacyParsers.values(),
71      ...this.perfettoParsers.values(),
72    ];
73    return fileAndParsers.map((fileAndParser) => fileAndParser.parser);
74  }
75
76  remove<T extends TraceType>(
77    parser: Parser<TraceEntryTypeMap[T]>,
78    keepForDownload = false,
79  ) {
80    const predicate = (
81      fileAndParser: FileAndParser,
82      parsersToKeep: FileAndParser[],
83    ) => {
84      const shouldRemove = fileAndParser.parser === parser;
85      if (shouldRemove && keepForDownload) {
86        parsersToKeep.push(fileAndParser);
87      }
88      return !shouldRemove;
89    };
90    this.legacyParsers = this.legacyParsers.filter(
91      (fileAndParser: FileAndParser) =>
92        predicate(fileAndParser, this.legacyParsersKeptForDownload),
93    );
94    this.perfettoParsers = this.perfettoParsers.filter(
95      (fileAndParser: FileAndParser) =>
96        predicate(fileAndParser, this.perfettoParsersKeptForDownload),
97    );
98  }
99
100  clear() {
101    this.legacyParsers = [];
102    this.perfettoParsers = [];
103  }
104
105  async makeZipArchive(onProgressUpdate?: OnProgressUpdateType): Promise<Blob> {
106    const outputFilesSoFar = new Set<File>();
107    const outputFilenameToFiles = new Map<string, File[]>();
108
109    if (onProgressUpdate) onProgressUpdate(0);
110    const totalParsers =
111      this.perfettoParsers.length +
112      this.perfettoParsersKeptForDownload.length +
113      this.legacyParsers.length +
114      this.legacyParsersKeptForDownload.length;
115    let progress = 0;
116
117    const tryPushOutputFile = (file: File, filename: string) => {
118      // Remove duplicates because some parsers (e.g. view capture) could share the same file
119      if (outputFilesSoFar.has(file)) {
120        return;
121      }
122      outputFilesSoFar.add(file);
123
124      if (outputFilenameToFiles.get(filename) === undefined) {
125        outputFilenameToFiles.set(filename, []);
126      }
127      assertDefined(outputFilenameToFiles.get(filename)).push(file);
128    };
129
130    const makeArchiveFile = (
131      filename: string,
132      file: File,
133      clashCount: number,
134    ): File => {
135      if (clashCount === 0) {
136        return new File([file], filename);
137      }
138
139      const filenameWithoutExt =
140        FileUtils.removeExtensionFromFilename(filename);
141      const extension = FileUtils.getFileExtension(filename);
142
143      if (extension === undefined) {
144        return new File([file], `${filename} (${clashCount})`);
145      }
146
147      return new File(
148        [file],
149        `${filenameWithoutExt} (${clashCount}).${extension}`,
150      );
151    };
152
153    const tryPushOutPerfettoFile = (parsers: FileAndParser[]) => {
154      const file: TraceFile = parsers.values().next().value.file;
155      let outputFilename = FileUtils.removeDirFromFileName(file.file.name);
156      if (FileUtils.getFileExtension(file.file.name) === undefined) {
157        outputFilename += '.perfetto-trace';
158      }
159      tryPushOutputFile(file.file, outputFilename);
160    };
161
162    if (this.perfettoParsers.length > 0) {
163      tryPushOutPerfettoFile(this.perfettoParsers);
164    } else if (this.perfettoParsersKeptForDownload.length > 0) {
165      tryPushOutPerfettoFile(this.perfettoParsersKeptForDownload);
166    }
167    if (onProgressUpdate) {
168      progress =
169        this.perfettoParsers.length +
170        this.perfettoParsersKeptForDownload.length;
171      onProgressUpdate((0.5 * progress) / totalParsers);
172    }
173
174    const tryPushOutputLegacyFile = (fileAndParser: FileAndParser) => {
175      const {file, parser} = fileAndParser;
176      const traceType = parser.getTraceType();
177      const archiveDir =
178        TRACE_INFO[traceType].downloadArchiveDir.length > 0
179          ? TRACE_INFO[traceType].downloadArchiveDir + '/'
180          : '';
181      let outputFilename =
182        archiveDir + FileUtils.removeDirFromFileName(file.file.name);
183      if (FileUtils.getFileExtension(file.file.name) === undefined) {
184        outputFilename += TRACE_INFO[traceType].legacyExt;
185      }
186      tryPushOutputFile(file.file, outputFilename);
187      if (onProgressUpdate) {
188        progress++;
189        onProgressUpdate((0.5 * progress) / totalParsers);
190      }
191    };
192
193    this.legacyParsers.forEach(tryPushOutputLegacyFile);
194    this.legacyParsersKeptForDownload.forEach(tryPushOutputLegacyFile);
195
196    const archiveFiles = [...outputFilenameToFiles.entries()]
197      .map(([filename, files]) => {
198        return files.map((file, clashCount) =>
199          makeArchiveFile(filename, file, clashCount),
200        );
201      })
202      .flat();
203
204    return await FileUtils.createZipArchive(
205      archiveFiles,
206      onProgressUpdate
207        ? (perc: number) => onProgressUpdate(0.5 * (1 + perc))
208        : undefined,
209    );
210  }
211
212  getLatestRealToMonotonicOffset(
213    parsers: Array<Parser<object>>,
214  ): bigint | undefined {
215    const p = parsers
216      .filter((offset) => offset.getRealToMonotonicTimeOffsetNs() !== undefined)
217      .sort((a, b) => {
218        return Number(
219          (a.getRealToMonotonicTimeOffsetNs() ?? 0n) -
220            (b.getRealToMonotonicTimeOffsetNs() ?? 0n),
221        );
222      })
223      .at(-1);
224    return p?.getRealToMonotonicTimeOffsetNs();
225  }
226
227  getLatestRealToBootTimeOffset(
228    parsers: Array<Parser<object>>,
229  ): bigint | undefined {
230    const p = parsers
231      .filter((offset) => offset.getRealToBootTimeOffsetNs() !== undefined)
232      .sort((a, b) => {
233        return Number(
234          (a.getRealToBootTimeOffsetNs() ?? 0n) -
235            (b.getRealToBootTimeOffsetNs() ?? 0n),
236        );
237      })
238      .at(-1);
239    return p?.getRealToBootTimeOffsetNs();
240  }
241
242  private addLegacyParsers(parsers: FileAndParser[]) {
243    const legacyParsersBeingLoaded = new Map<TraceType, Parser<object>>();
244
245    parsers.forEach((fileAndParser) => {
246      const {parser} = fileAndParser;
247      if (this.shouldUseLegacyParser(parser)) {
248        legacyParsersBeingLoaded.set(parser.getTraceType(), parser);
249        this.legacyParsers.push(fileAndParser);
250      }
251    });
252  }
253
254  private addPerfettoParsers({file, parsers}: FileAndParsers) {
255    // We currently run only one Perfetto TP WebWorker at a time, so Perfetto parsers previously
256    // loaded are now invalid and must be removed (previous WebWorker is not running anymore).
257    this.perfettoParsers = [];
258
259    parsers.forEach((parser) => {
260      this.perfettoParsers.push(new FileAndParser(file, parser));
261
262      // While transitioning to the Perfetto format, devices might still have old legacy trace files
263      // dangling in the disk that get automatically included into bugreports. Hence, Perfetto
264      // parsers must always override legacy ones so that dangling legacy files are ignored.
265      this.legacyParsers = this.legacyParsers.filter((fileAndParser) => {
266        const isOverriddenByPerfettoParser =
267          fileAndParser.parser.getTraceType() === parser.getTraceType();
268        if (isOverriddenByPerfettoParser) {
269          UserNotifier.add(
270            new TraceOverridden(fileAndParser.parser.getDescriptors().join()),
271          );
272        }
273        return !isOverriddenByPerfettoParser;
274      });
275    });
276  }
277
278  private shouldUseLegacyParser(newParser: Parser<object>): boolean {
279    // While transitioning to the Perfetto format, devices might still have old legacy trace files
280    // dangling in the disk that get automatically included into bugreports. Hence, Perfetto parsers
281    // must always override legacy ones so that dangling legacy files are ignored.
282    const isOverriddenByPerfettoParser = this.perfettoParsers.some(
283      (fileAndParser) =>
284        fileAndParser.parser.getTraceType() === newParser.getTraceType(),
285    );
286    if (isOverriddenByPerfettoParser) {
287      UserNotifier.add(new TraceOverridden(newParser.getDescriptors().join()));
288      return false;
289    }
290
291    return true;
292  }
293
294  private filterOutLegacyParsersWithOldData(
295    newLegacyParsers: FileAndParser[],
296  ): FileAndParser[] {
297    let allParsers = [
298      ...newLegacyParsers,
299      ...this.legacyParsers.values(),
300      ...this.perfettoParsers.values(),
301    ];
302
303    const latestMonotonicOffset = this.getLatestRealToMonotonicOffset(
304      allParsers.map(({parser, file}) => parser),
305    );
306    const latestBootTimeOffset = this.getLatestRealToBootTimeOffset(
307      allParsers.map(({parser, file}) => parser),
308    );
309
310    newLegacyParsers = newLegacyParsers.filter(({parser, file}) => {
311      const monotonicOffset = parser.getRealToMonotonicTimeOffsetNs();
312      if (monotonicOffset && latestMonotonicOffset) {
313        const isOldData =
314          Math.abs(Number(monotonicOffset - latestMonotonicOffset)) >
315          LoadedParsers.MAX_ALLOWED_TIME_GAP_BETWEEN_RTE_OFFSET;
316        if (isOldData) {
317          UserNotifier.add(new TraceHasOldData(file.getDescriptor()));
318          return false;
319        }
320      }
321
322      const bootTimeOffset = parser.getRealToBootTimeOffsetNs();
323      if (bootTimeOffset && latestBootTimeOffset) {
324        const isOldData =
325          Math.abs(Number(bootTimeOffset - latestBootTimeOffset)) >
326          LoadedParsers.MAX_ALLOWED_TIME_GAP_BETWEEN_RTE_OFFSET;
327        if (isOldData) {
328          UserNotifier.add(new TraceHasOldData(file.getDescriptor()));
329          return false;
330        }
331      }
332
333      return true;
334    });
335
336    allParsers = [
337      ...newLegacyParsers,
338      ...this.legacyParsers.values(),
339      ...this.perfettoParsers.values(),
340    ];
341
342    const timeRanges = allParsers
343      .map(({parser}) => {
344        const timestamps = parser.getTimestamps();
345        if (!timestamps || timestamps.length === 0) {
346          return undefined;
347        }
348        return new TimeRange(timestamps[0], timestamps[timestamps.length - 1]);
349      })
350      .filter((range) => range !== undefined) as TimeRange[];
351
352    const timeGap = this.findLastTimeGapAboveThreshold(timeRanges);
353    if (!timeGap) {
354      return newLegacyParsers;
355    }
356
357    return newLegacyParsers.filter(({parser, file}) => {
358      // Only Shell Transition data used to set timestamps of merged Transition trace,
359      // so WM Transition data should not be considered by "old data" policy
360      if (parser.getTraceType() === TraceType.WM_TRANSITION) {
361        return true;
362      }
363
364      let timestamps = parser.getTimestamps();
365      if (!this.hasValidTimestamps(timestamps)) {
366        return true;
367      }
368      timestamps = assertDefined(timestamps);
369
370      const endTimestamp = timestamps[timestamps.length - 1];
371      const isOldData = endTimestamp.getValueNs() <= timeGap.from.getValueNs();
372      if (isOldData) {
373        UserNotifier.add(new TraceHasOldData(file.getDescriptor(), timeGap));
374        return false;
375      }
376
377      return true;
378    });
379  }
380
381  private filterScreenshotParsersIfRequired(
382    newLegacyParsers: FileAndParser[],
383  ): FileAndParser[] {
384    const hasOldScreenRecordingParsers = this.legacyParsers.some(
385      (entry) => entry.parser.getTraceType() === TraceType.SCREEN_RECORDING,
386    );
387    const hasNewScreenRecordingParsers = newLegacyParsers.some(
388      (entry) => entry.parser.getTraceType() === TraceType.SCREEN_RECORDING,
389    );
390    const hasScreenRecordingParsers =
391      hasOldScreenRecordingParsers || hasNewScreenRecordingParsers;
392
393    if (!hasScreenRecordingParsers) {
394      return newLegacyParsers;
395    }
396
397    const oldScreenshotParsers = this.legacyParsers.filter(
398      (fileAndParser) =>
399        fileAndParser.parser.getTraceType() === TraceType.SCREENSHOT,
400    );
401    const newScreenshotParsers = newLegacyParsers.filter(
402      (fileAndParser) =>
403        fileAndParser.parser.getTraceType() === TraceType.SCREENSHOT,
404    );
405
406    oldScreenshotParsers.forEach((fileAndParser) => {
407      UserNotifier.add(
408        new TraceOverridden(
409          fileAndParser.parser.getDescriptors().join(),
410          TraceType.SCREEN_RECORDING,
411        ),
412      );
413      this.remove(fileAndParser.parser);
414    });
415
416    newScreenshotParsers.forEach((newScreenshotParser) => {
417      UserNotifier.add(
418        new TraceOverridden(
419          newScreenshotParser.parser.getDescriptors().join(),
420          TraceType.SCREEN_RECORDING,
421        ),
422      );
423    });
424
425    return newLegacyParsers.filter(
426      (fileAndParser) =>
427        fileAndParser.parser.getTraceType() !== TraceType.SCREENSHOT,
428    );
429  }
430
431  private filterOutParsersWithoutOffsetsIfRequired(
432    newLegacyParsers: FileAndParser[],
433    perfettoParsers: FileAndParsers | undefined,
434  ): FileAndParser[] {
435    const hasParserWithOffset =
436      perfettoParsers ||
437      newLegacyParsers.find(({parser, file}) => {
438        return (
439          parser.getRealToBootTimeOffsetNs() !== undefined ||
440          parser.getRealToMonotonicTimeOffsetNs() !== undefined
441        );
442      });
443    const hasParserWithoutOffset = newLegacyParsers.find(({parser, file}) => {
444      const timestamps = parser.getTimestamps();
445      return (
446        this.hasValidTimestamps(timestamps) &&
447        parser.getRealToBootTimeOffsetNs() === undefined &&
448        parser.getRealToMonotonicTimeOffsetNs() === undefined
449      );
450    });
451
452    if (hasParserWithOffset && hasParserWithoutOffset) {
453      return newLegacyParsers.filter(({parser, file}) => {
454        if (
455          LoadedParsers.REAL_TIME_TRACES_WITHOUT_RTE_OFFSET.some(
456            (traceType) => parser.getTraceType() === traceType,
457          )
458        ) {
459          return true;
460        }
461        const hasOffset =
462          parser.getRealToMonotonicTimeOffsetNs() !== undefined ||
463          parser.getRealToBootTimeOffsetNs() !== undefined;
464        if (!hasOffset) {
465          UserNotifier.add(new TraceHasOldData(parser.getDescriptors().join()));
466        }
467        return hasOffset;
468      });
469    }
470
471    return newLegacyParsers;
472  }
473
474  private findLastTimeGapAboveThreshold(
475    ranges: readonly TimeRange[],
476  ): TimeRange | undefined {
477    const rangesSortedByEnd = ranges
478      .slice()
479      .sort((a, b) => (a.to.getValueNs() < b.to.getValueNs() ? -1 : +1));
480
481    for (let i = rangesSortedByEnd.length - 2; i >= 0; --i) {
482      const curr = rangesSortedByEnd[i];
483      const next = rangesSortedByEnd[i + 1];
484      const gap = next.from.getValueNs() - curr.to.getValueNs();
485      if (gap > LoadedParsers.MAX_ALLOWED_TIME_GAP_BETWEEN_TRACES_NS) {
486        return new TimeRange(curr.to, next.from);
487      }
488    }
489
490    return undefined;
491  }
492
493  private hasValidTimestamps(timestamps: Timestamp[] | undefined): boolean {
494    if (!timestamps || timestamps.length === 0) {
495      return false;
496    }
497
498    const isDump =
499      timestamps.length === 1 && timestamps[0].getValueNs() === INVALID_TIME_NS;
500    if (isDump) {
501      return false;
502    }
503    return true;
504  }
505}
506