(Hierarchy View) Create Viewer for surface flinger traces.

Created the viewer component for SF traces, including rects view,
properties view, hierarchy view (for re-use in other apps). This CL
contains hierarchy view and associated reusable components.

Bug: b/238089034 b/232081297
Test: npm run test:all. upload/run any SF trace to see what it looks
like.

Change-Id: I3dd7c3f73ac7d7dac9b65d5fc7853e9fc8b8e56e
This commit is contained in:
Priyanka Patel
2022-08-15 09:24:48 +00:00
parent 12f8941edb
commit 6e50f90cd7
39 changed files with 2189 additions and 303 deletions

View File

@@ -17,6 +17,7 @@ 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 { MatTooltipModule } from "@angular/material/tooltip";
import { AppComponent } from "./components/app.component";
import { ViewerWindowManagerComponent } from "viewers/viewer_window_manager/viewer_window_manager.component";
@@ -26,11 +27,14 @@ 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 { HierarchyComponent } from "viewers/components/hierarchy.component";
import { PropertiesComponent } from "viewers/components/properties.component";
import { RectsComponent } from "viewers/components/rects/rects.component";
import { TraceViewHeaderComponent } from "./components/trace_view_header.component";
import { TraceViewComponent } from "./components/trace_view.component";
import { TreeComponent } from "viewers/components/tree.component";
import { TreeNodeComponent } from "viewers/components/tree_node.component";
import { TreeElementComponent } from "viewers/components/tree_element.component";
@NgModule({
declarations: [
@@ -46,7 +50,10 @@ import { TraceViewComponent } from "./components/trace_view.component";
PropertiesComponent,
RectsComponent,
TraceViewHeaderComponent,
TraceViewComponent
TraceViewComponent,
TreeComponent,
TreeNodeComponent,
TreeElementComponent
],
imports: [
BrowserModule,
@@ -67,7 +74,8 @@ import { TraceViewComponent } from "./components/trace_view.component";
BrowserAnimationsModule,
HttpClientModule,
MatSliderModule,
MatRadioModule
MatRadioModule,
MatTooltipModule
],
bootstrap: [AppComponent]
})

View File

@@ -30,39 +30,42 @@ import { Viewer } from "viewers/viewer";
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>
<button mat-raised-button *ngIf="dataLoaded" (click)="toggleTimestamp()">Start/End Timestamp</button>
<button class="upload-new-btn" mat-raised-button *ngIf="dataLoaded" (click)="clearData()">Upload New</button>
</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>
<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>
<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>
<div id="timescrub">
<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 id="timestamps">
</div>
`,
styles: [".time-slider {width: 100%}"],
styles: [
".time-slider {width: 100%}",
".upload-new-btn {float: right}"
],
encapsulation: ViewEncapsulation.None
})
export class AppComponent {
@@ -128,7 +131,7 @@ export class AppComponent {
const traceCardContent = traceCard.querySelector(".trace-card-content")!;
const view = viewer.getView();
(view as any).showTrace = (traceView as any).showTrace;
(view as any).store = this.store;
traceCardContent.appendChild(view);
});
}

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, Inject, Input, Output, EventEmitter, OnInit, OnDestroy } from "@angular/core";
import { Component, 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";

View File

@@ -22,6 +22,7 @@ import { Viewer } from "viewers/viewer";
import { ViewerFactory } from "viewers/viewer_factory";
import { LoadedTrace } from "app/loaded_trace";
import { TRACE_INFO } from "./trace_info";
import { TimestampUtils } from "common/trace/timestamp_utils";
class TraceCoordinator {
private parsers: Parser[];
@@ -105,8 +106,17 @@ class TraceCoordinator {
this.parsers.forEach(parser => {
const targetTimestamp = timestamp;
const entry = parser.getTraceEntry(targetTimestamp);
let prevEntry = null;
const parserTimestamps = parser.getTimestamps(timestamp.getType());
if (parserTimestamps) {
const closestIndex = TimestampUtils.getClosestIndex(targetTimestamp, parserTimestamps);
if (closestIndex) {
prevEntry = parser.getTraceEntry(parserTimestamps[closestIndex-1]) ?? null;
}
}
if (entry !== undefined) {
traceEntries.set(parser.getTraceType(), entry);
traceEntries.set(parser.getTraceType(), [entry, prevEntry]);
}
});

View File

@@ -17,7 +17,6 @@
import { Layer, LayerProperties, Rect, toActiveBuffer, toColor, toRect, toRectF, toRegion } from "../common"
import { shortenName } from '../mixin'
import { RELATIVE_Z_CHIP, GPU_CHIP, HWC_CHIP } from '../treeview/Chips'
import Transform from './Transform'
Layer.fromProto = function (proto: any): Layer {
@@ -96,18 +95,6 @@ function addAttributes(entry: Layer, proto: any) {
entry.rect.ref = entry;
entry.rect.label = entry.name;
entry.chips = [];
updateChips(entry);
}
function updateChips(entry: Layer) {
if ((entry.zOrderRelativeOf || -1) !== -1) {
entry.chips.push(RELATIVE_Z_CHIP);
}
if (entry.hwcCompositionType === 'CLIENT') {
entry.chips.push(GPU_CHIP);
} else if (entry.hwcCompositionType === 'DEVICE' || entry.hwcCompositionType === 'SOLID_COLOR') {
entry.chips.push(HWC_CHIP);
}
}
export default Layer;

View File

@@ -16,7 +16,6 @@
import { Display, LayerTraceEntry, LayerTraceEntryBuilder, toRect, toSize, toTransform } from "../common"
import Layer from './Layer'
import { VISIBLE_CHIP, RELATIVE_Z_PARENT_CHIP, MISSING_LAYER } from '../treeview/Chips'
LayerTraceEntry.fromProto = function (protos: any[], displayProtos: any[],
timestamp: number, hwcBlob: string, where: string = ''): LayerTraceEntry {
@@ -25,7 +24,6 @@ LayerTraceEntry.fromProto = function (protos: any[], displayProtos: any[],
const builder = new LayerTraceEntryBuilder(timestamp, layers, displays, hwcBlob, where);
const entry: LayerTraceEntry = builder.build();
updateChildren(entry);
addAttributes(entry, protos);
return entry;
}
@@ -51,20 +49,6 @@ function addAttributes(entry: LayerTraceEntry, protos: any) {
entry.isVisible = true;
}
function updateChildren(entry: LayerTraceEntry) {
entry.flattenedLayers.forEach((it: any) => {
if (it.isVisible) {
it.chips.push(VISIBLE_CHIP);
}
if (it.zOrderRelativeOf) {
it.chips.push(RELATIVE_Z_PARENT_CHIP);
}
if (it.isMissing) {
it.chips.push(MISSING_LAYER);
}
});
}
function newDisplay(proto: any): Display {
return new Display(
proto.id,

View File

@@ -1,29 +0,0 @@
/*
* Copyright 2020, 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 ChipType from "./ChipType"
export default class Chip {
short: String
long: String
type: ChipType
constructor(short: String, long: String, type: ChipType) {
this.short = short
this.long = long
this.type = type
}
}

View File

@@ -1,50 +0,0 @@
/*
* Copyright 2020, 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 Chip from "./Chip"
import ChipType from "./ChipType"
export const VISIBLE_CHIP = new Chip("V", "visible", ChipType.DEFAULT)
export const RELATIVE_Z_CHIP = {
short: 'RelZ',
long: 'Is relative Z-ordered to another surface',
class: 'warn',
};
export const RELATIVE_Z_PARENT_CHIP = {
short: 'RelZParent',
long: 'Something is relative Z-ordered to this surface',
class: 'warn',
};
export const MISSING_LAYER = {
short: 'MissingLayer',
long: 'This layer was referenced from the parent, but not present in the trace',
class: 'error',
};
export const GPU_CHIP = {
short: 'GPU',
long: 'This layer was composed on the GPU',
class: 'gpu',
};
export const HWC_CHIP = {
short: 'HWC',
long: 'This layer was composed by Hardware Composer',
class: 'hwc',
};

View File

@@ -16,7 +16,6 @@
import { shortenName } from '../mixin'
import { Activity } from "../common"
import { VISIBLE_CHIP } from '../treeview/Chips'
import WindowContainer from "./WindowContainer"
Activity.fromProto = function (proto: any): Activity {
@@ -50,7 +49,7 @@ function addAttributes(entry: Activity, proto: any) {
entry.proto = proto;
entry.kind = entry.constructor.name;
entry.shortName = shortenName(entry.name);
entry.chips = entry.isVisible ? [VISIBLE_CHIP] : [];
entry.chips = [];
}
export default Activity;

View File

@@ -16,7 +16,6 @@
import { shortenName } from '../mixin'
import { toRect, Size, WindowState, WindowLayoutParams } from "../common"
import { VISIBLE_CHIP } from '../treeview/Chips'
import WindowContainer from "./WindowContainer"
WindowState.fromProto = function (proto: any, isActivityInTree: Boolean): WindowState {
@@ -131,7 +130,7 @@ function addAttributes(entry: WindowState, proto: any) {
entry.rect.label = entry.name;
entry.proto = proto;
entry.shortName = shortenName(entry.name);
entry.chips = entry.isVisible ? [VISIBLE_CHIP] : [];
entry.chips = [];
}
export default WindowState

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 { ArrayUtils } from "common/utils/array_utils";
import { Timestamp } from "common/trace/timestamp";
export class TimestampUtils {
static getClosestIndex(targetTimestamp: Timestamp, timestamps: Timestamp[]) {
if (timestamps === undefined) {
throw TypeError(`Timestamps with type "${targetTimestamp.getType()}" not available`);
}
return ArrayUtils.binarySearchLowerOrEqual(timestamps, targetTimestamp);
}
}

View File

@@ -29,7 +29,7 @@ describe("Viewer SurfaceFlinger", () => {
const loadData = element(by.css(".load-btn"));
loadData.click();
const surfaceFlingerCard: ElementFinder = element(by.css(".trace-card"));
const surfaceFlingerCard: ElementFinder = element(by.css(".trace-card-title-text"));
expect(surfaceFlingerCard.getText()).toContain("Surface Flinger");
});
});

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2020, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export default class Chip {
short: string;
long: string;
type: string;
constructor(short: string, long: string, type: string) {
this.short = short;
this.long = long;
this.type = type;
}
}
export const VISIBLE_CHIP = new Chip("V", "visible", "default");
export const RELATIVE_Z_CHIP = new Chip(
"RelZ",
"Is relative Z-ordered to another surface",
"warn",
);
export const RELATIVE_Z_PARENT_CHIP = new Chip(
"RelZParent",
"Something is relative Z-ordered to this surface",
"warn",
);
export const MISSING_LAYER = new Chip(
"MissingLayer",
"This layer was referenced from the parent, but not present in the trace",
"error",
);
export const GPU_CHIP = new Chip(
"GPU",
"This layer was composed on the GPU",
"gpu",
);
export const HWC_CHIP = new Chip(
"HWC",
"This layer was composed by Hardware Composer",
"hwc",
);

View File

@@ -0,0 +1,252 @@
/*
* 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 { RELATIVE_Z_CHIP } from "viewers/common/chip";
import { getFilter, TreeGenerator } from "viewers/common/tree_utils";
describe("TreeGenerator", () => {
it("generates tree", () => {
const tree = {
kind: "entry",
name: "BaseLayerTraceEntry",
shortName: "BLTE",
id: 0,
chips: [],
children: [{
kind: "3",
id: "3",
name: "Child1",
children: [
{
kind: "2",
id: "2",
name: "Child2",
children: []
}
]}]
};
const expected = {
simplifyNames: false,
name: "BaseLayerTraceEntry",
id: 0,
children: [
{
id: "3",
name: "Child1",
children: [{
kind: "2",
id: "2",
name: "Child2",
children: [],
simplifyNames: false,
showInFilteredView: true,
stableId: undefined,
shortName: undefined,
chips: [ RELATIVE_Z_CHIP ]
}],
kind: "3",
simplifyNames: false,
showInFilteredView: true,
stableId: undefined,
shortName: undefined,
chips: [ RELATIVE_Z_CHIP ],
}
],
kind: "entry",
stableId: undefined,
shortName: "BLTE",
chips: [],
showInFilteredView: true,
};
const userOptions = {};
const filter = getFilter("");
const generator = new TreeGenerator(tree, userOptions, filter);
expect(generator.generateTree()).toEqual(expected);
});
it("generates diff tree with no diff", () => {
const tree = {
kind: "entry",
name: "BaseLayerTraceEntry",
shortName: "BLTE",
stableId: "0",
chips: [],
id: 0,
children: [{
kind: "3",
id: "3",
stableId: "3",
name: "Child1",
children: [
{
kind: "2",
id: "2",
stableId: "2",
name: "Child2",
}
]}]
};
const newTree = tree;
const expected = {
simplifyNames: false,
name: "BaseLayerTraceEntry",
id: 0,
stableId: "0",
children: [
{
id: "3",
stableId: "3",
name: "Child1",
children: [{
kind: "2",
id: "2",
name: "Child2",
children: [],
simplifyNames: false,
showInFilteredView: true,
stableId: "2",
shortName: undefined,
diff: "none",
chips: [ RELATIVE_Z_CHIP ]
}],
kind: "3",
shortName: undefined,
simplifyNames: false,
showInFilteredView: true,
diff: "none",
chips: [ RELATIVE_Z_CHIP ]
}
],
kind: "entry",
shortName: "BLTE",
chips: [],
showInFilteredView: true,
diff: "none"
};
const userOptions = {};
const filter = getFilter("");
const generator = new TreeGenerator(tree, userOptions, filter);
expect(generator.withUniqueNodeId((node: any) => {
if (node) return node.stableId;
else return null;
}).compareWith(newTree).generateFinalDiffTree()).toEqual(expected);
});
it("generates diff tree with moved node", () => {
const tree = {
kind: "entry",
name: "BaseLayerTraceEntry",
shortName: "BLTE",
stableId: "0",
chips: [],
id: 0,
children: [{
kind: "3",
id: "3",
stableId: "3",
name: "Child1",
children: [
{
kind: "2",
id: "2",
stableId: "2",
name: "Child2",
}
]}]
};
const newTree = {
kind: "entry",
name: "BaseLayerTraceEntry",
shortName: "BLTE",
stableId: "0",
chips: [],
id: 0,
children: [
{
kind: "3",
id: "3",
stableId: "3",
name: "Child1",
children: []
},
{
kind: "2",
id: "2",
stableId: "2",
name: "Child2",
}
]
};
const expected = {
simplifyNames: false,
name: "BaseLayerTraceEntry",
id: 0,
stableId: "0",
children: [
{
id: "3",
stableId: "3",
name: "Child1",
diff: "none",
children: [ {
kind: "2",
id: "2",
name: "Child2",
diff: "addedMove",
children: [],
simplifyNames: false,
showInFilteredView: true,
stableId: "2",
shortName: undefined,
chips: [ RELATIVE_Z_CHIP ]
}],
kind: "3",
shortName: undefined,
simplifyNames: false,
showInFilteredView: true,
chips: [ RELATIVE_Z_CHIP ]
},
{
kind: "2",
id: "2",
name: "Child2",
diff: "deletedMove",
children: [],
simplifyNames: false,
showInFilteredView: true,
stableId: "2",
shortName: undefined,
chips: [ RELATIVE_Z_CHIP ]
}
],
kind: "entry",
shortName: "BLTE",
chips: [],
showInFilteredView: true,
diff: "none"
};
const userOptions = {};
const filter = getFilter("");
const generator = new TreeGenerator(tree, userOptions, filter);
const newDiffTree = generator.withUniqueNodeId((node: any) => {
if (node) return node.stableId;
else return null;
}).compareWith(newTree).generateFinalDiffTree();
expect(newDiffTree).toEqual(expected);
});
});

View File

@@ -0,0 +1,449 @@
/*
* 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 { Layer, BaseLayerTraceEntry } from "common/trace/flickerlib/common";
import ObjectFormatter from "common/trace/flickerlib/ObjectFormatter";
import { UserOptions } from "viewers/common/user_options";
import { HWC_CHIP, GPU_CHIP, MISSING_LAYER, VISIBLE_CHIP, RELATIVE_Z_CHIP, RELATIVE_Z_PARENT_CHIP } from "viewers/common/chip";
type GetNodeIdCallbackType = (node: Tree | null) => number | null;
type IsModifiedCallbackType = (newTree: Tree | null, oldTree: Tree | null) => boolean;
interface IdNodeMap { [key: string]: Tree }
const DiffType = {
NONE: "none",
ADDED: "added",
DELETED: "deleted",
ADDED_MOVE: "addedMove",
DELETED_MOVE: "deletedMove",
MODIFIED: "modified",
};
const HwcCompositionType = {
CLIENT: 1,
DEVICE: 2,
SOLID_COLOR: 3,
};
export type FilterType = (item: Tree | null) => boolean;
export type Tree = Layer | BaseLayerTraceEntry;
export function diffClass(item: Tree): string {
const diff = item!.diff;
return diff ?? "";
}
export function isHighlighted(item: Tree, highlightedItems: Array<string>) {
return highlightedItems.includes(`${item.id}`);
}
export function getFilter(filterString: string): FilterType {
const filterStrings = filterString.split(",");
const positive: Tree | null[] = [];
const negative: Tree | null[] = [];
filterStrings.forEach((f) => {
f = f.trim();
if (f.startsWith("!")) {
const regex = new RegExp(f.substring(1), "i");
negative.push((s:any) => !regex.test(s));
} else {
const regex = new RegExp(f, "i");
positive.push((s:any) => regex.test(s));
}
});
const filter = (item:Tree | null) => {
if (item) {
const apply = (f:any) => f(`${item.name}`);
return (positive.length === 0 || positive.some(apply)) &&
(negative.length === 0 || negative.every(apply));
}
return false;
};
return filter;
}
export class TreeGenerator {
private userOptions: UserOptions;
private filter: FilterType;
private tree: Tree;
private diffWithTree: Tree | null = null;
private getNodeId?: GetNodeIdCallbackType;
private isModified?: IsModifiedCallbackType;
private newMapping: IdNodeMap | null = null;
private oldMapping: IdNodeMap | null = null;
private readonly pinnedIds: Array<string>;
private pinnedItems: Array<Tree> = [];
private relZParentIds: Array<number> = [];
private flattenedChildren: Array<Tree> = [];
constructor(tree: Tree, userOptions: UserOptions, filter: FilterType, pinnedIds?: Array<string>) {
this.tree = tree;
this.userOptions = userOptions;
this.filter = filter;
this.pinnedIds = pinnedIds ?? [];
}
public generateTree(): Tree {
return this.getCustomisedTree(this.tree);
}
public compareWith(tree: Tree | null): TreeGenerator {
this.diffWithTree = tree;
return this;
}
public withUniqueNodeId(getNodeId?: GetNodeIdCallbackType): TreeGenerator {
this.getNodeId = (node: Tree | null) => {
const id = getNodeId ? getNodeId(node) : this.defaultNodeIdCallback(node);
if (id === null || id === undefined) {
console.error("Null node ID for node", node);
throw new Error("Node ID can't be null or undefined");
}
return id;
};
return this;
}
public withModifiedCheck(isModified?: IsModifiedCallbackType): TreeGenerator {
this.isModified = isModified ?? this.defaultModifiedCheck;
return this;
}
public generateFinalDiffTree(): Tree {
this.newMapping = this.generateIdToNodeMapping(this.tree);
this.oldMapping = this.diffWithTree ? this.generateIdToNodeMapping(this.diffWithTree) : null;
const diffTrees = this.generateDiffTree(this.tree, this.diffWithTree, [], []);
let diffTree;
if (diffTrees.length > 1) {
diffTree = {
kind: "",
name: "DiffTree",
children: diffTrees,
stableId: "DiffTree",
};
} else {
diffTree = diffTrees[0];
}
return this.getCustomisedTree(diffTree);
}
private getCustomisedTree(tree: Tree | null) {
if (!tree) return null;
tree = this.generateTreeWithUserOptions(tree, false);
tree = this.updateTreeWithRelZParentChips(tree);
if (this.isFlatView() && tree.children) {
this.flattenChildren(tree.children);
tree.children = this.flattenedChildren;
}
return Object.freeze(tree);
}
public getPinnedItems() {
return this.pinnedItems;
}
private flattenChildren(children: Array<Tree>): Tree {
for (let i = 0; i < children.length; i++) {
const child = children[i];
const showInOnlyVisibleView = this.isOnlyVisibleView() && child.isVisible;
const passVisibleCheck = !this.isOnlyVisibleView() || showInOnlyVisibleView;
if (this.filterMatches(child) && passVisibleCheck) {
this.flattenedChildren.push(child);
}
if (child.children) {
this.flattenChildren(child.children);
}
}
}
private isOnlyVisibleView(): boolean {
return this.userOptions["onlyVisible"]?.enabled ?? false;
}
private isSimplifyNames(): boolean {
return this.userOptions["simplifyNames"]?.enabled ?? false;
}
private isFlatView(): boolean {
return this.userOptions["flat"]?.enabled ?? false;
}
private filterMatches(item: Tree | null): boolean {
return this.filter(item) ?? false;
}
private generateTreeWithUserOptions(tree: Tree | null, parentFilterMatch: boolean): Tree | null {
return tree ? this.applyChecks(tree, this.cloneNode(tree), parentFilterMatch) : null;
}
private updateTreeWithRelZParentChips(tree: Tree): Tree {
return this.applyRelZParentCheck(tree);
}
private applyRelZParentCheck(tree: Tree) {
if (this.relZParentIds.includes(tree.id)) {
tree.chips.push(RELATIVE_Z_PARENT_CHIP);
}
const numOfChildren = tree.children?.length ?? 0;
for (let i = 0; i < numOfChildren; i++) {
tree.children[i] = this.updateTreeWithRelZParentChips(tree.children[i]);
}
return tree;
}
private addChips(tree: Tree) {
tree.chips = [];
if (tree.hwcCompositionType == HwcCompositionType.CLIENT) {
tree.chips.push(GPU_CHIP);
} else if ((tree.hwcCompositionType == HwcCompositionType.DEVICE || tree.hwcCompositionType == HwcCompositionType.SOLID_COLOR)) {
tree.chips.push(HWC_CHIP);
}
if (tree.isVisible && tree.kind !== "entry") {
tree.chips.push(VISIBLE_CHIP);
}
if (tree.zOrderRelativeOfId !== -1 && tree.kind !== "entry" && !tree.isRootLayer) {
tree.chips.push(RELATIVE_Z_CHIP);
this.relZParentIds.push(tree.zOrderRelativeOfId);
}
if (tree.isMissing) {
tree.chips.push(MISSING_LAYER);
}
return tree;
}
private applyChecks(tree: Tree | null, newTree: Tree | null, parentFilterMatch: boolean): Tree | null {
if (!tree || !newTree) {
return null;
}
// simplify names check
newTree.simplifyNames = this.isSimplifyNames();
// check item either matches filter, or has parents/children matching filter
if (tree.kind === "entry" || parentFilterMatch) {
newTree.showInFilteredView = true;
} else {
newTree.showInFilteredView = this.filterMatches(tree);
parentFilterMatch = newTree.showInFilteredView;
}
if (this.isOnlyVisibleView()) {
newTree.showInOnlyVisibleView = newTree.isVisible;
}
newTree.children = [];
const numOfChildren = tree.children?.length ?? 0;
for (let i = 0; i < numOfChildren; i++) {
const child = tree.children[i];
const newTreeChild = this.generateTreeWithUserOptions(child, parentFilterMatch);
if (newTreeChild) {
if (newTreeChild.showInFilteredView) {
newTree.showInFilteredView = true;
}
if (this.isOnlyVisibleView() && newTreeChild.showInOnlyVisibleView) {
newTree.showInOnlyVisibleView = true;
}
newTree.children.push(newTreeChild);
}
}
const doNotShowInOnlyVisibleView = this.isOnlyVisibleView() && !newTree.showInOnlyVisibleView;
if (!newTree.showInFilteredView || doNotShowInOnlyVisibleView) {
return null;
}
newTree = this.addChips(newTree);
if (this.pinnedIds.includes(`${newTree.id}`)) {
this.pinnedItems.push(newTree);
}
return newTree;
}
private generateIdToNodeMapping(node: Tree, acc?: IdNodeMap): IdNodeMap {
acc = acc || {};
const nodeId = this.getNodeId!(node)!;
if (acc[nodeId]) {
throw new Error(`Duplicate node id '${nodeId}' detected...`);
}
acc[nodeId] = node;
if (node.children) {
for (const child of node.children) {
this.generateIdToNodeMapping(child, acc);
}
}
return acc;
}
private cloneNode(node: Tree | null): Tree | null {
const clone = ObjectFormatter.cloneObject(node);
if (node) {
clone.children = node.children;
clone.name = node.name;
clone.kind = node.kind;
clone.stableId = node.stableId;
clone.shortName = node.shortName;
if ("chips" in node) {
clone.chips = node.chips.slice();
}
if ("diff" in node) {
clone.diff = node.diff;
}
}
return clone;
}
private generateDiffTree(
newTree: Tree | null,
oldTree: Tree | null,
newTreeSiblings: Array<Tree | null>,
oldTreeSiblings: Array<Tree | null>
): Array<Tree | null> {
const diffTrees = [];
// NOTE: A null ID represents a non existent node.
if (!this.getNodeId) {
return [];
}
const newId = newTree ? this.getNodeId(newTree) : null;
const oldId = oldTree ? this.getNodeId(oldTree) : null;
const newTreeSiblingIds = newTreeSiblings.map(this.getNodeId);
const oldTreeSiblingIds = oldTreeSiblings.map(this.getNodeId);
if (newTree) {
// Clone is required because trees are frozen objects — we can't modify the original tree object.
const diffTree = this.cloneNode(newTree)!;
// Default to no changes
diffTree.diff = DiffType.NONE;
if (newTree.kind !== "entry" && newId !== oldId) {
// A move, addition, or deletion has occurred
let nextOldTree = null;
// Check if newTree has been added or moved
if (newId && !oldTreeSiblingIds.includes(newId)) {
if (this.oldMapping && this.oldMapping[newId]) {
// Objected existed in old tree, so DELETED_MOVE will be/has been flagged and added to the
// diffTree when visiting it in the oldTree.
diffTree.diff = DiffType.ADDED_MOVE;
// Switch out oldTree for new one to compare against
nextOldTree = this.oldMapping[newId];
} else {
diffTree.diff = DiffType.ADDED;
// Stop comparing against oldTree
nextOldTree = null;
}
}
// Check if oldTree has been deleted of moved
if (oldId && oldTree && !newTreeSiblingIds.includes(oldId)) {
const deletedTreeDiff = this.cloneNode(oldTree)!;
if (this.newMapping![oldId]) {
deletedTreeDiff.diff = DiffType.DELETED_MOVE;
// Stop comparing against oldTree, will be/has been
// visited when object is seen in newTree
nextOldTree = null;
} else {
deletedTreeDiff.diff = DiffType.DELETED;
// Visit all children to check if they have been deleted or moved
deletedTreeDiff.children = this.visitChildren(null, oldTree);
}
diffTrees.push(deletedTreeDiff);
}
oldTree = nextOldTree;
} else {
if (this.isModified && this.isModified(newTree, oldTree)) {
diffTree.diff = DiffType.MODIFIED;
}
}
diffTree.children = this.visitChildren(newTree, oldTree);
diffTrees.push(diffTree);
} else if (oldTree) {
if (oldId && !newTreeSiblingIds.includes(oldId)) {
// Deep clone oldTree omitting children field
const diffTree = this.cloneNode(oldTree)!;
// newTree doesn't exist, oldTree has either been moved or deleted.
if (this.newMapping![oldId]) {
diffTree.diff = DiffType.DELETED_MOVE;
} else {
diffTree.diff = DiffType.DELETED;
}
diffTree.children = this.visitChildren(null, oldTree);
diffTrees.push(diffTree);
}
} else {
throw new Error("Both newTree and oldTree are undefined...");
}
return diffTrees;
}
private visitChildren(newTree: Tree | null, oldTree: Tree | null) {
// Recursively traverse all children of new and old tree.
const diffChildren = [];
if (!newTree) newTree = {};
if (!oldTree) oldTree = {};
const numOfChildren = Math.max(newTree.children?.length ?? 0, oldTree.children?.length ?? 0);
for (let i = 0; i < numOfChildren; i++) {
const newChild = newTree.children ? newTree.children[i] : null;
const oldChild = oldTree.children ? oldTree.children[i] : null;
const childDiffTrees = this.generateDiffTree(
newChild, oldChild,
newTree.children ?? [], oldTree.children ?? [],
);
diffChildren.push(...childDiffTrees);
}
return diffChildren;
}
private defaultNodeIdCallback(node: Tree | null): number | null {
return node ? node.stableId : null;
}
private defaultModifiedCheck(newNode: Tree | null, oldNode: Tree | null): boolean {
if (!newNode && !oldNode) {
return false;
} else if (newNode && newNode.kind==="entry") {
return false;
} else if ((newNode && !oldNode) || (!newNode && oldNode)) {
return true;
}
return !newNode.equals(oldNode);
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2020, The Android Open Source Project
* 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
* 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,
@@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
enum ChipType {
DEFAULT = 'default'
}
export default ChipType
export interface UserOptions {
[key: string]: {
name: string,
enabled: boolean
}
}

View File

@@ -13,17 +13,9 @@
* 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 {
}
export const ViewerEvents = {
HierarchyPinnedChange: "HierarchyPinnedChange",
HighlightedChange: "HighlightedChange",
HierarchyUserOptionsChange: "HierarchyUserOptionsChange",
HierarchyFilterChange: "HierarchyFilterChange"
};

View File

@@ -0,0 +1,102 @@
/*
* 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, ComponentFixtureAutoDetect} from "@angular/core/testing";
import { HierarchyComponent } from "./hierarchy.component";
import { NO_ERRORS_SCHEMA } from "@angular/core";
import { PersistentStore } from "common/persistent_store";
import { CommonModule } from "@angular/common";
import { MatInputModule } from "@angular/material/input";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatCheckboxModule } from "@angular/material/checkbox";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
describe("HierarchyComponent", () => {
let fixture: ComponentFixture<HierarchyComponent>;
let component: HierarchyComponent;
let htmlElement: HTMLElement;
beforeAll(async () => {
await TestBed.configureTestingModule({
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true }
],
declarations: [
HierarchyComponent
],
imports: [
CommonModule,
MatInputModule,
MatFormFieldModule,
MatCheckboxModule,
BrowserAnimationsModule
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(HierarchyComponent);
component = fixture.componentInstance;
htmlElement = fixture.nativeElement;
component.tree = {
simplifyNames: false,
kind: "entry",
name: "BaseLayerTraceEntry",
shortName: "BLTE",
chips: [],
children: [{kind: "3", id: "3", name: "Child1"}]
};
component.store = new PersistentStore();
component.userOptions = {
onlyVisible: {
name: "Only visible",
enabled: false
},
};
component.pinnedItems = [{
simplifyNames: false,
kind: "entry",
name: "BaseLayerTraceEntry",
shortName: "BLTE",
chips: [],
children: [{kind: "3", id: "3", name: "Child1"}]
}];
component.diffClass = jasmine.createSpy().and.returnValue("none");
});
it("can be created", () => {
fixture.detectChanges();
expect(component).toBeTruthy();
});
it("creates title", () => {
fixture.detectChanges();
const title = htmlElement.querySelector(".hierarchy-title");
expect(title).toBeTruthy();
});
it("creates view controls", () => {
fixture.detectChanges();
const viewControls = htmlElement.querySelector(".view-controls");
expect(viewControls).toBeTruthy();
});
it("creates initial tree elements", () => {
fixture.detectChanges();
const tree = htmlElement.querySelector(".tree-wrapper");
expect(tree).toBeTruthy();
});
});

View File

@@ -0,0 +1,218 @@
/*
* 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, Inject, ElementRef } from "@angular/core";
import { UserOptions } from "viewers/common/user_options";
import { PersistentStore } from "common/persistent_store";
import { Tree, diffClass, isHighlighted } from "viewers/common/tree_utils";
import { nodeStyles } from "viewers/styles/node.styles";
import { ViewerEvents } from "viewers/common/viewer_events";
import { TraceType } from "common/trace/trace_type";
@Component({
selector: "hierarchy-view",
template: `
<mat-card-header class="view-header">
<mat-card-title class="title-filter">
<span class="hierarchy-title">Hierarchy</span>
<mat-form-field class="filter-field">
<mat-label>Filter...</mat-label>
<input
matInput
[(ngModel)]="filterString"
(ngModelChange)="filterTree()"
name="filter"
/>
</mat-form-field>
</mat-card-title>
<div class="view-controls">
<mat-checkbox
*ngFor="let option of objectKeys(userOptions)"
class="trace-box"
[(ngModel)]="userOptions[option].enabled"
(ngModelChange)="updateTree()"
>{{userOptions[option].name}}</mat-checkbox>
</div>
<div class="pinned-items" *ngIf="pinnedItems.length > 0">
<tree-node
*ngFor="let pinnedItem of pinnedItems"
class="node"
[class]="diffClass(pinnedItem)"
[class.selected]="isHighlighted(pinnedItem, highlightedItems)"
[class.clickable]="true"
[item]="pinnedItem"
[isPinned]="true"
[isInPinnedSection]="true"
(pinNodeChange)="pinnedItemChange($event)"
(click)="onPinnedNodeClick($event, pinnedItem.id)"
></tree-node>
</div>
</mat-card-header>
<mat-card-content class="hierarchy-content" [style]="maxHierarchyHeight()">
<div class="tree-wrapper">
<tree-view
class="tree-view"
*ngIf="tree"
[isFlattened]="isFlattened()"
[isShaded]="true"
[item]="tree"
[dependencies]="dependencies"
[store]="store"
[useGlobalCollapsedState]="true"
[itemsClickable]="true"
[highlightedItems]="highlightedItems"
[pinnedItems]="pinnedItems"
(highlightedItemChange)="highlightedItemChange($event)"
(pinnedItemChange)="pinnedItemChange($event)"
></tree-view>
</div>
</mat-card-content>
`,
styles: [
`
.view-header {
display: block;
width: 100%;
min-height: 3.75rem;
align-items: center;
border-bottom: 1px solid lightgrey;
}
.title-filter {
position: relative;
display: flex;
align-items: center;
width: 100%;
}
.hierarchy-title {
font-size: 16px;
}
.filter-field {
font-size: 16px;
transform: scale(0.7);
right: 0px;
position: absolute
}
.view-controls {
display: inline-block;
font-size: 12px;
font-weight: normal;
margin-left: 5px
}
.hierarchy-content{
display: flex;
flex-direction: column;
overflow-y: auto;
overflow-x:hidden
}
.tree-view {
white-space: pre-line;
flex: 1 0 0;
height: 100%;
overflow-y: auto
}
.pinned-items {
border: 2px solid yellow;
position: relative;
display: block;
width: 100%;
}
`,
nodeStyles
],
})
export class HierarchyComponent {
objectKeys = Object.keys;
filterString = "";
diffClass = diffClass;
isHighlighted = isHighlighted;
@Input() tree!: Tree | null;
@Input() dependencies: Array<TraceType> = [];
@Input() highlightedItems: Array<string> = [];
@Input() pinnedItems: Array<Tree> = [];
@Input() store!: PersistentStore;
@Input() userOptions: UserOptions = {};
constructor(
@Inject(ElementRef) private elementRef: ElementRef,
) {}
isFlattened() {
return this.userOptions["flat"]?.enabled;
}
maxHierarchyHeight() {
const headerHeight = this.elementRef.nativeElement.querySelector(".view-header").clientHeight;
return {
height: `${800 - headerHeight}px`
};
}
onPinnedNodeClick(event: MouseEvent, pinnedItemId: string) {
event.preventDefault();
if (window.getSelection()?.type === "range") {
return;
}
this.highlightedItemChange(`${pinnedItemId}`);
}
updateTree() {
const event: CustomEvent = new CustomEvent(
ViewerEvents.HierarchyUserOptionsChange,
{
bubbles: true,
detail: { userOptions: this.userOptions }
});
this.elementRef.nativeElement.dispatchEvent(event);
}
filterTree() {
const event: CustomEvent = new CustomEvent(
ViewerEvents.HierarchyFilterChange,
{
bubbles: true,
detail: { filterString: this.filterString }
});
this.elementRef.nativeElement.dispatchEvent(event);
}
highlightedItemChange(newId: string) {
const event: CustomEvent = new CustomEvent(
ViewerEvents.HighlightedChange,
{
bubbles: true,
detail: { id: newId }
});
this.elementRef.nativeElement.dispatchEvent(event);
}
pinnedItemChange(item: Tree) {
const event: CustomEvent = new CustomEvent(
ViewerEvents.HierarchyPinnedChange,
{
bubbles: true,
detail: { pinnedItem: item }
});
this.elementRef.nativeElement.dispatchEvent(event);
}
}

View File

@@ -41,8 +41,7 @@ export class CanvasGraphics {
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.position.set(this.xCameraPos, Math.abs(this.xCameraPos), 6);
this.camera.lookAt(0, 0, 0);
this.camera.zoom = this.camZoom;
this.camera.updateProjectionMatrix();
@@ -142,17 +141,17 @@ export class CanvasGraphics {
) {
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) {
const mustNotDrawInVisibleView = this.visibleView && !rect.isVisible;
const mustNotDrawInXrayViewWithoutVirtualDisplays = !this.visibleView && !this.showVirtualDisplays && rect.isDisplay && rect.isVirtual;
if (mustNotDrawInVisibleView || mustNotDrawInXrayViewWithoutVirtualDisplays) {
rectCounter++;
return;
}
//set colour mapping
let planeColor;
if (this.highlighted === `${rect.id}`) {
planeColor = this.colorMapping("highlight", numberOfRects, 0);
if (this.highlightedItems.includes(`${rect.id}`)) {
planeColor = this.colorMapping("highlighted", numberOfRects, 0);
} else if (rect.isVisible) {
planeColor = this.colorMapping("green", visibleRects, visibleDarkFactor);
visibleDarkFactor++;
@@ -178,7 +177,9 @@ export class CanvasGraphics {
// label circular marker
const circle = this.setCircleMaterial(planeRect, rect);
scene.add(circle);
this.targetObjects.push(planeRect);
// if not a display rect, should be clickable
if (!rect.isDisplay) this.targetObjects.push(planeRect);
// label line
const [line, rectLabel] = this.createLabel(rect, circle, lowestY, rectCounter);
@@ -252,7 +253,7 @@ export class CanvasGraphics {
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);
endPos = new THREE.Vector3(cornerPos.x - 0.75, cornerPos.y - 0.75*this.labelShift, cornerPos.z);
} else {
endPos = cornerPos;
}
@@ -313,8 +314,8 @@ export class CanvasGraphics {
return this.visibleView;
}
getXyCameraPos() {
return this.xyCameraPos;
getXCameraPos() {
return this.xCameraPos;
}
getShowVirtualDisplays() {
@@ -326,14 +327,14 @@ export class CanvasGraphics {
}
updateRotation(userInput: number) {
this.xyCameraPos = userInput;
this.xCameraPos = userInput;
this.camZoom = userInput/4 * 0.2 + 0.9;
this.labelShift = userInput/4 * this.maxLabelShift;
this.lowestYShift = userInput/4 + 2;
this.lowestYShift = Math.abs(userInput)/4 + 2;
}
updateHighlighted(highlighted: string) {
this.highlighted = highlighted;
updateHighlightedItems(newItems: Array<string>) {
this.highlightedItems = newItems;
}
updateRects(rects: Rectangle[]) {
@@ -358,14 +359,16 @@ export class CanvasGraphics {
updateZoom(isZoomIn: boolean) {
if (isZoomIn && this.camZoom < 2) {
this.labelXFactor -= 0.001;
this.camZoom += this.camZoomFactor * 1.5;
} else if (!isZoomIn && this.camZoom > 0.5) {
this.labelXFactor += 0.001;
this.camZoom -= this.camZoomFactor * 1.5;
}
}
colorMapping(scale: string, numberOfRects: number, darkFactor:number): THREE.Color {
if (scale === "highlight") {
if (scale === "highlighted") {
return new THREE.Color(0xD2E3FC);
} else if (scale === "grey") {
// darkness of grey rect depends on z order - darkest 64, lightest 128
@@ -387,8 +390,8 @@ export class CanvasGraphics {
}
shortenText(text: string): string {
if (text.length > 40) {
text = text.slice(0, 40);
if (text.length > 35) {
text = text.slice(0, 35);
}
return text;
}
@@ -397,17 +400,17 @@ export class CanvasGraphics {
readonly cameraHalfWidth = 2.8;
readonly cameraHalfHeight = 3.2;
private readonly maxLabelShift = 0.305;
private readonly labelXFactor = 0.008;
private labelXFactor = 0.009;
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 xCameraPos = 4;
private highlightedItems: Array<string> = [];
private camera: THREE.OrthographicCamera;
private rects: Rectangle[] = [];
private labelElements: HTMLElement[] = [];

View File

@@ -14,15 +14,15 @@
* limitations under the License.
*/
import { CommonModule } from "@angular/common";
import { Component , ViewChild } from "@angular/core";
import { Component, ViewChild } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { RectsComponent } from "./rects.component";
import { RectsComponent } from "viewers/components/rects/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";
import { Rectangle } from "viewers/viewer_surface_flinger/ui_data";
describe("RectsComponent", () => {
let component: TestHostComponent;
@@ -61,7 +61,7 @@ describe("RectsComponent", () => {
});
it("check that layer separation slider causes view to change", () => {
const slider = htmlElement.querySelector("mat-slider");
const slider = htmlElement.querySelector(".spacing-slider");
spyOn(component.rectsComponent.canvasGraphics, "updateLayerSeparation");
slider?.dispatchEvent(new MouseEvent("mousedown"));
fixture.detectChanges();
@@ -97,6 +97,7 @@ describe("RectsComponent", () => {
ref: null,
id: 12345,
displayId: 0,
isVirtual: false
}
]);
spyOn(component.rectsComponent, "drawRects").and.callThrough();

View File

@@ -14,45 +14,30 @@
* limitations under the License.
*/
import { Component, Input, OnChanges, OnDestroy, Inject, ElementRef, SimpleChanges } from "@angular/core";
import { RectsUtils } from "./rects_utils";
import { RectsUtils } from "viewers/components/rects/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 { CanvasGraphics } from "viewers/components/rects/canvas_graphics";
import * as THREE from "three";
import { ViewerEvents } from "viewers/common/viewer_events";
@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-card-title>Layers</mat-card-title>
<div class="top-view-controls">
<mat-checkbox
[disabled]="visibleView()"
class="rects-checkbox"
[checked]="visibleView()"
(change)="onChangeView($event.checked!)"
>Only visible layers</mat-checkbox>
<mat-checkbox
[disabled]="!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">
>Show virtual</mat-checkbox>
<div class="zoom-container">
<button id="zoom-btn" (click)="canvasGraphics.updateZoom(true)">
<mat-icon aria-hidden="true">
@@ -65,6 +50,35 @@ import * as THREE from "three";
</mat-icon>
</button>
</div>
</div>
<div class="slider-view-controls">
<div class="slider" [class.rotation]="true">
<span>Flat to isometric</span>
<mat-slider
step="0.01"
min="0"
max="4"
aria-label="units"
[value]="xCameraPos()"
(input)="canvasGraphics.updateRotation($event.value!)"
></mat-slider>
</div>
<div class="slider" [class.spacing]="true">
<span>Layer spacing</span>
<mat-slider
class="spacing-slider"
step="0.001"
min="0.1"
max="0.4"
aria-label="units"
[value]="getLayerSeparation()"
(input)="canvasGraphics.updateLayerSeparation($event.value!)"
></mat-slider>
</div>
</div>
</mat-card-header>
<mat-card-content class="rects-content">
<div class="canvas-container">
<canvas id="rects-canvas" (click)="onRectClick($event)">
</canvas>
</div>
@@ -79,9 +93,16 @@ import * as THREE from "three";
".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}",
".view-controls, .slider-view-controls {display: inline-block; position: relative; min-height: 4.5rem; width: 100%}",
".slider {display: inline-block}",
".slider.spacing {float: right}",
".slider span, .slider mat-slider { display: block; padding-left: 0px; padding-top: 0px; font-weight: bold}",
".top-view-controls {min-height: 1.5rem; width: 100%; position: relative; display: inline-block; vertical-align: middle;}",
".zoom-container {position: relative; vertical-align: middle; float: right}",
"#zoom-btn {position:relative; display: inline-flex; background: none; border: none}",
"mat-card-title {font-size: 16px !important}",
":host /deep/ .mat-card-header-text {width: 100%; margin: 0;}",
"mat-radio-group {vertical-align: middle}",
"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}",
@@ -96,7 +117,7 @@ import * as THREE from "three";
export class RectsComponent implements OnChanges, OnDestroy {
@Input() rects!: Rectangle[];
@Input() displayIds: Array<number> = [];
@Input() highlighted = "";
@Input() highlightedItems: Array<string> = [];
constructor(
@Inject(ElementRef) private elementRef: ElementRef,
@@ -112,6 +133,9 @@ export class RectsComponent implements OnChanges, OnDestroy {
}
ngOnChanges(changes: SimpleChanges) {
if (changes["highlightedItems"]) {
this.canvasGraphics.updateHighlightedItems(this.highlightedItems);
}
if (this.rects.length > 0) {
//change in rects so they must undergo transformation and scaling before canvas refreshed
this.canvasGraphics.clearLabelElements();
@@ -126,13 +150,15 @@ export class RectsComponent implements OnChanges, OnDestroy {
}
});
this.scaleRects();
this.drawRects();
if (changes["rects"]) {
this.drawRects();
}
} else if (this.canvasSubscription) {
this.canvasSubscription.unsubscribe();
}
}
onRectClick(event:PointerEvent) {
onRectClick(event:MouseEvent) {
this.setNormalisedMousePos(event);
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(this.mouse, this.canvasGraphics.getCamera());
@@ -140,18 +166,12 @@ export class RectsComponent implements OnChanges, OnDestroy {
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();
const id = intersects[0].object.name;
this.updateHighlightedItems(id);
}
}
setNormalisedMousePos(event:PointerEvent) {
setNormalisedMousePos(event:MouseEvent) {
event.preventDefault();
const canvas = (event.target as Element);
const canvasOffset = canvas.getBoundingClientRect();
@@ -160,11 +180,13 @@ export class RectsComponent implements OnChanges, OnDestroy {
this.mouse.z = 0;
}
updateHighlightedRect() {
const event: CustomEvent = new CustomEvent("highlightedChange", {
bubbles: true,
detail: { layerId: this.highlighted }
});
updateHighlightedItems(newId: string) {
const event: CustomEvent = new CustomEvent(
ViewerEvents.HighlightedChange,
{
bubbles: true,
detail: { id: newId }
});
this.elementRef.nativeElement.dispatchEvent(event);
}
@@ -181,8 +203,8 @@ export class RectsComponent implements OnChanges, OnDestroy {
}
updateVariablesBeforeRefresh() {
this.rects = this.rects.filter(rect => rect.displayId === this.currentDisplayId);
this.canvasGraphics.updateRects(this.rects);
const rects = this.rects.filter(rect => rect.displayId === this.currentDisplayId);
this.canvasGraphics.updateRects(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);
}
@@ -272,8 +294,8 @@ export class RectsComponent implements OnChanges, OnDestroy {
return this.canvasGraphics.getLayerSeparation();
}
xyCameraPos() {
return this.canvasGraphics.getXyCameraPos();
xCameraPos() {
return this.canvasGraphics.getXCameraPos();
}
showVirtualDisplays() {

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { RectsUtils } from "./rects_utils";
import { RectsUtils } from "viewers/components/rects/rects_utils";
describe("RectsUtils", () => {
it("transforms rect", () => {
@@ -38,7 +38,8 @@ describe("RectsUtils", () => {
width: 1,
ref: null,
id: 12345,
displayId: 0
displayId: 0,
isVirtual: false
};
const expected = {
topLeft: {x: 1, y: 1},
@@ -52,7 +53,7 @@ describe("RectsUtils", () => {
ref: null,
id: 12345,
displayId: 0,
isVirtual: undefined
isVirtual: false
};
expect(RectsUtils.transformRect(rect.transform.matrix, rect)).toEqual(expected);
});

View File

@@ -0,0 +1,66 @@
/*
* 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 { TreeComponent } from "./tree.component";
import { ComponentFixtureAutoDetect } from "@angular/core/testing";
import { NO_ERRORS_SCHEMA } from "@angular/core";
describe("TreeComponent", () => {
let fixture: ComponentFixture<TreeComponent>;
let component: TreeComponent;
let htmlElement: HTMLElement;
beforeAll(async () => {
await TestBed.configureTestingModule({
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true }
],
declarations: [
TreeComponent
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TreeComponent);
component = fixture.componentInstance;
htmlElement = fixture.nativeElement;
component.isFlattened = true;
component.item = {
simplifyNames: false,
kind: "entry",
name: "BaseLayerTraceEntry",
shortName: "BLTE",
chips: [],
children: [{kind: "3", id: "3", name: "Child1"}]
};
component.diffClass = jasmine.createSpy().and.returnValue("none");
component.isHighlighted = jasmine.createSpy().and.returnValue(false);
component.hasChildren = jasmine.createSpy().and.returnValue(true);
});
it("can be created", () => {
fixture.detectChanges();
expect(component).toBeTruthy();
});
it("creates node element", () => {
fixture.detectChanges();
const nodeElement = htmlElement.querySelector(".node");
expect(nodeElement).toBeTruthy();
});
});

View File

@@ -0,0 +1,242 @@
/*
* 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, Input, Output, ElementRef, EventEmitter } from "@angular/core";
import { PersistentStore } from "common/persistent_store";
import { nodeStyles, treeNodeStyles } from "viewers/styles/node.styles";
import { Tree, diffClass, isHighlighted } from "viewers/common/tree_utils";
import { TraceType } from "common/trace/trace_type";
@Component({
selector: "tree-view",
template: `
<div class="tree-view">
<tree-node
class="node"
[class.leaf]="isLeaf()"
[class.selected]="isHighlighted(item, highlightedItems)"
[class.clickable]="isClickable()"
[class.shaded]="isShaded"
[class.hover]="nodeHover"
[class.childHover]="childHover"
[class]="diffClass(item)"
[style]="nodeOffsetStyle()"
[item]="item"
[flattened]="isFlattened"
[isLeaf]="isLeaf()"
[isCollapsed]="isCollapsed()"
[hasChildren]="hasChildren()"
[isPinned]="isPinned()"
(toggleTreeChange)="toggleTree()"
(click)="onNodeClick($event)"
(expandTreeChange)="expandTree()"
(pinNodeChange)="sendNewPinnedItemToHierarchy($event)"
></tree-node>
<div class="children" *ngIf="hasChildren()" [hidden]="isCollapsed()" [style]="childrenIndentation()">
<ng-container *ngFor="let child of children()">
<tree-view
class="childrenTree"
[item]="child"
[store]="store"
[dependencies]="dependencies"
[isFlattened]="isFlattened"
[isShaded]="!isShaded"
[useGlobalCollapsedState]="useGlobalCollapsedState"
[initialDepth]="initialDepth + 1"
[highlightedItems]="highlightedItems"
[pinnedItems]="pinnedItems"
(highlightedItemChange)="sendNewHighlightedItemToHierarchy($event)"
(pinnedItemChange)="sendNewPinnedItemToHierarchy($event)"
[itemsClickable]="itemsClickable"
(hoverStart)="childHover = true"
(hoverEnd)="childHover = false"
></tree-view>
</ng-container>
</div>
</div>
`,
styles: [nodeStyles, treeNodeStyles]
})
export class TreeComponent {
diffClass = diffClass;
isHighlighted = isHighlighted;
@Input() item!: Tree;
@Input() dependencies: Array<TraceType> = [];
@Input() store!: PersistentStore;
@Input() isFlattened? = false;
@Input() isShaded? = false;
@Input() initialDepth = 0;
@Input() highlightedItems: Array<string> = [];
@Input() pinnedItems?: Array<Tree> = [];
@Input() itemsClickable?: boolean;
@Input() useGlobalCollapsedState?: boolean;
@Output() highlightedItemChange = new EventEmitter<string>();
@Output() pinnedItemChange = new EventEmitter<Tree>();
@Output() hoverStart = new EventEmitter<void>();
@Output() hoverEnd = new EventEmitter<void>();
isCollapsedByDefault = true;
localCollapsedState = this.isCollapsedByDefault;
nodeHover = false;
childHover = false;
readonly levelOffset = 24;
nodeElement: HTMLElement;
constructor(
@Inject(ElementRef) elementRef: ElementRef,
) {
this.nodeElement = elementRef.nativeElement.querySelector(".node");
this.nodeElement?.addEventListener("mousedown", this.nodeMouseDownEventListener);
this.nodeElement?.addEventListener("mouseenter", this.nodeMouseEnterEventListener);
this.nodeElement?.addEventListener("mouseleave", this.nodeMouseLeaveEventListener);
}
ngOnDestroy() {
this.nodeElement?.removeEventListener("mousedown", this.nodeMouseDownEventListener);
this.nodeElement?.removeEventListener("mouseenter", this.nodeMouseEnterEventListener);
this.nodeElement?.removeEventListener("mouseleave", this.nodeMouseLeaveEventListener);
}
onNodeClick(event: MouseEvent) {
event.preventDefault();
if (window.getSelection()?.type === "range") {
return;
}
if (!this.isLeaf() && event.detail % 2 === 0) {
// Double click collapsable node
event.preventDefault();
this.toggleTree();
} else {
this.updateHighlightedItems();
}
}
nodeOffsetStyle() {
const offset = this.levelOffset * (this.initialDepth) + "px";
return {
marginLeft: "-" + offset,
paddingLeft: offset,
};
}
updateHighlightedItems() {
if (this.item && this.item.id) {
this.highlightedItemChange.emit(`${this.item.id}`);
}
}
isPinned() {
if (this.item) {
return this.pinnedItems?.map((item: Tree) => `${item.id}`).includes(`${this.item.id}`);
}
return false;
}
sendNewHighlightedItemToHierarchy(newId: string) {
this.highlightedItemChange.emit(newId);
}
sendNewPinnedItemToHierarchy(newPinnedItem: Tree) {
this.pinnedItemChange.emit(newPinnedItem);
}
isLeaf() {
return !this.item.children || this.item.children.length === 0;
}
isClickable() {
return !this.isLeaf() || this.itemsClickable;
}
toggleTree() {
this.setCollapseValue(!this.isCollapsed());
}
expandTree() {
this.setCollapseValue(false);
}
isCollapsed() {
if (this.isLeaf()) {
return false;
}
if (this.useGlobalCollapsedState) {
return this.store.getFromStore(`collapsedState.item.${this.dependencies}.${this.item.id}`)==="true"
?? this.isCollapsedByDefault;
}
return this.localCollapsedState;
}
children() {
return this.item.children;
}
hasChildren() {
const isParentEntryInFlatView = this.item.kind === "entry" && this.isFlattened;
return (!this.isFlattened || isParentEntryInFlatView) && !this.isLeaf();
}
setCollapseValue(isCollapsed:boolean) {
if (this.useGlobalCollapsedState) {
this.store.addToStore(`collapsedState.item.${this.dependencies}.${this.item.id}`, `${isCollapsed}`);
} else {
this.localCollapsedState = isCollapsed;
}
}
childrenIndentation() {
if (this.isFlattened) {
return {
marginLeft: "0px",
paddingLeft: "0px",
marginTop: "0px",
};
} else {
// Aligns border with collapse arrows
return {
marginLeft: "12px",
paddingLeft: "11px",
borderLeft: "1px solid rgb(238, 238, 238)",
marginTop: "0px",
};
}
}
nodeMouseDownEventListener = (event:MouseEvent) => {
if (event.detail > 1) {
event.preventDefault();
return false;
}
return true;
};
nodeMouseEnterEventListener = () => {
this.nodeHover = true;
this.hoverStart.emit();
};
nodeMouseLeaveEventListener = () => {
this.nodeHover = false;
this.hoverEnd.emit();
};
}

View File

@@ -0,0 +1,47 @@
/*
* 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 { TreeElementComponent } from "./tree_element.component";
import { ComponentFixtureAutoDetect } from "@angular/core/testing";
import { NO_ERRORS_SCHEMA } from "@angular/core";
describe("TreeElementComponent", () => {
let fixture: ComponentFixture<TreeElementComponent>;
let component: TreeElementComponent;
let htmlElement: HTMLElement;
beforeAll(async () => {
await TestBed.configureTestingModule({
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true }
],
declarations: [
TreeElementComponent
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TreeElementComponent);
component = fixture.componentInstance;
htmlElement = fixture.nativeElement;
});
it("can be created", () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,54 @@
/*
* 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 { treeElementStyles } from "viewers/styles/tree_element.styles";
import { Tree } from "viewers/common/tree_utils";
import Chip from "viewers/common/chip";
@Component({
selector: "tree-element",
template: `
<span>
<span class="kind">{{item.kind}}</span>
<span *ngIf="item.kind && item.name">-</span>
<span *ngIf="showShortName()" [matTooltip]="item.name">{{ item.shortName }}</span>
<span *ngIf="!showShortName()">{{item.name}}</span>
<div
*ngFor="let chip of item.chips"
[class]="chipClass(chip)"
[matTooltip]="chip.long"
>{{chip.short}}</div>
</span>
`,
styles: [ treeElementStyles ]
})
export class TreeElementComponent {
@Input() item!: Tree;
showShortName() {
return this.item.simplifyNames && this.item.shortName !== this.item.name;
}
chipClass(chip: Chip) {
return [
"tree-view-internal-chip",
"tree-view-chip",
"tree-view-chip" + "-" +
(chip.type.toString() || "default"),
];
}
}

View File

@@ -0,0 +1,65 @@
/*
* 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 { TreeNodeComponent } from "./tree_node.component";
import { ComponentFixtureAutoDetect } from "@angular/core/testing";
import { NO_ERRORS_SCHEMA } from "@angular/core";
describe("TreeNodeComponent", () => {
let fixture: ComponentFixture<TreeNodeComponent>;
let component: TreeNodeComponent;
let htmlElement: HTMLElement;
beforeAll(async () => {
await TestBed.configureTestingModule({
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true }
],
declarations: [
TreeNodeComponent
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TreeNodeComponent);
component = fixture.componentInstance;
htmlElement = fixture.nativeElement;
component.item = {
simplifyNames: false,
kind: "entry",
name: "BaseLayerTraceEntry",
shortName: "BLTE",
chips: [],
};
component.isCollapsed = true;
component.hasChildren = false;
component.isPinned = false;
component.isInPinnedSection = false;
});
it("can be created", () => {
fixture.detectChanges();
expect(component).toBeTruthy();
});
it("creates tree element", () => {
fixture.detectChanges();
const treeElement = htmlElement.querySelector("tree-element");
expect(treeElement).toBeTruthy();
});
});

View File

@@ -0,0 +1,114 @@
/*
* 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 { nodeInnerItemStyles } from "viewers/styles/node.styles";
import { Tree } from "viewers/common/tree_utils";
@Component({
selector: "tree-node",
template: `
<button
id="toggle-tree-btn"
class="icon-button"
(click)="toggleTree($event)"
*ngIf="showChevron()"
>
<mat-icon class="icon-button">
{{isCollapsed ? "chevron_right" : "expand_more"}}
</mat-icon>
</button>
<div
class="leaf-node-icon-wrapper"
*ngIf="showLeafNodeIcon()"
>
<mat-icon class="leaf-node-icon"></mat-icon>
</div>
<button
id="pin-node-btn"
class="icon-button"
(click)="pinNode($event)"
*ngIf="!isEntryNode()"
>
<mat-icon class="icon-button">
{{isPinned ? "star" : "star_border"}}
</mat-icon>
</button>
<div class="description">
<tree-element
[item]="item"
></tree-element>
</div>
<button
id="expand-tree-btn"
*ngIf="hasChildren && isCollapsed"
(click)="expandTree($event)"
class="icon-button"
>
<mat-icon
aria-hidden="true"
class="icon-button"
>
more_horiz
</mat-icon>
</button>
`,
styles: [nodeInnerItemStyles]
})
export class TreeNodeComponent {
@Input() item!: Tree | null;
@Input() isLeaf?: boolean;
@Input() flattened?: boolean;
@Input() isCollapsed?: boolean;
@Input() hasChildren?: boolean = false;
@Input() isPinned?: boolean = false;
@Input() isInPinnedSection?: boolean = false;
@Output() toggleTreeChange = new EventEmitter<void>();
@Output() expandTreeChange = new EventEmitter<boolean>();
@Output() pinNodeChange = new EventEmitter<Tree>();
isEntryNode() {
return this.item.kind === "entry" ?? false;
}
toggleTree(event: MouseEvent) {
event.stopPropagation();
this.toggleTreeChange.emit();
}
showChevron() {
return !this.isLeaf && !this.flattened && !this.isInPinnedSection;
}
showLeafNodeIcon() {
return !this.showChevron() && !this.isInPinnedSection;
}
expandTree(event: MouseEvent) {
event.stopPropagation();
this.expandTreeChange.emit();
}
pinNode(event: MouseEvent) {
event.stopPropagation();
this.pinNodeChange.emit(this.item);
}
}

View File

@@ -0,0 +1,68 @@
/*
* 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.
*/
export const nodeStyles = `
.node {position: relative;display: inline-block;padding: 2px; height: 100%; width: 100%;}
.node.clickable {cursor: pointer;}
.node:not(.selected).added,
.node:not(.selected).addedMove,
.expand-tree-btn.added,
.expand-tree-btn.addedMove {
background: #03ff35;
}
.node:not(.selected).deleted,
.node:not(.selected).deletedMove,
.expand-tree-btn.deleted,
.expand-tree-btn.deletedMove {
background: #ff6b6b;
}
.node:hover:not(.selected) {background: #f1f1f1;}
.node:not(.selected).modified,
.expand-tree-btn.modified {
background: cyan;
}
.node.addedMove:after,
.node.deletedMove:after {
content: 'moved';
margin: 0 5px;
background: #448aff;
border-radius: 5px;
padding: 3px;
color: white;
}
.selected {background-color: #365179;color: white;}
`;
export const treeNodeStyles = `
.node.shaded:not(:hover):not(.selected):not(.added):not(.addedMove):not(.deleted):not(.deletedMove):not(.modified) {background: #f8f9fa}
.node.selected + .children {border-left: 1px solid rgb(200, 200, 200);}
.node.child-hover + .children {border-left: 1px solid #b4b4b4;}
.node.hover + .children { border-left: 1px solid rgb(200, 200, 200);}
`;
export const nodeInnerItemStyles = `
.leaf-node-icon {content: ''; display: inline-block; margin-left: 40%; margin-top: 40%; height: 5px; width: 5px; border-radius: 50%;background-color: #9b9b9b;}
.leaf-node-icon-wrapper, .description, #toggle-tree-btn, #expand-tree-btn, #pin-node-btn { position: relative; display: inline-block;}
mat-icon {margin: 0}
#pin-node-btn {padding: 0; transform: scale(0.7)}
.description {position: relative; align-items: center; flex: 1 1 auto; vertical-align: middle; word-break: break-all;}
.leaf-node-icon-wrapper{padding-left: 6px; padding-right: 6px; min-height: 24px; width: 24px; position:relative; align-content: center; vertical-align: middle;}
.icon-button { background: none;border: none;display: inline-block;vertical-align: middle;}
`;

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.
*/
export const treeElementStyles = `
.kind {font-weight: bold}
span {overflow-wrap: break-word; flex: 1 1 auto; width: 0; word-break: break-all}
.tree-view-internal-chip {
display: inline-block;
}
.tree-view-chip {
padding: 0 10px;
border-radius: 10px;
background-color: #aaa;
color: black;
}
.tree-view-chip.tree-view-chip-warn {
background-color: #ffaa6b;
color: black;
}
.tree-view-chip.tree-view-chip-error {
background-color: #ff6b6b;
color: black;
}
.tree-view-chip.tree-view-chip-gpu {
background-color: #00c853;
color: black;
}
.tree-view-chip.tree-view-chip-hwc {
background-color: #448aff;
color: black;
}
`;

View File

@@ -15,36 +15,69 @@
*/
import { Rectangle, RectMatrix, RectTransform, UiData } from "viewers/viewer_surface_flinger/ui_data";
import { TraceType } from "common/trace/trace_type";
import { UserOptions } from "viewers/common/user_options";
import { TreeGenerator, getFilter, FilterType, Tree } from "viewers/common/tree_utils";
type NotifyViewCallbackType = (uiData: UiData) => void;
class Presenter {
constructor(notifyViewCallback: NotifyViewCallbackType) {
this.notifyViewCallback = notifyViewCallback;
this.uiData = new UiData("Initial UI data");
this.uiData = new UiData();
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);
public updatePinnedItems(event: CustomEvent) {
const pinnedItem = event.detail.pinnedItem;
const pinnedId = `${pinnedItem.id}`;
if (this.pinnedItems.map(item => `${item.id}`).includes(pinnedId)) {
this.pinnedItems = this.pinnedItems.filter(pinned => `${pinned.id}` != pinnedId);
} else {
this.pinnedItems.push(pinnedItem);
}
this.updatePinnedIds(pinnedId);
this.uiData.pinnedItems = this.pinnedItems;
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");
public updateHighlightedItems(event: CustomEvent) {
const id = `${event.detail.id}`;
if (this.highlightedItems.includes(id)) {
this.highlightedItems = this.highlightedItems.filter(hl => hl != id);
} else {
this.highlightedItems = []; //if multi-select implemented, remove this line
this.highlightedItems.push(id);
}
this.uiData.highlightedItems = this.highlightedItems;
this.notifyViewCallback(this.uiData);
}
public updateHierarchyTree(event: CustomEvent) {
this.hierarchyUserOptions = event.detail.userOptions;
this.uiData.hierarchyUserOptions = this.hierarchyUserOptions;
this.uiData.tree = this.generateTree();
this.notifyViewCallback(this.uiData);
}
public filterHierarchyTree(event: CustomEvent) {
this.hierarchyFilter = getFilter(event.detail.filterString);
this.uiData.tree = this.generateTree();
this.notifyViewCallback(this.uiData);
}
public notifyCurrentTraceEntries(entries: Map<TraceType, any>) {
this.uiData = new UiData();
const entry = entries.get(TraceType.SURFACE_FLINGER)[0];
this.uiData.rects = [];
const displayRects = entry.displays.map((display: any) => {
const rect = display.layerStackSpace;
rect.label = display.name;
rect.id = display.id;
rect.displayId = display.layerStackId;
rect.isDisplay = true;
rect.isVirtual = display.isVirtual;
rect.isVirtual = display.isVirtual ?? false;
return rect;
}) ?? [];
this.uiData.highlighted = this.highlighted;
this.displayIds = [];
const rects = entry.visibleLayers
@@ -59,10 +92,36 @@ class Presenter {
});
this.uiData.rects = this.rectsToUiData(rects.concat(displayRects));
this.uiData.displayIds = this.displayIds;
this.uiData.highlightedItems = this.highlightedItems;
this.uiData.rects = this.rectsToUiData(entry.rects.concat(displayRects));
this.uiData.hierarchyUserOptions = this.hierarchyUserOptions;
this.previousEntry = entries.get(TraceType.SURFACE_FLINGER)[1];
this.entry = entry;
this.uiData.tree = this.generateTree();
this.notifyViewCallback(this.uiData);
}
rectsToUiData(rects: any[]): Rectangle[] {
private generateTree() {
if (!this.entry) {
return null;
}
const generator = new TreeGenerator(this.entry, this.hierarchyUserOptions, this.hierarchyFilter, this.pinnedIds)
.withUniqueNodeId();
let tree: Tree;
if (!this.hierarchyUserOptions["showDiff"]?.enabled) {
tree = generator.generateTree();
} else {
tree = generator.compareWith(this.previousEntry)
.withModifiedCheck()
.generateFinalDiffTree();
}
this.pinnedItems = generator.getPinnedItems();
this.uiData.pinnedItems = this.pinnedItems;
return tree;
}
private rectsToUiData(rects: any[]): Rectangle[] {
const uiRects: Rectangle[] = [];
rects.forEach((rect: any) => {
let t = null;
@@ -86,14 +145,6 @@ class Presenter {
};
}
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},
@@ -101,22 +152,53 @@ class Presenter {
width: rect.width,
label: rect.label,
transform: transform,
isVisible: isVisible,
isDisplay: isDisplay,
isVisible: rect.ref?.isVisible ?? false,
isDisplay: rect.isDisplay ?? false,
ref: rect.ref,
id: rect.id ?? rect.ref.id,
displayId: rect.displayId ?? rect.ref.stackId,
isVirtual: rect.isVirtual
isVirtual: rect.isVirtual ?? false
};
uiRects.push(newRect);
});
return uiRects;
}
private updatePinnedIds(newId: string) {
if (this.pinnedIds.includes(newId)) {
this.pinnedIds = this.pinnedIds.filter(pinned => pinned != newId);
} else {
this.pinnedIds.push(newId);
}
}
private readonly notifyViewCallback: NotifyViewCallbackType;
private uiData: UiData;
private highlighted = "";
private displayIds: Array<number> = [];
private hierarchyFilter: FilterType = getFilter("");
private highlightedItems: Array<string> = [];
private pinnedItems: Array<Tree> = [];
private pinnedIds: Array<string> = [];
private previousEntry: any = null;
private entry: any = null;
private hierarchyUserOptions: UserOptions = {
showDiff: {
name: "Show diff",
enabled: false
},
simplifyNames: {
name: "Simplify names",
enabled: true
},
onlyVisible: {
name: "Only visible",
enabled: false
},
flat: {
name: "Flat",
enabled: false
}
};
}
export {Presenter};

View File

@@ -13,13 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class UiData {
constructor(public text: string) {
console.log(text);
}
import { TraceType } from "common/trace/trace_type";
import { Tree } from "viewers/common/tree_utils";
import { UserOptions } from "viewers/common/user_options";
export class UiData {
dependencies: Array<TraceType> = [TraceType.SURFACE_FLINGER];
rects?: Rectangle[] = [];
highlighted?: string = "";
displayIds?: number[] = [];
highlightedItems?: Array<string> = [];
pinnedItems?: Array<Tree> = [];
hierarchyUserOptions?: UserOptions = {};
tree?: Tree | null = null;
}
export interface Rectangle {
@@ -34,7 +39,7 @@ export interface Rectangle {
ref: any;
id: number;
displayId: number;
isVirtual?: boolean;
isVirtual: boolean;
}
export interface Point {
@@ -60,5 +65,3 @@ export interface RectMatrix {
tx: number;
ty: number;
}
export {UiData};

View File

@@ -16,13 +16,13 @@
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 { HierarchyComponent } from "viewers/components/hierarchy.component";
import { PropertiesComponent } from "viewers/components/properties.component";
import { RectsComponent } from "viewers/components/rects/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";
import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from "@angular/core";
describe("ViewerSurfaceFlingerComponent", () => {
let fixture: ComponentFixture<ViewerSurfaceFlingerComponent>;
@@ -44,7 +44,7 @@ describe("ViewerSurfaceFlingerComponent", () => {
PropertiesComponent,
RectsComponent
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA]
}).compileComponents();
});

View File

@@ -20,6 +20,7 @@ import {
import { UiData } from "./ui_data";
import { TRACE_INFO } from "app/trace_info";
import { TraceType } from "common/trace/trace_type";
import { PersistentStore } from "common/persistent_store";
@Component({
selector: "viewer-surface-flinger",
@@ -29,33 +30,86 @@ import { TraceType } from "common/trace/trace_type";
<rects-view
[rects]="inputData?.rects ?? []"
[displayIds]="inputData?.displayIds ?? []"
[highlighted]="inputData?.highlighted ?? ''"
class="rects-view"
[highlightedItems]="inputData?.highlightedItems ?? []"
></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 fxLayout="row wrap" fxLayoutGap="10px grid" class="card-grid">
<mat-card id="sf-hierarchy-view" class="hierarchy-view">
<hierarchy-view
[tree]="inputData?.tree"
[dependencies]="inputData?.dependencies ?? []"
[highlightedItems]="inputData?.highlightedItems ?? []"
[pinnedItems]="inputData?.pinnedItems ?? []"
[store]="store"
[userOptions]="inputData?.hierarchyUserOptions ?? {}"
></hierarchy-view>
</mat-card>
<mat-card id="sf-properties-view" class="properties-view">
<properties-view></properties-view>
</mat-card>
</div>
</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;}",
`
@import 'https://fonts.googleapis.com/icon?family=Material+Icons';
mat-icon {
margin: 5px
}
.icon-button {
background: none;
border: none;
display: inline-block;
vertical-align: middle;
}
viewer-surface-flinger {
font-family: Arial, Helvetica, sans-serif;
}
.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: 350px;
height: 52.5rem;
margin: 0px;
border: 1px solid rgb(129, 129, 129);
border-radius: 0;
}
.hierarchy-view, .properties-view {
font: inherit;
margin: 0px;
width: 50%;
height: 52.5rem;
border-radius: 0;
border-top: 1px solid rgb(129, 129, 129);
border-right: 1px solid rgb(129, 129, 129);
border-bottom: 1px solid rgb(129, 129, 129);
}
`,
]
})
export class ViewerSurfaceFlingerComponent {
@Input()
inputData?: UiData;
@Input() inputData?: UiData;
@Input() store: PersistentStore = new PersistentStore();
TRACE_INFO = TRACE_INFO;
TraceType = TraceType;
}

View File

@@ -17,6 +17,7 @@ import {TraceType} from "common/trace/trace_type";
import {Viewer} from "viewers/viewer";
import {Presenter} from "./presenter";
import {UiData} from "./ui_data";
import { ViewerEvents } from "viewers/common/viewer_events";
class ViewerSurfaceFlinger implements Viewer {
constructor() {
@@ -24,7 +25,10 @@ class ViewerSurfaceFlinger implements Viewer {
this.presenter = new Presenter((uiData: UiData) => {
(this.view as any).inputData = uiData;
});
this.view.addEventListener("highlightedChange", (event) => this.presenter.updateHighlightedRect((event as CustomEvent)));
this.view.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) => this.presenter.updatePinnedItems((event as CustomEvent)));
this.view.addEventListener(ViewerEvents.HighlightedChange, (event) => this.presenter.updateHighlightedItems((event as CustomEvent)));
this.view.addEventListener(ViewerEvents.HierarchyUserOptionsChange, (event) => this.presenter.updateHierarchyTree((event as CustomEvent)));
this.view.addEventListener(ViewerEvents.HierarchyFilterChange, (event) => this.presenter.filterHierarchyTree((event as CustomEvent)));
}
public notifyCurrentTraceEntries(entries: Map<TraceType, any>): void {

View File

@@ -20,6 +20,7 @@ import {
Output
} from "@angular/core";
import {UiData} from "./ui_data";
import { PersistentStore } from "common/persistent_store";
@Component({
selector: "viewer-window-manager",
@@ -32,11 +33,10 @@ import {UiData} from "./ui_data";
`
})
export class ViewerWindowManagerComponent {
@Input()
inputData?: UiData;
@Input() inputData?: UiData;
@Input() store?: PersistentStore;
@Output()
outputEvent = new EventEmitter<DummyEvent>(); // or EventEmitter<void>()
@Output() outputEvent = new EventEmitter<DummyEvent>(); // or EventEmitter<void>()
public generateOutputEvent(event: MouseEvent) {
this.outputEvent.emit(new DummyEvent());