diff --git a/tools/winscope/src/app/app_module.ts b/tools/winscope/src/app/app_module.ts index 903765fe3..a98e97c97 100644 --- a/tools/winscope/src/app/app_module.ts +++ b/tools/winscope/src/app/app_module.ts @@ -67,7 +67,7 @@ import { } from './components/bottomnav/bottom_drawer_component'; import {CollectTracesComponent} from './components/collect_traces_component'; import {LoadProgressComponent} from './components/load_progress_component'; -import {ParserErrorSnackBarComponent} from './components/parser_error_snack_bar_component'; +import {SnackBarComponent} from './components/snack_bar_component'; import {ExpandedTimelineComponent} from './components/timeline/expanded_timeline_component'; import {MiniTimelineComponent} from './components/timeline/mini_timeline_component'; import {SingleTimelineComponent} from './components/timeline/single_timeline_component'; @@ -101,7 +101,6 @@ import {WebAdbComponent} from './components/web_adb_component'; TreeNodePropertiesDataViewComponent, PropertyGroupsComponent, TransformMatrixComponent, - ParserErrorSnackBarComponent, PropertiesTableComponent, ImeAdditionalPropertiesComponent, CoordinatesTableComponent, @@ -109,6 +108,7 @@ import {WebAdbComponent} from './components/web_adb_component'; MiniTimelineComponent, ExpandedTimelineComponent, SingleTimelineComponent, + SnackBarComponent, MatDrawer, MatDrawerContent, MatDrawerContainer, diff --git a/tools/winscope/src/app/components/app_component.ts b/tools/winscope/src/app/components/app_component.ts index e22b7c737..6c1d7877b 100644 --- a/tools/winscope/src/app/components/app_component.ts +++ b/tools/winscope/src/app/components/app_component.ts @@ -42,6 +42,8 @@ import {ViewerScreenRecordingComponent} from 'viewers/viewer_screen_recording/vi import {ViewerSurfaceFlingerComponent} from 'viewers/viewer_surface_flinger/viewer_surface_flinger_component'; import {ViewerTransactionsComponent} from 'viewers/viewer_transactions/viewer_transactions_component'; import {ViewerWindowManagerComponent} from 'viewers/viewer_window_manager/viewer_window_manager_component'; +import {CollectTracesComponent} from './collect_traces_component'; +import {SnackBarOpener} from './snack_bar_opener'; import {TimelineComponent} from './timeline/timeline_component'; import {UploadTracesComponent} from './upload_traces_component'; @@ -73,7 +75,7 @@ import {UploadTracesComponent} from './upload_traces_component'; mat-icon-button matTooltip="Report bug" (click)="goToLink('https://b.corp.google.com/issues/new?component=909476')"> - bug_report + bug_report + (filesUploaded)="mediator.onWinscopeFilesUploaded($event)" + (viewTracesButtonClick)="mediator.onWinscopeViewTracesRequest()"> @@ -186,18 +188,12 @@ import {UploadTracesComponent} from './upload_traces_component'; export class AppComponent implements TraceDataListener { title = 'winscope'; changeDetectorRef: ChangeDetectorRef; + snackbarOpener: SnackBarOpener; tracePipeline = new TracePipeline(); timelineData = new TimelineData(); abtChromeExtensionProtocol = new AbtChromeExtensionProtocol(); crossToolProtocol = new CrossToolProtocol(); - mediator = new Mediator( - this.tracePipeline, - this.timelineData, - this.abtChromeExtensionProtocol, - this.crossToolProtocol, - this, - localStorage - ); + mediator: Mediator; states = ProxyState; store: PersistentStore = new PersistentStore(); currentTimestamp?: Timestamp; @@ -208,13 +204,25 @@ export class AppComponent implements TraceDataListener { activeTraceFileInfo = ''; collapsedTimelineHeight = 0; @ViewChild(UploadTracesComponent) uploadTracesComponent?: UploadTracesComponent; + @ViewChild(CollectTracesComponent) collectTracesComponent?: UploadTracesComponent; @ViewChild(TimelineComponent) timelineComponent?: TimelineComponent; constructor( @Inject(Injector) injector: Injector, - @Inject(ChangeDetectorRef) changeDetectorRef: ChangeDetectorRef + @Inject(ChangeDetectorRef) changeDetectorRef: ChangeDetectorRef, + @Inject(SnackBarOpener) snackBar: SnackBarOpener ) { this.changeDetectorRef = changeDetectorRef; + this.snackbarOpener = snackBar; + this.mediator = new Mediator( + this.tracePipeline, + this.timelineData, + this.abtChromeExtensionProtocol, + this.crossToolProtocol, + this, + this.snackbarOpener, + localStorage + ); const storeDarkMode = this.store.get('dark-mode'); const prefersDarkQuery = window.matchMedia?.('(prefers-color-scheme: dark)'); @@ -259,11 +267,12 @@ export class AppComponent implements TraceDataListener { } ngAfterViewInit() { - this.mediator.setUploadTracesComponent(this.uploadTracesComponent); this.mediator.onWinscopeInitialized(); } ngAfterViewChecked() { + this.mediator.setUploadTracesComponent(this.uploadTracesComponent); + this.mediator.setCollectTracesComponent(this.collectTracesComponent); this.mediator.setTimelineComponent(this.timelineComponent); } diff --git a/tools/winscope/src/app/components/collect_traces_component.ts b/tools/winscope/src/app/components/collect_traces_component.ts index 961f798f5..edcc2e478 100644 --- a/tools/winscope/src/app/components/collect_traces_component.ts +++ b/tools/winscope/src/app/components/collect_traces_component.ts @@ -26,10 +26,8 @@ import { Output, ViewEncapsulation, } from '@angular/core'; -import {MatSnackBar} from '@angular/material/snack-bar'; -import {TracePipeline} from 'app/trace_pipeline'; import {PersistentStore} from 'common/persistent_store'; -import {TraceFile} from 'trace/trace_file'; +import {ProgressListener} from 'interfaces/progress_listener'; import {Connection} from 'trace_collection/connection'; import {ProxyState} from 'trace_collection/proxy_client'; import {ProxyConnection} from 'trace_collection/proxy_connection'; @@ -40,7 +38,7 @@ import { traceConfigurations, } from 'trace_collection/trace_collection_utils'; import {TracingConfig} from 'trace_collection/tracing_config'; -import {ParserErrorSnackBarComponent} from './parser_error_snack_bar_component'; +import {LoadProgressComponent} from './load_progress_component'; @Component({ selector: 'collect-traces', @@ -102,7 +100,7 @@ import {ParserErrorSnackBarComponent} from './parser_error_snack_bar_component'; @@ -116,7 +114,7 @@ import {ParserErrorSnackBarComponent} from './parser_error_snack_bar_component'; class="change-btn" mat-button (click)="connect.resetLastDevice()" - [disabled]="connect.isEndTraceState() || connect.isLoadDataState()"> + [disabled]="connect.isEndTraceState() || isOperationInProgress()"> Change device @@ -126,7 +124,7 @@ import {ParserErrorSnackBarComponent} from './parser_error_snack_bar_component'; + [disabled]="connect.isEndTraceState() || isOperationInProgress()"> @@ -150,8 +148,10 @@ import {ParserErrorSnackBarComponent} from './parser_error_snack_bar_component'; - - + + @@ -161,9 +161,7 @@ import {ParserErrorSnackBarComponent} from './parser_error_snack_bar_component'; - + Dump targets @@ -184,9 +182,9 @@ import {ParserErrorSnackBarComponent} from './parser_error_snack_bar_component'; + *ngIf="isOperationInProgress()" + [progressPercentage]="progressPercentage" + [message]="progressMessage"> @@ -350,21 +348,22 @@ import {ParserErrorSnackBarComponent} from './parser_error_snack_bar_component'; ], encapsulation: ViewEncapsulation.None, }) -export class CollectTracesComponent implements OnInit, OnDestroy { +export class CollectTracesComponent implements OnInit, OnDestroy, ProgressListener { objectKeys = Object.keys; isAdbProxy = true; traceConfigurations = traceConfigurations; connect: Connection; tracingConfig = TracingConfig.getInstance(); - loadProgress = 0; + + isExternalOperationInProgress = false; + progressMessage = 'Fetching...'; + progressPercentage: number | undefined; + lastUiProgressUpdateTimeMs?: number; @Input() store!: PersistentStore; - @Input() tracePipeline!: TracePipeline; - - @Output() traceDataLoaded = new EventEmitter(); + @Output() filesCollected = new EventEmitter(); constructor( - @Inject(MatSnackBar) private snackBar: MatSnackBar, @Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef, @Inject(NgZone) private ngZone: NgZone ) { @@ -393,6 +392,27 @@ export class CollectTracesComponent implements OnInit, OnDestroy { this.connect.proxy?.removeOnProxyChange(this.onProxyChange); } + onProgressUpdate(message: string, progressPercentage: number | undefined) { + if (!LoadProgressComponent.canUpdateComponent(this.lastUiProgressUpdateTimeMs)) { + return; + } + this.isExternalOperationInProgress = true; + this.progressMessage = message; + this.progressPercentage = progressPercentage; + this.lastUiProgressUpdateTimeMs = Date.now(); + this.changeDetectorRef.detectChanges(); + } + + onOperationFinished() { + this.isExternalOperationInProgress = false; + this.lastUiProgressUpdateTimeMs = undefined; + this.changeDetectorRef.detectChanges(); + } + + isOperationInProgress(): boolean { + return this.connect.isLoadDataState() || this.isExternalOperationInProgress; + } + onAddKey(key: string) { if (this.connect.setProxyKey) { this.connect.setProxyKey(key); @@ -435,16 +455,14 @@ export class CollectTracesComponent implements OnInit, OnDestroy { this.tracingConfig.requestedDumps = this.requestedDumps(); const dumpSuccessful = await this.connect.dumpState(); if (dumpSuccessful) { - await this.loadFiles(); - } else { - this.tracePipeline.clear(); + this.filesCollected.emit(this.connect.adbData()); } } async endTrace() { console.log('end tracing'); await this.connect.endTrace(); - await this.loadFiles(); + this.filesCollected.emit(this.connect.adbData()); } tabClass(adbTab: boolean) { @@ -514,18 +532,8 @@ export class CollectTracesComponent implements OnInit, OnDestroy { return selected; } - private async loadFiles() { - console.log('loading files', this.connect.adbData()); - this.tracePipeline.clear(); - const traceFiles = this.connect.adbData().map((file) => new TraceFile(file)); - const parserErrors = await this.tracePipeline.loadTraceFiles(traceFiles); - ParserErrorSnackBarComponent.showIfNeeded(this.ngZone, this.snackBar, parserErrors); - this.traceDataLoaded.emit(); - console.log('finished loading data!'); - } - - private onLoadProgressUpdate(progress: number) { - this.loadProgress = progress; + private onLoadProgressUpdate(progressPercentage: number) { + this.progressPercentage = progressPercentage; this.changeDetectorRef.detectChanges(); } } diff --git a/tools/winscope/src/app/components/load_progress_component.ts b/tools/winscope/src/app/components/load_progress_component.ts index 787933c47..2c174457b 100644 --- a/tools/winscope/src/app/components/load_progress_component.ts +++ b/tools/winscope/src/app/components/load_progress_component.ts @@ -65,4 +65,15 @@ import {Component, Input} from '@angular/core'; export class LoadProgressComponent { @Input() progressPercentage?: number; @Input() message = 'Loading...'; + private static readonly MIN_UI_UPDATE_PERIOD_MS = 200; + + static canUpdateComponent(lastUpdateTimeMs: number | undefined): boolean { + if (lastUpdateTimeMs === undefined) { + return true; + } + // Limit the amount of UI updates, because the progress bar component + // renders weird stuff when updated too frequently. + // Also, this way we save some resources. + return Date.now() - lastUpdateTimeMs >= LoadProgressComponent.MIN_UI_UPDATE_PERIOD_MS; + } } diff --git a/tools/winscope/src/app/components/snack_bar_component.ts b/tools/winscope/src/app/components/snack_bar_component.ts new file mode 100644 index 000000000..e94d00c94 --- /dev/null +++ b/tools/winscope/src/app/components/snack_bar_component.ts @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, Inject} from '@angular/core'; +import {MatSnackBarRef, MAT_SNACK_BAR_DATA} from '@angular/material/snack-bar'; + +@Component({ + selector: 'snack-bar', + template: ` + + + {{ message }} + + + Close + + + `, + styles: [ + ` + .snack-bar-container { + display: flex; + flex-direction: column; + } + .snack-bar-action { + margin-left: 12px; + } + `, + ], +}) +export class SnackBarComponent { + constructor( + @Inject(MatSnackBarRef) public snackBarRef: MatSnackBarRef, + @Inject(MAT_SNACK_BAR_DATA) public messages: string[] + ) {} +} diff --git a/tools/winscope/src/app/components/parser_error_snack_bar_component.ts b/tools/winscope/src/app/components/snack_bar_opener.ts similarity index 59% rename from tools/winscope/src/app/components/parser_error_snack_bar_component.ts rename to tools/winscope/src/app/components/snack_bar_opener.ts index dd7ebede3..5b832e678 100644 --- a/tools/winscope/src/app/components/parser_error_snack_bar_component.ts +++ b/tools/winscope/src/app/components/snack_bar_opener.ts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,61 +13,41 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {Component, Inject, NgZone} from '@angular/core'; -import {MatSnackBar, MatSnackBarRef, MAT_SNACK_BAR_DATA} from '@angular/material/snack-bar'; -import {TRACE_INFO} from 'app/trace_info'; -import {ParserError, ParserErrorType} from 'parsers/parser_factory'; -@Component({ - selector: 'upload-snack-bar', - template: ` - - - {{ message }} - - - Close - - - `, - styles: [ - ` - .snack-bar-container { - display: flex; - flex-direction: column; - } - .snack-bar-action { - margin-left: 12px; - } - `, - ], -}) -export class ParserErrorSnackBarComponent { +import {Inject, Injectable, NgZone} from '@angular/core'; +import {MatSnackBar} from '@angular/material/snack-bar'; +import {TRACE_INFO} from 'app/trace_info'; +import {UserNotificationListener} from 'interfaces/user_notification_listener'; +import {ParserError, ParserErrorType} from 'parsers/parser_factory'; +import {SnackBarComponent} from './snack_bar_component'; + +@Injectable({providedIn: 'root'}) +export class SnackBarOpener implements UserNotificationListener { constructor( - @Inject(MatSnackBarRef) public snackBarRef: MatSnackBarRef, - @Inject(MAT_SNACK_BAR_DATA) public messages: string[] + @Inject(NgZone) private ngZone: NgZone, + @Inject(MatSnackBar) private snackBar: MatSnackBar ) {} - static showIfNeeded(ngZone: NgZone, snackBar: MatSnackBar, errors: ParserError[]) { - const messages = ParserErrorSnackBarComponent.convertErrorsToMessages(errors); + onParserErrors(errors: ParserError[]) { + const messages = this.convertErrorsToMessages(errors); if (messages.length === 0) { return; } - ngZone.run(() => { + this.ngZone.run(() => { // The snackbar needs to be opened within ngZone, // otherwise it will first display on the left and then will jump to the center - snackBar.openFromComponent(ParserErrorSnackBarComponent, { + this.snackBar.openFromComponent(SnackBarComponent, { data: messages, duration: 10000, }); }); } - private static convertErrorsToMessages(errors: ParserError[]): string[] { + private convertErrorsToMessages(errors: ParserError[]): string[] { const messages: string[] = []; - const groups = ParserErrorSnackBarComponent.groupErrorsByType(errors); + const groups = this.groupErrorsByType(errors); for (const [type, groupedErrors] of groups) { const CROP_THRESHOLD = 5; @@ -75,18 +55,18 @@ export class ParserErrorSnackBarComponent { const countCropped = groupedErrors.length - countUsed; groupedErrors.slice(0, countUsed).forEach((error) => { - messages.push(ParserErrorSnackBarComponent.convertErrorToMessage(error)); + messages.push(this.convertErrorToMessage(error)); }); if (countCropped > 0) { - messages.push(ParserErrorSnackBarComponent.makeCroppedMessage(type, countCropped)); + messages.push(this.makeCroppedMessage(type, countCropped)); } } return messages; } - private static convertErrorToMessage(error: ParserError): string { + private convertErrorToMessage(error: ParserError): string { const fileName = error.trace !== undefined ? error.trace.name : ''; const traceTypeName = error.traceType !== undefined ? TRACE_INFO[error.traceType].name : ''; @@ -104,7 +84,7 @@ export class ParserErrorSnackBarComponent { } } - private static makeCroppedMessage(type: ParserErrorType, count: number): string { + private makeCroppedMessage(type: ParserErrorType, count: number): string { switch (type) { case ParserErrorType.OVERRIDE: return `... (cropped ${count} overridden trace messages)`; @@ -115,7 +95,7 @@ export class ParserErrorSnackBarComponent { } } - private static groupErrorsByType(errors: ParserError[]): Map { + private groupErrorsByType(errors: ParserError[]): Map { const groups = new Map(); errors.forEach((error) => { diff --git a/tools/winscope/src/app/components/snack_bar_opener_stub.ts b/tools/winscope/src/app/components/snack_bar_opener_stub.ts new file mode 100644 index 000000000..8852bc361 --- /dev/null +++ b/tools/winscope/src/app/components/snack_bar_opener_stub.ts @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {UserNotificationListener} from 'interfaces/user_notification_listener'; +import {ParserError} from 'parsers/parser_factory'; + +export class SnackBarOpenerStub implements UserNotificationListener { + onParserErrors(errors: ParserError[]) { + // do nothing + } +} diff --git a/tools/winscope/src/app/components/upload_traces_component.ts b/tools/winscope/src/app/components/upload_traces_component.ts index f53e696f8..c9b7c1ee9 100644 --- a/tools/winscope/src/app/components/upload_traces_component.ts +++ b/tools/winscope/src/app/components/upload_traces_component.ts @@ -22,13 +22,11 @@ import { NgZone, Output, } from '@angular/core'; -import {MatSnackBar} from '@angular/material/snack-bar'; import {TRACE_INFO} from 'app/trace_info'; import {TracePipeline} from 'app/trace_pipeline'; -import {FileUtils, OnFile} from 'common/file_utils'; -import {FilesDownloadListener} from 'interfaces/files_download_listener'; -import {LoadedTraceFile, TraceFile} from 'trace/trace_file'; -import {ParserErrorSnackBarComponent} from './parser_error_snack_bar_component'; +import {ProgressListener} from 'interfaces/progress_listener'; +import {LoadedTraceFile} from 'trace/trace_file'; +import {LoadProgressComponent} from './load_progress_component'; @Component({ selector: 'upload-traces', @@ -169,18 +167,19 @@ import {ParserErrorSnackBarComponent} from './parser_error_snack_bar_component'; `, ], }) -export class UploadTracesComponent implements FilesDownloadListener { +export class UploadTracesComponent implements ProgressListener { TRACE_INFO = TRACE_INFO; isLoadingFiles = false; progressMessage = ''; progressPercentage?: number; + lastUiProgressUpdateTimeMs?: number; @Input() tracePipeline!: TracePipeline; - @Output() traceDataLoaded = new EventEmitter(); + @Output() filesUploaded = new EventEmitter(); + @Output() viewTracesButtonClick = new EventEmitter(); constructor( @Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef, - @Inject(MatSnackBar) private snackBar: MatSnackBar, @Inject(NgZone) private ngZone: NgZone ) {} @@ -188,29 +187,34 @@ export class UploadTracesComponent implements FilesDownloadListener { this.tracePipeline.clear(); } - onFilesDownloadStart() { + onProgressUpdate(message: string | undefined, progressPercentage: number | undefined) { + if (!LoadProgressComponent.canUpdateComponent(this.lastUiProgressUpdateTimeMs)) { + return; + } this.isLoadingFiles = true; - this.progressMessage = 'Downloading files...'; - this.progressPercentage = undefined; + this.progressMessage = message ? message : 'Loading...'; + this.progressPercentage = progressPercentage; + this.lastUiProgressUpdateTimeMs = Date.now(); this.changeDetectorRef.detectChanges(); } - async onFilesDownloaded(files: File[]) { - await this.processFiles(files); + onOperationFinished() { + this.isLoadingFiles = false; + this.lastUiProgressUpdateTimeMs = undefined; + this.changeDetectorRef.detectChanges(); } - async onInputFiles(event: Event) { + onInputFiles(event: Event) { const files = this.getInputFiles(event); - await this.processFiles(files); + this.filesUploaded.emit(files); } onViewTracesButtonClick() { - this.traceDataLoaded.emit(); + this.viewTracesButtonClick.emit(); } onClearButtonClick() { this.tracePipeline.clear(); - this.changeDetectorRef.detectChanges(); } onFileDragIn(e: DragEvent) { @@ -223,56 +227,18 @@ export class UploadTracesComponent implements FilesDownloadListener { e.stopPropagation(); } - async onHandleFileDrop(e: DragEvent) { + onHandleFileDrop(e: DragEvent) { e.preventDefault(); e.stopPropagation(); const droppedFiles = e.dataTransfer?.files; if (!droppedFiles) return; - await this.processFiles(Array.from(droppedFiles)); + this.filesUploaded.emit(Array.from(droppedFiles)); } onRemoveTrace(event: MouseEvent, trace: LoadedTraceFile) { event.preventDefault(); event.stopPropagation(); this.tracePipeline.removeTraceFile(trace.type); - this.changeDetectorRef.detectChanges(); - } - - private async processFiles(files: File[]) { - const UI_PROGRESS_UPDATE_PERIOD_MS = 200; - let lastUiProgressUpdate = Date.now(); - - const onProgressUpdate = (progress: number) => { - const now = Date.now(); - if (Date.now() - lastUiProgressUpdate < UI_PROGRESS_UPDATE_PERIOD_MS) { - // Let's limit the amount of UI updates, because the progress bar component - // renders weird stuff when updated too frequently - return; - } - lastUiProgressUpdate = now; - - this.progressPercentage = progress; - this.changeDetectorRef.detectChanges(); - }; - - const traceFiles: TraceFile[] = []; - const onFile: OnFile = (file: File, parentArchive?: File) => { - traceFiles.push(new TraceFile(file, parentArchive)); - }; - - this.isLoadingFiles = true; - this.progressMessage = 'Unzipping files...'; - this.changeDetectorRef.detectChanges(); - await FileUtils.unzipFilesIfNeeded(files, onFile, onProgressUpdate); - - this.progressMessage = 'Parsing files...'; - this.changeDetectorRef.detectChanges(); - const parserErrors = await this.tracePipeline.loadTraceFiles(traceFiles, onProgressUpdate); - - this.isLoadingFiles = false; - this.changeDetectorRef.detectChanges(); - - ParserErrorSnackBarComponent.showIfNeeded(this.ngZone, this.snackBar, parserErrors); } private getInputFiles(event: Event): File[] { diff --git a/tools/winscope/src/app/mediator.ts b/tools/winscope/src/app/mediator.ts index 5513b90bd..3544a2cd6 100644 --- a/tools/winscope/src/app/mediator.ts +++ b/tools/winscope/src/app/mediator.ts @@ -14,15 +14,18 @@ * limitations under the License. */ +import {FileUtils, OnFile} from 'common/file_utils'; import {BuganizerAttachmentsDownloadEmitter} from 'interfaces/buganizer_attachments_download_emitter'; -import {FilesDownloadListener} from 'interfaces/files_download_listener'; +import {ProgressListener} from 'interfaces/progress_listener'; import {RemoteBugreportReceiver} from 'interfaces/remote_bugreport_receiver'; import {RemoteTimestampReceiver} from 'interfaces/remote_timestamp_receiver'; import {RemoteTimestampSender} from 'interfaces/remote_timestamp_sender'; import {Runnable} from 'interfaces/runnable'; import {TraceDataListener} from 'interfaces/trace_data_listener'; import {TracePositionUpdateListener} from 'interfaces/trace_position_update_listener'; +import {UserNotificationListener} from 'interfaces/user_notification_listener'; import {Timestamp, TimestampType} from 'trace/timestamp'; +import {TraceFile} from 'trace/trace_file'; import {TracePosition} from 'trace/trace_position'; import {TraceType} from 'trace/trace_type'; import {Viewer} from 'viewers/viewer'; @@ -30,21 +33,19 @@ import {ViewerFactory} from 'viewers/viewer_factory'; import {TimelineData} from './timeline_data'; import {TracePipeline} from './trace_pipeline'; -export type CrossToolProtocolDependencyInversion = RemoteBugreportReceiver & +type CrossToolProtocolInterface = RemoteBugreportReceiver & RemoteTimestampReceiver & RemoteTimestampSender; -export type AbtChromeExtensionProtocolDependencyInversion = BuganizerAttachmentsDownloadEmitter & - Runnable; -export type AppComponentDependencyInversion = TraceDataListener; -export type TimelineComponentDependencyInversion = TracePositionUpdateListener; -export type UploadTracesComponentDependencyInversion = FilesDownloadListener; +type AbtChromeExtensionProtocolInterface = BuganizerAttachmentsDownloadEmitter & Runnable; export class Mediator { - private abtChromeExtensionProtocol: AbtChromeExtensionProtocolDependencyInversion; - private crossToolProtocol: CrossToolProtocolDependencyInversion; - private uploadTracesComponent?: UploadTracesComponentDependencyInversion; - private timelineComponent?: TimelineComponentDependencyInversion; - private appComponent: AppComponentDependencyInversion; + private abtChromeExtensionProtocol: AbtChromeExtensionProtocolInterface; + private crossToolProtocol: CrossToolProtocolInterface; + private uploadTracesComponent?: ProgressListener; + private collectTracesComponent?: ProgressListener; + private timelineComponent?: TracePositionUpdateListener; + private appComponent: TraceDataListener; + private userNotificationListener: UserNotificationListener; private storage: Storage; private tracePipeline: TracePipeline; @@ -53,13 +54,15 @@ export class Mediator { private isChangingCurrentTimestamp = false; private isTraceDataVisualized = false; private lastRemoteToolTimestampReceived: Timestamp | undefined; + private currentProgressListener?: ProgressListener; constructor( tracePipeline: TracePipeline, timelineData: TimelineData, - abtChromeExtensionProtocol: AbtChromeExtensionProtocolDependencyInversion, - crossToolProtocol: CrossToolProtocolDependencyInversion, - appComponent: AppComponentDependencyInversion, + abtChromeExtensionProtocol: AbtChromeExtensionProtocolInterface, + crossToolProtocol: CrossToolProtocolInterface, + appComponent: TraceDataListener, + userNotificationListener: UserNotificationListener, storage: Storage ) { this.tracePipeline = tracePipeline; @@ -67,6 +70,7 @@ export class Mediator { this.abtChromeExtensionProtocol = abtChromeExtensionProtocol; this.crossToolProtocol = crossToolProtocol; this.appComponent = appComponent; + this.userNotificationListener = userNotificationListener; this.storage = storage; this.timelineData.setOnTracePositionUpdate((position) => { @@ -94,13 +98,15 @@ export class Mediator { ); } - setUploadTracesComponent( - uploadTracesComponent: UploadTracesComponentDependencyInversion | undefined - ) { + setUploadTracesComponent(uploadTracesComponent: ProgressListener | undefined) { this.uploadTracesComponent = uploadTracesComponent; } - setTimelineComponent(timelineComponent: TimelineComponentDependencyInversion | undefined) { + setCollectTracesComponent(collectTracesComponent: ProgressListener | undefined) { + this.collectTracesComponent = collectTracesComponent; + } + + setTimelineComponent(timelineComponent: TracePositionUpdateListener | undefined) { this.timelineComponent = timelineComponent; } @@ -112,8 +118,19 @@ export class Mediator { this.resetAppToInitialState(); } - onWinscopeTraceDataLoaded() { - this.processTraces(); + async onWinscopeFilesUploaded(files: File[]) { + this.currentProgressListener = this.uploadTracesComponent; + await this.processFiles(files); + } + + async onWinscopeFilesCollected(files: File[]) { + this.currentProgressListener = this.collectTracesComponent; + await this.processFiles(files); + await this.processLoadedTraceFiles(); + } + + async onWinscopeViewTracesRequest() { + await this.processLoadedTraceFiles(); } onWinscopeTracePositionUpdate(position: TracePosition) { @@ -137,14 +154,17 @@ export class Mediator { private onBuganizerAttachmentsDownloadStart() { this.resetAppToInitialState(); - this.uploadTracesComponent?.onFilesDownloadStart(); + this.currentProgressListener = this.uploadTracesComponent; + this.currentProgressListener?.onProgressUpdate('Downloading files...', undefined); } private async onBuganizerAttachmentsDownloaded(attachments: File[]) { + this.currentProgressListener = this.uploadTracesComponent; await this.processRemoteFilesReceived(attachments); } private async onRemoteBugreportReceived(bugreport: File, timestamp?: Timestamp) { + this.currentProgressListener = this.uploadTracesComponent; await this.processRemoteFilesReceived([bugreport]); if (timestamp !== undefined) { this.onRemoteTimestampReceived(timestamp); @@ -183,11 +203,40 @@ export class Mediator { private async processRemoteFilesReceived(files: File[]) { this.resetAppToInitialState(); - this.uploadTracesComponent?.onFilesDownloaded(files); + this.processFiles(files); } - private processTraces() { + private async processFiles(files: File[]) { + let progressMessage = ''; + const onProgressUpdate = (progressPercentage: number) => { + this.currentProgressListener?.onProgressUpdate(progressMessage, progressPercentage); + }; + + const traceFiles: TraceFile[] = []; + const onFile: OnFile = (file: File, parentArchive?: File) => { + traceFiles.push(new TraceFile(file, parentArchive)); + }; + + progressMessage = 'Unzipping files...'; + this.currentProgressListener?.onProgressUpdate(progressMessage, 0); + await FileUtils.unzipFilesIfNeeded(files, onFile, onProgressUpdate); + + progressMessage = 'Parsing files...'; + this.currentProgressListener?.onProgressUpdate(progressMessage, 0); + const parserErrors = await this.tracePipeline.loadTraceFiles(traceFiles, onProgressUpdate); + this.currentProgressListener?.onOperationFinished(); + this.userNotificationListener?.onParserErrors(parserErrors); + } + + private async processLoadedTraceFiles() { + this.currentProgressListener?.onProgressUpdate('Computing frame mapping...', undefined); + + // allow the UI to update before making the main thread very busy + await new Promise((resolve) => setTimeout(resolve, 10)); + this.tracePipeline.buildTraces(); + this.currentProgressListener?.onOperationFinished(); + this.timelineData.initialize( this.tracePipeline.getTraces(), this.tracePipeline.getScreenRecordingVideo() diff --git a/tools/winscope/src/app/mediator_test.ts b/tools/winscope/src/app/mediator_test.ts index b597ddc50..24865374b 100644 --- a/tools/winscope/src/app/mediator_test.ts +++ b/tools/winscope/src/app/mediator_test.ts @@ -16,6 +16,7 @@ import {AbtChromeExtensionProtocolStub} from 'abt_chrome_extension/abt_chrome_extension_protocol_stub'; import {CrossToolProtocolStub} from 'cross_tool/cross_tool_protocol_stub'; +import {ProgressListenerStub} from 'interfaces/progress_listener_stub'; import {MockStorage} from 'test/unit/mock_storage'; import {UnitTestUtils} from 'test/unit/utils'; import {RealTimestamp} from 'trace/timestamp'; @@ -24,21 +25,24 @@ import {TracePosition} from 'trace/trace_position'; import {ViewerFactory} from 'viewers/viewer_factory'; import {ViewerStub} from 'viewers/viewer_stub'; import {AppComponentStub} from './components/app_component_stub'; +import {SnackBarOpenerStub} from './components/snack_bar_opener_stub'; import {TimelineComponentStub} from './components/timeline/timeline_component_stub'; -import {UploadTracesComponentStub} from './components/upload_traces_component_stub'; import {Mediator} from './mediator'; import {TimelineData} from './timeline_data'; import {TracePipeline} from './trace_pipeline'; describe('Mediator', () => { const viewerStub = new ViewerStub('Title'); + let inputFiles: File[]; let tracePipeline: TracePipeline; let timelineData: TimelineData; let abtChromeExtensionProtocol: AbtChromeExtensionProtocolStub; let crossToolProtocol: CrossToolProtocolStub; let appComponent: AppComponentStub; let timelineComponent: TimelineComponentStub; - let uploadTracesComponent: UploadTracesComponentStub; + let uploadTracesComponent: ProgressListenerStub; + let collectTracesComponent: ProgressListenerStub; + let snackBarOpener: SnackBarOpenerStub; let mediator: Mediator; const TIMESTAMP_10 = new RealTimestamp(10n); @@ -46,6 +50,16 @@ describe('Mediator', () => { const POSITION_10 = TracePosition.fromTimestamp(TIMESTAMP_10); const POSITION_11 = TracePosition.fromTimestamp(TIMESTAMP_11); + beforeAll(async () => { + inputFiles = [ + await UnitTestUtils.getFixtureFile('traces/elapsed_and_real_timestamp/SurfaceFlinger.pb'), + await UnitTestUtils.getFixtureFile('traces/elapsed_and_real_timestamp/WindowManager.pb'), + await UnitTestUtils.getFixtureFile( + 'traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4' + ), + ]; + }); + beforeEach(async () => { timelineComponent = new TimelineComponentStub(); tracePipeline = new TracePipeline(); @@ -54,32 +68,68 @@ describe('Mediator', () => { crossToolProtocol = new CrossToolProtocolStub(); appComponent = new AppComponentStub(); timelineComponent = new TimelineComponentStub(); - uploadTracesComponent = new UploadTracesComponentStub(); + uploadTracesComponent = new ProgressListenerStub(); + collectTracesComponent = new ProgressListenerStub(); + snackBarOpener = new SnackBarOpenerStub(); mediator = new Mediator( tracePipeline, timelineData, abtChromeExtensionProtocol, crossToolProtocol, appComponent, + snackBarOpener, new MockStorage() ); mediator.setTimelineComponent(timelineComponent); mediator.setUploadTracesComponent(uploadTracesComponent); + mediator.setCollectTracesComponent(collectTracesComponent); spyOn(ViewerFactory.prototype, 'createViewers').and.returnValue([viewerStub]); }); - it('handles data load event from Winscope', async () => { - spyOn(timelineData, 'initialize').and.callThrough(); - spyOn(appComponent, 'onTraceDataLoaded'); - spyOn(viewerStub, 'onTracePositionUpdate'); + it('handles uploaded traces from Winscope', async () => { + const spies = [ + spyOn(uploadTracesComponent, 'onProgressUpdate'), + spyOn(uploadTracesComponent, 'onOperationFinished'), + spyOn(timelineData, 'initialize').and.callThrough(), + spyOn(appComponent, 'onTraceDataLoaded'), + spyOn(viewerStub, 'onTracePositionUpdate'), + ]; - await loadTraces(); - expect(timelineData.initialize).toHaveBeenCalledTimes(0); - expect(appComponent.onTraceDataLoaded).toHaveBeenCalledTimes(0); - expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(0); + await mediator.onWinscopeFilesUploaded(inputFiles); - mediator.onWinscopeTraceDataLoaded(); + expect(uploadTracesComponent.onProgressUpdate).toHaveBeenCalled(); + expect(uploadTracesComponent.onOperationFinished).toHaveBeenCalled(); + expect(timelineData.initialize).not.toHaveBeenCalled(); + expect(appComponent.onTraceDataLoaded).not.toHaveBeenCalled(); + expect(viewerStub.onTracePositionUpdate).not.toHaveBeenCalled(); + + spies.forEach((spy) => { + spy.calls.reset(); + }); + await mediator.onWinscopeViewTracesRequest(); + + expect(uploadTracesComponent.onProgressUpdate).toHaveBeenCalled(); + expect(uploadTracesComponent.onOperationFinished).toHaveBeenCalled(); + expect(timelineData.initialize).toHaveBeenCalledTimes(1); + expect(appComponent.onTraceDataLoaded).toHaveBeenCalledOnceWith([viewerStub]); + // notifies viewer about current timestamp on creation + expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(1); + }); + + it('handles collected traces from Winscope', async () => { + const spies = [ + spyOn(collectTracesComponent, 'onProgressUpdate'), + spyOn(collectTracesComponent, 'onOperationFinished'), + spyOn(timelineData, 'initialize').and.callThrough(), + spyOn(appComponent, 'onTraceDataLoaded'), + spyOn(viewerStub, 'onTracePositionUpdate'), + ]; + + await mediator.onWinscopeFilesCollected(inputFiles); + + expect(collectTracesComponent.onProgressUpdate).toHaveBeenCalled(); + expect(collectTracesComponent.onOperationFinished).toHaveBeenCalled(); expect(timelineData.initialize).toHaveBeenCalledTimes(1); expect(appComponent.onTraceDataLoaded).toHaveBeenCalledOnceWith([viewerStub]); // notifies viewer about current timestamp on creation @@ -93,26 +143,26 @@ describe('Mediator', () => { // (b/262269229). it('handles start download event from ABT chrome extension', () => { - spyOn(uploadTracesComponent, 'onFilesDownloadStart'); - expect(uploadTracesComponent.onFilesDownloadStart).toHaveBeenCalledTimes(0); + spyOn(uploadTracesComponent, 'onProgressUpdate'); + expect(uploadTracesComponent.onProgressUpdate).toHaveBeenCalledTimes(0); abtChromeExtensionProtocol.onBuganizerAttachmentsDownloadStart(); - expect(uploadTracesComponent.onFilesDownloadStart).toHaveBeenCalledTimes(1); + expect(uploadTracesComponent.onProgressUpdate).toHaveBeenCalledTimes(1); }); it('handles empty downloaded files from ABT chrome extension', async () => { - spyOn(uploadTracesComponent, 'onFilesDownloaded'); - expect(uploadTracesComponent.onFilesDownloaded).toHaveBeenCalledTimes(0); + spyOn(uploadTracesComponent, 'onOperationFinished'); + expect(uploadTracesComponent.onOperationFinished).toHaveBeenCalledTimes(0); // Pass files even if empty so that the upload component will update the progress bar // and display error messages await abtChromeExtensionProtocol.onBuganizerAttachmentsDownloaded([]); - expect(uploadTracesComponent.onFilesDownloaded).toHaveBeenCalledTimes(1); + expect(uploadTracesComponent.onOperationFinished).toHaveBeenCalledTimes(1); }); it('propagates trace position update from timeline data', async () => { - await loadTraces(); - mediator.onWinscopeTraceDataLoaded(); + await loadTraceFiles(); + await mediator.onWinscopeViewTracesRequest(); spyOn(viewerStub, 'onTracePositionUpdate'); spyOn(timelineComponent, 'onTracePositionUpdate'); @@ -142,8 +192,8 @@ describe('Mediator', () => { describe('timestamp received from remote tool', () => { it('propagates trace position update', async () => { - await loadTraces(); - mediator.onWinscopeTraceDataLoaded(); + await loadTraceFiles(); + await mediator.onWinscopeViewTracesRequest(); spyOn(viewerStub, 'onTracePositionUpdate'); spyOn(timelineComponent, 'onTracePositionUpdate'); @@ -167,8 +217,8 @@ describe('Mediator', () => { }); it("doesn't propagate timestamp back to remote tool", async () => { - await loadTraces(); - mediator.onWinscopeTraceDataLoaded(); + await loadTraceFiles(); + await mediator.onWinscopeViewTracesRequest(); spyOn(viewerStub, 'onTracePositionUpdate'); spyOn(crossToolProtocol, 'sendTimestamp'); @@ -191,27 +241,15 @@ describe('Mediator', () => { expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(0); // apply timestamp - await loadTraces(); - mediator.onWinscopeTraceDataLoaded(); + await loadTraceFiles(); + await mediator.onWinscopeViewTracesRequest(); expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledWith(POSITION_11); }); }); - const loadTraces = async () => { - const files = [ - new TraceFile( - await UnitTestUtils.getFixtureFile('traces/elapsed_and_real_timestamp/SurfaceFlinger.pb') - ), - new TraceFile( - await UnitTestUtils.getFixtureFile('traces/elapsed_and_real_timestamp/WindowManager.pb') - ), - new TraceFile( - await UnitTestUtils.getFixtureFile( - 'traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4' - ) - ), - ]; - const errors = await tracePipeline.loadTraceFiles(files); + const loadTraceFiles = async () => { + const traceFiles = inputFiles.map((file) => new TraceFile(file)); + const errors = await tracePipeline.loadTraceFiles(traceFiles); expect(errors).toEqual([]); }; }); diff --git a/tools/winscope/src/interfaces/progress_listener.ts b/tools/winscope/src/interfaces/progress_listener.ts new file mode 100644 index 000000000..bae51a2d9 --- /dev/null +++ b/tools/winscope/src/interfaces/progress_listener.ts @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface ProgressListener { + onProgressUpdate(message: string, progressPercentage: number | undefined): void; + onOperationFinished(): void; +} diff --git a/tools/winscope/src/app/components/upload_traces_component_stub.ts b/tools/winscope/src/interfaces/progress_listener_stub.ts similarity index 75% rename from tools/winscope/src/app/components/upload_traces_component_stub.ts rename to tools/winscope/src/interfaces/progress_listener_stub.ts index 7ae7acdfb..57c723ed0 100644 --- a/tools/winscope/src/app/components/upload_traces_component_stub.ts +++ b/tools/winscope/src/interfaces/progress_listener_stub.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import {FilesDownloadListener} from 'interfaces/files_download_listener'; +import {ProgressListener} from 'interfaces/progress_listener'; -export class UploadTracesComponentStub implements FilesDownloadListener { - onFilesDownloadStart() { +export class ProgressListenerStub implements ProgressListener { + onProgressUpdate() { // do nothing } - async onFilesDownloaded(files: File[]) { + onOperationFinished() { // do nothing } } diff --git a/tools/winscope/src/interfaces/files_download_listener.ts b/tools/winscope/src/interfaces/user_notification_listener.ts similarity index 74% rename from tools/winscope/src/interfaces/files_download_listener.ts rename to tools/winscope/src/interfaces/user_notification_listener.ts index 8dd9101fb..d2b1744d0 100644 --- a/tools/winscope/src/interfaces/files_download_listener.ts +++ b/tools/winscope/src/interfaces/user_notification_listener.ts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,8 @@ * limitations under the License. */ -export interface FilesDownloadListener { - onFilesDownloadStart(): void; - onFilesDownloaded(files: File[]): Promise; +import {ParserError} from 'parsers/parser_factory'; + +export interface UserNotificationListener { + onParserErrors(errors: ParserError[]): void; } diff --git a/tools/winscope/src/test/e2e/upload_traces_test.ts b/tools/winscope/src/test/e2e/upload_traces_test.ts index 77db45483..d5a1ad38a 100644 --- a/tools/winscope/src/test/e2e/upload_traces_test.ts +++ b/tools/winscope/src/test/e2e/upload_traces_test.ts @@ -54,17 +54,17 @@ describe('Upload traces', () => { }; const checkEmitsUnsupportedFileFormatMessages = async () => { - const text = await element(by.css('upload-snack-bar')).getText(); + const text = await element(by.css('snack-bar')).getText(); expect(text).toContain('unsupported file format'); }; const checkEmitsOverriddenTracesMessages = async () => { - const text = await element(by.css('upload-snack-bar')).getText(); + const text = await element(by.css('snack-bar')).getText(); expect(text).toContain('overridden by another trace'); }; const areMessagesEmitted = async (): Promise => { - return element(by.css('upload-snack-bar')).isPresent(); + return element(by.css('snack-bar')).isPresent(); }; const checkRendersSurfaceFlingerView = async () => {
+ {{ message }} +
- {{ message }} -