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 {FunctionUtils} from 'common/function_utils'; 19import {InMemoryStorage} from 'common/in_memory_storage'; 20import {TimezoneInfo} from 'common/time'; 21import {TimestampConverter} from 'common/timestamp_converter'; 22import {CrossToolProtocol} from 'cross_tool/cross_tool_protocol'; 23import {ProgressListener} from 'messaging/progress_listener'; 24import {ProgressListenerStub} from 'messaging/progress_listener_stub'; 25import {UserWarning} from 'messaging/user_warning'; 26import { 27 FailedToCreateTracesParser, 28 IncompleteFrameMapping, 29 InvalidLegacyTrace, 30 NoTraceTargetsSelected, 31 NoValidFiles, 32 UnsupportedFileFormat, 33} from 'messaging/user_warnings'; 34import { 35 ActiveTraceChanged, 36 AppFilesCollected, 37 AppFilesUploaded, 38 AppInitialized, 39 AppRefreshDumpsRequest, 40 AppResetRequest, 41 AppTraceViewRequest, 42 DarkModeToggled, 43 ExpandedTimelineToggled, 44 FilterPresetApplyRequest, 45 FilterPresetSaveRequest, 46 InitializeTraceSearchRequest, 47 NoTraceTargetsSelected as NoTraceTargetsSelectedEvent, 48 RemoteToolDownloadStart, 49 RemoteToolFilesReceived, 50 RemoteToolTimestampReceived, 51 TabbedViewSwitched, 52 TabbedViewSwitchRequest, 53 TraceAddRequest, 54 TracePositionUpdate, 55 TraceRemoveRequest, 56 TraceSearchCompleted, 57 TraceSearchFailed, 58 TraceSearchInitialized, 59 TraceSearchRequest, 60 ViewersLoaded, 61 ViewersUnloaded, 62 WinscopeEvent, 63 WinscopeEventType, 64} from 'messaging/winscope_event'; 65import {WinscopeEventEmitter} from 'messaging/winscope_event_emitter'; 66import {WinscopeEventEmitterStub} from 'messaging/winscope_event_emitter_stub'; 67import {WinscopeEventListener} from 'messaging/winscope_event_listener'; 68import {WinscopeEventListenerStub} from 'messaging/winscope_event_listener_stub'; 69import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils'; 70import {TraceBuilder} from 'test/unit/trace_builder'; 71import {UserNotifierChecker} from 'test/unit/user_notifier_checker'; 72import {UnitTestUtils} from 'test/unit/utils'; 73import {Trace} from 'trace/trace'; 74import {TracePosition} from 'trace/trace_position'; 75import {TraceType} from 'trace/trace_type'; 76import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node'; 77import {ViewType} from 'viewers/viewer'; 78import {ViewerFactory} from 'viewers/viewer_factory'; 79import {ViewerStub} from 'viewers/viewer_stub'; 80import {Mediator} from './mediator'; 81import {TimelineData} from './timeline_data'; 82import {TracePipeline} from './trace_pipeline'; 83import {TraceSearchInitializer} from './trace_search/trace_search_initializer'; 84 85describe('Mediator', () => { 86 const TIMESTAMP_10 = TimestampConverterUtils.makeRealTimestamp(10n); 87 const TIMESTAMP_11 = TimestampConverterUtils.makeRealTimestamp(11n); 88 89 const POSITION_10 = TracePosition.fromTimestamp(TIMESTAMP_10); 90 const POSITION_11 = TracePosition.fromTimestamp(TIMESTAMP_11); 91 92 const traceSf = new TraceBuilder<HierarchyTreeNode>() 93 .setType(TraceType.SURFACE_FLINGER) 94 .setTimestamps([TIMESTAMP_10]) 95 .build(); 96 const traceWm = new TraceBuilder<HierarchyTreeNode>() 97 .setType(TraceType.WINDOW_MANAGER) 98 .setTimestamps([TIMESTAMP_11]) 99 .build(); 100 const traceDump = new TraceBuilder<HierarchyTreeNode>() 101 .setType(TraceType.SURFACE_FLINGER) 102 .setTimestamps([TimestampConverterUtils.makeZeroTimestamp()]) 103 .build(); 104 105 let inputFiles: File[]; 106 let eventLogFile: File; 107 let perfettoFile: File; 108 let tracePipeline: TracePipeline; 109 let timelineData: TimelineData; 110 let abtChromeExtensionProtocol: WinscopeEventEmitter & WinscopeEventListener; 111 let crossToolProtocol: CrossToolProtocol; 112 let appComponent: WinscopeEventListener; 113 let timelineComponent: WinscopeEventEmitter & WinscopeEventListener; 114 let uploadTracesComponent: ProgressListenerStub; 115 let collectTracesComponent: ProgressListenerStub & 116 WinscopeEventEmitterStub & 117 WinscopeEventListenerStub; 118 let traceViewComponent: WinscopeEventEmitter & WinscopeEventListener; 119 let mediator: Mediator; 120 let spies: Array<jasmine.Spy<jasmine.Func>>; 121 let userNotifierChecker: UserNotifierChecker; 122 let createViewersSpy: jasmine.Spy; 123 124 const viewerStub0 = new ViewerStub('Title0', undefined, traceSf); 125 const viewerStub1 = new ViewerStub('Title1', undefined, traceWm); 126 const viewerOverlay = new ViewerStub( 127 'TitleOverlay', 128 undefined, 129 traceWm, 130 ViewType.OVERLAY, 131 ); 132 const viewerDump = new ViewerStub('TitleDump', undefined, traceDump); 133 const viewers = [viewerStub0, viewerStub1, viewerOverlay, viewerDump]; 134 let tracePositionUpdateListeners: WinscopeEventListener[]; 135 136 beforeAll(async () => { 137 inputFiles = [ 138 await UnitTestUtils.getFixtureFile( 139 'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb', 140 ), 141 await UnitTestUtils.getFixtureFile( 142 'traces/elapsed_and_real_timestamp/WindowManager.pb', 143 ), 144 await UnitTestUtils.getFixtureFile( 145 'traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4', 146 ), 147 ]; 148 perfettoFile = await UnitTestUtils.getFixtureFile( 149 'traces/perfetto/layers_trace.perfetto-trace', 150 ); 151 eventLogFile = await UnitTestUtils.getFixtureFile( 152 'traces/eventlog_no_cujs.winscope', 153 ); 154 userNotifierChecker = new UserNotifierChecker(); 155 }); 156 157 beforeEach(() => { 158 userNotifierChecker.reset(); 159 jasmine.addCustomEqualityTester(tracePositionUpdateEqualityTester); 160 tracePipeline = new TracePipeline(); 161 timelineData = new TimelineData(); 162 abtChromeExtensionProtocol = FunctionUtils.mixin( 163 new WinscopeEventEmitterStub(), 164 new WinscopeEventListenerStub(), 165 ); 166 crossToolProtocol = new CrossToolProtocol( 167 tracePipeline.getTimestampConverter(), 168 ); 169 appComponent = new WinscopeEventListenerStub(); 170 timelineComponent = FunctionUtils.mixin( 171 new WinscopeEventEmitterStub(), 172 new WinscopeEventListenerStub(), 173 ); 174 uploadTracesComponent = new ProgressListenerStub(); 175 collectTracesComponent = FunctionUtils.mixin( 176 FunctionUtils.mixin( 177 new ProgressListenerStub(), 178 new WinscopeEventListenerStub(), 179 ), 180 new WinscopeEventEmitterStub(), 181 ); 182 traceViewComponent = FunctionUtils.mixin( 183 new WinscopeEventEmitterStub(), 184 new WinscopeEventListenerStub(), 185 ); 186 mediator = new Mediator( 187 tracePipeline, 188 timelineData, 189 abtChromeExtensionProtocol, 190 crossToolProtocol, 191 appComponent, 192 new InMemoryStorage(), 193 ); 194 mediator.setTimelineComponent(timelineComponent); 195 mediator.setUploadTracesComponent(uploadTracesComponent); 196 mediator.setCollectTracesComponent(collectTracesComponent); 197 mediator.setTraceViewComponent(traceViewComponent); 198 199 tracePositionUpdateListeners = [ 200 ...viewers, 201 timelineComponent, 202 crossToolProtocol, 203 ]; 204 205 createViewersSpy = spyOn( 206 ViewerFactory.prototype, 207 'createViewers', 208 ).and.returnValue(viewers); 209 210 spies = [ 211 spyOn(abtChromeExtensionProtocol, 'onWinscopeEvent'), 212 spyOn(appComponent, 'onWinscopeEvent'), 213 spyOn(collectTracesComponent, 'onOperationFinished'), 214 spyOn(collectTracesComponent, 'onProgressUpdate'), 215 spyOn(collectTracesComponent, 'onWinscopeEvent'), 216 spyOn(crossToolProtocol, 'onWinscopeEvent'), 217 spyOn(timelineComponent, 'onWinscopeEvent'), 218 spyOn(timelineData, 'initialize').and.callThrough(), 219 spyOn(traceViewComponent, 'onWinscopeEvent'), 220 spyOn(uploadTracesComponent, 'onProgressUpdate'), 221 spyOn(uploadTracesComponent, 'onOperationFinished'), 222 spyOn(viewerStub0, 'onWinscopeEvent'), 223 spyOn(viewerStub1, 'onWinscopeEvent'), 224 spyOn(viewerOverlay, 'onWinscopeEvent'), 225 spyOn(viewerDump, 'onWinscopeEvent'), 226 ]; 227 }); 228 229 it('notifies ABT chrome extension about app initialization', async () => { 230 expect(abtChromeExtensionProtocol.onWinscopeEvent).not.toHaveBeenCalled(); 231 232 await mediator.onWinscopeEvent(new AppInitialized()); 233 expect(abtChromeExtensionProtocol.onWinscopeEvent).toHaveBeenCalledOnceWith( 234 new AppInitialized(), 235 ); 236 }); 237 238 it('handles uploaded traces from Winscope', async () => { 239 await mediator.onWinscopeEvent(new AppFilesUploaded(inputFiles)); 240 241 expect(uploadTracesComponent.onProgressUpdate).toHaveBeenCalled(); 242 expect(uploadTracesComponent.onOperationFinished).toHaveBeenCalled(); 243 expect(timelineData.initialize).not.toHaveBeenCalled(); 244 expect(appComponent.onWinscopeEvent).not.toHaveBeenCalled(); 245 expect(viewerStub0.onWinscopeEvent).not.toHaveBeenCalled(); 246 247 resetSpyCalls(); 248 await mediator.onWinscopeEvent(new AppTraceViewRequest()); 249 await checkLoadTraceViewEvents(uploadTracesComponent); 250 userNotifierChecker.expectNotified([]); 251 }); 252 253 it('handles collected traces from Winscope', async () => { 254 await mediator.onWinscopeEvent( 255 new AppFilesCollected({ 256 requested: [], 257 collected: [inputFiles[0], inputFiles[1]], 258 }), 259 ); 260 userNotifierChecker.expectNone(); 261 await checkLoadTraceViewEvents(collectTracesComponent); 262 }); 263 264 it('handles invalid collected traces from Winscope', async () => { 265 await mediator.onWinscopeEvent( 266 new AppFilesCollected({ 267 requested: [], 268 collected: [await UnitTestUtils.getFixtureFile('traces/empty.pb')], 269 }), 270 ); 271 expect( 272 userNotifierChecker.expectNotified([ 273 new UnsupportedFileFormat('empty.pb'), 274 ]), 275 ); 276 expect(appComponent.onWinscopeEvent).not.toHaveBeenCalled(); 277 }); 278 279 it('handles collected traces with no entries from Winscope', async () => { 280 await mediator.onWinscopeEvent( 281 new AppFilesCollected({ 282 requested: [], 283 collected: [ 284 await UnitTestUtils.getFixtureFile( 285 'traces/no_entries_InputMethodClients.pb', 286 ), 287 ], 288 }), 289 ); 290 expect( 291 userNotifierChecker.expectNotified([ 292 new InvalidLegacyTrace( 293 'no_entries_InputMethodClients.pb', 294 'Trace has no entries', 295 ), 296 ]), 297 ); 298 expect(appComponent.onWinscopeEvent).not.toHaveBeenCalled(); 299 }); 300 301 it('handles collected trace with no visualization from Winscope', async () => { 302 await mediator.onWinscopeEvent( 303 new AppFilesCollected({ 304 requested: [], 305 collected: [eventLogFile], 306 }), 307 ); 308 expect( 309 userNotifierChecker.expectNotified([ 310 new FailedToCreateTracesParser( 311 TraceType.CUJS, 312 'eventlog_no_cujs.winscope has no relevant entries', 313 ), 314 ]), 315 ); 316 expect(appComponent.onWinscopeEvent).not.toHaveBeenCalled(); 317 }); 318 319 it('handles empty collected traces from Winscope', async () => { 320 await mediator.onWinscopeEvent( 321 new AppFilesCollected({ 322 requested: [], 323 collected: [], 324 }), 325 ); 326 expect(userNotifierChecker.expectNotified([new NoValidFiles()])); 327 expect(appComponent.onWinscopeEvent).not.toHaveBeenCalled(); 328 }); 329 330 it('handles requested traces with missing collected traces from Winscope', async () => { 331 await mediator.onWinscopeEvent( 332 new AppFilesCollected({ 333 requested: [ 334 { 335 name: 'Collected Trace', 336 types: [TraceType.SURFACE_FLINGER], 337 }, 338 { 339 name: 'Uncollected Trace', 340 types: [TraceType.TRANSITION], 341 }, 342 ], 343 collected: [inputFiles[0]], 344 }), 345 ); 346 expect( 347 userNotifierChecker.expectNotified([ 348 new NoValidFiles(['Uncollected Trace']), 349 ]), 350 ); 351 expect(appComponent.onWinscopeEvent).toHaveBeenCalled(); 352 }); 353 354 it('handles app reset request', async () => { 355 await mediator.onWinscopeEvent(new AppFilesUploaded(inputFiles)); 356 const clearSpies = [ 357 spyOn(tracePipeline, 'clear'), 358 spyOn(timelineData, 'clear'), 359 ]; 360 await mediator.onWinscopeEvent(new AppResetRequest()); 361 clearSpies.forEach((spy) => expect(spy).toHaveBeenCalled()); 362 expect(appComponent.onWinscopeEvent).toHaveBeenCalledOnceWith( 363 new ViewersUnloaded(), 364 ); 365 }); 366 367 it('handles request to refresh dumps', async () => { 368 const dumpFiles = [ 369 await UnitTestUtils.getFixtureFile( 370 'traces/elapsed_and_real_timestamp/dump_SurfaceFlinger.pb', 371 ), 372 await UnitTestUtils.getFixtureFile('traces/dump_WindowManager.pb'), 373 ]; 374 await loadFiles(dumpFiles); 375 await mediator.onWinscopeEvent(new AppTraceViewRequest()); 376 await checkLoadTraceViewEvents(uploadTracesComponent); 377 378 await mediator.onWinscopeEvent(new AppRefreshDumpsRequest()); 379 expect(collectTracesComponent.onWinscopeEvent).toHaveBeenCalled(); 380 }); 381 382 //TODO: test "bugreport data from cross-tool protocol" when FileUtils is fully compatible with 383 // Node.js (b/262269229). FileUtils#unzipFile() currently can't execute on Node.js. 384 385 //TODO: test "data from ABT chrome extension" when FileUtils is fully compatible with Node.js 386 // (b/262269229). 387 388 it('handles start download event from remote tool', async () => { 389 expect(uploadTracesComponent.onProgressUpdate).toHaveBeenCalledTimes(0); 390 391 await mediator.onWinscopeEvent(new RemoteToolDownloadStart()); 392 expect(uploadTracesComponent.onProgressUpdate).toHaveBeenCalledTimes(1); 393 }); 394 395 it('handles empty downloaded files from remote tool', async () => { 396 expect(uploadTracesComponent.onOperationFinished).toHaveBeenCalledTimes(0); 397 398 // Pass files even if empty so that the upload component will update the progress bar 399 // and display error messages 400 await mediator.onWinscopeEvent(new RemoteToolFilesReceived([])); 401 expect(uploadTracesComponent.onOperationFinished).toHaveBeenCalledTimes(1); 402 }); 403 404 it('notifies overlay viewer of expanded timeline toggle change', async () => { 405 await loadFiles(); 406 await loadTraceView(); 407 const event = new ExpandedTimelineToggled(true); 408 await mediator.onWinscopeEvent(new ExpandedTimelineToggled(true)); 409 expect(viewerOverlay.onWinscopeEvent).toHaveBeenCalledWith(event); 410 }); 411 412 it('propagates trace position update', async () => { 413 await loadFiles(); 414 await loadTraceView(); 415 416 // notify position 417 resetSpyCalls(); 418 await mediator.onWinscopeEvent(new TracePositionUpdate(POSITION_10)); 419 checkTracePositionUpdateEvents( 420 [viewerStub0, viewerOverlay, timelineComponent, crossToolProtocol], 421 [], 422 POSITION_10, 423 ); 424 425 // notify position 426 resetSpyCalls(); 427 await mediator.onWinscopeEvent(new TracePositionUpdate(POSITION_11)); 428 checkTracePositionUpdateEvents( 429 [viewerStub0, viewerOverlay, timelineComponent, crossToolProtocol], 430 [], 431 POSITION_11, 432 ); 433 }); 434 435 it('propagates trace position update according to timezone', async () => { 436 const timezoneInfo: TimezoneInfo = { 437 timezone: 'Asia/Kolkata', 438 locale: 'en-US', 439 }; 440 const converter = new TimestampConverter(timezoneInfo, 0n); 441 spyOn(tracePipeline, 'getTimestampConverter').and.returnValue(converter); 442 await loadFiles(); 443 await loadTraceView(); 444 445 // notify position 446 resetSpyCalls(); 447 const expectedPosition = TracePosition.fromTimestamp( 448 converter.makeTimestampFromRealNs(10n), 449 ); 450 await mediator.onWinscopeEvent(new TracePositionUpdate(expectedPosition)); 451 checkTracePositionUpdateEvents( 452 [viewerStub0, viewerOverlay, timelineComponent, crossToolProtocol], 453 [], 454 expectedPosition, 455 POSITION_10, 456 ); 457 }); 458 459 it('propagates trace position update and updates timeline data', async () => { 460 await loadFiles(); 461 await loadTraceView(); 462 463 // notify position 464 resetSpyCalls(); 465 const finalTimestampNs = timelineData.getFullTimeRange().to.getValueNs(); 466 const timestamp = 467 TimestampConverterUtils.makeRealTimestamp(finalTimestampNs); 468 const position = TracePosition.fromTimestamp(timestamp); 469 470 await mediator.onWinscopeEvent(new TracePositionUpdate(position, true)); 471 checkTracePositionUpdateEvents( 472 [viewerStub0, viewerOverlay, timelineComponent, crossToolProtocol], 473 [], 474 position, 475 ); 476 expect( 477 assertDefined(timelineData.getCurrentPosition()).timestamp.getValueNs(), 478 ).toEqual(finalTimestampNs); 479 }); 480 481 it("initializes viewers' trace position also when loaded traces have no valid timestamps", async () => { 482 const dumpFile = await UnitTestUtils.getFixtureFile( 483 'traces/dump_WindowManager.pb', 484 ); 485 await mediator.onWinscopeEvent(new AppFilesUploaded([dumpFile])); 486 487 resetSpyCalls(); 488 await mediator.onWinscopeEvent(new AppTraceViewRequest()); 489 await checkLoadTraceViewEvents(uploadTracesComponent); 490 userNotifierChecker.expectNotified([]); 491 }); 492 493 it('filters traces without visualization on loading viewers', async () => { 494 const fileWithoutVisualization = await UnitTestUtils.getFixtureFile( 495 'traces/elapsed_and_real_timestamp/shell_transition_trace.pb', 496 ); 497 await loadFiles(); 498 await mediator.onWinscopeEvent( 499 new AppFilesUploaded([fileWithoutVisualization]), 500 ); 501 await loadTraceView(); 502 }); 503 504 it('warns user if frame mapping fails', async () => { 505 const errorMsg = 'frame mapping failed'; 506 spyOn(tracePipeline, 'buildTraces').and.throwError(errorMsg); 507 const dumpFile = await UnitTestUtils.getFixtureFile( 508 'traces/dump_WindowManager.pb', 509 ); 510 await mediator.onWinscopeEvent(new AppFilesUploaded([dumpFile])); 511 512 resetSpyCalls(); 513 await mediator.onWinscopeEvent(new AppTraceViewRequest()); 514 await checkLoadTraceViewEvents(uploadTracesComponent, undefined, [ 515 new IncompleteFrameMapping(errorMsg), 516 ]); 517 }); 518 519 describe('timestamp received from remote tool', () => { 520 it('propagates trace position update', async () => { 521 tracePipeline.getTimestampConverter().setRealToMonotonicTimeOffsetNs(0n); 522 await loadFiles(); 523 await loadTraceView(); 524 const traceSfEntry = assertDefined( 525 tracePipeline.getTraces().getTrace(TraceType.SURFACE_FLINGER), 526 ).getEntry(2); 527 528 // receive timestamp 529 resetSpyCalls(); 530 await mediator.onWinscopeEvent( 531 new RemoteToolTimestampReceived(() => traceSfEntry.getTimestamp()), 532 ); 533 534 checkTracePositionUpdateEvents( 535 [viewerStub0, viewerOverlay, timelineComponent], 536 [], 537 TracePosition.fromTraceEntry(traceSfEntry), 538 ); 539 }); 540 541 it("doesn't propagate timestamp back to remote tool", async () => { 542 tracePipeline.getTimestampConverter().setRealToMonotonicTimeOffsetNs(0n); 543 await loadFiles(); 544 await loadTraceView(); 545 546 // receive timestamp 547 resetSpyCalls(); 548 await mediator.onWinscopeEvent( 549 new RemoteToolTimestampReceived(() => TIMESTAMP_10), 550 ); 551 checkTracePositionUpdateEvents( 552 [viewerStub0, viewerOverlay, timelineComponent], 553 [], 554 ); 555 }); 556 557 it('defers trace position propagation till traces are loaded and visualized', async () => { 558 // ensure converter has been used to create real timestamps 559 tracePipeline.getTimestampConverter().makeTimestampFromRealNs(0n); 560 561 // load files but do not load trace view 562 await loadFiles(); 563 expect(timelineComponent.onWinscopeEvent).not.toHaveBeenCalled(); 564 const traceSf = assertDefined( 565 tracePipeline.getTraces().getTrace(TraceType.SURFACE_FLINGER), 566 ); 567 568 // keep timestamp for later 569 await mediator.onWinscopeEvent( 570 new RemoteToolTimestampReceived(() => 571 traceSf.getEntry(1).getTimestamp(), 572 ), 573 ); 574 expect(timelineComponent.onWinscopeEvent).not.toHaveBeenCalled(); 575 576 // keep timestamp for later (replace previous one) 577 await mediator.onWinscopeEvent( 578 new RemoteToolTimestampReceived(() => 579 traceSf.getEntry(2).getTimestamp(), 580 ), 581 ); 582 expect(timelineComponent.onWinscopeEvent).not.toHaveBeenCalled(); 583 584 // apply timestamp 585 await loadTraceView(); 586 587 expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith( 588 makeExpectedTracePositionUpdate( 589 TracePosition.fromTraceEntry(traceSf.getEntry(2)), 590 ), 591 ); 592 }); 593 }); 594 595 describe('tab view switches', () => { 596 it('forwards switch notifications', async () => { 597 await loadFiles(); 598 await loadTraceView(); 599 resetSpyCalls(); 600 601 const view = viewerStub1.getViews()[0]; 602 await mediator.onWinscopeEvent(new TabbedViewSwitched(view)); 603 expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith( 604 new ActiveTraceChanged(view.traces[0]), 605 ); 606 userNotifierChecker.expectNotified([]); 607 userNotifierChecker.reset(); 608 const viewDump = viewerDump.getViews()[0]; 609 await mediator.onWinscopeEvent(new TabbedViewSwitched(viewDump)); 610 expect(timelineComponent.onWinscopeEvent).not.toHaveBeenCalledWith( 611 new ActiveTraceChanged(viewDump.traces[0]), 612 ); 613 userNotifierChecker.expectNotified([]); 614 }); 615 616 it('forwards switch requests from viewers to trace view component', async () => { 617 await loadFiles(); 618 await loadTraceView(); 619 expect(traceViewComponent.onWinscopeEvent).not.toHaveBeenCalled(); 620 621 await viewerStub0.emitAppEventForTesting( 622 new TabbedViewSwitchRequest(traceSf), 623 ); 624 expect(traceViewComponent.onWinscopeEvent).toHaveBeenCalledOnceWith( 625 new TabbedViewSwitchRequest(traceSf), 626 ); 627 userNotifierChecker.expectNotified([]); 628 }); 629 }); 630 631 it('notifies only visible viewers about trace position updates', async () => { 632 await loadFiles(); 633 await loadTraceView(); 634 635 // Position update -> update only visible viewers 636 // Note: Viewer 0 is visible (gets focus) upon UI initialization 637 resetSpyCalls(); 638 await mediator.onWinscopeEvent(new TracePositionUpdate(POSITION_10)); 639 checkTracePositionUpdateEvents( 640 [viewerStub0, viewerOverlay, timelineComponent, crossToolProtocol], 641 [], 642 POSITION_10, 643 ); 644 645 // Tab switch -> update only newly visible viewers 646 // Note: overlay viewer is considered always visible 647 resetSpyCalls(); 648 await mediator.onWinscopeEvent( 649 new TabbedViewSwitched(viewerStub1.getViews()[0]), 650 ); 651 userNotifierChecker.expectNone(); 652 const tracePositionUpdate = makeExpectedTracePositionUpdate(undefined); 653 const activeTraceChanged = new ActiveTraceChanged( 654 viewerStub1.getViews()[0].traces[0], 655 ); 656 expect(viewerStub0.onWinscopeEvent).toHaveBeenCalledOnceWith( 657 activeTraceChanged, 658 ); 659 expect(viewerDump.onWinscopeEvent).toHaveBeenCalledOnceWith( 660 activeTraceChanged, 661 ); 662 663 expect(viewerStub1.onWinscopeEvent).toHaveBeenCalledWith( 664 tracePositionUpdate, 665 ); 666 expect(viewerStub1.onWinscopeEvent).toHaveBeenCalledWith( 667 activeTraceChanged, 668 ); 669 670 expect(viewerOverlay.onWinscopeEvent).toHaveBeenCalledWith( 671 tracePositionUpdate, 672 ); 673 expect(viewerOverlay.onWinscopeEvent).toHaveBeenCalledWith( 674 activeTraceChanged, 675 ); 676 677 expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith( 678 tracePositionUpdate, 679 ); 680 expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith( 681 activeTraceChanged, 682 ); 683 684 expect(crossToolProtocol.onWinscopeEvent).toHaveBeenCalledOnceWith( 685 tracePositionUpdate, 686 ); 687 688 // Position update -> update only visible viewers 689 // Note: overlay viewer is considered always visible 690 resetSpyCalls(); 691 await mediator.onWinscopeEvent(new TracePositionUpdate(POSITION_10)); 692 checkTracePositionUpdateEvents( 693 [viewerStub1, viewerOverlay, timelineComponent, crossToolProtocol], 694 [], 695 ); 696 }); 697 698 it('notifies timeline of dark mode toggle', async () => { 699 const event = new DarkModeToggled(true); 700 await mediator.onWinscopeEvent(event); 701 expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledOnceWith(event); 702 }); 703 704 it('notifies timeline and viewers of active trace change', async () => { 705 await loadFiles(); 706 await loadTraceView(); 707 resetSpyCalls(); 708 709 const activeTraceChanged = new ActiveTraceChanged(traceWm); 710 await mediator.onWinscopeEvent(activeTraceChanged); 711 expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledOnceWith( 712 activeTraceChanged, 713 ); 714 viewers.forEach((viewer) => 715 expect(viewer.onWinscopeEvent).toHaveBeenCalledOnceWith( 716 activeTraceChanged, 717 ), 718 ); 719 }); 720 721 it('notifies user of no trace targets selected', async () => { 722 await mediator.onWinscopeEvent(new NoTraceTargetsSelectedEvent()); 723 userNotifierChecker.expectNotified([new NoTraceTargetsSelected()]); 724 }); 725 726 it('notifies correct viewer of filter preset requests', async () => { 727 await loadFiles(); 728 await loadTraceView(); 729 resetSpyCalls(); 730 731 const saveRequest = new FilterPresetSaveRequest( 732 'test_preset', 733 TraceType.SURFACE_FLINGER, 734 ); 735 await mediator.onWinscopeEvent(saveRequest); 736 737 const applyRequest = new FilterPresetApplyRequest( 738 'test_preset', 739 TraceType.WINDOW_MANAGER, 740 ); 741 await mediator.onWinscopeEvent(applyRequest); 742 743 await mediator.onWinscopeEvent( 744 new FilterPresetSaveRequest('test_preset', TraceType.PROTO_LOG), 745 ); 746 await mediator.onWinscopeEvent( 747 new FilterPresetApplyRequest('test_preset', TraceType.PROTO_LOG), 748 ); 749 750 expect(viewerStub0.onWinscopeEvent).toHaveBeenCalledOnceWith(saveRequest); 751 expect(viewerStub1.onWinscopeEvent).toHaveBeenCalledOnceWith(applyRequest); 752 }); 753 754 it('initializes trace search', async () => { 755 const searchViewer = await loadPerfettoFilesAndReturnSearchViewer(); 756 const spy = spyOn( 757 TraceSearchInitializer, 758 'createSearchViews', 759 ).and.returnValue(Promise.resolve(['test'])); 760 const initializeRequest = new InitializeTraceSearchRequest(); 761 await mediator.onWinscopeEvent(initializeRequest); 762 expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith( 763 initializeRequest, 764 ); 765 expect(spy).toHaveBeenCalledTimes(1); 766 const initializedEvent = new TraceSearchInitialized(['test']); 767 expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith( 768 initializedEvent, 769 ); 770 expect(searchViewer.onWinscopeEvent).toHaveBeenCalledWith(initializedEvent); 771 }); 772 773 it('handles trace search request for successful queries', async () => { 774 const searchViewer = await loadPerfettoFilesAndReturnSearchViewer(); 775 await requestSearch('select ts from surfaceflinger_layers_snapshot'); 776 checkNewSearchTracePropagation(searchViewer, true); 777 await requestSearch('select id from surfaceflinger_layers_snapshot'); 778 checkNewSearchTracePropagation(searchViewer, false); 779 }); 780 781 it('handles trace search request for unsuccessful query', async () => { 782 const searchViewer = await loadPerfettoFilesAndReturnSearchViewer(); 783 await requestSearch('select * from fake_table'); 784 expect(searchViewer.onWinscopeEvent).toHaveBeenCalledWith( 785 new TraceSearchFailed(), 786 ); 787 expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith( 788 new TraceSearchCompleted(), 789 ); 790 }); 791 792 it('handles trace removal requests', async () => { 793 await loadPerfettoFilesAndReturnSearchViewer(); 794 await requestSearch('select ts from surfaceflinger_layers_snapshot'); 795 removeSearchTraceAndCheckPropagation(true); 796 await requestSearch('select id from surfaceflinger_layers_snapshot'); 797 removeSearchTraceAndCheckPropagation(false); 798 }); 799 800 async function loadFiles( 801 files = inputFiles, 802 viewersToReassignTraces = [viewerStub0, viewerStub1], 803 ) { 804 for (const file of files) { 805 await mediator.onWinscopeEvent(new AppFilesUploaded([file])); 806 } 807 userNotifierChecker.expectNone(); 808 viewersToReassignTraces.forEach((viewer) => 809 reassignViewerStubTrace(viewer), 810 ); 811 } 812 813 function reassignViewerStubTrace(viewerStub: ViewerStub) { 814 const viewerStubTraces = viewerStub.getViews()[0].traces; 815 viewerStubTraces[0] = tracePipeline 816 .getTraces() 817 .getTrace(viewerStubTraces[0].type) as Trace<object>; 818 } 819 820 async function loadTraceView(expectedViewers = viewers) { 821 // Simulate "View traces" button click 822 resetSpyCalls(); 823 await mediator.onWinscopeEvent(new AppTraceViewRequest()); 824 825 await checkLoadTraceViewEvents(uploadTracesComponent, expectedViewers); 826 827 // Simulate notification of TraceViewComponent about initially selected/focused tab 828 resetSpyCalls(); 829 await mediator.onWinscopeEvent( 830 new TabbedViewSwitched(viewerStub0.getViews()[0]), 831 ); 832 833 expect(viewerStub0.onWinscopeEvent).toHaveBeenCalledOnceWith( 834 makeExpectedTracePositionUpdate(), 835 ); 836 expect(viewerStub1.onWinscopeEvent).not.toHaveBeenCalled(); 837 userNotifierChecker.expectNotified([]); 838 } 839 840 async function checkLoadTraceViewEvents( 841 progressListener: ProgressListener, 842 expectedViewers = viewers, 843 notifications: UserWarning[] = [], 844 ) { 845 expect(progressListener.onProgressUpdate).toHaveBeenCalled(); 846 expect(progressListener.onOperationFinished).toHaveBeenCalled(); 847 expect(timelineData.initialize).toHaveBeenCalledTimes(1); 848 expect(appComponent.onWinscopeEvent).toHaveBeenCalledOnceWith( 849 new ViewersLoaded(expectedViewers), 850 ); 851 852 // Mediator triggers the viewers initialization 853 // by sending them a "trace position update" event 854 checkTracePositionUpdateEvents( 855 (expectedViewers as WinscopeEventListener[]).concat([timelineComponent]), 856 notifications, 857 ); 858 } 859 860 function checkTracePositionUpdateEvents( 861 listenersToBeNotified: WinscopeEventListener[], 862 userNotifications: UserWarning[], 863 position?: TracePosition, 864 crossToolProtocolPosition = position, 865 ) { 866 userNotifierChecker.expectNotified(userNotifications); 867 const event = makeExpectedTracePositionUpdate(position); 868 const crossToolProtocolEvent = 869 crossToolProtocolPosition !== position 870 ? makeExpectedTracePositionUpdate(crossToolProtocolPosition) 871 : event; 872 tracePositionUpdateListeners.forEach((listener) => { 873 const isVisible = listenersToBeNotified.includes(listener); 874 if (isVisible) { 875 const expected = 876 listener === crossToolProtocol ? crossToolProtocolEvent : event; 877 expect(listener.onWinscopeEvent).toHaveBeenCalledOnceWith(expected); 878 } else { 879 expect(listener.onWinscopeEvent).not.toHaveBeenCalled(); 880 } 881 }); 882 } 883 884 function resetSpyCalls() { 885 spies.forEach((spy) => { 886 spy.calls.reset(); 887 }); 888 userNotifierChecker.reset(); 889 } 890 891 async function loadPerfettoFilesAndReturnSearchViewer(): Promise<ViewerStub> { 892 await loadFiles([perfettoFile], [viewerStub0]); 893 const searchViewer = new ViewerStub( 894 'search', 895 undefined, 896 undefined, 897 ViewType.GLOBAL_SEARCH, 898 ); 899 spyOn(searchViewer, 'onWinscopeEvent'); 900 const expectedViewers = [viewerStub0, searchViewer]; 901 createViewersSpy.and.returnValue(expectedViewers); 902 await loadTraceView(expectedViewers); 903 resetSpyCalls(); 904 return searchViewer; 905 } 906 907 async function requestSearch(query: string) { 908 const event = new TraceSearchRequest(query); 909 await mediator.onWinscopeEvent(event); 910 expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith(event); 911 } 912 913 function checkNewSearchTracePropagation( 914 searchViewer: ViewerStub, 915 hasTimestamps: boolean, 916 ) { 917 const searchTraces = tracePipeline.getTraces().getTraces(TraceType.SEARCH); 918 const newTrace = searchTraces[searchTraces.length - 1]; 919 const newTraceEvent = new TraceAddRequest(newTrace); 920 expect(searchViewer.onWinscopeEvent).toHaveBeenCalledWith(newTraceEvent); 921 expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith( 922 new TraceSearchCompleted(), 923 ); 924 expect(timelineData.hasTrace(newTrace)).toEqual(hasTimestamps); 925 const timelineComponentSpy = timelineComponent.onWinscopeEvent; 926 if (hasTimestamps) { 927 expect(timelineComponentSpy).toHaveBeenCalledWith(newTraceEvent); 928 } else { 929 expect(timelineComponentSpy).not.toHaveBeenCalledWith(newTraceEvent); 930 } 931 } 932 933 async function removeSearchTraceAndCheckPropagation(hasTimestamps: boolean) { 934 const searchTraces = tracePipeline.getTraces().getTraces(TraceType.SEARCH); 935 const newTrace = searchTraces[searchTraces.length - 1]; 936 const removalRequest = new TraceRemoveRequest(newTrace); 937 await mediator.onWinscopeEvent(removalRequest); 938 expect(tracePipeline.getTraces().hasTrace(newTrace)).toBeFalse(); 939 expect(timelineData.hasTrace(newTrace)).toBeFalse(); 940 const timelineComponentSpy = timelineComponent.onWinscopeEvent; 941 if (hasTimestamps) { 942 expect(timelineComponentSpy).toHaveBeenCalledWith(removalRequest); 943 } else { 944 expect(timelineComponentSpy).not.toHaveBeenCalledWith(removalRequest); 945 } 946 } 947 948 function makeExpectedTracePositionUpdate( 949 tracePosition?: TracePosition, 950 ): WinscopeEvent { 951 if (tracePosition !== undefined) { 952 return new TracePositionUpdate(tracePosition); 953 } 954 return {type: WinscopeEventType.TRACE_POSITION_UPDATE} as WinscopeEvent; 955 } 956 957 function tracePositionUpdateEqualityTester( 958 first: any, 959 second: any, 960 ): boolean | undefined { 961 if ( 962 first instanceof TracePositionUpdate && 963 second instanceof TracePositionUpdate 964 ) { 965 return testTracePositionUpdates(first, second); 966 } 967 if ( 968 first instanceof TracePositionUpdate && 969 second.type === WinscopeEventType.TRACE_POSITION_UPDATE 970 ) { 971 return first.type === second.type; 972 } 973 return undefined; 974 } 975 976 function testTracePositionUpdates( 977 event: TracePositionUpdate, 978 expectedEvent: TracePositionUpdate, 979 ): boolean { 980 if (event.type !== expectedEvent.type) return false; 981 if ( 982 event.position.timestamp.getValueNs() !== 983 expectedEvent.position.timestamp.getValueNs() 984 ) { 985 return false; 986 } 987 if (event.position.frame !== expectedEvent.position.frame) return false; 988 return true; 989 } 990}); 991