Add screenrecording viewer

This change also needed and includes the refactorings listed below.

Extend viewer's interface to support overlay views:
viewers return a list of views that could have either "tab" or "overlay" type.

Extend TraceViewComponent to render overlay views as well.

Simplify TraceViewComponent's interface for simpler event handling:
receive list of viewers + callback as input instead of entire TraceCoordinator

Test: npm run build:all && npm run test:all
Fix: b/238090772
Change-Id: Iac4c7e66ebe662a76166318d045c2c35e689ef15
This commit is contained in:
Kean Mariotti
2022-10-21 13:58:48 +00:00
parent d4046b3bad
commit 5d96d3c955
23 changed files with 609 additions and 244 deletions

View File

@@ -1,5 +1,6 @@
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { DragDropModule } from "@angular/cdk/drag-drop";
import { ScrollingModule } from "@angular/cdk/scrolling";
import { CommonModule } from "@angular/common";
import { MatCardModule } from "@angular/material/card";
@@ -46,6 +47,7 @@ import { TreeNodeDataViewComponent } from "viewers/components/tree_node_data_vie
import { TreeNodePropertiesDataViewComponent } from "viewers/components/tree_node_properties_data_view.component";
import { ViewerInputMethodComponent } from "viewers/components/viewer_input_method.component";
import { ViewerProtologComponent} from "viewers/viewer_protolog/viewer_protolog.component";
import { ViewerScreenRecordingComponent } from "viewers/viewer_screen_recording/viewer_screen_recording.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";
@@ -58,6 +60,7 @@ import { ViewerWindowManagerComponent } from "viewers/viewer_window_manager/view
ViewerInputMethodComponent,
ViewerProtologComponent,
ViewerTransactionsComponent,
ViewerScreenRecordingComponent,
CollectTracesComponent,
UploadTracesComponent,
AdbProxyComponent,
@@ -103,6 +106,7 @@ import { ViewerWindowManagerComponent } from "viewers/viewer_window_manager/view
MatTabsModule,
MatSnackBarModule,
ScrollingModule,
DragDropModule,
],
bootstrap: [AppComponent]
})

View File

@@ -19,12 +19,15 @@ 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 { FileUtils } from "common/utils/file_utils";
import { proxyClient, ProxyState } from "trace_collection/proxy_client";
import { ViewerInputMethodComponent } from "viewers/components/viewer_input_method.component";
import { Viewer } from "viewers/viewer";
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";
import { ViewerScreenRecordingComponent } from "viewers/viewer_screen_recording/viewer_screen_recording.component";
@Component({
selector: "app-root",
@@ -51,8 +54,9 @@ import { ViewerTransactionsComponent } from "viewers/viewer_transactions/viewer_
<trace-view
*ngIf="dataLoaded"
id="viewers"
[viewers]="allViewers"
[store]="store"
[traceCoordinator]="traceCoordinator"
(downloadTracesButtonClick)="onDownloadTracesButtonClick()"
></trace-view>
<div *ngIf="dataLoaded" id="timescrub">
@@ -129,6 +133,7 @@ export class AppComponent {
currentTimestamp?: Timestamp;
currentTimestampIndex = 0;
allTimestamps: Timestamp[] = [];
allViewers: Viewer[] = [];
@Input() dataLoaded = false;
constructor(
@@ -144,6 +149,10 @@ export class AppComponent {
customElements.define("viewer-protolog",
createCustomElement(ViewerProtologComponent, {injector}));
}
if (!customElements.get("viewer-screen-recording")) {
customElements.define("viewer-screen-recording",
createCustomElement(ViewerScreenRecordingComponent, {injector}));
}
if (!customElements.get("viewer-surface-flinger")) {
customElements.define("viewer-surface-flinger",
createCustomElement(ViewerSurfaceFlingerComponent, {injector}));
@@ -182,8 +191,9 @@ export class AppComponent {
public onDataLoadedChange(dataLoaded: boolean) {
if (dataLoaded && !(this.traceCoordinator.getViewers().length > 0)) {
this.allTimestamps = this.traceCoordinator.getTimestamps();
this.traceCoordinator.createViewers();
this.allViewers = this.traceCoordinator.getViewers();
this.allTimestamps = this.traceCoordinator.getTimestamps();
this.currentTimestampIndex = 0;
this.notifyCurrentTimestamp();
this.dataLoaded = dataLoaded;
@@ -194,4 +204,18 @@ export class AppComponent {
this.currentTimestamp = this.allTimestamps[this.currentTimestampIndex];
this.traceCoordinator.notifyCurrentTimestamp(this.currentTimestamp);
}
private async onDownloadTracesButtonClick() {
const traces = await this.traceCoordinator.getAllTracesForDownload();
const zipFileBlob = await FileUtils.createZipArchive(traces);
const zipFileName = "winscope.zip";
const a = document.createElement("a");
document.body.appendChild(a);
const url = window.URL.createObjectURL(zipFileBlob);
a.href = url;
a.download = zipFileName;
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
}
}

View File

@@ -13,12 +13,35 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CommonModule } from "@angular/common";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { TraceViewComponent } from "./trace_view.component";
import { MatCardModule } from "@angular/material/card";
import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from "@angular/core";
import { TraceCoordinator } from "app/trace_coordinator";
import {CommonModule} from "@angular/common";
import {CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA} from "@angular/core";
import {ComponentFixture, TestBed} from "@angular/core/testing";
import {MatCardModule} from "@angular/material/card";
import {TraceViewComponent} from "./trace_view.component";
import {View, Viewer, ViewType} from "viewers/viewer";
class FakeViewer implements Viewer {
constructor(title: string, content: string) {
this.title = title;
this.htmlElement = document.createElement("div");
this.htmlElement.innerText = content;
}
notifyCurrentTraceEntries(entries: any) {
// do nothing
}
getViews(): View[] {
return [new View(ViewType.TAB, this.htmlElement, this.title)];
}
getDependencies(): any[] {
return [];
}
private htmlElement: HTMLElement;
private title: string;
}
describe("TraceViewComponent", () => {
let fixture: ComponentFixture<TraceViewComponent>;
@@ -27,27 +50,22 @@ describe("TraceViewComponent", () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [TraceViewComponent],
imports: [
CommonModule,
MatCardModule
],
declarations: [TraceViewComponent],
schemas: [NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents();
fixture = TestBed.createComponent(TraceViewComponent);
component = fixture.componentInstance;
component.traceCoordinator = new TraceCoordinator();
component.viewerTabs = [
{
label: "Surface Flinger",
cardId: 0,
},
{
label: "Window Manager",
cardId: 1,
}
];
htmlElement = fixture.nativeElement;
component = fixture.componentInstance;
component.viewers = [
new FakeViewer("Title0", "Content0"),
new FakeViewer("Title1", "Content1")
];
component.ngOnChanges();
fixture.detectChanges();
});
it("can be created", () => {
@@ -56,35 +74,57 @@ describe("TraceViewComponent", () => {
});
it("creates viewer tabs", () => {
fixture.detectChanges();
const tabs = htmlElement.querySelectorAll(".viewer-tab");
const tabs: NodeList = htmlElement.querySelectorAll(".viewer-tab");
expect(tabs.length).toEqual(2);
expect(component.activeViewerCardId).toEqual(0);
expect(tabs.item(0)!.textContent).toEqual("Title0");
expect(tabs.item(1)!.textContent).toEqual("Title1");
});
it("changes active viewer on click", async () => {
fixture.detectChanges();
expect(component.activeViewerCardId).toEqual(0);
it("changes active viewer on click", () => {
const tabs = htmlElement.querySelectorAll(".viewer-tab");
tabs[0].dispatchEvent(new Event("click"));
const tabsContent =
htmlElement.querySelectorAll(".trace-view-content div");
// Initially tab 0
fixture.detectChanges();
await fixture.whenStable();
const firstId = component.activeViewerCardId;
expect(tabsContent.length).toEqual(2);
expect(tabsContent[0].innerHTML).toEqual("Content0");
expect(tabsContent[1].innerHTML).toEqual("Content1");
expect((<any>tabsContent[0]).style?.display).toEqual("");
expect((<any>tabsContent[1]).style?.display).toEqual("none");
// Switch to tab 1
tabs[1].dispatchEvent(new Event("click"));
fixture.detectChanges();
await fixture.whenStable();
const secondId = component.activeViewerCardId;
expect(firstId !== secondId).toBeTrue;
expect(tabsContent.length).toEqual(2);
expect(tabsContent[0].innerHTML).toEqual("Content0");
expect(tabsContent[1].innerHTML).toEqual("Content1");
expect((<any>tabsContent[0]).style?.display).toEqual("none");
expect((<any>tabsContent[1]).style?.display).toEqual("");
// Switch to tab 0
tabs[0].dispatchEvent(new Event("click"));
fixture.detectChanges();
expect(tabsContent.length).toEqual(2);
expect(tabsContent[0].innerHTML).toEqual("Content0");
expect(tabsContent[1].innerHTML).toEqual("Content1");
expect((<any>tabsContent[0]).style?.display).toEqual("");
expect((<any>tabsContent[1]).style?.display).toEqual("none");
});
it("downloads all traces", async () => {
spyOn(component, "downloadAllTraces").and.callThrough();
fixture.detectChanges();
const downloadButton: HTMLButtonElement | null = htmlElement.querySelector(".save-btn");
it("emits event on download button click", () => {
const spy = spyOn(component.downloadTracesButtonClick, "emit");
const downloadButton: null|HTMLButtonElement =
htmlElement.querySelector(".save-btn");
expect(downloadButton).toBeInstanceOf(HTMLButtonElement);
downloadButton?.dispatchEvent(new Event("click"));
fixture.detectChanges();
await fixture.whenStable();
expect(component.downloadAllTraces).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(1);
downloadButton?.dispatchEvent(new Event("click"));
fixture.detectChanges();
expect(spy).toHaveBeenCalledTimes(2);
});
});

View File

@@ -13,20 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Component,
Input,
Inject,
ElementRef,
} from "@angular/core";
import { TraceCoordinator } from "app/trace_coordinator";
import { PersistentStore } from "common/persistent_store";
import { FileUtils } from "common/utils/file_utils";
import { Viewer } from "viewers/viewer";
import {Component, ElementRef, EventEmitter, Inject, Input, Output} from "@angular/core";
import {PersistentStore} from "common/persistent_store";
import {Viewer, View, ViewType} from "viewers/viewer";
@Component({
selector: "trace-view",
template: `
<div class="container-overlay">
</div>
<div class="header-items-wrapper">
<nav mat-tab-nav-bar class="viewer-nav-bar">
<a
@@ -41,7 +36,7 @@ import { Viewer } from "viewers/viewer";
color="primary"
mat-button
class="save-btn"
(click)="downloadAllTraces()"
(click)="downloadTracesButtonClick.emit()"
>Download all traces</button>
</div>
<div class="trace-view-content">
@@ -49,6 +44,16 @@ import { Viewer } from "viewers/viewer";
`,
styles: [
`
.container-overlay {
z-index: 10;
position: fixed;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
pointer-events: none;
}
.header-items-wrapper {
width: 100%;
display: flex;
@@ -73,45 +78,21 @@ import { Viewer } from "viewers/viewer";
]
})
export class TraceViewComponent {
@Input() viewers!: Viewer[];
@Input() store!: PersistentStore;
@Input() traceCoordinator!: TraceCoordinator;
viewerTabs: ViewerTab[] = [];
activeViewerCardId = 0;
views: HTMLElement[] = [];
@Output() downloadTracesButtonClick = new EventEmitter<void>();
constructor(
@Inject(ElementRef) private elementRef: ElementRef,
) {}
private elementRef: ElementRef;
private viewerTabs: ViewerTab[] = [];
private activeViewerCardId = 0;
ngDoCheck() {
if (this.traceCoordinator.getViewers().length > 0 && !this.viewersAdded()) {
let cardCounter = 0;
this.activeViewerCardId = 0;
this.viewerTabs = [];
this.traceCoordinator.getViewers().forEach((viewer: Viewer) => {
// create tab for viewer nav bar
const tab = {
label: viewer.getTitle(),
cardId: cardCounter,
};
this.viewerTabs.push(tab);
constructor(@Inject(ElementRef) elementRef: ElementRef) {
this.elementRef = elementRef;
}
// add properties to view and add view to trace view card
const view = viewer.getView();
(view as any).store = this.store;
view.id = `card-${cardCounter}`;
view.style.display = this.isActiveViewerCard(cardCounter) ? "" : "none";
const traceViewContent = this.elementRef.nativeElement.querySelector(".trace-view-content")!;
traceViewContent.appendChild(view);
this.views.push(view);
cardCounter++;
});
} else if (this.traceCoordinator.getViewers().length === 0 && this.viewersAdded()) {
this.activeViewerCardId = 0;
this.views.forEach(view => view.remove());
this.views = [];
}
ngOnChanges() {
this.renderViewsTab();
this.renderViewsOverlay();
}
public showViewer(cardId: number) {
@@ -124,22 +105,56 @@ export class TraceViewComponent {
return this.activeViewerCardId === cardId;
}
public async downloadAllTraces() {
const traces = await this.traceCoordinator.getAllTracesForDownload();
const zipFileBlob = await FileUtils.createZipArchive(traces);
const zipFileName = "winscope.zip";
const a = document.createElement("a");
document.body.appendChild(a);
const url = window.URL.createObjectURL(zipFileBlob);
a.href = url;
a.download = zipFileName;
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
private renderViewsTab() {
this.activeViewerCardId = 0;
this.viewerTabs = [];
const views: View[] = this.viewers
.map(viewer => viewer.getViews())
.flat()
.filter(view => (view.type === ViewType.TAB));
for (const [cardCounter, view] of views.entries()) {
if (!view) {
continue;
}
// create tab for viewer nav bar
const tab = {
label: view.title,
cardId: cardCounter,
};
this.viewerTabs.push(tab);
// add properties to view and add view to trace view card
(view as any).store = this.store;
view.htmlElement.id = `card-${cardCounter}`;
view.htmlElement.style.display = this.isActiveViewerCard(cardCounter) ? "" : "none";
const traceViewContent = this.elementRef.nativeElement.querySelector(".trace-view-content")!;
traceViewContent.appendChild(view.htmlElement);
}
}
private viewersAdded() {
return this.views.length > 0;
private renderViewsOverlay() {
const views: View[] = this.viewers
.map(viewer => viewer.getViews())
.flat()
.filter(view => (view.type === ViewType.OVERLAY));
views.forEach(view => {
view.htmlElement.style.pointerEvents = "all";
view.htmlElement.style.position = "absolute";
view.htmlElement.style.bottom = "10%";
view.htmlElement.style.right = "0px";
const containerOverlay = this.elementRef.nativeElement.querySelector(".container-overlay");
if (!containerOverlay) {
throw new Error("Failed to find overlay container sub-element");
}
containerOverlay!.appendChild(view.htmlElement);
});
}
private isActiveViewerCard(cardId: number) {

View File

@@ -38,7 +38,6 @@ class TraceCoordinator {
traces = this.parsers.map(parser => parser.getTrace()).concat(traces);
let parserErrors: ParserError[];
[this.parsers, parserErrors] = await new ParserFactory().createParsers(traces);
console.log("created parsers: ", this.parsers);
return parserErrors;
}

View File

@@ -23,7 +23,7 @@ describe("ParserScreenRecording", () => {
let parser: Parser;
beforeAll(async () => {
parser = await UnitTestUtils.getParser("traces/elapsed_and_real_timestamp/screen_recording.mp4");
parser = await UnitTestUtils.getParser("traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4");
});
it("has expected trace type", () => {
@@ -32,14 +32,15 @@ describe("ParserScreenRecording", () => {
it ("provides elapsed timestamps", () => {
const timestamps = parser.getTimestamps(TimestampType.ELAPSED)!;
console.log(timestamps[timestamps.length-1]);
expect(timestamps.length)
.toEqual(15);
.toEqual(123);
const expected = [
new Timestamp(TimestampType.ELAPSED, 144857685000n),
new Timestamp(TimestampType.ELAPSED, 144866679000n),
new Timestamp(TimestampType.ELAPSED, 144875772000n),
new Timestamp(TimestampType.ELAPSED, 211827840430n),
new Timestamp(TimestampType.ELAPSED, 211842401430n),
new Timestamp(TimestampType.ELAPSED, 211862172430n),
];
expect(timestamps.slice(0, 3))
.toEqual(expected);
@@ -49,12 +50,12 @@ describe("ParserScreenRecording", () => {
const timestamps = parser.getTimestamps(TimestampType.REAL)!;
expect(timestamps.length)
.toEqual(15);
.toEqual(123);
const expected = [
new Timestamp(TimestampType.REAL, 1659687791485257266n),
new Timestamp(TimestampType.REAL, 1659687791494251266n),
new Timestamp(TimestampType.REAL, 1659687791503344266n),
new Timestamp(TimestampType.REAL, 1666361048792787045n),
new Timestamp(TimestampType.REAL, 1666361048807348045n),
new Timestamp(TimestampType.REAL, 1666361048827119045n),
];
expect(timestamps.slice(0, 3))
.toEqual(expected);
@@ -62,33 +63,33 @@ describe("ParserScreenRecording", () => {
it("retrieves trace entry from elapsed timestamp", () => {
{
const timestamp = new Timestamp(TimestampType.ELAPSED, 144857685000n);
const timestamp = new Timestamp(TimestampType.ELAPSED, 211827840430n);
const entry = parser.getTraceEntry(timestamp)!;
expect(entry).toBeInstanceOf(ScreenRecordingTraceEntry);
expect(Number(entry.videoTimeSeconds)).toBeCloseTo(0);
}
{
const timestamp = new Timestamp(TimestampType.ELAPSED, 145300550000n);
const timestamp = new Timestamp(TimestampType.ELAPSED, 213198917430n);
const entry = parser.getTraceEntry(timestamp)!;
expect(entry).toBeInstanceOf(ScreenRecordingTraceEntry);
expect(Number(entry.videoTimeSeconds)).toBeCloseTo(0.442, 0.001);
expect(Number(entry.videoTimeSeconds)).toBeCloseTo(1.371077000, 0.001);
}
});
it("retrieves trace entry from real timestamp", () => {
{
const timestamp = new Timestamp(TimestampType.REAL, 1659687791485257266n);
const timestamp = new Timestamp(TimestampType.REAL, 1666361048792787045n);
const entry = parser.getTraceEntry(timestamp)!;
expect(entry).toBeInstanceOf(ScreenRecordingTraceEntry);
expect(Number(entry.videoTimeSeconds)).toBeCloseTo(0);
}
{
const timestamp = new Timestamp(TimestampType.REAL, 1659687791928122266n);
const timestamp = new Timestamp(TimestampType.REAL, 1666361050163864045n);
const entry = parser.getTraceEntry(timestamp)!;
expect(entry).toBeInstanceOf(ScreenRecordingTraceEntry);
expect(Number(entry.videoTimeSeconds)).toBeCloseTo(0.322, 0.001);
expect(Number(entry.videoTimeSeconds)).toBeCloseTo(1.371077000, 0.001);
}
});
});

View File

@@ -20,7 +20,7 @@ import {Parser} from "./parser";
import {ScreenRecordingTraceEntry} from "common/trace/screen_recording";
class ScreenRecordingMetadataEntry {
constructor(public timestampMonotonicNs: bigint, public timestampRealtimeNs: bigint) {
constructor(public timestampElapsedNs: bigint, public timestampRealtimeNs: bigint) {
}
}
@@ -40,15 +40,31 @@ class ParserScreenRecording extends Parser {
override decodeTrace(videoData: Uint8Array): ScreenRecordingMetadataEntry[] {
const posVersion = this.searchMagicString(videoData);
const [posTimeOffset, metadataVersion] = this.parseMetadataVersion(videoData, posVersion);
if (metadataVersion !== 1) {
if (metadataVersion !== 1 && metadataVersion !== 2) {
throw TypeError(`Metadata version "${metadataVersion}" not supported`);
}
const [posCount, timeOffsetNs] = this.parseRealToMonotonicTimeOffsetNs(videoData, posTimeOffset);
const [posTimestamps, count] = this.parseFramesCount(videoData, posCount);
const timestampsMonotonicNs = this.parseTimestampsMonotonicNs(videoData, posTimestamps, count);
return timestampsMonotonicNs.map((timestampMonotonicNs: bigint) => {
return new ScreenRecordingMetadataEntry(timestampMonotonicNs, timestampMonotonicNs + timeOffsetNs);
if (metadataVersion === 1) {
// UI traces contain "elapsed" timestamps (SYSTEM_TIME_BOOTTIME), whereas
// metadata Version 1 contains SYSTEM_TIME_MONOTONIC timestamps.
//
// Here we are pretending that metadata Version 1 contains "elapsed"
// timestamps as well, in order to synchronize with the other traces.
//
// If no device suspensions are involved, SYSTEM_TIME_MONOTONIC should
// indeed correspond to SYSTEM_TIME_BOOTTIME and things will work as
// expected.
console.warn(`Screen recording may not be synchronized with the
other traces. Metadata contains monotonic time instead of elapsed.`);
}
const [posCount, timeOffsetNs] = this.parseRealToElapsedTimeOffsetNs(videoData, posTimeOffset);
const [posTimestamps, count] = this.parseFramesCount(videoData, posCount);
const timestampsElapsedNs = this.parseTimestampsElapsedNs(videoData, posTimestamps, count);
return timestampsElapsedNs.map((timestampElapsedNs: bigint) => {
return new ScreenRecordingMetadataEntry(timestampElapsedNs, timestampElapsedNs + timeOffsetNs);
});
}
@@ -57,15 +73,7 @@ class ParserScreenRecording extends Parser {
return undefined;
}
if (type === TimestampType.ELAPSED) {
// Traces typically contain "elapsed" timestamps (SYSTEM_TIME_BOOTTIME),
// whereas screen recordings contain SYSTEM_TIME_MONOTONIC timestamps.
//
// Here we are pretending that screen recordings contain "elapsed" timestamps
// as well, in order to synchronize with the other traces.
//
// If no device suspensions are involved, SYSTEM_TIME_MONOTONIC should indeed
// correspond to SYSTEM_TIME_BOOTTIME and things will work as expected.
return new Timestamp(type, decodedEntry.timestampMonotonicNs);
return new Timestamp(type, decodedEntry.timestampElapsedNs);
}
else if (type === TimestampType.REAL) {
return new Timestamp(type, decodedEntry.timestampRealtimeNs);
@@ -75,7 +83,7 @@ class ParserScreenRecording extends Parser {
override processDecodedEntry(index: number, entry: ScreenRecordingMetadataEntry): ScreenRecordingTraceEntry {
const initialTimestampNs = this.getTimestamps(TimestampType.ELAPSED)![0].getValueNs();
const currentTimestampNs = entry.timestampMonotonicNs;
const currentTimestampNs = entry.timestampElapsedNs;
const videoTimeSeconds = Number(currentTimestampNs - initialTimestampNs) / 1000000000;
const videoData = this.trace;
return new ScreenRecordingTraceEntry(videoTimeSeconds, videoData);
@@ -99,9 +107,9 @@ class ParserScreenRecording extends Parser {
return [pos, version];
}
private parseRealToMonotonicTimeOffsetNs(videoData: Uint8Array, pos: number) : [number, bigint] {
private parseRealToElapsedTimeOffsetNs(videoData: Uint8Array, pos: number) : [number, bigint] {
if (pos + 8 > videoData.length) {
throw new TypeError("Failed to parse realtime-to-monotonic time offset. Video data is too short.");
throw new TypeError("Failed to parse realtime-to-elapsed time offset. Video data is too short.");
}
const offset = ArrayUtils.toIntLittleEndian(videoData, pos, pos+8);
pos += 8;
@@ -117,9 +125,9 @@ class ParserScreenRecording extends Parser {
return [pos, count];
}
private parseTimestampsMonotonicNs(videoData: Uint8Array, pos: number, count: number) : bigint[] {
if (pos + count * 16 > videoData.length) {
throw new TypeError("Failed to parse monotonic timestamps. Video data is too short.");
private parseTimestampsElapsedNs(videoData: Uint8Array, pos: number, count: number) : bigint[] {
if (pos + count * 8 > videoData.length) {
throw new TypeError("Failed to parse timestamps. Video data is too short.");
}
const timestamps: bigint[] = [];
for (let i = 0; i < count; ++i) {

View File

@@ -0,0 +1,45 @@
/*
* 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 ScreenRecording", () => {
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/screen_recording_metadata_v2.mp4"));
const loadData = element(by.css(".load-btn"));
await loadData.click();
const viewer = element(by.css("viewer-screen-recording"));
expect(await viewer.isPresent())
.toBeTruthy();
const video = element(by.css("viewer-screen-recording video"));
expect(await video.isPresent())
.toBeTruthy();
expect(await video.getAttribute("src"))
.toContain("blob:");
expect(await video.getAttribute("currentTime"))
.toEqual("0");
});
});

View File

@@ -14,14 +14,14 @@
* limitations under the License.
*/
import {TraceType} from "common/trace/trace_type";
import {Viewer} from "viewers/viewer";
import {View, Viewer, ViewType} from "viewers/viewer";
import {ViewerEvents} from "viewers/common/viewer_events";
import { PresenterInputMethod } from "viewers/common/presenter_input_method";
import { ImeUiData } from "viewers/common/ime_ui_data";
import {PresenterInputMethod} from "viewers/common/presenter_input_method";
import {ImeUiData} from "viewers/common/ime_ui_data";
abstract class ViewerInputMethod implements Viewer {
constructor() {
this.view = document.createElement("viewer-input-method");
this.htmlElement = document.createElement("viewer-input-method");
this.presenter = this.initialisePresenter();
this.addViewerEventListeners();
}
@@ -30,35 +30,31 @@ abstract class ViewerInputMethod implements Viewer {
this.presenter.notifyCurrentTraceEntries(entries);
}
public getView(): HTMLElement {
return this.view;
}
public abstract getViews(): View[];
public abstract getDependencies(): TraceType[];
protected imeUiCallback = (uiData: ImeUiData) => {
// Angular does not deep watch @Input properties. Clearing inputData to null before repopulating
// automatically ensures that the UI will change via the Angular change detection cycle. Without
// resetting, Angular does not auto-detect that inputData has changed.
(this.view as any).inputData = null;
(this.view as any).inputData = uiData;
(this.htmlElement as any).inputData = null;
(this.htmlElement as any).inputData = uiData;
};
protected addViewerEventListeners() {
this.view.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) => this.presenter.updatePinnedItems(((event as CustomEvent).detail.pinnedItem)));
this.view.addEventListener(ViewerEvents.HighlightedChange, (event) => this.presenter.updateHighlightedItems(`${(event as CustomEvent).detail.id}`));
this.view.addEventListener(ViewerEvents.HierarchyUserOptionsChange, (event) => this.presenter.updateHierarchyTree((event as CustomEvent).detail.userOptions));
this.view.addEventListener(ViewerEvents.HierarchyFilterChange, (event) => this.presenter.filterHierarchyTree((event as CustomEvent).detail.filterString));
this.view.addEventListener(ViewerEvents.PropertiesUserOptionsChange, (event) => this.presenter.updatePropertiesTree((event as CustomEvent).detail.userOptions));
this.view.addEventListener(ViewerEvents.PropertiesFilterChange, (event) => this.presenter.filterPropertiesTree((event as CustomEvent).detail.filterString));
this.view.addEventListener(ViewerEvents.SelectedTreeChange, (event) => this.presenter.newPropertiesTree((event as CustomEvent).detail.selectedItem));
this.view.addEventListener(ViewerEvents.AdditionalPropertySelected, (event) => this.presenter.newAdditionalPropertiesTree((event as CustomEvent).detail.selectedItem));
this.htmlElement.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) => this.presenter.updatePinnedItems(((event as CustomEvent).detail.pinnedItem)));
this.htmlElement.addEventListener(ViewerEvents.HighlightedChange, (event) => this.presenter.updateHighlightedItems(`${(event as CustomEvent).detail.id}`));
this.htmlElement.addEventListener(ViewerEvents.HierarchyUserOptionsChange, (event) => this.presenter.updateHierarchyTree((event as CustomEvent).detail.userOptions));
this.htmlElement.addEventListener(ViewerEvents.HierarchyFilterChange, (event) => this.presenter.filterHierarchyTree((event as CustomEvent).detail.filterString));
this.htmlElement.addEventListener(ViewerEvents.PropertiesUserOptionsChange, (event) => this.presenter.updatePropertiesTree((event as CustomEvent).detail.userOptions));
this.htmlElement.addEventListener(ViewerEvents.PropertiesFilterChange, (event) => this.presenter.filterPropertiesTree((event as CustomEvent).detail.filterString));
this.htmlElement.addEventListener(ViewerEvents.SelectedTreeChange, (event) => this.presenter.newPropertiesTree((event as CustomEvent).detail.selectedItem));
this.htmlElement.addEventListener(ViewerEvents.AdditionalPropertySelected, (event) => this.presenter.newAdditionalPropertiesTree((event as CustomEvent).detail.selectedItem));
}
abstract getDependencies(): TraceType[];
abstract getTitle(): string;
protected abstract initialisePresenter(): PresenterInputMethod;
protected view: HTMLElement;
protected htmlElement: HTMLElement;
protected presenter: PresenterInputMethod;
}

View File

@@ -15,11 +15,24 @@
*/
import { TraceType } from "common/trace/trace_type";
enum ViewType {
TAB,
OVERLAY
}
class View {
constructor(
public type: ViewType,
public htmlElement: HTMLElement,
public title: string
) {
}
}
interface Viewer {
notifyCurrentTraceEntries(entries: Map<TraceType, any>): void;
getView(): HTMLElement;
getTitle(): string;
getViews(): View[];
getDependencies(): TraceType[];
}
export { Viewer };
export {Viewer, View, ViewType};

View File

@@ -22,6 +22,7 @@ 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";
import {ViewerScreenRecording} from "./viewer_screen_recording/viewer_screen_recording";
class ViewerFactory {
static readonly VIEWERS = [
@@ -29,6 +30,7 @@ class ViewerFactory {
ViewerInputMethodManagerService,
ViewerInputMethodService,
ViewerProtoLog,
ViewerScreenRecording,
ViewerSurfaceFlinger,
ViewerTransactions,
ViewerWindowManager,

View File

@@ -14,19 +14,20 @@
* limitations under the License.
*/
import {TraceType} from "common/trace/trace_type";
import { PresenterInputMethodClients } from "./presenter_input_method_clients";
import { ViewerInputMethod } from "viewers/common/viewer_input_method";
import {PresenterInputMethodClients} from "./presenter_input_method_clients";
import {ViewerInputMethod} from "viewers/common/viewer_input_method";
import {View, ViewType} from "viewers/viewer";
class ViewerInputMethodClients extends ViewerInputMethod {
public getTitle(): string {
return "Input Method Clients";
override getViews(): View[] {
return [new View(ViewType.TAB, this.htmlElement, "Input Method Clients")];
}
public getDependencies(): TraceType[] {
override getDependencies(): TraceType[] {
return ViewerInputMethodClients.DEPENDENCIES;
}
protected initialisePresenter() {
override initialisePresenter() {
return new PresenterInputMethodClients(this.imeUiCallback, this.getDependencies());
}

View File

@@ -14,19 +14,24 @@
* limitations under the License.
*/
import {TraceType} from "common/trace/trace_type";
import { PresenterInputMethodManagerService } from "./presenter_input_method_manager_service";
import { ViewerInputMethod } from "viewers/common/viewer_input_method";
import {PresenterInputMethodManagerService} from "./presenter_input_method_manager_service";
import {ViewerInputMethod} from "viewers/common/viewer_input_method";
import {View, ViewType} from "viewers/viewer";
class ViewerInputMethodManagerService extends ViewerInputMethod {
public getTitle(): string {
return "Input Method Manager Service";
override getViews(): View[] {
return [
new View(ViewType.TAB,
this.htmlElement,
"Input Method Manager Service")
];
}
public getDependencies(): TraceType[] {
override getDependencies(): TraceType[] {
return ViewerInputMethodManagerService.DEPENDENCIES;
}
protected initialisePresenter() {
override initialisePresenter() {
return new PresenterInputMethodManagerService(this.imeUiCallback, this.getDependencies());
}

View File

@@ -14,19 +14,26 @@
* limitations under the License.
*/
import {TraceType} from "common/trace/trace_type";
import { PresenterInputMethodService } from "./presenter_input_method_service";
import { ViewerInputMethod } from "viewers/common/viewer_input_method";
import {PresenterInputMethodService} from "./presenter_input_method_service";
import {ViewerInputMethod} from "viewers/common/viewer_input_method";
import {View, ViewType} from "viewers/viewer";
class ViewerInputMethodService extends ViewerInputMethod {
public getTitle(): string {
return "Input Method Service";
override getViews(): View[] {
return [
new View(
ViewType.TAB,
this.htmlElement,
"Input Method Service"
)
];
}
public getDependencies(): TraceType[] {
override getDependencies(): TraceType[] {
return ViewerInputMethodService.DEPENDENCIES;
}
protected initialisePresenter() {
override initialisePresenter() {
return new PresenterInputMethodService(this.imeUiCallback, this.getDependencies());
}

View File

@@ -14,29 +14,29 @@
* limitations under the License.
*/
import {TraceType} from "common/trace/trace_type";
import {Viewer} from "viewers/viewer";
import {View, Viewer, ViewType} 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.htmlElement = document.createElement("viewer-protolog");
this.presenter = new Presenter((data: UiData) => {
(this.view as any).inputData = data;
(this.htmlElement as any).inputData = data;
});
this.view.addEventListener(Events.LogLevelsFilterChanged, (event) => {
this.htmlElement.addEventListener(Events.LogLevelsFilterChanged, (event) => {
return this.presenter.onLogLevelsFilterChanged((event as CustomEvent).detail);
});
this.view.addEventListener(Events.TagsFilterChanged, (event) => {
this.htmlElement.addEventListener(Events.TagsFilterChanged, (event) => {
return this.presenter.onTagsFilterChanged((event as CustomEvent).detail);
});
this.view.addEventListener(Events.SourceFilesFilterChanged, (event) => {
this.htmlElement.addEventListener(Events.SourceFilesFilterChanged, (event) => {
return this.presenter.onSourceFilesFilterChanged((event as CustomEvent).detail);
});
this.view.addEventListener(Events.SearchStringFilterChanged, (event) => {
this.htmlElement.addEventListener(Events.SearchStringFilterChanged, (event) => {
return this.presenter.onSearchStringFilterChanged((event as CustomEvent).detail);
});
}
@@ -45,12 +45,8 @@ class ViewerProtoLog implements Viewer {
this.presenter.notifyCurrentTraceEntries(entries);
}
public getView(): HTMLElement {
return this.view;
}
public getTitle() {
return "ProtoLog";
public getViews(): View[] {
return [new View(ViewType.TAB, this.htmlElement, "ProtoLog")];
}
public getDependencies(): TraceType[] {
@@ -58,7 +54,7 @@ class ViewerProtoLog implements Viewer {
}
public static readonly DEPENDENCIES: TraceType[] = [TraceType.PROTO_LOG];
private view: HTMLElement;
private htmlElement: HTMLElement;
private presenter: Presenter;
}

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 { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import {ComponentFixture, ComponentFixtureAutoDetect, TestBed} from "@angular/core/testing";
import {ViewerScreenRecordingComponent} from "./viewer_screen_recording.component";
describe("ViewerScreenRecordingComponent", () => {
let fixture: ComponentFixture<ViewerScreenRecordingComponent>;
let component: ViewerScreenRecordingComponent;
let htmlElement: HTMLElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true }
],
declarations: [
ViewerScreenRecordingComponent,
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents();
fixture = TestBed.createComponent(ViewerScreenRecordingComponent);
component = fixture.componentInstance;
htmlElement = fixture.nativeElement;
fixture.detectChanges();
});
it("can be created", () => {
expect(component).toBeTruthy();
});
it("can be minimized and maximized", () => {
const buttonMinimize = htmlElement.querySelector(".button-minimize");
const video = htmlElement.querySelector("video");
expect(buttonMinimize).toBeTruthy();
expect(video).toBeTruthy();
expect(video!.style.visibility).toEqual("visible");
buttonMinimize!.dispatchEvent(new Event("click"));
fixture.detectChanges();
expect(video!.style.visibility).toEqual("hidden");
buttonMinimize!.dispatchEvent(new Event("click"));
fixture.detectChanges();
expect(video!.style.visibility).toEqual("visible");
});
});

View File

@@ -0,0 +1,114 @@
/*
* 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 {Component, ElementRef, Inject, Input} from "@angular/core";
import {DomSanitizer, SafeUrl} from "@angular/platform-browser";
import {ScreenRecordingTraceEntry} from "common/trace/screen_recording";
@Component({
selector: "viewer-screen-recording",
template: `
<div class="container" cdkDrag cdkDragBoundary=".container-overlay">
<div class="header">
<button mat-button class="button-drag" cdkDragHandle>
<mat-icon class="drag-icon">drag_indicator</mat-icon>
<span class="mat-body-2">Screen recording</span>
</button>
<button mat-button class="button-minimize"
(click)="onMinimizeButtonClick()">
<mat-icon>
{{isMinimized ? "maximize" : "minimize"}}
</mat-icon>
</button>
</div>
<video
[currentTime]="videoCurrentTime"
[src]=videoUrl
[style.visibility]="isMinimized ? 'hidden' : 'visible'"
cdkDragHandle>
</video>
</div>
`,
styles: [
`
.container {
width: fit-content;
height: fit-content;
display: flex;
flex-direction: column;
}
.header {
background-color: white;
border: 1px solid var(--default-border);
display: flex;
flex-direction: row;
}
.button-drag {
flex-grow: 1;
cursor: grab;
}
.drag-icon {
float: left;
}
.button-minimize {
flex-grow: 0;
}
video {
height: 50vh;
cursor: grab;
}
`,
]
})
class ViewerScreenRecordingComponent {
constructor(
@Inject(ElementRef) elementRef: ElementRef,
@Inject(DomSanitizer) sanitizer: DomSanitizer) {
this.elementRef = elementRef;
this.sanitizer = sanitizer;
}
@Input()
public set currentTraceEntry(entry: undefined|ScreenRecordingTraceEntry) {
if (entry === undefined) {
return;
}
if (this.videoUrl === undefined) {
this.videoUrl = this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(entry.videoData));
}
this.videoCurrentTime = entry.videoTimeSeconds;
}
public onMinimizeButtonClick() {
this.isMinimized = !this.isMinimized;
}
public videoUrl: undefined|SafeUrl = undefined;
public videoCurrentTime = 0;
public isMinimized = false;
private elementRef: ElementRef;
private sanitizer: DomSanitizer;
}
export {ViewerScreenRecordingComponent};

View File

@@ -0,0 +1,45 @@
/*
* 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 {View, Viewer, ViewType} from "viewers/viewer";
import {ScreenRecordingTraceEntry} from "common/trace/screen_recording";
class ViewerScreenRecording implements Viewer {
constructor() {
this.htmlElement = document.createElement("viewer-screen-recording");
}
public notifyCurrentTraceEntries(entries: Map<TraceType, any>): void {
const entry: undefined | ScreenRecordingTraceEntry = entries.get(TraceType.SCREEN_RECORDING)
? entries.get(TraceType.SCREEN_RECORDING)[0]
: undefined;
(<any>this.htmlElement).currentTraceEntry = entry;
}
public getViews(): View[] {
return [new View(ViewType.OVERLAY, this.htmlElement, "ScreenRecording")];
}
public getDependencies(): TraceType[] {
return ViewerScreenRecording.DEPENDENCIES;
}
public static readonly DEPENDENCIES: TraceType[] = [TraceType.SCREEN_RECORDING];
private htmlElement: HTMLElement;
}
export {ViewerScreenRecording};

View File

@@ -14,40 +14,36 @@
* limitations under the License.
*/
import {TraceType} from "common/trace/trace_type";
import {Viewer} from "viewers/viewer";
import {View, Viewer, ViewType} from "viewers/viewer";
import {Presenter} from "./presenter";
import {UiData} from "./ui_data";
import {ViewerEvents} from "viewers/common/viewer_events";
class ViewerSurfaceFlinger implements Viewer {
constructor() {
this.view = document.createElement("viewer-surface-flinger");
this.htmlElement = document.createElement("viewer-surface-flinger");
this.presenter = new Presenter((uiData: UiData) => {
// Angular does not deep watch @Input properties. Clearing inputData to null before repopulating
// automatically ensures that the UI will change via the Angular change detection cycle. Without
// resetting, Angular does not auto-detect that inputData has changed.
(this.view as any).inputData = null;
(this.view as any).inputData = uiData;
(this.htmlElement as any).inputData = null;
(this.htmlElement as any).inputData = uiData;
});
this.view.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) => this.presenter.updatePinnedItems(((event as CustomEvent).detail.pinnedItem)));
this.view.addEventListener(ViewerEvents.HighlightedChange, (event) => this.presenter.updateHighlightedItems(`${(event as CustomEvent).detail.id}`));
this.view.addEventListener(ViewerEvents.HierarchyUserOptionsChange, (event) => this.presenter.updateHierarchyTree((event as CustomEvent).detail.userOptions));
this.view.addEventListener(ViewerEvents.HierarchyFilterChange, (event) => this.presenter.filterHierarchyTree((event as CustomEvent).detail.filterString));
this.view.addEventListener(ViewerEvents.PropertiesUserOptionsChange, (event) => this.presenter.updatePropertiesTree((event as CustomEvent).detail.userOptions));
this.view.addEventListener(ViewerEvents.PropertiesFilterChange, (event) => this.presenter.filterPropertiesTree((event as CustomEvent).detail.filterString));
this.view.addEventListener(ViewerEvents.SelectedTreeChange, (event) => this.presenter.newPropertiesTree((event as CustomEvent).detail.selectedItem));
this.htmlElement.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) => this.presenter.updatePinnedItems(((event as CustomEvent).detail.pinnedItem)));
this.htmlElement.addEventListener(ViewerEvents.HighlightedChange, (event) => this.presenter.updateHighlightedItems(`${(event as CustomEvent).detail.id}`));
this.htmlElement.addEventListener(ViewerEvents.HierarchyUserOptionsChange, (event) => this.presenter.updateHierarchyTree((event as CustomEvent).detail.userOptions));
this.htmlElement.addEventListener(ViewerEvents.HierarchyFilterChange, (event) => this.presenter.filterHierarchyTree((event as CustomEvent).detail.filterString));
this.htmlElement.addEventListener(ViewerEvents.PropertiesUserOptionsChange, (event) => this.presenter.updatePropertiesTree((event as CustomEvent).detail.userOptions));
this.htmlElement.addEventListener(ViewerEvents.PropertiesFilterChange, (event) => this.presenter.filterPropertiesTree((event as CustomEvent).detail.filterString));
this.htmlElement.addEventListener(ViewerEvents.SelectedTreeChange, (event) => this.presenter.newPropertiesTree((event as CustomEvent).detail.selectedItem));
}
public notifyCurrentTraceEntries(entries: Map<TraceType, any>): void {
this.presenter.notifyCurrentTraceEntries(entries);
}
public getView(): HTMLElement {
return this.view;
}
public getTitle(): string {
return "Surface Flinger";
public getViews(): View[] {
return [new View(ViewType.TAB, this.htmlElement, "Surface Flinger")];
}
public getDependencies(): TraceType[] {
@@ -55,7 +51,7 @@ class ViewerSurfaceFlinger implements Viewer {
}
public static readonly DEPENDENCIES: TraceType[] = [TraceType.SURFACE_FLINGER];
private view: HTMLElement;
private htmlElement: HTMLElement;
private presenter: Presenter;
}

View File

@@ -14,36 +14,36 @@
* limitations under the License.
*/
import {TraceType} from "common/trace/trace_type";
import {Viewer} from "viewers/viewer";
import {View, Viewer, ViewType} 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.htmlElement = document.createElement("viewer-transactions");
this.presenter = new Presenter((data: UiData) => {
(this.view as any).inputData = data;
(this.htmlElement as any).inputData = data;
});
this.view.addEventListener(Events.PidFilterChanged, (event) => {
this.htmlElement.addEventListener(Events.PidFilterChanged, (event) => {
this.presenter.onPidFilterChanged((event as CustomEvent).detail);
});
this.view.addEventListener(Events.UidFilterChanged, (event) => {
this.htmlElement.addEventListener(Events.UidFilterChanged, (event) => {
this.presenter.onUidFilterChanged((event as CustomEvent).detail);
});
this.view.addEventListener(Events.TypeFilterChanged, (event) => {
this.htmlElement.addEventListener(Events.TypeFilterChanged, (event) => {
this.presenter.onTypeFilterChanged((event as CustomEvent).detail);
});
this.view.addEventListener(Events.IdFilterChanged, (event) => {
this.htmlElement.addEventListener(Events.IdFilterChanged, (event) => {
this.presenter.onIdFilterChanged((event as CustomEvent).detail);
});
this.view.addEventListener(Events.EntryClicked, (event) => {
this.htmlElement.addEventListener(Events.EntryClicked, (event) => {
this.presenter.onEntryClicked((event as CustomEvent).detail);
});
}
@@ -52,12 +52,8 @@ class ViewerTransactions implements Viewer {
this.presenter.notifyCurrentTraceEntries(entries);
}
public getView(): HTMLElement {
return this.view;
}
public getTitle() {
return "Transactions";
public getViews(): View[] {
return [new View(ViewType.TAB, this.htmlElement, "Transactions")];
}
public getDependencies(): TraceType[] {
@@ -65,7 +61,7 @@ class ViewerTransactions implements Viewer {
}
public static readonly DEPENDENCIES: TraceType[] = [TraceType.TRANSACTIONS];
private view: HTMLElement;
private htmlElement: HTMLElement;
private presenter: Presenter;
}

View File

@@ -14,40 +14,36 @@
* limitations under the License.
*/
import {TraceType} from "common/trace/trace_type";
import {Viewer} from "viewers/viewer";
import {Viewer, View, ViewType} from "viewers/viewer";
import {Presenter} from "./presenter";
import {UiData} from "./ui_data";
import { ViewerEvents } from "viewers/common/viewer_events";
class ViewerWindowManager implements Viewer {
constructor() {
this.view = document.createElement("viewer-window-manager");
this.htmlElement = document.createElement("viewer-window-manager");
this.presenter = new Presenter((uiData: UiData) => {
// Angular does not deep watch @Input properties. Clearing inputData to null before repopulating
// automatically ensures that the UI will change via the Angular change detection cycle. Without
// resetting, Angular does not auto-detect that inputData has changed.
(this.view as any).inputData = null;
(this.view as any).inputData = uiData;
(this.htmlElement as any).inputData = null;
(this.htmlElement as any).inputData = uiData;
});
this.view.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) => this.presenter.updatePinnedItems(((event as CustomEvent).detail.pinnedItem)));
this.view.addEventListener(ViewerEvents.HighlightedChange, (event) => this.presenter.updateHighlightedItems(`${(event as CustomEvent).detail.id}`));
this.view.addEventListener(ViewerEvents.HierarchyUserOptionsChange, (event) => this.presenter.updateHierarchyTree((event as CustomEvent).detail.userOptions));
this.view.addEventListener(ViewerEvents.HierarchyFilterChange, (event) => this.presenter.filterHierarchyTree((event as CustomEvent).detail.filterString));
this.view.addEventListener(ViewerEvents.PropertiesUserOptionsChange, (event) => this.presenter.updatePropertiesTree((event as CustomEvent).detail.userOptions));
this.view.addEventListener(ViewerEvents.PropertiesFilterChange, (event) => this.presenter.filterPropertiesTree((event as CustomEvent).detail.filterString));
this.view.addEventListener(ViewerEvents.SelectedTreeChange, (event) => this.presenter.newPropertiesTree((event as CustomEvent).detail.selectedItem));
this.htmlElement.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) => this.presenter.updatePinnedItems(((event as CustomEvent).detail.pinnedItem)));
this.htmlElement.addEventListener(ViewerEvents.HighlightedChange, (event) => this.presenter.updateHighlightedItems(`${(event as CustomEvent).detail.id}`));
this.htmlElement.addEventListener(ViewerEvents.HierarchyUserOptionsChange, (event) => this.presenter.updateHierarchyTree((event as CustomEvent).detail.userOptions));
this.htmlElement.addEventListener(ViewerEvents.HierarchyFilterChange, (event) => this.presenter.filterHierarchyTree((event as CustomEvent).detail.filterString));
this.htmlElement.addEventListener(ViewerEvents.PropertiesUserOptionsChange, (event) => this.presenter.updatePropertiesTree((event as CustomEvent).detail.userOptions));
this.htmlElement.addEventListener(ViewerEvents.PropertiesFilterChange, (event) => this.presenter.filterPropertiesTree((event as CustomEvent).detail.filterString));
this.htmlElement.addEventListener(ViewerEvents.SelectedTreeChange, (event) => this.presenter.newPropertiesTree((event as CustomEvent).detail.selectedItem));
}
public notifyCurrentTraceEntries(entries: Map<TraceType, any>): void {
this.presenter.notifyCurrentTraceEntries(entries);
}
public getView(): HTMLElement {
return this.view;
}
public getTitle() {
return "Window Manager";
public getViews(): View[] {
return [new View(ViewType.TAB, this.htmlElement, "Window Manager")];
}
public getDependencies(): TraceType[] {
@@ -55,7 +51,7 @@ class ViewerWindowManager implements Viewer {
}
public static readonly DEPENDENCIES: TraceType[] = [TraceType.WINDOW_MANAGER];
private view: HTMLElement;
private htmlElement: HTMLElement;
private presenter: Presenter;
}