Display progress bar while computing frame mapping

- Move duplicated trace load logic from TraceUploadComponent/TraceCollectionComponent into mediator
- Open snackbar messages from Mediator
- Change TraceUploadComponent's interface (FileDownloadListener -> ProgressListener)
- Notify TraceUploadComponent about frame mapping progress

Bug: b/256564627
Test: npm run build:all && npm run test:all
Change-Id: I43412fc8eba2806feb2170ea50702333cf1dd963
This commit is contained in:
Kean Mariotti
2023-03-22 10:15:37 +00:00
parent 297b641a04
commit 0b2bfff758
14 changed files with 384 additions and 229 deletions

View File

@@ -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,

View File

@@ -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')">
<mat-icon> bug_report </mat-icon>
<mat-icon> bug_report</mat-icon>
</button>
<button
@@ -122,14 +124,14 @@ import {UploadTracesComponent} from './upload_traces_component';
<div class="card-grid landing-grid">
<collect-traces
class="collect-traces-card homepage-card"
[tracePipeline]="tracePipeline"
(traceDataLoaded)="mediator.onWinscopeTraceDataLoaded()"
(filesCollected)="mediator.onWinscopeFilesCollected($event)"
[store]="store"></collect-traces>
<upload-traces
class="upload-traces-card homepage-card"
[tracePipeline]="tracePipeline"
(traceDataLoaded)="mediator.onWinscopeTraceDataLoaded()"></upload-traces>
(filesUploaded)="mediator.onWinscopeFilesUploaded($event)"
(viewTracesButtonClick)="mediator.onWinscopeViewTracesRequest()"></upload-traces>
</div>
</div>
</div>
@@ -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);
}

View File

@@ -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';
<div
*ngIf="
connect.isStartTraceState() || connect.isEndTraceState() || connect.isLoadDataState()
connect.isStartTraceState() || connect.isEndTraceState() || isOperationInProgress()
"
class="trace-collection-config">
<mat-list>
@@ -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
</button>
</p>
@@ -126,7 +124,7 @@ import {ParserErrorSnackBarComponent} from './parser_error_snack_bar_component';
<mat-tab-group class="tracing-tabs">
<mat-tab
label="Trace"
[disabled]="connect.isEndTraceState() || connect.isLoadDataState()">
[disabled]="connect.isEndTraceState() || isOperationInProgress()">
<div class="tabbed-section">
<div class="trace-section" *ngIf="connect.isStartTraceState()">
<trace-config></trace-config>
@@ -150,8 +148,10 @@ import {ParserErrorSnackBarComponent} from './parser_error_snack_bar_component';
</div>
</div>
<div *ngIf="connect.isLoadDataState()" class="load-data">
<load-progress [progressPercentage]="loadProgress" [message]="'Loading data...'">
<div *ngIf="isOperationInProgress()" class="load-data">
<load-progress
[progressPercentage]="progressPercentage"
[message]="progressMessage">
</load-progress>
<div class="end-btn">
<button color="primary" mat-raised-button (click)="endTrace()" disabled="true">
@@ -161,9 +161,7 @@ import {ParserErrorSnackBarComponent} from './parser_error_snack_bar_component';
</div>
</div>
</mat-tab>
<mat-tab
label="Dump"
[disabled]="connect.isEndTraceState() || connect.isLoadDataState()">
<mat-tab label="Dump" [disabled]="connect.isEndTraceState() || isOperationInProgress()">
<div class="tabbed-section">
<div class="dump-section" *ngIf="connect.isStartTraceState()">
<h3 class="mat-subheading-2">Dump targets</h3>
@@ -184,9 +182,9 @@ import {ParserErrorSnackBarComponent} from './parser_error_snack_bar_component';
</div>
<load-progress
*ngIf="connect.isLoadDataState()"
[progressPercentage]="loadProgress"
[message]="'Loading data...'">
*ngIf="isOperationInProgress()"
[progressPercentage]="progressPercentage"
[message]="progressMessage">
</load-progress>
</div>
</mat-tab>
@@ -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<void>();
@Output() filesCollected = new EventEmitter<File[]>();
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();
}
}

View File

@@ -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;
}
}

View File

@@ -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: `
<div class="snack-bar-container">
<p *ngFor="let message of messages" class="mat-body-1">
{{ message }}
</p>
<button color="primary" mat-button class="snack-bar-action" (click)="snackBarRef.dismiss()">
Close
</button>
</div>
`,
styles: [
`
.snack-bar-container {
display: flex;
flex-direction: column;
}
.snack-bar-action {
margin-left: 12px;
}
`,
],
})
export class SnackBarComponent {
constructor(
@Inject(MatSnackBarRef) public snackBarRef: MatSnackBarRef<SnackBarComponent>,
@Inject(MAT_SNACK_BAR_DATA) public messages: string[]
) {}
}

View File

@@ -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: `
<div class="snack-bar-container">
<p *ngFor="let message of messages" class="mat-body-1">
{{ message }}
</p>
<button color="primary" mat-button class="snack-bar-action" (click)="snackBarRef.dismiss()">
Close
</button>
</div>
`,
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<ParserErrorSnackBarComponent>,
@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 : '<no file name>';
const traceTypeName =
error.traceType !== undefined ? TRACE_INFO[error.traceType].name : '<unknown>';
@@ -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<ParserErrorType, ParserError[]> {
private groupErrorsByType(errors: ParserError[]): Map<ParserErrorType, ParserError[]> {
const groups = new Map<ParserErrorType, ParserError[]>();
errors.forEach((error) => {

View File

@@ -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
}
}

View File

@@ -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<void>();
@Output() filesUploaded = new EventEmitter<File[]>();
@Output() viewTracesButtonClick = new EventEmitter<void>();
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[] {

View File

@@ -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<void>((resolve) => setTimeout(resolve, 10));
this.tracePipeline.buildTraces();
this.currentProgressListener?.onOperationFinished();
this.timelineData.initialize(
this.tracePipeline.getTraces(),
this.tracePipeline.getScreenRecordingVideo()

View File

@@ -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([]);
};
});

View File

@@ -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;
}

View File

@@ -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
}
}

View File

@@ -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<void>;
import {ParserError} from 'parsers/parser_factory';
export interface UserNotificationListener {
onParserErrors(errors: ParserError[]): void;
}

View File

@@ -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<boolean> => {
return element(by.css('upload-snack-bar')).isPresent();
return element(by.css('snack-bar')).isPresent();
};
const checkRendersSurfaceFlingerView = async () => {