xref: /aosp_15_r20/development/tools/winscope/src/app/mediator.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 {Store} from 'common/store';
19import {Timestamp} from 'common/time';
20import {TimeUtils} from 'common/time_utils';
21import {UserNotifier} from 'common/user_notifier';
22import {CrossToolProtocol} from 'cross_tool/cross_tool_protocol';
23import {Analytics} from 'logging/analytics';
24import {ProgressListener} from 'messaging/progress_listener';
25import {UserWarning} from 'messaging/user_warning';
26import {
27  CannotVisualizeTraceEntry,
28  FailedToInitializeTimelineData,
29  IncompleteFrameMapping,
30  NoTraceTargetsSelected,
31  NoValidFiles,
32} from 'messaging/user_warnings';
33import {
34  ActiveTraceChanged,
35  ExpandedTimelineToggled,
36  TraceAddRequest,
37  TracePositionUpdate,
38  TraceSearchCompleted,
39  TraceSearchFailed,
40  TraceSearchInitialized,
41  ViewersLoaded,
42  ViewersUnloaded,
43  WinscopeEvent,
44  WinscopeEventType,
45} from 'messaging/winscope_event';
46import {WinscopeEventEmitter} from 'messaging/winscope_event_emitter';
47import {WinscopeEventListener} from 'messaging/winscope_event_listener';
48import {TraceEntry} from 'trace/trace';
49import {TRACE_INFO} from 'trace/trace_info';
50import {TracePosition} from 'trace/trace_position';
51import {TraceType} from 'trace/trace_type';
52import {RequestedTraceTypes} from 'trace_collection/adb_files';
53import {View, Viewer, ViewType} from 'viewers/viewer';
54import {ViewerFactory} from 'viewers/viewer_factory';
55import {FilesSource} from './files_source';
56import {TimelineData} from './timeline_data';
57import {TracePipeline} from './trace_pipeline';
58import {TraceSearchInitializer} from './trace_search/trace_search_initializer';
59
60export class Mediator {
61  private abtChromeExtensionProtocol: WinscopeEventEmitter &
62    WinscopeEventListener;
63  private crossToolProtocol: CrossToolProtocol;
64  private uploadTracesComponent?: ProgressListener;
65  private collectTracesComponent?: ProgressListener &
66    WinscopeEventEmitter &
67    WinscopeEventListener;
68  private traceViewComponent?: WinscopeEventEmitter & WinscopeEventListener;
69  private timelineComponent?: WinscopeEventEmitter & WinscopeEventListener;
70  private appComponent: WinscopeEventListener;
71  private storage: Store;
72
73  private tracePipeline: TracePipeline;
74  private timelineData: TimelineData;
75  private viewers: Viewer[] = [];
76  private focusedTabView: undefined | View;
77  private areViewersLoaded = false;
78  private lastRemoteToolDeferredTimestampReceived?: () => Timestamp | undefined;
79  private currentProgressListener?: ProgressListener;
80
81  constructor(
82    tracePipeline: TracePipeline,
83    timelineData: TimelineData,
84    abtChromeExtensionProtocol: WinscopeEventEmitter & WinscopeEventListener,
85    crossToolProtocol: CrossToolProtocol,
86    appComponent: WinscopeEventListener,
87    storage: Store,
88  ) {
89    this.tracePipeline = tracePipeline;
90    this.timelineData = timelineData;
91    this.abtChromeExtensionProtocol = abtChromeExtensionProtocol;
92    this.crossToolProtocol = crossToolProtocol;
93    this.appComponent = appComponent;
94    this.storage = storage;
95
96    this.crossToolProtocol.setEmitEvent(async (event) => {
97      await this.onWinscopeEvent(event);
98    });
99
100    this.abtChromeExtensionProtocol.setEmitEvent(async (event) => {
101      await this.onWinscopeEvent(event);
102    });
103  }
104
105  setUploadTracesComponent(component: ProgressListener | undefined) {
106    this.uploadTracesComponent = component;
107  }
108
109  setCollectTracesComponent(
110    component:
111      | (ProgressListener & WinscopeEventEmitter & WinscopeEventListener)
112      | undefined,
113  ) {
114    this.collectTracesComponent = component;
115    this.collectTracesComponent?.setEmitEvent(async (event) => {
116      await this.onWinscopeEvent(event);
117    });
118  }
119
120  setTraceViewComponent(
121    component: (WinscopeEventEmitter & WinscopeEventListener) | undefined,
122  ) {
123    this.traceViewComponent = component;
124    this.traceViewComponent?.setEmitEvent(async (event) => {
125      await this.onWinscopeEvent(event);
126    });
127  }
128
129  setTimelineComponent(
130    component: (WinscopeEventEmitter & WinscopeEventListener) | undefined,
131  ) {
132    this.timelineComponent = component;
133    this.timelineComponent?.setEmitEvent(async (event) => {
134      await this.onWinscopeEvent(event);
135    });
136  }
137
138  async onWinscopeEvent(event: WinscopeEvent) {
139    await event.visit(WinscopeEventType.APP_INITIALIZED, async (event) => {
140      await this.abtChromeExtensionProtocol.onWinscopeEvent(event);
141    });
142
143    await event.visit(WinscopeEventType.APP_FILES_UPLOADED, async (event) => {
144      this.currentProgressListener = this.uploadTracesComponent;
145      await this.loadFiles(event.files, FilesSource.UPLOADED);
146      UserNotifier.notify();
147    });
148
149    await event.visit(WinscopeEventType.APP_FILES_COLLECTED, async (event) => {
150      this.currentProgressListener = this.collectTracesComponent;
151      if (event.files.collected.length > 0) {
152        await this.loadFiles(event.files.collected, FilesSource.COLLECTED);
153        const traces = this.tracePipeline.getTraces();
154        if (traces.getSize() > 0) {
155          const failedTraces: string[] = [];
156          event.files.requested.forEach((requested: RequestedTraceTypes) => {
157            if (
158              !requested.types.some((type) => traces.getTraces(type).length > 0)
159            ) {
160              failedTraces.push(requested.name);
161            }
162          });
163          if (failedTraces.length > 0) {
164            UserNotifier.add(new NoValidFiles(failedTraces));
165          }
166          await this.loadViewers();
167        } else {
168          this.currentProgressListener?.onOperationFinished(false);
169        }
170      } else {
171        UserNotifier.add(new NoValidFiles());
172      }
173      UserNotifier.notify();
174    });
175
176    await event.visit(WinscopeEventType.APP_RESET_REQUEST, async () => {
177      await this.resetAppToInitialState();
178    });
179
180    await event.visit(
181      WinscopeEventType.APP_REFRESH_DUMPS_REQUEST,
182      async (event) => {
183        await this.resetAppToInitialState();
184        await this.collectTracesComponent?.onWinscopeEvent(event);
185      },
186    );
187
188    await event.visit(WinscopeEventType.APP_TRACE_VIEW_REQUEST, async () => {
189      await this.loadViewers();
190      UserNotifier.notify();
191    });
192
193    await event.visit(
194      WinscopeEventType.REMOTE_TOOL_DOWNLOAD_START,
195      async () => {
196        Analytics.Tracing.logOpenFromABT();
197        await this.resetAppToInitialState();
198        this.currentProgressListener = this.uploadTracesComponent;
199        this.currentProgressListener?.onProgressUpdate(
200          'Downloading files...',
201          undefined,
202        );
203      },
204    );
205
206    await event.visit(
207      WinscopeEventType.REMOTE_TOOL_FILES_RECEIVED,
208      async (event) => {
209        await this.processRemoteFilesReceived(
210          event.files,
211          FilesSource.REMOTE_TOOL,
212        );
213        if (event.deferredTimestamp) {
214          await this.processRemoteToolDeferredTimestampReceived(
215            event.deferredTimestamp,
216          );
217        }
218      },
219    );
220
221    await event.visit(
222      WinscopeEventType.REMOTE_TOOL_TIMESTAMP_RECEIVED,
223      async (event) => {
224        await this.processRemoteToolDeferredTimestampReceived(
225          event.deferredTimestamp,
226        );
227      },
228    );
229
230    await event.visit(
231      WinscopeEventType.TABBED_VIEW_SWITCH_REQUEST,
232      async (event) => {
233        await this.traceViewComponent?.onWinscopeEvent(event);
234      },
235    );
236
237    await event.visit(WinscopeEventType.TABBED_VIEW_SWITCHED, async (event) => {
238      const newActiveTrace = event.newFocusedView.traces[0];
239      if (this.timelineData.trySetActiveTrace(newActiveTrace)) {
240        const activeTraceChanged = new ActiveTraceChanged(newActiveTrace);
241        await this.timelineComponent?.onWinscopeEvent(activeTraceChanged);
242        for (const viewer of this.viewers) {
243          await viewer.onWinscopeEvent(activeTraceChanged);
244        }
245      }
246      this.focusedTabView = event.newFocusedView;
247      await this.propagateTracePosition(
248        this.timelineData.getCurrentPosition(),
249        false,
250      );
251      UserNotifier.notify();
252    });
253
254    await event.visit(
255      WinscopeEventType.TRACE_POSITION_UPDATE,
256      async (event) => {
257        if (event.updateTimeline) {
258          this.timelineData.setPosition(event.position);
259        }
260        await this.propagateTracePosition(event.position, false);
261        UserNotifier.notify();
262      },
263    );
264
265    await event.visit(
266      WinscopeEventType.EXPANDED_TIMELINE_TOGGLED,
267      async (event) => {
268        await this.propagateToOverlays(event);
269      },
270    );
271
272    await event.visit(WinscopeEventType.ACTIVE_TRACE_CHANGED, async (event) => {
273      this.timelineData.trySetActiveTrace(event.trace);
274      for (const viewer of this.viewers) {
275        await viewer.onWinscopeEvent(event);
276      }
277      await this.timelineComponent?.onWinscopeEvent(event);
278    });
279
280    await event.visit(WinscopeEventType.DARK_MODE_TOGGLED, async (event) => {
281      await this.timelineComponent?.onWinscopeEvent(event);
282      for (const viewer of this.viewers) {
283        await viewer.onWinscopeEvent(event);
284      }
285    });
286
287    await event.visit(
288      WinscopeEventType.NO_TRACE_TARGETS_SELECTED,
289      async (event) => {
290        UserNotifier.add(new NoTraceTargetsSelected()).notify();
291      },
292    );
293
294    await event.visit(
295      WinscopeEventType.FILTER_PRESET_SAVE_REQUEST,
296      async (event) => {
297        await this.findViewerByType(event.traceType)?.onWinscopeEvent(event);
298      },
299    );
300
301    await event.visit(
302      WinscopeEventType.FILTER_PRESET_APPLY_REQUEST,
303      async (event) => {
304        await this.findViewerByType(event.traceType)?.onWinscopeEvent(event);
305      },
306    );
307
308    await event.visit(WinscopeEventType.TRACE_SEARCH_REQUEST, async (event) => {
309      await this.timelineComponent?.onWinscopeEvent(event);
310      const searchViewer = this.viewers.find(
311        (viewer) => viewer.getViews()[0].type === ViewType.GLOBAL_SEARCH,
312      );
313      const trace = await this.tracePipeline.tryCreateSearchTrace(event.query);
314      this.timelineComponent?.onWinscopeEvent(new TraceSearchCompleted());
315      if (!trace) {
316        await searchViewer?.onWinscopeEvent(new TraceSearchFailed());
317        return;
318      }
319      const newSearchTrace = new TraceAddRequest(trace);
320      await searchViewer?.onWinscopeEvent(newSearchTrace);
321      if (trace.lengthEntries > 0 && !trace.isDumpWithoutTimestamp()) {
322        assertDefined(this.timelineData).getTraces().addTrace(trace);
323        await this.timelineComponent?.onWinscopeEvent(newSearchTrace);
324      }
325    });
326
327    await event.visit(WinscopeEventType.TRACE_REMOVE_REQUEST, async (event) => {
328      this.tracePipeline.getTraces().deleteTrace(event.trace);
329      if (this.timelineData.hasTrace(event.trace)) {
330        this.timelineData.getTraces().deleteTrace(event.trace);
331        await this.timelineComponent?.onWinscopeEvent(event);
332      }
333    });
334
335    await event.visit(
336      WinscopeEventType.INITIALIZE_TRACE_SEARCH_REQUEST,
337      async (event) => {
338        await this.timelineComponent?.onWinscopeEvent(event);
339        const traces = this.tracePipeline.getTraces();
340        const views = await TraceSearchInitializer.createSearchViews(traces);
341        const searchViewer = this.viewers.find(
342          (viewer) => viewer.getViews()[0].type === ViewType.GLOBAL_SEARCH,
343        );
344        const initializedEvent = new TraceSearchInitialized(views);
345        await searchViewer?.onWinscopeEvent(initializedEvent);
346        await this.timelineComponent?.onWinscopeEvent(initializedEvent);
347      },
348    );
349  }
350
351  private async loadFiles(files: File[], source: FilesSource) {
352    await this.tracePipeline.loadFiles(
353      files,
354      source,
355      this.currentProgressListener,
356    );
357  }
358
359  private async propagateTracePosition(
360    position: TracePosition | undefined,
361    omitCrossToolProtocol: boolean,
362  ) {
363    if (!position) {
364      return;
365    }
366
367    const event = new TracePositionUpdate(position);
368    const viewers: Viewer[] = [...this.viewers].filter((viewer) =>
369      this.isViewerVisible(viewer),
370    );
371
372    const warnings: UserWarning[] = [];
373
374    for (const viewer of viewers) {
375      try {
376        await viewer.onWinscopeEvent(event);
377      } catch (e) {
378        const traceType = assertDefined(viewer.getTraces().at(0)?.type);
379        warnings.push(
380          new CannotVisualizeTraceEntry(
381            `Cannot parse entry for ${TRACE_INFO[traceType].name} trace: Trace may be corrupted.`,
382          ),
383        );
384      }
385    }
386
387    if (this.timelineComponent) {
388      await this.timelineComponent.onWinscopeEvent(event);
389    }
390
391    if (!omitCrossToolProtocol) {
392      await this.crossToolProtocol.onWinscopeEvent(event);
393    }
394
395    if (warnings.length > 0) {
396      warnings.forEach((w) => UserNotifier.add(w));
397    }
398  }
399
400  private isViewerVisible(viewer: Viewer): boolean {
401    if (!this.focusedTabView) {
402      // During initialization no tab is focused.
403      // Let's just consider all viewers as visible and to be updated.
404      return true;
405    }
406
407    return viewer.getViews().some((view) => {
408      if (view === this.focusedTabView) {
409        return true;
410      }
411      if (view.type === ViewType.OVERLAY) {
412        // Nice to have: update viewer only if overlay view is actually visible (not minimized)
413        return true;
414      }
415      return false;
416    });
417  }
418
419  private async processRemoteToolDeferredTimestampReceived(
420    deferredTimestamp: () => Timestamp | undefined,
421  ) {
422    this.lastRemoteToolDeferredTimestampReceived = deferredTimestamp;
423
424    if (!this.areViewersLoaded) {
425      return; // apply timestamp later when traces are visualized
426    }
427
428    const timestamp = deferredTimestamp();
429    if (!timestamp) {
430      return;
431    }
432
433    const position = this.timelineData.makePositionFromActiveTrace(timestamp);
434    this.timelineData.setPosition(position);
435
436    await this.propagateTracePosition(
437      this.timelineData.getCurrentPosition(),
438      true,
439    );
440    UserNotifier.notify();
441  }
442
443  private async processRemoteFilesReceived(files: File[], source: FilesSource) {
444    await this.resetAppToInitialState();
445    this.currentProgressListener = this.uploadTracesComponent;
446    await this.loadFiles(files, source);
447    UserNotifier.notify();
448  }
449
450  private async loadViewers() {
451    this.currentProgressListener?.onProgressUpdate(
452      'Computing frame mapping...',
453      undefined,
454    );
455
456    // TODO: move this into the ProgressListener
457    // allow the UI to update before making the main thread very busy
458    await TimeUtils.sleepMs(10);
459
460    this.tracePipeline.filterTracesWithoutVisualization();
461    if (this.tracePipeline.getTraces().getSize() === 0) {
462      this.currentProgressListener?.onOperationFinished(false);
463      return;
464    }
465
466    try {
467      await this.tracePipeline.buildTraces();
468      this.currentProgressListener?.onOperationFinished(true);
469    } catch (e) {
470      UserNotifier.add(new IncompleteFrameMapping((e as Error).message));
471      this.currentProgressListener?.onOperationFinished(false);
472    }
473
474    this.currentProgressListener?.onProgressUpdate(
475      'Initializing UI...',
476      undefined,
477    );
478
479    // TODO: move this into the ProgressListener
480    // allow the UI to update before making the main thread very busy
481    await TimeUtils.sleepMs(10);
482
483    try {
484      await this.timelineData.initialize(
485        this.tracePipeline.getTraces(),
486        await this.tracePipeline.getScreenRecordingVideo(),
487        this.tracePipeline.getTimestampConverter(),
488      );
489    } catch {
490      this.currentProgressListener?.onOperationFinished(false);
491      UserNotifier.add(new FailedToInitializeTimelineData());
492      return;
493    }
494
495    this.viewers = new ViewerFactory().createViewers(
496      this.tracePipeline.getTraces(),
497      this.storage,
498    );
499    this.viewers.forEach((viewer) =>
500      viewer.setEmitEvent(async (event) => {
501        await this.onWinscopeEvent(event);
502      }),
503    );
504
505    // Set initial trace position as soon as UI is created
506    const initialPosition = this.getInitialTracePosition();
507    this.timelineData.setPosition(initialPosition);
508
509    // Make sure all viewers are initialized and have performed the heavy pre-processing they need
510    // at this stage, while the "initializing UI" progress message is still being displayed.
511    // The viewers initialization is triggered by sending them a "trace position update".
512    await this.propagateTracePosition(initialPosition, true);
513
514    this.focusedTabView = this.viewers
515      .find((v) => v.getViews()[0].type === ViewType.TRACE_TAB)
516      ?.getViews()[0];
517    this.areViewersLoaded = true;
518
519    // Notify app component (i.e. render viewers), only after all viewers have been initialized
520    // (see above).
521    //
522    // Notifying the app component first could result in this kind of interleaved execution:
523    // 1. Mediator notifies app component
524    //    1.1. App component renders UI components
525    //    1.2. Mediator receives back a "view switched" event
526    //    1.2. Mediator sends "trace position update" to viewers
527    // 2. Mediator sends "trace position update" to viewers to initialize them (see above)
528    //
529    // and because our data load operations are async and involve task suspensions, the two
530    // "trace position update" could be processed concurrently within the same viewer.
531    // Meaning the viewer could perform twice the initial heavy pre-processing,
532    // thus increasing UI initialization times.
533    await this.appComponent.onWinscopeEvent(new ViewersLoaded(this.viewers));
534  }
535
536  private getInitialTracePosition(): TracePosition | undefined {
537    if (this.lastRemoteToolDeferredTimestampReceived) {
538      const lastRemoteToolTimestamp =
539        this.lastRemoteToolDeferredTimestampReceived();
540      if (lastRemoteToolTimestamp) {
541        return this.timelineData.makePositionFromActiveTrace(
542          lastRemoteToolTimestamp,
543        );
544      }
545    }
546
547    const position = this.timelineData.getCurrentPosition();
548    if (position) {
549      return position;
550    }
551
552    // TimelineData might not provide a TracePosition because all the loaded traces are
553    // dumps with invalid timestamps (value zero). In this case let's create a TracePosition
554    // out of any entry from the loaded traces (if available).
555    const firstEntries = this.tracePipeline
556      .getTraces()
557      .mapTrace((trace) => {
558        if (trace.lengthEntries > 0) {
559          return trace.getEntry(0);
560        }
561        return undefined;
562      })
563      .filter((entry) => {
564        return entry !== undefined;
565      }) as Array<TraceEntry<object>>;
566
567    if (firstEntries.length > 0) {
568      return TracePosition.fromTraceEntry(firstEntries[0]);
569    }
570
571    return undefined;
572  }
573
574  private async resetAppToInitialState() {
575    this.tracePipeline.clear();
576    this.timelineData.clear();
577    this.viewers = [];
578    this.areViewersLoaded = false;
579    this.lastRemoteToolDeferredTimestampReceived = undefined;
580    this.focusedTabView = undefined;
581    await this.appComponent.onWinscopeEvent(new ViewersUnloaded());
582  }
583
584  private async propagateToOverlays(event: ExpandedTimelineToggled) {
585    const overlayViewers = this.viewers.filter((viewer) =>
586      viewer.getViews().some((view) => view.type === ViewType.OVERLAY),
587    );
588    for (const overlay of overlayViewers) {
589      await overlay.onWinscopeEvent(event);
590    }
591  }
592
593  private findViewerByType(type: TraceType): Viewer | undefined {
594    return this.viewers.find((viewer) => viewer.getTraces()[0].type === type);
595  }
596}
597