• 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 {FileUtils, OnFile} from 'common/file_utils';
18import {BuganizerAttachmentsDownloadEmitter} from 'interfaces/buganizer_attachments_download_emitter';
19import {ProgressListener} from 'interfaces/progress_listener';
20import {RemoteBugreportReceiver} from 'interfaces/remote_bugreport_receiver';
21import {RemoteTimestampReceiver} from 'interfaces/remote_timestamp_receiver';
22import {RemoteTimestampSender} from 'interfaces/remote_timestamp_sender';
23import {Runnable} from 'interfaces/runnable';
24import {TraceDataListener} from 'interfaces/trace_data_listener';
25import {TracePositionUpdateEmitter} from 'interfaces/trace_position_update_emitter';
26import {TracePositionUpdateListener} from 'interfaces/trace_position_update_listener';
27import {UserNotificationListener} from 'interfaces/user_notification_listener';
28import {Timestamp, TimestampType} from 'trace/timestamp';
29import {TraceFile} from 'trace/trace_file';
30import {TracePosition} from 'trace/trace_position';
31import {TraceType} from 'trace/trace_type';
32import {View, Viewer} from 'viewers/viewer';
33import {ViewerFactory} from 'viewers/viewer_factory';
34import {TimelineData} from './timeline_data';
35import {TracePipeline} from './trace_pipeline';
36
37type TimelineComponentInterface = TracePositionUpdateListener & TracePositionUpdateEmitter;
38type CrossToolProtocolInterface = RemoteBugreportReceiver &
39  RemoteTimestampReceiver &
40  RemoteTimestampSender;
41type AbtChromeExtensionProtocolInterface = BuganizerAttachmentsDownloadEmitter & Runnable;
42
43export class Mediator {
44  private abtChromeExtensionProtocol: AbtChromeExtensionProtocolInterface;
45  private crossToolProtocol: CrossToolProtocolInterface;
46  private uploadTracesComponent?: ProgressListener;
47  private collectTracesComponent?: ProgressListener;
48  private timelineComponent?: TimelineComponentInterface;
49  private appComponent: TraceDataListener;
50  private userNotificationListener: UserNotificationListener;
51  private storage: Storage;
52
53  private tracePipeline: TracePipeline;
54  private timelineData: TimelineData;
55  private viewers: Viewer[] = [];
56  private isChangingCurrentTimestamp = false;
57  private isTraceDataVisualized = false;
58  private lastRemoteToolTimestampReceived: Timestamp | undefined;
59  private currentProgressListener?: ProgressListener;
60
61  constructor(
62    tracePipeline: TracePipeline,
63    timelineData: TimelineData,
64    abtChromeExtensionProtocol: AbtChromeExtensionProtocolInterface,
65    crossToolProtocol: CrossToolProtocolInterface,
66    appComponent: TraceDataListener,
67    userNotificationListener: UserNotificationListener,
68    storage: Storage
69  ) {
70    this.tracePipeline = tracePipeline;
71    this.timelineData = timelineData;
72    this.abtChromeExtensionProtocol = abtChromeExtensionProtocol;
73    this.crossToolProtocol = crossToolProtocol;
74    this.appComponent = appComponent;
75    this.userNotificationListener = userNotificationListener;
76    this.storage = storage;
77
78    this.crossToolProtocol.setOnBugreportReceived(
79      async (bugreport: File, timestamp?: Timestamp) => {
80        await this.onRemoteBugreportReceived(bugreport, timestamp);
81      }
82    );
83
84    this.crossToolProtocol.setOnTimestampReceived(async (timestamp: Timestamp) => {
85      await this.onRemoteTimestampReceived(timestamp);
86    });
87
88    this.abtChromeExtensionProtocol.setOnBuganizerAttachmentsDownloadStart(() => {
89      this.onBuganizerAttachmentsDownloadStart();
90    });
91
92    this.abtChromeExtensionProtocol.setOnBuganizerAttachmentsDownloaded(
93      async (attachments: File[]) => {
94        await this.onBuganizerAttachmentsDownloaded(attachments);
95      }
96    );
97  }
98
99  setUploadTracesComponent(uploadTracesComponent: ProgressListener | undefined) {
100    this.uploadTracesComponent = uploadTracesComponent;
101  }
102
103  setCollectTracesComponent(collectTracesComponent: ProgressListener | undefined) {
104    this.collectTracesComponent = collectTracesComponent;
105  }
106
107  setTimelineComponent(timelineComponent: TimelineComponentInterface | undefined) {
108    this.timelineComponent = timelineComponent;
109    this.timelineComponent?.setOnTracePositionUpdate(async (position) => {
110      await this.onTimelineTracePositionUpdate(position);
111    });
112  }
113
114  onWinscopeInitialized() {
115    this.abtChromeExtensionProtocol.run();
116  }
117
118  onWinscopeUploadNew() {
119    this.resetAppToInitialState();
120  }
121
122  async onWinscopeFilesUploaded(files: File[]) {
123    this.currentProgressListener = this.uploadTracesComponent;
124    await this.processFiles(files);
125  }
126
127  async onWinscopeFilesCollected(files: File[]) {
128    this.currentProgressListener = this.collectTracesComponent;
129    await this.processFiles(files);
130    await this.processLoadedTraceFiles();
131  }
132
133  async onWinscopeViewTracesRequest() {
134    await this.processLoadedTraceFiles();
135  }
136
137  async onWinscopeActiveViewChanged(view: View) {
138    this.timelineData.setActiveViewTraceTypes(view.dependencies);
139    await this.propagateTracePosition(this.timelineData.getCurrentPosition());
140  }
141
142  async onTimelineTracePositionUpdate(position: TracePosition) {
143    await this.propagateTracePosition(position);
144  }
145
146  private async propagateTracePosition(
147    position?: TracePosition,
148    omitCrossToolProtocol: boolean = false
149  ) {
150    if (!position) {
151      return;
152    }
153
154    //TODO (b/289478304): update only visible viewers (1 tab viewer + overlay viewers)
155    const promises = this.viewers.map((viewer) => {
156      return viewer.onTracePositionUpdate(position);
157    });
158    await Promise.all(promises);
159
160    this.timelineComponent?.onTracePositionUpdate(position);
161
162    if (omitCrossToolProtocol) {
163      return;
164    }
165
166    const timestamp = position.timestamp;
167    if (timestamp.getType() !== TimestampType.REAL) {
168      console.warn(
169        'Cannot propagate timestamp change to remote tool.' +
170          ` Remote tool expects timestamp type ${TimestampType.REAL},` +
171          ` but Winscope wants to notify timestamp type ${timestamp.getType()}.`
172      );
173      return;
174    }
175
176    this.crossToolProtocol.sendTimestamp(timestamp);
177  }
178
179  private onBuganizerAttachmentsDownloadStart() {
180    this.resetAppToInitialState();
181    this.currentProgressListener = this.uploadTracesComponent;
182    this.currentProgressListener?.onProgressUpdate('Downloading files...', undefined);
183  }
184
185  private async onBuganizerAttachmentsDownloaded(attachments: File[]) {
186    this.currentProgressListener = this.uploadTracesComponent;
187    await this.processRemoteFilesReceived(attachments);
188  }
189
190  private async onRemoteBugreportReceived(bugreport: File, timestamp?: Timestamp) {
191    this.currentProgressListener = this.uploadTracesComponent;
192    await this.processRemoteFilesReceived([bugreport]);
193    if (timestamp !== undefined) {
194      await this.onRemoteTimestampReceived(timestamp);
195    }
196  }
197
198  private async onRemoteTimestampReceived(timestamp: Timestamp) {
199    this.lastRemoteToolTimestampReceived = timestamp;
200
201    if (!this.isTraceDataVisualized) {
202      return; // apply timestamp later when traces are visualized
203    }
204
205    if (this.timelineData.getTimestampType() !== timestamp.getType()) {
206      console.warn(
207        'Cannot apply new timestamp received from remote tool.' +
208          ` Remote tool notified timestamp type ${timestamp.getType()},` +
209          ` but Winscope is accepting timestamp type ${this.timelineData.getTimestampType()}.`
210      );
211      return;
212    }
213
214    const position = TracePosition.fromTimestamp(timestamp);
215    this.timelineData.setPosition(position);
216
217    await this.propagateTracePosition(this.timelineData.getCurrentPosition(), true);
218  }
219
220  private async processRemoteFilesReceived(files: File[]) {
221    this.resetAppToInitialState();
222    await this.processFiles(files);
223  }
224
225  private async processFiles(files: File[]) {
226    let progressMessage = '';
227    const onProgressUpdate = (progressPercentage: number) => {
228      this.currentProgressListener?.onProgressUpdate(progressMessage, progressPercentage);
229    };
230
231    const traceFiles: TraceFile[] = [];
232    const onFile: OnFile = (file: File, parentArchive?: File) => {
233      traceFiles.push(new TraceFile(file, parentArchive));
234    };
235
236    progressMessage = 'Unzipping files...';
237    this.currentProgressListener?.onProgressUpdate(progressMessage, 0);
238    await FileUtils.unzipFilesIfNeeded(files, onFile, onProgressUpdate);
239
240    progressMessage = 'Parsing files...';
241    this.currentProgressListener?.onProgressUpdate(progressMessage, 0);
242    const parserErrors = await this.tracePipeline.loadTraceFiles(traceFiles, onProgressUpdate);
243    this.currentProgressListener?.onOperationFinished();
244    this.userNotificationListener?.onParserErrors(parserErrors);
245  }
246
247  private async processLoadedTraceFiles() {
248    this.currentProgressListener?.onProgressUpdate('Computing frame mapping...', undefined);
249
250    // allow the UI to update before making the main thread very busy
251    await new Promise<void>((resolve) => setTimeout(resolve, 10));
252
253    await this.tracePipeline.buildTraces();
254    this.currentProgressListener?.onOperationFinished();
255
256    this.timelineData.initialize(
257      this.tracePipeline.getTraces(),
258      await this.tracePipeline.getScreenRecordingVideo()
259    );
260    await this.createViewers();
261    this.appComponent.onTraceDataLoaded(this.viewers);
262    this.isTraceDataVisualized = true;
263
264    if (this.lastRemoteToolTimestampReceived !== undefined) {
265      await this.onRemoteTimestampReceived(this.lastRemoteToolTimestampReceived);
266    }
267  }
268
269  private async createViewers() {
270    const traces = this.tracePipeline.getTraces();
271    const traceTypes = new Set<TraceType>();
272    traces.forEachTrace((trace) => {
273      traceTypes.add(trace.type);
274    });
275    this.viewers = new ViewerFactory().createViewers(traceTypes, traces, this.storage);
276
277    // Set position as soon as the viewers are created
278    await this.propagateTracePosition(this.timelineData.getCurrentPosition(), true);
279  }
280
281  private async executeIgnoringRecursiveTimestampNotifications(op: () => Promise<void>) {
282    if (this.isChangingCurrentTimestamp) {
283      return;
284    }
285    this.isChangingCurrentTimestamp = true;
286    try {
287      await op();
288    } finally {
289      this.isChangingCurrentTimestamp = false;
290    }
291  }
292
293  private resetAppToInitialState() {
294    this.tracePipeline.clear();
295    this.timelineData.clear();
296    this.viewers = [];
297    this.isTraceDataVisualized = false;
298    this.lastRemoteToolTimestampReceived = undefined;
299    this.appComponent.onTraceDataUnloaded();
300  }
301}
302