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