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 { 18 ChangeDetectorRef, 19 Component, 20 Inject, 21 Injector, 22 NgZone, 23 ViewChild, 24 ViewEncapsulation, 25} from '@angular/core'; 26import {createCustomElement} from '@angular/elements'; 27import {FormControl, Validators} from '@angular/forms'; 28import {MatDialog} from '@angular/material/dialog'; 29import {Title} from '@angular/platform-browser'; 30import {AbtChromeExtensionProtocol} from 'abt_chrome_extension/abt_chrome_extension_protocol'; 31import {Mediator} from 'app/mediator'; 32import {TimelineData} from 'app/timeline_data'; 33import {TracePipeline} from 'app/trace_pipeline'; 34import {Download} from 'common/download'; 35import {FileUtils} from 'common/file_utils'; 36import {globalConfig} from 'common/global_config'; 37import {InMemoryStorage} from 'common/in_memory_storage'; 38import {PersistentStore} from 'common/persistent_store'; 39import {Store} from 'common/store'; 40import {Timestamp} from 'common/time'; 41import {UrlUtils} from 'common/url_utils'; 42import {UserNotifier} from 'common/user_notifier'; 43import {CrossToolProtocol} from 'cross_tool/cross_tool_protocol'; 44import {Analytics} from 'logging/analytics'; 45import {ProgressListener} from 'messaging/progress_listener'; 46import { 47 AppFilesCollected, 48 AppFilesUploaded, 49 AppInitialized, 50 AppRefreshDumpsRequest, 51 AppResetRequest, 52 AppTraceViewRequest, 53 DarkModeToggled, 54 WinscopeEvent, 55 WinscopeEventType, 56} from 'messaging/winscope_event'; 57import {WinscopeEventListener} from 'messaging/winscope_event_listener'; 58import {AdbConnection} from 'trace_collection/adb_connection'; 59import {AdbFiles} from 'trace_collection/adb_files'; 60import {ProxyConnection} from 'trace_collection/proxy_connection'; 61import {iconDividerStyle} from 'viewers/components/styles/icon_divider.styles'; 62import {ViewerInputMethodComponent} from 'viewers/components/viewer_input_method_component'; 63import {Viewer} from 'viewers/viewer'; 64import {ViewerInputComponent} from 'viewers/viewer_input/viewer_input_component'; 65import {ViewerJankCujsComponent} from 'viewers/viewer_jank_cujs/viewer_jank_cujs_component'; 66import {ViewerMediaBasedComponent} from 'viewers/viewer_media_based/viewer_media_based_component'; 67import {ViewerProtologComponent} from 'viewers/viewer_protolog/viewer_protolog_component'; 68import {ViewerSearchComponent} from 'viewers/viewer_search/viewer_search_component'; 69import {ViewerSurfaceFlingerComponent} from 'viewers/viewer_surface_flinger/viewer_surface_flinger_component'; 70import {ViewerTransactionsComponent} from 'viewers/viewer_transactions/viewer_transactions_component'; 71import {ViewerTransitionsComponent} from 'viewers/viewer_transitions/viewer_transitions_component'; 72import {ViewerViewCaptureComponent} from 'viewers/viewer_view_capture/viewer_view_capture_component'; 73import {ViewerWindowManagerComponent} from 'viewers/viewer_window_manager/viewer_window_manager_component'; 74import {CollectTracesComponent} from './collect_traces_component'; 75import {ShortcutsComponent} from './shortcuts_component'; 76import {SnackBarOpener} from './snack_bar_opener'; 77import {TimelineComponent} from './timeline/timeline_component'; 78import {TraceViewComponent} from './trace_view_component'; 79import {UploadTracesComponent} from './upload_traces_component'; 80 81@Component({ 82 selector: 'app-root', 83 encapsulation: ViewEncapsulation.None, 84 template: ` 85 <mat-toolbar class="toolbar"> 86 <div class="horizontal-align vertical-align"> 87 <img class="app-title fixed" [src]="getLogoUrl()"/> 88 </div> 89 90 <div class="horizontal-align vertical-align"> 91 <div *ngIf="showDataLoadedElements" class="download-files-section"> 92 <div class="file-descriptor vertical-align"> 93 <button 94 mat-icon-button 95 *ngIf="showCrossToolSyncButton()" 96 [matTooltip]="getCrossToolSyncTooltip()" 97 class="cross-tool-sync-button" 98 (click)="onCrossToolSyncButtonClick()" 99 [color]="getCrossToolSyncButtonColor()"> 100 <mat-icon class="material-symbols-outlined">cloud_sync</mat-icon> 101 </button> 102 <span *ngIf="!isEditingFilename" class="download-file-info mat-body-2"> 103 {{ filenameFormControl.value }} 104 </span> 105 <span *ngIf="!isEditingFilename" class="download-file-ext mat-body-2">.zip</span> 106 <mat-form-field 107 class="file-name-input-field" 108 *ngIf="isEditingFilename" 109 floatLabel="always" 110 (keydown.esc)="trySubmitFilename()" 111 (keydown.enter)="trySubmitFilename()" 112 (focusout)="trySubmitFilename()" 113 matTooltip="Allowed: A-Z a-z 0-9 . _ - #"> 114 <mat-label>Edit file name</mat-label> 115 <input matInput class="right-align" [formControl]="filenameFormControl" /> 116 <span matSuffix>.zip</span> 117 </mat-form-field> 118 <button 119 *ngIf="isEditingFilename" 120 mat-icon-button 121 class="check-button" 122 matTooltip="Submit file name" 123 (click)="trySubmitFilename()"> 124 <mat-icon>check</mat-icon> 125 </button> 126 <button 127 *ngIf="!isEditingFilename" 128 mat-icon-button 129 class="edit-button" 130 matTooltip="Edit file name" 131 (click)="onPencilIconClick()"> 132 <mat-icon>edit</mat-icon> 133 </button> 134 <button 135 mat-icon-button 136 [disabled]="isEditingFilename" 137 matTooltip="Download all traces" 138 class="save-button" 139 (click)="onDownloadTracesButtonClick()"> 140 <mat-icon class="material-symbols-outlined">download</mat-icon> 141 </button> 142 </div> 143 <mat-progress-bar 144 *ngIf="downloadProgress !== undefined" 145 mode="determinate" 146 [value]="downloadProgress"> 147 </mat-progress-bar> 148 </div> 149 150 <div *ngIf="showDataLoadedElements" class="icon-divider toolbar-icon-divider"></div> 151 <button 152 *ngIf="showDataLoadedElements && dumpsUploaded()" 153 color="primary" 154 mat-icon-button 155 matTooltip="Refresh dumps" 156 class="refresh-dumps" 157 (click)="onRefreshDumpsButtonClick()"> 158 <mat-icon class="material-symbols-outlined">refresh</mat-icon> 159 </button> 160 <button 161 *ngIf="showDataLoadedElements" 162 mat-icon-button 163 matTooltip="Upload or collect new trace" 164 class="upload-new" 165 (click)="onUploadNewButtonClick()"> 166 <mat-icon class="material-symbols-outlined">upload</mat-icon> 167 </button> 168 169 <button 170 mat-icon-button 171 matTooltip="Shortcuts" 172 class="shortcuts" 173 (click)="openShortcutsPanel()"> 174 <mat-icon>keyboard_command_key</mat-icon> 175 </button> 176 177 <button 178 mat-icon-button 179 matTooltip="Documentation" 180 class="documentation" 181 (click)="goToDocumentation()"> 182 <mat-icon>menu_book</mat-icon> 183 </button> 184 185 <button 186 mat-icon-button 187 class="report-bug" 188 matTooltip="Report bug" 189 (click)="goToBuganizer()"> 190 <mat-icon>bug_report</mat-icon> 191 </button> 192 193 <button 194 mat-icon-button 195 class="dark-mode" 196 matTooltip="Switch to {{ isDarkModeOn ? 'light' : 'dark' }} mode" 197 (click)="toggleDarkMode()"> 198 <mat-icon> 199 {{ isDarkModeOn ? 'brightness_5' : 'brightness_4' }} 200 </mat-icon> 201 </button> 202 </div> 203 </mat-toolbar> 204 205 <mat-divider></mat-divider> 206 207 <mat-drawer-container autosize disableClose autoFocus> 208 <mat-drawer-content> 209 <ng-container *ngIf="dataLoaded; else noLoadedTracesBlock"> 210 <trace-view class="viewers" [viewers]="viewers" [store]="store"></trace-view> 211 212 <mat-divider></mat-divider> 213 </ng-container> 214 </mat-drawer-content> 215 216 <mat-drawer #drawer mode="overlay" opened="true" [baseHeight]="collapsedTimelineHeight"> 217 <timeline 218 *ngIf="dataLoaded" 219 [allTraces]="tracePipeline.getTraces()" 220 [timelineData]="timelineData" 221 [store]="store" 222 (collapsedTimelineSizeChanged)="onCollapsedTimelineSizeChanged($event)"></timeline> 223 </mat-drawer> 224 </mat-drawer-container> 225 226 <ng-template #noLoadedTracesBlock> 227 <div class="center"> 228 <div class="landing-content"> 229 <h1 class="welcome-info mat-headline"> 230 Welcome to Winscope. Please select source to view traces. 231 </h1> 232 233 <div class="card-grid landing-grid"> 234 <collect-traces 235 class="collect-traces-card homepage-card" 236 [storage]="traceConfigStorage" 237 [adbConnection]="adbConnection" 238 (filesCollected)="onFilesCollected($event)"></collect-traces> 239 240 <upload-traces 241 #uploadTraces 242 class="upload-traces-card homepage-card" 243 [tracePipeline]="tracePipeline" 244 (filesUploaded)="onFilesUploaded($event)" 245 (viewTracesButtonClick)="onViewTracesButtonClick()" 246 (downloadTracesClick)="onDownloadTracesButtonClick(uploadTraces)"></upload-traces> 247 </div> 248 </div> 249 </div> 250 </ng-template> 251 `, 252 styles: [ 253 ` 254 .toolbar { 255 gap: 10px; 256 justify-content: space-between; 257 min-height: 64px; 258 } 259 .app-title { 260 height: 100%; 261 } 262 .welcome-info { 263 margin: 16px 0 6px 0; 264 text-align: center; 265 } 266 .homepage-card { 267 display: flex; 268 flex-direction: column; 269 flex: 1; 270 overflow: auto; 271 height: 870px; 272 } 273 .horizontal-align { 274 justify-content: center; 275 } 276 .vertical-align { 277 text-align: center; 278 align-items: center; 279 overflow-x: hidden; 280 display: flex; 281 } 282 .fixed { 283 min-width: fit-content; 284 } 285 .download-files-section { 286 overflow-x: hidden; 287 } 288 .file-descriptor { 289 font-size: 14px; 290 padding-left: 10px; 291 max-width: 700px; 292 } 293 .download-file-info { 294 text-overflow: ellipsis; 295 overflow-x: hidden; 296 padding-top: 3px; 297 max-width: 650px; 298 } 299 .download-file-ext { 300 padding-top: 3px; 301 } 302 .file-name-input-field .right-align { 303 text-align: right; 304 } 305 .file-name-input-field .mat-form-field-wrapper { 306 padding-bottom: 10px; 307 width: 600px; 308 } 309 .toolbar-icon-divider { 310 margin-right: 6px; 311 margin-left: 6px; 312 height: 20px; 313 } 314 .viewers { 315 height: 0; 316 flex-grow: 1; 317 display: flex; 318 flex-direction: column; 319 overflow: auto; 320 } 321 .center { 322 display: flex; 323 align-content: center; 324 flex-direction: column; 325 justify-content: center; 326 align-items: center; 327 justify-items: center; 328 flex-grow: 1; 329 } 330 .landing-content { 331 width: 100%; 332 } 333 .landing-content .card-grid { 334 max-width: 1800px; 335 flex-grow: 1; 336 margin: auto; 337 } 338 `, 339 iconDividerStyle, 340 ], 341}) 342export class AppComponent implements WinscopeEventListener { 343 title = 'winscope'; 344 timelineData = new TimelineData(); 345 abtChromeExtensionProtocol = new AbtChromeExtensionProtocol(); 346 crossToolProtocol: CrossToolProtocol; 347 dataLoaded = false; 348 showDataLoadedElements = false; 349 collapsedTimelineHeight = 0; 350 isEditingFilename = false; 351 store = new PersistentStore(); 352 viewers: Viewer[] = []; 353 354 isDarkModeOn = false; 355 changeDetectorRef: ChangeDetectorRef; 356 tracePipeline: TracePipeline; 357 mediator: Mediator; 358 currentTimestamp?: Timestamp; 359 filenameFormControl = new FormControl( 360 'winscope', 361 Validators.compose([ 362 Validators.required, 363 Validators.pattern(FileUtils.DOWNLOAD_FILENAME_REGEX), 364 ]), 365 ); 366 adbConnection: AdbConnection = new ProxyConnection(); 367 traceConfigStorage: Store; 368 downloadProgress: number | undefined; 369 370 @ViewChild(UploadTracesComponent) 371 uploadTracesComponent?: UploadTracesComponent; 372 @ViewChild(CollectTracesComponent) 373 collectTracesComponent?: CollectTracesComponent; 374 @ViewChild(TraceViewComponent) traceViewComponent?: TraceViewComponent; 375 @ViewChild(TimelineComponent) timelineComponent?: TimelineComponent; 376 377 constructor( 378 @Inject(Injector) injector: Injector, 379 @Inject(ChangeDetectorRef) changeDetectorRef: ChangeDetectorRef, 380 @Inject(SnackBarOpener) snackbarOpener: SnackBarOpener, 381 @Inject(Title) private pageTitle: Title, 382 @Inject(NgZone) private ngZone: NgZone, 383 @Inject(MatDialog) private dialog: MatDialog, 384 ) { 385 this.changeDetectorRef = changeDetectorRef; 386 UserNotifier.setSnackBarOpener(snackbarOpener); 387 this.tracePipeline = new TracePipeline(); 388 this.crossToolProtocol = new CrossToolProtocol( 389 this.tracePipeline.getTimestampConverter(), 390 ); 391 this.mediator = new Mediator( 392 this.tracePipeline, 393 this.timelineData, 394 this.abtChromeExtensionProtocol, 395 this.crossToolProtocol, 396 this, 397 new PersistentStore(), 398 ); 399 400 const storeDarkMode = this.store.get('dark-mode'); 401 const prefersDarkQuery = window.matchMedia?.( 402 '(prefers-color-scheme: dark)', 403 ); 404 this.setDarkMode( 405 storeDarkMode ? storeDarkMode === 'true' : prefersDarkQuery.matches, 406 ); 407 408 if (!customElements.get('viewer-input-method')) { 409 customElements.define( 410 'viewer-input-method', 411 createCustomElement(ViewerInputMethodComponent, {injector}), 412 ); 413 } 414 if (!customElements.get('viewer-protolog')) { 415 customElements.define( 416 'viewer-protolog', 417 createCustomElement(ViewerProtologComponent, {injector}), 418 ); 419 } 420 if (!customElements.get('viewer-media-based')) { 421 customElements.define( 422 'viewer-media-based', 423 createCustomElement(ViewerMediaBasedComponent, {injector}), 424 ); 425 } 426 if (!customElements.get('viewer-surface-flinger')) { 427 customElements.define( 428 'viewer-surface-flinger', 429 createCustomElement(ViewerSurfaceFlingerComponent, {injector}), 430 ); 431 } 432 if (!customElements.get('viewer-transactions')) { 433 customElements.define( 434 'viewer-transactions', 435 createCustomElement(ViewerTransactionsComponent, {injector}), 436 ); 437 } 438 if (!customElements.get('viewer-window-manager')) { 439 customElements.define( 440 'viewer-window-manager', 441 createCustomElement(ViewerWindowManagerComponent, {injector}), 442 ); 443 } 444 if (!customElements.get('viewer-transitions')) { 445 customElements.define( 446 'viewer-transitions', 447 createCustomElement(ViewerTransitionsComponent, {injector}), 448 ); 449 } 450 if (!customElements.get('viewer-view-capture')) { 451 customElements.define( 452 'viewer-view-capture', 453 createCustomElement(ViewerViewCaptureComponent, {injector}), 454 ); 455 } 456 if (!customElements.get('viewer-jank-cujs')) { 457 customElements.define( 458 'viewer-jank-cujs', 459 createCustomElement(ViewerJankCujsComponent, {injector}), 460 ); 461 } 462 if (!customElements.get('viewer-input')) { 463 customElements.define( 464 'viewer-input', 465 createCustomElement(ViewerInputComponent, {injector}), 466 ); 467 } 468 if (!customElements.get('viewer-search')) { 469 customElements.define( 470 'viewer-search', 471 createCustomElement(ViewerSearchComponent, {injector}), 472 ); 473 } 474 475 this.traceConfigStorage = 476 globalConfig.MODE === 'PROD' 477 ? new PersistentStore() 478 : new InMemoryStorage(); 479 480 window.onunhandledrejection = (evt) => { 481 Analytics.Error.logGlobalException(evt.reason); 482 }; 483 } 484 485 async ngAfterViewInit() { 486 await this.mediator.onWinscopeEvent(new AppInitialized()); 487 } 488 489 ngAfterViewChecked() { 490 this.mediator.setUploadTracesComponent(this.uploadTracesComponent); 491 this.mediator.setCollectTracesComponent(this.collectTracesComponent); 492 this.mediator.setTraceViewComponent(this.traceViewComponent); 493 this.mediator.setTimelineComponent(this.timelineComponent); 494 } 495 496 onCollapsedTimelineSizeChanged(height: number) { 497 this.collapsedTimelineHeight = height; 498 this.changeDetectorRef.detectChanges(); 499 } 500 501 getLogoUrl(): string { 502 const logoPath = this.isDarkModeOn 503 ? 'logo_dark_mode.svg' 504 : 'logo_light_mode.svg'; 505 return UrlUtils.getRootUrl() + logoPath; 506 } 507 508 async setDarkMode(enabled: boolean) { 509 document.body.classList.toggle('dark-mode', enabled); 510 this.store.add('dark-mode', `${enabled}`); 511 this.isDarkModeOn = enabled; 512 await this.mediator.onWinscopeEvent(new DarkModeToggled(enabled)); 513 } 514 515 onPencilIconClick() { 516 this.isEditingFilename = true; 517 } 518 519 trySubmitFilename() { 520 if (this.filenameFormControl.invalid) { 521 return; 522 } 523 this.isEditingFilename = false; 524 this.pageTitle.setTitle(`Winscope | ${this.filenameFormControl.value}`); 525 } 526 527 async onDownloadTracesButtonClick(progressListener: ProgressListener = this) { 528 if (this.filenameFormControl.invalid) { 529 return; 530 } 531 const archiveBlob = 532 await this.tracePipeline.makeZipArchiveWithLoadedTraceFiles( 533 (perc: number) => { 534 progressListener.onProgressUpdate('Downloading', 90 * perc); 535 }, 536 ); 537 const archiveFilename = `${ 538 this.showDataLoadedElements 539 ? this.filenameFormControl.value 540 : this.tracePipeline.getDownloadArchiveFilename() 541 }.zip`; 542 this.downloadTraces(archiveBlob, archiveFilename); 543 progressListener.onOperationFinished(true); 544 } 545 546 async onFilesCollected(files: AdbFiles) { 547 await this.mediator.onWinscopeEvent(new AppFilesCollected(files)); 548 } 549 550 async onFilesUploaded(files: File[]) { 551 await this.mediator.onWinscopeEvent(new AppFilesUploaded(files)); 552 } 553 554 async onRefreshDumpsButtonClick() { 555 Analytics.Tracing.logRefreshDumps(); 556 await this.mediator.onWinscopeEvent(new AppRefreshDumpsRequest()); 557 } 558 559 async onUploadNewButtonClick() { 560 await this.mediator.onWinscopeEvent(new AppResetRequest()); 561 this.store.clear('treeView'); 562 } 563 564 async onViewTracesButtonClick() { 565 await this.mediator.onWinscopeEvent(new AppTraceViewRequest()); 566 } 567 568 onProgressUpdate(message: string, progressPercentage: number | undefined) { 569 this.ngZone.run(() => { 570 this.downloadProgress = progressPercentage; 571 }); 572 } 573 574 onOperationFinished(success: boolean) { 575 this.ngZone.run(() => { 576 this.downloadProgress = undefined; 577 }); 578 } 579 580 downloadTraces(blob: Blob, filename: string) { 581 const url = window.URL.createObjectURL(blob); 582 Download.fromUrl(url, filename); 583 } 584 585 async onWinscopeEvent(event: WinscopeEvent) { 586 await event.visit(WinscopeEventType.VIEWERS_LOADED, async (event) => { 587 this.viewers = event.viewers; 588 this.filenameFormControl.setValue( 589 this.tracePipeline.getDownloadArchiveFilename(), 590 ); 591 this.pageTitle.setTitle(`Winscope | ${this.filenameFormControl.value}`); 592 this.isEditingFilename = false; 593 594 // some elements e.g. timeline require dataLoaded to be set outside NgZone to render 595 this.dataLoaded = true; 596 this.changeDetectorRef.detectChanges(); 597 598 // tooltips must be rendered inside ngZone due to limitation of MatTooltip, 599 // therefore toolbar elements controlled by a different boolean 600 this.ngZone.run(() => { 601 this.showDataLoadedElements = true; 602 }); 603 }); 604 605 await event.visit(WinscopeEventType.VIEWERS_UNLOADED, async (event) => { 606 this.dataLoaded = false; 607 this.showDataLoadedElements = false; 608 this.pageTitle.setTitle('Winscope'); 609 this.changeDetectorRef.detectChanges(); 610 }); 611 } 612 613 openShortcutsPanel() { 614 this.dialog.open(ShortcutsComponent, { 615 height: 'fit-content', 616 maxWidth: '860px', 617 }); 618 } 619 620 goToDocumentation() { 621 Analytics.Help.logDocumentationOpened(); 622 this.goToLink( 623 'https://source.android.com/docs/core/graphics/tracing-win-transitions', 624 ); 625 } 626 627 goToBuganizer() { 628 Analytics.Help.logBuganizerOpened(); 629 this.goToLink('https://b.corp.google.com/issues/new?component=909476'); 630 } 631 632 toggleDarkMode() { 633 if (!this.isDarkModeOn) { 634 Analytics.Settings.logDarkModeEnabled(); 635 } 636 this.setDarkMode(!this.isDarkModeOn); 637 } 638 639 dumpsUploaded() { 640 return !this.timelineData.hasMoreThanOneDistinctTimestamp(); 641 } 642 643 showCrossToolSyncButton() { 644 return this.crossToolProtocol.isConnected(); 645 } 646 647 getCrossToolSyncTooltip() { 648 const currStatus = this.crossToolProtocol.getAllowTimestampSync(); 649 650 return `Cross Tool Sync ${this.translateStatus( 651 currStatus, 652 )} (Click to turn ${this.translateStatus(!currStatus)})`; 653 } 654 655 onCrossToolSyncButtonClick() { 656 this.crossToolProtocol.setAllowTimestampSync( 657 !this.crossToolProtocol.getAllowTimestampSync(), 658 ); 659 Analytics.Settings.logCrossToolSync( 660 this.crossToolProtocol.getAllowTimestampSync(), 661 ); 662 } 663 664 getCrossToolSyncButtonColor() { 665 return this.crossToolProtocol.getAllowTimestampSync() 666 ? 'primary' 667 : 'accent'; 668 } 669 670 private goToLink(url: string) { 671 window.open(url, '_blank'); 672 } 673 674 private translateStatus(status: boolean) { 675 return status ? 'ON' : 'OFF'; 676 } 677} 678