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:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
49
tools/winscope/src/app/components/snack_bar_component.ts
Normal file
49
tools/winscope/src/app/components/snack_bar_component.ts
Normal 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[]
|
||||
) {}
|
||||
}
|
||||
@@ -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) => {
|
||||
24
tools/winscope/src/app/components/snack_bar_opener_stub.ts
Normal file
24
tools/winscope/src/app/components/snack_bar_opener_stub.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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([]);
|
||||
};
|
||||
});
|
||||
|
||||
20
tools/winscope/src/interfaces/progress_listener.ts
Normal file
20
tools/winscope/src/interfaces/progress_listener.ts
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user