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