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