Merge changes from topic "Rects"

* changes:
  (Rects View Review Changes) Create SF viewer.
  Create Viewer for surface flinger traces.
This commit is contained in:
TreeHugger Robot
2022-08-17 17:31:29 +00:00
committed by Android (Google) Code Review
47 changed files with 2347 additions and 213 deletions

View File

@@ -23,11 +23,14 @@
"@ngrx/store": "^14.0.2",
"@ngxs/store": "^3.7.4",
"@types/jsbn": "^1.2.30",
"@types/three": "^0.143.0",
"angular2-template-loader": "^0.6.2",
"auth0": "^2.42.0",
"gl-matrix": "^3.4.3",
"html-loader": "^3.1.0",
"html-webpack-inline-source-plugin": "^1.0.0-beta.2",
"html-webpack-plugin": "^5.5.0",
"html2canvas": "^1.4.1",
"jsbn": "^1.1.0",
"jsbn-rsa": "^1.0.4",
"kotlin": "^1.7.0",
@@ -62,7 +65,10 @@
"karma-jasmine": "~5.0.0",
"karma-sourcemap-loader": "^0.3.8",
"karma-webpack": "^5.0.0",
"protractor": "^7.0.0"
"protractor": "^7.0.0",
"three": "^0.143.0",
"webgl-utils": "^1.0.1",
"webgl-utils.js": "^1.1.0"
}
},
"node_modules/@ampproject/remapping": {
@@ -3491,12 +3497,25 @@
"@types/node": "*"
}
},
"node_modules/@types/three": {
"version": "0.143.0",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.143.0.tgz",
"integrity": "sha512-c5PonXOt8xk5q4ygmyjOX4Ec+FA7gwfdcMT/PveE9xrJs/0DDcf2lJkWrhEcmvx2ZefQCQBcogABnGqB0P4OsA==",
"dependencies": {
"@types/webxr": "*"
}
},
"node_modules/@types/w3c-web-usb": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.6.tgz",
"integrity": "sha512-cSjhgrr8g4KbPnnijAr/KJDNKa/bBa+ixYkywFRvrhvi9n1WEl7yYbtRyzE6jqNQiSxxJxoAW3STaOQwJHndaw==",
"dev": true
},
"node_modules/@types/webxr": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.0.tgz",
"integrity": "sha512-IUMDPSXnYIbEO2IereEFcgcqfDREOgmbGqtrMpVPpACTU6pltYLwHgVkrnYv0XhWEcjio9sYEfIEzgn3c7nDqA=="
},
"node_modules/@types/ws": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
@@ -4442,6 +4461,14 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -5611,6 +5638,14 @@
"postcss": "^8.4"
}
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/css-loader": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.1.tgz",
@@ -7729,6 +7764,11 @@
"assert-plus": "^1.0.0"
}
},
"node_modules/gl-matrix": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz",
"integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA=="
},
"node_modules/glob": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/glob/-/glob-8.0.1.tgz",
@@ -8079,6 +8119,18 @@
"webpack": "^5.20.0"
}
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/htmlparser2": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
@@ -14399,12 +14451,26 @@
"node": "*"
}
},
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
"node_modules/three": {
"version": "0.143.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.143.0.tgz",
"integrity": "sha512-oKcAGYHhJ46TGEuHjodo2n6TY2R6lbvrkp+feKZxqsUL/WkH7GKKaeu6RHeyb2Xjfk2dPLRKLsOP0KM2VgT8Zg==",
"dev": true
},
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
@@ -14843,6 +14909,14 @@
"node": ">= 0.4.0"
}
},
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
@@ -15104,6 +15178,18 @@
"node": ">=0.8.0"
}
},
"node_modules/webgl-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/webgl-utils/-/webgl-utils-1.0.1.tgz",
"integrity": "sha512-ox5xQ3YkrrOR6pCZHTOud49zzMXP9LnXzx7jIQvHOinV4FK59rGGJURw2Lq1cCTPgMVU2wAWq7e6vEVjz9FdBw==",
"dev": true
},
"node_modules/webgl-utils.js": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/webgl-utils.js/-/webgl-utils.js-1.1.0.tgz",
"integrity": "sha512-cmO2aPd6gR6bK/ttdk8ZIypJfZMOcTvsvXv/LxXZjAFu5TC6vXqFrZYudlPuKxVsA34Pc8Fysq2rCnflu+wuuA==",
"dev": true
},
"node_modules/webidl-conversions": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
@@ -18141,12 +18227,25 @@
"@types/node": "*"
}
},
"@types/three": {
"version": "0.143.0",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.143.0.tgz",
"integrity": "sha512-c5PonXOt8xk5q4ygmyjOX4Ec+FA7gwfdcMT/PveE9xrJs/0DDcf2lJkWrhEcmvx2ZefQCQBcogABnGqB0P4OsA==",
"requires": {
"@types/webxr": "*"
}
},
"@types/w3c-web-usb": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.6.tgz",
"integrity": "sha512-cSjhgrr8g4KbPnnijAr/KJDNKa/bBa+ixYkywFRvrhvi9n1WEl7yYbtRyzE6jqNQiSxxJxoAW3STaOQwJHndaw==",
"dev": true
},
"@types/webxr": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.0.tgz",
"integrity": "sha512-IUMDPSXnYIbEO2IereEFcgcqfDREOgmbGqtrMpVPpACTU6pltYLwHgVkrnYv0XhWEcjio9sYEfIEzgn3c7nDqA=="
},
"@types/ws": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
@@ -18845,6 +18944,11 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="
},
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -19749,6 +19853,14 @@
"postcss-selector-parser": "^6.0.9"
}
},
"css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"requires": {
"utrie": "^1.0.2"
}
},
"css-loader": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.1.tgz",
@@ -21244,6 +21356,11 @@
"assert-plus": "^1.0.0"
}
},
"gl-matrix": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz",
"integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA=="
},
"glob": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/glob/-/glob-8.0.1.tgz",
@@ -21517,6 +21634,15 @@
"tapable": "^2.0.0"
}
},
"html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"requires": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
}
},
"htmlparser2": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
@@ -26286,12 +26412,26 @@
}
}
},
"text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"requires": {
"utrie": "^1.0.2"
}
},
"text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
"three": {
"version": "0.143.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.143.0.tgz",
"integrity": "sha512-oKcAGYHhJ46TGEuHjodo2n6TY2R6lbvrkp+feKZxqsUL/WkH7GKKaeu6RHeyb2Xjfk2dPLRKLsOP0KM2VgT8Zg==",
"dev": true
},
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
@@ -26610,6 +26750,14 @@
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"dev": true
},
"utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"requires": {
"base64-arraybuffer": "^1.0.2"
}
},
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
@@ -26816,6 +26964,18 @@
}
}
},
"webgl-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/webgl-utils/-/webgl-utils-1.0.1.tgz",
"integrity": "sha512-ox5xQ3YkrrOR6pCZHTOud49zzMXP9LnXzx7jIQvHOinV4FK59rGGJURw2Lq1cCTPgMVU2wAWq7e6vEVjz9FdBw==",
"dev": true
},
"webgl-utils.js": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/webgl-utils.js/-/webgl-utils.js-1.1.0.tgz",
"integrity": "sha512-cmO2aPd6gR6bK/ttdk8ZIypJfZMOcTvsvXv/LxXZjAFu5TC6vXqFrZYudlPuKxVsA34Pc8Fysq2rCnflu+wuuA==",
"dev": true
},
"webidl-conversions": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",

View File

@@ -31,11 +31,14 @@
"@ngrx/store": "^14.0.2",
"@ngxs/store": "^3.7.4",
"@types/jsbn": "^1.2.30",
"@types/three": "^0.143.0",
"angular2-template-loader": "^0.6.2",
"auth0": "^2.42.0",
"gl-matrix": "^3.4.3",
"html-loader": "^3.1.0",
"html-webpack-inline-source-plugin": "^1.0.0-beta.2",
"html-webpack-plugin": "^5.5.0",
"html2canvas": "^1.4.1",
"jsbn": "^1.1.0",
"jsbn-rsa": "^1.0.4",
"kotlin": "^1.7.0",
@@ -70,6 +73,9 @@
"karma-jasmine": "~5.0.0",
"karma-sourcemap-loader": "^0.3.8",
"karma-webpack": "^5.0.0",
"protractor": "^7.0.0"
"protractor": "^7.0.0",
"three": "^0.143.0",
"webgl-utils": "^1.0.1",
"webgl-utils.js": "^1.1.0"
}
}

View File

@@ -1,109 +0,0 @@
/*
* 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 {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, proxyClient} from "trace_collection/proxy_client";
import { Viewer } from "viewers/viewer";
@Component({
selector: "app-root",
template: `
<div id="title">
<span>Winscope Viewer 2.0</span>
</div>
<div *ngIf="!dataLoaded" fxLayout="row wrap" fxLayoutGap="10px grid" class="home">
<mat-card class="homepage-card" id="collect-traces-card">
<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" (dataLoadedChange)="onDataLoadedChange($event)"></upload-traces>
</mat-card>
</div>
<div *ngIf="dataLoaded">
<mat-card class="homepage-card" id="loaded-data-card">
<mat-card-title>Loaded data</mat-card-title>
<button mat-raised-button (click)="clearData()">Back to Home</button>
</mat-card>
</div>
<div id="timescrub">
<button mat-raised-button (click)="notifyCurrentTimestamp()">Update current timestamp</button>
</div>
<div id="timestamps">
</div>
<div id="viewers">
</div>
`,
styles: [".home{width: 100%; display:flex; flex-direction: row; overflow: auto;}"]
})
export class AppComponent {
title = "winscope-ng";
core: Core;
states = ProxyState;
store: PersistentStore = new PersistentStore();
@Input() dataLoaded = false;
viewersCreated = false;
constructor(
@Inject(Injector) injector: Injector
) {
this.core = new Core();
if (!customElements.get("viewer-window-manager")) {
customElements.define("viewer-window-manager",
createCustomElement(ViewerWindowManagerComponent, {injector}));
}
}
onDataLoadedChange(dataLoaded: boolean) {
if (dataLoaded && !this.viewersCreated) {
this.core.createViewers();
this.createViewerElements();
const dummyTimestamp = this.core.getTimestamps()[1]; //TODO: get timestamp from time scrub
this.core.notifyCurrentTimestamp(dummyTimestamp);
this.viewersCreated = true;
this.dataLoaded = dataLoaded;
}
}
createViewerElements() {
const viewersDiv = document.querySelector("div#viewers")!;
viewersDiv.innerHTML = "";
this.core.getViews().forEach((view: HTMLElement) => {
viewersDiv.appendChild(view);
});
}
public notifyCurrentTimestamp() {
const dummyTimestamp = new Timestamp(TimestampType.ELAPSED, 1000000n);
this.core.notifyCurrentTimestamp(dummyTimestamp);
}
public clearData() {
this.dataLoaded = false;
this.viewersCreated = false;
this.core.clearData();
proxyClient.adbData = [];
}
}

View File

@@ -13,27 +13,40 @@ import { MatFormFieldModule } from "@angular/material/form-field";
import { MatIconModule } from "@angular/material/icon";
import { MatInputModule } from "@angular/material/input";
import { MatSelectModule } from "@angular/material/select";
import { MatRadioModule } from "@angular/material/radio";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { HttpClientModule } from "@angular/common/http";
import { MatSliderModule } from "@angular/material/slider";
import { AppComponent } from "./app.component";
import { AppComponent } from "./components/app.component";
import { ViewerWindowManagerComponent } from "viewers/viewer_window_manager/viewer_window_manager.component";
import { CollectTracesComponent } from "./collect_traces.component";
import { AdbProxyComponent } from "./adb_proxy.component";
import { WebAdbComponent } from "./web_adb.component";
import { TraceConfigComponent } from "./trace_config.component";
import { UploadTracesComponent } from "./upload_traces.component";
import { ViewerSurfaceFlingerComponent } from "viewers/viewer_surface_flinger/viewer_surface_flinger.component";
import { CollectTracesComponent } from "./components/collect_traces.component";
import { AdbProxyComponent } from "./components/adb_proxy.component";
import { WebAdbComponent } from "./components/web_adb.component";
import { TraceConfigComponent } from "./components/trace_config.component";
import { UploadTracesComponent } from "./components/upload_traces.component";
import { HierarchyComponent } from "viewers/hierarchy.component";
import { PropertiesComponent } from "viewers/properties.component";
import { RectsComponent } from "viewers/rects.component";
import { TraceViewHeaderComponent } from "./components/trace_view_header.component";
import { TraceViewComponent } from "./components/trace_view.component";
@NgModule({
declarations: [
AppComponent,
ViewerWindowManagerComponent,
ViewerSurfaceFlingerComponent,
CollectTracesComponent,
UploadTracesComponent,
AdbProxyComponent,
WebAdbComponent,
TraceConfigComponent,
HierarchyComponent,
PropertiesComponent,
RectsComponent,
TraceViewHeaderComponent,
TraceViewComponent
],
imports: [
BrowserModule,
@@ -52,9 +65,10 @@ import { UploadTracesComponent } from "./upload_traces.component";
MatInputModule,
MatSelectModule,
BrowserAnimationsModule,
HttpClientModule
HttpClientModule,
MatSliderModule,
MatRadioModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

View File

@@ -16,7 +16,7 @@
import { CommonModule } from "@angular/common";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { AdbProxyComponent } from "./adb_proxy.component";
import { proxyClient, ProxyState } from "../trace_collection/proxy_client";
import { proxyClient, ProxyState } from "trace_collection/proxy_client";
import { MatIconModule } from "@angular/material/icon";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatInputModule } from "@angular/material/input";

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { Component, Input, Output, EventEmitter } from "@angular/core";
import { proxyClient, ProxyClient, ProxyState } from "../trace_collection/proxy_client";
import { proxyClient, ProxyClient, ProxyState } from "trace_collection/proxy_client";
@Component({
selector: "adb-proxy",

View File

@@ -28,6 +28,8 @@ import { WebAdbComponent } from "./web_adb.component";
import { TraceConfigComponent } from "./trace_config.component";
import { ComponentFixtureAutoDetect } from "@angular/core/testing";
import { ViewerSurfaceFlingerComponent } from "viewers/viewer_surface_flinger/viewer_surface_flinger.component";
import { MatSliderModule } from "@angular/material/slider";
describe("AppComponent", () => {
@@ -45,6 +47,7 @@ describe("AppComponent", () => {
MatCardModule,
MatButtonModule,
MatGridListModule,
MatSliderModule
],
declarations: [
AppComponent,
@@ -53,6 +56,7 @@ describe("AppComponent", () => {
AdbProxyComponent,
WebAdbComponent,
TraceConfigComponent,
ViewerSurfaceFlingerComponent
],
}).overrideComponent(AppComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
@@ -71,7 +75,7 @@ describe("AppComponent", () => {
});
it("renders the page title", () => {
expect(htmlElement.querySelector("#title")?.innerHTML).toContain("Winscope Viewer 2.0");
expect(htmlElement.querySelector("#app-title")?.innerHTML).toContain("Winscope Viewer 2.0");
});
it("displays correct elements when no data loaded", async () => {
@@ -87,6 +91,6 @@ describe("AppComponent", () => {
fixture.detectChanges();
expect(htmlElement.querySelector("#collect-traces-card")).toBeFalsy();
expect(htmlElement.querySelector("#upload-traces-card")).toBeFalsy();
expect(htmlElement.querySelector("#loaded-data-card")).toBeTruthy();
expect(htmlElement.querySelector(".viewers.show")).toBeTruthy();
});
});

View File

@@ -0,0 +1,168 @@
/*
* 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 { Component, Injector, Inject, ViewEncapsulation, Input } from "@angular/core";
import { createCustomElement } from "@angular/elements";
import { TraceCoordinator } from "../trace_coordinator";
import { proxyClient, ProxyState } from "trace_collection/proxy_client";
import { PersistentStore } from "common/persistent_store";
import { ViewerWindowManagerComponent } from "viewers/viewer_window_manager/viewer_window_manager.component";
import { ViewerSurfaceFlingerComponent } from "viewers/viewer_surface_flinger/viewer_surface_flinger.component";
import { TraceViewComponent } from "./trace_view.component";
import { Timestamp } from "common/trace/timestamp";
import { MatSliderChange } from "@angular/material/slider";
import { Viewer } from "viewers/viewer";
@Component({
selector: "app-root",
template: `
<div id="app-title">
<span>Winscope Viewer 2.0</span>
<button mat-raised-button *ngIf="dataLoaded" (click)="clearData()">Back to Home</button>
<button mat-raised-button *ngIf="dataLoaded" (click)="toggleTimestamp()">Start/End Timestamp</button>
<mat-slider
*ngIf="dataLoaded"
step="1"
min="0"
[max]="this.allTimestamps.length-1"
aria-label="units"
[value]="currentTimestampIndex"
(input)="updateCurrentTimestamp($event)"
class="time-slider"
></mat-slider>
</div>
<div *ngIf="!dataLoaded" fxLayout="row wrap" fxLayoutGap="10px grid" class="card-grid">
<mat-card class="homepage-card" id="collect-traces-card">
<collect-traces [(traceCoordinator)]="traceCoordinator" (dataLoadedChange)="onDataLoadedChange($event)"[store]="store"></collect-traces>
</mat-card>
<mat-card class="homepage-card" id="upload-traces-card">
<upload-traces [(traceCoordinator)]="traceCoordinator" (dataLoadedChange)="onDataLoadedChange($event)"></upload-traces>
</mat-card>
</div>
<div id="timescrub">
</div>
<div id="timestamps">
</div>
<div id="viewers" [class]="showViewers()">
</div>
`,
styles: [".time-slider {width: 100%}"],
encapsulation: ViewEncapsulation.None
})
export class AppComponent {
title = "winscope-ng";
traceCoordinator: TraceCoordinator;
states = ProxyState;
store: PersistentStore = new PersistentStore();
@Input() dataLoaded = false;
viewersCreated = false;
currentTimestamp?: Timestamp;
currentTimestampIndex = 0;
allTimestamps: Timestamp[] = [];
constructor(
@Inject(Injector) injector: Injector
) {
this.traceCoordinator = new TraceCoordinator();
if (!customElements.get("viewer-window-manager")) {
customElements.define("viewer-window-manager",
createCustomElement(ViewerWindowManagerComponent, {injector}));
}
if (!customElements.get("viewer-surface-flinger")) {
customElements.define("viewer-surface-flinger",
createCustomElement(ViewerSurfaceFlingerComponent, {injector}));
}
if (!customElements.get("trace-view")) {
customElements.define("trace-view",
createCustomElement(TraceViewComponent, {injector}));
}
}
onDataLoadedChange(dataLoaded: boolean) {
if (dataLoaded && !this.viewersCreated) {
this.allTimestamps = this.traceCoordinator.getTimestamps();
this.traceCoordinator.createViewers();
this.createViewerElements();
this.currentTimestampIndex = 0;
this.notifyCurrentTimestamp();
this.viewersCreated = true;
this.dataLoaded = dataLoaded;
}
}
createViewerElements() {
const viewersDiv = document.querySelector("div#viewers")!;
viewersDiv.innerHTML = "";
let cardCounter = 0;
this.traceCoordinator.getViewers().forEach((viewer: Viewer) => {
const traceView = document.createElement("trace-view");
(traceView as any).title = viewer.getTitle();
(traceView as any).dependencies = viewer.getDependencies();
(traceView as any).showTrace = true;
traceView.addEventListener("saveTraces", ($event: any) => {
this.traceCoordinator.saveTraces($event.detail);
});
viewersDiv.appendChild(traceView);
const traceCard = traceView.querySelector(".trace-card")!;
traceCard.id = `card-${cardCounter}`;
(traceView as any).cardId = cardCounter;
cardCounter++;
const traceCardContent = traceCard.querySelector(".trace-card-content")!;
const view = viewer.getView();
(view as any).showTrace = (traceView as any).showTrace;
traceCardContent.appendChild(view);
});
}
updateCurrentTimestamp(event: MatSliderChange) {
if (event.value) {
this.currentTimestampIndex = event.value;
this.notifyCurrentTimestamp();
}
}
public notifyCurrentTimestamp() {
this.currentTimestamp = this.allTimestamps[this.currentTimestampIndex];
this.traceCoordinator.notifyCurrentTimestamp(this.currentTimestamp);
}
public toggleTimestamp() {
if (this.currentTimestampIndex===0) {
this.currentTimestampIndex = this.allTimestamps.length-1;
} else {
this.currentTimestampIndex = 0;
}
this.notifyCurrentTimestamp();
}
public clearData() {
this.dataLoaded = false;
this.viewersCreated = false;
this.traceCoordinator.clearData();
proxyClient.adbData = [];
}
public showViewers() {
const isShown = this.dataLoaded ? "show" : "hide";
return ["viewers", isShown];
}
}

View File

@@ -24,6 +24,7 @@ import { MatListModule } from "@angular/material/list";
import { MatButtonModule } from "@angular/material/button";
import { MatProgressBarModule } from "@angular/material/progress-bar";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { NO_ERRORS_SCHEMA } from "@angular/core";
describe("CollectTracesComponent", () => {
let fixture: ComponentFixture<CollectTracesComponent>;
@@ -36,7 +37,6 @@ describe("CollectTracesComponent", () => {
MatIconModule,
MatCardModule,
MatListModule,
MatIconModule,
MatButtonModule,
MatProgressBarModule,
BrowserAnimationsModule
@@ -47,6 +47,7 @@ describe("CollectTracesComponent", () => {
WebAdbComponent,
TraceConfigComponent,
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
fixture = TestBed.createComponent(CollectTracesComponent);
component = fixture.componentInstance;

View File

@@ -13,14 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, Input, OnInit, Output, EventEmitter, NgZone, Inject } from "@angular/core";
import { Component, Inject, Input, Output, EventEmitter, OnInit, OnDestroy } 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, EnableConfiguration } from "../trace_collection/trace_collection_utils";
import { Core } from "app/core";
import { PersistentStore } from "../common/persistent_store";
import { ProxyState } from "trace_collection/proxy_client";
import { traceConfigurations, configMap, SelectionConfiguration, EnableConfiguration } from "trace_collection/trace_collection_utils";
import { TraceCoordinator } from "app/trace_coordinator";
import { PersistentStore } from "common/persistent_store";
@Component({
@@ -33,9 +33,9 @@ import { PersistentStore } from "../common/persistent_store";
<div class="set-up-adb" *ngIf="!connect.adbSuccess()">
<button id="proxy-tab" mat-raised-button [ngClass]="tabClass(true)" (click)="displayAdbProxyTab()">ADB Proxy</button>
<button id="web-tab" mat-raised-button [ngClass]="tabClass(false)" (click)="displayWebAdbTab()">Web ADB</button>
<!-- <button id="web-tab" 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>
<!-- <web-adb *ngIf="!isAdbProxy"></web-adb> TODO: fix web adb workflow -->
</div>
<div id="devices-connecting" *ngIf="connect.isDevicesState()">
@@ -110,30 +110,26 @@ import { PersistentStore } from "../common/persistent_store";
</mat-card-content>
`,
styles: [".device-choice {cursor: pointer}"]
styles: [
".device-choice {cursor: pointer}",
".mat-checkbox .mat-checkbox-frame {transform: scale(0.7); font-size: 10;}",
".mat-checkbox-checked .mat-checkbox-background {transform: scale(0.7); font-size: 10;}"
]
})
export class CollectTracesComponent implements OnInit {
export class CollectTracesComponent implements OnInit, OnDestroy {
objectKeys = Object.keys;
isAdbProxy = true;
traceConfigurations = traceConfigurations;
connect: Connection = new ProxyConnection();
setTraces = setTraces;
@Input()
store: PersistentStore = new PersistentStore();
@Input()
core?: Core;
@Output()
coreChange = new EventEmitter<Core>();
dataLoaded = false;
@Output()
dataLoadedChange = new EventEmitter<boolean>();
@Input() store!: PersistentStore;
@Input() traceCoordinator!: TraceCoordinator;
ngOnInit(): void {
@Output() dataLoadedChange = new EventEmitter<boolean>();
ngOnInit() {
if (this.isAdbProxy) {
this.connect = new ProxyConnection();
} else {
@@ -142,8 +138,6 @@ export class CollectTracesComponent implements OnInit {
}
}
constructor(@Inject(NgZone) private ngZone: NgZone) {}
ngOnDestroy(): void {
this.connect.proxy?.removeOnProxyChange(this.onProxyChange);
}
@@ -258,7 +252,7 @@ export class CollectTracesComponent implements OnInit {
if (!setTraces.dumpError) {
await this.loadFiles();
} else {
this.core?.clearData();
this.traceCoordinator.clearData();
}
}
@@ -273,13 +267,12 @@ export class CollectTracesComponent implements OnInit {
public async loadFiles() {
console.log("loading files", this.connect.adbData());
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!");
});
this.traceCoordinator.clearData();
await this.traceCoordinator.addTraces(this.connect.adbData());
this.dataLoaded = true;
this.dataLoadedChange.emit(this.dataLoaded);
console.log("finished loading data!");
}
public tabClass(adbTab: boolean) {

View File

@@ -104,7 +104,7 @@ describe("TraceConfigComponent", () => {
expect(adv?.innerHTML).toContain("tracing level");
});
it("check that changing enable config causes box to change", async () => { spyOn(component, "changeTraceCollectionConfig");
it("check that changing enable config causes box to change", async () => {
component.trace.config!.enableConfigs[0].enabled = false;
fixture.detectChanges();
await fixture.whenStable();

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { Component, Input } from "@angular/core";
import { EnableConfiguration, SelectionConfiguration, TraceConfiguration } from "../trace_collection/trace_collection_utils";
import { EnableConfiguration, SelectionConfiguration, TraceConfiguration } from "trace_collection/trace_collection_utils";
@Component({
selector: "trace-config",

View File

@@ -0,0 +1,61 @@
/*
* 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 { CommonModule } from "@angular/common";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { TraceViewComponent } from "./trace_view.component";
import { MatCardModule } from "@angular/material/card";
import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from "@angular/core";
import { TraceType } from "common/trace/trace_type";
describe("TraceViewComponent", () => {
let fixture: ComponentFixture<TraceViewComponent>;
let component: TraceViewComponent;
let htmlElement: HTMLElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
CommonModule,
MatCardModule
],
declarations: [TraceViewComponent],
schemas: [NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents();
fixture = TestBed.createComponent(TraceViewComponent);
component = fixture.componentInstance;
htmlElement = fixture.nativeElement;
component.dependencies = [TraceType.SURFACE_FLINGER, TraceType.WINDOW_MANAGER];
component.showTrace = true;
});
it("can be created", () => {
expect(component).toBeTruthy();
});
it("check that mat card title and contents are displayed", () => {
fixture.detectChanges();
const title = htmlElement.querySelector(".trace-card-title");
expect(title).toBeTruthy();
const header = title?.querySelector("trace-view-header");
expect(header).toBeTruthy();
});
it("check that card content is created", () => {
fixture.detectChanges();
const content = htmlElement.querySelector(".trace-card-content") as HTMLElement;
expect(content).toBeTruthy();
});
});

View File

@@ -0,0 +1,57 @@
/*
* 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 {
Component,
Input,
Output,
EventEmitter
} from "@angular/core";
import { TRACE_INFO } from "../trace_info";
import { TraceType } from "common/trace/trace_type";
@Component({
selector: "trace-view",
template: `
<mat-card class="trace-card">
<mat-card-header>
<mat-card-title class="trace-card-title" *ngIf="dependencies">
<trace-view-header
[title]="title"
[(showTrace)]="showTrace"
[dependencies]="dependencies"
[cardId]="cardId"
(saveTraceChange)="onSaveTraces($event)"
></trace-view-header>
</mat-card-title>
</mat-card-header>
<mat-card-content class="trace-card-content" [hidden]="!showTrace">
</mat-card-content>
</mat-card>
`,
})
export class TraceViewComponent {
@Input() title!: string;
@Input() dependencies!: TraceType[];
@Input() showTrace = true;
@Input() cardId = 0;
@Output() saveTraces = new EventEmitter<TraceType[]>();
TRACE_INFO = TRACE_INFO;
onSaveTraces(dependencies: TraceType[]) {
this.saveTraces.emit(dependencies);
}
}

View File

@@ -0,0 +1,116 @@
/*
* 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 { CommonModule } from "@angular/common";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { TraceViewHeaderComponent } from "./trace_view_header.component";
import { MatIconModule } from "@angular/material/icon";
import { MatButtonModule } from "@angular/material/button";
import { TraceType } from "common/trace/trace_type";
describe("TraceViewHeaderComponent", () => {
let fixture: ComponentFixture<TraceViewHeaderComponent>;
let component: TraceViewHeaderComponent;
let htmlElement: HTMLElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
CommonModule,
MatIconModule,
MatButtonModule
],
declarations: [TraceViewHeaderComponent]
}).compileComponents();
fixture = TestBed.createComponent(TraceViewHeaderComponent);
component = fixture.componentInstance;
htmlElement = fixture.nativeElement;
component.dependencies = [TraceType.SURFACE_FLINGER, TraceType.WINDOW_MANAGER];
});
it("can be created", () => {
expect(component).toBeTruthy();
});
it("check that toggle button is displayed, expanded on default", () => {
component.showTrace = true;
fixture.detectChanges();
const toggleButton = htmlElement.querySelector("#toggle-btn");
expect(toggleButton).toBeTruthy();
const chevronIcon = toggleButton?.querySelector("mat-icon");
expect(chevronIcon).toBeTruthy;
expect(chevronIcon?.innerHTML).toContain("expand_more");
});
it("check that toggle button icon is a right chevron when minimised ", () => {
component.showTrace = false;
fixture.detectChanges();
const toggleButton = htmlElement.querySelector("#toggle-btn");
const chevronIcon = toggleButton?.querySelector("mat-icon");
expect(chevronIcon?.innerHTML).toContain("chevron_right");
});
it("check that clicking toggle button causes view to minimise", async () => {
component.showTrace = true;
fixture.detectChanges();
spyOn(component, "toggleView").and.callThrough();
const button: HTMLButtonElement | null = htmlElement.querySelector("#toggle-btn");
expect(button).toBeInstanceOf(HTMLButtonElement);
button?.dispatchEvent(new Event("click"));
await fixture.whenStable();
expect(component.toggleView).toHaveBeenCalled();
fixture.detectChanges();
expect (htmlElement.querySelector("#toggle-btn")?.querySelector("mat-icon")?.innerHTML).toContain("chevron_right");
});
it("check that dependency icons show", () => {
fixture.detectChanges();
const dependencyIcons = htmlElement.querySelectorAll("#dep-icon");
expect(dependencyIcons).toBeTruthy();
expect(dependencyIcons.length).toBe(2);
});
it("check that title is displayed", () => {
component.title = "Surface Flinger, Window Manager";
fixture.detectChanges();
const title = htmlElement.querySelector(".trace-card-title-text");
expect(title).toBeTruthy();
expect(title?.innerHTML).toContain("Surface Flinger");
expect(title?.innerHTML).toContain("Window Manager");
});
it("check that save button is displayed", () => {
fixture.detectChanges();
const saveButton = htmlElement.querySelectorAll("#save-btn");
expect(saveButton).toBeTruthy();
});
it("check that clicking save button emits", async () => {
spyOn(component, "saveTraces").and.callThrough();
spyOn(component.saveTraceChange, "emit");
const button: HTMLButtonElement | null = htmlElement.querySelector("#save-btn");
expect(button).toBeInstanceOf(HTMLButtonElement);
button?.dispatchEvent(new Event("click"));
await fixture.whenStable();
expect(component.saveTraces).toHaveBeenCalled();
expect(component.saveTraceChange.emit).toHaveBeenCalled();
});
it("check that screenshot button is displayed", () => {
fixture.detectChanges();
const screenshotButton = htmlElement.querySelectorAll("#screenshot-btn");
expect(screenshotButton).toBeTruthy();
});
});

View File

@@ -0,0 +1,88 @@
/*
* 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 {
Component,
Input,
Output,
EventEmitter
} from "@angular/core";
import { TRACE_INFO } from "../trace_info";
import { TraceType } from "common/trace/trace_type";
import html2canvas from "html2canvas";
@Component({
selector: "trace-view-header",
template: `
<button class="icon-button" id="toggle-btn" (click)="toggleView()">
<mat-icon aria-hidden="true">
{{ showTrace ? "expand_more" : "chevron_right" }}
</mat-icon>
</button>
<mat-icon id="dep-icon" *ngFor="let dep of dependencies" aria-hidden="true" class="icon-button">{{TRACE_INFO[dep].icon}}</mat-icon>
<span class="trace-card-title-text">
{{title}}
</span>
<button id="save-btn" class="icon-button" (click)="saveTraces()">
<mat-icon aria-hidden="true">save_alt</mat-icon>
</button>
<button id="screenshot-btn" (click)="takeScreenshot()" class="icon-button">
<mat-icon aria-hidden="true">camera_alt</mat-icon>
</button>
`,
styles: [
".trace-card-title {font: inherit; display: inline-block; vertical-align: middle;}",
]
})
export class TraceViewHeaderComponent {
@Input() title?: string;
@Input() dependencies?: TraceType[];
@Input() showTrace = true;
@Input() cardId!: number ;
@Output() showTraceChange = new EventEmitter<boolean>();
@Output() saveTraceChange = new EventEmitter<TraceType[]>();
TRACE_INFO = TRACE_INFO;
toggleView() {
this.showTrace = !this.showTrace;
this.showTraceChange.emit(this.showTrace);
}
public saveTraces() {
this.saveTraceChange.emit(this.dependencies);
}
public takeScreenshot() {
const el = document.querySelector(`#card-${this.cardId}`);
if (el) {
html2canvas((el as HTMLElement)).then((canvas) => {
const uri = canvas.toDataURL();
const filename = "Winscope-Screenshot.png";
const link = document.createElement("a");
if (typeof link.download === "string") {
link.href = uri;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} else {
window.open(uri);
}
});
}
}
}

View File

@@ -17,7 +17,7 @@ import {ComponentFixture, TestBed} from "@angular/core/testing";
import {UploadTracesComponent} from "./upload_traces.component";
import { MatCardModule } from "@angular/material/card";
describe("CollectTracesComponent", () => {
describe("UploadTracesComponent", () => {
let fixture: ComponentFixture<UploadTracesComponent>;
let component: UploadTracesComponent;
let htmlElement: HTMLElement;

View File

@@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, Input, Inject, Output, EventEmitter, NgZone } from "@angular/core";
import { Core } from "app/core";
import { TRACE_ICONS } from "app/trace_icons";
import { Component, Input, Output, EventEmitter, Inject, NgZone } from "@angular/core";
import { TraceCoordinator } from "app/trace_coordinator";
import { TRACE_INFO } from "app/trace_info";
import { LoadedTrace } from "app/loaded_trace";
@Component({
@@ -57,45 +57,43 @@ import { LoadedTrace } from "app/loaded_trace";
*ngIf="this.loadedTraces.length > 0"
>
<mat-list-item *ngFor="let trace of loadedTraces">
<mat-icon>{{TRACE_ICONS[trace.type]}}</mat-icon>
<mat-icon>{{TRACE_INFO[trace.type].icon}}</mat-icon>
<span>{{trace.name}} ({{trace.type}})
</span>
<button
(click)="onRemoveTrace(trace)"
><mat-icon class="file-icon">close</mat-icon>
class="icon-button"
><mat-icon>close</mat-icon>
</button>
</mat-list-item>
</mat-list>
</mat-card-content>
`,
styles: [".drop-info{font-weight: normal;}"]
styles: [
".drop-info{font-weight: normal; pointer-events: none;}",
]
})
export class UploadTracesComponent {
@Input()
core?: Core;
@Output()
coreChange = new EventEmitter<Core>();
@Input() traceCoordinator!: TraceCoordinator;
dataLoaded = false;
@Output()
dataLoadedChange = new EventEmitter<boolean>();
loadedTraces: LoadedTrace[] = [];
TRACE_ICONS = TRACE_ICONS;
@Output() dataLoadedChange = new EventEmitter<boolean>();
constructor(@Inject(NgZone) private ngZone: NgZone) {}
loadedTraces: LoadedTrace[] = [];
TRACE_INFO = TRACE_INFO;
public async onInputFile(event: Event) {
const files = this.getInputFiles(event);
await this.processFiles(files);
}
public async processFiles(files: File[]) {
await this.core?.addTraces(files);
await this.traceCoordinator.addTraces(files);
this.ngZone.run(() => {
if (this.core) this.loadedTraces = this.core.getLoadedTraces();
this.loadedTraces = this.traceCoordinator.getLoadedTraces();
});
}
@@ -114,7 +112,10 @@ export class UploadTracesComponent {
}
public onClearData() {
this.core?.clearData();
this.traceCoordinator.clearData();
this.dataLoaded = false;
this.loadedTraces = [];
this.dataLoadedChange.emit(this.dataLoaded);
}
public onFileDragIn(e: DragEvent) {
@@ -136,7 +137,7 @@ export class UploadTracesComponent {
}
public onRemoveTrace(trace: LoadedTrace) {
this.core?.removeTrace(trace.type);
this.traceCoordinator.removeTrace(trace.type);
this.loadedTraces = this.loadedTraces.filter(loaded => loaded.type !== trace.type);
}
}

View File

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

View File

@@ -21,8 +21,9 @@ import { setTraces } from "trace_collection/set_traces";
import { Viewer } from "viewers/viewer";
import { ViewerFactory } from "viewers/viewer_factory";
import { LoadedTrace } from "app/loaded_trace";
import { TRACE_INFO } from "./trace_info";
class Core {
class TraceCoordinator {
private parsers: Parser[];
private viewers: Viewer[];
@@ -37,7 +38,6 @@ class Core {
console.log("created parsers: ", this.parsers);
}
removeTrace(type: TraceType) {
this.parsers = this.parsers.filter(parser => parser.getTraceType() !== type);
}
@@ -62,10 +62,19 @@ class Core {
return this.viewers.map(viewer => viewer.getView());
}
getViewers(): Viewer[] {
return this.viewers;
}
loadedTraceTypes(): TraceType[] {
return this.parsers.map(parser => parser.getTraceType());
}
findParser(fileType: TraceType): Parser | null {
const parser = this.parsers.find(parser => parser.getTraceType() === fileType);
return parser ?? null;
}
getTimestamps(): Timestamp[] {
for (const type of [TimestampType.REAL, TimestampType.ELAPSED]) {
const mergedTimestamps: Timestamp[] = [];
@@ -94,8 +103,9 @@ class Core {
const traceEntries: Map<TraceType, any> = new Map<TraceType, any>();
this.parsers.forEach(parser => {
const entry = parser.getTraceEntry(timestamp);
if (entry != undefined) {
const targetTimestamp = timestamp;
const entry = parser.getTraceEntry(targetTimestamp);
if (entry !== undefined) {
traceEntries.set(parser.getTraceType(), entry);
}
});
@@ -106,10 +116,31 @@ class Core {
}
clearData() {
this.getViews().forEach(view => view.remove());
this.parsers = [];
this.viewers = [];
setTraces.dataReady = false;
}
saveTraces(traceTypes: TraceType[]) {
const blobs: Blob[] = [];
traceTypes.forEach(type => {
const trace = this.findParser(type)?.getTrace();
if (trace) {
blobs.push(trace);
}
});
blobs.forEach((blob, idx) => {
const a = document.createElement("a");
document.body.appendChild(a);
const url = window.URL.createObjectURL(blob);
a.href = url;
a.download = (blob as any).name ?? `${TRACE_INFO[traceTypes[idx]].name}.pb`;
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
});
}
}
export { Core };
export { TraceCoordinator };

View File

@@ -1,4 +1,4 @@
import { TraceType } from "../common/trace/trace_type";
import { TraceType } from "common/trace/trace_type";
const WINDOW_MANAGER_ICON = "view_compact";
const SURFACE_FLINGER_ICON = "filter_none";

View File

@@ -0,0 +1,88 @@
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 = "accessibility";
const TAG_ICON = "details";
const TRACE_ERROR_ICON = "warning";
type traceInfoMap = {
[key: number]: {
name: string,
icon: string
};
}
export const TRACE_INFO: traceInfoMap = {
[TraceType.ACCESSIBILITY]: {
name: "Accessibility",
icon: ACCESSIBILITY_ICON
},
[TraceType.WINDOW_MANAGER]: {
name: "Window Manager",
icon: WINDOW_MANAGER_ICON
},
[TraceType.SURFACE_FLINGER]: {
name: "Surface Flinger",
icon: SURFACE_FLINGER_ICON
},
[TraceType.SCREEN_RECORDING]: {
name: "Screen Recording",
icon: SCREEN_RECORDING_ICON
},
[TraceType.TRANSACTIONS]: {
name: "Transactions",
icon: TRANSACTION_ICON
},
[TraceType.TRANSACTIONS_LEGACY]: {
name: "Transactions Legacy",
icon: TRANSACTION_ICON
},
[TraceType.WAYLAND]: {
name: "Wayland",
icon: WAYLAND_ICON
},
[TraceType.WAYLAND_DUMP]: {
name: "Wayland Dump",
icon: WAYLAND_ICON
},
[TraceType.PROTO_LOG]: {
name: "Proto Log",
icon: PROTO_LOG_ICON
},
[TraceType.SYSTEM_UI]: {
name: "System UI",
icon: SYSTEM_UI_ICON
},
[TraceType.LAUNCHER]: {
name: "Launcher",
icon: LAUNCHER_ICON
},
[TraceType.INPUT_METHOD_CLIENTS]: {
name: "IME Clients",
icon: IME_ICON
},
[TraceType.INPUT_METHOD_SERVICE]: {
name: "IME Service",
icon: IME_ICON
},
[TraceType.INPUT_METHOD_MANAGER_SERVICE]: {
name: "IME Manager Service",
icon: IME_ICON
},
[TraceType.TAG]: {
name: "Tag",
icon: TAG_ICON
},
[TraceType.ERROR]: {
name: "Error",
icon: TRACE_ERROR_ICON
},
};

View File

@@ -16,11 +16,18 @@
@import "~@angular/material/prebuilt-themes/indigo-pink.css";
@import 'https://fonts.googleapis.com/icon?family=Material+Icons';
#app-title {
font-weight: bold;
font-family: Arial, Helvetica, sans-serif;
color:rgb(194, 65, 108);
font-size: 30;
}
#title {
font-weight: bold;
font-family: Arial, Helvetica, sans-serif;
color:rgb(194, 65, 108);
font-size: 20;
font-size: 24;
}
button {
@@ -29,20 +36,53 @@ button {
.homepage-card {
border: 1px solid rgb(129, 129, 129);
width: 45rem;
height: 30rem;
width: 50%;
height: 35rem;
overflow: auto;
display: flex;
margin: 10px;
}
mat-checkbox {
margin-left: 10px;
.trace-card {
border: 1px solid rgb(129, 129, 129);
height: 100%;
overflow: auto;
display: flex;
margin: 10px;
}
mat-form-field {
margin: 10px;
height: 5px;
.card-grid {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
overflow: auto;
}
mat-checkbox {
margin-left: 5px;
}
.mat-checkbox .mat-checkbox-frame {
transform: scale(0.7);
}
.mat-checkbox-checked .mat-checkbox-background {
transform: scale(0.7);
}
.mat-checkbox-indeterminate .mat-checkbox-background {
transform: scale(0.7);
}
.mat-radio-button, .mat-radio-button-frame {
transform: scale(0.8);
}
.mat-form-field {
transform: scale(0.85);
margin: 2px;
padding: 0;
}
mat-icon {
@@ -75,12 +115,12 @@ button.mat-raised-button {
}
.drop-box {
outline: 2px dashed rgb(194, 65, 108); /* the dash box */
outline: 2px dashed rgb(194, 65, 108);
outline-offset: -10px;
background: white;
color: rgb(194, 65, 108);
padding: 10px 10px 10px 10px;
min-height: 200px; /* minimum height */
min-height: 200px;
position: relative;
cursor: pointer;
display: flex;
@@ -104,4 +144,19 @@ button.mat-raised-button {
background-color: rgb(194, 65, 108);
border-radius: 21.5px;
cursor: pointer;
}
}
.viewers.hide {
display: none !important;
}
[hidden] {
display: none !important;
}
.icon-button {
background: none;
border: none;
display: inline-block;
vertical-align: middle;
}

View File

@@ -0,0 +1,35 @@
/*
* 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 {browser, element, by, ElementFinder} from "protractor";
import {E2eTestUtils} from "./utils";
describe("Viewer SurfaceFlinger", () => {
beforeAll(async () => {
browser.manage().timeouts().implicitlyWait(1000);
browser.get("file://" + E2eTestUtils.getProductionIndexHtmlPath());
}),
it("processes trace and renders view", () => {
const inputFile = element(by.css("input[type=\"file\"]"));
inputFile.sendKeys(E2eTestUtils.getFixturePath("traces/elapsed_and_real_timestamp/SurfaceFlinger.pb"));
const loadData = element(by.css(".load-btn"));
loadData.click();
const surfaceFlingerCard: ElementFinder = element(by.css(".trace-card"));
expect(surfaceFlingerCard.getText()).toContain("Surface Flinger");
});
});

View File

@@ -22,7 +22,7 @@ describe("winscope", () => {
}),
it("has title", () => {
const title = element(by.css("#title"));
const title = element(by.css("#app-title"));
expect(title.getText()).toContain("Winscope");
});
});

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { PersistentStore } from "../common/persistent_store";
import { PersistentStore } from "common/persistent_store";
import { configMap, TRACES } from "./trace_collection_utils";
import { setTraces, SetTraces } from "./set_traces";
import { Device } from "./connection";

View File

@@ -0,0 +1,416 @@
/*
* 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 { Rectangle } from "viewers/viewer_surface_flinger/ui_data";
import * as THREE from "three";
import { CSS2DRenderer, CSS2DObject } from "three/examples/jsm/renderers/CSS2DRenderer";
export class CanvasGraphics {
constructor() {
//set up camera
const left = -this.cameraHalfWidth,
right = this.cameraHalfWidth,
top = this.cameraHalfHeight,
bottom = -this.cameraHalfHeight,
near = 0.001,
far = 100;
this.camera = new THREE.OrthographicCamera(
left,right,top,bottom,near,far
);
}
initialise(canvas: HTMLCanvasElement) {
// initialise canvas
this.canvas = canvas;
}
refreshCanvas() {
//set canvas size
this.canvas!.style.width = "100%";
this.canvas!.style.height = "40rem";
// TODO: click and drag rotation control
this.camera.position.set(this.xyCameraPos, this.xyCameraPos, 6);
this.camera.lookAt(0, 0, 0);
this.camera.zoom = this.camZoom;
this.camera.updateProjectionMatrix();
// scene
const scene = new THREE.Scene();
// renderers
const renderer = new THREE.WebGLRenderer({
antialias: true,
canvas: this.canvas,
alpha: true
});
let labelRenderer: CSS2DRenderer;
if (document.querySelector("#labels-canvas")) {
labelRenderer = new CSS2DRenderer({
element: document.querySelector("#labels-canvas")! as HTMLElement
});
} else {
labelRenderer = new CSS2DRenderer();
labelRenderer.domElement.style.position = "absolute";
labelRenderer.domElement.style.top = "0px";
labelRenderer.domElement.style.width = "100%";
labelRenderer.domElement.style.height = "40rem";
labelRenderer.domElement.id = "labels-canvas";
labelRenderer.domElement.style.pointerEvents = "none";
document.querySelector(".canvas-container")?.appendChild(labelRenderer.domElement);
}
// set various factors for shading and shifting
const visibleDarkFactor = 0, nonVisibleDarkFactor = 0, rectCounter = 0;
const numberOfRects = this.rects.length;
const numberOfVisibleRects = this.rects.filter(rect => rect.isVisible).length;
const numberOfDisplayRects = this.rects.filter(rect => rect.isDisplay).length;
const zShift = numberOfRects*this.layerSeparation;
let xShift = 0, yShift = 3.25, labelYShift = 0;
if (this.isLandscape) {
xShift = 1;
yShift = 1.5;
labelYShift = 1.25;
}
const lowestY = Math.min(...this.rects.map(rect => {
const y = rect.topLeft.y - rect.height + this.lowestYShift;
if (this.isLandscape) {
if (y<0) {
return 0;
} else if (y > 2) {
return 2;
}
} else if (y > -1) {
return -1;
}
return y;
})) - labelYShift;
this.drawScene(
rectCounter,
numberOfVisibleRects,
visibleDarkFactor,
numberOfDisplayRects,
nonVisibleDarkFactor,
numberOfRects,
scene,
xShift,
yShift,
zShift,
lowestY
);
// const axesHelper = new THREE.AxesHelper(1);
// const gridHelper = new THREE.GridHelper(5);
// scene.add(axesHelper, gridHelper)
renderer.setSize(this.canvas!.clientWidth, this.canvas!.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.render(scene, this.camera);
labelRenderer.setSize(this.canvas!.clientWidth, this.canvas!.clientHeight);
labelRenderer.render(scene, this.camera);
}
private drawScene(
rectCounter: number,
visibleRects: number,
visibleDarkFactor:number,
displayRects: number,
nonVisibleDarkFactor: number,
numberOfRects: number,
scene: THREE.Scene,
xShift: number,
yShift: number,
zShift: number,
lowestY: number
) {
this.targetObjects = [];
this.rects.forEach(rect => {
const visibleViewInvisibleRect = this.visibleView && !rect.isVisible;
const xrayViewNoVirtualDisplaysVirtualRect = !this.visibleView && !this.showVirtualDisplays && rect.isDisplay && rect.isVirtual;
if (visibleViewInvisibleRect || xrayViewNoVirtualDisplaysVirtualRect) {
rectCounter++;
return;
}
//set colour mapping
let planeColor;
if (this.highlighted === `${rect.id}`) {
planeColor = this.colorMapping("highlight", numberOfRects, 0);
} else if (rect.isVisible) {
planeColor = this.colorMapping("green", visibleRects, visibleDarkFactor);
visibleDarkFactor++;
} else if (rect.isDisplay) {
planeColor = this.colorMapping("grey", displayRects, nonVisibleDarkFactor);
nonVisibleDarkFactor++;
} else {
planeColor = this.colorMapping("unknown", numberOfRects, 0);
}
//set plane geometry and material
const geometry = new THREE.PlaneGeometry(rect.width, rect.height);
const planeRect = this.setPlaneMaterial(rect, geometry, planeColor, xShift, yShift, zShift);
scene.add(planeRect);
zShift -= this.layerSeparation;
// bolder edges of each plane if in x-ray view
if (!this.visibleView) {
const edgeSegments = this.setEdgeMaterial(planeRect, geometry);
scene.add(edgeSegments);
}
// label circular marker
const circle = this.setCircleMaterial(planeRect, rect);
scene.add(circle);
this.targetObjects.push(planeRect);
// label line
const [line, rectLabel] = this.createLabel(rect, circle, lowestY, rectCounter);
scene.add(line);
scene.add(rectLabel);
rectCounter++;
});
}
private setPlaneMaterial(
rect: Rectangle,
geometry: THREE.PlaneGeometry,
color: THREE.Color,
xShift: number,
yShift: number,
zShift: number
) {
const planeRect = new THREE.Mesh(
geometry,
new THREE.MeshBasicMaterial({
color: color,
opacity: this.visibleView ? 1 : 0.75,
transparent: true,
}));
planeRect.position.y = rect.topLeft.y - rect.height/2 + yShift;
planeRect.position.x = rect.topLeft.x + rect.width/2 - xShift;
planeRect.position.z = zShift;
planeRect.name = `${rect.id}`;
return planeRect;
}
private setEdgeMaterial(planeRect: THREE.Mesh, geometry: THREE.PlaneGeometry) {
const edgeColor = 0x000000;
const edgeGeo = new THREE.EdgesGeometry(geometry);
const edgeMaterial = new THREE.LineBasicMaterial({color: edgeColor, linewidth: 1});
const edgeSegments = new THREE.LineSegments(
edgeGeo, edgeMaterial
);
edgeSegments.position.set(planeRect.position.x, planeRect.position.y, planeRect.position.z);
return edgeSegments;
}
private setCircleMaterial(planeRect: THREE.Mesh, rect: Rectangle) {
const labelCircle = new THREE.CircleGeometry(0.02, 200);
const circleMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 });
const circle = new THREE.Mesh(labelCircle, circleMaterial);
circle.position.set(
planeRect.position.x + rect.width/2 - 0.05,
planeRect.position.y,
planeRect.position.z + 0.05
);
circle.rotateY(THREE.MathUtils.degToRad(30));
return circle;
}
private createLabel(rect: Rectangle, circle: THREE.Mesh, lowestY: number, rectCounter: number):
[THREE.Line, CSS2DObject] {
const labelText = this.shortenText(rect.label);
const isGrey = !this.visibleView && !rect.isVisible;
let cornerPos, endPos;
const labelYSeparation = 0.3;
if (this.isLandscape) {
cornerPos = new THREE.Vector3(
circle.position.x, lowestY - 0.5 - rectCounter*labelYSeparation, circle.position.z
);
} else {
cornerPos = new THREE.Vector3(
circle.position.x, lowestY + 0.5 - rectCounter*labelYSeparation, circle.position.z
);
}
const linePoints = [circle.position, cornerPos];
if (this.isLandscape && cornerPos.x > 0 || !this.isLandscape) {
endPos = new THREE.Vector3(cornerPos.x - 1, cornerPos.y - this.labelShift, cornerPos.z);
} else {
endPos = cornerPos;
}
linePoints.push(endPos);
//add rectangle label
document.querySelector(`.label-${rectCounter}`)?.remove();
const rectLabelDiv: HTMLElement = document.createElement("div");
this.labelElements.push(rectLabelDiv);
rectLabelDiv.className = `label-${rectCounter}`;
rectLabelDiv.textContent = labelText;
rectLabelDiv.style.fontSize = "10px";
if (isGrey) {
rectLabelDiv.style.color = "grey";
}
const rectLabel = new CSS2DObject(rectLabelDiv);
rectLabel.name = rect.label;
const textCanvas = document.createElement("canvas");
const labelContext = textCanvas.getContext("2d");
let labelWidth = 0;
if (labelContext?.font) {
labelContext.font = rectLabelDiv.style.font;
labelWidth = labelContext?.measureText(labelText).width;
}
textCanvas.remove();
if (this.isLandscape && endPos.x < 0) {
rectLabel.position.set(
endPos.x + 0.6, endPos.y - 0.15, endPos.z - 0.6
);
} else {
rectLabel.position.set(
endPos.x - labelWidth * this.labelXFactor, endPos.y - this.labelShift * labelWidth * this.labelXFactor, endPos.z
);
}
const lineGeo = new THREE.BufferGeometry().setFromPoints(linePoints);
const lineMaterial = new THREE.LineBasicMaterial({color: isGrey ? 0x808080 : 0x000000});
const line = new THREE.Line(lineGeo, lineMaterial);
return [line, rectLabel];
}
getCamera() {
return this.camera;
}
getTargetObjects() {
return this.targetObjects;
}
getLayerSeparation() {
return this.layerSeparation;
}
getVisibleView() {
return this.visibleView;
}
getXyCameraPos() {
return this.xyCameraPos;
}
getShowVirtualDisplays() {
return this.showVirtualDisplays;
}
updateLayerSeparation(userInput: number) {
this.layerSeparation = userInput;
}
updateRotation(userInput: number) {
this.xyCameraPos = userInput;
this.camZoom = userInput/4 * 0.2 + 0.9;
this.labelShift = userInput/4 * this.maxLabelShift;
this.lowestYShift = userInput/4 + 2;
}
updateHighlighted(highlighted: string) {
this.highlighted = highlighted;
}
updateRects(rects: Rectangle[]) {
this.rects = rects;
}
updateIsLandscape(isLandscape: boolean) {
this.isLandscape = isLandscape;
}
updateVisibleView(visible: boolean) {
this.visibleView = visible;
}
updateVirtualDisplays(show: boolean) {
this.showVirtualDisplays = show;
}
clearLabelElements() {
this.labelElements.forEach(el => el.remove());
}
updateZoom(isZoomIn: boolean) {
if (isZoomIn && this.camZoom < 2) {
this.camZoom += this.camZoomFactor * 1.5;
} else if (!isZoomIn && this.camZoom > 0.5) {
this.camZoom -= this.camZoomFactor * 1.5;
}
}
colorMapping(scale: string, numberOfRects: number, darkFactor:number): THREE.Color {
if (scale === "highlight") {
return new THREE.Color(0xD2E3FC);
} else if (scale === "grey") {
// darkness of grey rect depends on z order - darkest 64, lightest 128
//Separate RGB values between 0 and 1
const lower = 120;
const upper = 220;
const darkness = ((upper-lower)*(numberOfRects-darkFactor)/numberOfRects + lower)/255;
return new THREE.Color(darkness, darkness, darkness);
} else if (scale === "green") {
// darkness of green rect depends on z order
//Separate RGB values between 0 and 1
const red = ((200-45)*(numberOfRects-darkFactor)/numberOfRects + 45)/255;
const green = ((232-182)*(numberOfRects-darkFactor)/numberOfRects + 182)/255;
const blue = ((183-44)*(numberOfRects-darkFactor)/numberOfRects + 44)/255;
return new THREE.Color(red, green, blue);
} else {
return new THREE.Color(0, 0, 0);
}
}
shortenText(text: string): string {
if (text.length > 40) {
text = text.slice(0, 40);
}
return text;
}
// dynamic scaling and canvas variables
readonly cameraHalfWidth = 2.8;
readonly cameraHalfHeight = 3.2;
private readonly maxLabelShift = 0.305;
private readonly labelXFactor = 0.008;
private lowestYShift = 3;
private camZoom = 1.1;
private camZoomFactor = 0.1;
private labelShift = this.maxLabelShift;
private highlighted = "";
private visibleView = false;
private isLandscape = false;
private showVirtualDisplays = false;
private layerSeparation = 0.4;
private xyCameraPos = 4;
private camera: THREE.OrthographicCamera;
private rects: Rectangle[] = [];
private labelElements: HTMLElement[] = [];
private targetObjects: any[] = [];
private canvas?: HTMLCanvasElement;
}

View File

@@ -0,0 +1,29 @@
/*
* 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 { Component } from "@angular/core";
@Component({
selector: "hierarchy-view",
template: `
<mat-card-title class="trace-view-subtitle">Hierarchy</mat-card-title>
`,
styles: [
".trace-view-subtitle { font-size: 18px}"
]
})
export class HierarchyComponent {
}

View File

@@ -0,0 +1,26 @@
/*
* 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 { Component } from "@angular/core";
@Component({
selector: "properties-view",
template: `
<mat-card-title class="trace-view-subtitle">Properties</mat-card-title>
`,
})
export class PropertiesComponent {
}

View File

@@ -0,0 +1,121 @@
/*
* 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 { CommonModule } from "@angular/common";
import { Component , ViewChild } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { RectsComponent } from "./rects.component";
import { MatCheckboxModule } from "@angular/material/checkbox";
import { MatCardModule } from "@angular/material/card";
import { MatRadioModule } from "@angular/material/radio";
import { MatSliderModule } from "@angular/material/slider";
import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { Rectangle } from "./viewer_surface_flinger/ui_data";
describe("RectsComponent", () => {
let component: TestHostComponent;
let fixture: ComponentFixture<TestHostComponent>;
let htmlElement: HTMLElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
CommonModule,
MatCheckboxModule,
MatCardModule,
MatSliderModule,
MatRadioModule
],
declarations: [RectsComponent, TestHostComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TestHostComponent);
component = fixture.componentInstance;
htmlElement = fixture.nativeElement;
fixture.detectChanges();
});
it("can be created", () => {
expect(component).toBeTruthy();
});
it("check that layer separation slider is rendered", () => {
fixture.detectChanges();
const slider = htmlElement.querySelector("mat-slider");
expect(slider).toBeTruthy();
});
it("check that layer separation slider causes view to change", () => {
const slider = htmlElement.querySelector("mat-slider");
spyOn(component.rectsComponent.canvasGraphics, "updateLayerSeparation");
slider?.dispatchEvent(new MouseEvent("mousedown"));
fixture.detectChanges();
expect(component.rectsComponent.canvasGraphics.updateLayerSeparation).toHaveBeenCalled();
});
it("check that rects canvas is rendered", () => {
fixture.detectChanges();
const rectsCanvas = htmlElement.querySelector("#rects-canvas");
expect(rectsCanvas).toBeTruthy();
});
it("check that canvas is refreshed if rects are present", async () => {
component.addRects([
{
topLeft: {x:0, y:0},
bottomRight: {x:1, y:-1},
label: "rectangle1",
transform: {
matrix: {
dsdx: 1,
dsdy: 0,
dtdx: 0,
dtdy: 1,
tx: 0,
ty: 0
}
},
height: 1,
width: 1,
isVisible: true,
isDisplay: false,
ref: null,
id: 12345,
stackId: 0,
}
]);
spyOn(component.rectsComponent, "drawRects").and.callThrough();
fixture.detectChanges();
expect(component.rectsComponent.drawRects).toHaveBeenCalled();
});
@Component({
selector: "host-component",
template: "<rects-view [rects]=\"rects\"></rects-view>"
})
class TestHostComponent {
public rects: Rectangle[] = [];
addRects(newRects: Rectangle[]) {
this.rects = newRects;
}
@ViewChild(RectsComponent)
public rectsComponent!: RectsComponent;
}
});

View File

@@ -0,0 +1,286 @@
/*
* 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 { Component, Input, OnChanges, OnDestroy, Inject, ElementRef, SimpleChanges } from "@angular/core";
import { RectsUtils } from "./rects_utils";
import { Point, Rectangle, RectMatrix, RectTransform } from "viewers/viewer_surface_flinger/ui_data";
import { interval, Subscription } from "rxjs";
import { CanvasGraphics } from "./canvas_graphics";
import * as THREE from "three";
@Component({
selector: "rects-view",
template: `
<mat-card-header class="view-controls">
<mat-radio-group (change)="onChangeView($event.value)">
<mat-radio-button class="visible-radio" [value]="true" [checked]="visibleView()">Visible</mat-radio-button>
<mat-radio-button class="xray-radio" [value]="false" [checked]="!visibleView()">X-ray</mat-radio-button>
</mat-radio-group>
<mat-slider
step="0.001"
min="0.1"
max="0.4"
aria-label="units"
[value]="getLayerSeparation()"
(input)="canvasGraphics.updateLayerSeparation($event.value!)"
></mat-slider>
<mat-slider
step="0.01"
min="0.00"
max="4"
aria-label="units"
[value]="xyCameraPos()"
(input)="canvasGraphics.updateRotation($event.value!)"
></mat-slider>
<mat-checkbox
[hidden]="visibleView()"
class="rects-checkbox"
[checked]="showVirtualDisplays()"
(change)="canvasGraphics.updateVirtualDisplays($event.checked!)"
>Show virtual displays</mat-checkbox>
</mat-card-header>
<mat-card-content class="rects-content">
<div class="canvas-container">
<div class="zoom-container">
<button id="zoom-btn" (click)="canvasGraphics.updateZoom(true)">
<mat-icon aria-hidden="true">
zoom_in
</mat-icon>
</button>
<button id="zoom-btn" (click)="canvasGraphics.updateZoom(false)">
<mat-icon aria-hidden="true">
zoom_out
</mat-icon>
</button>
</div>
<canvas id="rects-canvas" (click)="onRectClick($event)">
</canvas>
</div>
</mat-card-content>
`,
styles: [
"@import 'https://fonts.googleapis.com/icon?family=Material+Icons';",
".rects-content {position: relative}",
".canvas-container {height: 40rem; width: 100%; position: relative}",
"#rects-canvas {height: 40rem; width: 100%; cursor: pointer; position: absolute; top: 0px}",
"#labels-canvas {height: 40rem; width: 100%; position: absolute; top: 0px}",
".view-controls {display: inline-block; position: relative; min-height: 72px}",
".zoom-container {position: absolute; top: 0px; z-index: 10}",
"#zoom-btn {position:relative; display: block; background: none; border: none}",
"mat-radio-button {font-size: 16px; font-weight: normal}",
".mat-radio-button, .mat-radio-button-frame {transform: scale(0.8);}",
".rects-checkbox {font-size: 14px; font-weight: normal}",
"mat-icon {margin: 5px}",
"mat-checkbox {margin-left: 5px;}",
".mat-checkbox .mat-checkbox-frame { transform: scale(0.7);}",
".mat-checkbox-checked .mat-checkbox-background {transform: scale(0.7);}",
".mat-checkbox-indeterminate .mat-checkbox-background {transform: scale(0.7);}",
]
})
export class RectsComponent implements OnChanges, OnDestroy {
@Input() rects!: Rectangle[];
@Input() highlighted = "";
constructor(
@Inject(ElementRef) private elementRef: ElementRef,
) {
this.canvasGraphics = new CanvasGraphics();
}
ngOnDestroy() {
if (this.canvasSubscription) {
this.canvasSubscription.unsubscribe();
}
}
ngOnChanges(changes: SimpleChanges) {
if (this.rects.length > 0) {
//change in rects so they must undergo transformation and scaling before canvas refreshed
this.canvasGraphics.clearLabelElements();
this.rects = this.rects.filter(rect => rect.isVisible || rect.isDisplay);
this.displayRects = this.rects.filter(rect => rect.isDisplay);
this.computeBounds();
this.rects = this.rects.map(rect => {
if (changes["rects"] && rect.transform) {
return RectsUtils.transformRect(rect.transform.matrix ?? rect.transform, rect);
} else {
return rect;
}
});
this.scaleRects();
this.drawRects();
} else if (this.canvasSubscription) {
this.canvasSubscription.unsubscribe();
}
}
onRectClick(event:PointerEvent) {
this.setNormalisedMousePos(event);
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(this.mouse, this.canvasGraphics.getCamera());
// create an array containing all objects in the scene with which the ray intersects
const intersects = raycaster.intersectObjects(this.canvasGraphics.getTargetObjects());
// if there is one (or more) intersections
if (intersects.length > 0){
if (this.highlighted === intersects[0].object.name) {
this.highlighted = "";
this.canvasGraphics.updateHighlighted("");
} else {
this.highlighted = intersects[0].object.name;
this.canvasGraphics.updateHighlighted(intersects[0].object.name);
}
this.updateHighlightedRect();
}
}
setNormalisedMousePos(event:PointerEvent) {
event.preventDefault();
const canvas = (event.target as Element);
const canvasOffset = canvas.getBoundingClientRect();
this.mouse.x = ((event.clientX-canvasOffset.left)/canvas.clientWidth) * 2 - 1;
this.mouse.y = -((event.clientY-canvasOffset.top)/canvas.clientHeight) * 2 + 1;
this.mouse.z = 0;
}
updateHighlightedRect() {
const event: CustomEvent = new CustomEvent("highlightedChange", {
bubbles: true,
detail: { layerId: this.highlighted }
});
this.elementRef.nativeElement.dispatchEvent(event);
}
drawRects() {
if (this.canvasSubscription) {
this.canvasSubscription.unsubscribe();
}
const canvas = document.getElementById("rects-canvas") as HTMLCanvasElement;
this.canvasGraphics.initialise(canvas);
this.canvasSubscription = this.drawRectsInterval.subscribe(() => {
this.updateVariablesBeforeRefresh();
this.canvasGraphics.refreshCanvas();
});
}
updateVariablesBeforeRefresh() {
this.canvasGraphics.updateRects(this.rects);
const biggestX = Math.max(...this.rects.map(rect => rect.topLeft.x + rect.width/2));
this.canvasGraphics.updateIsLandscape(biggestX > this.s({x: this.boundsWidth, y:this.boundsHeight}).x/2);
}
onChangeView(visible: boolean) {
this.canvasGraphics.updateVisibleView(visible);
this.canvasGraphics.clearLabelElements();
}
scaleRects() {
this.rects = this.rects.map(rect => {
rect.bottomRight = this.s(rect.bottomRight);
rect.topLeft = this.s(rect.topLeft);
rect.height = Math.abs(rect.topLeft.y - rect.bottomRight.y);
rect.width = Math.abs(rect.bottomRight.x - rect.topLeft.x);
const mat = this.getMatrix(rect);
if (mat) {
const newTranslation = this.s({x: mat.tx!, y: mat.ty!});
mat.tx = newTranslation.x;
mat.ty = newTranslation.y;
}
return rect;
});
}
computeBounds(): any {
this.boundsWidth = Math.max(...this.rects.map((rect) => {
const mat = this.getMatrix(rect);
if (mat) {
return RectsUtils.transformRect(mat, rect).width;
} else {
return rect.width;
}}));
this.boundsHeight = Math.max(...this.rects.map((rect) => {
const mat = this.getMatrix(rect);
if (mat) {
return RectsUtils.transformRect(mat, rect).height;
} else {
return rect.height;
}}));
if (this.displayRects.length > 0) {
this.boundsWidth = Math.min(this.boundsWidth, this.maxWidth());
this.boundsHeight = Math.min(this.boundsHeight, this.maxHeight());
}
}
maxWidth() {
return Math.max(...this.displayRects.map(rect => rect.width)) * 1.2;
}
maxHeight() {
return Math.max(...this.displayRects.map(rect => rect.height)) * 1.2;
}
// scales coordinates to canvas
s(sourceCoordinates: Point) {
let scale;
if (this.boundsWidth < this.boundsHeight) {
scale = this.canvasGraphics.cameraHalfHeight*2 * 0.6 / this.boundsHeight;
} else {
scale = this.canvasGraphics.cameraHalfWidth*2 * 0.6 / this.boundsWidth;
}
return {
x: sourceCoordinates.x * scale,
y: sourceCoordinates.y * scale,
};
}
getMatrix(rect: Rectangle) {
if (rect.transform) {
let matrix: RectTransform | RectMatrix = rect.transform;
if (rect.transform && rect.transform.matrix) {
matrix = rect.transform.matrix;
}
return matrix;
} else {
return false;
}
}
visibleView() {
return this.canvasGraphics.getVisibleView();
}
getLayerSeparation() {
return this.canvasGraphics.getLayerSeparation();
}
xyCameraPos() {
return this.canvasGraphics.getXyCameraPos();
}
showVirtualDisplays() {
return this.canvasGraphics.getShowVirtualDisplays();
}
canvasGraphics: CanvasGraphics;
private readonly _60fpsInterval = 16.66666666666667;
private drawRectsInterval = interval(this._60fpsInterval);
private boundsWidth = 0;
private boundsHeight = 0;
private displayRects!: Rectangle[];
private canvasSubscription?: Subscription;
private mouse = new THREE.Vector3(0, 0, 0);
}

View File

@@ -0,0 +1,59 @@
/*
* 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 { RectsUtils } from "./rects_utils";
describe("RectsUtils", () => {
it("transforms rect", () => {
const transform = {
matrix: {
dsdx: 1,
dsdy: 0,
dtdx: 0,
dtdy: 1,
tx: 1,
ty: 1
}
};
const rect = {
topLeft: {x: 0, y: 0},
bottomRight: {x: 1, y: -1},
label: "TestRect",
transform: transform,
isVisible: true,
isDisplay: false,
height: 1,
width: 1,
ref: null,
id: 12345,
stackId: 0
};
const expected = {
topLeft: {x: 1, y: 1},
bottomRight: {x: 2, y: 0},
label: "TestRect",
transform: transform,
isVisible: true,
isDisplay: false,
height: 1,
width: 1,
ref: null,
id: 12345,
stackId: 0,
isVirtual: undefined
};
expect(RectsUtils.transformRect(rect.transform.matrix, rect)).toEqual(expected);
});
});

View File

@@ -0,0 +1,60 @@
/*
* 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 { Point, Rectangle, RectMatrix, RectTransform } from "viewers/viewer_surface_flinger/ui_data";
export const RectsUtils = {
multiplyMatrix(matrix:any, corner: Point): Point {
if (!matrix) return corner;
// |dsdx dsdy tx| | x | |x*dsdx + y*dsdy + tx|
// |dtdx dtdy ty| x | y | = |x*dtdx + y*dtdy + ty|
// |0 0 1 | | 1 | | 1 |
return {
x: matrix.dsdx * corner.x + matrix.dsdy * corner.y + matrix.tx,
y: matrix.dtdx * corner.x + matrix.dtdy * corner.y + matrix.ty,
};
},
transformRect(matrix: RectMatrix | RectTransform, rect:Rectangle): Rectangle {
// | dsdx dsdy tx | | left top 1 |
// matrix = | dtdx dtdy ty | rect = | 1 1 1 |
// | 0 0 1 | | 1 right bottom |
const tl = this.multiplyMatrix(matrix, rect.topLeft);
const tr = this.multiplyMatrix(matrix, {x:rect.bottomRight.x, y:rect.topLeft.y});
const bl = this.multiplyMatrix(matrix, {x:rect.topLeft.x, y:rect.bottomRight.y});
const br = this.multiplyMatrix(matrix, rect.bottomRight);
const left = Math.min(tl.x, tr.x, bl.x, br.x);
const top = Math.max(tl.y, tr.y, bl.y, br.y);
const right = Math.max(tl.x, tr.x, bl.x, br.x);
const bottom = Math.min(tl.y, tr.y, bl.y, br.y);
const outrect: Rectangle = {
topLeft: {x: left, y: top},
bottomRight: {x: right, y: bottom},
label: rect.label,
transform: rect.transform,
isVisible: rect.isVisible,
isDisplay: rect.isDisplay,
height: Math.abs(top - bottom),
width: Math.abs(right - left),
ref: rect.ref,
id: rect.id,
stackId: rect.stackId,
isVirtual: rect.isVirtual
};
return outrect;
}
};

View File

@@ -19,6 +19,8 @@ interface Viewer {
//TODO: add TraceEntry data type
notifyCurrentTraceEntries(entries: Map<TraceType, any>): void;
getView(): HTMLElement;
getTitle(): string;
getDependencies(): TraceType[];
}
export { Viewer };

View File

@@ -16,10 +16,12 @@
import { TraceType } from "common/trace/trace_type";
import { Viewer } from "./viewer";
import { ViewerWindowManager } from "./viewer_window_manager/viewer_window_manager";
import { ViewerSurfaceFlinger } from "./viewer_surface_flinger/viewer_surface_flinger";
class ViewerFactory {
static readonly VIEWERS = [
ViewerWindowManager,
ViewerSurfaceFlinger
];
public createViewers(activeTraceTypes: Set<TraceType>): Viewer[] {
@@ -29,7 +31,6 @@ class ViewerFactory {
const areViewerDepsSatisfied = Viewer.DEPENDENCIES.every((traceType: TraceType) =>
activeTraceTypes.has(traceType)
);
if (areViewerDepsSatisfied) {
viewers.push(new Viewer());
}

View File

@@ -0,0 +1,108 @@
/*
* 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 { Rectangle, RectMatrix, RectTransform, UiData } from "viewers/viewer_surface_flinger/ui_data";
import { TraceType } from "common/trace/trace_type";
type NotifyViewCallbackType = (uiData: UiData) => void;
class Presenter {
constructor(notifyViewCallback: NotifyViewCallbackType) {
this.notifyViewCallback = notifyViewCallback;
this.uiData = new UiData("Initial UI data");
this.notifyViewCallback(this.uiData);
}
updateHighlightedRect(event: CustomEvent) {
this.highlighted = event.detail.layerId;
this.uiData.highlighted = this.highlighted;
console.log("changed highlighted rect: ", this.uiData.highlighted);
this.notifyViewCallback(this.uiData);
}
notifyCurrentTraceEntries(entries: Map<TraceType, any>) {
const entry = entries.get(TraceType.SURFACE_FLINGER);
this.uiData = new UiData("New surface flinger ui data");
const displayRects = entry.displays.map((display: any) => {
const rect = display.layerStackSpace;
rect.label = display.name;
rect.id = display.id;
rect.stackId = display.layerStackId;
rect.isDisplay = true;
rect.isVirtual = display.isVirtual;
return rect;
}) ?? [];
this.uiData.highlighted = this.highlighted;
this.uiData.rects = this.rectsToUiData(entry.rects.concat(displayRects));
this.notifyViewCallback(this.uiData);
}
rectsToUiData(rects: any[]): Rectangle[] {
const uiRects: Rectangle[] = [];
rects.forEach((rect: any) => {
let t = null;
if (rect.transform && rect.transform.matrix) {
t = rect.transform.matrix;
} else if (rect.transform) {
t = rect.transform;
}
let transform: RectTransform | null = null;
if (t !== null) {
const matrix: RectMatrix = {
dsdx: t.dsdx,
dsdy: t.dsdy,
dtdx: t.dtdx,
dtdy: t.dtdy,
tx: t.tx,
ty: -t.ty
};
transform = {
matrix: matrix,
};
}
let isVisible = false, isDisplay = false;
if (rect.ref && rect.ref.isVisible) {
isVisible = rect.ref.isVisible;
}
if (rect.isDisplay) {
isDisplay = rect.isDisplay;
}
const newRect: Rectangle = {
topLeft: {x: rect.left, y: rect.top},
bottomRight: {x: rect.right, y: -rect.bottom},
height: rect.height,
width: rect.width,
label: rect.label,
transform: transform,
isVisible: isVisible,
isDisplay: isDisplay,
ref: rect.ref,
id: rect.id ?? rect.ref.id,
stackId: rect.stackId ?? rect.ref.stackId,
isVirtual: rect.isVirtual
};
uiRects.push(newRect);
});
return uiRects;
}
private readonly notifyViewCallback: NotifyViewCallbackType;
private uiData: UiData;
private highlighted = "";
}
export {Presenter};

View File

@@ -0,0 +1,63 @@
/*
* 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.
*/
class UiData {
constructor(public text: string) {
console.log(text);
}
rects?: Rectangle[] = [];
highlighted?: string = "";
}
export interface Rectangle {
topLeft: Point;
bottomRight: Point;
label: string;
transform: RectTransform | null;
height: number;
width: number;
isVisible: boolean;
isDisplay: boolean;
ref: any;
id: number;
stackId: number;
isVirtual?: boolean;
}
export interface Point {
x: number,
y: number
}
export interface RectTransform {
matrix?: RectMatrix;
dsdx?: number;
dsdy?: number;
dtdx?: number;
dtdy?: number;
tx?: number;
ty?: number;
}
export interface RectMatrix {
dsdx: number;
dsdy: number;
dtdx: number;
dtdy: number;
tx: number;
ty: number;
}
export {UiData};

View File

@@ -0,0 +1,75 @@
/*
* 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 {ComponentFixture, TestBed} from "@angular/core/testing";
import {ViewerSurfaceFlingerComponent} from "./viewer_surface_flinger.component";
import { HierarchyComponent } from "viewers/hierarchy.component";
import { PropertiesComponent } from "viewers/properties.component";
import { RectsComponent } from "viewers/rects.component";
import { MatIconModule } from "@angular/material/icon";
import { MatCardModule } from "@angular/material/card";
import { ComponentFixtureAutoDetect } from "@angular/core/testing";
import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
describe("ViewerSurfaceFlingerComponent", () => {
let fixture: ComponentFixture<ViewerSurfaceFlingerComponent>;
let component: ViewerSurfaceFlingerComponent;
let htmlElement: HTMLElement;
beforeAll(async () => {
await TestBed.configureTestingModule({
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true }
],
imports: [
MatIconModule,
MatCardModule
],
declarations: [
ViewerSurfaceFlingerComponent,
HierarchyComponent,
PropertiesComponent,
RectsComponent
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ViewerSurfaceFlingerComponent);
component = fixture.componentInstance;
htmlElement = fixture.nativeElement;
});
it("can be created", () => {
expect(component).toBeTruthy();
});
it("creates rects view", () => {
const rectsView = htmlElement.querySelector(".rects-view");
expect(rectsView).toBeTruthy();
});
it("creates hierarchy view", () => {
const hierarchyView = htmlElement.querySelector("#sf-hierarchy-view");
expect(hierarchyView).toBeTruthy();
});
it("creates properties view", () => {
const propertiesView = htmlElement.querySelector("#sf-properties-view");
expect(propertiesView).toBeTruthy();
});
});

View File

@@ -0,0 +1,60 @@
/*
* 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 {
Component,
Input
} from "@angular/core";
import { UiData } from "./ui_data";
import { TRACE_INFO } from "app/trace_info";
import { TraceType } from "common/trace/trace_type";
@Component({
selector: "viewer-surface-flinger",
template: `
<div fxLayout="row wrap" fxLayoutGap="10px grid" class="card-grid">
<mat-card class="rects-view">
<rects-view
[rects]="inputData?.rects ?? []"
[highlighted]="inputData?.highlighted ?? ''"
class="rects-view"
></rects-view>
</mat-card>
<mat-card id="sf-hierarchy-view" class="hierarchy-view">
<hierarchy-view></hierarchy-view>
</mat-card>
<mat-card id="sf-properties-view" class="properties-view">
<properties-view></properties-view>
</mat-card>
</div>
`,
styles: [
"@import 'https://fonts.googleapis.com/icon?family=Material+Icons';",
"mat-icon {margin: 5px}",
"viewer-surface-flinger {font-family: Arial, Helvetica, sans-serif;}",
".trace-card-title {display: inline-block; vertical-align: middle;}",
".header-button {background: none; border: none; display: inline-block; vertical-align: middle;}",
".card-grid {width: 100%;height: 100%;display: flex;flex-direction: row;overflow: auto;}",
".rects-view {font: inherit; flex: none !important;width: 400px;margin: 8px;}",
".hierarchy-view, .properties-view {font: inherit; flex: 1;margin: 8px;min-width: 400px;min-height: 50rem;max-height: 50rem;}",
]
})
export class ViewerSurfaceFlingerComponent {
@Input()
inputData?: UiData;
TRACE_INFO = TRACE_INFO;
TraceType = TraceType;
}

View File

@@ -0,0 +1,51 @@
/*
* 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 {TraceType} from "common/trace/trace_type";
import {Viewer} from "viewers/viewer";
import {Presenter} from "./presenter";
import {UiData} from "./ui_data";
class ViewerSurfaceFlinger implements Viewer {
constructor() {
this.view = document.createElement("viewer-surface-flinger");
this.presenter = new Presenter((uiData: UiData) => {
(this.view as any).inputData = uiData;
});
this.view.addEventListener("highlightedChange", (event) => this.presenter.updateHighlightedRect((event as CustomEvent)));
}
public notifyCurrentTraceEntries(entries: Map<TraceType, any>): void {
this.presenter.notifyCurrentTraceEntries(entries);
}
public getView(): HTMLElement {
return this.view;
}
public getTitle(): string {
return "Surface Flinger";
}
public getDependencies(): TraceType[] {
return ViewerSurfaceFlinger.DEPENDENCIES;
}
public static readonly DEPENDENCIES: TraceType[] = [TraceType.SURFACE_FLINGER];
private view: HTMLElement;
private presenter: Presenter;
}
export {ViewerSurfaceFlinger};

View File

@@ -16,29 +16,29 @@
import {TraceType} from "common/trace/trace_type";
import {UiData} from "./ui_data";
type UiDataCallbackType = (uiData: UiData) => void;
type NotifyViewCallbackType = (uiData: UiData) => void;
class Presenter {
constructor(uiDataCallback: UiDataCallbackType) {
this.uiDataCallback = uiDataCallback;
constructor(notifyViewCallback: NotifyViewCallbackType) {
this.notifyViewCallback = notifyViewCallback;
this.uiData = new UiData("Initial UI data");
this.uiDataCallback(this.uiData);
this.notifyViewCallback(this.uiData);
}
public notifyCurrentTraceEntries(entries: Map<TraceType, any>) {
this.uiData = new UiData("UI data selected by user on time scrub");
this.uiDataCallback(this.uiData);
this.notifyViewCallback(this.uiData);
}
public notifyUiEvent() {
const oldUiDataText = this.uiData ? this.uiData.text : "";
this.uiData = new UiData(oldUiDataText);
this.uiData.text += " | UI data updated because of UI event";
this.uiDataCallback(this.uiData!);
this.notifyViewCallback(this.uiData!);
}
private readonly uiDataCallback: UiDataCallbackType;
private uiData?: UiData;
readonly notifyViewCallback: NotifyViewCallbackType;
uiData?: UiData;
}
export {Presenter, UiDataCallbackType};
export {Presenter};

View File

@@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class UiData {
constructor(public text: string) {
console.log("new UI data", text);
}
}

View File

@@ -50,5 +50,4 @@ describe("ViewerWindowManagerComponent", () => {
const divInputValue = htmlElement.querySelector(".viewer-window-manager div.input-value");
expect(divInputValue?.innerHTML).toContain("UI Data Value");
});
});

View File

@@ -22,10 +22,11 @@ import {
import {UiData} from "./ui_data";
@Component({
selector: "viewer-window-manager",
template: `
<div class="viewer-window-manager">
<div class="title">Window Manager</div>
<div class="input-value">Input value: {{inputData.text}}</div>
<div class="input-value">Input value: {{inputData?.text}}</div>
<div class="button"><button mat-icon-button (click)="generateOutputEvent($event)">Output event!</button></div>
</div>
`

View File

@@ -27,6 +27,10 @@ class ViewerWindowManager implements Viewer {
this.view.addEventListener("outputEvent", () => this.presenter.notifyUiEvent());
}
public getTitle() {
return "Window Manager";
}
public notifyCurrentTraceEntries(entries: Map<TraceType, any>): void {
this.presenter.notifyCurrentTraceEntries(entries);
}
@@ -35,6 +39,10 @@ class ViewerWindowManager implements Viewer {
return this.view;
}
public getDependencies(): TraceType[] {
return ViewerWindowManager.DEPENDENCIES;
}
public static readonly DEPENDENCIES: TraceType[] = [TraceType.WINDOW_MANAGER];
private view: HTMLElement;
private presenter: Presenter;