• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {FunctionUtils} from 'common/function_utils';
19import {InMemoryStorage} from 'common/store/in_memory_storage';
20import {TimestampConverterUtils} from 'common/time/test_utils';
21import {TimezoneInfo} from 'common/time/time';
22import {TimestampConverter} from 'common/time/timestamp_converter';
23import {CrossToolProtocol} from 'cross_tool/cross_tool_protocol';
24import {ProgressListener} from 'messaging/progress_listener';
25import {ProgressListenerStub} from 'messaging/progress_listener_stub';
26import {UserWarning} from 'messaging/user_warning';
27import {
28  FailedToCreateTracesParser,
29  IncompleteFrameMapping,
30  InvalidLegacyTrace,
31  NoTraceTargetsSelected,
32  NoValidFiles,
33  UnsupportedFileFormat,
34} from 'messaging/user_warnings';
35import {
36  ActiveTraceChanged,
37  AppFilesCollected,
38  AppFilesUploaded,
39  AppInitialized,
40  AppRefreshDumpsRequest,
41  AppResetRequest,
42  AppTraceViewRequest,
43  AppTraceViewRequestHandled,
44  DarkModeToggled,
45  ExpandedTimelineToggled,
46  FilterPresetApplyRequest,
47  FilterPresetSaveRequest,
48  InitializeTraceSearchRequest,
49  NoTraceTargetsSelected as NoTraceTargetsSelectedEvent,
50  RemoteToolDownloadStart,
51  RemoteToolFilesReceived,
52  RemoteToolTimestampReceived,
53  TabbedViewSwitched,
54  TabbedViewSwitchRequest,
55  TraceAddRequest,
56  TracePositionUpdate,
57  TraceRemoveRequest,
58  TraceSearchCompleted,
59  TraceSearchFailed,
60  TraceSearchInitialized,
61  TraceSearchRequest,
62  ViewersLoaded,
63  ViewersUnloaded,
64  WinscopeEvent,
65  WinscopeEventType,
66} from 'messaging/winscope_event';
67import {getFixtureFile} from 'test/unit/fixture_utils';
68
69import {WinscopeEventEmitter} from 'messaging/winscope_event_emitter';
70import {WinscopeEventEmitterStub} from 'messaging/winscope_event_emitter_stub';
71import {WinscopeEventListener} from 'messaging/winscope_event_listener';
72import {WinscopeEventListenerStub} from 'messaging/winscope_event_listener_stub';
73import {TraceBuilder} from 'test/unit/trace_builder';
74import {UserNotifierChecker} from 'test/unit/user_notifier_checker';
75import {Trace} from 'trace/trace';
76import {TracePosition} from 'trace/trace_position';
77import {TraceType} from 'trace/trace_type';
78import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
79import {ViewType} from 'viewers/viewer';
80import {ViewerFactory} from 'viewers/viewer_factory';
81import {ViewerStub} from 'viewers/viewer_stub';
82import {Mediator} from './mediator';
83import {TimelineData} from './timeline_data';
84import {TracePipeline} from './trace_pipeline';
85import {TraceSearchInitializer} from './trace_search/trace_search_initializer';
86
87describe('Mediator', () => {
88  const TIMESTAMP_10 = TimestampConverterUtils.makeRealTimestamp(10n);
89  const TIMESTAMP_11 = TimestampConverterUtils.makeRealTimestamp(11n);
90
91  const POSITION_10 = TracePosition.fromTimestamp(TIMESTAMP_10);
92  const POSITION_11 = TracePosition.fromTimestamp(TIMESTAMP_11);
93
94  const traceSf = new TraceBuilder<HierarchyTreeNode>()
95    .setType(TraceType.SURFACE_FLINGER)
96    .setTimestamps([TIMESTAMP_10])
97    .build();
98  const traceWm = new TraceBuilder<HierarchyTreeNode>()
99    .setType(TraceType.WINDOW_MANAGER)
100    .setTimestamps([TIMESTAMP_11])
101    .build();
102  const traceDump = new TraceBuilder<HierarchyTreeNode>()
103    .setType(TraceType.SURFACE_FLINGER)
104    .setTimestamps([TimestampConverterUtils.makeZeroTimestamp()])
105    .build();
106
107  let inputFiles: File[];
108  let eventLogFile: File;
109  let perfettoFile: File;
110  let tracePipeline: TracePipeline;
111  let timelineData: TimelineData;
112  let abtChromeExtensionProtocol: WinscopeEventEmitter & WinscopeEventListener;
113  let crossToolProtocol: CrossToolProtocol;
114  let appComponent: WinscopeEventListener;
115  let timelineComponent: WinscopeEventEmitter & WinscopeEventListener;
116  let uploadTracesComponent: WinscopeEventListenerStub & ProgressListenerStub;
117  let collectTracesComponent: ProgressListenerStub &
118    WinscopeEventEmitterStub &
119    WinscopeEventListenerStub;
120  let traceViewComponent: WinscopeEventEmitter & WinscopeEventListener;
121  let mediator: Mediator;
122  let spies: Array<jasmine.Spy<jasmine.Func>>;
123  let userNotifierChecker: UserNotifierChecker;
124  let createViewersSpy: jasmine.Spy;
125
126  const viewerStub0 = new ViewerStub('Title0', undefined, traceSf);
127  const viewerStub1 = new ViewerStub('Title1', undefined, traceWm);
128  const viewerOverlay = new ViewerStub(
129    'TitleOverlay',
130    undefined,
131    traceWm,
132    ViewType.OVERLAY,
133  );
134  const viewerDump = new ViewerStub('TitleDump', undefined, traceDump);
135  const viewers = [viewerStub0, viewerStub1, viewerOverlay, viewerDump];
136  let tracePositionUpdateListeners: WinscopeEventListener[];
137
138  beforeAll(async () => {
139    inputFiles = [
140      await getFixtureFile(
141        'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb',
142      ),
143      await getFixtureFile(
144        'traces/elapsed_and_real_timestamp/WindowManager.pb',
145      ),
146      await getFixtureFile(
147        'traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4',
148      ),
149    ];
150    perfettoFile = await getFixtureFile(
151      'traces/perfetto/layers_trace.perfetto-trace',
152    );
153    eventLogFile = await getFixtureFile('traces/eventlog_no_cujs.winscope');
154    userNotifierChecker = new UserNotifierChecker();
155  });
156
157  beforeEach(() => {
158    userNotifierChecker.reset();
159    jasmine.addCustomEqualityTester(tracePositionUpdateEqualityTester);
160    tracePipeline = new TracePipeline();
161    timelineData = new TimelineData();
162    abtChromeExtensionProtocol = FunctionUtils.mixin(
163      new WinscopeEventEmitterStub(),
164      new WinscopeEventListenerStub(),
165    );
166    crossToolProtocol = new CrossToolProtocol(
167      tracePipeline.getTimestampConverter(),
168    );
169    appComponent = new WinscopeEventListenerStub();
170    timelineComponent = FunctionUtils.mixin(
171      new WinscopeEventEmitterStub(),
172      new WinscopeEventListenerStub(),
173    );
174    uploadTracesComponent = FunctionUtils.mixin(
175      new ProgressListenerStub(),
176      new WinscopeEventListenerStub(),
177    );
178    collectTracesComponent = FunctionUtils.mixin(
179      FunctionUtils.mixin(
180        new ProgressListenerStub(),
181        new WinscopeEventListenerStub(),
182      ),
183      new WinscopeEventEmitterStub(),
184    );
185    traceViewComponent = FunctionUtils.mixin(
186      new WinscopeEventEmitterStub(),
187      new WinscopeEventListenerStub(),
188    );
189    mediator = new Mediator(
190      tracePipeline,
191      timelineData,
192      abtChromeExtensionProtocol,
193      crossToolProtocol,
194      appComponent,
195      new InMemoryStorage(),
196    );
197    mediator.setTimelineComponent(timelineComponent);
198    mediator.setUploadTracesComponent(uploadTracesComponent);
199    mediator.setCollectTracesComponent(collectTracesComponent);
200    mediator.setTraceViewComponent(traceViewComponent);
201
202    tracePositionUpdateListeners = [
203      ...viewers,
204      timelineComponent,
205      crossToolProtocol,
206    ];
207
208    createViewersSpy = spyOn(
209      ViewerFactory.prototype,
210      'createViewers',
211    ).and.returnValue(viewers);
212
213    spies = [
214      spyOn(abtChromeExtensionProtocol, 'onWinscopeEvent'),
215      spyOn(appComponent, 'onWinscopeEvent'),
216      spyOn(collectTracesComponent, 'onOperationFinished'),
217      spyOn(collectTracesComponent, 'onProgressUpdate'),
218      spyOn(collectTracesComponent, 'onWinscopeEvent'),
219      spyOn(crossToolProtocol, 'onWinscopeEvent'),
220      spyOn(timelineComponent, 'onWinscopeEvent'),
221      spyOn(timelineData, 'initialize').and.callThrough(),
222      spyOn(traceViewComponent, 'onWinscopeEvent'),
223      spyOn(uploadTracesComponent, 'onWinscopeEvent'),
224      spyOn(uploadTracesComponent, 'onProgressUpdate'),
225      spyOn(uploadTracesComponent, 'onOperationFinished'),
226      spyOn(viewerStub0, 'onWinscopeEvent'),
227      spyOn(viewerStub1, 'onWinscopeEvent'),
228      spyOn(viewerOverlay, 'onWinscopeEvent'),
229      spyOn(viewerDump, 'onWinscopeEvent'),
230    ];
231  });
232
233  it('notifies ABT chrome extension about app initialization', async () => {
234    expect(abtChromeExtensionProtocol.onWinscopeEvent).not.toHaveBeenCalled();
235
236    await mediator.onWinscopeEvent(new AppInitialized());
237    expect(abtChromeExtensionProtocol.onWinscopeEvent).toHaveBeenCalledOnceWith(
238      new AppInitialized(),
239    );
240  });
241
242  it('handles uploaded traces from Winscope', async () => {
243    await mediator.onWinscopeEvent(new AppFilesUploaded(inputFiles));
244
245    expect(uploadTracesComponent.onProgressUpdate).toHaveBeenCalled();
246    expect(uploadTracesComponent.onOperationFinished).toHaveBeenCalled();
247    expect(timelineData.initialize).not.toHaveBeenCalled();
248    expect(appComponent.onWinscopeEvent).not.toHaveBeenCalled();
249    expect(viewerStub0.onWinscopeEvent).not.toHaveBeenCalled();
250
251    resetSpyCalls();
252    await mediator.onWinscopeEvent(new AppTraceViewRequest());
253    checkLoadTraceViewEvents(uploadTracesComponent);
254    userNotifierChecker.expectNotified([]);
255  });
256
257  it('handles collected traces from Winscope', async () => {
258    await mediator.onWinscopeEvent(
259      new AppFilesCollected({
260        requested: [],
261        collected: [inputFiles[0], inputFiles[1]],
262      }),
263    );
264    userNotifierChecker.expectNone();
265    checkLoadTraceViewEvents(collectTracesComponent);
266    checkUploadTracesComponentTraceViewEvents();
267  });
268
269  it('handles invalid collected traces from Winscope', async () => {
270    await mediator.onWinscopeEvent(
271      new AppFilesCollected({
272        requested: [],
273        collected: [await getFixtureFile('traces/empty.pb')],
274      }),
275    );
276    expect(
277      userNotifierChecker.expectNotified([
278        new UnsupportedFileFormat('empty.pb'),
279      ]),
280    );
281    expect(appComponent.onWinscopeEvent).not.toHaveBeenCalled();
282  });
283
284  it('handles collected traces with no entries from Winscope', async () => {
285    await mediator.onWinscopeEvent(
286      new AppFilesCollected({
287        requested: [],
288        collected: [
289          await getFixtureFile('traces/no_entries_InputMethodClients.pb'),
290        ],
291      }),
292    );
293    expect(
294      userNotifierChecker.expectNotified([
295        new InvalidLegacyTrace(
296          'no_entries_InputMethodClients.pb',
297          'Trace has no entries',
298        ),
299      ]),
300    );
301    expect(appComponent.onWinscopeEvent).not.toHaveBeenCalled();
302  });
303
304  it('handles collected trace with no visualization from Winscope', async () => {
305    await mediator.onWinscopeEvent(
306      new AppFilesCollected({
307        requested: [],
308        collected: [eventLogFile],
309      }),
310    );
311    expect(
312      userNotifierChecker.expectNotified([
313        new FailedToCreateTracesParser(
314          TraceType.CUJS,
315          'eventlog_no_cujs.winscope has no relevant entries',
316        ),
317      ]),
318    );
319    expect(appComponent.onWinscopeEvent).not.toHaveBeenCalled();
320    checkUploadTracesComponentTraceViewEvents();
321  });
322
323  it('handles empty collected traces from Winscope', async () => {
324    await mediator.onWinscopeEvent(
325      new AppFilesCollected({
326        requested: [],
327        collected: [],
328      }),
329    );
330    expect(userNotifierChecker.expectNotified([new NoValidFiles()]));
331    expect(appComponent.onWinscopeEvent).not.toHaveBeenCalled();
332  });
333
334  it('handles requested traces with missing collected traces from Winscope', async () => {
335    await mediator.onWinscopeEvent(
336      new AppFilesCollected({
337        requested: [
338          {
339            name: 'Collected Trace',
340            types: [TraceType.SURFACE_FLINGER],
341          },
342          {
343            name: 'Uncollected Trace',
344            types: [TraceType.TRANSITION],
345          },
346        ],
347        collected: [inputFiles[0]],
348      }),
349    );
350    expect(
351      userNotifierChecker.expectNotified([
352        new NoValidFiles(['Uncollected Trace']),
353      ]),
354    );
355    expect(appComponent.onWinscopeEvent).toHaveBeenCalled();
356    checkUploadTracesComponentTraceViewEvents();
357  });
358
359  it('handles app reset request', async () => {
360    await mediator.onWinscopeEvent(new AppFilesUploaded(inputFiles));
361    const clearSpies = [
362      spyOn(tracePipeline, 'clear'),
363      spyOn(timelineData, 'clear'),
364    ];
365    await mediator.onWinscopeEvent(new AppResetRequest());
366    clearSpies.forEach((spy) => expect(spy).toHaveBeenCalled());
367    expect(appComponent.onWinscopeEvent).toHaveBeenCalledOnceWith(
368      new ViewersUnloaded(),
369    );
370  });
371
372  it('handles request to refresh dumps', async () => {
373    const dumpFiles = [
374      await getFixtureFile(
375        'traces/elapsed_and_real_timestamp/dump_SurfaceFlinger.pb',
376      ),
377      await getFixtureFile('traces/dump_WindowManager.pb'),
378    ];
379    await loadFiles(dumpFiles);
380    await mediator.onWinscopeEvent(new AppTraceViewRequest());
381    checkLoadTraceViewEvents(uploadTracesComponent);
382
383    await mediator.onWinscopeEvent(new AppRefreshDumpsRequest());
384    expect(collectTracesComponent.onWinscopeEvent).toHaveBeenCalled();
385  });
386
387  //TODO: test "bugreport data from cross-tool protocol" when FileUtils is fully compatible with
388  //      Node.js (b/262269229). FileUtils#unzipFile() currently can't execute on Node.js.
389
390  //TODO: test "data from ABT chrome extension" when FileUtils is fully compatible with Node.js
391  //      (b/262269229).
392
393  it('handles start download event from remote tool', async () => {
394    expect(uploadTracesComponent.onProgressUpdate).toHaveBeenCalledTimes(0);
395
396    await mediator.onWinscopeEvent(new RemoteToolDownloadStart());
397    expect(uploadTracesComponent.onProgressUpdate).toHaveBeenCalledTimes(1);
398  });
399
400  it('handles empty downloaded files from remote tool', async () => {
401    expect(uploadTracesComponent.onOperationFinished).toHaveBeenCalledTimes(0);
402
403    // Pass files even if empty so that the upload component will update the progress bar
404    // and display error messages
405    await mediator.onWinscopeEvent(new RemoteToolFilesReceived([]));
406    expect(uploadTracesComponent.onOperationFinished).toHaveBeenCalledTimes(1);
407  });
408
409  it('notifies overlay viewer of expanded timeline toggle change', async () => {
410    await loadFiles();
411    await loadTraceView();
412    const event = new ExpandedTimelineToggled(true);
413    await mediator.onWinscopeEvent(new ExpandedTimelineToggled(true));
414    expect(viewerOverlay.onWinscopeEvent).toHaveBeenCalledWith(event);
415  });
416
417  it('propagates trace position update', async () => {
418    await loadFiles();
419    await loadTraceView();
420
421    // notify position
422    resetSpyCalls();
423    await mediator.onWinscopeEvent(new TracePositionUpdate(POSITION_10));
424    checkTracePositionUpdateEvents(
425      [viewerStub0, viewerOverlay, timelineComponent, crossToolProtocol],
426      [],
427      POSITION_10,
428    );
429
430    // notify position
431    resetSpyCalls();
432    await mediator.onWinscopeEvent(new TracePositionUpdate(POSITION_11));
433    checkTracePositionUpdateEvents(
434      [viewerStub0, viewerOverlay, timelineComponent, crossToolProtocol],
435      [],
436      POSITION_11,
437    );
438  });
439
440  it('propagates trace position update according to timezone', async () => {
441    const timezoneInfo: TimezoneInfo = {
442      timezone: 'Asia/Kolkata',
443      locale: 'en-US',
444    };
445    const converter = new TimestampConverter(timezoneInfo, 0n);
446    spyOn(tracePipeline, 'getTimestampConverter').and.returnValue(converter);
447    await loadFiles();
448    await loadTraceView();
449
450    // notify position
451    resetSpyCalls();
452    const expectedPosition = TracePosition.fromTimestamp(
453      converter.makeTimestampFromRealNs(10n),
454    );
455    await mediator.onWinscopeEvent(new TracePositionUpdate(expectedPosition));
456    checkTracePositionUpdateEvents(
457      [viewerStub0, viewerOverlay, timelineComponent, crossToolProtocol],
458      [],
459      expectedPosition,
460      POSITION_10,
461    );
462  });
463
464  it('propagates trace position update and updates timeline data', async () => {
465    await loadFiles();
466    await loadTraceView();
467
468    // notify position
469    resetSpyCalls();
470    const finalTimestampNs = timelineData.getFullTimeRange().to.getValueNs();
471    const timestamp =
472      TimestampConverterUtils.makeRealTimestamp(finalTimestampNs);
473    const position = TracePosition.fromTimestamp(timestamp);
474
475    await mediator.onWinscopeEvent(new TracePositionUpdate(position, true));
476    checkTracePositionUpdateEvents(
477      [viewerStub0, viewerOverlay, timelineComponent, crossToolProtocol],
478      [],
479      position,
480    );
481    expect(
482      assertDefined(timelineData.getCurrentPosition()).timestamp.getValueNs(),
483    ).toEqual(finalTimestampNs);
484  });
485
486  it("initializes viewers' trace position also when loaded traces have no valid timestamps", async () => {
487    const dumpFile = await getFixtureFile('traces/dump_WindowManager.pb');
488    await mediator.onWinscopeEvent(new AppFilesUploaded([dumpFile]));
489
490    resetSpyCalls();
491    await mediator.onWinscopeEvent(new AppTraceViewRequest());
492    checkLoadTraceViewEvents(uploadTracesComponent);
493    userNotifierChecker.expectNotified([]);
494  });
495
496  it('filters traces without visualization on loading viewers', async () => {
497    const fileWithoutVisualization = await getFixtureFile(
498      'traces/elapsed_and_real_timestamp/shell_transition_trace.pb',
499    );
500    await loadFiles();
501    await mediator.onWinscopeEvent(
502      new AppFilesUploaded([fileWithoutVisualization]),
503    );
504    await loadTraceView();
505  });
506
507  it('warns user if frame mapping fails', async () => {
508    const errorMsg = 'frame mapping failed';
509    spyOn(tracePipeline, 'buildTraces').and.throwError(errorMsg);
510    const dumpFile = await getFixtureFile('traces/dump_WindowManager.pb');
511    await mediator.onWinscopeEvent(new AppFilesUploaded([dumpFile]));
512
513    resetSpyCalls();
514    await mediator.onWinscopeEvent(new AppTraceViewRequest());
515    checkLoadTraceViewEvents(uploadTracesComponent, undefined, [
516      new IncompleteFrameMapping(errorMsg),
517    ]);
518  });
519
520  describe('timestamp received from remote tool', () => {
521    it('propagates trace position update', async () => {
522      tracePipeline.getTimestampConverter().setRealToMonotonicTimeOffsetNs(0n);
523      await loadFiles();
524      await loadTraceView();
525      const traceSfEntry = assertDefined(
526        tracePipeline.getTraces().getTrace(TraceType.SURFACE_FLINGER),
527      ).getEntry(2);
528
529      // receive timestamp
530      resetSpyCalls();
531      await mediator.onWinscopeEvent(
532        new RemoteToolTimestampReceived(() => traceSfEntry.getTimestamp()),
533      );
534
535      checkTracePositionUpdateEvents(
536        [viewerStub0, viewerOverlay, timelineComponent],
537        [],
538        TracePosition.fromTraceEntry(traceSfEntry),
539      );
540    });
541
542    it("doesn't propagate timestamp back to remote tool", async () => {
543      tracePipeline.getTimestampConverter().setRealToMonotonicTimeOffsetNs(0n);
544      await loadFiles();
545      await loadTraceView();
546
547      // receive timestamp
548      resetSpyCalls();
549      await mediator.onWinscopeEvent(
550        new RemoteToolTimestampReceived(() => TIMESTAMP_10),
551      );
552      checkTracePositionUpdateEvents(
553        [viewerStub0, viewerOverlay, timelineComponent],
554        [],
555      );
556    });
557
558    it('defers trace position propagation till traces are loaded and visualized', async () => {
559      // ensure converter has been used to create real timestamps
560      tracePipeline.getTimestampConverter().makeTimestampFromRealNs(0n);
561
562      // load files but do not load trace view
563      await loadFiles();
564      expect(timelineComponent.onWinscopeEvent).not.toHaveBeenCalled();
565      const traceSf = assertDefined(
566        tracePipeline.getTraces().getTrace(TraceType.SURFACE_FLINGER),
567      );
568
569      // keep timestamp for later
570      await mediator.onWinscopeEvent(
571        new RemoteToolTimestampReceived(() =>
572          traceSf.getEntry(1).getTimestamp(),
573        ),
574      );
575      expect(timelineComponent.onWinscopeEvent).not.toHaveBeenCalled();
576
577      // keep timestamp for later (replace previous one)
578      await mediator.onWinscopeEvent(
579        new RemoteToolTimestampReceived(() =>
580          traceSf.getEntry(2).getTimestamp(),
581        ),
582      );
583      expect(timelineComponent.onWinscopeEvent).not.toHaveBeenCalled();
584
585      // apply timestamp
586      await loadTraceView();
587
588      expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith(
589        makeExpectedTracePositionUpdate(
590          TracePosition.fromTraceEntry(traceSf.getEntry(2)),
591        ),
592      );
593    });
594  });
595
596  describe('tab view switches', () => {
597    it('forwards switch notifications', async () => {
598      await loadFiles();
599      await loadTraceView();
600      resetSpyCalls();
601
602      const view = viewerStub1.getViews()[0];
603      await mediator.onWinscopeEvent(new TabbedViewSwitched(view));
604      expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith(
605        new ActiveTraceChanged(view.traces[0]),
606      );
607      userNotifierChecker.expectNotified([]);
608      userNotifierChecker.reset();
609      const viewDump = viewerDump.getViews()[0];
610      await mediator.onWinscopeEvent(new TabbedViewSwitched(viewDump));
611      expect(timelineComponent.onWinscopeEvent).not.toHaveBeenCalledWith(
612        new ActiveTraceChanged(viewDump.traces[0]),
613      );
614      userNotifierChecker.expectNotified([]);
615    });
616
617    it('forwards switch requests from viewers to trace view component', async () => {
618      await loadFiles();
619      await loadTraceView();
620      expect(traceViewComponent.onWinscopeEvent).not.toHaveBeenCalled();
621
622      await viewerStub0.emitAppEventForTesting(
623        new TabbedViewSwitchRequest(traceSf),
624      );
625      expect(traceViewComponent.onWinscopeEvent).toHaveBeenCalledOnceWith(
626        new TabbedViewSwitchRequest(traceSf),
627      );
628      userNotifierChecker.expectNotified([]);
629    });
630  });
631
632  it('notifies only visible viewers about trace position updates', async () => {
633    await loadFiles();
634    await loadTraceView();
635
636    // Position update -> update only visible viewers
637    // Note: Viewer 0 is visible (gets focus) upon UI initialization
638    resetSpyCalls();
639    await mediator.onWinscopeEvent(new TracePositionUpdate(POSITION_10));
640    checkTracePositionUpdateEvents(
641      [viewerStub0, viewerOverlay, timelineComponent, crossToolProtocol],
642      [],
643      POSITION_10,
644    );
645
646    // Tab switch -> update only newly visible viewers
647    // Note: overlay viewer is considered always visible
648    resetSpyCalls();
649    await mediator.onWinscopeEvent(
650      new TabbedViewSwitched(viewerStub1.getViews()[0]),
651    );
652    userNotifierChecker.expectNone();
653    const tracePositionUpdate = makeExpectedTracePositionUpdate(undefined);
654    const activeTraceChanged = new ActiveTraceChanged(
655      viewerStub1.getViews()[0].traces[0],
656    );
657    expect(viewerStub0.onWinscopeEvent).toHaveBeenCalledOnceWith(
658      activeTraceChanged,
659    );
660    expect(viewerDump.onWinscopeEvent).toHaveBeenCalledOnceWith(
661      activeTraceChanged,
662    );
663
664    expect(viewerStub1.onWinscopeEvent).toHaveBeenCalledWith(
665      tracePositionUpdate,
666    );
667    expect(viewerStub1.onWinscopeEvent).toHaveBeenCalledWith(
668      activeTraceChanged,
669    );
670
671    expect(viewerOverlay.onWinscopeEvent).toHaveBeenCalledWith(
672      tracePositionUpdate,
673    );
674    expect(viewerOverlay.onWinscopeEvent).toHaveBeenCalledWith(
675      activeTraceChanged,
676    );
677
678    expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith(
679      tracePositionUpdate,
680    );
681    expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith(
682      activeTraceChanged,
683    );
684
685    expect(crossToolProtocol.onWinscopeEvent).toHaveBeenCalledOnceWith(
686      tracePositionUpdate,
687    );
688
689    // Position update -> update only visible viewers
690    // Note: overlay viewer is considered always visible
691    resetSpyCalls();
692    await mediator.onWinscopeEvent(new TracePositionUpdate(POSITION_10));
693    checkTracePositionUpdateEvents(
694      [viewerStub1, viewerOverlay, timelineComponent, crossToolProtocol],
695      [],
696    );
697  });
698
699  it('notifies timeline of dark mode toggle', async () => {
700    const event = new DarkModeToggled(true);
701    await mediator.onWinscopeEvent(event);
702    expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledOnceWith(event);
703  });
704
705  it('notifies timeline and viewers of active trace change if successful', async () => {
706    await loadFiles();
707    await loadTraceView();
708    resetSpyCalls();
709
710    await mediator.onWinscopeEvent(new ActiveTraceChanged(traceDump));
711    expect(timelineComponent.onWinscopeEvent).not.toHaveBeenCalled();
712    viewers.forEach((viewer) => {
713      expect(viewer.onWinscopeEvent).not.toHaveBeenCalled();
714    });
715
716    const activeTraceChanged = new ActiveTraceChanged(
717      viewerStub1.getViews()[0].traces[0],
718    );
719    await mediator.onWinscopeEvent(activeTraceChanged);
720    expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledOnceWith(
721      activeTraceChanged,
722    );
723    viewers.forEach((viewer) =>
724      expect(viewer.onWinscopeEvent).toHaveBeenCalledOnceWith(
725        activeTraceChanged,
726      ),
727    );
728  });
729
730  it('notifies user of no trace targets selected', async () => {
731    await mediator.onWinscopeEvent(new NoTraceTargetsSelectedEvent());
732    userNotifierChecker.expectNotified([new NoTraceTargetsSelected()]);
733  });
734
735  it('notifies correct viewer of filter preset requests', async () => {
736    await loadFiles();
737    await loadTraceView();
738    resetSpyCalls();
739
740    const saveRequest = new FilterPresetSaveRequest(
741      'test_preset',
742      TraceType.SURFACE_FLINGER,
743    );
744    await mediator.onWinscopeEvent(saveRequest);
745
746    const applyRequest = new FilterPresetApplyRequest(
747      'test_preset',
748      TraceType.WINDOW_MANAGER,
749    );
750    await mediator.onWinscopeEvent(applyRequest);
751
752    await mediator.onWinscopeEvent(
753      new FilterPresetSaveRequest('test_preset', TraceType.PROTO_LOG),
754    );
755    await mediator.onWinscopeEvent(
756      new FilterPresetApplyRequest('test_preset', TraceType.PROTO_LOG),
757    );
758
759    expect(viewerStub0.onWinscopeEvent).toHaveBeenCalledOnceWith(saveRequest);
760    expect(viewerStub1.onWinscopeEvent).toHaveBeenCalledOnceWith(applyRequest);
761  });
762
763  it('initializes trace search', async () => {
764    const searchViewer = await loadPerfettoFilesAndReturnSearchViewer();
765    const spy = spyOn(
766      TraceSearchInitializer,
767      'createSearchViews',
768    ).and.returnValue(Promise.resolve(['test']));
769    const initializeRequest = new InitializeTraceSearchRequest();
770    await mediator.onWinscopeEvent(initializeRequest);
771    expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith(
772      initializeRequest,
773    );
774    expect(spy).toHaveBeenCalledTimes(1);
775    const initializedEvent = new TraceSearchInitialized(['test']);
776    expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith(
777      initializedEvent,
778    );
779    expect(searchViewer.onWinscopeEvent).toHaveBeenCalledWith(initializedEvent);
780  });
781
782  it('handles trace search request for successful queries', async () => {
783    const searchViewer = await loadPerfettoFilesAndReturnSearchViewer();
784    await requestSearch('select ts from surfaceflinger_layers_snapshot');
785    checkNewSearchTracePropagation(searchViewer, true);
786    await requestSearch('select id from surfaceflinger_layers_snapshot');
787    checkNewSearchTracePropagation(searchViewer, false);
788  });
789
790  it('handles trace search request for unsuccessful query', async () => {
791    const searchViewer = await loadPerfettoFilesAndReturnSearchViewer();
792    await requestSearch('select * from fake_table');
793    expect(searchViewer.onWinscopeEvent).toHaveBeenCalledWith(
794      new TraceSearchFailed(),
795    );
796    expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith(
797      new TraceSearchCompleted(),
798    );
799  });
800
801  it('handles trace removal requests', async () => {
802    await loadPerfettoFilesAndReturnSearchViewer();
803    await requestSearch('select ts from surfaceflinger_layers_snapshot');
804    removeSearchTraceAndCheckPropagation(true);
805    await requestSearch('select id from surfaceflinger_layers_snapshot');
806    removeSearchTraceAndCheckPropagation(false);
807  });
808
809  async function loadFiles(
810    files = inputFiles,
811    viewersToReassignTraces = [viewerStub0, viewerStub1],
812  ) {
813    for (const file of files) {
814      await mediator.onWinscopeEvent(new AppFilesUploaded([file]));
815    }
816    userNotifierChecker.expectNone();
817    viewersToReassignTraces.forEach((viewer) =>
818      reassignViewerStubTrace(viewer),
819    );
820  }
821
822  function reassignViewerStubTrace(viewerStub: ViewerStub) {
823    const viewerStubTraces = viewerStub.getViews()[0].traces;
824    viewerStubTraces[0] = tracePipeline
825      .getTraces()
826      .getTrace(viewerStubTraces[0].type) as Trace<object>;
827  }
828
829  async function loadTraceView(expectedViewers = viewers) {
830    // Simulate "View traces" button click
831    resetSpyCalls();
832    await mediator.onWinscopeEvent(new AppTraceViewRequest());
833
834    checkLoadTraceViewEvents(uploadTracesComponent, expectedViewers);
835
836    // Simulate notification of TraceViewComponent about initially selected/focused tab
837    resetSpyCalls();
838    await mediator.onWinscopeEvent(
839      new TabbedViewSwitched(viewerStub0.getViews()[0]),
840    );
841
842    expect(viewerStub0.onWinscopeEvent).toHaveBeenCalledOnceWith(
843      makeExpectedTracePositionUpdate(),
844    );
845    expect(viewerStub1.onWinscopeEvent).not.toHaveBeenCalled();
846    userNotifierChecker.expectNotified([]);
847  }
848
849  function checkLoadTraceViewEvents(
850    progressListener: ProgressListener,
851    expectedViewers = viewers,
852    notifications: UserWarning[] = [],
853  ) {
854    expect(progressListener.onProgressUpdate).toHaveBeenCalled();
855    expect(progressListener.onOperationFinished).toHaveBeenCalled();
856    expect(timelineData.initialize).toHaveBeenCalledTimes(1);
857    expect(appComponent.onWinscopeEvent).toHaveBeenCalledOnceWith(
858      new ViewersLoaded(expectedViewers),
859    );
860
861    // Mediator triggers the viewers initialization
862    // by sending them a "trace position update" event
863    checkTracePositionUpdateEvents(
864      (expectedViewers as WinscopeEventListener[]).concat([timelineComponent]),
865      notifications,
866    );
867  }
868
869  function checkUploadTracesComponentTraceViewEvents() {
870    const uploadTracesSpy =
871      uploadTracesComponent.onWinscopeEvent as jasmine.Spy;
872    expect(uploadTracesSpy.calls.allArgs()).toEqual([
873      [new AppTraceViewRequest()],
874      [new AppTraceViewRequestHandled()],
875    ]);
876  }
877
878  function checkTracePositionUpdateEvents(
879    listenersToBeNotified: WinscopeEventListener[],
880    userNotifications: UserWarning[],
881    position?: TracePosition,
882    crossToolProtocolPosition = position,
883  ) {
884    userNotifierChecker.expectNotified(userNotifications);
885    const event = makeExpectedTracePositionUpdate(position);
886    const crossToolProtocolEvent =
887      crossToolProtocolPosition !== position
888        ? makeExpectedTracePositionUpdate(crossToolProtocolPosition)
889        : event;
890    tracePositionUpdateListeners.forEach((listener) => {
891      const isVisible = listenersToBeNotified.includes(listener);
892      if (isVisible) {
893        const expected =
894          listener === crossToolProtocol ? crossToolProtocolEvent : event;
895        expect(listener.onWinscopeEvent).toHaveBeenCalledOnceWith(expected);
896      } else {
897        expect(listener.onWinscopeEvent).not.toHaveBeenCalled();
898      }
899    });
900  }
901
902  function resetSpyCalls() {
903    spies.forEach((spy) => {
904      spy.calls.reset();
905    });
906    userNotifierChecker.reset();
907  }
908
909  async function loadPerfettoFilesAndReturnSearchViewer(): Promise<ViewerStub> {
910    await loadFiles([perfettoFile], [viewerStub0]);
911    const searchViewer = new ViewerStub(
912      'search',
913      undefined,
914      undefined,
915      ViewType.GLOBAL_SEARCH,
916    );
917    spyOn(searchViewer, 'onWinscopeEvent');
918    const expectedViewers = [viewerStub0, searchViewer];
919    createViewersSpy.and.returnValue(expectedViewers);
920    await loadTraceView(expectedViewers);
921    resetSpyCalls();
922    return searchViewer;
923  }
924
925  async function requestSearch(query: string) {
926    const event = new TraceSearchRequest(query);
927    await mediator.onWinscopeEvent(event);
928    expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith(event);
929  }
930
931  function checkNewSearchTracePropagation(
932    searchViewer: ViewerStub,
933    hasTimestamps: boolean,
934  ) {
935    const searchTraces = tracePipeline.getTraces().getTraces(TraceType.SEARCH);
936    const newTrace = searchTraces[searchTraces.length - 1];
937    const newTraceEvent = new TraceAddRequest(newTrace);
938    expect(searchViewer.onWinscopeEvent).toHaveBeenCalledWith(newTraceEvent);
939    expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith(
940      new TraceSearchCompleted(),
941    );
942    expect(timelineData.hasTrace(newTrace)).toEqual(hasTimestamps);
943    const timelineComponentSpy = timelineComponent.onWinscopeEvent;
944    if (hasTimestamps) {
945      expect(timelineComponentSpy).toHaveBeenCalledWith(newTraceEvent);
946    } else {
947      expect(timelineComponentSpy).not.toHaveBeenCalledWith(newTraceEvent);
948    }
949  }
950
951  async function removeSearchTraceAndCheckPropagation(hasTimestamps: boolean) {
952    const searchTraces = tracePipeline.getTraces().getTraces(TraceType.SEARCH);
953    const newTrace = searchTraces[searchTraces.length - 1];
954    const removalRequest = new TraceRemoveRequest(newTrace);
955    await mediator.onWinscopeEvent(removalRequest);
956    expect(tracePipeline.getTraces().hasTrace(newTrace)).toBeFalse();
957    expect(timelineData.hasTrace(newTrace)).toBeFalse();
958    const timelineComponentSpy = timelineComponent.onWinscopeEvent;
959    if (hasTimestamps) {
960      expect(timelineComponentSpy).toHaveBeenCalledWith(removalRequest);
961    } else {
962      expect(timelineComponentSpy).not.toHaveBeenCalledWith(removalRequest);
963    }
964  }
965
966  function makeExpectedTracePositionUpdate(
967    tracePosition?: TracePosition,
968  ): WinscopeEvent {
969    if (tracePosition !== undefined) {
970      return new TracePositionUpdate(tracePosition);
971    }
972    return {type: WinscopeEventType.TRACE_POSITION_UPDATE} as WinscopeEvent;
973  }
974
975  function tracePositionUpdateEqualityTester(
976    first: any,
977    second: any,
978  ): boolean | undefined {
979    if (
980      first instanceof TracePositionUpdate &&
981      second instanceof TracePositionUpdate
982    ) {
983      return testTracePositionUpdates(first, second);
984    }
985    if (
986      first instanceof TracePositionUpdate &&
987      second.type === WinscopeEventType.TRACE_POSITION_UPDATE
988    ) {
989      return first.type === second.type;
990    }
991    return undefined;
992  }
993
994  function testTracePositionUpdates(
995    event: TracePositionUpdate,
996    expectedEvent: TracePositionUpdate,
997  ): boolean {
998    if (event.type !== expectedEvent.type) return false;
999    if (
1000      event.position.timestamp.getValueNs() !==
1001      expectedEvent.position.timestamp.getValueNs()
1002    ) {
1003      return false;
1004    }
1005    if (event.position.frame !== expectedEvent.position.frame) return false;
1006    return true;
1007  }
1008});
1009