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