Add viewer Transaction

Fix: 238088678
Test: npm run build:all && npm run test:all
Change-Id: I90ad36a0a18e3b68216ea18d2a47e3ff38d98412
This commit is contained in:
Kean Mariotti
2022-10-11 06:40:31 +00:00
parent 00c160c5fd
commit 81c2db9a67
18 changed files with 1438 additions and 19 deletions

View File

@@ -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,

View File

@@ -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}));

View 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};

View File

@@ -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) {

View File

@@ -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", () => {

View File

@@ -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;

View File

@@ -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";

View 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();
});
});

View File

@@ -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);
});
});

View File

@@ -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};

View File

@@ -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};

View 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};

View File

@@ -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);
});
});

View 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};

View 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};

View File

@@ -0,0 +1,102 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {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);
}

View File

@@ -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};

View File

@@ -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};