diff --git a/tools/winscope/src/app/app_module.ts b/tools/winscope/src/app/app_module.ts index a98e97c97..5954034b9 100644 --- a/tools/winscope/src/app/app_module.ts +++ b/tools/winscope/src/app/app_module.ts @@ -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, diff --git a/tools/winscope/src/app/components/app_component.ts b/tools/winscope/src/app/components/app_component.ts index b47d29e42..659cafc1d 100644 --- a/tools/winscope/src/app/components/app_component.ts +++ b/tools/winscope/src/app/components/app_component.ts @@ -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() { diff --git a/tools/winscope/src/app/trace_pipeline.ts b/tools/winscope/src/app/trace_pipeline.ts index 010541573..bca254110 100644 --- a/tools/winscope/src/app/trace_pipeline.ts +++ b/tools/winscope/src/app/trace_pipeline.ts @@ -108,7 +108,7 @@ class TracePipeline { this.parsers = []; this.traces = undefined; this.commonTimestampType = undefined; - this.files.clear(); + this.files = new Map(); } private getCommonTimestampType(): TimestampType { diff --git a/tools/winscope/src/test/e2e/viewer_transitions_test.ts b/tools/winscope/src/test/e2e/viewer_transitions_test.ts new file mode 100644 index 000000000..85b6dbca1 --- /dev/null +++ b/tools/winscope/src/test/e2e/viewer_transitions_test.ts @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {browser, 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(); + }); +}); diff --git a/tools/winscope/src/viewers/viewer_factory.ts b/tools/winscope/src/viewers/viewer_factory.ts index b1b348537..a8da67e78 100644 --- a/tools/winscope/src/viewers/viewer_factory.ts +++ b/tools/winscope/src/viewers/viewer_factory.ts @@ -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, traces: Traces, storage: Storage): Viewer[] { diff --git a/tools/winscope/src/viewers/viewer_transitions/events.ts b/tools/winscope/src/viewers/viewer_transitions/events.ts new file mode 100644 index 000000000..95f96657d --- /dev/null +++ b/tools/winscope/src/viewers/viewer_transitions/events.ts @@ -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}; diff --git a/tools/winscope/src/viewers/viewer_transitions/presenter.ts b/tools/winscope/src/viewers/viewer_transitions/presenter.ts new file mode 100644 index 000000000..8776b8c93 --- /dev/null +++ b/tools/winscope/src/viewers/viewer_transitions/presenter.ts @@ -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; + private surfaceFlingerTrace: Trace | undefined; + private windowManagerTrace: Trace | undefined; + private uiData: UiData; + private readonly notifyUiDataCallback: (data: UiData) => void; +} diff --git a/tools/winscope/src/viewers/viewer_transitions/ui_data.ts b/tools/winscope/src/viewers/viewer_transitions/ui_data.ts new file mode 100644 index 000000000..40c67a402 --- /dev/null +++ b/tools/winscope/src/viewers/viewer_transitions/ui_data.ts @@ -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); +} diff --git a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions.ts b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions.ts new file mode 100644 index 000000000..106c93ddc --- /dev/null +++ b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions.ts @@ -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; +} diff --git a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component.ts b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component.ts new file mode 100644 index 000000000..997a19b90 --- /dev/null +++ b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component.ts @@ -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: ` +
+
+
+
+
Id
+
Type
+
Send Time
+
Duration
+
+ +
+
+ {{ transition.id }} +
+
+ {{ transition.type }} +
+
+ {{ + formattedElapsedTime(transition.sendTime.elapsedNanos.toString()) + }} + n/a +
+
+ {{ + formattedElapsedTimeDiff( + transition.sendTime.elapsedNanos.toString(), + transition.finishTime.elapsedNanos.toString() + ) + }} + n/a +
+
+
+
+ + + +
+

Selected Transition

+ + +
+ No selected transition.
+ Select the tranitions below. +
+
+
+ +
+
+
+ + + + +
+
+
+
+ `, + 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 { + const fullRange = this.getMaxOfRanges() - this.getMinOfRanges(); + const assignedRows = new Map(); + + 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(); + 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) => args.reduce((m, e) => (e > m ? e : m)); +const bigIntMin = (...args: Array) => args.reduce((m, e) => (e < m ? e : m)); diff --git a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component_test.ts b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component_test.ts new file mode 100644 index 000000000..229294b88 --- /dev/null +++ b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component_test.ts @@ -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; + 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;