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