diff --git a/tools/winscope-ng/src/app/app.module.ts b/tools/winscope-ng/src/app/app.module.ts index bb20e1833..6ed397ac3 100644 --- a/tools/winscope-ng/src/app/app.module.ts +++ b/tools/winscope-ng/src/app/app.module.ts @@ -1,5 +1,6 @@ import { NgModule } from "@angular/core"; import { BrowserModule } from "@angular/platform-browser"; +import { ScrollingModule } from "@angular/cdk/scrolling"; import { CommonModule } from "@angular/common"; import { MatCardModule } from "@angular/material/card"; import { MatButtonModule } from "@angular/material/button"; @@ -22,29 +23,31 @@ import { MatToolbarModule } from "@angular/material/toolbar"; import { MatTabsModule } from "@angular/material/tabs"; import { MatSnackBarModule } from "@angular/material/snack-bar"; -import { AppComponent } from "./components/app.component"; -import { ViewerWindowManagerComponent } from "viewers/viewer_window_manager/viewer_window_manager.component"; -import { ViewerSurfaceFlingerComponent } from "viewers/viewer_surface_flinger/viewer_surface_flinger.component"; -import { CollectTracesComponent } from "./components/collect_traces.component"; import { AdbProxyComponent } from "./components/adb_proxy.component"; -import { WebAdbComponent } from "./components/web_adb.component"; +import { AppComponent } from "./components/app.component"; +import { CollectTracesComponent } from "./components/collect_traces.component"; +import { ParserErrorSnackBarComponent } from "./components/parser_error_snack_bar_component"; import { TraceConfigComponent } from "./components/trace_config.component"; -import { UploadTracesComponent } from "./components/upload_traces.component"; -import { HierarchyComponent } from "viewers/components/hierarchy.component"; -import { PropertiesComponent } from "viewers/components/properties.component"; -import { RectsComponent } from "viewers/components/rects/rects.component"; import { TraceViewComponent } from "./components/trace_view.component"; +import { UploadTracesComponent } from "./components/upload_traces.component"; +import { WebAdbComponent } from "./components/web_adb.component"; + +import { CoordinatesTableComponent } from "viewers/components/coordinates_table.component"; +import { HierarchyComponent } from "viewers/components/hierarchy.component"; +import { ImeAdditionalPropertiesComponent } from "viewers/components/ime_additional_properties.component"; +import { PropertiesComponent } from "viewers/components/properties.component"; +import { PropertiesTableComponent } from "viewers/components/properties_table.component"; +import { PropertyGroupsComponent } from "viewers/components/property_groups.component"; +import { RectsComponent } from "viewers/components/rects/rects.component"; +import { TransformMatrixComponent } from "viewers/components/transform_matrix.component"; import { TreeComponent } from "viewers/components/tree.component"; import { TreeNodeComponent } from "viewers/components/tree_node.component"; import { TreeNodeDataViewComponent } from "viewers/components/tree_node_data_view.component"; import { TreeNodePropertiesDataViewComponent } from "viewers/components/tree_node_properties_data_view.component"; -import { PropertyGroupsComponent } from "viewers/components/property_groups.component"; -import { TransformMatrixComponent } from "viewers/components/transform_matrix.component"; -import { ParserErrorSnackBarComponent } from "./components/parser_error_snack_bar_component"; import { ViewerInputMethodComponent } from "viewers/components/viewer_input_method.component"; -import { PropertiesTableComponent } from "viewers/components/properties_table.component"; -import { ImeAdditionalPropertiesComponent } from "viewers/components/ime_additional_properties.component"; -import { CoordinatesTableComponent } from "viewers/components/coordinates_table.component"; +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"; @NgModule({ declarations: [ @@ -52,6 +55,7 @@ import { CoordinatesTableComponent } from "viewers/components/coordinates_table. ViewerWindowManagerComponent, ViewerSurfaceFlingerComponent, ViewerInputMethodComponent, + ViewerProtologComponent, CollectTracesComponent, UploadTracesComponent, AdbProxyComponent, @@ -96,6 +100,7 @@ import { CoordinatesTableComponent } from "viewers/components/coordinates_table. MatToolbarModule, MatTabsModule, MatSnackBarModule, + ScrollingModule, ], bootstrap: [AppComponent] }) diff --git a/tools/winscope-ng/src/app/components/app.component.ts b/tools/winscope-ng/src/app/components/app.component.ts index bc4ee9550..3952c2b41 100644 --- a/tools/winscope-ng/src/app/components/app.component.ts +++ b/tools/winscope-ng/src/app/components/app.component.ts @@ -15,14 +15,15 @@ */ import { Component, Injector, Inject, ViewEncapsulation, Input } from "@angular/core"; import { createCustomElement } from "@angular/elements"; -import { TraceCoordinator } from "../trace_coordinator"; -import { proxyClient, ProxyState } from "trace_collection/proxy_client"; -import { PersistentStore } from "common/persistent_store"; -import { ViewerWindowManagerComponent } from "viewers/viewer_window_manager/viewer_window_manager.component"; -import { ViewerSurfaceFlingerComponent } from "viewers/viewer_surface_flinger/viewer_surface_flinger.component"; -import { Timestamp } from "common/trace/timestamp"; import { MatSliderChange } from "@angular/material/slider"; +import { TraceCoordinator } from "app/trace_coordinator"; +import { PersistentStore } from "common/persistent_store"; +import { Timestamp } from "common/trace/timestamp"; +import { proxyClient, ProxyState } from "trace_collection/proxy_client"; 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 { ViewerWindowManagerComponent } from "viewers/viewer_window_manager/viewer_window_manager.component"; @Component({ selector: "app-root", @@ -133,17 +134,22 @@ export class AppComponent { @Inject(Injector) injector: Injector ) { this.traceCoordinator = new TraceCoordinator(); - if (!customElements.get("viewer-window-manager")) { - customElements.define("viewer-window-manager", - createCustomElement(ViewerWindowManagerComponent, {injector})); + + if (!customElements.get("viewer-input-method")) { + customElements.define("viewer-input-method", + createCustomElement(ViewerInputMethodComponent, {injector})); + } + if (!customElements.get("viewer-protolog")) { + customElements.define("viewer-protolog", + createCustomElement(ViewerProtologComponent, {injector})); } if (!customElements.get("viewer-surface-flinger")) { customElements.define("viewer-surface-flinger", createCustomElement(ViewerSurfaceFlingerComponent, {injector})); } - if (!customElements.get("viewer-input-method")) { - customElements.define("viewer-input-method", - createCustomElement(ViewerInputMethodComponent, {injector})); + if (!customElements.get("viewer-window-manager")) { + customElements.define("viewer-window-manager", + createCustomElement(ViewerWindowManagerComponent, {injector})); } } diff --git a/tools/winscope-ng/src/app/trace_info.ts b/tools/winscope-ng/src/app/trace_info.ts index b28544342..efba5381e 100644 --- a/tools/winscope-ng/src/app/trace_info.ts +++ b/tools/winscope-ng/src/app/trace_info.ts @@ -5,7 +5,7 @@ const SURFACE_FLINGER_ICON = "layers"; const SCREEN_RECORDING_ICON = "videocam"; const TRANSACTION_ICON = "show_chart"; const WAYLAND_ICON = "filter_none"; -const PROTO_LOG_ICON = "text_ad"; +const PROTO_LOG_ICON = "notes"; const SYSTEM_UI_ICON = "filter_none"; const LAUNCHER_ICON = "filter_none"; const IME_ICON = "keyboard_alt"; @@ -54,7 +54,7 @@ export const TRACE_INFO: traceInfoMap = { icon: WAYLAND_ICON }, [TraceType.PROTO_LOG]: { - name: "Proto Log", + name: "ProtoLog", icon: PROTO_LOG_ICON }, [TraceType.SYSTEM_UI]: { diff --git a/tools/winscope-ng/src/common/trace/protolog.ts b/tools/winscope-ng/src/common/trace/protolog.ts index e251da1d7..92ccc5404 100644 --- a/tools/winscope-ng/src/common/trace/protolog.ts +++ b/tools/winscope-ng/src/common/trace/protolog.ts @@ -16,6 +16,11 @@ import {StringUtils} from "common/utils/string_utils"; import configJson from "../../../../../../frameworks/base/data/etc/services.core.protolog.json"; +class ProtoLogTraceEntry { + constructor(public messages: LogMessage[], public currentMessageIndex: number) { + } +} + class LogMessage { text: string; time: string; @@ -133,4 +138,4 @@ function getParam(arr: T[], idx: number): T { return arr[idx]; } -export {FormattedLogMessage, LogMessage, UnformattedLogMessage}; +export {FormattedLogMessage, LogMessage, ProtoLogTraceEntry, UnformattedLogMessage}; diff --git a/tools/winscope-ng/src/parsers/parser.ts b/tools/winscope-ng/src/parsers/parser.ts index 11dbba20e..a014dd66f 100644 --- a/tools/winscope-ng/src/parsers/parser.ts +++ b/tools/winscope-ng/src/parsers/parser.ts @@ -65,6 +65,7 @@ abstract class Parser { return this.timestamps.get(type); } + //TODO: factor out timestamp search policy. Receive index parameter instead. public getTraceEntry(timestamp: Timestamp): undefined|any { const timestamps = this.getTimestamps(timestamp.getType()); if (timestamps === undefined) { @@ -75,19 +76,13 @@ abstract class Parser { if (index === undefined) { return undefined; } - return this.processDecodedEntry(this.decodedEntries[index]); - } - - public getTraceEntries(): any[] { - throw new Error("Batch retrieval of trace entries not implemented for this parser!" + - " Note that the usage of this functionality is discouraged," + - " since creating all the trace entry objects may consume too much memory."); + return this.processDecodedEntry(index, this.decodedEntries[index]); } protected abstract getMagicNumber(): undefined|number[]; protected abstract decodeTrace(trace: Uint8Array): any[]; protected abstract getTimestamp(type: TimestampType, decodedEntry: any): undefined|Timestamp; - protected abstract processDecodedEntry(decodedEntry: any): any; + protected abstract processDecodedEntry(index: number, decodedEntry: any): any; protected trace: File; protected decodedEntries: any[] = []; diff --git a/tools/winscope-ng/src/parsers/parser_accessibility.ts b/tools/winscope-ng/src/parsers/parser_accessibility.ts index 6fc0913cc..f74895ea3 100644 --- a/tools/winscope-ng/src/parsers/parser_accessibility.ts +++ b/tools/winscope-ng/src/parsers/parser_accessibility.ts @@ -53,7 +53,7 @@ class ParserAccessibility extends Parser { return undefined; } - override processDecodedEntry(entryProto: any): any { + override processDecodedEntry(index: number, entryProto: any): any { return entryProto; } diff --git a/tools/winscope-ng/src/parsers/parser_input_method_clients.ts b/tools/winscope-ng/src/parsers/parser_input_method_clients.ts index 9c4a2ba08..628a5afb0 100644 --- a/tools/winscope-ng/src/parsers/parser_input_method_clients.ts +++ b/tools/winscope-ng/src/parsers/parser_input_method_clients.ts @@ -57,7 +57,7 @@ class ParserInputMethodClients extends Parser { return undefined; } - override processDecodedEntry(entryProto: TraceTreeNode): TraceTreeNode { + override processDecodedEntry(index: number, entryProto: TraceTreeNode): TraceTreeNode { return { name: StringUtils.nanosecondsToHuman(entryProto.elapsedRealtimeNanos ?? 0) + " - " + entryProto.where, kind: "InputMethodClient entry", diff --git a/tools/winscope-ng/src/parsers/parser_input_method_manager_service.ts b/tools/winscope-ng/src/parsers/parser_input_method_manager_service.ts index 88f03c8e7..5e6f145a6 100644 --- a/tools/winscope-ng/src/parsers/parser_input_method_manager_service.ts +++ b/tools/winscope-ng/src/parsers/parser_input_method_manager_service.ts @@ -55,7 +55,7 @@ class ParserInputMethodManagerService extends Parser { return undefined; } - protected override processDecodedEntry(entryProto: TraceTreeNode): TraceTreeNode { + protected override processDecodedEntry(index: number, entryProto: TraceTreeNode): TraceTreeNode { return { name: StringUtils.nanosecondsToHuman(entryProto.elapsedRealtimeNanos ?? 0) + " - " + entryProto.where, kind: "InputMethodManagerService entry", diff --git a/tools/winscope-ng/src/parsers/parser_input_method_service.ts b/tools/winscope-ng/src/parsers/parser_input_method_service.ts index 103cc6f3d..4503eced3 100644 --- a/tools/winscope-ng/src/parsers/parser_input_method_service.ts +++ b/tools/winscope-ng/src/parsers/parser_input_method_service.ts @@ -56,7 +56,7 @@ class ParserInputMethodService extends Parser { return undefined; } - override processDecodedEntry(entryProto: TraceTreeNode): TraceTreeNode { + override processDecodedEntry(index: number, entryProto: TraceTreeNode): TraceTreeNode { return { name: StringUtils.nanosecondsToHuman(entryProto.elapsedRealtimeNanos ?? 0) + " - " + entryProto.where, kind: "InputMethodService entry", diff --git a/tools/winscope-ng/src/parsers/parser_protolog.spec.ts b/tools/winscope-ng/src/parsers/parser_protolog.spec.ts index 018138c64..380246586 100644 --- a/tools/winscope-ng/src/parsers/parser_protolog.spec.ts +++ b/tools/winscope-ng/src/parsers/parser_protolog.spec.ts @@ -69,22 +69,14 @@ describe("ParserProtoLog", () => { it("reconstructs human-readable log message", () => { const timestamp = new Timestamp(TimestampType.ELAPSED, 850746266486n); - const actualMessage = parser.getTraceEntry(timestamp)!; + const entry = parser.getTraceEntry(timestamp)!; - expect(actualMessage).toBeInstanceOf(LogMessage); - expect(Object.assign({}, actualMessage)).toEqual(expectedFirstLogMessage); - }); + expect(entry.currentMessageIndex).toEqual(0); - it("allows retrieving all the log messages", () => { - const actualMessages = parser.getTraceEntries(); - - expect(actualMessages.length).toEqual(50); - - actualMessages.forEach(message => { + expect(entry.messages.length).toEqual(50); + expect(Object.assign({}, entry.messages[0])).toEqual(expectedFirstLogMessage); + entry.messages.forEach((message: any) => { expect(message).toBeInstanceOf(LogMessage); }); - - const actualFirstLogMessage = Object.assign({}, actualMessages[0]); - expect(actualFirstLogMessage).toEqual(expectedFirstLogMessage); }); }); diff --git a/tools/winscope-ng/src/parsers/parser_protolog.ts b/tools/winscope-ng/src/parsers/parser_protolog.ts index fb3fc134b..9e57eb7ec 100644 --- a/tools/winscope-ng/src/parsers/parser_protolog.ts +++ b/tools/winscope-ng/src/parsers/parser_protolog.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {FormattedLogMessage, LogMessage, UnformattedLogMessage} from "common/trace/protolog"; +import {FormattedLogMessage, LogMessage, ProtoLogTraceEntry, UnformattedLogMessage} from "common/trace/protolog"; import {Timestamp, TimestampType} from "common/trace/timestamp"; import {TraceType} from "common/trace/trace_type"; import {Parser} from "./parser"; @@ -67,7 +67,17 @@ class ParserProtoLog extends Parser { return undefined; } - override processDecodedEntry(entryProto: any): LogMessage { + override processDecodedEntry(index: number, entryProto: any): ProtoLogTraceEntry { + if (!this.decodedMessages) { + this.decodedMessages = this.decodedEntries.map((entryProto: any) => { + return this.decodeProtoLogMessage(entryProto); + }); + } + + return new ProtoLogTraceEntry(this.decodedMessages, index); + } + + private decodeProtoLogMessage(entryProto: any): LogMessage { const message = (configJson).messages[entryProto.messageHash]; if (!message) { return new FormattedLogMessage(entryProto); @@ -84,12 +94,7 @@ class ParserProtoLog extends Parser { } } - override getTraceEntries(): LogMessage[] { - return this.decodedEntries.map((entryProto: any) => { - return this.processDecodedEntry(entryProto); - }); - } - + private decodedMessages?: LogMessage[]; private realToElapsedTimeOffsetNs: undefined|bigint = undefined; private static readonly MAGIC_NUMBER = [0x09, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x4c, 0x4f, 0x47]; // .PROTOLOG private static readonly PROTOLOG_VERSION = "1.0.0"; diff --git a/tools/winscope-ng/src/parsers/parser_screen_recording.ts b/tools/winscope-ng/src/parsers/parser_screen_recording.ts index fd2bb4a74..ed7f5356c 100644 --- a/tools/winscope-ng/src/parsers/parser_screen_recording.ts +++ b/tools/winscope-ng/src/parsers/parser_screen_recording.ts @@ -73,7 +73,7 @@ class ParserScreenRecording extends Parser { return undefined; } - override processDecodedEntry(entry: ScreenRecordingMetadataEntry): ScreenRecordingTraceEntry { + override processDecodedEntry(index: number, entry: ScreenRecordingMetadataEntry): ScreenRecordingTraceEntry { const initialTimestampNs = this.getTimestamps(TimestampType.ELAPSED)![0].getValueNs(); const currentTimestampNs = entry.timestampMonotonicNs; const videoTimeSeconds = Number(currentTimestampNs - initialTimestampNs) / 1000000000; diff --git a/tools/winscope-ng/src/parsers/parser_screen_recording_legacy.ts b/tools/winscope-ng/src/parsers/parser_screen_recording_legacy.ts index db13d38ee..789ee7ef1 100644 --- a/tools/winscope-ng/src/parsers/parser_screen_recording_legacy.ts +++ b/tools/winscope-ng/src/parsers/parser_screen_recording_legacy.ts @@ -45,7 +45,7 @@ class ParserScreenRecordingLegacy extends Parser { return decodedEntry; } - override processDecodedEntry(entry: Timestamp): ScreenRecordingTraceEntry { + override processDecodedEntry(index: number, entry: Timestamp): ScreenRecordingTraceEntry { const currentTimestamp = entry; const initialTimestamp = this.getTimestamps(TimestampType.ELAPSED)![0]; const videoTimeSeconds = diff --git a/tools/winscope-ng/src/parsers/parser_surface_flinger.ts b/tools/winscope-ng/src/parsers/parser_surface_flinger.ts index d158ed900..6a6e16329 100644 --- a/tools/winscope-ng/src/parsers/parser_surface_flinger.ts +++ b/tools/winscope-ng/src/parsers/parser_surface_flinger.ts @@ -59,7 +59,7 @@ class ParserSurfaceFlinger extends Parser { return undefined; } - override processDecodedEntry(entryProto: any): LayerTraceEntry { + override processDecodedEntry(index: number, entryProto: any): LayerTraceEntry { return LayerTraceEntry.fromProto(entryProto.layers.layers, entryProto.displays, entryProto.elapsedRealtimeNanos, entryProto.hwcBlob); } diff --git a/tools/winscope-ng/src/parsers/parser_transactions.ts b/tools/winscope-ng/src/parsers/parser_transactions.ts index c3427e85a..3f9f9c9d4 100644 --- a/tools/winscope-ng/src/parsers/parser_transactions.ts +++ b/tools/winscope-ng/src/parsers/parser_transactions.ts @@ -53,7 +53,7 @@ class ParserTransactions extends Parser { return undefined; } - override processDecodedEntry(entryProto: any): any { + override processDecodedEntry(index: number, entryProto: any): any { return entryProto; } diff --git a/tools/winscope-ng/src/parsers/parser_window_manager.ts b/tools/winscope-ng/src/parsers/parser_window_manager.ts index 018f653b3..5a38f72a0 100644 --- a/tools/winscope-ng/src/parsers/parser_window_manager.ts +++ b/tools/winscope-ng/src/parsers/parser_window_manager.ts @@ -54,7 +54,7 @@ class ParserWindowManager extends Parser { return undefined; } - override processDecodedEntry(entryProto: any): WindowManagerState { + override processDecodedEntry(index: number, entryProto: any): WindowManagerState { return WindowManagerState.fromProto(entryProto.windowManagerService, entryProto.elapsedRealtimeNanos, entryProto.where); } diff --git a/tools/winscope-ng/src/parsers/parser_window_manager_dump.ts b/tools/winscope-ng/src/parsers/parser_window_manager_dump.ts index c82719689..0fc7d81c0 100644 --- a/tools/winscope-ng/src/parsers/parser_window_manager_dump.ts +++ b/tools/winscope-ng/src/parsers/parser_window_manager_dump.ts @@ -43,7 +43,7 @@ class ParserWindowManagerDump extends Parser { return new Timestamp(TimestampType.ELAPSED, 0n); } - override processDecodedEntry(entryProto: any): WindowManagerState { + override processDecodedEntry(index: number, entryProto: any): WindowManagerState { return WindowManagerState.fromProto(entryProto); } } diff --git a/tools/winscope-ng/src/test/e2e/viewer_protolog.spec.ts b/tools/winscope-ng/src/test/e2e/viewer_protolog.spec.ts new file mode 100644 index 000000000..5182713ed --- /dev/null +++ b/tools/winscope-ng/src/test/e2e/viewer_protolog.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 ProtoLog", () => { + 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/ProtoLog.pb")); + + const loadData = element(by.css(".load-btn")); + await loadData.click(); + + const isViewerRendered = await element(by.css("viewer-protolog")).isPresent(); + expect(isViewerRendered).toBeTruthy(); + + const isFirstMessageRendered = await element(by.css("viewer-protolog .scroll-messages .message")).isPresent(); + expect(isFirstMessageRendered).toBeTruthy(); + }); +}); diff --git a/tools/winscope-ng/src/viewers/viewer_factory.ts b/tools/winscope-ng/src/viewers/viewer_factory.ts index 64ca5fa62..c8642f51f 100644 --- a/tools/winscope-ng/src/viewers/viewer_factory.ts +++ b/tools/winscope-ng/src/viewers/viewer_factory.ts @@ -15,19 +15,21 @@ */ import { TraceType } from "common/trace/trace_type"; import { Viewer } from "./viewer"; -import { ViewerWindowManager } from "./viewer_window_manager/viewer_window_manager"; -import { ViewerSurfaceFlinger } from "./viewer_surface_flinger/viewer_surface_flinger"; 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"; class ViewerFactory { static readonly VIEWERS = [ - ViewerWindowManager, - ViewerSurfaceFlinger, ViewerInputMethodClients, - ViewerInputMethodService, ViewerInputMethodManagerService, + ViewerInputMethodService, + ViewerProtoLog, + ViewerSurfaceFlinger, + ViewerWindowManager, ]; public createViewers(activeTraceTypes: Set): Viewer[] { diff --git a/tools/winscope-ng/src/viewers/viewer_protolog/events.ts b/tools/winscope-ng/src/viewers/viewer_protolog/events.ts new file mode 100644 index 000000000..773e9706a --- /dev/null +++ b/tools/winscope-ng/src/viewers/viewer_protolog/events.ts @@ -0,0 +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. + */ +class Events { + public static LogLevelsFilterChanged = "ViewerProtoLogEvent_LogLevelsFilterChanged"; + public static TagsFilterChanged = "ViewerProtoLogEvent_TagsFilterChanged"; + public static SourceFilesFilterChanged = "ViewerProtoLogEvent_SourceFilesFilterChanged"; + public static SearchStringFilterChanged = "ViewerProtoLogEvent_SearchStringFilterChanged"; +} + +export {Events}; diff --git a/tools/winscope-ng/src/viewers/viewer_protolog/presenter.spec.ts b/tools/winscope-ng/src/viewers/viewer_protolog/presenter.spec.ts new file mode 100644 index 000000000..248c2fe7f --- /dev/null +++ b/tools/winscope-ng/src/viewers/viewer_protolog/presenter.spec.ts @@ -0,0 +1,151 @@ +/* + * 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 {LogMessage, ProtoLogTraceEntry} from "common/trace/protolog"; +import {TraceType} from "common/trace/trace_type"; +import {Presenter} from "./presenter"; +import {UiData} from "./ui_data"; + +describe("ViewerProtoLogPresenter", () => { + let presenter: Presenter; + let inputMessages: LogMessage[]; + let inputTraceEntries: Map; + let outputUiData: undefined|UiData; + + beforeEach(async () => { + inputMessages = [ + new LogMessage("text0", "time", "tag0", "level0", "sourcefile0", 10), + new LogMessage("text1", "time", "tag1", "level1", "sourcefile1", 10), + new LogMessage("text2", "time", "tag2", "level2", "sourcefile2", 10), + ]; + inputTraceEntries = new Map(); + inputTraceEntries.set(TraceType.PROTO_LOG, [new ProtoLogTraceEntry(inputMessages, 0)]); + + outputUiData = undefined; + + presenter = new Presenter((data: UiData) => { + outputUiData = data; + }); + }); + + it("is robust to undefined trace entry", () => { + presenter.notifyCurrentTraceEntries(new Map()); + expect(outputUiData!.messages).toEqual([]); + expect(outputUiData!.currentMessageIndex).toBeUndefined(); + }); + + it("ignores undefined trace entry and doesn't discard displayed messages", () => { + presenter.notifyCurrentTraceEntries(inputTraceEntries); + expect(outputUiData!.messages).toEqual(inputMessages); + + presenter.notifyCurrentTraceEntries(new Map()); + expect(outputUiData!.messages).toEqual(inputMessages); + }); + + it("processes current trace entries", () => { + presenter.notifyCurrentTraceEntries(inputTraceEntries); + + expect(outputUiData!.allLogLevels).toEqual(["level0", "level1", "level2"]); + expect(outputUiData!.allTags).toEqual(["tag0", "tag1", "tag2"]); + expect(outputUiData!.allSourceFiles).toEqual(["sourcefile0", "sourcefile1", "sourcefile2"]); + expect(outputUiData!.messages).toEqual(inputMessages); + expect(outputUiData!.currentMessageIndex).toEqual(0); + }); + + it("updated displayed messages according to log levels filter", () => { + presenter.notifyCurrentTraceEntries(inputTraceEntries); + expect(outputUiData!.messages).toEqual(inputMessages); + + presenter.onLogLevelsFilterChanged([]); + expect(outputUiData!.messages).toEqual(inputMessages); + + presenter.onLogLevelsFilterChanged(["level1"]); + expect(outputUiData!.messages).toEqual([inputMessages[1]]); + + presenter.onLogLevelsFilterChanged(["level0", "level1", "level2"]); + expect(outputUiData!.messages).toEqual(inputMessages); + }); + + it("updates displayed messages according to tags filter", () => { + presenter.notifyCurrentTraceEntries(inputTraceEntries); + expect(outputUiData!.messages).toEqual(inputMessages); + + presenter.onTagsFilterChanged([]); + expect(outputUiData!.messages).toEqual(inputMessages); + + presenter.onTagsFilterChanged(["tag1"]); + expect(outputUiData!.messages).toEqual([inputMessages[1]]); + + presenter.onTagsFilterChanged(["tag0", "tag1", "tag2"]); + expect(outputUiData!.messages).toEqual(inputMessages); + }); + + it("updates displayed messages according to source files filter", () => { + presenter.notifyCurrentTraceEntries(inputTraceEntries); + expect(outputUiData!.messages).toEqual(inputMessages); + + presenter.onSourceFilesFilterChanged([]); + expect(outputUiData!.messages).toEqual(inputMessages); + + presenter.onSourceFilesFilterChanged(["sourcefile1"]); + expect(outputUiData!.messages).toEqual([inputMessages[1]]); + + presenter.onSourceFilesFilterChanged(["sourcefile0", "sourcefile1", "sourcefile2"]); + expect(outputUiData!.messages).toEqual(inputMessages); + }); + + it("updates displayed messages according to search string filter", () => { + presenter.notifyCurrentTraceEntries(inputTraceEntries); + expect(outputUiData!.messages).toEqual(inputMessages); + + presenter.onSearchStringFilterChanged(""); + expect(outputUiData!.messages).toEqual(inputMessages); + + presenter.onSearchStringFilterChanged("text"); + expect(outputUiData!.messages).toEqual(inputMessages); + + presenter.onSearchStringFilterChanged("text0"); + expect(outputUiData!.messages).toEqual([inputMessages[0]]); + + presenter.onSearchStringFilterChanged("text1"); + expect(outputUiData!.messages).toEqual([inputMessages[1]]); + }); + + it("computes current message index", () => { + presenter.notifyCurrentTraceEntries(inputTraceEntries); + presenter.onLogLevelsFilterChanged([]); + expect(outputUiData!.currentMessageIndex).toEqual(0); + + presenter.onLogLevelsFilterChanged(["level0"]); + expect(outputUiData!.currentMessageIndex).toEqual(0); + + presenter.onLogLevelsFilterChanged([]); + expect(outputUiData!.currentMessageIndex).toEqual(0); + + (inputTraceEntries.get(TraceType.PROTO_LOG)[0]).currentMessageIndex = 1; + presenter.notifyCurrentTraceEntries(inputTraceEntries); + presenter.onLogLevelsFilterChanged([]); + expect(outputUiData!.currentMessageIndex).toEqual(1); + + presenter.onLogLevelsFilterChanged(["level0"]); + expect(outputUiData!.currentMessageIndex).toEqual(0); + + presenter.onLogLevelsFilterChanged(["level1"]); + expect(outputUiData!.currentMessageIndex).toEqual(0); + + presenter.onLogLevelsFilterChanged(["level0", "level1"]); + expect(outputUiData!.currentMessageIndex).toEqual(1); + }); +}); diff --git a/tools/winscope-ng/src/viewers/viewer_protolog/presenter.ts b/tools/winscope-ng/src/viewers/viewer_protolog/presenter.ts new file mode 100644 index 000000000..1db0867dc --- /dev/null +++ b/tools/winscope-ng/src/viewers/viewer_protolog/presenter.ts @@ -0,0 +1,139 @@ +/* + * 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} from "./ui_data"; +import {ArrayUtils} from "common/utils/array_utils"; +import {LogMessage, ProtoLogTraceEntry} from "common/trace/protolog"; +import {TraceType} from "common/trace/trace_type"; + +export class Presenter { + constructor( + notifyUiDataCallback: (data: UiData) => void) { + this.notifyUiDataCallback = notifyUiDataCallback; + this.originalIndicesOfFilteredOutputMessages = []; + 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.PROTO_LOG) ? entries.get(TraceType.PROTO_LOG)[0] : undefined; + if (this.uiData === UiData.EMPTY) { + this.computeUiDataMessages(); + } + this.computeUiDataCurrentMessageIndex(); + this.notifyUiDataCallback(this.uiData); + } + + public onLogLevelsFilterChanged(levels: string[]) { + this.levels = levels; + this.computeUiDataMessages(); + this.computeUiDataCurrentMessageIndex(); + this.notifyUiDataCallback(this.uiData); + } + + public onTagsFilterChanged(tags: string[]) { + this.tags = tags; + this.computeUiDataMessages(); + this.computeUiDataCurrentMessageIndex(); + this.notifyUiDataCallback(this.uiData); + } + + public onSourceFilesFilterChanged(files: string[]) { + this.files = files; + this.computeUiDataMessages(); + this.computeUiDataCurrentMessageIndex(); + this.notifyUiDataCallback(this.uiData); + } + + public onSearchStringFilterChanged(searchString: string) { + this.searchString = searchString; + this.computeUiDataMessages(); + this.computeUiDataCurrentMessageIndex(); + this.notifyUiDataCallback(this.uiData); + } + + private computeUiDataMessages() { + if (!this.entry) { + return; + } + + const allLogLevels = this.getUniqueMessageValues( + this.entry!.messages, + (message: LogMessage) => message.level); + const allTags = this.getUniqueMessageValues( + this.entry!.messages, + (message: LogMessage) => message.tag); + const allSourceFiles = this.getUniqueMessageValues( + this.entry!.messages, + (message: LogMessage) => message.at); + + let filteredMessagesAndOriginalIndex: [number, LogMessage][] = [...this.entry!.messages.entries()]; + + if (this.levels.length > 0) { + filteredMessagesAndOriginalIndex = + filteredMessagesAndOriginalIndex.filter(value => this.levels.includes(value[1].level)); + } + + if (this.tags.length > 0) { + filteredMessagesAndOriginalIndex = + filteredMessagesAndOriginalIndex.filter(value => this.tags.includes(value[1].tag)); + } + + if (this.files.length > 0) { + filteredMessagesAndOriginalIndex = + filteredMessagesAndOriginalIndex.filter(value => this.files.includes(value[1].at)); + } + + filteredMessagesAndOriginalIndex = + filteredMessagesAndOriginalIndex.filter(value => value[1].text.includes(this.searchString)); + + this.originalIndicesOfFilteredOutputMessages = filteredMessagesAndOriginalIndex.map(value => value[0]); + const filteredMessages = filteredMessagesAndOriginalIndex.map(value => value[1]); + + this.uiData = new UiData(allLogLevels, allTags, allSourceFiles, filteredMessages, 0); + } + + private computeUiDataCurrentMessageIndex() { + if (!this.entry) { + return; + } + + this.uiData.currentMessageIndex = ArrayUtils.binarySearchLowerOrEqual( + this.originalIndicesOfFilteredOutputMessages, + this.entry.currentMessageIndex + ); + } + + private getUniqueMessageValues(messages: LogMessage[], getValue: (message :LogMessage) => string): string[] { + const uniqueValues = new Set(); + messages.forEach(message => { + uniqueValues.add(getValue(message)); + }); + const result = [...uniqueValues]; + result.sort(); + return result; + } + + private entry?: ProtoLogTraceEntry; + private originalIndicesOfFilteredOutputMessages: number[]; + private uiData: UiData; + private readonly notifyUiDataCallback: (data: UiData) => void; + + private tags: string[] = []; + private files: string[] = []; + private levels: string[] = []; + private searchString = ""; +} diff --git a/tools/winscope-ng/src/viewers/viewer_protolog/ui_data.ts b/tools/winscope-ng/src/viewers/viewer_protolog/ui_data.ts new file mode 100644 index 000000000..e5cef89da --- /dev/null +++ b/tools/winscope-ng/src/viewers/viewer_protolog/ui_data.ts @@ -0,0 +1,30 @@ +/* + * 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 {LogMessage} from "common/trace/protolog"; + +class UiData { + constructor( + public allLogLevels: string[], + public allTags: string[], + public allSourceFiles: string[], + public messages: LogMessage[], + public currentMessageIndex: undefined|number) { + } + + public static EMPTY = new UiData([], [], [], [], undefined); +} + +export {UiData}; diff --git a/tools/winscope-ng/src/viewers/viewer_protolog/viewer_protolog.component.spec.ts b/tools/winscope-ng/src/viewers/viewer_protolog/viewer_protolog.component.spec.ts new file mode 100644 index 000000000..ba4ec9377 --- /dev/null +++ b/tools/winscope-ng/src/viewers/viewer_protolog/viewer_protolog.component.spec.ts @@ -0,0 +1,62 @@ +/* + * 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 {ComponentFixture, ComponentFixtureAutoDetect, TestBed} from "@angular/core/testing"; +import {ViewerProtologComponent} from "./viewer_protolog.component"; + +import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from "@angular/core"; + +describe("ViewerProtologComponent", () => { + let fixture: ComponentFixture; + let component: ViewerProtologComponent; + let htmlElement: HTMLElement; + + beforeAll(async () => { + await TestBed.configureTestingModule({ + providers: [ + { provide: ComponentFixtureAutoDetect, useValue: true } + ], + imports: [ + ScrollingModule + ], + declarations: [ + ViewerProtologComponent, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ViewerProtologComponent); + component = fixture.componentInstance; + htmlElement = fixture.nativeElement; + }); + + it("can be created", () => { + expect(component).toBeTruthy(); + }); + + it("creates message filters", () => { + expect(htmlElement.querySelector(".filters .log-level")).toBeTruthy(); + expect(htmlElement.querySelector(".filters .tag")).toBeTruthy(); + expect(htmlElement.querySelector(".filters .source-file")).toBeTruthy(); + expect(htmlElement.querySelector(".filters .text")).toBeTruthy(); + }); + + it("renders log messages", () => { + expect(htmlElement.querySelector(".scroll-messages")).toBeTruthy(); + }); +}); diff --git a/tools/winscope-ng/src/viewers/viewer_protolog/viewer_protolog.component.ts b/tools/winscope-ng/src/viewers/viewer_protolog/viewer_protolog.component.ts new file mode 100644 index 000000000..815566b1a --- /dev/null +++ b/tools/winscope-ng/src/viewers/viewer_protolog/viewer_protolog.component.ts @@ -0,0 +1,196 @@ +/* + * 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-protolog", + template: ` +
+
+
+ + Log level + + + {{level}} + + + +
+
+ + Tags + + + {{tag}} + + + +
+
+ + Source files + + + {{file}} + + + +
+
+ + Search text + + +
+
+ +
+
{{message.time}}
+
{{message.level}}
+
{{message.tag}}
+
{{message.at}}
+
{{message.text}}
+
+
+
+ `, + styles: [ + ` + .container { + padding: 16px; + box-sizing: border-box; + display: flex; + flex-direction: column; + } + + .filters { + display: flex; + flex-direction: row; + margin-top: 16px; + } + + .scroll-messages { + height: 100%; + flex: 1; + } + + .message { + display: flex; + flex-direction: row; + overflow-wrap: anywhere; + } + + .message.current-message { + background-color: #365179;color: white; + } + + .time { + flex: 2; + } + + .log-level { + flex: 1; + } + + .filters .log-level { + flex: 3; + } + + .tag { + flex: 2; + } + + .source-file { + flex: 4; + } + + .text { + flex: 10; + } + + .filters div { + margin: 4px; + } + + .message div { + margin: 4px; + } + + mat-form-field { + width: 100%; + } + `, + ] +}) +export class ViewerProtologComponent { + constructor(@Inject(ElementRef) elementRef: ElementRef) { + this.elementRef = elementRef; + } + + @Input() + public set inputData(data: UiData) { + this.uiData = data; + if (this.uiData.currentMessageIndex !== undefined && this.scrollComponent) { + this.scrollComponent.scrollToIndex(this.uiData.currentMessageIndex); + } + } + + public onLogLevelsChange(event: MatSelectChange) { + this.emitEvent(Events.LogLevelsFilterChanged, event.value); + } + + public onTagsChange(event: MatSelectChange) { + this.emitEvent(Events.TagsFilterChanged, event.value); + } + + public onSourceFilesChange(event: MatSelectChange) { + this.emitEvent(Events.SourceFilesFilterChanged, event.value); + } + + public onSearchStringChange() { + this.emitEvent(Events.SearchStringFilterChanged, this.searchString); + } + + public isCurrentMessage(index: number): boolean { + return index === this.uiData.currentMessageIndex; + } + + 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 searchString = ""; + private elementRef: ElementRef; +} diff --git a/tools/winscope-ng/src/viewers/viewer_protolog/viewer_protolog.ts b/tools/winscope-ng/src/viewers/viewer_protolog/viewer_protolog.ts new file mode 100644 index 000000000..33f4ad112 --- /dev/null +++ b/tools/winscope-ng/src/viewers/viewer_protolog/viewer_protolog.ts @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {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 ViewerProtoLog implements Viewer { + constructor() { + this.view = document.createElement("viewer-protolog"); + + this.presenter = new Presenter((data: UiData) => { + (this.view as any).inputData = data; + }); + + this.view.addEventListener(Events.LogLevelsFilterChanged, (event) => { + return this.presenter.onLogLevelsFilterChanged((event as CustomEvent).detail); + }); + this.view.addEventListener(Events.TagsFilterChanged, (event) => { + return this.presenter.onTagsFilterChanged((event as CustomEvent).detail); + }); + this.view.addEventListener(Events.SourceFilesFilterChanged, (event) => { + return this.presenter.onSourceFilesFilterChanged((event as CustomEvent).detail); + }); + this.view.addEventListener(Events.SearchStringFilterChanged, (event) => { + return this.presenter.onSearchStringFilterChanged((event as CustomEvent).detail); + }); + } + + public notifyCurrentTraceEntries(entries: Map): void { + this.presenter.notifyCurrentTraceEntries(entries); + } + + public getView(): HTMLElement { + return this.view; + } + + public getTitle() { + return "ProtoLog"; + } + + public getDependencies(): TraceType[] { + return ViewerProtoLog.DEPENDENCIES; + } + + public static readonly DEPENDENCIES: TraceType[] = [TraceType.PROTO_LOG]; + private view: HTMLElement; + private presenter: Presenter; +} + +export {ViewerProtoLog};