blob: 961f798f5b07176c8892c5659dd916d7835f2deb [file] [log] [blame]
/*
* Copyright (C) 2022 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 {
ChangeDetectorRef,
Component,
EventEmitter,
Inject,
Input,
NgZone,
OnDestroy,
OnInit,
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 {Connection} from 'trace_collection/connection';
import {ProxyState} from 'trace_collection/proxy_client';
import {ProxyConnection} from 'trace_collection/proxy_connection';
import {
ConfigMap,
EnableConfiguration,
SelectionConfiguration,
traceConfigurations,
} from 'trace_collection/trace_collection_utils';
import {TracingConfig} from 'trace_collection/tracing_config';
import {ParserErrorSnackBarComponent} from './parser_error_snack_bar_component';
@Component({
selector: 'collect-traces',
template: `
<mat-card class="collect-card">
<mat-card-title class="title">Collect Traces</mat-card-title>
<mat-card-content class="collect-card-content">
<p *ngIf="connect.isConnectingState()" class="connecting-message mat-body-1">
Connecting...
</p>
<div *ngIf="!connect.adbSuccess()" class="set-up-adb">
<button
class="proxy-tab"
color="primary"
mat-stroked-button
[ngClass]="tabClass(true)"
(click)="displayAdbProxyTab()">
ADB Proxy
</button>
<!-- <button class="web-tab" color="primary" mat-raised-button [ngClass]="tabClass(false)" (click)="displayWebAdbTab()">Web ADB</button> -->
<adb-proxy
*ngIf="isAdbProxy"
[(proxy)]="connect.proxy!"
(addKey)="onAddKey($event)"></adb-proxy>
<!-- <web-adb *ngIf="!isAdbProxy"></web-adb> TODO: fix web adb workflow -->
</div>
<div *ngIf="connect.isDevicesState()" class="devices-connecting">
<div *ngIf="objectKeys(connect.devices()).length === 0" class="no-device-detected">
<p class="mat-body-3 icon"><mat-icon inline fontIcon="phonelink_erase"></mat-icon></p>
<p class="mat-body-1">No devices detected</p>
</div>
<div *ngIf="objectKeys(connect.devices()).length > 0" class="device-selection">
<p class="mat-body-1 instruction">Select a device:</p>
<mat-list *ngIf="objectKeys(connect.devices()).length > 0">
<mat-list-item
*ngFor="let deviceId of objectKeys(connect.devices())"
(click)="connect.selectDevice(deviceId)"
class="available-device">
<mat-icon matListIcon>
{{
connect.devices()[deviceId].authorised ? 'smartphone' : 'screen_lock_portrait'
}}
</mat-icon>
<p matLine>
{{
connect.devices()[deviceId].authorised
? connect.devices()[deviceId]?.model
: 'unauthorised'
}}
({{ deviceId }})
</p>
</mat-list-item>
</mat-list>
</div>
</div>
<div
*ngIf="
connect.isStartTraceState() || connect.isEndTraceState() || connect.isLoadDataState()
"
class="trace-collection-config">
<mat-list>
<mat-list-item>
<mat-icon matListIcon>smartphone</mat-icon>
<p matLine>
{{ connect.selectedDevice()?.model }} ({{ connect.selectedDeviceId() }})
<button
color="primary"
class="change-btn"
mat-button
(click)="connect.resetLastDevice()"
[disabled]="connect.isEndTraceState() || connect.isLoadDataState()">
Change device
</button>
</p>
</mat-list-item>
</mat-list>
<mat-tab-group class="tracing-tabs">
<mat-tab
label="Trace"
[disabled]="connect.isEndTraceState() || connect.isLoadDataState()">
<div class="tabbed-section">
<div class="trace-section" *ngIf="connect.isStartTraceState()">
<trace-config></trace-config>
<div class="start-btn">
<button color="primary" mat-stroked-button (click)="startTracing()">
Start trace
</button>
</div>
</div>
<div *ngIf="connect.isEndTraceState()" class="end-tracing">
<div class="progress-desc">
<p class="mat-body-3"><mat-icon fontIcon="cable"></mat-icon></p>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
<p class="mat-body-1">Tracing...</p>
</div>
<div class="end-btn">
<button color="primary" mat-raised-button (click)="endTrace()">
End trace
</button>
</div>
</div>
<div *ngIf="connect.isLoadDataState()" class="load-data">
<load-progress [progressPercentage]="loadProgress" [message]="'Loading data...'">
</load-progress>
<div class="end-btn">
<button color="primary" mat-raised-button (click)="endTrace()" disabled="true">
End trace
</button>
</div>
</div>
</div>
</mat-tab>
<mat-tab
label="Dump"
[disabled]="connect.isEndTraceState() || connect.isLoadDataState()">
<div class="tabbed-section">
<div class="dump-section" *ngIf="connect.isStartTraceState()">
<h3 class="mat-subheading-2">Dump targets</h3>
<div class="selection">
<mat-checkbox
*ngFor="let dumpKey of objectKeys(tracingConfig.getDumpConfig())"
color="primary"
class="dump-checkbox"
[(ngModel)]="tracingConfig.getDumpConfig()[dumpKey].run"
>{{ tracingConfig.getDumpConfig()[dumpKey].name }}</mat-checkbox
>
</div>
<div class="dump-btn">
<button color="primary" mat-stroked-button (click)="dumpState()">
Dump state
</button>
</div>
</div>
<load-progress
*ngIf="connect.isLoadDataState()"
[progressPercentage]="loadProgress"
[message]="'Loading data...'">
</load-progress>
</div>
</mat-tab>
</mat-tab-group>
</div>
<div *ngIf="connect.isErrorState()" class="unknown-error">
<p class="error-wrapper mat-body-1">
<mat-icon class="error-icon">error</mat-icon>
Error:
</p>
<pre> {{ connect.proxy?.errorText }} </pre>
<button color="primary" class="retry-btn" mat-raised-button (click)="connect.restart()">
Retry
</button>
</div>
</mat-card-content>
</mat-card>
`,
styles: [
`
.change-btn,
.retry-btn {
margin-left: 5px;
}
.mat-card.collect-card {
display: flex;
}
.collect-card {
height: 100%;
flex-direction: column;
overflow: auto;
margin: 10px;
}
.collect-card-content {
overflow: auto;
}
.selection {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
}
.set-up-adb,
.trace-collection-config,
.trace-section,
.dump-section,
.end-tracing,
.load-data,
trace-config {
display: flex;
flex-direction: column;
gap: 10px;
}
.trace-section,
.dump-section,
.end-tracing,
.load-data {
height: 100%;
}
.trace-collection-config {
height: 100%;
}
.proxy-tab,
.web-tab,
.start-btn,
.dump-btn,
.end-btn {
align-self: flex-start;
}
.start-btn,
.dump-btn,
.end-btn {
margin: auto 0 0 0;
padding: 1rem 0 0 0;
}
.error-wrapper {
display: flex;
flex-direction: row;
align-items: center;
}
.error-icon {
margin-right: 5px;
}
.available-device {
cursor: pointer;
}
.no-device-detected {
display: flex;
flex-direction: column;
justify-content: center;
align-content: center;
align-items: center;
height: 100%;
}
.no-device-detected p,
.device-selection p.instruction {
padding-top: 1rem;
opacity: 0.6;
font-size: 1.2rem;
}
.no-device-detected .icon {
font-size: 3rem;
margin: 0 0 0.2rem 0;
}
.devices-connecting {
height: 100%;
}
mat-card-content {
flex-grow: 1;
}
mat-tab-body {
padding: 1rem;
}
.loading-info {
opacity: 0.8;
padding: 1rem 0;
}
.tracing-tabs {
flex-grow: 1;
}
.tracing-tabs .mat-tab-body-wrapper {
flex-grow: 1;
}
.tabbed-section {
height: 100%;
}
.load-data p,
.end-tracing p {
opacity: 0.7;
}
.progress-desc {
display: flex;
height: 100%;
flex-direction: column;
justify-content: center;
align-content: center;
align-items: center;
}
.progress-desc > * {
max-width: 250px;
}
load-progress {
height: 100%;
}
`,
],
encapsulation: ViewEncapsulation.None,
})
export class CollectTracesComponent implements OnInit, OnDestroy {
objectKeys = Object.keys;
isAdbProxy = true;
traceConfigurations = traceConfigurations;
connect: Connection;
tracingConfig = TracingConfig.getInstance();
loadProgress = 0;
@Input() store!: PersistentStore;
@Input() tracePipeline!: TracePipeline;
@Output() traceDataLoaded = new EventEmitter<void>();
constructor(
@Inject(MatSnackBar) private snackBar: MatSnackBar,
@Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef,
@Inject(NgZone) private ngZone: NgZone
) {
this.connect = new ProxyConnection(
(newState) => this.changeDetectorRef.detectChanges(),
(progress) => this.onLoadProgressUpdate(progress)
);
}
ngOnInit() {
if (this.isAdbProxy) {
this.connect = new ProxyConnection(
(newState) => this.changeDetectorRef.detectChanges(),
(progress) => this.onLoadProgressUpdate(progress)
);
} else {
// TODO: change to WebAdbConnection
this.connect = new ProxyConnection(
(newState) => this.changeDetectorRef.detectChanges(),
(progress) => this.onLoadProgressUpdate(progress)
);
}
}
ngOnDestroy(): void {
this.connect.proxy?.removeOnProxyChange(this.onProxyChange);
}
onAddKey(key: string) {
if (this.connect.setProxyKey) {
this.connect.setProxyKey(key);
}
this.connect.restart();
}
displayAdbProxyTab() {
this.isAdbProxy = true;
this.connect = new ProxyConnection(
(newState) => this.changeDetectorRef.detectChanges(),
(progress) => this.onLoadProgressUpdate(progress)
);
}
displayWebAdbTab() {
this.isAdbProxy = false;
//TODO: change to WebAdbConnection
this.connect = new ProxyConnection(
(newState) => this.changeDetectorRef.detectChanges(),
(progress) => this.onLoadProgressUpdate(progress)
);
}
startTracing() {
console.log('begin tracing');
this.tracingConfig.requestedTraces = this.requestedTraces();
const reqEnableConfig = this.requestedEnableConfig();
const reqSelectedSfConfig = this.requestedSelection('layers_trace');
const reqSelectedWmConfig = this.requestedSelection('window_trace');
if (this.tracingConfig.requestedTraces.length < 1) {
this.connect.throwNoTargetsError();
return;
}
this.connect.startTrace(reqEnableConfig, reqSelectedSfConfig, reqSelectedWmConfig);
}
async dumpState() {
console.log('begin dump');
this.tracingConfig.requestedDumps = this.requestedDumps();
const dumpSuccessful = await this.connect.dumpState();
if (dumpSuccessful) {
await this.loadFiles();
} else {
this.tracePipeline.clear();
}
}
async endTrace() {
console.log('end tracing');
await this.connect.endTrace();
await this.loadFiles();
}
tabClass(adbTab: boolean) {
let isActive: string;
if (adbTab) {
isActive = this.isAdbProxy ? 'active' : 'inactive';
} else {
isActive = !this.isAdbProxy ? 'active' : 'inactive';
}
return ['tab', isActive];
}
private onProxyChange(newState: ProxyState) {
this.connect.onConnectChange.bind(this.connect)(newState);
}
private requestedTraces() {
const tracesFromCollection: string[] = [];
const tracingConfig = this.tracingConfig.getTraceConfig();
const req = Object.keys(tracingConfig).filter((traceKey: string) => {
const traceConfig = tracingConfig[traceKey];
if (traceConfig.isTraceCollection) {
traceConfig.config?.enableConfigs.forEach((innerTrace: EnableConfiguration) => {
if (innerTrace.enabled) {
tracesFromCollection.push(innerTrace.key);
}
});
return false;
}
return traceConfig.run;
});
return req.concat(tracesFromCollection);
}
private requestedDumps() {
const dumpConfig = this.tracingConfig.getDumpConfig();
return Object.keys(dumpConfig).filter((dumpKey: string) => {
return dumpConfig[dumpKey].run;
});
}
private requestedEnableConfig(): string[] {
const req: string[] = [];
const tracingConfig = this.tracingConfig.getTraceConfig();
Object.keys(tracingConfig).forEach((traceKey: string) => {
const trace = tracingConfig[traceKey];
if (!trace.isTraceCollection && trace.run && trace.config && trace.config.enableConfigs) {
trace.config.enableConfigs.forEach((con: EnableConfiguration) => {
if (con.enabled) {
req.push(con.key);
}
});
}
});
return req;
}
private requestedSelection(traceType: string): ConfigMap | undefined {
const tracingConfig = this.tracingConfig.getTraceConfig();
if (!tracingConfig[traceType].run) {
return undefined;
}
const selected: ConfigMap = {};
tracingConfig[traceType].config?.selectionConfigs.forEach((con: SelectionConfiguration) => {
selected[con.key] = con.value;
});
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;
this.changeDetectorRef.detectChanges();
}
}