Add viewer Transaction
Fix: 238088678 Test: npm run build:all && npm run test:all Change-Id: I90ad36a0a18e3b68216ea18d2a47e3ff38d98412
This commit is contained in:
@@ -47,6 +47,7 @@ import { TreeNodePropertiesDataViewComponent } from "viewers/components/tree_nod
|
||||
import { ViewerInputMethodComponent } from "viewers/components/viewer_input_method.component";
|
||||
import { ViewerProtologComponent} from "viewers/viewer_protolog/viewer_protolog.component";
|
||||
import { ViewerSurfaceFlingerComponent } from "viewers/viewer_surface_flinger/viewer_surface_flinger.component";
|
||||
import { ViewerTransactionsComponent } from "viewers/viewer_transactions/viewer_transactions.component";
|
||||
import { ViewerWindowManagerComponent } from "viewers/viewer_window_manager/viewer_window_manager.component";
|
||||
|
||||
@NgModule({
|
||||
@@ -56,6 +57,7 @@ import { ViewerWindowManagerComponent } from "viewers/viewer_window_manager/view
|
||||
ViewerSurfaceFlingerComponent,
|
||||
ViewerInputMethodComponent,
|
||||
ViewerProtologComponent,
|
||||
ViewerTransactionsComponent,
|
||||
CollectTracesComponent,
|
||||
UploadTracesComponent,
|
||||
AdbProxyComponent,
|
||||
|
||||
@@ -24,6 +24,7 @@ import { ViewerInputMethodComponent } from "viewers/components/viewer_input_meth
|
||||
import { ViewerProtologComponent} from "viewers/viewer_protolog/viewer_protolog.component";
|
||||
import { ViewerSurfaceFlingerComponent } from "viewers/viewer_surface_flinger/viewer_surface_flinger.component";
|
||||
import { ViewerWindowManagerComponent } from "viewers/viewer_window_manager/viewer_window_manager.component";
|
||||
import { ViewerTransactionsComponent } from "viewers/viewer_transactions/viewer_transactions.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-root",
|
||||
@@ -147,6 +148,10 @@ export class AppComponent {
|
||||
customElements.define("viewer-surface-flinger",
|
||||
createCustomElement(ViewerSurfaceFlingerComponent, {injector}));
|
||||
}
|
||||
if (!customElements.get("viewer-transactions")) {
|
||||
customElements.define("viewer-transactions",
|
||||
createCustomElement(ViewerTransactionsComponent, {injector}));
|
||||
}
|
||||
if (!customElements.get("viewer-window-manager")) {
|
||||
customElements.define("viewer-window-manager",
|
||||
createCustomElement(ViewerWindowManagerComponent, {injector}));
|
||||
|
||||
22
tools/winscope-ng/src/common/trace/transactions.ts
Normal file
22
tools/winscope-ng/src/common/trace/transactions.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 TransactionsTraceEntry {
|
||||
constructor(public entriesProto: any[], public currentEntryIndex: number) {
|
||||
}
|
||||
}
|
||||
|
||||
export {TransactionsTraceEntry};
|
||||
@@ -65,7 +65,9 @@ abstract class Parser {
|
||||
return this.timestamps.get(type);
|
||||
}
|
||||
|
||||
//TODO: factor out timestamp search policy. Receive index parameter instead.
|
||||
//TODO:
|
||||
// - factor out timestamp search policy. Receive index parameter instead.
|
||||
// - make async for possible lazy disk reads in the future
|
||||
public getTraceEntry(timestamp: Timestamp): undefined|any {
|
||||
const timestamps = this.getTimestamps(timestamp.getType());
|
||||
if (timestamps === undefined) {
|
||||
|
||||
@@ -17,6 +17,7 @@ import {Timestamp, TimestampType} from "common/trace/timestamp";
|
||||
import {TraceType} from "common/trace/trace_type";
|
||||
import {Parser} from "./parser";
|
||||
import {UnitTestUtils} from "test/unit/utils";
|
||||
import {TransactionsTraceEntry} from "../common/trace/transactions";
|
||||
|
||||
describe("ParserTransactions", () => {
|
||||
describe("trace with elapsed + real timestamp", () => {
|
||||
@@ -62,15 +63,31 @@ describe("ParserTransactions", () => {
|
||||
|
||||
it("retrieves trace entry from elapsed timestamp", () => {
|
||||
const timestamp = new Timestamp(TimestampType.ELAPSED, 2517952515n);
|
||||
expect(BigInt(parser.getTraceEntry(timestamp)!.elapsedRealtimeNanos))
|
||||
const entry: TransactionsTraceEntry = parser.getTraceEntry(timestamp)!;
|
||||
expect(entry.currentEntryIndex).toEqual(1);
|
||||
expect(BigInt(entry.entriesProto[entry.currentEntryIndex].elapsedRealtimeNanos))
|
||||
.toEqual(2517952515n);
|
||||
});
|
||||
|
||||
it("retrieves trace entry from real timestamp", () => {
|
||||
const timestamp = new Timestamp(TimestampType.REAL, 1659507541118452067n);
|
||||
expect(BigInt(parser.getTraceEntry(timestamp)!.elapsedRealtimeNanos))
|
||||
const entry: TransactionsTraceEntry = parser.getTraceEntry(timestamp)!;
|
||||
expect(entry.currentEntryIndex).toEqual(1);
|
||||
expect(BigInt(entry.entriesProto[entry.currentEntryIndex].elapsedRealtimeNanos))
|
||||
.toEqual(2517952515n);
|
||||
});
|
||||
|
||||
it("decodes 'what' field in proto", () => {
|
||||
const timestamp = new Timestamp(TimestampType.ELAPSED, 2517952515n);
|
||||
const entry: TransactionsTraceEntry = parser.getTraceEntry(timestamp)!;
|
||||
|
||||
expect(entry.entriesProto[0].transactions[0].layerChanges[0].what)
|
||||
.toEqual("eLayerChanged");
|
||||
expect(entry.entriesProto[0].transactions[1].layerChanges[0].what)
|
||||
.toEqual("eFlagsChanged | eDestinationFrameChanged");
|
||||
expect(entry.entriesProto[222].transactions[1].displayChanges[0].what)
|
||||
.toEqual("eLayerStackChanged | eDisplayProjectionChanged | eFlagsChanged");
|
||||
});
|
||||
});
|
||||
|
||||
describe("trace with elapsed (only) timestamp", () => {
|
||||
|
||||
@@ -15,8 +15,9 @@
|
||||
*/
|
||||
import {Timestamp, TimestampType} from "common/trace/timestamp";
|
||||
import {TraceType} from "common/trace/trace_type";
|
||||
import {TransactionsTraceEntry} from "common/trace/transactions";
|
||||
import {Parser} from "./parser";
|
||||
import {AccessibilityTraceFileProto, TransactionsTraceFileProto} from "./proto_types";
|
||||
import {TransactionsTraceFileProto} from "./proto_types";
|
||||
|
||||
class ParserTransactions extends Parser {
|
||||
constructor(trace: File) {
|
||||
@@ -33,14 +34,60 @@ class ParserTransactions extends Parser {
|
||||
}
|
||||
|
||||
override decodeTrace(buffer: Uint8Array): any[] {
|
||||
const decoded = <any>TransactionsTraceFileProto.decode(buffer);
|
||||
if (Object.prototype.hasOwnProperty.call(decoded, "realToElapsedTimeOffsetNanos")) {
|
||||
this.realToElapsedTimeOffsetNs = BigInt(decoded.realToElapsedTimeOffsetNanos);
|
||||
const decodedProto = <any>TransactionsTraceFileProto.decode(buffer);
|
||||
this.decodeWhatFields(decodedProto);
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(decodedProto, "realToElapsedTimeOffsetNanos")) {
|
||||
this.realToElapsedTimeOffsetNs = BigInt(decodedProto.realToElapsedTimeOffsetNanos);
|
||||
}
|
||||
else {
|
||||
this.realToElapsedTimeOffsetNs = undefined;
|
||||
}
|
||||
return decoded.entry;
|
||||
return decodedProto.entry;
|
||||
}
|
||||
|
||||
private decodeWhatFields(decodedProto: any) {
|
||||
const decodeBitset32 = (bitset: number, EnumProto: any) => {
|
||||
return Object.keys(EnumProto).filter(key => {
|
||||
const value = EnumProto[key];
|
||||
return (bitset & value) != 0;
|
||||
});
|
||||
};
|
||||
|
||||
const concatBitsetTokens = (tokens: string[]) => {
|
||||
if (tokens.length == 0) {
|
||||
return "0";
|
||||
}
|
||||
return tokens.join(" | ");
|
||||
};
|
||||
|
||||
const LayerStateChangesLsbEnum = (<any>TransactionsTraceFileProto?.parent).LayerState.ChangesLsb;
|
||||
const LayerStateChangesMsbEnum = (<any>TransactionsTraceFileProto?.parent).LayerState.ChangesMsb;
|
||||
const DisplayStateChangesEnum = (<any>TransactionsTraceFileProto?.parent).DisplayState.Changes;
|
||||
|
||||
decodedProto.entry.forEach((transactionTraceEntry: any) => {
|
||||
transactionTraceEntry.transactions.forEach((transactionState: any) => {
|
||||
transactionState.layerChanges.forEach((layerState: any) => {
|
||||
layerState.what = concatBitsetTokens(
|
||||
decodeBitset32(layerState.what.low, LayerStateChangesLsbEnum).concat(
|
||||
decodeBitset32(layerState.what.high, LayerStateChangesMsbEnum)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
transactionState.displayChanges.forEach((displayState: any) => {
|
||||
displayState.what = concatBitsetTokens(
|
||||
decodeBitset32(displayState.what, DisplayStateChangesEnum)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
transactionTraceEntry.addedDisplays.forEach((displayState: any) => {
|
||||
displayState.what = concatBitsetTokens(
|
||||
decodeBitset32(displayState.what, DisplayStateChangesEnum)
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
override getTimestamp(type: TimestampType, entryProto: any): undefined|Timestamp {
|
||||
@@ -53,8 +100,8 @@ class ParserTransactions extends Parser {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
override processDecodedEntry(index: number, entryProto: any): any {
|
||||
return entryProto;
|
||||
override processDecodedEntry(index: number, entryProto: any): TransactionsTraceEntry {
|
||||
return new TransactionsTraceEntry(this.decodedEntries, index);
|
||||
}
|
||||
|
||||
private realToElapsedTimeOffsetNs: undefined|bigint;
|
||||
|
||||
@@ -1,4 +1,23 @@
|
||||
/*
|
||||
* 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 * as protobuf from "protobufjs";
|
||||
import Long from "long";
|
||||
|
||||
protobuf.util.Long = Long; // otherwise 64-bit types would be decoded as javascript number (only 53-bits precision)
|
||||
protobuf.configure();
|
||||
|
||||
import accessibilityJson from "frameworks/base/core/proto/android/server/accessibilitytrace.proto";
|
||||
import inputMethodClientsJson from "frameworks/base/core/proto/android/view/inputmethod/inputmethodeditortrace.proto";
|
||||
|
||||
38
tools/winscope-ng/src/test/e2e/viewer_transactions.spec.ts
Normal file
38
tools/winscope-ng/src/test/e2e/viewer_transactions.spec.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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} from "protractor";
|
||||
import {E2eTestUtils} from "./utils";
|
||||
|
||||
describe("Viewer Transactions", () => {
|
||||
beforeAll(async () => {
|
||||
browser.manage().timeouts().implicitlyWait(1000);
|
||||
browser.get("file://" + E2eTestUtils.getProductionIndexHtmlPath());
|
||||
}),
|
||||
|
||||
it("processes trace and renders view", async () => {
|
||||
const inputFile = element(by.css("input[type=\"file\"]"));
|
||||
await inputFile.sendKeys(E2eTestUtils.getFixturePath("traces/elapsed_and_real_timestamp/Transactions.pb"));
|
||||
|
||||
const loadData = element(by.css(".load-btn"));
|
||||
await loadData.click();
|
||||
|
||||
const isViewerRendered = await element(by.css("viewer-transactions")).isPresent();
|
||||
expect(isViewerRendered).toBeTruthy();
|
||||
|
||||
const isFirstEntryRendered = await element(by.css("viewer-transactions .scroll .entry")).isPresent();
|
||||
expect(isFirstEntryRendered).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* 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 { PropertiesTreeGenerator } from "viewers/common/properties_tree_generator";
|
||||
import {PropertiesTreeNode} from "./ui_tree_utils";
|
||||
|
||||
describe("PropertiesTreeGenerator", () => {
|
||||
it("handles boolean", () => {
|
||||
const input = true;
|
||||
const actual = new PropertiesTreeGenerator().generate("root", input);
|
||||
|
||||
const expected: PropertiesTreeNode = {
|
||||
propertyKey: "root",
|
||||
propertyValue: "true"
|
||||
};
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it("handles number", () => {
|
||||
const input = 10;
|
||||
const actual = new PropertiesTreeGenerator().generate("root", input);
|
||||
|
||||
const expected: PropertiesTreeNode = {
|
||||
propertyKey: "root",
|
||||
propertyValue: "10"
|
||||
};
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it("handles string", () => {
|
||||
const input = "value";
|
||||
const actual = new PropertiesTreeGenerator().generate("root", input);
|
||||
|
||||
const expected: PropertiesTreeNode = {
|
||||
propertyKey: "root",
|
||||
propertyValue: "value"
|
||||
};
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it("handles empty array", () => {
|
||||
const input: any[] = [];
|
||||
const actual = new PropertiesTreeGenerator().generate("root", input);
|
||||
|
||||
const expected: PropertiesTreeNode = {
|
||||
propertyKey: "root",
|
||||
propertyValue: "[]"
|
||||
};
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it("handles array", () => {
|
||||
const input = ["value0", "value1"];
|
||||
const actual = new PropertiesTreeGenerator().generate("root", input);
|
||||
|
||||
const expected: PropertiesTreeNode = {
|
||||
propertyKey: "root",
|
||||
children: [
|
||||
{
|
||||
propertyKey: "0",
|
||||
propertyValue: "value0"
|
||||
},
|
||||
{
|
||||
propertyKey: "1",
|
||||
propertyValue: "value1"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it("handles empty object", () => {
|
||||
const input = {};
|
||||
const actual = new PropertiesTreeGenerator().generate("root", input);
|
||||
|
||||
const expected: PropertiesTreeNode = {
|
||||
propertyKey: "root",
|
||||
propertyValue: "{}"
|
||||
};
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it("handles object", () => {
|
||||
const input = {
|
||||
key0: "value0",
|
||||
key1: "value1"
|
||||
};
|
||||
const actual = new PropertiesTreeGenerator().generate("root", input);
|
||||
|
||||
const expected: PropertiesTreeNode = {
|
||||
propertyKey: "root",
|
||||
children: [
|
||||
{
|
||||
propertyKey: "key0",
|
||||
propertyValue: "value0"
|
||||
},
|
||||
{
|
||||
propertyKey: "key1",
|
||||
propertyValue: "value1"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it("handles nested objects", () => {
|
||||
const input = {
|
||||
object: {
|
||||
key: "object_value"
|
||||
},
|
||||
array: [
|
||||
"array_value"
|
||||
]
|
||||
};
|
||||
const actual = new PropertiesTreeGenerator().generate("root", input);
|
||||
|
||||
const expected: PropertiesTreeNode = {
|
||||
propertyKey: "root",
|
||||
children: [
|
||||
{
|
||||
propertyKey: "object",
|
||||
children: [
|
||||
{
|
||||
propertyKey: "key",
|
||||
propertyValue: "object_value"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
propertyKey: "array",
|
||||
children: [
|
||||
{
|
||||
propertyKey: "0",
|
||||
propertyValue: "array_value"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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 {PropertiesTreeNode} from "./ui_tree_utils";
|
||||
|
||||
class PropertiesTreeGenerator {
|
||||
public generate(
|
||||
key: string,
|
||||
value: any,
|
||||
): PropertiesTreeNode {
|
||||
if (this.isLeaf(value)) {
|
||||
return {
|
||||
propertyKey: key,
|
||||
propertyValue: this.leafToString(value)!,
|
||||
};
|
||||
}
|
||||
|
||||
let children: PropertiesTreeNode[];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
children = value.map((element, index) => this.generate("" + index, element));
|
||||
}
|
||||
else {
|
||||
children = Object.keys(value).map(childName => this.generate(childName, value[childName]));
|
||||
}
|
||||
|
||||
return {
|
||||
propertyKey: key,
|
||||
children: children
|
||||
};
|
||||
}
|
||||
|
||||
private isLeaf(value: any): boolean {
|
||||
return this.leafToString(value) !== undefined;
|
||||
}
|
||||
|
||||
private leafToString(value: any): undefined|string {
|
||||
if (typeof value === "boolean") {
|
||||
return "" + value;
|
||||
}
|
||||
if (typeof value === "number") {
|
||||
return "" + value;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
if (Array.isArray(value) && value.length === 0) {
|
||||
return "[]";
|
||||
}
|
||||
if (typeof value === "object" && Object.keys(value).length === 0) {
|
||||
return "{}";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export {PropertiesTreeGenerator};
|
||||
@@ -13,14 +13,15 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { TraceType } from "common/trace/trace_type";
|
||||
import { Viewer } from "./viewer";
|
||||
import { ViewerInputMethodClients } from "./viewer_input_method_clients/viewer_input_method_clients";
|
||||
import { ViewerInputMethodService } from "./viewer_input_method_service/viewer_input_method_service";
|
||||
import { ViewerInputMethodManagerService } from "./viewer_input_method_manager_service/viewer_input_method_manager_service";
|
||||
import { ViewerProtoLog } from "./viewer_protolog/viewer_protolog";
|
||||
import { ViewerSurfaceFlinger } from "./viewer_surface_flinger/viewer_surface_flinger";
|
||||
import { ViewerWindowManager } from "./viewer_window_manager/viewer_window_manager";
|
||||
import {TraceType} from "common/trace/trace_type";
|
||||
import {Viewer} from "./viewer";
|
||||
import {ViewerInputMethodClients} from "./viewer_input_method_clients/viewer_input_method_clients";
|
||||
import {ViewerInputMethodService} from "./viewer_input_method_service/viewer_input_method_service";
|
||||
import {ViewerInputMethodManagerService} from "./viewer_input_method_manager_service/viewer_input_method_manager_service";
|
||||
import {ViewerProtoLog} from "./viewer_protolog/viewer_protolog";
|
||||
import {ViewerSurfaceFlinger} from "./viewer_surface_flinger/viewer_surface_flinger";
|
||||
import {ViewerWindowManager} from "./viewer_window_manager/viewer_window_manager";
|
||||
import {ViewerTransactions} from "./viewer_transactions/viewer_transactions";
|
||||
|
||||
class ViewerFactory {
|
||||
static readonly VIEWERS = [
|
||||
@@ -29,6 +30,7 @@ class ViewerFactory {
|
||||
ViewerInputMethodService,
|
||||
ViewerProtoLog,
|
||||
ViewerSurfaceFlinger,
|
||||
ViewerTransactions,
|
||||
ViewerWindowManager,
|
||||
];
|
||||
|
||||
@@ -49,4 +51,4 @@ class ViewerFactory {
|
||||
}
|
||||
}
|
||||
|
||||
export { ViewerFactory };
|
||||
export {ViewerFactory};
|
||||
|
||||
24
tools/winscope-ng/src/viewers/viewer_transactions/events.ts
Normal file
24
tools/winscope-ng/src/viewers/viewer_transactions/events.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 Events {
|
||||
public static PidFilterChanged = "ViewerTransactionsEvent_PidFilterChanged";
|
||||
public static UidFilterChanged = "ViewerTransactionsEvent_UidFilterChanged";
|
||||
public static TypeFilterChanged = "ViewerTransactionsEvent_TypeFilterChanged";
|
||||
public static IdFilterChanged = "ViewerTransactionsEvent_IdFilterChanged";
|
||||
public static EntryClicked = "ViewerTransactionsEvent_EntryClicked";
|
||||
}
|
||||
|
||||
export {Events};
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* 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 ANYf KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {Timestamp, TimestampType} from "common/trace/timestamp";
|
||||
import {TraceType} from "common/trace/trace_type";
|
||||
import {Parser} from "parsers/parser";
|
||||
import {Presenter} from "./presenter";
|
||||
import {UnitTestUtils} from "test/unit/utils";
|
||||
import {UiData, UiDataEntryType} from "./ui_data";
|
||||
import {TransactionsTraceEntry} from "../../common/trace/transactions";
|
||||
|
||||
describe("ViewerTransactionsPresenter", () => {
|
||||
let parser: Parser;
|
||||
let presenter: Presenter;
|
||||
let inputTraceEntry: TransactionsTraceEntry;
|
||||
let inputTraceEntries: Map<TraceType, any>;
|
||||
let outputUiData: undefined|UiData;
|
||||
const TOTAL_OUTPUT_ENTRIES = 1504;
|
||||
|
||||
beforeAll(async () => {
|
||||
parser = await UnitTestUtils.getParser("traces/elapsed_and_real_timestamp/Transactions.pb");
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const timestamp = new Timestamp(TimestampType.ELAPSED, 2450981445n);
|
||||
inputTraceEntry = parser.getTraceEntry(timestamp)!;
|
||||
inputTraceEntries = new Map<TraceType, any>();
|
||||
inputTraceEntries.set(TraceType.TRANSACTIONS, [inputTraceEntry]);
|
||||
outputUiData = undefined;
|
||||
|
||||
presenter = new Presenter((data: UiData) => {
|
||||
outputUiData = data;
|
||||
});
|
||||
});
|
||||
|
||||
it("is robust to undefined trace entry", () => {
|
||||
inputTraceEntries = new Map<TraceType, any>();
|
||||
presenter.notifyCurrentTraceEntries(inputTraceEntries);
|
||||
expect(outputUiData).toEqual(UiData.EMPTY);
|
||||
});
|
||||
|
||||
it("processes trace entry and computes output UI data", () => {
|
||||
presenter.notifyCurrentTraceEntries(inputTraceEntries);
|
||||
|
||||
expect(outputUiData!.allPids).toEqual(["N/A", "0", "515", "1593", "2022", "2322", "2463", "3300"]);
|
||||
expect(outputUiData!.allUids).toEqual(["N/A", "1000", "1003", "10169", "10235", "10239"]);
|
||||
expect(outputUiData!.allTypes).toEqual(["DISPLAY_CHANGED", "LAYER_ADDED", "LAYER_CHANGED", "LAYER_HANDLE_REMOVED", "LAYER_REMOVED"]);
|
||||
expect(outputUiData!.allIds.length).toEqual(115);
|
||||
|
||||
expect(outputUiData!.entries.length).toEqual(TOTAL_OUTPUT_ENTRIES);
|
||||
|
||||
expect(outputUiData?.currentEntryIndex).toEqual(0);
|
||||
expect(outputUiData?.selectedEntryIndex).toBeUndefined();
|
||||
expect(outputUiData?.scrollToIndex).toEqual(0);
|
||||
expect(outputUiData?.currentPropertiesTree).toBeDefined();
|
||||
});
|
||||
|
||||
it("ignores undefined trace entry and doesn't discard previously computed UI data", () => {
|
||||
presenter.notifyCurrentTraceEntries(inputTraceEntries);
|
||||
expect(outputUiData!.entries.length).toEqual(TOTAL_OUTPUT_ENTRIES);
|
||||
|
||||
presenter.notifyCurrentTraceEntries(new Map<TraceType, any>());
|
||||
expect(outputUiData!.entries.length).toEqual(TOTAL_OUTPUT_ENTRIES);
|
||||
});
|
||||
|
||||
it("processes trace entry and updates current entry and scroll position", () => {
|
||||
presenter.notifyCurrentTraceEntries(inputTraceEntries);
|
||||
expect(outputUiData!.currentEntryIndex).toEqual(0);
|
||||
expect(outputUiData!.scrollToIndex).toEqual(0);
|
||||
|
||||
(<TransactionsTraceEntry>inputTraceEntries.get(TraceType.TRANSACTIONS)[0]).currentEntryIndex = 10;
|
||||
presenter.notifyCurrentTraceEntries(inputTraceEntries);
|
||||
expect(outputUiData!.currentEntryIndex).toEqual(13);
|
||||
expect(outputUiData!.scrollToIndex).toEqual(13);
|
||||
});
|
||||
|
||||
it("filters entries according to PID filter", () => {
|
||||
presenter.notifyCurrentTraceEntries(inputTraceEntries);
|
||||
|
||||
presenter.onPidFilterChanged([]);
|
||||
expect(new Set(outputUiData!.entries.map(entry => entry.pid)))
|
||||
.toEqual(new Set(["N/A", "0", "515", "1593", "2022", "2322", "2463", "3300"]));
|
||||
|
||||
presenter.onPidFilterChanged(["0"]);
|
||||
expect(new Set(outputUiData!.entries.map(entry => entry.pid)))
|
||||
.toEqual(new Set(["0"]));
|
||||
|
||||
presenter.onPidFilterChanged(["0", "515"]);
|
||||
expect(new Set(outputUiData!.entries.map(entry => entry.pid)))
|
||||
.toEqual(new Set(["0", "515"]));
|
||||
});
|
||||
|
||||
it("filters entries according to UID filter", () => {
|
||||
presenter.notifyCurrentTraceEntries(inputTraceEntries);
|
||||
|
||||
presenter.onUidFilterChanged([]);
|
||||
expect(new Set(outputUiData!.entries.map(entry => entry.uid)))
|
||||
.toEqual(new Set(["N/A", "1000", "1003", "10169", "10235", "10239"]));
|
||||
|
||||
presenter.onUidFilterChanged(["1000"]);
|
||||
expect(new Set(outputUiData!.entries.map(entry => entry.uid)))
|
||||
.toEqual(new Set(["1000"]));
|
||||
|
||||
presenter.onUidFilterChanged(["1000", "1003"]);
|
||||
expect(new Set(outputUiData!.entries.map(entry => entry.uid)))
|
||||
.toEqual(new Set(["1000", "1003"]));
|
||||
});
|
||||
|
||||
it("filters entries according to type filter", () => {
|
||||
presenter.notifyCurrentTraceEntries(inputTraceEntries);
|
||||
|
||||
presenter.onTypeFilterChanged([]);
|
||||
expect(new Set(outputUiData!.entries.map(entry => entry.type)))
|
||||
.toEqual(new Set([
|
||||
UiDataEntryType.DisplayChanged,
|
||||
UiDataEntryType.LayerAdded,
|
||||
UiDataEntryType.LayerChanged,
|
||||
UiDataEntryType.LayerRemoved,
|
||||
UiDataEntryType.LayerHandleRemoved
|
||||
]));
|
||||
|
||||
presenter.onTypeFilterChanged([UiDataEntryType.LayerAdded]);
|
||||
expect(new Set(outputUiData!.entries.map(entry => entry.type)))
|
||||
.toEqual(new Set([UiDataEntryType.LayerAdded]));
|
||||
|
||||
presenter.onTypeFilterChanged([UiDataEntryType.LayerAdded, UiDataEntryType.LayerRemoved]);
|
||||
expect(new Set(outputUiData!.entries.map(entry => entry.type)))
|
||||
.toEqual(new Set([UiDataEntryType.LayerAdded, UiDataEntryType.LayerRemoved]));
|
||||
});
|
||||
|
||||
it("filters entries according to ID filter", () => {
|
||||
presenter.notifyCurrentTraceEntries(inputTraceEntries);
|
||||
|
||||
presenter.onIdFilterChanged([]);
|
||||
expect(new Set(outputUiData!.entries.map(entry => entry.id)).size)
|
||||
.toBeGreaterThan(20);
|
||||
|
||||
presenter.onIdFilterChanged(["1"]);
|
||||
expect(new Set(outputUiData!.entries.map(entry => entry.id)))
|
||||
.toEqual(new Set(["1"]));
|
||||
|
||||
presenter.onIdFilterChanged(["1", "3"]);
|
||||
expect(new Set(outputUiData!.entries.map(entry => entry.id)))
|
||||
.toEqual(new Set(["1", "3"]));
|
||||
});
|
||||
|
||||
it ("updates selected entry and properties tree when entry is clicked", () => {
|
||||
presenter.notifyCurrentTraceEntries(inputTraceEntries);
|
||||
expect(outputUiData!.currentEntryIndex).toEqual(0);
|
||||
expect(outputUiData!.selectedEntryIndex).toBeUndefined();
|
||||
expect(outputUiData!.scrollToIndex).toEqual(0);
|
||||
expect(outputUiData!.currentPropertiesTree)
|
||||
.toEqual(outputUiData!.entries[0].propertiesTree);
|
||||
|
||||
presenter.onEntryClicked(10);
|
||||
expect(outputUiData!.currentEntryIndex).toEqual(0);
|
||||
expect(outputUiData!.selectedEntryIndex).toEqual(10);
|
||||
expect(outputUiData!.scrollToIndex).toBeUndefined(); // no scrolling
|
||||
expect(outputUiData!.currentPropertiesTree)
|
||||
.toEqual(outputUiData!.entries[10].propertiesTree);
|
||||
|
||||
// remove selection when selected entry is clicked again
|
||||
presenter.onEntryClicked(10);
|
||||
expect(outputUiData!.currentEntryIndex).toEqual(0);
|
||||
expect(outputUiData!.selectedEntryIndex).toBeUndefined();
|
||||
expect(outputUiData!.scrollToIndex).toBeUndefined(); // no scrolling
|
||||
expect(outputUiData!.currentPropertiesTree)
|
||||
.toEqual(outputUiData!.entries[0].propertiesTree);
|
||||
});
|
||||
|
||||
it("computes current entry index", () => {
|
||||
presenter.notifyCurrentTraceEntries(inputTraceEntries);
|
||||
expect(outputUiData!.currentEntryIndex).toEqual(0);
|
||||
|
||||
(<TransactionsTraceEntry>inputTraceEntries.get(TraceType.TRANSACTIONS)[0]).currentEntryIndex = 10;
|
||||
presenter.notifyCurrentTraceEntries(inputTraceEntries);
|
||||
expect(outputUiData!.currentEntryIndex).toEqual(13);
|
||||
});
|
||||
|
||||
it("updates current entry index when filters change", () => {
|
||||
(<TransactionsTraceEntry>inputTraceEntries.get(TraceType.TRANSACTIONS)[0]).currentEntryIndex = 10;
|
||||
presenter.notifyCurrentTraceEntries(inputTraceEntries);
|
||||
|
||||
presenter.onPidFilterChanged([]);
|
||||
expect(outputUiData!.currentEntryIndex).toEqual(13);
|
||||
|
||||
presenter.onPidFilterChanged(["0"]);
|
||||
expect(outputUiData!.currentEntryIndex).toEqual(10);
|
||||
|
||||
presenter.onPidFilterChanged(["0", "515"]);
|
||||
expect(outputUiData!.currentEntryIndex).toEqual(11);
|
||||
|
||||
presenter.onPidFilterChanged(["0", "515", "N/A"]);
|
||||
expect(outputUiData!.currentEntryIndex).toEqual(13);
|
||||
});
|
||||
});
|
||||
320
tools/winscope-ng/src/viewers/viewer_transactions/presenter.ts
Normal file
320
tools/winscope-ng/src/viewers/viewer_transactions/presenter.ts
Normal file
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
* 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 {UiData, UiDataEntry, UiDataEntryType} from "./ui_data";
|
||||
import {ArrayUtils} from "common/utils/array_utils";
|
||||
import {TraceType} from "common/trace/trace_type";
|
||||
import {TransactionsTraceEntry} from "common/trace/transactions";
|
||||
import {PropertiesTreeGenerator} from "viewers/common/properties_tree_generator";
|
||||
import {PropertiesTreeNode} from "viewers/common/ui_tree_utils";
|
||||
import {StringUtils} from "common/utils/string_utils";
|
||||
|
||||
class Presenter {
|
||||
constructor(notifyUiDataCallback: (data: UiData) => void) {
|
||||
this.notifyUiDataCallback = notifyUiDataCallback;
|
||||
this.originalIndicesOfUiDataEntries = [];
|
||||
this.uiData = UiData.EMPTY;
|
||||
this.notifyUiDataCallback(this.uiData);
|
||||
}
|
||||
|
||||
//TODO: replace input with something like iterator/cursor (same for other viewers/presenters)
|
||||
public notifyCurrentTraceEntries(entries: Map<TraceType, any>): void {
|
||||
this.entry = entries.get(TraceType.TRANSACTIONS) ? entries.get(TraceType.TRANSACTIONS)[0] : undefined;
|
||||
if (this.uiData === UiData.EMPTY) {
|
||||
this.computeUiData();
|
||||
}
|
||||
else {
|
||||
// update only "position" data
|
||||
this.uiData.currentEntryIndex = this.computeCurrentEntryIndex();
|
||||
this.uiData.selectedEntryIndex = undefined;
|
||||
this.uiData.scrollToIndex = this.uiData.currentEntryIndex;
|
||||
this.uiData.currentPropertiesTree = this.computeCurrentPropertiesTree(
|
||||
this.uiData.entries,
|
||||
this.uiData.currentEntryIndex,
|
||||
this.uiData.selectedEntryIndex);
|
||||
}
|
||||
this.notifyUiDataCallback(this.uiData);
|
||||
}
|
||||
|
||||
public onPidFilterChanged(pids: string[]) {
|
||||
this.pidFilter = pids;
|
||||
this.computeUiData();
|
||||
this.notifyUiDataCallback(this.uiData);
|
||||
}
|
||||
|
||||
public onUidFilterChanged(uids: string[]) {
|
||||
this.uidFilter = uids;
|
||||
this.computeUiData();
|
||||
this.notifyUiDataCallback(this.uiData);
|
||||
}
|
||||
|
||||
public onTypeFilterChanged(types: string[]) {
|
||||
this.typeFilter = types;
|
||||
this.computeUiData();
|
||||
this.notifyUiDataCallback(this.uiData);
|
||||
}
|
||||
|
||||
public onIdFilterChanged(ids: string[]) {
|
||||
this.idFilter = ids;
|
||||
this.computeUiData();
|
||||
this.notifyUiDataCallback(this.uiData);
|
||||
}
|
||||
|
||||
public onEntryClicked(index: number) {
|
||||
if (this.uiData.selectedEntryIndex === index) {
|
||||
this.uiData.selectedEntryIndex = undefined; // remove selection when clicked again
|
||||
}
|
||||
else {
|
||||
this.uiData.selectedEntryIndex = index;
|
||||
}
|
||||
|
||||
this.uiData.scrollToIndex = undefined; // no scrolling
|
||||
|
||||
this.uiData.currentPropertiesTree = this.computeCurrentPropertiesTree(
|
||||
this.uiData.entries,
|
||||
this.uiData.currentEntryIndex,
|
||||
this.uiData.selectedEntryIndex);
|
||||
|
||||
this.notifyUiDataCallback(this.uiData);
|
||||
}
|
||||
|
||||
private computeUiData() {
|
||||
if (!this.entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = this.makeUiDataEntries(this.entry!.entriesProto);
|
||||
|
||||
const allPids = this.getUniqueUiDataEntryValues(entries, (entry: UiDataEntry) => entry.pid);
|
||||
const allUids = this.getUniqueUiDataEntryValues(entries, (entry: UiDataEntry) => entry.uid);
|
||||
const allTypes = this.getUniqueUiDataEntryValues(entries, (entry: UiDataEntry) => entry.type);
|
||||
const allIds = this.getUniqueUiDataEntryValues(entries, (entry: UiDataEntry) => entry.id);
|
||||
|
||||
let filteredEntries = entries;
|
||||
|
||||
if (this.pidFilter.length > 0) {
|
||||
filteredEntries =
|
||||
filteredEntries.filter(entry => this.pidFilter.includes(entry.pid));
|
||||
}
|
||||
|
||||
if (this.uidFilter.length > 0) {
|
||||
filteredEntries =
|
||||
filteredEntries.filter(entry => this.uidFilter.includes(entry.uid));
|
||||
}
|
||||
|
||||
if (this.typeFilter.length > 0) {
|
||||
filteredEntries =
|
||||
filteredEntries.filter(entry => this.typeFilter.includes(entry.type));
|
||||
}
|
||||
|
||||
if (this.idFilter.length > 0) {
|
||||
filteredEntries =
|
||||
filteredEntries.filter(entry => this.idFilter.includes(entry.id));
|
||||
}
|
||||
|
||||
this.originalIndicesOfUiDataEntries = filteredEntries.map(entry => entry.originalIndexInTraceEntry);
|
||||
|
||||
const currentEntryIndex = this.computeCurrentEntryIndex();
|
||||
const selectedEntryIndex = undefined;
|
||||
const currentPropertiesTree = this.computeCurrentPropertiesTree(filteredEntries, currentEntryIndex, selectedEntryIndex);
|
||||
|
||||
this.uiData = new UiData(
|
||||
allPids,
|
||||
allUids,
|
||||
allTypes,
|
||||
allIds,
|
||||
filteredEntries,
|
||||
currentEntryIndex,
|
||||
selectedEntryIndex,
|
||||
currentEntryIndex,
|
||||
currentPropertiesTree);
|
||||
}
|
||||
|
||||
private computeCurrentEntryIndex(): undefined|number {
|
||||
if (!this.entry) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return ArrayUtils.binarySearchLowerOrEqual(
|
||||
this.originalIndicesOfUiDataEntries,
|
||||
this.entry.currentEntryIndex
|
||||
);
|
||||
}
|
||||
|
||||
private computeCurrentPropertiesTree(
|
||||
entries: UiDataEntry[],
|
||||
currentEntryIndex: undefined|number,
|
||||
selectedEntryIndex: undefined|number): undefined|PropertiesTreeNode {
|
||||
if (selectedEntryIndex !== undefined) {
|
||||
return entries[selectedEntryIndex].propertiesTree;
|
||||
}
|
||||
if (currentEntryIndex !== undefined) {
|
||||
return entries[currentEntryIndex].propertiesTree;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private makeUiDataEntries(entriesProto: any[]): UiDataEntry[] {
|
||||
const treeGenerator = new PropertiesTreeGenerator();
|
||||
|
||||
const entries: UiDataEntry[] = [];
|
||||
|
||||
for (const [originalIndex, entryProto] of entriesProto.entries()) {
|
||||
for (const transactionStateProto of entryProto.transactions) {
|
||||
for (const layerStateProto of transactionStateProto.layerChanges) {
|
||||
entries.push(new UiDataEntry(
|
||||
originalIndex,
|
||||
StringUtils.nanosecondsToHuman(Number(entryProto.elapsedRealtimeNanos)),
|
||||
Number(entryProto.vsyncId),
|
||||
transactionStateProto.pid.toString(),
|
||||
transactionStateProto.uid.toString(),
|
||||
UiDataEntryType.LayerChanged,
|
||||
layerStateProto.layerId.toString(),
|
||||
treeGenerator.generate("LayerState", layerStateProto)
|
||||
));
|
||||
}
|
||||
|
||||
for (const displayStateProto of transactionStateProto.displayChanges) {
|
||||
entries.push(new UiDataEntry(
|
||||
originalIndex,
|
||||
StringUtils.nanosecondsToHuman(Number(entryProto.elapsedRealtimeNanos)),
|
||||
Number(entryProto.vsyncId),
|
||||
transactionStateProto.pid.toString(),
|
||||
transactionStateProto.uid.toString(),
|
||||
UiDataEntryType.DisplayChanged,
|
||||
displayStateProto.id.toString(),
|
||||
treeGenerator.generate("DisplayState", displayStateProto)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
for (const layerCreationArgsProto of entryProto.addedLayers) {
|
||||
entries.push(new UiDataEntry(
|
||||
originalIndex,
|
||||
StringUtils.nanosecondsToHuman(Number(entryProto.elapsedRealtimeNanos)),
|
||||
Number(entryProto.vsyncId),
|
||||
Presenter.VALUE_NA,
|
||||
Presenter.VALUE_NA,
|
||||
UiDataEntryType.LayerAdded,
|
||||
layerCreationArgsProto.layerId.toString(),
|
||||
treeGenerator.generate("LayerCreationArgs", layerCreationArgsProto)
|
||||
));
|
||||
}
|
||||
|
||||
for (const removedLayerId of entryProto.removedLayers) {
|
||||
entries.push(new UiDataEntry(
|
||||
originalIndex,
|
||||
StringUtils.nanosecondsToHuman(Number(entryProto.elapsedRealtimeNanos)),
|
||||
Number(entryProto.vsyncId),
|
||||
Presenter.VALUE_NA,
|
||||
Presenter.VALUE_NA,
|
||||
UiDataEntryType.LayerRemoved,
|
||||
removedLayerId.toString(),
|
||||
treeGenerator.generate("RemovedLayerId", removedLayerId)
|
||||
));
|
||||
}
|
||||
|
||||
for (const displayStateProto of entryProto.addedDisplays) {
|
||||
entries.push(new UiDataEntry(
|
||||
originalIndex,
|
||||
StringUtils.nanosecondsToHuman(Number(entryProto.elapsedRealtimeNanos)),
|
||||
Number(entryProto.vsyncId),
|
||||
Presenter.VALUE_NA,
|
||||
Presenter.VALUE_NA,
|
||||
UiDataEntryType.DisplayAdded,
|
||||
displayStateProto.id.toString(),
|
||||
treeGenerator.generate("DisplayState", displayStateProto)
|
||||
));
|
||||
}
|
||||
|
||||
for (const removedDisplayId of entryProto.removedDisplays) {
|
||||
entries.push(new UiDataEntry(
|
||||
originalIndex,
|
||||
StringUtils.nanosecondsToHuman(Number(entryProto.elapsedRealtimeNanos)),
|
||||
Number(entryProto.vsyncId),
|
||||
Presenter.VALUE_NA,
|
||||
Presenter.VALUE_NA,
|
||||
UiDataEntryType.DisplayRemoved,
|
||||
removedDisplayId.toString(),
|
||||
treeGenerator.generate("RemovedDisplayId", removedDisplayId)
|
||||
));
|
||||
}
|
||||
|
||||
for (const removedLayerHandleId of entryProto.removedLayerHandles) {
|
||||
entries.push(new UiDataEntry(
|
||||
originalIndex,
|
||||
StringUtils.nanosecondsToHuman(Number(entryProto.elapsedRealtimeNanos)),
|
||||
Number(entryProto.vsyncId),
|
||||
Presenter.VALUE_NA,
|
||||
Presenter.VALUE_NA,
|
||||
UiDataEntryType.LayerHandleRemoved,
|
||||
removedLayerHandleId.toString(),
|
||||
treeGenerator.generate("RemovedLayerHandleId", removedLayerHandleId)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private getUniqueUiDataEntryValues(entries: UiDataEntry[], getValue: (entry: UiDataEntry) => string): string[] {
|
||||
const uniqueValues = new Set<string>();
|
||||
entries.forEach((entry: UiDataEntry) => {
|
||||
uniqueValues.add(getValue(entry));
|
||||
});
|
||||
|
||||
const result = [...uniqueValues];
|
||||
|
||||
result.sort((a, b) => {
|
||||
const aIsNumber = !isNaN(Number(a));
|
||||
const bIsNumber = !isNaN(Number(b));
|
||||
|
||||
if (aIsNumber && bIsNumber) {
|
||||
return Number(a) - Number(b);
|
||||
}
|
||||
else if (aIsNumber) {
|
||||
return 1; // place number after strings in the result
|
||||
}
|
||||
else if (bIsNumber) {
|
||||
return -1; // place number after strings in the result
|
||||
}
|
||||
|
||||
// a and b are both strings
|
||||
if (a < b) {
|
||||
return -1;
|
||||
}
|
||||
else if (a > b) {
|
||||
return 1;
|
||||
}
|
||||
else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private entry?: TransactionsTraceEntry;
|
||||
private originalIndicesOfUiDataEntries: number[];
|
||||
private uiData: UiData;
|
||||
private readonly notifyUiDataCallback: (data: UiData) => void;
|
||||
private static readonly VALUE_NA = "N/A";
|
||||
private pidFilter: string[] = [];
|
||||
private uidFilter: string[] = [];
|
||||
private typeFilter: string[] = [];
|
||||
private idFilter: string[] = [];
|
||||
}
|
||||
|
||||
export {Presenter};
|
||||
67
tools/winscope-ng/src/viewers/viewer_transactions/ui_data.ts
Normal file
67
tools/winscope-ng/src/viewers/viewer_transactions/ui_data.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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 {PropertiesTreeNode} from "viewers/common/ui_tree_utils";
|
||||
|
||||
class UiData {
|
||||
constructor(
|
||||
public allPids: string[],
|
||||
public allUids: string[],
|
||||
public allTypes: string[],
|
||||
public allIds: string[],
|
||||
public entries: UiDataEntry[],
|
||||
public currentEntryIndex: undefined|number,
|
||||
public selectedEntryIndex: undefined|number,
|
||||
public scrollToIndex: undefined|number,
|
||||
public currentPropertiesTree: undefined|PropertiesTreeNode) {
|
||||
}
|
||||
|
||||
public static EMPTY = new UiData(
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined);
|
||||
}
|
||||
|
||||
class UiDataEntry {
|
||||
constructor(
|
||||
public originalIndexInTraceEntry: number,
|
||||
public time: string,
|
||||
public vsyncId: number,
|
||||
public pid: string,
|
||||
public uid: string,
|
||||
public type: string,
|
||||
public id: string,
|
||||
public propertiesTree?: PropertiesTreeNode
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
class UiDataEntryType {
|
||||
public static DisplayAdded = "DISPLAY_ADDED";
|
||||
public static DisplayRemoved = "DISPLAY_REMOVED";
|
||||
public static DisplayChanged = "DISPLAY_CHANGED";
|
||||
public static LayerAdded = "LAYER_ADDED";
|
||||
public static LayerRemoved = "LAYER_REMOVED";
|
||||
public static LayerChanged = "LAYER_CHANGED";
|
||||
public static LayerHandleRemoved = "LAYER_HANDLE_REMOVED";
|
||||
}
|
||||
|
||||
export {UiData, UiDataEntry, UiDataEntryType};
|
||||
@@ -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 {ScrollingModule} from "@angular/cdk/scrolling";
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from "@angular/core";
|
||||
import {ComponentFixture, ComponentFixtureAutoDetect, TestBed} from "@angular/core/testing";
|
||||
import {UiData, UiDataEntry} from "./ui_data";
|
||||
import {PropertiesTreeGenerator} from "viewers/common/properties_tree_generator";
|
||||
import {ViewerTransactionsComponent} from "./viewer_transactions.component";
|
||||
|
||||
describe("ViewerTransactionsComponent", () => {
|
||||
let fixture: ComponentFixture<ViewerTransactionsComponent>;
|
||||
let component: ViewerTransactionsComponent;
|
||||
let htmlElement: HTMLElement;
|
||||
|
||||
beforeAll(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: ComponentFixtureAutoDetect, useValue: true }
|
||||
],
|
||||
imports: [
|
||||
ScrollingModule
|
||||
],
|
||||
declarations: [
|
||||
ViewerTransactionsComponent,
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ViewerTransactionsComponent);
|
||||
component = fixture.componentInstance;
|
||||
htmlElement = fixture.nativeElement;
|
||||
|
||||
component.uiData = await makeUiData();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("can be created", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders filters", () => {
|
||||
expect(htmlElement.querySelector(".entries .filters .pid")).toBeTruthy();
|
||||
expect(htmlElement.querySelector(".entries .filters .uid")).toBeTruthy();
|
||||
expect(htmlElement.querySelector(".entries .filters .type")).toBeTruthy();
|
||||
expect(htmlElement.querySelector(".entries .filters .id")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders entries", () => {
|
||||
expect(htmlElement.querySelector(".scroll")).toBeTruthy();
|
||||
|
||||
const entry = htmlElement.querySelector(".scroll .entry");
|
||||
expect(entry).toBeTruthy();
|
||||
expect(entry!.innerHTML).toContain("TIME_VALUE");
|
||||
expect(entry!.innerHTML).toContain("-111");
|
||||
expect(entry!.innerHTML).toContain("PID_VALUE");
|
||||
expect(entry!.innerHTML).toContain("UID_VALUE");
|
||||
expect(entry!.innerHTML).toContain("TYPE_VALUE");
|
||||
expect(entry!.innerHTML).toContain("ID_VALUE");
|
||||
});
|
||||
|
||||
it("renders properties", () => {
|
||||
expect(htmlElement.querySelector((".properties-tree"))).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
async function makeUiData(): Promise<UiData> {
|
||||
const propertiesTree = new PropertiesTreeGenerator().generate("ROOT", {"KEY": "VALUE"});
|
||||
|
||||
const entry = new UiDataEntry(
|
||||
0,
|
||||
"TIME_VALUE",
|
||||
-111,
|
||||
"PID_VALUE",
|
||||
"UID_VALUE",
|
||||
"TYPE_VALUE",
|
||||
"ID_VALUE",
|
||||
propertiesTree);
|
||||
|
||||
return new UiData(
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[entry],
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
propertiesTree);
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* 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 {CdkVirtualScrollViewport} from "@angular/cdk/scrolling";
|
||||
import {
|
||||
Component, ElementRef, Inject, Input, ViewChild
|
||||
} from "@angular/core";
|
||||
import {MatSelectChange} from "@angular/material/select";
|
||||
import {Events} from "./events";
|
||||
import {UiData} from "./ui_data";
|
||||
|
||||
@Component({
|
||||
selector: "viewer-transactions",
|
||||
template: `
|
||||
<div class="card-grid">
|
||||
<div class="entries">
|
||||
<div class="filters">
|
||||
<div class="time">
|
||||
</div>
|
||||
<div class="vsyncid" style="display:table">
|
||||
<span class="mat-body-1"
|
||||
style="display:table-cell; vertical-align:middle">VSYNC ID</span>
|
||||
</div>
|
||||
<div class="pid">
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-label>PID</mat-label>
|
||||
<mat-select (selectionChange)="onPidFilterChanged($event)"
|
||||
multiple>
|
||||
<mat-option *ngFor="let pid of uiData.allPids"
|
||||
[value]="pid">
|
||||
{{pid}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="uid">
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-label>UID</mat-label>
|
||||
<mat-select (selectionChange)="onUidFilterChanged($event)"
|
||||
multiple>
|
||||
<mat-option *ngFor="let uid of uiData.allUids"
|
||||
[value]="uid">
|
||||
{{uid}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="type">
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-label>Type</mat-label>
|
||||
<mat-select (selectionChange)="onTypeFilterChanged($event)"
|
||||
multiple>
|
||||
<mat-option *ngFor="let type of uiData.allTypes"
|
||||
[value]="type">
|
||||
{{type}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="id">
|
||||
<mat-form-field appearance="fill" style="width: 200px;">
|
||||
<mat-label>LAYER/DISPLAY ID</mat-label>
|
||||
<mat-select (selectionChange)="onIdFilterChanged($event)"
|
||||
multiple>
|
||||
<mat-option *ngFor="let id of uiData.allIds"
|
||||
[value]="id">
|
||||
{{id}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<cdk-virtual-scroll-viewport itemSize="24" class="scroll">
|
||||
<div *cdkVirtualFor="let entry of uiData.entries; let i = index;"
|
||||
class="entry"
|
||||
[class.current-entry]="isCurrentEntry(i)"
|
||||
[class.selected-entry]="isSelectedEntry(i)"
|
||||
(click)="onEntryClicked(i)">
|
||||
<div class="time">
|
||||
<span class="mat-body-1">{{entry.time}}</span>
|
||||
</div>
|
||||
<div class="vsyncid">
|
||||
<span class="mat-body-1">{{entry.vsyncId}}</span>
|
||||
</div>
|
||||
<div class="pid">
|
||||
<span class="mat-body-1">{{entry.pid}}</span>
|
||||
</div>
|
||||
<div class="uid">
|
||||
<span class="mat-body-1">{{entry.uid}}</span>
|
||||
</div>
|
||||
<div class="type">
|
||||
<span class="mat-body-1">{{entry.type}}</span>
|
||||
</div>
|
||||
<div class="id">
|
||||
<span class="mat-body-1">{{entry.id}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
</div>
|
||||
|
||||
<div class="container-properties">
|
||||
<h3 class="properties-title mat-title">Properties - Proto Dump</h3>
|
||||
<tree-view
|
||||
class="properties-tree"
|
||||
[item]="uiData.currentPropertiesTree"
|
||||
></tree-view>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.entries {
|
||||
flex: 3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
border-top: 1px solid var(--default-border);
|
||||
border-right: 1px solid var(--default-border);
|
||||
}
|
||||
|
||||
.container-properties {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
border-top: 1px solid var(--default-border);
|
||||
}
|
||||
|
||||
.entries .filters {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.entries .scroll {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.scroll .entry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.filters div {
|
||||
flex: 1;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.entry div {
|
||||
flex: 1;
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
.entry.current-entry {
|
||||
color: white;
|
||||
background-color: #365179;
|
||||
}
|
||||
|
||||
.entry.selected-entry {
|
||||
color: white;
|
||||
background-color: #98aecd;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
width: 100px;
|
||||
}
|
||||
`,
|
||||
]
|
||||
})
|
||||
class ViewerTransactionsComponent {
|
||||
constructor(@Inject(ElementRef) elementRef: ElementRef) {
|
||||
this.elementRef = elementRef;
|
||||
}
|
||||
|
||||
@Input()
|
||||
public set inputData(data: UiData) {
|
||||
this.uiData = data;
|
||||
if (this.uiData.scrollToIndex !== undefined && this.scrollComponent) {
|
||||
this.scrollComponent.scrollToIndex(this.uiData.scrollToIndex);
|
||||
}
|
||||
}
|
||||
|
||||
public onPidFilterChanged(event: MatSelectChange) {
|
||||
this.emitEvent(Events.PidFilterChanged, event.value);
|
||||
}
|
||||
|
||||
public onUidFilterChanged(event: MatSelectChange) {
|
||||
this.emitEvent(Events.UidFilterChanged, event.value);
|
||||
}
|
||||
|
||||
public onTypeFilterChanged(event: MatSelectChange) {
|
||||
this.emitEvent(Events.TypeFilterChanged, event.value);
|
||||
}
|
||||
|
||||
public onIdFilterChanged(event: MatSelectChange) {
|
||||
this.emitEvent(Events.IdFilterChanged, event.value);
|
||||
}
|
||||
|
||||
public onEntryClicked(index: number) {
|
||||
this.emitEvent(Events.EntryClicked, index);
|
||||
}
|
||||
|
||||
public isCurrentEntry(index: number): boolean {
|
||||
return index === this.uiData.currentEntryIndex;
|
||||
}
|
||||
|
||||
public isSelectedEntry(index: number): boolean {
|
||||
return index === this.uiData.selectedEntryIndex;
|
||||
}
|
||||
|
||||
private emitEvent(event: string, data: any) {
|
||||
const customEvent = new CustomEvent(
|
||||
event,
|
||||
{
|
||||
bubbles: true,
|
||||
detail: data
|
||||
});
|
||||
this.elementRef.nativeElement.dispatchEvent(customEvent);
|
||||
}
|
||||
|
||||
@ViewChild(CdkVirtualScrollViewport) scrollComponent?: CdkVirtualScrollViewport;
|
||||
|
||||
public uiData: UiData = UiData.EMPTY;
|
||||
private elementRef: ElementRef;
|
||||
}
|
||||
|
||||
export {ViewerTransactionsComponent};
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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 {Events} from "./events";
|
||||
import {UiData} from "./ui_data";
|
||||
|
||||
class ViewerTransactions implements Viewer {
|
||||
constructor() {
|
||||
this.view = document.createElement("viewer-transactions");
|
||||
|
||||
this.presenter = new Presenter((data: UiData) => {
|
||||
(this.view as any).inputData = data;
|
||||
});
|
||||
|
||||
this.view.addEventListener(Events.PidFilterChanged, (event) => {
|
||||
this.presenter.onPidFilterChanged((event as CustomEvent).detail);
|
||||
});
|
||||
|
||||
this.view.addEventListener(Events.UidFilterChanged, (event) => {
|
||||
this.presenter.onUidFilterChanged((event as CustomEvent).detail);
|
||||
});
|
||||
|
||||
this.view.addEventListener(Events.TypeFilterChanged, (event) => {
|
||||
this.presenter.onTypeFilterChanged((event as CustomEvent).detail);
|
||||
});
|
||||
|
||||
this.view.addEventListener(Events.IdFilterChanged, (event) => {
|
||||
this.presenter.onIdFilterChanged((event as CustomEvent).detail);
|
||||
});
|
||||
|
||||
this.view.addEventListener(Events.EntryClicked, (event) => {
|
||||
this.presenter.onEntryClicked((event as CustomEvent).detail);
|
||||
});
|
||||
}
|
||||
|
||||
public notifyCurrentTraceEntries(entries: Map<TraceType, any>): void {
|
||||
this.presenter.notifyCurrentTraceEntries(entries);
|
||||
}
|
||||
|
||||
public getView(): HTMLElement {
|
||||
return this.view;
|
||||
}
|
||||
|
||||
public getTitle() {
|
||||
return "Transactions";
|
||||
}
|
||||
|
||||
public getDependencies(): TraceType[] {
|
||||
return ViewerTransactions.DEPENDENCIES;
|
||||
}
|
||||
|
||||
public static readonly DEPENDENCIES: TraceType[] = [TraceType.TRANSACTIONS];
|
||||
private view: HTMLElement;
|
||||
private presenter: Presenter;
|
||||
}
|
||||
|
||||
export {ViewerTransactions};
|
||||
Reference in New Issue
Block a user