xref: /aosp_15_r20/development/tools/winscope/src/app/trace_pipeline.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
1/*
2 * Copyright (C) 2022 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 {FileUtils} from 'common/file_utils';
18import {OnProgressUpdateType} from 'common/function_utils';
19import {
20  TimestampConverter,
21  UTC_TIMEZONE_INFO,
22} from 'common/timestamp_converter';
23import {UserNotifier} from 'common/user_notifier';
24import {Analytics} from 'logging/analytics';
25import {ProgressListener} from 'messaging/progress_listener';
26import {CorruptedArchive, NoValidFiles} from 'messaging/user_warnings';
27import {FileAndParsers} from 'parsers/file_and_parsers';
28import {ParserFactory as LegacyParserFactory} from 'parsers/legacy/parser_factory';
29import {ParserFactory as PerfettoParserFactory} from 'parsers/perfetto/parser_factory';
30import {ParserSearch} from 'parsers/search/parser_search';
31import {TracesParserFactory} from 'parsers/traces/traces_parser_factory';
32import {FrameMapper} from 'trace/frame_mapper';
33import {Trace} from 'trace/trace';
34import {Traces} from 'trace/traces';
35import {TraceFile} from 'trace/trace_file';
36import {TraceEntryTypeMap, TraceType, TraceTypeUtils} from 'trace/trace_type';
37import {QueryResult} from 'trace_processor/query_result';
38import {FilesSource} from './files_source';
39import {LoadedParsers} from './loaded_parsers';
40import {TraceFileFilter} from './trace_file_filter';
41
42type UnzippedArchive = TraceFile[];
43
44export class TracePipeline {
45  private loadedParsers = new LoadedParsers();
46  private traceFileFilter = new TraceFileFilter();
47  private tracesParserFactory = new TracesParserFactory();
48  private traces = new Traces();
49  private downloadArchiveFilename?: string;
50  private timestampConverter = new TimestampConverter(UTC_TIMEZONE_INFO);
51
52  async loadFiles(
53    files: File[],
54    source: FilesSource,
55    progressListener: ProgressListener | undefined,
56  ) {
57    this.downloadArchiveFilename = this.makeDownloadArchiveFilename(
58      files,
59      source,
60    );
61
62    try {
63      const unzippedArchives = await this.unzipFiles(files, progressListener);
64
65      if (unzippedArchives.length === 0) {
66        UserNotifier.add(new NoValidFiles());
67        return;
68      }
69
70      for (const unzippedArchive of unzippedArchives) {
71        await this.loadUnzippedArchive(unzippedArchive, progressListener);
72      }
73
74      this.traces = new Traces();
75
76      this.loadedParsers.getParsers().forEach((parser) => {
77        const trace = Trace.fromParser(parser);
78        this.traces.addTrace(trace);
79        Analytics.Tracing.logTraceLoaded(parser);
80      });
81
82      const tracesParsers = await this.tracesParserFactory.createParsers(
83        this.traces,
84        this.timestampConverter,
85      );
86
87      tracesParsers.forEach((tracesParser) => {
88        const trace = Trace.fromParser(tracesParser);
89        this.traces.addTrace(trace);
90      });
91
92      const hasTransitionTrace =
93        this.traces.getTrace(TraceType.TRANSITION) !== undefined;
94      if (hasTransitionTrace) {
95        this.removeTracesAndParsersByType(TraceType.WM_TRANSITION);
96        this.removeTracesAndParsersByType(TraceType.SHELL_TRANSITION);
97      }
98
99      const hasCujTrace = this.traces.getTrace(TraceType.CUJS) !== undefined;
100      if (hasCujTrace) {
101        this.removeTracesAndParsersByType(TraceType.EVENT_LOG);
102      }
103
104      const hasMergedInputTrace =
105        this.traces.getTrace(TraceType.INPUT_EVENT_MERGED) !== undefined;
106      if (hasMergedInputTrace) {
107        this.removeTracesAndParsersByType(TraceType.INPUT_KEY_EVENT);
108        this.removeTracesAndParsersByType(TraceType.INPUT_MOTION_EVENT);
109      }
110    } finally {
111      progressListener?.onOperationFinished(true);
112    }
113  }
114
115  removeTrace<T extends TraceType>(
116    trace: Trace<TraceEntryTypeMap[T]>,
117    keepFileForDownload = false,
118  ) {
119    this.loadedParsers.remove(trace.getParser(), keepFileForDownload);
120    this.traces.deleteTrace(trace);
121  }
122
123  async makeZipArchiveWithLoadedTraceFiles(
124    onProgressUpdate?: OnProgressUpdateType,
125  ): Promise<Blob> {
126    return this.loadedParsers.makeZipArchive(onProgressUpdate);
127  }
128
129  filterTracesWithoutVisualization() {
130    const tracesWithoutVisualization = this.traces
131      .mapTrace((trace) => {
132        if (!TraceTypeUtils.isTraceTypeWithViewer(trace.type)) {
133          return trace;
134        }
135        return undefined;
136      })
137      .filter((trace) => trace !== undefined) as Array<Trace<object>>;
138    tracesWithoutVisualization.forEach((trace) =>
139      this.traces.deleteTrace(trace),
140    );
141  }
142
143  async buildTraces() {
144    for (const trace of this.traces) {
145      if (trace.lengthEntries === 0 || trace.isDumpWithoutTimestamp()) {
146        continue;
147      } else {
148        const timestamp = trace.getEntry(0).getTimestamp();
149        this.timestampConverter.initializeUTCOffset(timestamp);
150        break;
151      }
152    }
153    await new FrameMapper(this.traces).computeMapping();
154  }
155
156  getTraces(): Traces {
157    return this.traces;
158  }
159
160  getDownloadArchiveFilename(): string {
161    return this.downloadArchiveFilename ?? 'winscope';
162  }
163
164  getTimestampConverter(): TimestampConverter {
165    return this.timestampConverter;
166  }
167
168  async getScreenRecordingVideo(): Promise<undefined | Blob> {
169    const traces = this.getTraces();
170    const screenRecording =
171      traces.getTrace(TraceType.SCREEN_RECORDING) ??
172      traces.getTrace(TraceType.SCREENSHOT);
173    if (!screenRecording || screenRecording.lengthEntries === 0) {
174      return undefined;
175    }
176    return (await screenRecording.getEntry(0).getValue()).videoData;
177  }
178
179  async tryCreateSearchTrace(
180    query: string,
181  ): Promise<Trace<QueryResult> | undefined> {
182    try {
183      const parser = new ParserSearch(query, this.timestampConverter);
184      await parser.parse();
185      const trace = Trace.fromParser(parser);
186      this.traces.addTrace(trace);
187      return trace;
188    } catch (e) {
189      return undefined;
190    }
191  }
192
193  clear() {
194    this.loadedParsers.clear();
195    this.traces = new Traces();
196    this.timestampConverter.clear();
197    this.downloadArchiveFilename = undefined;
198  }
199
200  private async loadUnzippedArchive(
201    unzippedArchive: UnzippedArchive,
202    progressListener: ProgressListener | undefined,
203  ) {
204    const filterResult = await this.traceFileFilter.filter(unzippedArchive);
205    if (filterResult.timezoneInfo) {
206      this.timestampConverter = new TimestampConverter(
207        filterResult.timezoneInfo,
208      );
209    }
210
211    if (!filterResult.perfetto && filterResult.legacy.length === 0) {
212      UserNotifier.add(new NoValidFiles());
213      return;
214    }
215
216    const legacyParsers = await new LegacyParserFactory().createParsers(
217      filterResult.legacy,
218      this.timestampConverter,
219      filterResult.metadata,
220      progressListener,
221    );
222
223    let perfettoParsers: FileAndParsers | undefined;
224
225    if (filterResult.perfetto) {
226      const parsers = await new PerfettoParserFactory().createParsers(
227        filterResult.perfetto,
228        this.timestampConverter,
229        progressListener,
230      );
231      perfettoParsers = new FileAndParsers(filterResult.perfetto, parsers);
232    }
233
234    const monotonicTimeOffset =
235      this.loadedParsers.getLatestRealToMonotonicOffset(
236        legacyParsers
237          .map((fileAndParser) => fileAndParser.parser)
238          .concat(perfettoParsers?.parsers ?? []),
239      );
240
241    const realToBootTimeOffset =
242      this.loadedParsers.getLatestRealToBootTimeOffset(
243        legacyParsers
244          .map((fileAndParser) => fileAndParser.parser)
245          .concat(perfettoParsers?.parsers ?? []),
246      );
247
248    if (monotonicTimeOffset !== undefined) {
249      this.timestampConverter.setRealToMonotonicTimeOffsetNs(
250        monotonicTimeOffset,
251      );
252    }
253    if (realToBootTimeOffset !== undefined) {
254      this.timestampConverter.setRealToBootTimeOffsetNs(realToBootTimeOffset);
255    }
256
257    perfettoParsers?.parsers.forEach((p) => p.createTimestamps());
258    legacyParsers.forEach((fileAndParser) =>
259      fileAndParser.parser.createTimestamps(),
260    );
261
262    this.loadedParsers.addParsers(legacyParsers, perfettoParsers);
263  }
264
265  private makeDownloadArchiveFilename(
266    files: File[],
267    source: FilesSource,
268  ): string {
269    // set download archive file name, used to download all traces
270    let filenameWithCurrTime: string;
271    const currTime = new Date().toISOString().slice(0, -5).replace('T', '_');
272    if (!this.downloadArchiveFilename && files.length === 1) {
273      const filenameNoDir = FileUtils.removeDirFromFileName(files[0].name);
274      const filenameNoDirOrExt =
275        FileUtils.removeExtensionFromFilename(filenameNoDir);
276      filenameWithCurrTime = `${filenameNoDirOrExt}_${currTime}`;
277    } else {
278      filenameWithCurrTime = `${source}_${currTime}`;
279    }
280
281    const archiveFilenameNoIllegalChars = filenameWithCurrTime.replace(
282      FileUtils.ILLEGAL_FILENAME_CHARACTERS_REGEX,
283      '_',
284    );
285    if (FileUtils.DOWNLOAD_FILENAME_REGEX.test(archiveFilenameNoIllegalChars)) {
286      return archiveFilenameNoIllegalChars;
287    } else {
288      console.error(
289        "Cannot convert uploaded archive filename to acceptable format for download. Defaulting download filename to 'winscope.zip'.",
290      );
291      return 'winscope';
292    }
293  }
294
295  private async unzipFiles(
296    files: File[],
297    progressListener: ProgressListener | undefined,
298  ): Promise<UnzippedArchive[]> {
299    const unzippedArchives: UnzippedArchive[] = [];
300    const progressMessage = 'Unzipping files...';
301
302    progressListener?.onProgressUpdate(progressMessage, 0);
303
304    const currArchive: UnzippedArchive = [];
305    for (let i = 0; i < files.length; i++) {
306      let file = files[i];
307
308      const onSubProgressUpdate = (subPercentage: number) => {
309        const totalPercentage =
310          (100 * i) / files.length + subPercentage / files.length;
311        progressListener?.onProgressUpdate(progressMessage, totalPercentage);
312      };
313
314      if (await FileUtils.isGZipFile(file)) {
315        file = await FileUtils.decompressGZipFile(file);
316      }
317
318      if (await FileUtils.isZipFile(file)) {
319        try {
320          const subFiles = await FileUtils.unzipFile(file, onSubProgressUpdate);
321          const subTraceFiles = subFiles.map((subFile) => {
322            return new TraceFile(subFile, file);
323          });
324          unzippedArchives.push([...subTraceFiles]);
325          onSubProgressUpdate(100);
326        } catch (e) {
327          UserNotifier.add(new CorruptedArchive(file));
328        }
329      } else {
330        currArchive.push(new TraceFile(file, undefined));
331      }
332    }
333    if (currArchive.length > 0) {
334      unzippedArchives.push(currArchive);
335    }
336    progressListener?.onProgressUpdate(progressMessage, 100);
337
338    return unzippedArchives;
339  }
340
341  private removeTracesAndParsersByType(type: TraceType) {
342    const traces = this.traces.getTraces(type);
343    traces.forEach((trace) => {
344      this.removeTrace(trace, true);
345    });
346  }
347}
348