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 {Timestamp} from 'common/time'; 18import {TimeUtils} from 'common/time_utils'; 19import {CrossToolProtocol} from 'cross_tool/cross_tool_protocol'; 20import {Analytics} from 'logging/analytics'; 21import {ProgressListener} from 'messaging/progress_listener'; 22import {UserNotificationsListener} from 'messaging/user_notifications_listener'; 23import {UserWarning} from 'messaging/user_warning'; 24import { 25 ActiveTraceChanged, 26 ExpandedTimelineToggled, 27 TracePositionUpdate, 28 ViewersLoaded, 29 ViewersUnloaded, 30 WinscopeEvent, 31 WinscopeEventType, 32} from 'messaging/winscope_event'; 33import {WinscopeEventEmitter} from 'messaging/winscope_event_emitter'; 34import {WinscopeEventListener} from 'messaging/winscope_event_listener'; 35import {TraceEntry} from 'trace/trace'; 36import {TracePosition} from 'trace/trace_position'; 37import {View, Viewer, ViewType} from 'viewers/viewer'; 38import {ViewerFactory} from 'viewers/viewer_factory'; 39import {FilesSource} from './files_source'; 40import {TimelineData} from './timeline_data'; 41import {TracePipeline} from './trace_pipeline'; 42 43export class Mediator { 44 private abtChromeExtensionProtocol: WinscopeEventEmitter & 45 WinscopeEventListener; 46 private crossToolProtocol: CrossToolProtocol; 47 private uploadTracesComponent?: ProgressListener; 48 private collectTracesComponent?: ProgressListener & WinscopeEventListener; 49 private traceViewComponent?: WinscopeEventEmitter & WinscopeEventListener; 50 private timelineComponent?: WinscopeEventEmitter & WinscopeEventListener; 51 private appComponent: WinscopeEventListener; 52 private userNotificationsListener: UserNotificationsListener; 53 private storage: Storage; 54 55 private tracePipeline: TracePipeline; 56 private timelineData: TimelineData; 57 private viewers: Viewer[] = []; 58 private focusedTabView: undefined | View; 59 private areViewersLoaded = false; 60 private lastRemoteToolDeferredTimestampReceived?: () => Timestamp | undefined; 61 private currentProgressListener?: ProgressListener; 62 63 constructor( 64 tracePipeline: TracePipeline, 65 timelineData: TimelineData, 66 abtChromeExtensionProtocol: WinscopeEventEmitter & WinscopeEventListener, 67 crossToolProtocol: CrossToolProtocol, 68 appComponent: WinscopeEventListener, 69 userNotificationsListener: UserNotificationsListener, 70 storage: Storage, 71 ) { 72 this.tracePipeline = tracePipeline; 73 this.timelineData = timelineData; 74 this.abtChromeExtensionProtocol = abtChromeExtensionProtocol; 75 this.crossToolProtocol = crossToolProtocol; 76 this.appComponent = appComponent; 77 this.userNotificationsListener = userNotificationsListener; 78 this.storage = storage; 79 80 this.crossToolProtocol.setEmitEvent(async (event) => { 81 await this.onWinscopeEvent(event); 82 }); 83 84 this.abtChromeExtensionProtocol.setEmitEvent(async (event) => { 85 await this.onWinscopeEvent(event); 86 }); 87 } 88 89 setUploadTracesComponent(component: ProgressListener | undefined) { 90 this.uploadTracesComponent = component; 91 } 92 93 setCollectTracesComponent( 94 component: (ProgressListener & WinscopeEventListener) | undefined, 95 ) { 96 this.collectTracesComponent = component; 97 } 98 99 setTraceViewComponent( 100 component: (WinscopeEventEmitter & WinscopeEventListener) | undefined, 101 ) { 102 this.traceViewComponent = component; 103 this.traceViewComponent?.setEmitEvent(async (event) => { 104 await this.onWinscopeEvent(event); 105 }); 106 } 107 108 setTimelineComponent( 109 component: (WinscopeEventEmitter & WinscopeEventListener) | undefined, 110 ) { 111 this.timelineComponent = component; 112 this.timelineComponent?.setEmitEvent(async (event) => { 113 await this.onWinscopeEvent(event); 114 }); 115 } 116 117 async onWinscopeEvent(event: WinscopeEvent) { 118 await event.visit(WinscopeEventType.APP_INITIALIZED, async (event) => { 119 await this.abtChromeExtensionProtocol.onWinscopeEvent(event); 120 }); 121 122 await event.visit(WinscopeEventType.APP_FILES_UPLOADED, async (event) => { 123 this.currentProgressListener = this.uploadTracesComponent; 124 await this.loadFiles(event.files, FilesSource.UPLOADED); 125 }); 126 127 await event.visit(WinscopeEventType.APP_FILES_COLLECTED, async (event) => { 128 this.currentProgressListener = this.collectTracesComponent; 129 await this.loadFiles(event.files, FilesSource.COLLECTED); 130 await this.loadViewers(); 131 }); 132 133 await event.visit(WinscopeEventType.APP_RESET_REQUEST, async () => { 134 await this.resetAppToInitialState(); 135 }); 136 137 await event.visit( 138 WinscopeEventType.APP_REFRESH_DUMPS_REQUEST, 139 async (event) => { 140 await this.resetAppToInitialState(); 141 await this.collectTracesComponent?.onWinscopeEvent(event); 142 }, 143 ); 144 145 await event.visit(WinscopeEventType.APP_TRACE_VIEW_REQUEST, async () => { 146 await this.loadViewers(); 147 }); 148 149 await event.visit( 150 WinscopeEventType.REMOTE_TOOL_DOWNLOAD_START, 151 async () => { 152 Analytics.Tracing.logOpenFromABT(); 153 await this.resetAppToInitialState(); 154 this.currentProgressListener = this.uploadTracesComponent; 155 this.currentProgressListener?.onProgressUpdate( 156 'Downloading files...', 157 undefined, 158 ); 159 }, 160 ); 161 162 await event.visit( 163 WinscopeEventType.REMOTE_TOOL_FILES_RECEIVED, 164 async (event) => { 165 await this.processRemoteFilesReceived( 166 event.files, 167 FilesSource.REMOTE_TOOL, 168 ); 169 if (event.deferredTimestamp) { 170 await this.processRemoteToolDeferredTimestampReceived( 171 event.deferredTimestamp, 172 ); 173 } 174 }, 175 ); 176 177 await event.visit( 178 WinscopeEventType.REMOTE_TOOL_TIMESTAMP_RECEIVED, 179 async (event) => { 180 await this.processRemoteToolDeferredTimestampReceived( 181 event.deferredTimestamp, 182 ); 183 }, 184 ); 185 186 await event.visit( 187 WinscopeEventType.TABBED_VIEW_SWITCH_REQUEST, 188 async (event) => { 189 await this.traceViewComponent?.onWinscopeEvent(event); 190 }, 191 ); 192 193 await event.visit(WinscopeEventType.TABBED_VIEW_SWITCHED, async (event) => { 194 if (this.timelineData.trySetActiveTrace(event.newFocusedView.traces[0])) { 195 await this.timelineComponent?.onWinscopeEvent( 196 new ActiveTraceChanged(event.newFocusedView.traces[0]), 197 ); 198 } 199 this.focusedTabView = event.newFocusedView; 200 await this.propagateTracePosition( 201 this.timelineData.getCurrentPosition(), 202 false, 203 ); 204 }); 205 206 await event.visit( 207 WinscopeEventType.TRACE_POSITION_UPDATE, 208 async (event) => { 209 if (event.updateTimeline) { 210 this.timelineData.setPosition(event.position); 211 } 212 await this.propagateTracePosition(event.position, false); 213 }, 214 ); 215 216 await event.visit( 217 WinscopeEventType.EXPANDED_TIMELINE_TOGGLED, 218 async (event) => { 219 await this.propagateToOverlays(event); 220 }, 221 ); 222 223 await event.visit(WinscopeEventType.ACTIVE_TRACE_CHANGED, async (event) => { 224 this.timelineData.trySetActiveTrace(event.trace); 225 await this.timelineComponent?.onWinscopeEvent(event); 226 }); 227 228 await event.visit(WinscopeEventType.DARK_MODE_TOGGLED, async (event) => { 229 await this.timelineComponent?.onWinscopeEvent(event); 230 }); 231 } 232 233 private async loadFiles(files: File[], source: FilesSource) { 234 const warnings: UserWarning[] = []; 235 const notificationsListener: UserNotificationsListener = { 236 onNotifications(notifications: UserWarning[]) { 237 warnings.push(...notifications); 238 }, 239 }; 240 await this.tracePipeline.loadFiles( 241 files, 242 source, 243 notificationsListener, 244 this.currentProgressListener, 245 ); 246 247 if (warnings.length > 0) { 248 this.userNotificationsListener.onNotifications(warnings); 249 } 250 } 251 252 private async propagateTracePosition( 253 position: TracePosition | undefined, 254 omitCrossToolProtocol: boolean, 255 ) { 256 if (!position) { 257 return; 258 } 259 260 const event = new TracePositionUpdate(position); 261 const receivers: WinscopeEventListener[] = [...this.viewers].filter( 262 (viewer) => this.isViewerVisible(viewer), 263 ); 264 if (this.timelineComponent) { 265 receivers.push(this.timelineComponent); 266 } 267 268 const promises = receivers.map((receiver) => { 269 return receiver.onWinscopeEvent(event); 270 }); 271 272 if (!omitCrossToolProtocol) { 273 const event = new TracePositionUpdate(position); 274 promises.push(this.crossToolProtocol.onWinscopeEvent(event)); 275 } 276 277 await Promise.all(promises); 278 } 279 280 private isViewerVisible(viewer: Viewer): boolean { 281 if (!this.focusedTabView) { 282 // During initialization no tab is focused. 283 // Let's just consider all viewers as visible and to be updated. 284 return true; 285 } 286 287 return viewer.getViews().some((view) => { 288 if (view === this.focusedTabView) { 289 return true; 290 } 291 if (view.type === ViewType.OVERLAY) { 292 // Nice to have: update viewer only if overlay view is actually visible (not minimized) 293 return true; 294 } 295 return false; 296 }); 297 } 298 299 private async processRemoteToolDeferredTimestampReceived( 300 deferredTimestamp: () => Timestamp | undefined, 301 ) { 302 this.lastRemoteToolDeferredTimestampReceived = deferredTimestamp; 303 304 if (!this.areViewersLoaded) { 305 return; // apply timestamp later when traces are visualized 306 } 307 308 const timestamp = deferredTimestamp(); 309 if (!timestamp) { 310 return; 311 } 312 313 const position = this.timelineData.makePositionFromActiveTrace(timestamp); 314 this.timelineData.setPosition(position); 315 316 await this.propagateTracePosition( 317 this.timelineData.getCurrentPosition(), 318 true, 319 ); 320 } 321 322 private async processRemoteFilesReceived(files: File[], source: FilesSource) { 323 await this.resetAppToInitialState(); 324 this.currentProgressListener = this.uploadTracesComponent; 325 await this.loadFiles(files, source); 326 } 327 328 private async loadViewers() { 329 this.currentProgressListener?.onProgressUpdate( 330 'Computing frame mapping...', 331 undefined, 332 ); 333 334 // TODO: move this into the ProgressListener 335 // allow the UI to update before making the main thread very busy 336 await TimeUtils.sleepMs(10); 337 338 this.tracePipeline.filterTracesWithoutVisualization(); 339 await this.tracePipeline.buildTraces(); 340 this.currentProgressListener?.onOperationFinished(); 341 342 this.currentProgressListener?.onProgressUpdate( 343 'Initializing UI...', 344 undefined, 345 ); 346 347 // TODO: move this into the ProgressListener 348 // allow the UI to update before making the main thread very busy 349 await TimeUtils.sleepMs(10); 350 351 await this.timelineData.initialize( 352 this.tracePipeline.getTraces(), 353 await this.tracePipeline.getScreenRecordingVideo(), 354 this.tracePipeline.getTimestampConverter(), 355 ); 356 357 this.viewers = new ViewerFactory().createViewers( 358 this.tracePipeline.getTraces(), 359 this.storage, 360 ); 361 this.viewers.forEach((viewer) => 362 viewer.setEmitEvent(async (event) => { 363 await this.onWinscopeEvent(event); 364 }), 365 ); 366 367 // Set initial trace position as soon as UI is created 368 const initialPosition = this.getInitialTracePosition(); 369 this.timelineData.setPosition(initialPosition); 370 371 // Make sure all viewers are initialized and have performed the heavy pre-processing they need 372 // at this stage, while the "initializing UI" progress message is still being displayed. 373 // The viewers initialization is triggered by sending them a "trace position update". 374 await this.propagateTracePosition(initialPosition, true); 375 376 this.focusedTabView = this.viewers 377 .find((v) => v.getViews()[0].type !== ViewType.OVERLAY) 378 ?.getViews()[0]; 379 this.areViewersLoaded = true; 380 381 // Notify app component (i.e. render viewers), only after all viewers have been initialized 382 // (see above). 383 // 384 // Notifying the app component first could result in this kind of interleaved execution: 385 // 1. Mediator notifies app component 386 // 1.1. App component renders UI components 387 // 1.2. Mediator receives back a "view switched" event 388 // 1.2. Mediator sends "trace position update" to viewers 389 // 2. Mediator sends "trace position update" to viewers to initialize them (see above) 390 // 391 // and because our data load operations are async and involve task suspensions, the two 392 // "trace position update" could be processed concurrently within the same viewer. 393 // Meaning the viewer could perform twice the initial heavy pre-processing, 394 // thus increasing UI initialization times. 395 await this.appComponent.onWinscopeEvent(new ViewersLoaded(this.viewers)); 396 } 397 398 private getInitialTracePosition(): TracePosition | undefined { 399 if (this.lastRemoteToolDeferredTimestampReceived) { 400 const lastRemoteToolTimestamp = 401 this.lastRemoteToolDeferredTimestampReceived(); 402 if (lastRemoteToolTimestamp) { 403 return this.timelineData.makePositionFromActiveTrace( 404 lastRemoteToolTimestamp, 405 ); 406 } 407 } 408 409 const position = this.timelineData.getCurrentPosition(); 410 if (position) { 411 return position; 412 } 413 414 // TimelineData might not provide a TracePosition because all the loaded traces are 415 // dumps with invalid timestamps (value zero). In this case let's create a TracePosition 416 // out of any entry from the loaded traces (if available). 417 const firstEntries = this.tracePipeline 418 .getTraces() 419 .mapTrace((trace) => { 420 if (trace.lengthEntries > 0) { 421 return trace.getEntry(0); 422 } 423 return undefined; 424 }) 425 .filter((entry) => { 426 return entry !== undefined; 427 }) as Array<TraceEntry<object>>; 428 429 if (firstEntries.length > 0) { 430 return TracePosition.fromTraceEntry(firstEntries[0]); 431 } 432 433 return undefined; 434 } 435 436 private async resetAppToInitialState() { 437 this.tracePipeline.clear(); 438 this.timelineData.clear(); 439 this.viewers = []; 440 this.areViewersLoaded = false; 441 this.lastRemoteToolDeferredTimestampReceived = undefined; 442 this.focusedTabView = undefined; 443 await this.appComponent.onWinscopeEvent(new ViewersUnloaded()); 444 } 445 446 private async propagateToOverlays(event: ExpandedTimelineToggled) { 447 const overlayViewers = this.viewers.filter((viewer) => 448 viewer.getViews().some((view) => view.type === ViewType.OVERLAY), 449 ); 450 for (const overlay of overlayViewers) { 451 await overlay.onWinscopeEvent(event); 452 } 453 } 454} 455