Add transition trace viewer
Bug: 277181336 Test: npm run test:all Change-Id: I7eb981367191a47d3b50b31ee66f147ff1d6923a
This commit is contained in:
@@ -57,6 +57,7 @@ import {ViewerProtologComponent} from 'viewers/viewer_protolog/viewer_protolog_c
|
||||
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 {ViewerTransitionsComponent} from 'viewers/viewer_transitions/viewer_transitions_component';
|
||||
import {ViewerWindowManagerComponent} from 'viewers/viewer_window_manager/viewer_window_manager_component';
|
||||
import {AdbProxyComponent} from './components/adb_proxy_component';
|
||||
import {AppComponent} from './components/app_component';
|
||||
@@ -86,6 +87,7 @@ import {WebAdbComponent} from './components/web_adb_component';
|
||||
ViewerProtologComponent,
|
||||
ViewerTransactionsComponent,
|
||||
ViewerScreenRecordingComponent,
|
||||
ViewerTransitionsComponent,
|
||||
CollectTracesComponent,
|
||||
UploadTracesComponent,
|
||||
AdbProxyComponent,
|
||||
|
||||
@@ -41,6 +41,7 @@ import {ViewerProtologComponent} from 'viewers/viewer_protolog/viewer_protolog_c
|
||||
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 {ViewerTransitionsComponent} from 'viewers/viewer_transitions/viewer_transitions_component';
|
||||
import {ViewerWindowManagerComponent} from 'viewers/viewer_window_manager/viewer_window_manager_component';
|
||||
import {CollectTracesComponent} from './collect_traces_component';
|
||||
import {SnackBarOpener} from './snack_bar_opener';
|
||||
@@ -264,6 +265,12 @@ export class AppComponent implements TraceDataListener {
|
||||
createCustomElement(ViewerWindowManagerComponent, {injector})
|
||||
);
|
||||
}
|
||||
if (!customElements.get('viewer-transitions')) {
|
||||
customElements.define(
|
||||
'viewer-transitions',
|
||||
createCustomElement(ViewerTransitionsComponent, {injector})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
|
||||
@@ -108,7 +108,7 @@ class TracePipeline {
|
||||
this.parsers = [];
|
||||
this.traces = undefined;
|
||||
this.commonTimestampType = undefined;
|
||||
this.files.clear();
|
||||
this.files = new Map<TraceType, TraceFile>();
|
||||
}
|
||||
|
||||
private getCommonTimestampType(): TimestampType {
|
||||
|
||||
38
tools/winscope/src/test/e2e/viewer_transitions_test.ts
Normal file
38
tools/winscope/src/test/e2e/viewer_transitions_test.ts
Normal 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, by, element} from 'protractor';
|
||||
import {E2eTestUtils} from './utils';
|
||||
|
||||
describe('Viewer Transitions', () => {
|
||||
beforeAll(async () => {
|
||||
browser.manage().timeouts().implicitlyWait(1000);
|
||||
browser.get('file://' + E2eTestUtils.getProductionIndexHtmlPath());
|
||||
});
|
||||
it('processes trace and renders view', async () => {
|
||||
await E2eTestUtils.uploadFixture('traces/elapsed_and_real_timestamp/wm_transition_trace.pb');
|
||||
await E2eTestUtils.uploadFixture('traces/elapsed_and_real_timestamp/shell_transition_trace.pb');
|
||||
await E2eTestUtils.closeSnackBarIfNeeded();
|
||||
await E2eTestUtils.clickViewTracesButton();
|
||||
|
||||
const isViewerRendered = await element(by.css('viewer-transitions')).isPresent();
|
||||
expect(isViewerRendered).toBeTruthy();
|
||||
|
||||
const isFirstEntryRendered = await element(
|
||||
by.css('viewer-transitions .scroll .entry')
|
||||
).isPresent();
|
||||
expect(isFirstEntryRendered).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -24,6 +24,7 @@ import {ViewerProtoLog} from './viewer_protolog/viewer_protolog';
|
||||
import {ViewerScreenRecording} from './viewer_screen_recording/viewer_screen_recording';
|
||||
import {ViewerSurfaceFlinger} from './viewer_surface_flinger/viewer_surface_flinger';
|
||||
import {ViewerTransactions} from './viewer_transactions/viewer_transactions';
|
||||
import {ViewerTransitions} from './viewer_transitions/viewer_transitions';
|
||||
import {ViewerWindowManager} from './viewer_window_manager/viewer_window_manager';
|
||||
|
||||
class ViewerFactory {
|
||||
@@ -39,6 +40,7 @@ class ViewerFactory {
|
||||
ViewerTransactions,
|
||||
ViewerProtoLog,
|
||||
ViewerScreenRecording,
|
||||
ViewerTransitions,
|
||||
];
|
||||
|
||||
createViewers(activeTraceTypes: Set<TraceType>, traces: Traces, storage: Storage): Viewer[] {
|
||||
|
||||
21
tools/winscope/src/viewers/viewer_transitions/events.ts
Normal file
21
tools/winscope/src/viewers/viewer_transitions/events.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 {
|
||||
static TransitionSelected = 'ViewerTransitionsEvent_TransitionSelected';
|
||||
}
|
||||
|
||||
export {Events};
|
||||
221
tools/winscope/src/viewers/viewer_transitions/presenter.ts
Normal file
221
tools/winscope/src/viewers/viewer_transitions/presenter.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 {assertDefined} from 'common/assert_utils';
|
||||
import {TimeUtils} from 'common/time_utils';
|
||||
import {LayerTraceEntry, Transition, WindowManagerState} from 'trace/flickerlib/common';
|
||||
import {ElapsedTimestamp} from 'trace/timestamp';
|
||||
import {Trace} from 'trace/trace';
|
||||
import {Traces} from 'trace/traces';
|
||||
import {TraceEntryFinder} from 'trace/trace_entry_finder';
|
||||
import {TracePosition} from 'trace/trace_position';
|
||||
import {TraceType} from 'trace/trace_type';
|
||||
import {PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
|
||||
import {UiData} from './ui_data';
|
||||
|
||||
export class Presenter {
|
||||
constructor(traces: Traces, notifyUiDataCallback: (data: UiData) => void) {
|
||||
this.transitionTrace = assertDefined(traces.getTrace(TraceType.TRANSITION));
|
||||
this.surfaceFlingerTrace = traces.getTrace(TraceType.SURFACE_FLINGER);
|
||||
this.windowManagerTrace = traces.getTrace(TraceType.WINDOW_MANAGER);
|
||||
this.notifyUiDataCallback = notifyUiDataCallback;
|
||||
this.uiData = this.computeUiData();
|
||||
this.notifyUiDataCallback(this.uiData);
|
||||
}
|
||||
|
||||
onTracePositionUpdate(position: TracePosition): void {
|
||||
const entry = TraceEntryFinder.findCorrespondingEntry(this.transitionTrace, position);
|
||||
|
||||
this.uiData.selectedTransition = entry?.getValue();
|
||||
|
||||
this.notifyUiDataCallback(this.uiData);
|
||||
}
|
||||
|
||||
onTransitionSelected(transition: Transition): void {
|
||||
this.uiData.selectedTransition = transition;
|
||||
this.uiData.selectedTransitionPropertiesTree =
|
||||
this.makeSelectedTransitionPropertiesTree(transition);
|
||||
this.notifyUiDataCallback(this.uiData);
|
||||
}
|
||||
|
||||
private computeUiData(): UiData {
|
||||
const transitions: Transition[] = [];
|
||||
|
||||
this.transitionTrace.forEachEntry((entry, originalIndex) => {
|
||||
transitions.push(entry.getValue());
|
||||
});
|
||||
|
||||
const selectedTransition = this.uiData?.selectedTransition ?? undefined;
|
||||
const selectedTransitionPropertiesTree =
|
||||
this.uiData?.selectedTransitionPropertiesTree ?? undefined;
|
||||
return new UiData(transitions, selectedTransition, selectedTransitionPropertiesTree);
|
||||
}
|
||||
|
||||
private makeSelectedTransitionPropertiesTree(transition: Transition): PropertiesTreeNode {
|
||||
const changes: PropertiesTreeNode[] = [];
|
||||
|
||||
for (const change of transition.changes) {
|
||||
let layerName: string | undefined = undefined;
|
||||
let windowName: string | undefined = undefined;
|
||||
|
||||
if (this.surfaceFlingerTrace) {
|
||||
this.surfaceFlingerTrace.forEachEntry((entry, originalIndex) => {
|
||||
if (layerName !== undefined) {
|
||||
return;
|
||||
}
|
||||
const layerTraceEntry = entry.getValue() as LayerTraceEntry;
|
||||
for (const layer of layerTraceEntry.flattenedLayers) {
|
||||
if (layer.id === change.layerId) {
|
||||
layerName = layer.name;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.windowManagerTrace) {
|
||||
this.windowManagerTrace.forEachEntry((entry, originalIndex) => {
|
||||
if (windowName !== undefined) {
|
||||
return;
|
||||
}
|
||||
const wmState = entry.getValue() as WindowManagerState;
|
||||
for (const window of wmState.windowContainers) {
|
||||
if (window.token.toLowerCase() === change.windowId.toString(16).toLowerCase()) {
|
||||
windowName = window.title;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const layerIdValue = layerName ? `${change.layerId} (${layerName})` : change.layerId;
|
||||
const windowIdValue = windowName
|
||||
? `0x${change.windowId.toString(16)} (${windowName})`
|
||||
: `0x${change.windowId.toString(16)}`;
|
||||
|
||||
changes.push({
|
||||
propertyKey: 'change',
|
||||
children: [
|
||||
{propertyKey: 'transitMode', propertyValue: change.transitMode},
|
||||
{propertyKey: 'layerId', propertyValue: layerIdValue},
|
||||
{propertyKey: 'windowId', propertyValue: windowIdValue},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const properties: PropertiesTreeNode[] = [
|
||||
{propertyKey: 'id', propertyValue: transition.id},
|
||||
{propertyKey: 'type', propertyValue: transition.type},
|
||||
{propertyKey: 'aborted', propertyValue: `${transition.aborted}`},
|
||||
];
|
||||
|
||||
if (transition.handler) {
|
||||
properties.push({propertyKey: 'handler', propertyValue: transition.handler});
|
||||
}
|
||||
|
||||
if (!transition.createTime.isMin) {
|
||||
properties.push({
|
||||
propertyKey: 'createTime',
|
||||
propertyValue: TimeUtils.format(
|
||||
new ElapsedTimestamp(BigInt(transition.createTime.elapsedNanos.toString()))
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (!transition.sendTime.isMin) {
|
||||
properties.push({
|
||||
propertyKey: 'sendTime',
|
||||
propertyValue: TimeUtils.format(
|
||||
new ElapsedTimestamp(BigInt(transition.sendTime.elapsedNanos.toString()))
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (!transition.dispatchTime.isMin) {
|
||||
properties.push({
|
||||
propertyKey: 'dispatchTime',
|
||||
propertyValue: TimeUtils.format(
|
||||
new ElapsedTimestamp(BigInt(transition.dispatchTime.elapsedNanos.toString()))
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (!transition.finishTime.isMax) {
|
||||
properties.push({
|
||||
propertyKey: 'finishTime',
|
||||
propertyValue: TimeUtils.format(
|
||||
new ElapsedTimestamp(BigInt(transition.finishTime.elapsedNanos.toString()))
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (transition.mergeRequestTime) {
|
||||
properties.push({
|
||||
propertyKey: 'mergeRequestTime',
|
||||
propertyValue: TimeUtils.format(
|
||||
new ElapsedTimestamp(BigInt(transition.mergeRequestTime.elapsedNanos.toString()))
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (transition.shellAbortTime) {
|
||||
properties.push({
|
||||
propertyKey: 'shellAbortTime',
|
||||
propertyValue: TimeUtils.format(
|
||||
new ElapsedTimestamp(BigInt(transition.shellAbortTime.elapsedNanos.toString()))
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (transition.mergeTime) {
|
||||
properties.push({
|
||||
propertyKey: 'mergeTime',
|
||||
propertyValue: TimeUtils.format(
|
||||
new ElapsedTimestamp(BigInt(transition.mergeTime.elapsedNanos.toString()))
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (transition.mergedInto) {
|
||||
properties.push({propertyKey: 'mergedInto', propertyValue: transition.mergedInto});
|
||||
}
|
||||
|
||||
if (transition.startTransactionId !== -1) {
|
||||
properties.push({
|
||||
propertyKey: 'startTransactionId',
|
||||
propertyValue: transition.startTransactionId,
|
||||
});
|
||||
}
|
||||
if (transition.finishTransactionId !== -1) {
|
||||
properties.push({
|
||||
propertyKey: 'finishTransactionId',
|
||||
propertyValue: transition.finishTransactionId,
|
||||
});
|
||||
}
|
||||
if (changes.length > 0) {
|
||||
properties.push({propertyKey: 'changes', children: changes});
|
||||
}
|
||||
|
||||
return {
|
||||
children: properties,
|
||||
propertyKey: 'Selected Transition',
|
||||
};
|
||||
}
|
||||
|
||||
private transitionTrace: Trace<object>;
|
||||
private surfaceFlingerTrace: Trace<object> | undefined;
|
||||
private windowManagerTrace: Trace<object> | undefined;
|
||||
private uiData: UiData;
|
||||
private readonly notifyUiDataCallback: (data: UiData) => void;
|
||||
}
|
||||
36
tools/winscope/src/viewers/viewer_transitions/ui_data.ts
Normal file
36
tools/winscope/src/viewers/viewer_transitions/ui_data.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 {Transition} from 'trace/flickerlib/common';
|
||||
import {PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
|
||||
|
||||
export class UiData {
|
||||
constructor(
|
||||
transitions: Transition[],
|
||||
selectedTransition: Transition,
|
||||
selectedTransitionPropertiesTree?: PropertiesTreeNode
|
||||
) {
|
||||
this.entries = transitions;
|
||||
this.selectedTransition = selectedTransition;
|
||||
this.selectedTransitionPropertiesTree = selectedTransitionPropertiesTree;
|
||||
}
|
||||
|
||||
entries: Transition[] = [];
|
||||
selectedTransition: Transition | undefined;
|
||||
selectedTransitionPropertiesTree: PropertiesTreeNode | undefined;
|
||||
|
||||
static EMPTY = new UiData([], undefined, undefined);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 {Traces} from 'trace/traces';
|
||||
import {TracePosition} from 'trace/trace_position';
|
||||
import {TraceType} from 'trace/trace_type';
|
||||
import {View, Viewer, ViewType} from 'viewers/viewer';
|
||||
import {Events} from './events';
|
||||
import {Presenter} from './presenter';
|
||||
import {UiData} from './ui_data';
|
||||
|
||||
export class ViewerTransitions implements Viewer {
|
||||
constructor(traces: Traces) {
|
||||
this.htmlElement = document.createElement('viewer-transitions');
|
||||
|
||||
this.presenter = new Presenter(traces, (data: UiData) => {
|
||||
(this.htmlElement as any).inputData = data;
|
||||
});
|
||||
|
||||
this.htmlElement.addEventListener(Events.TransitionSelected, (event) => {
|
||||
this.presenter.onTransitionSelected((event as CustomEvent).detail);
|
||||
});
|
||||
}
|
||||
|
||||
onTracePositionUpdate(position: TracePosition): void {
|
||||
this.presenter.onTracePositionUpdate(position);
|
||||
}
|
||||
|
||||
getViews(): View[] {
|
||||
return [new View(ViewType.TAB, this.getDependencies(), this.htmlElement, 'Transitions')];
|
||||
}
|
||||
|
||||
getDependencies(): TraceType[] {
|
||||
return ViewerTransitions.DEPENDENCIES;
|
||||
}
|
||||
|
||||
static readonly DEPENDENCIES: TraceType[] = [TraceType.TRANSITION];
|
||||
private htmlElement: HTMLElement;
|
||||
private presenter: Presenter;
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 {TimeUtils} from 'common/time_utils';
|
||||
import {Transition} from 'trace/flickerlib/common';
|
||||
import {ElapsedTimestamp} from 'trace/timestamp';
|
||||
import {Terminal} from 'viewers/common/ui_tree_utils';
|
||||
import {Events} from './events';
|
||||
import {UiData} from './ui_data';
|
||||
|
||||
@Component({
|
||||
selector: 'viewer-transitions',
|
||||
template: `
|
||||
<div class="card-grid container">
|
||||
<div class="top-viewer">
|
||||
<div class="entries">
|
||||
<div class="table-header table-row">
|
||||
<div class="id">Id</div>
|
||||
<div class="type">Type</div>
|
||||
<div class="send-time">Send Time</div>
|
||||
<div class="duration">Duration</div>
|
||||
</div>
|
||||
<cdk-virtual-scroll-viewport itemSize="53" class="scroll">
|
||||
<div
|
||||
*cdkVirtualFor="let transition of uiData.entries; let i = index"
|
||||
class="entry table-row"
|
||||
[class.current]="isCurrentTransition(transition)"
|
||||
(click)="onTransitionClicked(transition)">
|
||||
<div class="id">
|
||||
<span class="mat-body-1">{{ transition.id }}</span>
|
||||
</div>
|
||||
<div class="type">
|
||||
<span class="mat-body-1">{{ transition.type }}</span>
|
||||
</div>
|
||||
<div class="send-time">
|
||||
<span *ngIf="!transition.sendTime.isMin" class="mat-body-1">{{
|
||||
formattedElapsedTime(transition.sendTime.elapsedNanos.toString())
|
||||
}}</span>
|
||||
<span *ngIf="transition.sendTime.isMin"> n/a </span>
|
||||
</div>
|
||||
<div class="duration">
|
||||
<span
|
||||
*ngIf="!transition.sendTime.isMin && !transition.finishTime.isMax"
|
||||
class="mat-body-1"
|
||||
>{{
|
||||
formattedElapsedTimeDiff(
|
||||
transition.sendTime.elapsedNanos.toString(),
|
||||
transition.finishTime.elapsedNanos.toString()
|
||||
)
|
||||
}}</span
|
||||
>
|
||||
<span *ngIf="transition.sendTime.isMin || transition.finishTime.isMax">n/a</span>
|
||||
</div>
|
||||
</div>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
</div>
|
||||
|
||||
<mat-divider [vertical]="true"></mat-divider>
|
||||
|
||||
<div class="container-properties">
|
||||
<h3 class="properties-title mat-title">Selected Transition</h3>
|
||||
<tree-view
|
||||
[item]="uiData.selectedTransitionPropertiesTree"
|
||||
[showNode]="showNode"
|
||||
[isLeaf]="isLeaf"
|
||||
[isAlwaysCollapsed]="true">
|
||||
</tree-view>
|
||||
<div *ngIf="!uiData.selectedTransitionPropertiesTree">
|
||||
No selected transition.<br />
|
||||
Select the tranitions below.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom-viewer">
|
||||
<div class="transition-timeline">
|
||||
<div *ngFor="let row of timelineRows()" class="row">
|
||||
<svg width="100%" [attr.height]="transitionHeight">
|
||||
<rect
|
||||
*ngFor="let transition of transitionsOnRow(row)"
|
||||
[attr.width]="widthOf(transition)"
|
||||
[attr.height]="transitionHeight"
|
||||
[attr.style]="transitionRectStyle(transition)"
|
||||
rx="5"
|
||||
[attr.x]="startOf(transition)"
|
||||
(click)="onTransitionClicked(transition)" />
|
||||
<rect
|
||||
*ngFor="let transition of transitionsOnRow(row)"
|
||||
[attr.width]="transitionDividerWidth"
|
||||
[attr.height]="transitionHeight"
|
||||
[attr.style]="transitionDividerRectStyle(transition)"
|
||||
[attr.x]="sendOf(transition)" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.container {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.top-viewer {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex: 3;
|
||||
border-bottom: solid 1px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.bottom-viewer {
|
||||
display: flex;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.transition-timeline {
|
||||
flex-grow: 1;
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.entries {
|
||||
flex: 3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.container-properties {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.entries .scroll {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.entries .table-header {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
cursor: pointer;
|
||||
border-bottom: solid 1px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.table-header.table-row {
|
||||
font-weight: bold;
|
||||
border-bottom: solid 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.scroll .entry.current {
|
||||
color: white;
|
||||
background-color: #365179;
|
||||
}
|
||||
|
||||
.table-row > div {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.table-row .id {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.table-row .type {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.table-row .send-time {
|
||||
flex: 4;
|
||||
}
|
||||
|
||||
.table-row .duration {
|
||||
flex: 3;
|
||||
}
|
||||
|
||||
.transition-timeline .row svg rect {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 300px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.lines {
|
||||
flex-grow: 1;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.selected-transition {
|
||||
padding: 1rem;
|
||||
border-bottom: solid 1px rgba(0, 0, 0, 0.12);
|
||||
flex-grow: 1;
|
||||
}
|
||||
`,
|
||||
],
|
||||
})
|
||||
export class ViewerTransitionsComponent {
|
||||
transitionHeight = '20px';
|
||||
transitionDividerWidth = '3px';
|
||||
|
||||
constructor(@Inject(ElementRef) elementRef: ElementRef) {
|
||||
this.elementRef = elementRef;
|
||||
}
|
||||
|
||||
@Input()
|
||||
set inputData(data: UiData) {
|
||||
this.uiData = data;
|
||||
}
|
||||
|
||||
getMinOfRanges(): bigint {
|
||||
if (this.uiData.entries.length === 0) {
|
||||
return 0n;
|
||||
}
|
||||
const minOfRange = bigIntMin(
|
||||
...this.uiData.entries
|
||||
.filter((it) => !it.createTime.isMin)
|
||||
.map((it) => BigInt(it.createTime.elapsedNanos.toString()))
|
||||
);
|
||||
return minOfRange;
|
||||
}
|
||||
|
||||
getMaxOfRanges(): bigint {
|
||||
if (this.uiData.entries.length === 0) {
|
||||
return 0n;
|
||||
}
|
||||
const maxOfRange = bigIntMax(
|
||||
...this.uiData.entries
|
||||
.filter((it) => !it.finishTime.isMax)
|
||||
.map((it) => BigInt(it.finishTime.elapsedNanos.toString()))
|
||||
);
|
||||
return maxOfRange;
|
||||
}
|
||||
|
||||
formattedElapsedTime(timeStringNanos: string): string {
|
||||
return TimeUtils.format(new ElapsedTimestamp(BigInt(timeStringNanos)));
|
||||
}
|
||||
|
||||
formattedElapsedTimeDiff(timeStringNanos1: string, timeStringNanos2: string) {
|
||||
return TimeUtils.format(
|
||||
new ElapsedTimestamp(BigInt(timeStringNanos2) - BigInt(timeStringNanos1))
|
||||
);
|
||||
}
|
||||
|
||||
widthOf(transition: Transition) {
|
||||
const fullRange = this.getMaxOfRanges() - this.getMinOfRanges();
|
||||
|
||||
let finish = BigInt(transition.finishTime.elapsedNanos.toString());
|
||||
if (transition.finishTime.elapsedNanos.isMax) {
|
||||
finish = this.getMaxOfRanges();
|
||||
}
|
||||
|
||||
let start = BigInt(transition.createTime.elapsedNanos.toString());
|
||||
if (transition.createTime.elapsedNanos.isMin) {
|
||||
start = this.getMinOfRanges();
|
||||
}
|
||||
|
||||
const minWidthPercent = 0.5;
|
||||
return `${Math.max(minWidthPercent, Number((finish - start) * 100n) / Number(fullRange))}%`;
|
||||
}
|
||||
|
||||
startOf(transition: Transition) {
|
||||
const fullRange = this.getMaxOfRanges() - this.getMinOfRanges();
|
||||
return `${
|
||||
Number((BigInt(transition.createTime.elapsedNanos.toString()) - this.getMinOfRanges()) * 100n) /
|
||||
Number(fullRange)
|
||||
}%`;
|
||||
}
|
||||
|
||||
sendOf(transition: Transition) {
|
||||
const fullRange = this.getMaxOfRanges() - this.getMinOfRanges();
|
||||
return `${
|
||||
Number((BigInt(transition.sendTime.elapsedNanos.toString()) - this.getMinOfRanges()) * 100n) /
|
||||
Number(fullRange)
|
||||
}%`;
|
||||
}
|
||||
|
||||
onTransitionClicked(transition: Transition): void {
|
||||
this.emitEvent(Events.TransitionSelected, transition);
|
||||
}
|
||||
|
||||
transitionRectStyle(transition: Transition): string {
|
||||
if (this.uiData.selectedTransition === transition) {
|
||||
return 'fill:rgb(0, 0, 230)';
|
||||
} else if (transition.aborted) {
|
||||
return 'fill:rgb(255, 0, 0)';
|
||||
} else {
|
||||
return 'fill:rgb(78, 205, 230)';
|
||||
}
|
||||
}
|
||||
|
||||
transitionDividerRectStyle(transition: Transition): string {
|
||||
return 'fill:rgb(255, 0, 0)';
|
||||
}
|
||||
|
||||
showNode(item: any) {
|
||||
return (
|
||||
!(item instanceof Terminal) &&
|
||||
!(item.name instanceof Terminal) &&
|
||||
!(item.propertyKey instanceof Terminal)
|
||||
);
|
||||
}
|
||||
|
||||
isLeaf(item: any) {
|
||||
return (
|
||||
!item.children ||
|
||||
item.children.length === 0 ||
|
||||
item.children.filter((c: any) => !(c instanceof Terminal)).length === 0
|
||||
);
|
||||
}
|
||||
|
||||
isCurrentTransition(transition: Transition): boolean {
|
||||
return this.uiData.selectedTransition === transition;
|
||||
}
|
||||
|
||||
assignRowsToTransitions(): Map<Transition, number> {
|
||||
const fullRange = this.getMaxOfRanges() - this.getMinOfRanges();
|
||||
const assignedRows = new Map<Transition, number>();
|
||||
|
||||
const sortedTransitions = [...this.uiData.entries].sort((t1, t2) => {
|
||||
const diff =
|
||||
BigInt(t1.createTime.elapsedNanos.toString()) -
|
||||
BigInt(t2.createTime.elapsedNanos.toString());
|
||||
if (diff < 0) {
|
||||
return -1;
|
||||
}
|
||||
if (diff > 0) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
const rowFirstAvailableTime = new Map<number, bigint>();
|
||||
let rowsUsed = 1;
|
||||
rowFirstAvailableTime.set(0, 0n);
|
||||
|
||||
for (const transition of sortedTransitions) {
|
||||
const start = BigInt(transition.createTime.elapsedNanos.toString());
|
||||
const end = BigInt(transition.finishTime.elapsedNanos.toString());
|
||||
|
||||
let rowIndexWithSpace = undefined;
|
||||
for (let rowIndex = 0; rowIndex < rowsUsed; rowIndex++) {
|
||||
if (start > rowFirstAvailableTime.get(rowIndex)!) {
|
||||
// current row has space
|
||||
rowIndexWithSpace = rowIndex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (rowIndexWithSpace === undefined) {
|
||||
rowIndexWithSpace = rowsUsed;
|
||||
rowsUsed++;
|
||||
}
|
||||
|
||||
assignedRows.set(transition, rowIndexWithSpace);
|
||||
|
||||
const minimumPaddingBetweenEntries = fullRange / 100n;
|
||||
|
||||
rowFirstAvailableTime.set(rowIndexWithSpace, end + minimumPaddingBetweenEntries);
|
||||
}
|
||||
|
||||
return assignedRows;
|
||||
}
|
||||
|
||||
timelineRows(): number[] {
|
||||
return [...new Set(this.assignRowsToTransitions().values())];
|
||||
}
|
||||
|
||||
transitionsOnRow(row: number): Transition[] {
|
||||
const transitions = [];
|
||||
const assignedRows = this.assignRowsToTransitions();
|
||||
|
||||
for (const transition of assignedRows.keys()) {
|
||||
if (row === assignedRows.get(transition)) {
|
||||
transitions.push(transition);
|
||||
}
|
||||
}
|
||||
|
||||
return transitions;
|
||||
}
|
||||
|
||||
rowsRequiredForTransitions(): number {
|
||||
return Math.max(...this.assignRowsToTransitions().values());
|
||||
}
|
||||
|
||||
private emitEvent(event: string, data: any) {
|
||||
const customEvent = new CustomEvent(event, {
|
||||
bubbles: true,
|
||||
detail: data,
|
||||
});
|
||||
this.elementRef.nativeElement.dispatchEvent(customEvent);
|
||||
}
|
||||
|
||||
uiData: UiData = UiData.EMPTY;
|
||||
private elementRef: ElementRef;
|
||||
}
|
||||
|
||||
const bigIntMax = (...args: Array<bigint>) => args.reduce((m, e) => (e > m ? e : m));
|
||||
const bigIntMin = (...args: Array<bigint>) => args.reduce((m, e) => (e < m ? e : m));
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {ScrollingModule} from '@angular/cdk/scrolling';
|
||||
import {CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA} from '@angular/core';
|
||||
import {ComponentFixture, ComponentFixtureAutoDetect, TestBed} from '@angular/core/testing';
|
||||
import {MatDividerModule} from '@angular/material/divider';
|
||||
import {
|
||||
CrossPlatform,
|
||||
ShellTransitionData,
|
||||
Transition,
|
||||
TransitionChange,
|
||||
TransitionType,
|
||||
WmTransitionData,
|
||||
} from 'trace/flickerlib/common';
|
||||
import {UiData} from './ui_data';
|
||||
import {ViewerTransitionsComponent} from './viewer_transitions_component';
|
||||
|
||||
describe('ViewerTransitionsComponent', () => {
|
||||
let fixture: ComponentFixture<ViewerTransitionsComponent>;
|
||||
let component: ViewerTransitionsComponent;
|
||||
let htmlElement: HTMLElement;
|
||||
|
||||
beforeAll(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [{provide: ComponentFixtureAutoDetect, useValue: true}],
|
||||
imports: [MatDividerModule, ScrollingModule],
|
||||
declarations: [ViewerTransitionsComponent],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ViewerTransitionsComponent);
|
||||
component = fixture.componentInstance;
|
||||
htmlElement = fixture.nativeElement;
|
||||
|
||||
component.uiData = makeUiData();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('can be created', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders entries', () => {
|
||||
expect(htmlElement.querySelector('.scroll')).toBeTruthy();
|
||||
|
||||
const entry = htmlElement.querySelector('.scroll .entry');
|
||||
expect(entry).toBeTruthy();
|
||||
expect(entry!.innerHTML).toContain('TO_FRONT');
|
||||
expect(entry!.innerHTML).toContain('10ns');
|
||||
});
|
||||
|
||||
it('shows message when no transition is selected', () => {
|
||||
expect(htmlElement.querySelector('.container-properties')?.innerHTML).toContain(
|
||||
'No selected transition'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function makeUiData(): UiData {
|
||||
const transitions = [
|
||||
createMockTransition(10, 20, 30),
|
||||
createMockTransition(40, 42, 50),
|
||||
createMockTransition(45, 46, 49),
|
||||
createMockTransition(55, 58, 70),
|
||||
];
|
||||
|
||||
const selectedTransition = undefined;
|
||||
const selectedTransitionPropertiesTree = undefined;
|
||||
|
||||
return new UiData(transitions, selectedTransition, selectedTransitionPropertiesTree);
|
||||
}
|
||||
|
||||
function createMockTransition(
|
||||
createTimeNanos: number,
|
||||
sendTimeNanos: number,
|
||||
finishTimeNanos: number
|
||||
): Transition {
|
||||
const createTime = CrossPlatform.timestamp.fromString(createTimeNanos.toString(), null, null);
|
||||
const sendTime = CrossPlatform.timestamp.fromString(sendTimeNanos.toString(), null, null);
|
||||
const abortTime = null;
|
||||
const finishTime = CrossPlatform.timestamp.fromString(finishTimeNanos.toString(), null, null);
|
||||
|
||||
const startTransactionId = -1;
|
||||
const finishTransactionId = -1;
|
||||
const type = TransitionType.TO_FRONT;
|
||||
const changes: TransitionChange[] = [];
|
||||
|
||||
return new Transition(
|
||||
id++,
|
||||
new WmTransitionData(
|
||||
createTime,
|
||||
sendTime,
|
||||
abortTime,
|
||||
finishTime,
|
||||
startTransactionId,
|
||||
finishTransactionId,
|
||||
type,
|
||||
changes
|
||||
),
|
||||
new ShellTransitionData()
|
||||
);
|
||||
}
|
||||
|
||||
let id = 0;
|
||||
Reference in New Issue
Block a user