Add functionality to upload traces component.

Upload multiple files via clicking to browse or drag and drop.

Test: spin up winscope and see if the files upload.

Bug: b/241571689
Change-Id: I67b801008ddcfa5be3ae724eb4db6f9b723eebd6
This commit is contained in:
Priyanka Patel
2022-08-05 18:20:19 +00:00
parent fce1e58e06
commit 42b10df22d
9 changed files with 246 additions and 48 deletions

View File

@@ -13,13 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component, Inject, Injector} from "@angular/core";
import {Component, Inject, Injector, Input} from "@angular/core";
import {createCustomElement} from "@angular/elements";
import {Timestamp, TimestampType} from "common/trace/timestamp";
import {PersistentStore} from "common/persistent_store";
import {ViewerWindowManagerComponent} from "viewers/viewer_window_manager/viewer_window_manager.component";
import {Core} from "./core";
import {ProxyState} from "trace_collection/proxy_client";
import {ProxyState, proxyClient} from "trace_collection/proxy_client";
@Component({
selector: "app-root",
@@ -30,10 +30,10 @@ import {ProxyState} from "trace_collection/proxy_client";
<div *ngIf="!dataLoaded" fxLayout="row wrap" fxLayoutGap="10px grid" class="home">
<mat-card class="homepage-card" id="collect-traces-card">
<collect-traces [(core)]="core" [(dataLoaded)]="dataLoaded" [store]="store"></collect-traces>
<collect-traces [(core)]="core" (dataLoadedChange)="onDataLoadedChange($event)"[store]="store"></collect-traces>
</mat-card>
<mat-card class="homepage-card" id="upload-traces-card">
<upload-traces [(core)]="core"></upload-traces>
<upload-traces [(core)]="core" (dataLoadedChange)="onDataLoadedChange($event)"></upload-traces>
</mat-card>
</div>
@@ -61,7 +61,8 @@ export class AppComponent {
core: Core;
states = ProxyState;
store: PersistentStore = new PersistentStore();
dataLoaded = false;
@Input() dataLoaded = false;
viewersCreated = false;
constructor(
@Inject(Injector) injector: Injector
@@ -73,12 +74,14 @@ export class AppComponent {
}
}
onCoreChange(newCore: Core) {
this.core = newCore;
}
onDataLoadedChange(loaded: boolean) {
this.dataLoaded = loaded;
onDataLoadedChange(dataLoaded: boolean) {
if (dataLoaded && !this.viewersCreated) {
this.core.createViewers();
const dummyTimestamp = this.core.getTimestamps()[1]; //TODO: get timestamp from time scrub
this.core.notifyCurrentTimestamp(dummyTimestamp);
this.viewersCreated = true;
this.dataLoaded = dataLoaded;
}
}
public notifyCurrentTimestamp() {
@@ -88,6 +91,8 @@ export class AppComponent {
public clearData() {
this.dataLoaded = false;
this.viewersCreated = false;
this.core.clearData();
proxyClient.adbData = [];
}
}

View File

@@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, Input, OnInit, Output, EventEmitter } from "@angular/core";
import { Component, Input, OnInit, Output, EventEmitter, NgZone, Inject } from "@angular/core";
import { ProxyConnection } from "trace_collection/proxy_connection";
import { Connection } from "trace_collection/connection";
import { setTraces } from "trace_collection/set_traces";
import { ProxyState } from "../trace_collection/proxy_client";
import { traceConfigurations, configMap, SelectionConfiguration, TraceConfigurationMap, EnableConfiguration } from "../trace_collection/trace_collection_utils";
import { traceConfigurations, configMap, SelectionConfiguration, EnableConfiguration } from "../trace_collection/trace_collection_utils";
import { Core } from "app/core";
import { PersistentStore } from "../common/persistent_store";
@@ -123,13 +123,12 @@ export class CollectTracesComponent implements OnInit {
store: PersistentStore = new PersistentStore();
@Input()
core: Core = new Core();
core?: Core;
@Output()
coreChange = new EventEmitter<Core>();
@Input()
dataLoaded = false;
dataLoaded = false;
@Output()
dataLoadedChange = new EventEmitter<boolean>();
@@ -143,6 +142,8 @@ export class CollectTracesComponent implements OnInit {
}
}
constructor(@Inject(NgZone) private ngZone: NgZone) {}
ngOnDestroy(): void {
this.connect.proxy?.removeOnProxyChange(this.onProxyChange);
}
@@ -257,7 +258,7 @@ export class CollectTracesComponent implements OnInit {
if (!setTraces.dumpError) {
await this.loadFiles();
} else {
this.core.clearData();
this.core?.clearData();
}
}
@@ -272,11 +273,13 @@ export class CollectTracesComponent implements OnInit {
public async loadFiles() {
console.log("loading files", this.connect.adbData());
await this.core.bootstrap(this.connect.adbData());
this.dataLoaded = true;
this.dataLoadedChange.emit(this.dataLoaded);
this.coreChange.emit(this.core);
console.log("finished loading data!");
this.core?.clearData();
await this.core?.addTraces(this.connect.adbData());
this.ngZone.run(() => {
this.dataLoaded = true;
this.dataLoadedChange.emit(this.dataLoaded);
console.log("finished loading data!");
});
}
public tabClass(adbTab: boolean) {

View File

@@ -18,9 +18,9 @@ import {TraceType} from "common/trace/trace_type";
import {Parser} from "parsers/parser";
import {ParserFactory} from "parsers/parser_factory";
import { setTraces } from "trace_collection/set_traces";
import { proxyClient } from "trace_collection/proxy_client";
import {Viewer} from "viewers/viewer";
import {ViewerFactory} from "viewers/viewer_factory";
import { Viewer } from "viewers/viewer";
import { ViewerFactory } from "viewers/viewer_factory";
import { LoadedTrace } from "app/loaded_trace";
class Core {
private parsers: Parser[];
@@ -31,11 +31,18 @@ class Core {
this.viewers = [];
}
async bootstrap(traces: Blob[]) {
this.clearData();
async addTraces(traces: Blob[]) {
traces = this.parsers.map(parser => parser.getTrace()).concat(traces);
this.parsers = await new ParserFactory().createParsers(traces);
console.log("created parsers: ", this.parsers);
}
removeTrace(type: TraceType) {
this.parsers = this.parsers.filter(parser => parser.getTraceType() !== type);
}
createViewers() {
const activeTraceTypes = this.parsers.map(parser => parser.getTraceType());
console.log("active trace types: ", activeTraceTypes);
@@ -43,10 +50,22 @@ class Core {
console.log("created viewers: ", this.viewers);
}
getLoadedTraces(): LoadedTrace[] {
return this.parsers.map((parser: Parser) => {
const name = (<File>parser.getTrace()).name;
const type = parser.getTraceType();
return {name: name, type: type};
});
}
getViews(): HTMLElement[] {
return this.viewers.map(viewer => viewer.getView());
}
loadedTraceTypes(): TraceType[] {
return this.parsers.map(parser => parser.getTraceType());
}
getTimestamps(): Timestamp[] {
for (const type of [TimestampType.REAL, TimestampType.ELAPSED]) {
const mergedTimestamps: Timestamp[] = [];
@@ -90,7 +109,6 @@ class Core {
this.parsers = [];
this.viewers = [];
setTraces.dataReady = false;
proxyClient.adbData = [];
}
}

View File

@@ -0,0 +1,6 @@
import { TraceType } from "../common/trace/trace_type";
export interface LoadedTrace {
name: string;
type: TraceType
}

View File

@@ -0,0 +1,37 @@
import { TraceType } from "../common/trace/trace_type";
const WINDOW_MANAGER_ICON = "view_compact";
const SURFACE_FLINGER_ICON = "filter_none";
const SCREEN_RECORDING_ICON = "videocam";
const TRANSACTION_ICON = "timeline";
const WAYLAND_ICON = "filter_none";
const PROTO_LOG_ICON = "notes";
const SYSTEM_UI_ICON = "filter_none";
const LAUNCHER_ICON = "filter_none";
const IME_ICON = "keyboard";
const ACCESSIBILITY_ICON = "filter_none";
const TAG_ICON = "details";
const TRACE_ERROR_ICON = "warning";
type iconMap = {
[key: number]: string;
}
export const TRACE_ICONS: iconMap = {
[TraceType.ACCESSIBILITY]: ACCESSIBILITY_ICON,
[TraceType.WINDOW_MANAGER]: WINDOW_MANAGER_ICON,
[TraceType.SURFACE_FLINGER]: SURFACE_FLINGER_ICON,
[TraceType.SCREEN_RECORDING]: SCREEN_RECORDING_ICON,
[TraceType.TRANSACTIONS]: TRANSACTION_ICON,
[TraceType.TRANSACTIONS_LEGACY]: TRANSACTION_ICON,
[TraceType.WAYLAND]: WAYLAND_ICON,
[TraceType.WAYLAND_DUMP]: WAYLAND_ICON,
[TraceType.PROTO_LOG]: PROTO_LOG_ICON,
[TraceType.SYSTEM_UI]: SYSTEM_UI_ICON,
[TraceType.LAUNCHER]: LAUNCHER_ICON,
[TraceType.INPUT_METHOD_CLIENTS]: IME_ICON,
[TraceType.INPUT_METHOD_SERVICE]: IME_ICON,
[TraceType.INPUT_METHOD_MANAGER_SERVICE]: IME_ICON,
[TraceType.TAG]: TAG_ICON,
[TraceType.ERROR]: TRACE_ERROR_ICON,
};

View File

@@ -13,50 +13,130 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, Input, Output, EventEmitter } from "@angular/core";
import { Component, Input, Inject, Output, EventEmitter, NgZone } from "@angular/core";
import { Core } from "app/core";
import { TRACE_ICONS } from "app/trace_icons";
import { LoadedTrace } from "app/loaded_trace";
@Component({
selector: "upload-traces",
template: `
<mat-card-title>Upload Traces</mat-card-title>
<mat-card-title id="title">Upload Traces</mat-card-title>
<mat-card-content>
<div id="inputfile">
<input mat-input type="file" (change)="onInputFile($event)" #fileUpload>
<div
class="drop-box"
ref="drop-box"
(dragleave)="onFileDragOut($event)"
(dragover)="onFileDragIn($event)"
(drop)="onHandleFileDrop($event)"
>
<div id="inputfile">
<input
hidden
class="input-files"
id="fileDropRef"
type="file"
(change)="onInputFile($event)"
#fileDropRef
multiple
/>
<h3 class="drop-info">Drag and drop</h3>
<h3 class="drop-info">or click to upload</h3>
<button mat-raised-button for="fileDropRef" (click)="fileDropRef.click()">
Choose File
</button>
<div *ngIf="this.loadedTraces.length > 0">
<button mat-raised-button (click)="onLoadData()">Load Data</button>
<button mat-raised-button (click)="onClearData()">Clear All</button>
</div>
</div>
</div>
<mat-list
class="uploaded-files"
*ngIf="this.loadedTraces.length > 0"
>
<mat-list-item *ngFor="let trace of loadedTraces">
<mat-icon>{{TRACE_ICONS[trace.type]}}</mat-icon>
<span>{{trace.name}} ({{trace.type}})
</span>
<button
(click)="onRemoveTrace(trace)"
><mat-icon class="file-icon">close</mat-icon>
</button>
</mat-list-item>
</mat-list>
</mat-card-content>
`,
styles: [".drop-info{font-weight: normal;}"]
})
export class UploadTracesComponent {
@Input()
core: Core = new Core();
core?: Core;
@Output()
coreChange = new EventEmitter<Core>();
dataLoaded = false;
@Output()
dataLoadedChange = new EventEmitter<boolean>();
loadedTraces: LoadedTrace[] = [];
TRACE_ICONS = TRACE_ICONS;
constructor(@Inject(NgZone) private ngZone: NgZone) {}
public async onInputFile(event: Event) {
const files = this.getInputFiles(event);
await this.core.bootstrap(files);
await this.processFiles(files);
}
const viewersDiv = document.querySelector("div#viewers")!;
viewersDiv.innerHTML = "";
this.core.getViews().forEach(view => viewersDiv!.appendChild(view) );
this.coreChange.emit(this.core);
const timestampsDiv = document.querySelector("div#timestamps")!;
timestampsDiv.innerHTML = `Retrieved ${this.core.getTimestamps().length} unique timestamps`;
public async processFiles(files: File[]) {
await this.core?.addTraces(files);
this.ngZone.run(() => {
if (this.core) this.loadedTraces = this.core.getLoadedTraces();
});
}
//TODO: extend with support for multiple files, archives, etc...
private getInputFiles(event: Event): File[] {
const files: any = (event?.target as HTMLInputElement)?.files;
if (!files || !files[0]) {
return [];
}
return Array.from(files);
}
return [files[0]];
public onLoadData() {
this.dataLoaded = true;
this.dataLoadedChange.emit(this.dataLoaded);
}
public onClearData() {
this.core?.clearData();
}
public onFileDragIn(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
}
public onFileDragOut(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
}
async onHandleFileDrop(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
const droppedFiles = e.dataTransfer?.files;
if(!droppedFiles) return;
await this.processFiles(Array.from(droppedFiles));
}
public onRemoveTrace(trace: LoadedTrace) {
this.core?.removeTrace(trace.type);
this.loadedTraces = this.loadedTraces.filter(loaded => loaded.type !== trace.type);
}
}

View File

@@ -57,6 +57,10 @@ abstract class Parser {
public abstract getTraceType(): TraceType;
public getTrace(): Blob {
return this.trace;
}
public getTimestamps(type: TimestampType): undefined|Timestamp[] {
return this.timestamps.get(type);
}

View File

@@ -43,6 +43,7 @@ class ParserFactory {
async createParsers(traces: Blob[]): Promise<Parser[]> {
const parsers: Parser[] = [];
const completedParserTypes: any[] = [];
for (const [index, trace] of traces.entries()) {
console.log(`Loading trace #${index}`);
@@ -50,9 +51,15 @@ class ParserFactory {
try {
const parser = new ParserType(trace);
await parser.parse();
parsers.push(parser);
console.log(`Successfully loaded trace with parser type ${ParserType.name}`);
break;
if (completedParserTypes.includes(ParserType)) {
console.log(`Already successfully loaded a trace with parser type ${ParserType.name}`);
break;
} else {
parsers.push(parser);
completedParserTypes.push(ParserType);
console.log(`Successfully loaded trace with parser type ${ParserType.name}`);
break;
}
}
catch(error) {
console.log(`Failed to load trace with parser type ${ParserType.name}`);

View File

@@ -53,6 +53,12 @@ mat-icon {
vertical-align: middle;
}
.file-icon {
vertical-align: middle;
outline: none !important;
background: none !important;
}
.card-block {
margin: 15px;
}
@@ -66,4 +72,36 @@ button.mat-raised-button {
.tab.inactive {
background-color:white;
color: black;
}
.drop-box {
outline: 2px dashed rgb(194, 65, 108); /* the dash box */
outline-offset: -10px;
background: white;
color: rgb(194, 65, 108);
padding: 10px 10px 10px 10px;
min-height: 200px; /* minimum height */
position: relative;
cursor: pointer;
display: flex;
flex-direction: column;
text-align: center;
align-items: center;
justify-items: center;
}
.input {
opacity: 0 !important;
width: 100%;
height: 100%;
top: 0;
left: 0;
position: absolute;
}
.file-input-prompt {
color: white;
background-color: rgb(194, 65, 108);
border-radius: 21.5px;
cursor: pointer;
}