Make string timestamps interactive. Screencast: https://screencast.googleplex.com/cast/NTA0NjEyMTg2MzgzOTc0NHw2NGI2YWYyMC02OA Changes made in: - transitions send time and dispatch time (dispatch time added to table as this is what it used to set timestamps of transitions). - protolog log timestamp - transactions log timestamp Fixes: 301920476 Test: npm run test:unit:ci Change-Id: Ie85ed349758fa8ede6d3d34006f9766ca0f4b4ad
diff --git a/tools/winscope/src/app/mediator.ts b/tools/winscope/src/app/mediator.ts index 3796c62..72a206e 100644 --- a/tools/winscope/src/app/mediator.ts +++ b/tools/winscope/src/app/mediator.ts
@@ -149,6 +149,9 @@ }); await event.visit(WinscopeEventType.TRACE_POSITION_UPDATE, async (event) => { + if (event.updateTimeline) { + this.timelineData.setPosition(event.position); + } await this.propagateTracePosition(event.position, false); });
diff --git a/tools/winscope/src/app/mediator_test.ts b/tools/winscope/src/app/mediator_test.ts index 58a59bf..be21df4 100644 --- a/tools/winscope/src/app/mediator_test.ts +++ b/tools/winscope/src/app/mediator_test.ts
@@ -14,6 +14,7 @@ * limitations under the License. */ +import {assertDefined} from 'common/assert_utils'; import {FunctionUtils} from 'common/function_utils'; import {NO_TIMEZONE_OFFSET_FACTORY, TimestampFactory} from 'common/timestamp_factory'; import {ProgressListener} from 'messaging/progress_listener'; @@ -245,6 +246,26 @@ ); }); + it('propagates trace position update and updates timeline data', async () => { + await loadFiles(); + await loadTraceView(); + + // notify position + resetSpyCalls(); + const finalTimestampNs = timelineData.getFullTimeRange().to.getValueNs(); + const timestamp = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(finalTimestampNs); + const position = TracePosition.fromTimestamp(timestamp); + + await mediator.onWinscopeEvent(new TracePositionUpdate(position, true)); + checkTracePositionUpdateEvents( + [viewerStub0, viewerOverlay, timelineComponent, crossToolProtocol], + position + ); + expect(assertDefined(timelineData.getCurrentPosition()).timestamp.getValueNs()).toEqual( + finalTimestampNs + ); + }); + it("initializes viewers' trace position also when loaded traces have no valid timestamps", async () => { const dumpFile = await UnitTestUtils.getFixtureFile('traces/dump_WindowManager.pb'); await mediator.onWinscopeEvent(new AppFilesUploaded([dumpFile]));
diff --git a/tools/winscope/src/messaging/winscope_event.ts b/tools/winscope/src/messaging/winscope_event.ts index 86a52a6..2284048 100644 --- a/tools/winscope/src/messaging/winscope_event.ts +++ b/tools/winscope/src/messaging/winscope_event.ts
@@ -151,20 +151,22 @@ export class TracePositionUpdate extends WinscopeEvent { override readonly type = WinscopeEventType.TRACE_POSITION_UPDATE; readonly position: TracePosition; + readonly updateTimeline: boolean; - constructor(position: TracePosition) { + constructor(position: TracePosition, updateTimeline = false) { super(); this.position = position; + this.updateTimeline = updateTimeline; } - static fromTimestamp(timestamp: Timestamp): TracePositionUpdate { + static fromTimestamp(timestamp: Timestamp, updateTimeline = false): TracePositionUpdate { const position = TracePosition.fromTimestamp(timestamp); - return new TracePositionUpdate(position); + return new TracePositionUpdate(position, updateTimeline); } - static fromTraceEntry(entry: TraceEntry<object>): TracePositionUpdate { + static fromTraceEntry(entry: TraceEntry<object>, updateTimeline = false): TracePositionUpdate { const position = TracePosition.fromTraceEntry(entry); - return new TracePositionUpdate(position); + return new TracePositionUpdate(position, updateTimeline); } }
diff --git a/tools/winscope/src/trace/transition.ts b/tools/winscope/src/trace/transition.ts index f35f552..1e2ebec 100644 --- a/tools/winscope/src/trace/transition.ts +++ b/tools/winscope/src/trace/transition.ts
@@ -19,8 +19,8 @@ export interface Transition { id: number; type: string; - sendTime?: string; - finishTime?: string; + sendTime?: PropertyTreeNode; + dispatchTime?: PropertyTreeNode; duration?: string; merged: boolean; aborted: boolean;
diff --git a/tools/winscope/src/viewers/components/ime_additional_properties_component.ts b/tools/winscope/src/viewers/components/ime_additional_properties_component.ts index 0fc78e0..1eafa7e 100644 --- a/tools/winscope/src/viewers/components/ime_additional_properties_component.ts +++ b/tools/winscope/src/viewers/components/ime_additional_properties_component.ts
@@ -22,6 +22,7 @@ import {ImeAdditionalProperties} from 'viewers/common/ime_additional_properties'; import {ImeContainerProperties, InputMethodSurfaceProperties} from 'viewers/common/ime_utils'; import {ViewerEvents} from 'viewers/common/viewer_events'; +import {selectedElementStyle} from './styles/selected_element.styles'; @Component({ selector: 'ime-additional-properties', @@ -320,10 +321,10 @@ } .selected { - background-color: #87acec; color: black; } `, + selectedElementStyle, ], }) export class ImeAdditionalPropertiesComponent {
diff --git a/tools/winscope/src/viewers/components/styles/current_element.styles.ts b/tools/winscope/src/viewers/components/styles/current_element.styles.ts new file mode 100644 index 0000000..ac96b93 --- /dev/null +++ b/tools/winscope/src/viewers/components/styles/current_element.styles.ts
@@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 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. + */ + +export const currentElementStyle = ` + .current { + color: white; + background-color: #365179; + } +`;
diff --git a/tools/winscope/src/viewers/components/styles/node.styles.ts b/tools/winscope/src/viewers/components/styles/node.styles.ts index 0777e9c..c9bd563 100644 --- a/tools/winscope/src/viewers/components/styles/node.styles.ts +++ b/tools/winscope/src/viewers/components/styles/node.styles.ts
@@ -13,7 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export const nodeStyles = ` + +import {selectedElementStyle} from './selected_element.styles'; + +export const nodeStyles = + ` .node { position: relative; display: inline-flex; @@ -52,11 +56,7 @@ padding: 3px; color: white; } - - .selected { - background-color: #87ACEC; - } -`; +` + selectedElementStyle; // FIXME: child-hover selector is not working. export const treeNodeDataViewStyles = `
diff --git a/tools/winscope/src/viewers/components/styles/selected_element.styles.ts b/tools/winscope/src/viewers/components/styles/selected_element.styles.ts new file mode 100644 index 0000000..9b2d70e --- /dev/null +++ b/tools/winscope/src/viewers/components/styles/selected_element.styles.ts
@@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 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. + */ + +export const selectedElementStyle = ` + .selected { + background-color: #87ACEC; + } +`;
diff --git a/tools/winscope/src/viewers/components/styles/timestamp_button.styles.ts b/tools/winscope/src/viewers/components/styles/timestamp_button.styles.ts new file mode 100644 index 0000000..d3694c2 --- /dev/null +++ b/tools/winscope/src/viewers/components/styles/timestamp_button.styles.ts
@@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 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. + */ + +export const timeButtonStyle = ` + .time button { + padding: 0px; + line-height: normal; + text-align: left; + white-space: normal; + } +`;
diff --git a/tools/winscope/src/viewers/viewer_protolog/events.ts b/tools/winscope/src/viewers/viewer_protolog/events.ts index 4cbe3a7..8f3f8b4 100644 --- a/tools/winscope/src/viewers/viewer_protolog/events.ts +++ b/tools/winscope/src/viewers/viewer_protolog/events.ts
@@ -18,6 +18,7 @@ static TagsFilterChanged = 'ViewerProtoLogEvent_TagsFilterChanged'; static SourceFilesFilterChanged = 'ViewerProtoLogEvent_SourceFilesFilterChanged'; static SearchStringFilterChanged = 'ViewerProtoLogEvent_SearchStringFilterChanged'; + static TimestampSelected = 'ViewerProtoLogEvent_TimestampSelected'; } export {Events};
diff --git a/tools/winscope/src/viewers/viewer_protolog/presenter.ts b/tools/winscope/src/viewers/viewer_protolog/presenter.ts index 8f3f6b7..f6ee918 100644 --- a/tools/winscope/src/viewers/viewer_protolog/presenter.ts +++ b/tools/winscope/src/viewers/viewer_protolog/presenter.ts
@@ -123,7 +123,7 @@ return { originalIndex: index, text: assertDefined(messageNode.getChildByName('text')).formattedValue(), - time: assertDefined(messageNode.getChildByName('timestamp')).formattedValue(), + time: assertDefined(messageNode.getChildByName('timestamp')), tag: assertDefined(messageNode.getChildByName('tag')).formattedValue(), level: assertDefined(messageNode.getChildByName('level')).formattedValue(), at: assertDefined(messageNode.getChildByName('at')).formattedValue(),
diff --git a/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts b/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts index 4f583e8..b422a42 100644 --- a/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts +++ b/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts
@@ -45,33 +45,6 @@ const elapsedTime20 = NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(20n); const elapsedTime30 = NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(30n); - inputMessages = [ - { - originalIndex: 0, - text: 'text0', - time: '10ns', - tag: 'tag0', - level: 'level0', - at: 'sourcefile0', - }, - { - originalIndex: 1, - text: 'text1', - time: '20ns', - tag: 'tag1', - level: 'level1', - at: 'sourcefile1', - }, - { - originalIndex: 2, - text: 'text2', - time: '30ns', - tag: 'tag2', - level: 'level2', - at: 'sourcefile2', - }, - ]; - const entries = [ new PropertyTreeBuilder() .setRootId('ProtologTrace') @@ -110,6 +83,33 @@ .build(), ]; + inputMessages = [ + { + originalIndex: 0, + text: 'text0', + time: assertDefined(entries[0].getChildByName('timestamp')), + tag: 'tag0', + level: 'level0', + at: 'sourcefile0', + }, + { + originalIndex: 1, + text: 'text1', + time: assertDefined(entries[1].getChildByName('timestamp')), + tag: 'tag1', + level: 'level1', + at: 'sourcefile1', + }, + { + originalIndex: 2, + text: 'text2', + time: assertDefined(entries[2].getChildByName('timestamp')), + tag: 'tag2', + level: 'level2', + at: 'sourcefile2', + }, + ]; + trace = new TraceBuilder<PropertyTreeNode>() .setEntries(entries) .setTimestamps([time10, time11, time12])
diff --git a/tools/winscope/src/viewers/viewer_protolog/scroll_strategy/protolog_scroll_strategy.ts b/tools/winscope/src/viewers/viewer_protolog/scroll_strategy/protolog_scroll_strategy.ts index fa91c3f..d4afe7d 100644 --- a/tools/winscope/src/viewers/viewer_protolog/scroll_strategy/protolog_scroll_strategy.ts +++ b/tools/winscope/src/viewers/viewer_protolog/scroll_strategy/protolog_scroll_strategy.ts
@@ -25,7 +25,10 @@ protected override predictScrollItemHeight(message: UiDataMessage): number { const textHeight = this.subItemHeight(message.text, this.textCharsPerRow); - const timestampHeight = this.subItemHeight(message.time, this.timestampCharsPerRow); + const timestampHeight = this.subItemHeight( + message.time.formattedValue(), + this.timestampCharsPerRow + ); const sourceFileHeight = this.subItemHeight(message.at, this.sourceFileCharsPerRow); return Math.max(textHeight, timestampHeight, sourceFileHeight); }
diff --git a/tools/winscope/src/viewers/viewer_protolog/ui_data.ts b/tools/winscope/src/viewers/viewer_protolog/ui_data.ts index 0977b62..2032fc2 100644 --- a/tools/winscope/src/viewers/viewer_protolog/ui_data.ts +++ b/tools/winscope/src/viewers/viewer_protolog/ui_data.ts
@@ -14,10 +14,12 @@ * limitations under the License. */ +import {PropertyTreeNode} from 'trace/tree_node/property_tree_node'; + export interface UiDataMessage { readonly originalIndex: number; readonly text: string; - readonly time: string; + readonly time: PropertyTreeNode; readonly tag: string; readonly level: string; readonly at: string;
diff --git a/tools/winscope/src/viewers/viewer_protolog/viewer_protolog.ts b/tools/winscope/src/viewers/viewer_protolog/viewer_protolog.ts index 882129e..925a8bd 100644 --- a/tools/winscope/src/viewers/viewer_protolog/viewer_protolog.ts +++ b/tools/winscope/src/viewers/viewer_protolog/viewer_protolog.ts
@@ -14,9 +14,13 @@ * limitations under the License. */ -import {WinscopeEvent} from 'messaging/winscope_event'; +import {FunctionUtils} from 'common/function_utils'; +import {Timestamp} from 'common/time'; +import {TracePositionUpdate, WinscopeEvent} from 'messaging/winscope_event'; +import {EmitEvent} from 'messaging/winscope_event_emitter'; import {Traces} from 'trace/traces'; import {TraceType} from 'trace/trace_type'; +import {PropertyTreeNode} from 'trace/tree_node/property_tree_node'; import {View, Viewer, ViewType} from 'viewers/viewer'; import {Events} from './events'; import {Presenter} from './presenter'; @@ -28,6 +32,7 @@ private readonly htmlElement: HTMLElement; private readonly presenter: Presenter; private readonly view: View; + private emitAppEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC; constructor(traces: Traces) { this.htmlElement = document.createElement('viewer-protolog'); @@ -48,6 +53,9 @@ this.htmlElement.addEventListener(Events.SearchStringFilterChanged, (event) => { this.presenter.onSearchStringFilterChanged((event as CustomEvent).detail); }); + this.htmlElement.addEventListener(Events.TimestampSelected, (event) => { + this.propagateTimestamp((event as CustomEvent).detail); + }); this.view = new View( ViewType.TAB, @@ -62,8 +70,13 @@ await this.presenter.onAppEvent(event); } - setEmitEvent() { - // do nothing + setEmitEvent(callback: EmitEvent) { + this.emitAppEvent = callback; + } + + async propagateTimestamp(timestampNode: PropertyTreeNode) { + const timestamp: Timestamp = timestampNode.getValue(); + await this.emitAppEvent(TracePositionUpdate.fromTimestamp(timestamp, true)); } getViews(): View[] {
diff --git a/tools/winscope/src/viewers/viewer_protolog/viewer_protolog_component.ts b/tools/winscope/src/viewers/viewer_protolog/viewer_protolog_component.ts index 6c76d4d..3acbca8 100644 --- a/tools/winscope/src/viewers/viewer_protolog/viewer_protolog_component.ts +++ b/tools/winscope/src/viewers/viewer_protolog/viewer_protolog_component.ts
@@ -16,6 +16,9 @@ import {CdkVirtualScrollViewport} from '@angular/cdk/scrolling'; import {Component, ElementRef, Inject, Input, ViewChild} from '@angular/core'; import {MatSelectChange} from '@angular/material/select'; +import {PropertyTreeNode} from 'trace/tree_node/property_tree_node'; +import {currentElementStyle} from 'viewers/components/styles/current_element.styles'; +import {timeButtonStyle} from 'viewers/components/styles/timestamp_button.styles'; import {Events} from './events'; import {UiData} from './ui_data'; @@ -76,9 +79,14 @@ *cdkVirtualFor="let message of uiData.messages; let i = index" class="message" [attr.item-id]="i" - [class.current-message]="isCurrentMessage(i)"> + [class.current]="isCurrentMessage(i)"> <div class="time"> - <span class="mat-body-1">{{ message.time }}</span> + <button + mat-button + [color]="isCurrentMessage(i) ? 'secondary' : 'primary'" + (click)="onTimestampClicked(message.time)"> + {{ message.time.formattedValue() }} + </button> </div> <div class="log-level"> <span class="mat-body-1">{{ message.level }}</span> @@ -122,11 +130,6 @@ overflow-wrap: anywhere; } - .message.current-message { - background-color: #365179; - color: white; - } - .time { flex: 2; } @@ -175,12 +178,15 @@ font-size: 12px; } `, + currentElementStyle, + timeButtonStyle, ], }) export class ViewerProtologComponent { uiData: UiData = UiData.EMPTY; private searchString = ''; + private lastClicked = ''; @ViewChild(CdkVirtualScrollViewport) scrollComponent?: CdkVirtualScrollViewport; @@ -189,7 +195,12 @@ @Input() set inputData(data: UiData) { this.uiData = data; - if (this.uiData.currentMessageIndex !== undefined && this.scrollComponent) { + if ( + this.uiData.currentMessageIndex !== undefined && + this.scrollComponent && + this.lastClicked !== + this.uiData.messages[this.uiData.currentMessageIndex].time.formattedValue() + ) { this.scrollComponent.scrollToIndex(this.uiData.currentMessageIndex); } } @@ -216,6 +227,11 @@ } } + onTimestampClicked(timestamp: PropertyTreeNode) { + this.lastClicked = timestamp.formattedValue(); + this.emitEvent(Events.TimestampSelected, timestamp); + } + isCurrentMessage(index: number): boolean { return index === this.uiData.currentMessageIndex; }
diff --git a/tools/winscope/src/viewers/viewer_protolog/viewer_protolog_component_test.ts b/tools/winscope/src/viewers/viewer_protolog/viewer_protolog_component_test.ts index 297b86b..1f8ae22 100644 --- a/tools/winscope/src/viewers/viewer_protolog/viewer_protolog_component_test.ts +++ b/tools/winscope/src/viewers/viewer_protolog/viewer_protolog_component_test.ts
@@ -22,6 +22,9 @@ import {MatSelectModule} from '@angular/material/select'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {assertDefined} from 'common/assert_utils'; +import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory'; +import {PropertyTreeBuilder} from 'test/unit/property_tree_builder'; +import {TIMESTAMP_FORMATTER} from 'trace/tree_node/formatters'; import {executeScrollComponentTests} from 'viewers/common/scroll_component_test_utils'; import {Events} from './events'; import {ProtologScrollDirective} from './scroll_strategy/protolog_scroll_directive'; @@ -96,6 +99,21 @@ goToCurrentTimeButton.click(); expect(spy).toHaveBeenCalledWith(150); }); + + it('propagates timestamp on click', () => { + component.inputData = makeUiData(); + fixture.detectChanges(); + let timestamp = ''; + htmlElement.addEventListener(Events.TimestampSelected, (event) => { + timestamp = (event as CustomEvent).detail.formattedValue(); + }); + const logTimestampButton = assertDefined( + htmlElement.querySelector('.time button') + ) as HTMLButtonElement; + logTimestampButton.click(); + + expect(timestamp).toEqual('10ns'); + }); }); describe('Scroll component', () => { @@ -123,6 +141,13 @@ const allTags = ['WindowManager', 'INVALID']; const allSourceFiles = ['test_source_file.java', 'other_test_source_file.java']; + const time = new PropertyTreeBuilder() + .setRootId('ProtologMessage') + .setName('timestamp') + .setValue(NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(10n)) + .setFormatter(TIMESTAMP_FORMATTER) + .build(); + const messages = []; const shortMessage = 'test information about message'; const longMessage = shortMessage.repeat(10) + 'keep'; @@ -130,7 +155,7 @@ const uiDataMessage: UiDataMessage = { originalIndex: i, text: i % 2 === 0 ? shortMessage : longMessage, - time: '2022-11-21T18:05:09.777144978', + time, tag: i % 2 === 0 ? allTags[0] : allTags[1], level: i % 2 === 0 ? allLogLevels[0] : allLogLevels[1], at: i % 2 === 0 ? allSourceFiles[0] : allSourceFiles[1],
diff --git a/tools/winscope/src/viewers/viewer_transactions/events.ts b/tools/winscope/src/viewers/viewer_transactions/events.ts index 7af5bab..8a1302d 100644 --- a/tools/winscope/src/viewers/viewer_transactions/events.ts +++ b/tools/winscope/src/viewers/viewer_transactions/events.ts
@@ -23,6 +23,7 @@ static WhatFilterChanged = 'ViewerTransactionsEvent_WhatFilterChanged'; static EntryClicked = 'ViewerTransactionsEvent_EntryClicked'; static TransactionIdFilterChanged = 'ViewerTransactionsEvent_TransactionIdFilterChanged'; + static TimestampSelected = 'ViewerTransactionsEvent_TimestampSelected'; } export {Events};
diff --git a/tools/winscope/src/viewers/viewer_transactions/presenter.ts b/tools/winscope/src/viewers/viewer_transactions/presenter.ts index d8b39c8..1bb09d8 100644 --- a/tools/winscope/src/viewers/viewer_transactions/presenter.ts +++ b/tools/winscope/src/viewers/viewer_transactions/presenter.ts
@@ -17,13 +17,14 @@ import {ArrayUtils} from 'common/array_utils'; import {assertDefined} from 'common/assert_utils'; import {PersistentStoreProxy} from 'common/persistent_store_proxy'; -import {TimeUtils} from 'common/time_utils'; import {WinscopeEvent, WinscopeEventType} from 'messaging/winscope_event'; import {Trace, TraceEntry} from 'trace/trace'; import {Traces} from 'trace/traces'; import {TraceEntryFinder} from 'trace/trace_entry_finder'; import {TraceType} from 'trace/trace_type'; +import {TIMESTAMP_FORMATTER} from 'trace/tree_node/formatters'; import {PropertyTreeNode} from 'trace/tree_node/property_tree_node'; +import {DEFAULT_PROPERTY_TREE_NODE_FACTORY} from 'trace/tree_node/property_tree_node_factory'; import {Filter} from 'viewers/common/operations/filter'; import {UiPropertyTreeNode} from 'viewers/common/ui_property_tree_node'; import {UiTreeFormatter} from 'viewers/common/ui_tree_formatter'; @@ -348,7 +349,13 @@ const entry = this.trace.getEntry(originalIndex); const entryNode = entryProtos[originalIndex]; const vsyncId = Number(assertDefined(entryNode.getChildByName('vsyncId')).getValue()); - const entryTimestamp = TimeUtils.format(entry.getTimestamp()); + + const entryTimestamp = DEFAULT_PROPERTY_TREE_NODE_FACTORY.makeCalculatedProperty( + 'TransactionsTraceEntry', + 'timestamp', + entry.getTimestamp() + ); + entryTimestamp.setFormatter(TIMESTAMP_FORMATTER); for (const transactionState of assertDefined( entryNode.getChildByName('transactions')
diff --git a/tools/winscope/src/viewers/viewer_transactions/presenter_test.ts b/tools/winscope/src/viewers/viewer_transactions/presenter_test.ts index ac06b3a..253ccae 100644 --- a/tools/winscope/src/viewers/viewer_transactions/presenter_test.ts +++ b/tools/winscope/src/viewers/viewer_transactions/presenter_test.ts
@@ -320,12 +320,14 @@ it('formats real time', async () => { await setUpTestEnvironment(TimestampType.REAL); - expect(assertDefined(outputUiData).entries[0].time).toEqual('2022-08-03T06:19:01.051480997'); + expect(assertDefined(outputUiData).entries[0].time.formattedValue()).toEqual( + '2022-08-03T06:19:01.051480997' + ); }); it('formats elapsed time', async () => { await setUpTestEnvironment(TimestampType.ELAPSED); - expect(assertDefined(outputUiData).entries[0].time).toEqual('2s450ms981445ns'); + expect(assertDefined(outputUiData).entries[0].time.formattedValue()).toEqual('2s450ms981445ns'); }); const setUpTestEnvironment = async (timestampType: TimestampType) => {
diff --git a/tools/winscope/src/viewers/viewer_transactions/scroll_strategy/transactions_scroll_strategy.ts b/tools/winscope/src/viewers/viewer_transactions/scroll_strategy/transactions_scroll_strategy.ts index 20b1447..2535661 100644 --- a/tools/winscope/src/viewers/viewer_transactions/scroll_strategy/transactions_scroll_strategy.ts +++ b/tools/winscope/src/viewers/viewer_transactions/scroll_strategy/transactions_scroll_strategy.ts
@@ -24,7 +24,10 @@ protected override predictScrollItemHeight(entry: UiDataEntry): number { const whatHeight = this.subItemHeight(entry.what, this.whatCharsPerRow); - const timestampHeight = this.subItemHeight(entry.time, this.timestampCharsPerRow); + const timestampHeight = this.subItemHeight( + entry.time.formattedValue(), + this.timestampCharsPerRow + ); return Math.max(whatHeight, timestampHeight); } }
diff --git a/tools/winscope/src/viewers/viewer_transactions/ui_data.ts b/tools/winscope/src/viewers/viewer_transactions/ui_data.ts index 9af0c9f..c494224 100644 --- a/tools/winscope/src/viewers/viewer_transactions/ui_data.ts +++ b/tools/winscope/src/viewers/viewer_transactions/ui_data.ts
@@ -55,7 +55,7 @@ class UiDataEntry { constructor( public originalIndexInTraceEntry: number, - public time: string, + public time: PropertyTreeNode, public vsyncId: number, public pid: string, public uid: string,
diff --git a/tools/winscope/src/viewers/viewer_transactions/viewer_transactions.ts b/tools/winscope/src/viewers/viewer_transactions/viewer_transactions.ts index 2ff5415..cc52c8f 100644 --- a/tools/winscope/src/viewers/viewer_transactions/viewer_transactions.ts +++ b/tools/winscope/src/viewers/viewer_transactions/viewer_transactions.ts
@@ -14,9 +14,13 @@ * limitations under the License. */ -import {WinscopeEvent} from 'messaging/winscope_event'; +import {FunctionUtils} from 'common/function_utils'; +import {Timestamp} from 'common/time'; +import {TracePositionUpdate, WinscopeEvent} from 'messaging/winscope_event'; +import {EmitEvent} from 'messaging/winscope_event_emitter'; import {Traces} from 'trace/traces'; import {TraceType} from 'trace/trace_type'; +import {PropertyTreeNode} from 'trace/tree_node/property_tree_node'; import {ViewerEvents} from 'viewers/common/viewer_events'; import {View, Viewer, ViewType} from 'viewers/viewer'; import {Events} from './events'; @@ -29,6 +33,7 @@ private readonly htmlElement: HTMLElement; private readonly presenter: Presenter; private readonly view: View; + private emitAppEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC; constructor(traces: Traces, storage: Storage) { this.htmlElement = document.createElement('viewer-transactions'); @@ -68,13 +73,12 @@ this.htmlElement.addEventListener(Events.EntryClicked, (event) => { this.presenter.onEntryClicked((event as CustomEvent).detail); }); + this.htmlElement.addEventListener(Events.TimestampSelected, (event) => { + this.propagateTimestamp((event as CustomEvent).detail); + }); - this.htmlElement.addEventListener( - ViewerEvents.PropertiesUserOptionsChange, - async (event) => - await this.presenter.onPropertiesUserOptionsChange( - (event as CustomEvent).detail.userOptions - ) + this.htmlElement.addEventListener(ViewerEvents.PropertiesUserOptionsChange, (event) => + this.presenter.onPropertiesUserOptionsChange((event as CustomEvent).detail.userOptions) ); this.view = new View( @@ -90,8 +94,13 @@ await this.presenter.onAppEvent(event); } - setEmitEvent() { - // do nothing + setEmitEvent(callback: EmitEvent) { + this.emitAppEvent = callback; + } + + async propagateTimestamp(timestampNode: PropertyTreeNode) { + const timestamp: Timestamp = timestampNode.getValue(); + await this.emitAppEvent(TracePositionUpdate.fromTimestamp(timestamp, true)); } getViews(): View[] {
diff --git a/tools/winscope/src/viewers/viewer_transactions/viewer_transactions_component.ts b/tools/winscope/src/viewers/viewer_transactions/viewer_transactions_component.ts index eeda175..fb72c90 100644 --- a/tools/winscope/src/viewers/viewer_transactions/viewer_transactions_component.ts +++ b/tools/winscope/src/viewers/viewer_transactions/viewer_transactions_component.ts
@@ -16,7 +16,11 @@ import {CdkVirtualScrollViewport} from '@angular/cdk/scrolling'; import {Component, ElementRef, Inject, Input, ViewChild} from '@angular/core'; import {MatSelectChange} from '@angular/material/select'; +import {PropertyTreeNode} from 'trace/tree_node/property_tree_node'; import {ViewerEvents} from 'viewers/common/viewer_events'; +import {currentElementStyle} from 'viewers/components/styles/current_element.styles'; +import {selectedElementStyle} from 'viewers/components/styles/selected_element.styles'; +import {timeButtonStyle} from 'viewers/components/styles/timestamp_button.styles'; import {Events} from './events'; import {UiData} from './ui_data'; @@ -104,11 +108,16 @@ *cdkVirtualFor="let entry of uiData.entries; let i = index" class="entry" [attr.item-id]="i" - [class.current-entry]="isCurrentEntry(i)" - [class.selected-entry]="isSelectedEntry(i)" + [class.current]="isCurrentEntry(i)" + [class.selected]="isSelectedEntry(i)" (click)="onEntryClicked(i)"> <div class="time"> - <span class="mat-body-1">{{ entry.time }}</span> + <button + mat-button + [color]="isCurrentEntry(i) ? 'secondary' : 'primary'" + (click)="onTimestampClicked(entry.time)"> + {{ entry.time.formattedValue() }} + </button> </div> <div class="id"> <span class="mat-body-1">{{ entry.transactionId }}</span> @@ -227,16 +236,6 @@ margin-right: 16px; } - .entry.current-entry { - color: white; - background-color: #365179; - } - - .entry.selected-entry { - color: white; - background-color: #98aecd; - } - .go-to-current-time { flex: none; margin-top: 4px; @@ -251,11 +250,15 @@ max-height: 75vh; } `, + selectedElementStyle, + currentElementStyle, + timeButtonStyle, ], }) class ViewerTransactionsComponent { objectKeys = Object.keys; uiData: UiData = UiData.EMPTY; + private lastClicked = ''; @ViewChild(CdkVirtualScrollViewport) scrollComponent?: CdkVirtualScrollViewport; @@ -264,7 +267,11 @@ @Input() set inputData(data: UiData) { this.uiData = data; - if (this.uiData.scrollToIndex !== undefined && this.scrollComponent) { + if ( + this.uiData.scrollToIndex !== undefined && + this.scrollComponent && + this.lastClicked !== this.uiData.entries[this.uiData.scrollToIndex].time.formattedValue() + ) { this.scrollComponent.scrollToIndex(this.uiData.scrollToIndex); } } @@ -315,6 +322,11 @@ } } + onTimestampClicked(timestamp: PropertyTreeNode) { + this.lastClicked = timestamp.formattedValue(); + this.emitEvent(Events.TimestampSelected, timestamp); + } + isCurrentEntry(index: number): boolean { return index === this.uiData.currentEntryIndex; }
diff --git a/tools/winscope/src/viewers/viewer_transactions/viewer_transactions_component_test.ts b/tools/winscope/src/viewers/viewer_transactions/viewer_transactions_component_test.ts index f8e3cd3..258d294 100644 --- a/tools/winscope/src/viewers/viewer_transactions/viewer_transactions_component_test.ts +++ b/tools/winscope/src/viewers/viewer_transactions/viewer_transactions_component_test.ts
@@ -18,9 +18,12 @@ import {ComponentFixture, ComponentFixtureAutoDetect, TestBed} from '@angular/core/testing'; import {MatDividerModule} from '@angular/material/divider'; import {assertDefined} from 'common/assert_utils'; +import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory'; import {PropertyTreeBuilder} from 'test/unit/property_tree_builder'; +import {TIMESTAMP_FORMATTER} from 'trace/tree_node/formatters'; import {executeScrollComponentTests} from 'viewers/common/scroll_component_test_utils'; import {UiPropertyTreeNode} from 'viewers/common/ui_property_tree_node'; +import {Events} from './events'; import {TransactionsScrollDirective} from './scroll_strategy/transactions_scroll_directive'; import {UiData, UiDataEntry} from './ui_data'; import {ViewerTransactionsComponent} from './viewer_transactions_component'; @@ -62,7 +65,7 @@ expect(htmlElement.querySelector('.scroll')).toBeTruthy(); const entry = assertDefined(htmlElement.querySelector('.scroll .entry')); - expect(entry.innerHTML).toContain('TIME_VALUE'); + expect(entry.innerHTML).toContain('1ns'); expect(entry.innerHTML).toContain('-111'); expect(entry.innerHTML).toContain('PID_VALUE'); expect(entry.innerHTML).toContain('UID_VALUE'); @@ -84,6 +87,21 @@ expect(spy).toHaveBeenCalledWith(1); }); + it('propagates timestamp on click', () => { + component.inputData = makeUiData(); + fixture.detectChanges(); + let timestamp = ''; + htmlElement.addEventListener(Events.TimestampSelected, (event) => { + timestamp = (event as CustomEvent).detail.formattedValue(); + }); + const logTimestampButton = assertDefined( + htmlElement.querySelector('.time button') + ) as HTMLButtonElement; + logTimestampButton.click(); + + expect(timestamp).toEqual('1ns'); + }); + function makeUiData(): UiData { const propertiesTree = new PropertyTreeBuilder() .setRootId('Transactions') @@ -91,9 +109,16 @@ .setValue(null) .build(); + const time = new PropertyTreeBuilder() + .setRootId(propertiesTree.id) + .setName('timestamp') + .setValue(NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(1n)) + .setFormatter(TIMESTAMP_FORMATTER) + .build(); + const entry = new UiDataEntry( 0, - 'TIME_VALUE', + time, -111, 'PID_VALUE', 'UID_VALUE', @@ -106,7 +131,7 @@ const entry2 = new UiDataEntry( 1, - 'TIME_VALUE', + time, -222, 'PID_VALUE_2', 'UID_VALUE_2', @@ -144,6 +169,14 @@ .setName('tree') .setValue(null) .build(); + + const time = new PropertyTreeBuilder() + .setRootId(propertiesTree.id) + .setName('timestamp') + .setValue(NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(1n)) + .setFormatter(TIMESTAMP_FORMATTER) + .build(); + const uiData = new UiData( [], [], @@ -164,7 +197,7 @@ for (let i = 0; i < 200; i++) { const entry = new UiDataEntry( 0, - 'TIME_VALUE', + time, -111, 'PID_VALUE', 'UID_VALUE',
diff --git a/tools/winscope/src/viewers/viewer_transitions/events.ts b/tools/winscope/src/viewers/viewer_transitions/events.ts index 95f9665..aa19964 100644 --- a/tools/winscope/src/viewers/viewer_transitions/events.ts +++ b/tools/winscope/src/viewers/viewer_transitions/events.ts
@@ -16,6 +16,7 @@ class Events { static TransitionSelected = 'ViewerTransitionsEvent_TransitionSelected'; + static TimestampSelected = 'ViewerTransitionsEvent_TimestampSelected'; } export {Events};
diff --git a/tools/winscope/src/viewers/viewer_transitions/presenter.ts b/tools/winscope/src/viewers/viewer_transitions/presenter.ts index b8a165b..b9a08ca 100644 --- a/tools/winscope/src/viewers/viewer_transitions/presenter.ts +++ b/tools/winscope/src/viewers/viewer_transitions/presenter.ts
@@ -112,12 +112,13 @@ private makeTransitions(entries: PropertyTreeNode[]): Transition[] { return entries.map((transitionNode) => { const wmDataNode = assertDefined(transitionNode.getChildByName('wmData')); + const shellDataNode = assertDefined(transitionNode.getChildByName('shellData')); const transition: Transition = { id: assertDefined(transitionNode.getChildByName('id')).getValue(), type: wmDataNode.getChildByName('type')?.formattedValue() ?? 'NONE', - sendTime: wmDataNode.getChildByName('sendTimeNs')?.formattedValue(), - finishTime: wmDataNode.getChildByName('finishTimeNs')?.formattedValue(), + sendTime: wmDataNode.getChildByName('sendTimeNs'), + dispatchTime: shellDataNode.getChildByName('dispatchTimeNs'), duration: transitionNode.getChildByName('duration')?.formattedValue(), merged: assertDefined(transitionNode.getChildByName('merged')).getValue(), aborted: assertDefined(transitionNode.getChildByName('aborted')).getValue(),
diff --git a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions.ts b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions.ts index 495e825..f5e53a1 100644 --- a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions.ts +++ b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions.ts
@@ -13,9 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {WinscopeEvent} from 'messaging/winscope_event'; +import {FunctionUtils} from 'common/function_utils'; +import {Timestamp} from 'common/time'; +import {TracePositionUpdate, WinscopeEvent} from 'messaging/winscope_event'; +import {EmitEvent} from 'messaging/winscope_event_emitter'; import {Traces} from 'trace/traces'; import {TraceType} from 'trace/trace_type'; +import {PropertyTreeNode} from 'trace/tree_node/property_tree_node'; import {View, Viewer, ViewType} from 'viewers/viewer'; import {Events} from './events'; import {Presenter} from './presenter'; @@ -27,6 +31,7 @@ private readonly htmlElement: HTMLElement; private readonly presenter: Presenter; private readonly view: View; + private emitAppEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC; constructor(traces: Traces) { this.htmlElement = document.createElement('viewer-transitions'); @@ -39,6 +44,10 @@ this.presenter.onTransitionSelected((event as CustomEvent).detail); }); + this.htmlElement.addEventListener(Events.TimestampSelected, (event) => { + this.propagateTimestamp((event as CustomEvent).detail); + }); + this.view = new View( ViewType.TAB, this.getDependencies(), @@ -52,8 +61,13 @@ await this.presenter.onAppEvent(event); } - setEmitEvent() { - // do nothing + setEmitEvent(callback: EmitEvent) { + this.emitAppEvent = callback; + } + + async propagateTimestamp(timestampNode: PropertyTreeNode) { + const timestamp: Timestamp = timestampNode.getValue(); + await this.emitAppEvent(TracePositionUpdate.fromTimestamp(timestamp, true)); } getViews(): View[] {
diff --git a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component.ts b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component.ts index 8e8a066..ca78276 100644 --- a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component.ts +++ b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component.ts
@@ -17,6 +17,8 @@ import {Component, ElementRef, Inject, Input} from '@angular/core'; import {Transition} from 'trace/transition'; import {PropertyTreeNode} from 'trace/tree_node/property_tree_node'; +import {selectedElementStyle} from 'viewers/components/styles/selected_element.styles'; +import {timeButtonStyle} from 'viewers/components/styles/timestamp_button.styles'; import {Events} from './events'; import {UiData} from './ui_data'; @@ -29,6 +31,7 @@ <div class="id mat-body-2">Id</div> <div class="type mat-body-2">Type</div> <div class="send-time mat-body-2">Send Time</div> + <div class="dispatch-time mat-body-2">Dispatch Time</div> <div class="duration mat-body-2">Duration</div> <div class="status mat-body-2">Status</div> </div> @@ -36,7 +39,7 @@ <div *cdkVirtualFor="let transition of uiData.entries; let i = index" class="entry table-row" - [class.current]="isCurrentTransition(transition)" + [class.selected]="isSelectedTransition(transition)" (click)="onTransitionClicked(transition)"> <div class="id"> <span class="mat-body-1">{{ transition.id }}</span> @@ -44,10 +47,26 @@ <div class="type"> <span class="mat-body-1">{{ transition.type }}</span> </div> - <div class="send-time"> - <span *ngIf="transition.sendTime" class="mat-body-1">{{ transition.sendTime }}</span> + <div class="send-time time"> + <button + mat-button + color="primary" + *ngIf="transition.sendTime" + (click)="onTimestampClicked(transition.sendTime)"> + {{ transition.sendTime.formattedValue() }} + </button> <span *ngIf="!transition.sendTime" class="mat-body-1"> n/a </span> </div> + <div class="dispatch-time time"> + <button + mat-button + color="primary" + *ngIf="transition.dispatchTime" + (click)="onTimestampClicked(transition.dispatchTime)"> + {{ transition.dispatchTime.formattedValue() }} + </button> + <span *ngIf="!transition.dispatchTime" class="mat-body-1"> n/a </span> + </div> <div class="duration"> <span *ngIf="transition.duration" class="mat-body-1">{{ transition.duration }}</span> <span *ngIf="!transition.duration" class="mat-body-1"> n/a </span> @@ -133,11 +152,6 @@ border-bottom: solid 1px rgba(0, 0, 0, 0.5); } - .scroll .entry.current { - color: white; - background-color: #365179; - } - .table-row > div { padding: 16px; } @@ -150,6 +164,10 @@ flex: 2; } + .table-row .dispatch-time { + flex: 4; + } + .table-row .send-time { flex: 4; } @@ -169,7 +187,7 @@ gap: 5px; } - .current .status mat-icon { + .selected .status mat-icon { color: white !important; } @@ -186,13 +204,9 @@ flex-grow: 1; padding: 0.5rem; } - - .selected-transition { - padding: 1rem; - border-bottom: solid 1px rgba(0, 0, 0, 0.12); - flex-grow: 1; - } `, + selectedElementStyle, + timeButtonStyle, ], }) export class ViewerTransitionsComponent { @@ -207,7 +221,7 @@ this.emitEvent(Events.TransitionSelected, transition.propertiesTree); } - isCurrentTransition(transition: Transition): boolean { + isSelectedTransition(transition: Transition): boolean { return ( transition.id === this.uiData.selectedTransition @@ -222,6 +236,10 @@ ); } + onTimestampClicked(timestamp: PropertyTreeNode) { + this.emitEvent(Events.TimestampSelected, timestamp); + } + emitEvent(event: string, propertiesTree: PropertyTreeNode) { const customEvent = new CustomEvent(event, { bubbles: true,
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 index 835ba06..f8cc62f 100644 --- a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component_test.ts +++ b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component_test.ts
@@ -20,6 +20,7 @@ import {MatDividerModule} from '@angular/material/divider'; import {assertDefined} from 'common/assert_utils'; import {TimestampType} from 'common/time'; +import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory'; import {TracePositionUpdate} from 'messaging/winscope_event'; import {PropertyTreeBuilder} from 'test/unit/property_tree_builder'; import {UnitTestUtils} from 'test/unit/utils'; @@ -29,6 +30,7 @@ import {TracePosition} from 'trace/trace_position'; import {TraceType} from 'trace/trace_type'; import {Transition} from 'trace/transition'; +import {TIMESTAMP_FORMATTER} from 'trace/tree_node/formatters'; import {PropertyTreeNode} from 'trace/tree_node/property_tree_node'; import {TreeComponent} from 'viewers/components/tree_component'; import {TreeNodeComponent} from 'viewers/components/tree_node_component'; @@ -154,6 +156,19 @@ const textContentWithoutWhitespaces = treeView.textContent?.replace(/(\s|\t|\n)*/g, ''); expect(textContentWithoutWhitespaces).toContain(`id:${selectedTransitionId}`); }); + + it('propagates timestamp on click', () => { + let timestamp = ''; + htmlElement.addEventListener(Events.TimestampSelected, (event) => { + timestamp = (event as CustomEvent).detail.formattedValue(); + }); + const logTimestampButton = assertDefined( + htmlElement.querySelector('.time button') + ) as HTMLButtonElement; + logTimestampButton.click(); + + expect(timestamp).toEqual('20ns'); + }); }); function makeUiData(): UiData { @@ -181,11 +196,18 @@ .setChildren([{name: 'id', value: id}]) .build(); + const sendTimeNode = new PropertyTreeBuilder() + .setRootId(transitionTree.id) + .setName('sendTimeNs') + .setValue(NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(BigInt(sendTimeNanos))) + .setFormatter(TIMESTAMP_FORMATTER) + .build(); + return { id, type: 'TO_FRONT', - sendTime: sendTimeNanos.toString() + 'ns', - finishTime: finishTimeNanos.toString() + 'ns', + sendTime: sendTimeNode, + dispatchTime: undefined, duration: (finishTimeNanos - sendTimeNanos).toString() + 'ns', merged: false, aborted: false,