Add transition trace viewer

Bug: 277181336
Test: npm run test:all
Change-Id: I7eb981367191a47d3b50b31ee66f147ff1d6923a
This commit is contained in:
Pablo Gamito
2023-05-22 13:32:08 +00:00
parent a735b202f7
commit ac6415277c
11 changed files with 916 additions and 1 deletions

View File

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

View File

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

View File

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

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

View File

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

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

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

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

View File

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

View File

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

View File

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