From 81c2db9a67d45c884691ec3563ee3ff6440837d6 Mon Sep 17 00:00:00 2001 From: Kean Mariotti Date: Tue, 11 Oct 2022 06:40:31 +0000 Subject: [PATCH] Add viewer Transaction Fix: 238088678 Test: npm run build:all && npm run test:all Change-Id: I90ad36a0a18e3b68216ea18d2a47e3ff38d98412 --- tools/winscope-ng/src/app/app.module.ts | 2 + .../src/app/components/app.component.ts | 5 + .../src/common/trace/transactions.ts | 22 ++ tools/winscope-ng/src/parsers/parser.ts | 4 +- .../src/parsers/parser_transactions.spec.ts | 21 +- .../src/parsers/parser_transactions.ts | 61 +++- tools/winscope-ng/src/parsers/proto_types.js | 19 ++ .../src/test/e2e/viewer_transactions.spec.ts | 38 +++ .../common/properties_tree_generator.spec.ts | 162 +++++++++ .../common/properties_tree_generator.ts | 70 ++++ .../winscope-ng/src/viewers/viewer_factory.ts | 20 +- .../src/viewers/viewer_transactions/events.ts | 24 ++ .../viewer_transactions/presenter.spec.ts | 208 ++++++++++++ .../viewers/viewer_transactions/presenter.ts | 320 ++++++++++++++++++ .../viewers/viewer_transactions/ui_data.ts | 67 ++++ .../viewer_transactions.component.spec.ts | 102 ++++++ .../viewer_transactions.component.ts | 240 +++++++++++++ .../viewer_transactions.ts | 72 ++++ 18 files changed, 1438 insertions(+), 19 deletions(-) create mode 100644 tools/winscope-ng/src/common/trace/transactions.ts create mode 100644 tools/winscope-ng/src/test/e2e/viewer_transactions.spec.ts create mode 100644 tools/winscope-ng/src/viewers/common/properties_tree_generator.spec.ts create mode 100644 tools/winscope-ng/src/viewers/common/properties_tree_generator.ts create mode 100644 tools/winscope-ng/src/viewers/viewer_transactions/events.ts create mode 100644 tools/winscope-ng/src/viewers/viewer_transactions/presenter.spec.ts create mode 100644 tools/winscope-ng/src/viewers/viewer_transactions/presenter.ts create mode 100644 tools/winscope-ng/src/viewers/viewer_transactions/ui_data.ts create mode 100644 tools/winscope-ng/src/viewers/viewer_transactions/viewer_transactions.component.spec.ts create mode 100644 tools/winscope-ng/src/viewers/viewer_transactions/viewer_transactions.component.ts create mode 100644 tools/winscope-ng/src/viewers/viewer_transactions/viewer_transactions.ts diff --git a/tools/winscope-ng/src/app/app.module.ts b/tools/winscope-ng/src/app/app.module.ts index 6ed397ac3..076e644e5 100644 --- a/tools/winscope-ng/src/app/app.module.ts +++ b/tools/winscope-ng/src/app/app.module.ts @@ -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, diff --git a/tools/winscope-ng/src/app/components/app.component.ts b/tools/winscope-ng/src/app/components/app.component.ts index 3952c2b41..43505289e 100644 --- a/tools/winscope-ng/src/app/components/app.component.ts +++ b/tools/winscope-ng/src/app/components/app.component.ts @@ -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})); diff --git a/tools/winscope-ng/src/common/trace/transactions.ts b/tools/winscope-ng/src/common/trace/transactions.ts new file mode 100644 index 000000000..c984077b0 --- /dev/null +++ b/tools/winscope-ng/src/common/trace/transactions.ts @@ -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}; diff --git a/tools/winscope-ng/src/parsers/parser.ts b/tools/winscope-ng/src/parsers/parser.ts index a014dd66f..596b497aa 100644 --- a/tools/winscope-ng/src/parsers/parser.ts +++ b/tools/winscope-ng/src/parsers/parser.ts @@ -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) { diff --git a/tools/winscope-ng/src/parsers/parser_transactions.spec.ts b/tools/winscope-ng/src/parsers/parser_transactions.spec.ts index 64a3efb25..183bbcd00 100644 --- a/tools/winscope-ng/src/parsers/parser_transactions.spec.ts +++ b/tools/winscope-ng/src/parsers/parser_transactions.spec.ts @@ -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", () => { diff --git a/tools/winscope-ng/src/parsers/parser_transactions.ts b/tools/winscope-ng/src/parsers/parser_transactions.ts index 3f9f9c9d4..f464f77f1 100644 --- a/tools/winscope-ng/src/parsers/parser_transactions.ts +++ b/tools/winscope-ng/src/parsers/parser_transactions.ts @@ -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 = TransactionsTraceFileProto.decode(buffer); - if (Object.prototype.hasOwnProperty.call(decoded, "realToElapsedTimeOffsetNanos")) { - this.realToElapsedTimeOffsetNs = BigInt(decoded.realToElapsedTimeOffsetNanos); + const decodedProto = 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 = (TransactionsTraceFileProto?.parent).LayerState.ChangesLsb; + const LayerStateChangesMsbEnum = (TransactionsTraceFileProto?.parent).LayerState.ChangesMsb; + const DisplayStateChangesEnum = (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; diff --git a/tools/winscope-ng/src/parsers/proto_types.js b/tools/winscope-ng/src/parsers/proto_types.js index 964794335..9763ca5a2 100644 --- a/tools/winscope-ng/src/parsers/proto_types.js +++ b/tools/winscope-ng/src/parsers/proto_types.js @@ -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"; diff --git a/tools/winscope-ng/src/test/e2e/viewer_transactions.spec.ts b/tools/winscope-ng/src/test/e2e/viewer_transactions.spec.ts new file mode 100644 index 000000000..39d3e2e1f --- /dev/null +++ b/tools/winscope-ng/src/test/e2e/viewer_transactions.spec.ts @@ -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(); + }); +}); diff --git a/tools/winscope-ng/src/viewers/common/properties_tree_generator.spec.ts b/tools/winscope-ng/src/viewers/common/properties_tree_generator.spec.ts new file mode 100644 index 000000000..61441513f --- /dev/null +++ b/tools/winscope-ng/src/viewers/common/properties_tree_generator.spec.ts @@ -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); + }); +}); \ No newline at end of file diff --git a/tools/winscope-ng/src/viewers/common/properties_tree_generator.ts b/tools/winscope-ng/src/viewers/common/properties_tree_generator.ts new file mode 100644 index 000000000..d8cbbf486 --- /dev/null +++ b/tools/winscope-ng/src/viewers/common/properties_tree_generator.ts @@ -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}; diff --git a/tools/winscope-ng/src/viewers/viewer_factory.ts b/tools/winscope-ng/src/viewers/viewer_factory.ts index c8642f51f..65c0687c2 100644 --- a/tools/winscope-ng/src/viewers/viewer_factory.ts +++ b/tools/winscope-ng/src/viewers/viewer_factory.ts @@ -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}; diff --git a/tools/winscope-ng/src/viewers/viewer_transactions/events.ts b/tools/winscope-ng/src/viewers/viewer_transactions/events.ts new file mode 100644 index 000000000..db97840e3 --- /dev/null +++ b/tools/winscope-ng/src/viewers/viewer_transactions/events.ts @@ -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}; diff --git a/tools/winscope-ng/src/viewers/viewer_transactions/presenter.spec.ts b/tools/winscope-ng/src/viewers/viewer_transactions/presenter.spec.ts new file mode 100644 index 000000000..91c755d95 --- /dev/null +++ b/tools/winscope-ng/src/viewers/viewer_transactions/presenter.spec.ts @@ -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; + 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(); + inputTraceEntries.set(TraceType.TRANSACTIONS, [inputTraceEntry]); + outputUiData = undefined; + + presenter = new Presenter((data: UiData) => { + outputUiData = data; + }); + }); + + it("is robust to undefined trace entry", () => { + inputTraceEntries = new Map(); + 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()); + 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); + + (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); + + (inputTraceEntries.get(TraceType.TRANSACTIONS)[0]).currentEntryIndex = 10; + presenter.notifyCurrentTraceEntries(inputTraceEntries); + expect(outputUiData!.currentEntryIndex).toEqual(13); + }); + + it("updates current entry index when filters change", () => { + (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); + }); +}); diff --git a/tools/winscope-ng/src/viewers/viewer_transactions/presenter.ts b/tools/winscope-ng/src/viewers/viewer_transactions/presenter.ts new file mode 100644 index 000000000..1835f1a3d --- /dev/null +++ b/tools/winscope-ng/src/viewers/viewer_transactions/presenter.ts @@ -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): 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(); + 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}; diff --git a/tools/winscope-ng/src/viewers/viewer_transactions/ui_data.ts b/tools/winscope-ng/src/viewers/viewer_transactions/ui_data.ts new file mode 100644 index 000000000..4eb7919e9 --- /dev/null +++ b/tools/winscope-ng/src/viewers/viewer_transactions/ui_data.ts @@ -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}; diff --git a/tools/winscope-ng/src/viewers/viewer_transactions/viewer_transactions.component.spec.ts b/tools/winscope-ng/src/viewers/viewer_transactions/viewer_transactions.component.spec.ts new file mode 100644 index 000000000..1dee857cc --- /dev/null +++ b/tools/winscope-ng/src/viewers/viewer_transactions/viewer_transactions.component.spec.ts @@ -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; + 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 { + 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); +} diff --git a/tools/winscope-ng/src/viewers/viewer_transactions/viewer_transactions.component.ts b/tools/winscope-ng/src/viewers/viewer_transactions/viewer_transactions.component.ts new file mode 100644 index 000000000..67c0cf4d9 --- /dev/null +++ b/tools/winscope-ng/src/viewers/viewer_transactions/viewer_transactions.component.ts @@ -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: ` +
+
+
+
+
+
+ VSYNC ID +
+
+ + PID + + + {{pid}} + + + +
+
+ + UID + + + {{uid}} + + + +
+
+ + Type + + + {{type}} + + + +
+
+ + LAYER/DISPLAY ID + + + {{id}} + + + +
+
+ + +
+
+ {{entry.time}} +
+
+ {{entry.vsyncId}} +
+
+ {{entry.pid}} +
+
+ {{entry.uid}} +
+
+ {{entry.type}} +
+
+ {{entry.id}} +
+
+
+
+ +
+

Properties - Proto Dump

+ +
+
+ `, + 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}; diff --git a/tools/winscope-ng/src/viewers/viewer_transactions/viewer_transactions.ts b/tools/winscope-ng/src/viewers/viewer_transactions/viewer_transactions.ts new file mode 100644 index 000000000..3243f0cdc --- /dev/null +++ b/tools/winscope-ng/src/viewers/viewer_transactions/viewer_transactions.ts @@ -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): 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};