xref: /aosp_15_r20/development/tools/winscope/src/app/loaded_parsers_test.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 {TimeRange} from 'common/time';
20import {UserWarning} from 'messaging/user_warning';
21import {TraceHasOldData, TraceOverridden} from 'messaging/user_warnings';
22import {FileAndParser} from 'parsers/file_and_parser';
23import {FileAndParsers} from 'parsers/file_and_parsers';
24import {ParserBuilder} from 'test/unit/parser_builder';
25import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
26import {UserNotifierChecker} from 'test/unit/user_notifier_checker';
27import {Parser} from 'trace/parser';
28import {TraceFile} from 'trace/trace_file';
29import {TraceType} from 'trace/trace_type';
30import {LoadedParsers} from './loaded_parsers';
31
32describe('LoadedParsers', () => {
33  const realZeroTimestamp = TimestampConverterUtils.makeRealTimestamp(0n);
34  const elapsedZeroTimestamp = TimestampConverterUtils.makeElapsedTimestamp(0n);
35  const oldTimestamps = [
36    realZeroTimestamp,
37    TimestampConverterUtils.makeRealTimestamp(1n),
38    TimestampConverterUtils.makeRealTimestamp(2n),
39    TimestampConverterUtils.makeRealTimestamp(3n),
40    TimestampConverterUtils.makeRealTimestamp(4n),
41  ];
42
43  const elapsedTimestamps = [
44    elapsedZeroTimestamp,
45    TimestampConverterUtils.makeElapsedTimestamp(1n),
46    TimestampConverterUtils.makeElapsedTimestamp(2n),
47    TimestampConverterUtils.makeElapsedTimestamp(3n),
48    TimestampConverterUtils.makeElapsedTimestamp(4n),
49  ];
50
51  const timestamps = [
52    TimestampConverterUtils.makeRealTimestamp(5n * 60n * 1000000000n + 10n), // 5m10ns
53    TimestampConverterUtils.makeRealTimestamp(5n * 60n * 1000000000n + 11n), // 5m11ns
54    TimestampConverterUtils.makeRealTimestamp(5n * 60n * 1000000000n + 12n), // 5m12ns
55  ];
56
57  const filename = 'filename';
58
59  const parserSf0 = new ParserBuilder<object>()
60    .setType(TraceType.SURFACE_FLINGER)
61    .setTimestamps(timestamps)
62    .setDescriptors([filename])
63    .build();
64  const parserSf1 = new ParserBuilder<object>()
65    .setType(TraceType.SURFACE_FLINGER)
66    .setTimestamps(timestamps)
67    .setDescriptors([filename])
68    .build();
69  const parserSf_longButOldData = new ParserBuilder<object>()
70    .setType(TraceType.SURFACE_FLINGER)
71    .setTimestamps(oldTimestamps)
72    .setDescriptors([filename])
73    .build();
74  const parserSf_empty = new ParserBuilder<object>()
75    .setType(TraceType.SURFACE_FLINGER)
76    .setTimestamps([])
77    .setDescriptors([filename])
78    .build();
79  const parserSf_elapsed = new ParserBuilder<object>()
80    .setType(TraceType.SURFACE_FLINGER)
81    .setTimestamps(elapsedTimestamps)
82    .setDescriptors([filename])
83    .setNoOffsets(true)
84    .build();
85  const parserWm0 = new ParserBuilder<object>()
86    .setType(TraceType.WINDOW_MANAGER)
87    .setTimestamps(timestamps)
88    .setDescriptors([filename])
89    .build();
90  const parserWm1 = new ParserBuilder<object>()
91    .setType(TraceType.WINDOW_MANAGER)
92    .setTimestamps(timestamps)
93    .setDescriptors([filename])
94    .build();
95  const parserWm_dump = new ParserBuilder<object>()
96    .setType(TraceType.WINDOW_MANAGER)
97    .setTimestamps([realZeroTimestamp])
98    .setDescriptors([filename])
99    .build();
100  const parserWm_elapsed = new ParserBuilder<object>()
101    .setType(TraceType.WINDOW_MANAGER)
102    .setTimestamps(elapsedTimestamps)
103    .setDescriptors([filename])
104    .setNoOffsets(true)
105    .build();
106  const parserWmTransitions = new ParserBuilder<object>()
107    .setType(TraceType.WM_TRANSITION)
108    .setTimestamps([
109      elapsedZeroTimestamp,
110      elapsedZeroTimestamp,
111      elapsedZeroTimestamp,
112    ])
113    .setDescriptors([filename])
114    .build();
115  const parserEventlog = new ParserBuilder<object>()
116    .setType(TraceType.EVENT_LOG)
117    .setTimestamps(timestamps)
118    .setDescriptors([filename])
119    .setNoOffsets(true)
120    .build();
121  const parserScreenRecording = new ParserBuilder<object>()
122    .setType(TraceType.SCREEN_RECORDING)
123    .setTimestamps(timestamps)
124    .setDescriptors([filename])
125    .build();
126  const parserViewCapture0 = new ParserBuilder<object>()
127    .setType(TraceType.VIEW_CAPTURE)
128    .setEntries([])
129    .setDescriptors([filename])
130    .build();
131  const parserViewCapture1 = new ParserBuilder<object>()
132    .setType(TraceType.VIEW_CAPTURE)
133    .setEntries([])
134    .setDescriptors([filename])
135    .build();
136
137  let loadedParsers: LoadedParsers;
138  let userNotifierChecker: UserNotifierChecker;
139
140  beforeAll(() => {
141    userNotifierChecker = new UserNotifierChecker();
142  });
143
144  beforeEach(() => {
145    loadedParsers = new LoadedParsers();
146    expect(loadedParsers.getParsers().length).toEqual(0);
147    userNotifierChecker.reset();
148  });
149
150  it('can load a single legacy parser', () => {
151    loadParsers([parserSf0], []);
152    expectLoadResult([parserSf0], []);
153  });
154
155  it('can load a single perfetto parser', () => {
156    loadParsers([], [parserSf0]);
157    expectLoadResult([parserSf0], []);
158  });
159
160  it('loads multiple perfetto parsers with same trace type', async () => {
161    loadParsers([], [parserSf0, parserSf1]);
162    expectLoadResult([parserSf0, parserSf1], []);
163  });
164
165  it('loads legacy parser without dropping already-loaded legacy parser (different trace type)', async () => {
166    loadParsers([parserSf0], []);
167    expectLoadResult([parserSf0], []);
168
169    loadParsers([parserWm0], []);
170    expectLoadResult([parserSf0, parserWm0], []);
171  });
172
173  it('loads legacy parser without dropping already-loaded legacy parser (same trace type)', async () => {
174    loadParsers([parserSf0], []);
175    expectLoadResult([parserSf0], []);
176
177    loadParsers([parserSf1], []);
178    expectLoadResult([parserSf0, parserSf1], []);
179  });
180
181  it('drops elapsed-only parsers if parsers with real timestamps present', () => {
182    loadParsers([parserSf_elapsed, parserSf0], []);
183    expectLoadResult([parserSf0], [new TraceHasOldData(filename)]);
184  });
185
186  it('doesnt drop elapsed-only parsers if no parsers with real timestamps present', () => {
187    loadParsers([parserSf_elapsed, parserWm_elapsed], []);
188    expectLoadResult([parserSf_elapsed, parserWm_elapsed], []);
189  });
190
191  it('keeps real-time parsers without offset', () => {
192    loadParsers([parserSf0, parserEventlog], []);
193    expectLoadResult([parserSf0, parserEventlog], []);
194  });
195
196  describe('drops legacy parser with old data (dangling old trace file)', () => {
197    const timeGapFrom = assertDefined(
198      parserSf_longButOldData.getTimestamps()?.at(-1),
199    );
200    const timeGapTo = assertDefined(parserWm0.getTimestamps()?.at(0));
201    const timeGap = new TimeRange(timeGapFrom, timeGapTo);
202
203    it('taking into account other legacy parsers', () => {
204      loadParsers([parserSf_longButOldData, parserWm0], []);
205      expectLoadResult([parserWm0], [new TraceHasOldData(filename, timeGap)]);
206    });
207
208    it('taking into account perfetto parsers', () => {
209      loadParsers([parserSf_longButOldData], [parserWm0]);
210      expectLoadResult([parserWm0], [new TraceHasOldData(filename, timeGap)]);
211    });
212
213    it('taking into account already-loaded parsers', () => {
214      loadParsers([parserWm0], []);
215
216      // Drop parser with old data, even if it provides
217      // a longer trace than the already-loaded parser
218      loadParsers([parserSf_longButOldData], []);
219      expectLoadResult([parserWm0], [new TraceHasOldData(filename, timeGap)]);
220    });
221
222    it('doesnt drop legacy parser with dump (zero timestamp)', () => {
223      loadParsers([parserWm_dump, parserSf0], []);
224      expectLoadResult([parserWm_dump, parserSf0], []);
225    });
226
227    it('doesnt drop legacy parser with wm transitions', () => {
228      // Only Shell Transition data used to set timestamps of merged Transition trace,
229      // so WM Transition data should not be considered by "old data" policy
230      loadParsers([parserWmTransitions, parserSf0], []);
231      expectLoadResult([parserWmTransitions, parserSf0], []);
232    });
233
234    it('is robust to traces with time range overlap', () => {
235      const parser = parserSf0;
236      const timestamps = assertDefined(parserSf0.getTimestamps());
237
238      const timestampsOverlappingFront = [
239        timestamps[0].add(-1n),
240        timestamps[0].add(1n),
241      ];
242      const parserOverlappingFront = new ParserBuilder<object>()
243        .setType(TraceType.TRANSACTIONS)
244        .setTimestamps(timestampsOverlappingFront)
245        .setDescriptors([filename])
246        .build();
247
248      const timestampsOverlappingBack = [
249        timestamps[timestamps.length - 1].add(-1n),
250        timestamps[timestamps.length - 1].add(1n),
251      ];
252      const parserOverlappingBack = new ParserBuilder<object>()
253        .setType(TraceType.TRANSITION)
254        .setTimestamps(timestampsOverlappingBack)
255        .setDescriptors([filename])
256        .build();
257
258      const timestampsOverlappingEntirely = [
259        timestamps[0].add(-1n),
260        timestamps[timestamps.length - 1].add(1n),
261      ];
262      const parserOverlappingEntirely = new ParserBuilder<object>()
263        .setType(TraceType.VIEW_CAPTURE)
264        .setTimestamps(timestampsOverlappingEntirely)
265        .setDescriptors([filename])
266        .build();
267
268      const timestampsOverlappingExactly = [
269        timestamps[0],
270        timestamps[timestamps.length - 1],
271      ];
272      const parserOverlappingExactly = new ParserBuilder<object>()
273        .setType(TraceType.WINDOW_MANAGER)
274        .setTimestamps(timestampsOverlappingExactly)
275        .setDescriptors([filename])
276        .build();
277
278      loadParsers(
279        [
280          parser,
281          parserOverlappingFront,
282          parserOverlappingBack,
283          parserOverlappingEntirely,
284          parserOverlappingExactly,
285        ],
286        [],
287      );
288      expectLoadResult(
289        [
290          parser,
291          parserOverlappingFront,
292          parserOverlappingBack,
293          parserOverlappingEntirely,
294          parserOverlappingExactly,
295        ],
296        [],
297      );
298    });
299  });
300
301  it('loads perfetto parser dropping all already-loaded perfetto parsers', () => {
302    loadParsers([], [parserSf0, parserWm0]);
303    expectLoadResult([parserSf0, parserWm0], []);
304
305    // We currently run only one Perfetto TP WebWorker at a time,
306    // so Perfetto parsers previously loaded are now invalid
307    // and must be removed (previous WebWorker is not running anymore).
308    loadParsers([], [parserSf1, parserWm1]);
309    expectLoadResult([parserSf1, parserWm1], []);
310  });
311
312  describe('prioritizes perfetto parsers over legacy parsers', () => {
313    // While transitioning to the Perfetto format, devices might still have old legacy trace files
314    // dangling in the disk that get automatically included into bugreports. Hence, Perfetto parsers
315    // must always override legacy ones so that dangling legacy files are ignored.
316
317    it('when a perfetto parser is already loaded', () => {
318      loadParsers([parserSf0], [parserSf1]);
319      expectLoadResult([parserSf1], [new TraceOverridden(filename)]);
320      userNotifierChecker.reset();
321
322      loadParsers([parserSf0], []);
323      expectLoadResult([parserSf1], [new TraceOverridden(filename)]);
324    });
325
326    it('when a perfetto parser is loaded afterwards', () => {
327      loadParsers([parserSf0], []);
328      expectLoadResult([parserSf0], []);
329
330      loadParsers([], [parserSf1]);
331      expectLoadResult([parserSf1], [new TraceOverridden(filename)]);
332    });
333  });
334
335  describe('is robust to multiple parsers of same type loaded at once', () => {
336    it('legacy parsers', () => {
337      loadParsers([parserSf0, parserSf1], []);
338      expectLoadResult([parserSf0, parserSf1], []);
339    });
340
341    it('legacy + perfetto parsers', () => {
342      loadParsers([parserSf0, parserSf0], [parserSf1]);
343      expectLoadResult(
344        [parserSf1],
345        [new TraceOverridden(filename), new TraceOverridden(filename)],
346      );
347    });
348  });
349
350  describe('is robust to parser with no entries', () => {
351    it('legacy parser', () => {
352      loadParsers([parserSf_empty], []);
353      expectLoadResult([parserSf_empty], []);
354    });
355
356    it('perfetto parser', () => {
357      loadParsers([], [parserSf_empty]);
358      expectLoadResult([parserSf_empty], []);
359    });
360  });
361
362  describe('handles screen recordings and screenshots', () => {
363    const parserScreenRecording0 = new ParserBuilder<object>()
364      .setType(TraceType.SCREEN_RECORDING)
365      .setTimestamps(timestamps)
366      .setDescriptors(['screen_recording.mp4'])
367      .build();
368    const parserScreenRecording1 = new ParserBuilder<object>()
369      .setType(TraceType.SCREEN_RECORDING)
370      .setTimestamps(timestamps)
371      .setDescriptors(['screen_recording.mp4'])
372      .build();
373    const parserScreenshot0 = new ParserBuilder<object>()
374      .setType(TraceType.SCREENSHOT)
375      .setTimestamps(timestamps)
376      .setDescriptors(['screenshot.png'])
377      .build();
378    const parserScreenshot1 = new ParserBuilder<object>()
379      .setType(TraceType.SCREENSHOT)
380      .setTimestamps(timestamps)
381      .setDescriptors(['screenshot.png'])
382      .build();
383    const overrideError = new TraceOverridden(
384      'screenshot.png',
385      TraceType.SCREEN_RECORDING,
386    );
387
388    it('loads screenshot parser', () => {
389      loadParsers([parserScreenshot0], []);
390      expectLoadResult([parserScreenshot0], []);
391    });
392
393    it('loads screen recording parser', () => {
394      loadParsers([parserScreenRecording0], []);
395      expectLoadResult([parserScreenRecording0], []);
396    });
397
398    it('does not load screenshot parser after loading screen recording parser in same call', () => {
399      loadParsers([parserScreenshot0, parserScreenRecording0], []);
400      expectLoadResult([parserScreenRecording0], [overrideError]);
401    });
402
403    it('does not load screenshot parser after loading screen recording parser in previous call', () => {
404      loadParsers([parserScreenRecording0], []);
405      expectLoadResult([parserScreenRecording0], []);
406
407      loadParsers([parserScreenshot0], []);
408      expectLoadResult([parserScreenRecording0], [overrideError]);
409    });
410
411    it('overrides previously loaded screenshot parser with screen recording parser', () => {
412      loadParsers([parserScreenshot0], []);
413      expectLoadResult([parserScreenshot0], []);
414
415      loadParsers([parserScreenRecording0], []);
416      expectLoadResult([parserScreenRecording0], [overrideError]);
417    });
418
419    it('loads multiple screen recordings', () => {
420      loadParsers([parserScreenRecording0], []);
421      expectLoadResult([parserScreenRecording0], []);
422
423      loadParsers([parserScreenRecording1], []);
424      expectLoadResult([parserScreenRecording0, parserScreenRecording1], []);
425    });
426  });
427
428  it('can remove parsers', () => {
429    loadParsers([parserSf0], [parserWm0]);
430    expectLoadResult([parserSf0, parserWm0], []);
431
432    loadedParsers.remove(parserWm0);
433    expectLoadResult([parserSf0], []);
434
435    loadedParsers.remove(parserSf0);
436    expectLoadResult([], []);
437  });
438
439  it('can remove parsers but keep for download', async () => {
440    loadParsers([parserSf0, parserWm0], []);
441    expectLoadResult([parserSf0, parserWm0], []);
442
443    loadedParsers.remove(parserWm0, true);
444    expectLoadResult([parserSf0], []);
445
446    await expectDownloadResult([
447      'sf/filename.winscope',
448      'wm/filename.winscope',
449    ]);
450  });
451
452  it('can be cleared', () => {
453    loadedParsers.clear();
454    loadParsers([parserSf0], [parserWm0]);
455    expectLoadResult([parserSf0, parserWm0], []);
456
457    loadedParsers.clear();
458    expectLoadResult([], []);
459
460    loadParsers([parserSf0], [parserWm0]);
461    expectLoadResult([parserSf0, parserWm0], []);
462  });
463
464  it('can make zip archive of traces with appropriate directories and extensions', async () => {
465    const fileDuplicated = new File([], filename);
466
467    const legacyFiles = [
468      // ScreenRecording
469      new File([], filename),
470
471      // ViewCapture
472      // Multiple parsers point to the same viewcapture file,
473      // but we expect to see only one in the output archive (deduplicated)
474      fileDuplicated,
475      fileDuplicated,
476
477      // WM
478      new File([], filename + '.pb'),
479
480      // WM
481      // Same filename as above.
482      // Expect this file to be automatically renamed to avoid clashes/overwrites
483      new File([], filename + '.pb'),
484    ];
485
486    loadParsers(
487      [
488        parserScreenRecording,
489        parserViewCapture0,
490        parserViewCapture1,
491        parserWm0,
492        parserWm1,
493      ],
494      [parserSf0, parserWmTransitions],
495      legacyFiles,
496    );
497    expectLoadResult(
498      [
499        parserScreenRecording,
500        parserViewCapture0,
501        parserViewCapture1,
502        parserWm0,
503        parserWm1,
504        parserSf0,
505        parserWmTransitions,
506      ],
507      [],
508    );
509
510    await expectDownloadResult([
511      'filename.mp4',
512      'filename.perfetto-trace',
513      'vc/filename.winscope',
514      'wm/filename (1).pb',
515      'wm/filename.pb',
516    ]);
517  });
518
519  it('makes zip archive with progress listener', async () => {
520    loadParsers([parserSf0], [parserWm0]);
521    expectLoadResult([parserSf0, parserWm0], []);
522
523    const progressSpy = jasmine.createSpy();
524    await loadedParsers.makeZipArchive(progressSpy);
525
526    expect(progressSpy).toHaveBeenCalledTimes(5);
527    expect(progressSpy).toHaveBeenCalledWith(0);
528    expect(progressSpy).toHaveBeenCalledWith(0.25);
529    expect(progressSpy).toHaveBeenCalledWith(0.5);
530    expect(progressSpy).toHaveBeenCalledWith(0.75);
531    expect(progressSpy).toHaveBeenCalledWith(1);
532  });
533
534  function loadParsers(
535    legacy: Array<Parser<object>>,
536    perfetto: Array<Parser<object>>,
537    legacyFiles?: File[],
538  ) {
539    const legacyFileAndParsers = legacy.map((parser, i) => {
540      const legacyFile = legacyFiles ? legacyFiles[i] : new File([], filename);
541      return new FileAndParser(new TraceFile(legacyFile), parser);
542    });
543
544    const perfettoTraceFile = new TraceFile(new File([], filename));
545    const perfettoFileAndParsers =
546      perfetto.length > 0
547        ? new FileAndParsers(perfettoTraceFile, perfetto)
548        : undefined;
549
550    loadedParsers.addParsers(legacyFileAndParsers, perfettoFileAndParsers);
551  }
552
553  function expectLoadResult(
554    expectedParsers: Array<Parser<object>>,
555    expectedWarnings: UserWarning[],
556  ) {
557    const actualParsers = loadedParsers.getParsers();
558    expect(actualParsers.length).toEqual(expectedParsers.length);
559    expect(new Set([...actualParsers])).toEqual(new Set([...expectedParsers]));
560
561    userNotifierChecker.expectAdded(expectedWarnings);
562  }
563
564  async function expectDownloadResult(expectedArchiveContents: string[]) {
565    const zipArchive = await loadedParsers.makeZipArchive();
566    const actualArchiveContents = (await FileUtils.unzipFile(zipArchive))
567      .map((file) => file.name)
568      .sort();
569    expect(actualArchiveContents).toEqual(expectedArchiveContents);
570  }
571});
572