blob: e22b7c737a8cb06e5a4b4efdef83facb4efbff01 [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,
Inject,
Injector,
ViewChild,
ViewEncapsulation,
} from '@angular/core';
import {createCustomElement} from '@angular/elements';
import {AbtChromeExtensionProtocol} from 'abt_chrome_extension/abt_chrome_extension_protocol';
import {Mediator} from 'app/mediator';
import {TimelineData} from 'app/timeline_data';
import {TRACE_INFO} from 'app/trace_info';
import {TracePipeline} from 'app/trace_pipeline';
import {FileUtils} from 'common/file_utils';
import {PersistentStore} from 'common/persistent_store';
import {CrossToolProtocol} from 'cross_tool/cross_tool_protocol';
import {TraceDataListener} from 'interfaces/trace_data_listener';
import {Timestamp} from 'trace/timestamp';
import {TraceType} from 'trace/trace_type';
import {proxyClient, ProxyState} from 'trace_collection/proxy_client';
import {ViewerInputMethodComponent} from 'viewers/components/viewer_input_method_component';
import {View, Viewer} from 'viewers/viewer';
import {ViewerProtologComponent} from 'viewers/viewer_protolog/viewer_protolog_component';
import {ViewerScreenRecordingComponent} from 'viewers/viewer_screen_recording/viewer_screen_recording_component';
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 {TimelineComponent} from './timeline/timeline_component';
import {UploadTracesComponent} from './upload_traces_component';
@Component({
selector: 'app-root',
template: `
<mat-toolbar class="toolbar">
<span class="app-title">Winscope</span>
<a href="http://go/winscope-legacy">
<button color="primary" mat-button>Open legacy Winscope</button>
</a>
<div class="spacer">
<span *ngIf="dataLoaded" class="active-trace-file-info mat-body-2">
{{ activeTraceFileInfo }}
</span>
</div>
<button
*ngIf="dataLoaded"
color="primary"
mat-stroked-button
(click)="mediator.onWinscopeUploadNew()">
Upload New
</button>
<button
mat-icon-button
matTooltip="Report bug"
(click)="goToLink('https://b.corp.google.com/issues/new?component=909476')">
<mat-icon> bug_report </mat-icon>
</button>
<button
mat-icon-button
matTooltip="Switch to {{ isDarkModeOn ? 'light' : 'dark' }} mode"
(click)="setDarkMode(!isDarkModeOn)">
<mat-icon>
{{ isDarkModeOn ? 'brightness_5' : 'brightness_4' }}
</mat-icon>
</button>
</mat-toolbar>
<mat-divider></mat-divider>
<mat-drawer-container class="example-container" autosize disableClose autoFocus>
<mat-drawer-content>
<ng-container *ngIf="dataLoaded; else noLoadedTracesBlock">
<trace-view
class="viewers"
[viewers]="viewers"
[store]="store"
(downloadTracesButtonClick)="onDownloadTracesButtonClick()"
(activeViewChanged)="onActiveViewChanged($event)"></trace-view>
<mat-divider></mat-divider>
</ng-container>
</mat-drawer-content>
<mat-drawer #drawer mode="overlay" opened="true" [baseHeight]="collapsedTimelineHeight">
<timeline
*ngIf="dataLoaded"
[timelineData]="timelineData"
[activeViewTraceTypes]="activeView?.dependencies"
[availableTraces]="getLoadedTraceTypes()"
(collapsedTimelineSizeChanged)="onCollapsedTimelineSizeChanged($event)"></timeline>
</mat-drawer>
</mat-drawer-container>
<ng-template #noLoadedTracesBlock>
<div class="center">
<div class="landing-content">
<h1 class="welcome-info mat-headline">
Welcome to Winscope. Please select source to view traces.
</h1>
<div class="card-grid landing-grid">
<collect-traces
class="collect-traces-card homepage-card"
[tracePipeline]="tracePipeline"
(traceDataLoaded)="mediator.onWinscopeTraceDataLoaded()"
[store]="store"></collect-traces>
<upload-traces
class="upload-traces-card homepage-card"
[tracePipeline]="tracePipeline"
(traceDataLoaded)="mediator.onWinscopeTraceDataLoaded()"></upload-traces>
</div>
</div>
</div>
</ng-template>
`,
styles: [
`
.toolbar {
gap: 10px;
}
.welcome-info {
margin: 16px 0 6px 0;
text-align: center;
}
.homepage-card {
display: flex;
flex-direction: column;
flex: 1;
overflow: auto;
height: 820px;
}
.spacer {
flex: 1;
text-align: center;
}
.viewers {
height: 0;
flex-grow: 1;
display: flex;
flex-direction: column;
overflow: auto;
}
.center {
display: flex;
align-content: center;
flex-direction: column;
justify-content: center;
align-items: center;
justify-items: center;
flex-grow: 1;
}
.landing-content {
width: 100%;
}
.landing-content .card-grid {
max-width: 1800px;
flex-grow: 1;
margin: auto;
}
`,
],
encapsulation: ViewEncapsulation.None,
})
export class AppComponent implements TraceDataListener {
title = 'winscope';
changeDetectorRef: ChangeDetectorRef;
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
);
states = ProxyState;
store: PersistentStore = new PersistentStore();
currentTimestamp?: Timestamp;
viewers: Viewer[] = [];
isDarkModeOn!: boolean;
dataLoaded = false;
activeView?: View;
activeTraceFileInfo = '';
collapsedTimelineHeight = 0;
@ViewChild(UploadTracesComponent) uploadTracesComponent?: UploadTracesComponent;
@ViewChild(TimelineComponent) timelineComponent?: TimelineComponent;
constructor(
@Inject(Injector) injector: Injector,
@Inject(ChangeDetectorRef) changeDetectorRef: ChangeDetectorRef
) {
this.changeDetectorRef = changeDetectorRef;
const storeDarkMode = this.store.get('dark-mode');
const prefersDarkQuery = window.matchMedia?.('(prefers-color-scheme: dark)');
this.setDarkMode(storeDarkMode ? storeDarkMode === 'true' : prefersDarkQuery.matches);
if (!customElements.get('viewer-input-method')) {
customElements.define(
'viewer-input-method',
createCustomElement(ViewerInputMethodComponent, {injector})
);
}
if (!customElements.get('viewer-protolog')) {
customElements.define(
'viewer-protolog',
createCustomElement(ViewerProtologComponent, {injector})
);
}
if (!customElements.get('viewer-screen-recording')) {
customElements.define(
'viewer-screen-recording',
createCustomElement(ViewerScreenRecordingComponent, {injector})
);
}
if (!customElements.get('viewer-surface-flinger')) {
customElements.define(
'viewer-surface-flinger',
createCustomElement(ViewerSurfaceFlingerComponent, {injector})
);
}
if (!customElements.get('viewer-transactions')) {
customElements.define(
'viewer-transactions',
createCustomElement(ViewerTransactionsComponent, {injector})
);
}
if (!customElements.get('viewer-window-manager')) {
customElements.define(
'viewer-window-manager',
createCustomElement(ViewerWindowManagerComponent, {injector})
);
}
}
ngAfterViewInit() {
this.mediator.setUploadTracesComponent(this.uploadTracesComponent);
this.mediator.onWinscopeInitialized();
}
ngAfterViewChecked() {
this.mediator.setTimelineComponent(this.timelineComponent);
}
onCollapsedTimelineSizeChanged(height: number) {
this.collapsedTimelineHeight = height;
this.changeDetectorRef.detectChanges();
}
getLoadedTraceTypes(): TraceType[] {
return this.tracePipeline.getLoadedTraceFiles().map((trace) => trace.type);
}
onTraceDataLoaded(viewers: Viewer[]) {
this.viewers = viewers;
this.dataLoaded = true;
this.changeDetectorRef.detectChanges();
}
onTraceDataUnloaded() {
proxyClient.adbData = [];
this.dataLoaded = false;
this.changeDetectorRef.detectChanges();
}
setDarkMode(enabled: boolean) {
document.body.classList.toggle('dark-mode', enabled);
this.store.add('dark-mode', `${enabled}`);
this.isDarkModeOn = enabled;
}
async onDownloadTracesButtonClick() {
const traceFiles = await this.makeTraceFilesForDownload();
const zipFileBlob = await FileUtils.createZipArchive(traceFiles);
const zipFileName = 'winscope.zip';
const a = document.createElement('a');
document.body.appendChild(a);
const url = window.URL.createObjectURL(zipFileBlob);
a.href = url;
a.download = zipFileName;
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
}
onActiveViewChanged(view: View) {
this.activeView = view;
this.activeTraceFileInfo = this.makeActiveTraceFileInfo(view);
this.timelineData.setActiveViewTraceTypes(view.dependencies);
}
goToLink(url: string) {
window.open(url, '_blank');
}
private makeActiveTraceFileInfo(view: View): string {
const traceFile = this.tracePipeline
.getLoadedTraceFiles()
.find((file) => file.type === view.dependencies[0])?.traceFile;
if (!traceFile) {
return '';
}
if (!traceFile.parentArchive) {
return traceFile.file.name;
}
return `${traceFile.parentArchive.name} - ${traceFile.file.name}`;
}
private async makeTraceFilesForDownload(): Promise<File[]> {
return this.tracePipeline.getLoadedTraceFiles().map((trace) => {
const traceType = TRACE_INFO[trace.type].name;
const newName = traceType + '/' + FileUtils.removeDirFromFileName(trace.traceFile.file.name);
return new File([trace.traceFile.file], newName);
});
}
}