xref: /aosp_15_r20/development/tools/winscope/src/app/trace_pipeline_test.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 {assertDefined} from 'common/assert_utils';
18import {FileUtils} from 'common/file_utils';
19import {ProgressListenerStub} from 'messaging/progress_listener_stub';
20import {UserWarning} from 'messaging/user_warning';
21import {
22  CorruptedArchive,
23  InvalidPerfettoTrace,
24  NoValidFiles,
25  TraceOverridden,
26  UnsupportedFileFormat,
27} from 'messaging/user_warnings';
28import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
29import {TracesUtils} from 'test/unit/traces_utils';
30import {UserNotifierChecker} from 'test/unit/user_notifier_checker';
31import {UnitTestUtils} from 'test/unit/utils';
32import {TraceType} from 'trace/trace_type';
33import {FilesSource} from './files_source';
34import {TracePipeline} from './trace_pipeline';
35
36describe('TracePipeline', () => {
37  let validSfFile: File;
38  let validWmFile: File;
39  let shellTransitionFile: File;
40  let wmTransitionFile: File;
41  let screenshotFile: File;
42  let screenRecordingFile: File;
43  let brMainEntryFile: File;
44  let brCodenameFile: File;
45  let brSfFile: File;
46  let jpgFile: File;
47
48  let progressListener: ProgressListenerStub;
49  let tracePipeline: TracePipeline;
50  let userNotifierChecker: UserNotifierChecker;
51
52  beforeAll(async () => {
53    userNotifierChecker = new UserNotifierChecker();
54    wmTransitionFile = await UnitTestUtils.getFixtureFile(
55      'traces/elapsed_and_real_timestamp/wm_transition_trace.pb',
56    );
57    shellTransitionFile = await UnitTestUtils.getFixtureFile(
58      'traces/elapsed_and_real_timestamp/shell_transition_trace.pb',
59    );
60    validSfFile = await UnitTestUtils.getFixtureFile(
61      'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb',
62    );
63    validWmFile = await UnitTestUtils.getFixtureFile(
64      'traces/elapsed_and_real_timestamp/WindowManager.pb',
65    );
66    screenshotFile = await UnitTestUtils.getFixtureFile(
67      'traces/screenshot.png',
68    );
69    screenRecordingFile = await UnitTestUtils.getFixtureFile(
70      'traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4',
71    );
72    brMainEntryFile = await UnitTestUtils.getFixtureFile(
73      'bugreports/main_entry.txt',
74      'main_entry.txt',
75    );
76    brCodenameFile = await UnitTestUtils.getFixtureFile(
77      'bugreports/bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt',
78      'bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt',
79    );
80    brSfFile = await UnitTestUtils.getFixtureFile(
81      'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb',
82      'FS/data/misc/wmtrace/surface_flinger.bp',
83    );
84    jpgFile = await UnitTestUtils.getFixtureFile('winscope_homepage.jpg');
85  });
86
87  beforeEach(async () => {
88    jasmine.addCustomEqualityTester(UnitTestUtils.timestampEqualityTester);
89
90    progressListener = new ProgressListenerStub();
91    spyOn(progressListener, 'onProgressUpdate');
92    spyOn(progressListener, 'onOperationFinished');
93    userNotifierChecker.reset();
94
95    tracePipeline = new TracePipeline();
96  });
97
98  it('can load valid trace files', async () => {
99    expect(tracePipeline.getTraces().getSize()).toEqual(0);
100
101    await loadFiles([validSfFile, validWmFile], FilesSource.TEST);
102    await expectLoadResult(2, []);
103
104    expect(tracePipeline.getDownloadArchiveFilename()).toMatch(
105      new RegExp(`${FilesSource.TEST}_`),
106    );
107    expect(tracePipeline.getTraces().getSize()).toEqual(2);
108
109    const traceEntries = await TracesUtils.extractEntries(
110      tracePipeline.getTraces(),
111    );
112    expect(traceEntries.get(TraceType.WINDOW_MANAGER)?.length).toBeGreaterThan(
113      0,
114    );
115    expect(traceEntries.get(TraceType.SURFACE_FLINGER)?.length).toBeGreaterThan(
116      0,
117    );
118  });
119
120  it('can load valid gzipped file and archive', async () => {
121    expect(tracePipeline.getTraces().getSize()).toEqual(0);
122
123    const gzippedFile = await UnitTestUtils.getFixtureFile(
124      'traces/WindowManager.pb.gz',
125    );
126    const gzippedArchive = await UnitTestUtils.getFixtureFile(
127      'traces/WindowManager.zip.gz',
128    );
129
130    await loadFiles([gzippedFile, gzippedArchive], FilesSource.TEST);
131    await expectLoadResult(2, []);
132
133    const traces = tracePipeline.getTraces();
134    expect(traces.getSize()).toEqual(2);
135    expect(traces.getTraces(TraceType.WINDOW_MANAGER).length).toEqual(2);
136
137    const traceEntries = await TracesUtils.extractEntries(traces);
138    expect(traceEntries.get(TraceType.WINDOW_MANAGER)?.length).toBeGreaterThan(
139      0,
140    );
141  });
142
143  it('can set download archive filename based on files source', async () => {
144    await loadFiles([validSfFile]);
145    await expectLoadResult(1, []);
146    expect(tracePipeline.getDownloadArchiveFilename()).toMatch(
147      new RegExp('SurfaceFlinger_'),
148    );
149
150    tracePipeline.clear();
151
152    await loadFiles([validSfFile, validWmFile], FilesSource.COLLECTED);
153    await expectLoadResult(2, []);
154    expect(tracePipeline.getDownloadArchiveFilename()).toMatch(
155      new RegExp(`${FilesSource.COLLECTED}_`),
156    );
157  });
158
159  it('can convert illegal uploaded archive filename to legal name for download archive', async () => {
160    const fileWithIllegalName = await UnitTestUtils.getFixtureFile(
161      'traces/SFtrace(with_illegal_characters).pb',
162    );
163    await loadFiles([fileWithIllegalName]);
164    await expectLoadResult(1, []);
165    const downloadFilename = tracePipeline.getDownloadArchiveFilename();
166    expect(FileUtils.DOWNLOAD_FILENAME_REGEX.test(downloadFilename)).toBeTrue();
167  });
168
169  it('detects bugreports and filters out files based on their directory', async () => {
170    expect(tracePipeline.getTraces().getSize()).toEqual(0);
171
172    const bugreportFiles = [
173      brMainEntryFile,
174      brCodenameFile,
175      brSfFile,
176      await UnitTestUtils.getFixtureFile(
177        'traces/elapsed_and_real_timestamp/wm_transition_trace.pb',
178        'FS/data/misc/ignored-dir/window_manager.bp',
179      ),
180    ];
181
182    const bugreportArchive = new File(
183      [await FileUtils.createZipArchive(bugreportFiles)],
184      'bugreport.zip',
185    );
186
187    // Corner case:
188    // Another file is loaded along the bugreport -> the file must not be ignored
189    //
190    // Note:
191    // The even weirder corner case where two bugreports are loaded at the same time is
192    // currently not properly handled.
193    const otherFile = await UnitTestUtils.getFixtureFile(
194      'traces/elapsed_and_real_timestamp/InputMethodClients.pb',
195      'would-be-ignored-if-was-in-bugreport-archive/input_method_clients.pb',
196    );
197
198    await loadFiles([bugreportArchive, otherFile]);
199    await expectLoadResult(2, []);
200
201    const traces = tracePipeline.getTraces();
202    expect(traces.getTrace(TraceType.SURFACE_FLINGER)).toBeDefined();
203    expect(traces.getTrace(TraceType.WINDOW_MANAGER)).toBeUndefined(); // ignored
204    expect(traces.getTrace(TraceType.INPUT_METHOD_CLIENTS)).toBeDefined();
205  });
206
207  it('detects bugreports and extracts timezone info, then calculates utc offset', async () => {
208    const bugreportFiles = [brMainEntryFile, brCodenameFile, brSfFile];
209    const bugreportArchive = new File(
210      [await FileUtils.createZipArchive(bugreportFiles)],
211      'bugreport.zip',
212    );
213
214    await loadFiles([bugreportArchive]);
215    await expectLoadResult(1, []);
216
217    const timestampConverter = tracePipeline.getTimestampConverter();
218    expect(timestampConverter);
219    expect(timestampConverter.getUTCOffset()).toEqual('UTC+05:30');
220
221    const expectedTimestamp =
222      TimestampConverterUtils.makeRealTimestampWithUTCOffset(
223        1659107089102062832n,
224      );
225    expect(
226      timestampConverter.makeTimestampFromMonotonicNs(14500282843n),
227    ).toEqual(expectedTimestamp);
228  });
229
230  it('is robust to corrupted archive', async () => {
231    const corruptedArchive = await UnitTestUtils.getFixtureFile(
232      'corrupted_archive.zip',
233    );
234
235    await loadFiles([corruptedArchive]);
236
237    await expectLoadResult(0, [
238      new CorruptedArchive(corruptedArchive),
239      new NoValidFiles(),
240    ]);
241  });
242
243  it('is robust to invalid trace files', async () => {
244    const invalidFiles = [jpgFile];
245    await loadFiles(invalidFiles);
246
247    await expectLoadResult(0, [
248      new UnsupportedFileFormat('winscope_homepage.jpg'),
249    ]);
250  });
251
252  it('is robust to invalid perfetto trace files', async () => {
253    const invalidFiles = [
254      await UnitTestUtils.getFixtureFile(
255        'traces/perfetto/invalid_protolog.perfetto-trace',
256      ),
257    ];
258
259    await loadFiles(invalidFiles);
260
261    await expectLoadResult(0, [
262      new InvalidPerfettoTrace('invalid_protolog.perfetto-trace', [
263        'Perfetto trace has no IME Clients entries',
264        'Perfetto trace has no IME system_server entries',
265        'Perfetto trace has no IME Service entries',
266        'Perfetto trace has no ProtoLog entries',
267        'Perfetto trace has no Surface Flinger entries',
268        'Perfetto trace has no Transactions entries',
269        'Perfetto trace has no Transitions entries',
270        'Perfetto trace has no ViewCapture windows',
271        'Perfetto trace has no Window Manager entries',
272        'Perfetto trace has no Motion Events entries',
273        'Perfetto trace has no Key Events entries',
274      ]),
275    ]);
276  });
277
278  it('is robust to mixed valid and invalid trace files', async () => {
279    expect(tracePipeline.getTraces().getSize()).toEqual(0);
280    const files = [
281      jpgFile,
282      await UnitTestUtils.getFixtureFile('traces/dump_WindowManager.pb'),
283    ];
284
285    await loadFiles(files);
286
287    await expectLoadResult(1, [
288      new UnsupportedFileFormat('winscope_homepage.jpg'),
289    ]);
290  });
291
292  it('can remove traces', async () => {
293    await loadFiles([validSfFile, validWmFile]);
294    await expectLoadResult(2, []);
295
296    const sfTrace = assertDefined(
297      tracePipeline.getTraces().getTrace(TraceType.SURFACE_FLINGER),
298    );
299    const wmTrace = assertDefined(
300      tracePipeline.getTraces().getTrace(TraceType.WINDOW_MANAGER),
301    );
302
303    tracePipeline.removeTrace(sfTrace);
304    await expectLoadResult(1, []);
305
306    tracePipeline.removeTrace(wmTrace);
307    await expectLoadResult(0, []);
308  });
309
310  it('removes constituent traces of transitions trace but keeps for download', async () => {
311    const files = [wmTransitionFile, wmTransitionFile, shellTransitionFile];
312    await loadFiles(files);
313    await expectLoadResult(1, []);
314
315    const transitionTrace = assertDefined(
316      tracePipeline.getTraces().getTrace(TraceType.TRANSITION),
317    );
318
319    tracePipeline.removeTrace(transitionTrace);
320    await expectLoadResult(0, []);
321
322    await loadFiles([wmTransitionFile]);
323    await expectLoadResult(1, []);
324    expect(
325      tracePipeline.getTraces().getTrace(TraceType.WM_TRANSITION),
326    ).toBeDefined();
327    await expectDownloadResult([
328      'transition/shell_transition_trace.pb',
329      'transition/wm_transition_trace.pb',
330    ]);
331  });
332
333  it('removes constituent traces of CUJs trace but keeps for download', async () => {
334    const files = [
335      await UnitTestUtils.getFixtureFile('traces/eventlog.winscope'),
336    ];
337    await loadFiles(files);
338    await expectLoadResult(1, []);
339
340    const cujTrace = assertDefined(
341      tracePipeline.getTraces().getTrace(TraceType.CUJS),
342    );
343
344    tracePipeline.removeTrace(cujTrace);
345    await expectLoadResult(0, []);
346    await expectDownloadResult(['eventlog/eventlog.winscope']);
347  });
348
349  it('removes constituent traces of input trace but keeps for download', async () => {
350    const files = [
351      await UnitTestUtils.getFixtureFile(
352        'traces/perfetto/input-events.perfetto-trace',
353      ),
354    ];
355    await loadFiles(files);
356    await expectLoadResult(1, []);
357
358    const inputTrace = assertDefined(
359      tracePipeline.getTraces().getTrace(TraceType.INPUT_EVENT_MERGED),
360    );
361
362    tracePipeline.removeTrace(inputTrace);
363    await expectLoadResult(0, []);
364    await expectDownloadResult(['input-events.perfetto-trace']);
365  });
366
367  it('gets loaded traces', async () => {
368    await loadFiles([validSfFile, validWmFile]);
369    await expectLoadResult(2, []);
370
371    const traces = tracePipeline.getTraces();
372
373    const actualTraceTypes = new Set(traces.mapTrace((trace) => trace.type));
374    const expectedTraceTypes = new Set([
375      TraceType.SURFACE_FLINGER,
376      TraceType.WINDOW_MANAGER,
377    ]);
378    expect(actualTraceTypes).toEqual(expectedTraceTypes);
379
380    const sfTrace = assertDefined(traces.getTrace(TraceType.SURFACE_FLINGER));
381    expect(sfTrace.getDescriptors().length).toBeGreaterThan(0);
382  });
383
384  it('gets screenrecording data', async () => {
385    const files = [screenRecordingFile];
386    await loadFiles(files);
387    await expectLoadResult(1, []);
388
389    const video = await tracePipeline.getScreenRecordingVideo();
390    expect(video).toBeDefined();
391    expect(video?.size).toBeGreaterThan(0);
392  });
393
394  it('gets screenshot data', async () => {
395    const files = [screenshotFile];
396    await loadFiles(files);
397    await expectLoadResult(1, []);
398
399    const video = await tracePipeline.getScreenRecordingVideo();
400    expect(video).toBeDefined();
401    expect(video?.size).toBeGreaterThan(0);
402  });
403
404  it('prioritizes screenrecording over screenshot data', async () => {
405    const files = [screenshotFile, screenRecordingFile];
406    await loadFiles(files);
407    await expectLoadResult(1, [
408      new TraceOverridden('screenshot.png', TraceType.SCREEN_RECORDING),
409    ]);
410
411    const video = await tracePipeline.getScreenRecordingVideo();
412    expect(video).toBeDefined();
413    expect(video?.size).toBeGreaterThan(0);
414  });
415
416  it('creates traces with correct type', async () => {
417    await loadFiles([validSfFile, validWmFile]);
418    await expectLoadResult(2, []);
419
420    const traces = tracePipeline.getTraces();
421    traces.forEachTrace((trace, type) => {
422      expect(trace.type).toEqual(type);
423    });
424  });
425
426  it('creates zip archive with loaded trace files', async () => {
427    const files = [
428      screenRecordingFile,
429      await UnitTestUtils.getFixtureFile(
430        'traces/perfetto/transactions_trace.perfetto-trace',
431      ),
432    ];
433    await loadFiles(files);
434    await expectLoadResult(2, []);
435
436    await expectDownloadResult([
437      'screen_recording_metadata_v2.mp4',
438      'transactions_trace.perfetto-trace',
439    ]);
440  });
441
442  it('can be cleared', async () => {
443    await loadFiles([validSfFile, validWmFile]);
444    await expectLoadResult(2, []);
445
446    tracePipeline.clear();
447    expect(tracePipeline.getTraces().getSize()).toEqual(0);
448  });
449
450  it('can filter traces without visualization', async () => {
451    await loadFiles([shellTransitionFile]);
452    await expectLoadResult(1, []);
453
454    tracePipeline.filterTracesWithoutVisualization();
455    expect(tracePipeline.getTraces().getSize()).toEqual(0);
456    expect(
457      tracePipeline.getTraces().getTrace(TraceType.SHELL_TRANSITION),
458    ).toBeUndefined();
459  });
460
461  it('tries to create search trace', async () => {
462    const perfettoFile = await UnitTestUtils.getFixtureFile(
463      'traces/perfetto/layers_trace.perfetto-trace',
464    );
465    await loadFiles([perfettoFile]);
466    const validQuery = 'select ts from surfaceflinger_layers_snapshot';
467    expect(await tracePipeline.tryCreateSearchTrace(validQuery)).toBeDefined();
468    expect(await tracePipeline.tryCreateSearchTrace('fail')).toBeUndefined();
469  });
470
471  it('creates screen recording using metadata', async () => {
472    const screenRecording = await UnitTestUtils.getFixtureFile(
473      'traces/elapsed_and_real_timestamp/screen_recording_no_metadata.mp4',
474    );
475    const metadata = await UnitTestUtils.getFixtureFile(
476      'traces/elapsed_and_real_timestamp/screen_recording_metadata.json',
477    );
478    await loadFiles([screenRecording, metadata]);
479    await expectLoadResult(1, []);
480  });
481
482  async function loadFiles(
483    files: File[],
484    source: FilesSource = FilesSource.TEST,
485  ) {
486    await tracePipeline.loadFiles(files, source, progressListener);
487    expect(progressListener.onOperationFinished).toHaveBeenCalled();
488    await tracePipeline.buildTraces();
489  }
490
491  async function expectLoadResult(
492    numberOfTraces: number,
493    expectedWarnings: UserWarning[],
494  ) {
495    userNotifierChecker.expectAdded(expectedWarnings);
496    expect(tracePipeline.getTraces().getSize()).toEqual(numberOfTraces);
497  }
498
499  async function expectDownloadResult(expectedArchiveContents: string[]) {
500    const zipArchive = await tracePipeline.makeZipArchiveWithLoadedTraceFiles();
501    const actualArchiveContents = (await FileUtils.unzipFile(zipArchive))
502      .map((file) => file.name)
503      .sort();
504    expect(actualArchiveContents).toEqual(expectedArchiveContents);
505  }
506});
507