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