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