Add viewer ProtoLog

Fixes: 251159338
Test: npm run build:all && npm run test:all
Change-Id: Ibd6774ed0bf2bc91a5d128eb8258ed4073279166
This commit is contained in:
Kean Mariotti
2022-09-29 15:55:44 +00:00
parent a59e808275
commit 1fbdd3efaa
26 changed files with 788 additions and 74 deletions

View File

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

View File

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

View File

@@ -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]: {

View File

@@ -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<T>(arr: T[], idx: number): T {
return arr[idx];
}
export {FormattedLogMessage, LogMessage, UnformattedLogMessage};
export {FormattedLogMessage, LogMessage, ProtoLogTraceEntry, UnformattedLogMessage};

View File

@@ -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[] = [];

View File

@@ -53,7 +53,7 @@ class ParserAccessibility extends Parser {
return undefined;
}
override processDecodedEntry(entryProto: any): any {
override processDecodedEntry(index: number, entryProto: any): any {
return entryProto;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = (<any>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";

View File

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

View File

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

View File

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

View File

@@ -53,7 +53,7 @@ class ParserTransactions extends Parser {
return undefined;
}
override processDecodedEntry(entryProto: any): any {
override processDecodedEntry(index: number, entryProto: any): any {
return entryProto;
}

View File

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

View File

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

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

View File

@@ -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<TraceType>): Viewer[] {

View File

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

View File

@@ -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<TraceType, any>;
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<TraceType, any>();
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<TraceType, any>());
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<TraceType, any>());
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);
(<ProtoLogTraceEntry>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);
});
});

View File

@@ -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<TraceType, any>): 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<string>();
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 = "";
}

View File

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

View File

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

View File

@@ -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: `
<div class="card-grid container">
<div class="filters">
<div class="log-level">
<mat-form-field appearance="fill">
<mat-label>Log level</mat-label>
<mat-select (selectionChange)="onLogLevelsChange($event)" multiple>
<mat-option *ngFor="let level of uiData.allLogLevels"
[value]="level">
{{level}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tag">
<mat-form-field appearance="fill">
<mat-label>Tags</mat-label>
<mat-select (selectionChange)="onTagsChange($event)" multiple>
<mat-option *ngFor="let tag of uiData.allTags"
[value]="tag">
{{tag}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="source-file">
<mat-form-field appearance="fill">
<mat-label>Source files</mat-label>
<mat-select (selectionChange)="onSourceFilesChange($event)" multiple>
<mat-option *ngFor="let file of uiData.allSourceFiles"
[value]="file">
{{file}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="text">
<mat-form-field appearance="fill">
<mat-label>Search text</mat-label>
<input matInput [(ngModel)]="searchString" (input)="onSearchStringChange()">
</mat-form-field>
</div>
</div>
<cdk-virtual-scroll-viewport itemSize="16" class="scroll-messages">
<div *cdkVirtualFor="let message of uiData.messages; let i = index;" class="message" [class.current-message]="isCurrentMessage(i)">
<div class="time"><span class="mat-body-1">{{message.time}}</span></div>
<div class="log-level"><span class="mat-body-1">{{message.level}}</span></div>
<div class="tag"><span class="mat-body-1">{{message.tag}}</span></div>
<div class="source-file"><span class="mat-body-1">{{message.at}}</span></div>
<div class="text"><span class="mat-body-1">{{message.text}}</span></div>
</div>
</cdk-virtual-scroll-viewport>
</div>
`,
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;
}

View File

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