diff --git a/tools/winscope/src/viewers/common/ime_utils_test.ts b/tools/winscope/src/viewers/common/ime_utils_test.ts index 478363616..ef6de14f2 100644 --- a/tools/winscope/src/viewers/common/ime_utils_test.ts +++ b/tools/winscope/src/viewers/common/ime_utils_test.ts @@ -21,7 +21,7 @@ describe('ImeUtils', () => { it('processes WindowManager trace entry', async () => { const entries = await UnitTestUtils.getImeTraceEntries(); const processed = ImeUtils.processWindowManagerTraceEntry( - entries.get(TraceType.WINDOW_MANAGER)[0] + entries.get(TraceType.WINDOW_MANAGER) ); expect(processed.focusedApp).toEqual( @@ -59,10 +59,10 @@ describe('ImeUtils', () => { it('processes SurfaceFlinger trace entry', async () => { const entries = await UnitTestUtils.getImeTraceEntries(); const processedWindowManagerState = ImeUtils.processWindowManagerTraceEntry( - entries.get(TraceType.WINDOW_MANAGER)[0] + entries.get(TraceType.WINDOW_MANAGER) ); const layers = ImeUtils.getImeLayers( - entries.get(TraceType.SURFACE_FLINGER)[0], + entries.get(TraceType.SURFACE_FLINGER), processedWindowManagerState )!; diff --git a/tools/winscope/src/viewers/common/presenter_input_method.ts b/tools/winscope/src/viewers/common/presenter_input_method.ts index 7343517a5..6013376f8 100644 --- a/tools/winscope/src/viewers/common/presenter_input_method.ts +++ b/tools/winscope/src/viewers/common/presenter_input_method.ts @@ -13,8 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import {PersistentStoreProxy} from 'common/persistent_store_proxy'; import {FilterType, TreeUtils} from 'common/tree_utils'; +import {LayerTraceEntry} from 'trace/flickerlib/layers/LayerTraceEntry'; +import {WindowManagerState} from 'trace/flickerlib/windows/WindowManagerState'; +import {Trace, TraceEntry} from 'trace/trace'; +import {Traces} from 'trace/traces'; +import {TraceEntryFinder} from 'trace/trace_entry_finder'; +import {TracePosition} from 'trace/trace_position'; import {TraceTreeNode} from 'trace/trace_tree_node'; import {TraceType} from 'trace/trace_type'; import {ImeAdditionalProperties} from 'viewers/common/ime_additional_properties'; @@ -29,17 +36,91 @@ import {UserOptions} from 'viewers/common/user_options'; type NotifyImeViewCallbackType = (uiData: ImeUiData) => void; export abstract class PresenterInputMethod { + private readonly imeTrace: Trace; + private readonly wmTrace?: Trace; + private readonly sfTrace?: Trace; + private hierarchyFilter: FilterType = TreeUtils.makeNodeFilter(''); + private propertiesFilter: FilterType = TreeUtils.makeNodeFilter(''); + private pinnedItems: HierarchyTreeNode[] = []; + private pinnedIds: string[] = []; + private selectedHierarchyTree: HierarchyTreeNode | null = null; + + readonly notifyViewCallback: NotifyImeViewCallbackType; + protected readonly dependencies: TraceType[]; + protected uiData: ImeUiData; + protected highlightedItems: string[] = []; + protected entry: TraceTreeNode | null = null; + protected additionalPropertyEntry: TraceTreeNode | null = null; + protected hierarchyUserOptions: UserOptions = PersistentStoreProxy.new( + 'ImeHierarchyOptions', + { + simplifyNames: { + name: 'Simplify names', + enabled: true, + }, + onlyVisible: { + name: 'Only visible', + enabled: false, + }, + flat: { + name: 'Flat', + enabled: false, + }, + }, + this.storage + ); + protected propertiesUserOptions: UserOptions = PersistentStoreProxy.new( + 'ImePropertiesOptions', + { + showDefaults: { + name: 'Show defaults', + enabled: false, + tooltip: ` + If checked, shows the value of all properties. + Otherwise, hides all properties whose value is + the default for its data type. + `, + }, + }, + this.storage + ); + constructor( - notifyViewCallback: NotifyImeViewCallbackType, + traces: Traces, + private storage: Storage, dependencies: TraceType[], - private storage: Storage + notifyViewCallback: NotifyImeViewCallbackType ) { - this.notifyViewCallback = notifyViewCallback; + this.imeTrace = traces.getTrace(dependencies[0]) as Trace; + this.sfTrace = traces.getTrace(TraceType.SURFACE_FLINGER); + this.wmTrace = traces.getTrace(TraceType.WINDOW_MANAGER); + this.dependencies = dependencies; + this.notifyViewCallback = notifyViewCallback; this.uiData = new ImeUiData(dependencies); this.notifyViewCallback(this.uiData); } + onTracePositionUpdate(position: TracePosition) { + this.uiData = new ImeUiData(this.dependencies); + this.uiData.hierarchyUserOptions = this.hierarchyUserOptions; + this.uiData.propertiesUserOptions = this.propertiesUserOptions; + + const [imeEntry, sfEntry, wmEntry] = this.findTraceEntries(position); + + if (imeEntry) { + this.entry = imeEntry.getValue() as TraceTreeNode; + this.uiData.highlightedItems = this.highlightedItems; + this.uiData.additionalProperties = this.getAdditionalProperties( + wmEntry?.getValue(), + sfEntry?.getValue() + ); + this.uiData.tree = this.generateTree(); + this.uiData.hierarchyTableProperties = this.updateHierarchyTableProperties(); + } + this.notifyViewCallback(this.uiData); + } + updatePinnedItems(pinnedItem: HierarchyTreeNode) { const pinnedId = `${pinnedItem.id}`; if (this.pinnedItems.map((item) => `${item.id}`).includes(pinnedId)) { @@ -109,30 +190,6 @@ export abstract class PresenterInputMethod { this.updateSelectedTreeUiData(); } - notifyCurrentTraceEntries(entries: Map) { - this.uiData = new ImeUiData(this.dependencies); - this.uiData.hierarchyUserOptions = this.hierarchyUserOptions; - this.uiData.propertiesUserOptions = this.propertiesUserOptions; - - const imEntries = entries.get(this.dependencies[0]); - if (imEntries && imEntries[0]) { - this.entry = imEntries[0]; - this.uiData.highlightedItems = this.highlightedItems; - - const wmEntries = entries.get(TraceType.WINDOW_MANAGER); - const sfEntries = entries.get(TraceType.SURFACE_FLINGER); - - this.uiData.additionalProperties = this.getAdditionalProperties( - wmEntries ? wmEntries[0] : undefined, - sfEntries ? sfEntries[0] : undefined - ); - - this.uiData.tree = this.generateTree(); - this.uiData.hierarchyTableProperties = this.updateHierarchyTableProperties(); - } - this.notifyViewCallback(this.uiData); - } - protected getAdditionalProperties( wmEntry: TraceTreeNode | undefined, sfEntry: TraceTreeNode | undefined @@ -201,51 +258,33 @@ export abstract class PresenterInputMethod { return transformedTree; } - private hierarchyFilter: FilterType = TreeUtils.makeNodeFilter(''); - private propertiesFilter: FilterType = TreeUtils.makeNodeFilter(''); - private pinnedItems: HierarchyTreeNode[] = []; - private pinnedIds: string[] = []; - private selectedHierarchyTree: HierarchyTreeNode | null = null; + private findTraceEntries( + position: TracePosition + ): [ + TraceEntry | undefined, + TraceEntry | undefined, + TraceEntry | undefined + ] { + const imeEntry = TraceEntryFinder.findCorrespondingEntry(this.imeTrace, position); + if (!imeEntry) { + return [undefined, undefined, undefined]; + } - readonly notifyViewCallback: NotifyImeViewCallbackType; - protected readonly dependencies: TraceType[]; - protected uiData: ImeUiData; - protected highlightedItems: string[] = []; - protected entry: TraceTreeNode | null = null; - protected additionalPropertyEntry: TraceTreeNode | null = null; - protected hierarchyUserOptions: UserOptions = PersistentStoreProxy.new( - 'ImeHierarchyOptions', - { - simplifyNames: { - name: 'Simplify names', - enabled: true, - }, - onlyVisible: { - name: 'Only visible', - enabled: false, - }, - flat: { - name: 'Flat', - enabled: false, - }, - }, - this.storage - ); - protected propertiesUserOptions: UserOptions = PersistentStoreProxy.new( - 'ImePropertiesOptions', - { - showDefaults: { - name: 'Show defaults', - enabled: false, - tooltip: ` - If checked, shows the value of all properties. - Otherwise, hides all properties whose value is - the default for its data type. - `, - }, - }, - this.storage - ); + if (!this.imeTrace.hasFrameInfo()) { + return [imeEntry, undefined, undefined]; + } + + const frames = imeEntry.getFramesRange(); + if (!frames || frames.start === frames.end) { + return [imeEntry, undefined, undefined]; + } + + const frame = frames.start; + const sfEntry = this.sfTrace?.getFrame(frame)?.findClosestEntry(imeEntry.getTimestamp()); + const wmEntry = this.wmTrace?.getFrame(frame)?.findClosestEntry(imeEntry.getTimestamp()); + + return [imeEntry, sfEntry, wmEntry]; + } protected abstract updateHierarchyTableProperties(): TableProperties; } diff --git a/tools/winscope/src/viewers/common/presenter_input_method_test_utils.ts b/tools/winscope/src/viewers/common/presenter_input_method_test_utils.ts index 739b84310..9902b6cec 100644 --- a/tools/winscope/src/viewers/common/presenter_input_method_test_utils.ts +++ b/tools/winscope/src/viewers/common/presenter_input_method_test_utils.ts @@ -14,9 +14,14 @@ * limitations under the License.d */ +import {assertDefined} from 'common/assert_utils'; import {HierarchyTreeBuilder} from 'test/unit/hierarchy_tree_builder'; import {MockStorage} from 'test/unit/mock_storage'; +import {TracesBuilder} from 'test/unit/traces_builder'; +import {TraceBuilder} from 'test/unit/trace_builder'; import {UnitTestUtils} from 'test/unit/utils'; +import {Traces} from 'trace/traces'; +import {TracePosition} from 'trace/trace_position'; import {TraceType} from 'trace/trace_type'; import {ImeUiData} from 'viewers/common/ime_ui_data'; import {HierarchyTreeNode, PropertiesTreeNode} from 'viewers/common/ui_tree_utils'; @@ -35,64 +40,66 @@ export function executePresenterInputMethodTests( | typeof PresenterInputMethodClients | typeof PresenterInputMethodService | typeof PresenterInputMethodManagerService, - traceType: TraceType + imeTraceType: TraceType ) { describe('PresenterInputMethod', () => { let presenter: PresenterInputMethod; let uiData: ImeUiData; - let entries: Map; + let position: TracePosition; let selectedTree: HierarchyTreeNode; beforeEach(async () => { - entries = await UnitTestUtils.getImeTraceEntries(); selectedTree = selected; - presenter = new PresenterInputMethod( - (newData: ImeUiData) => { - uiData = newData; - }, - [traceType], - new MockStorage() - ); + await setUpTestEnvironment([ + imeTraceType, + TraceType.SURFACE_FLINGER, + TraceType.WINDOW_MANAGER, + ]); }); - it('can notify current trace entries', () => { - presenter.notifyCurrentTraceEntries(entries); + it('is robust to empty trace', () => { + const traces = new TracesBuilder().setEntries(imeTraceType, []).build(); + presenter = createPresenter(traces); + expect(uiData.hierarchyUserOptions).toBeTruthy(); expect(uiData.propertiesUserOptions).toBeTruthy(); - expect(Object.keys(uiData.tree!).length > 0).toBeTrue(); - }); - - it('is robust to trace entry without SF', () => { - entries.delete(TraceType.SURFACE_FLINGER); - presenter.notifyCurrentTraceEntries(entries); - expect(uiData.hierarchyUserOptions).toBeTruthy(); - expect(uiData.propertiesUserOptions).toBeTruthy(); - expect(Object.keys(uiData.tree!).length > 0).toBeTrue(); - }); - - it('is robust to trace entry without WM', () => { - entries.delete(TraceType.WINDOW_MANAGER); - presenter.notifyCurrentTraceEntries(entries); - expect(uiData.hierarchyUserOptions).toBeTruthy(); - expect(uiData.propertiesUserOptions).toBeTruthy(); - expect(Object.keys(uiData.tree!).length > 0).toBeTrue(); - }); - - it('is robust to trace entry without WM and SF', () => { - entries.delete(TraceType.SURFACE_FLINGER); - entries.delete(TraceType.WINDOW_MANAGER); - presenter.notifyCurrentTraceEntries(entries); - expect(uiData.hierarchyUserOptions).toBeTruthy(); - expect(uiData.propertiesUserOptions).toBeTruthy(); - expect(Object.keys(uiData.tree!).length > 0).toBeTrue(); - }); - - it('can handle unavailable trace entry', () => { - presenter.notifyCurrentTraceEntries(entries); - expect(Object.keys(uiData.tree!).length > 0).toBeTrue(); - const emptyEntries = new Map(); - presenter.notifyCurrentTraceEntries(emptyEntries); expect(uiData.tree).toBeFalsy(); + + presenter.onTracePositionUpdate(position); + expect(uiData.hierarchyUserOptions).toBeTruthy(); + expect(uiData.propertiesUserOptions).toBeTruthy(); + expect(uiData.tree).toBeFalsy(); + }); + + it('is robust to traces without SF', async () => { + await setUpTestEnvironment([imeTraceType, TraceType.WINDOW_MANAGER]); + presenter.onTracePositionUpdate(position); + expect(uiData.hierarchyUserOptions).toBeTruthy(); + expect(uiData.propertiesUserOptions).toBeTruthy(); + expect(Object.keys(uiData.tree!).length > 0).toBeTrue(); + }); + + it('is robust to traces without WM', async () => { + await setUpTestEnvironment([imeTraceType, TraceType.SURFACE_FLINGER]); + presenter.onTracePositionUpdate(position); + expect(uiData.hierarchyUserOptions).toBeTruthy(); + expect(uiData.propertiesUserOptions).toBeTruthy(); + expect(Object.keys(uiData.tree!).length > 0).toBeTrue(); + }); + + it('is robust to traces without WM and SF', async () => { + await setUpTestEnvironment([imeTraceType]); + presenter.onTracePositionUpdate(position); + expect(uiData.hierarchyUserOptions).toBeTruthy(); + expect(uiData.propertiesUserOptions).toBeTruthy(); + expect(Object.keys(uiData.tree!).length > 0).toBeTrue(); + }); + + it('processes trace position updates', () => { + presenter.onTracePositionUpdate(position); + expect(uiData.hierarchyUserOptions).toBeTruthy(); + expect(uiData.propertiesUserOptions).toBeTruthy(); + expect(Object.keys(uiData.tree!).length > 0).toBeTrue(); }); it('can update pinned items', () => { @@ -131,7 +138,7 @@ export function executePresenterInputMethodTests( }; let expectedChildren = expectHierarchyTreeWithSfSubtree ? 2 : 1; - presenter.notifyCurrentTraceEntries(entries); + presenter.onTracePositionUpdate(position); expect(uiData.tree?.children.length).toEqual(expectedChildren); // Filter out non-visible child @@ -158,7 +165,7 @@ export function executePresenterInputMethodTests( }; const expectedChildren = expectHierarchyTreeWithSfSubtree ? 12 : 1; - presenter.notifyCurrentTraceEntries(entries); + presenter.onTracePositionUpdate(position); presenter.updateHierarchyTree(userOptions); expect(uiData.tree?.children.length).toEqual(expectedChildren); @@ -168,14 +175,14 @@ export function executePresenterInputMethodTests( }); it('can set new properties tree and associated ui data', () => { - presenter.notifyCurrentTraceEntries(entries); + presenter.onTracePositionUpdate(position); presenter.newPropertiesTree(selectedTree); // does not check specific tree values as tree transformation method may change expect(uiData.propertiesTree).toBeTruthy(); }); it('can filter properties tree', () => { - presenter.notifyCurrentTraceEntries(entries); + presenter.onTracePositionUpdate(position); presenter.newPropertiesTree(selectedTree); let nonTerminalChildren = uiData.propertiesTree?.children?.filter( @@ -191,5 +198,35 @@ export function executePresenterInputMethodTests( ) ?? []; expect(nonTerminalChildren.length).toEqual(expectedChildren[1]); }); + + const setUpTestEnvironment = async (traceTypes: TraceType[]) => { + const traces = new Traces(); + const entries = await UnitTestUtils.getImeTraceEntries(); + + traceTypes.forEach((traceType) => { + const trace = new TraceBuilder() + .setEntries([entries.get(traceType)]) + .setFrame(0, 0) + .build(); + traces.setTrace(traceType, trace); + }); + + presenter = createPresenter(traces); + + position = TracePosition.fromTraceEntry( + assertDefined(traces.getTrace(imeTraceType)).getEntry(0) + ); + }; + + const createPresenter = (traces: Traces): PresenterInputMethod => { + return new PresenterInputMethod( + traces, + new MockStorage(), + [imeTraceType], + (newData: ImeUiData) => { + uiData = newData; + } + ); + }; }); } diff --git a/tools/winscope/src/viewers/common/viewer_input_method.ts b/tools/winscope/src/viewers/common/viewer_input_method.ts index b3046bebb..81d579f25 100644 --- a/tools/winscope/src/viewers/common/viewer_input_method.ts +++ b/tools/winscope/src/viewers/common/viewer_input_method.ts @@ -13,6 +13,9 @@ * 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 {ImeUiData} from 'viewers/common/ime_ui_data'; import {PresenterInputMethod} from 'viewers/common/presenter_input_method'; @@ -20,14 +23,14 @@ import {ViewerEvents} from 'viewers/common/viewer_events'; import {View, Viewer} from 'viewers/viewer'; abstract class ViewerInputMethod implements Viewer { - constructor(storage: Storage) { + constructor(traces: Traces, storage: Storage) { this.htmlElement = document.createElement('viewer-input-method'); - this.presenter = this.initialisePresenter(storage); + this.presenter = this.initialisePresenter(traces, storage); this.addViewerEventListeners(); } - notifyCurrentTraceEntries(entries: Map): void { - this.presenter.notifyCurrentTraceEntries(entries); + onTracePositionUpdate(position: TracePosition) { + this.presenter.onTracePositionUpdate(position); } abstract getViews(): View[]; @@ -68,7 +71,7 @@ abstract class ViewerInputMethod implements Viewer { ); } - protected abstract initialisePresenter(storage: Storage): PresenterInputMethod; + protected abstract initialisePresenter(traces: Traces, storage: Storage): PresenterInputMethod; protected htmlElement: HTMLElement; protected presenter: PresenterInputMethod; diff --git a/tools/winscope/src/viewers/viewer.ts b/tools/winscope/src/viewers/viewer.ts index a4bab2060..e2629e04e 100644 --- a/tools/winscope/src/viewers/viewer.ts +++ b/tools/winscope/src/viewers/viewer.ts @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import {TracePosition} from 'trace/trace_position'; import {TraceType} from 'trace/trace_type'; enum ViewType { @@ -30,7 +32,7 @@ class View { } interface Viewer { - notifyCurrentTraceEntries(entries: Map): void; + onTracePositionUpdate(position: TracePosition): void; getViews(): View[]; getDependencies(): TraceType[]; } diff --git a/tools/winscope/src/viewers/viewer_factory.ts b/tools/winscope/src/viewers/viewer_factory.ts index fce356749..b1b348537 100644 --- a/tools/winscope/src/viewers/viewer_factory.ts +++ b/tools/winscope/src/viewers/viewer_factory.ts @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import {Traces} from 'trace/traces'; import {TraceType} from 'trace/trace_type'; import {Viewer} from './viewer'; import {ViewerInputMethodClients} from './viewer_input_method_clients/viewer_input_method_clients'; @@ -39,7 +41,7 @@ class ViewerFactory { ViewerScreenRecording, ]; - createViewers(activeTraceTypes: Set, storage: Storage): Viewer[] { + createViewers(activeTraceTypes: Set, traces: Traces, storage: Storage): Viewer[] { const viewers: Viewer[] = []; for (const Viewer of ViewerFactory.VIEWERS) { @@ -48,7 +50,7 @@ class ViewerFactory { ); if (areViewerDepsSatisfied) { - viewers.push(new Viewer(storage)); + viewers.push(new Viewer(traces, storage)); } } diff --git a/tools/winscope/src/viewers/viewer_input_method_clients/viewer_input_method_clients.ts b/tools/winscope/src/viewers/viewer_input_method_clients/viewer_input_method_clients.ts index 1ac2b5e18..6d55511f3 100644 --- a/tools/winscope/src/viewers/viewer_input_method_clients/viewer_input_method_clients.ts +++ b/tools/winscope/src/viewers/viewer_input_method_clients/viewer_input_method_clients.ts @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import {Traces} from 'trace/traces'; import {TraceType} from 'trace/trace_type'; import {ViewerInputMethod} from 'viewers/common/viewer_input_method'; import {View, ViewType} from 'viewers/viewer'; @@ -29,8 +31,13 @@ class ViewerInputMethodClients extends ViewerInputMethod { return ViewerInputMethodClients.DEPENDENCIES; } - override initialisePresenter(storage: Storage) { - return new PresenterInputMethodClients(this.imeUiCallback, this.getDependencies(), storage); + override initialisePresenter(traces: Traces, storage: Storage): PresenterInputMethodClients { + return new PresenterInputMethodClients( + traces, + storage, + this.getDependencies(), + this.imeUiCallback + ); } static readonly DEPENDENCIES: TraceType[] = [TraceType.INPUT_METHOD_CLIENTS]; diff --git a/tools/winscope/src/viewers/viewer_input_method_manager_service/viewer_input_method_manager_service.ts b/tools/winscope/src/viewers/viewer_input_method_manager_service/viewer_input_method_manager_service.ts index 4333bc9c9..dd24e8050 100644 --- a/tools/winscope/src/viewers/viewer_input_method_manager_service/viewer_input_method_manager_service.ts +++ b/tools/winscope/src/viewers/viewer_input_method_manager_service/viewer_input_method_manager_service.ts @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import {Traces} from 'trace/traces'; import {TraceType} from 'trace/trace_type'; import {ViewerInputMethod} from 'viewers/common/viewer_input_method'; import {View, ViewType} from 'viewers/viewer'; @@ -34,11 +36,12 @@ class ViewerInputMethodManagerService extends ViewerInputMethod { return ViewerInputMethodManagerService.DEPENDENCIES; } - override initialisePresenter(storage: Storage) { + override initialisePresenter(traces: Traces, storage: Storage) { return new PresenterInputMethodManagerService( - this.imeUiCallback, + traces, + storage, this.getDependencies(), - storage + this.imeUiCallback ); } diff --git a/tools/winscope/src/viewers/viewer_input_method_service/viewer_input_method_service.ts b/tools/winscope/src/viewers/viewer_input_method_service/viewer_input_method_service.ts index 7bdc1c99a..fe205a71a 100644 --- a/tools/winscope/src/viewers/viewer_input_method_service/viewer_input_method_service.ts +++ b/tools/winscope/src/viewers/viewer_input_method_service/viewer_input_method_service.ts @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import {Traces} from 'trace/traces'; import {TraceType} from 'trace/trace_type'; import {ViewerInputMethod} from 'viewers/common/viewer_input_method'; import {View, ViewType} from 'viewers/viewer'; @@ -29,8 +31,13 @@ class ViewerInputMethodService extends ViewerInputMethod { return ViewerInputMethodService.DEPENDENCIES; } - override initialisePresenter(storage: Storage) { - return new PresenterInputMethodService(this.imeUiCallback, this.getDependencies(), storage); + override initialisePresenter(traces: Traces, storage: Storage) { + return new PresenterInputMethodService( + traces, + storage, + this.getDependencies(), + this.imeUiCallback + ); } static readonly DEPENDENCIES: TraceType[] = [TraceType.INPUT_METHOD_SERVICE]; diff --git a/tools/winscope/src/viewers/viewer_protolog/presenter.ts b/tools/winscope/src/viewers/viewer_protolog/presenter.ts index a4c49b3b2..f3129f08c 100644 --- a/tools/winscope/src/viewers/viewer_protolog/presenter.ts +++ b/tools/winscope/src/viewers/viewer_protolog/presenter.ts @@ -13,25 +13,39 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import {ArrayUtils} from 'common/array_utils'; -import {LogMessage, ProtoLogTraceEntry} from 'trace/protolog'; +import {LogMessage} from 'trace/protolog'; +import {Trace, TraceEntry} 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 {assertDefined} from '../../common/assert_utils'; import {UiData} from './ui_data'; export class Presenter { - constructor(notifyUiDataCallback: (data: UiData) => void) { + private readonly trace: Trace; + private readonly notifyUiDataCallback: (data: UiData) => void; + private entry?: TraceEntry; + private originalIndicesOfFilteredOutputMessages: number[]; + private uiData = UiData.EMPTY; + + private tags: string[] = []; + private files: string[] = []; + private levels: string[] = []; + private searchString = ''; + + constructor(traces: Traces, notifyUiDataCallback: (data: UiData) => void) { + this.trace = assertDefined(traces.getTrace(TraceType.PROTO_LOG)); this.notifyUiDataCallback = notifyUiDataCallback; this.originalIndicesOfFilteredOutputMessages = []; - this.uiData = UiData.EMPTY; + this.computeUiDataMessages(); this.notifyUiDataCallback(this.uiData); } - //TODO: replace input with something like iterator/cursor (same for other viewers/presenters) - notifyCurrentTraceEntries(entries: Map): void { - this.entry = entries.get(TraceType.PROTO_LOG) ? entries.get(TraceType.PROTO_LOG)[0] : undefined; - if (this.uiData === UiData.EMPTY) { - this.computeUiDataMessages(); - } + onTracePositionUpdate(position: TracePosition) { + this.entry = TraceEntryFinder.findCorrespondingEntry(this.trace, position); this.computeUiDataCurrentMessageIndex(); this.notifyUiDataCallback(this.uiData); } @@ -65,26 +79,14 @@ export class Presenter { } private computeUiDataMessages() { - if (!this.entry) { - return; - } + const allLogLevels = this.getUniqueMessageValues((message: LogMessage) => message.level); + const allTags = this.getUniqueMessageValues((message: LogMessage) => message.tag); + const allSourceFiles = this.getUniqueMessageValues((message: LogMessage) => message.at); - const allLogLevels = this.getUniqueMessageValues( - this.entry!.messages, - (message: LogMessage) => message.level - ); - const allTags = this.getUniqueMessageValues( - this.entry!.messages, - (message: LogMessage) => message.tag - ); - const allSourceFiles = this.getUniqueMessageValues( - this.entry!.messages, - (message: LogMessage) => message.at - ); - - let filteredMessagesAndOriginalIndex: Array<[number, LogMessage]> = [ - ...this.entry!.messages.entries(), - ]; + let filteredMessagesAndOriginalIndex = new Array<[number, LogMessage]>(); + this.trace.forEachEntry((entry) => { + filteredMessagesAndOriginalIndex.push([entry.getIndex(), entry.getValue()]); + }); if (this.levels.length > 0) { filteredMessagesAndOriginalIndex = filteredMessagesAndOriginalIndex.filter((value) => @@ -113,40 +115,34 @@ export class Presenter { ); const filteredMessages = filteredMessagesAndOriginalIndex.map((value) => value[1]); - this.uiData = new UiData(allLogLevels, allTags, allSourceFiles, filteredMessages, 0); + this.uiData = new UiData(allLogLevels, allTags, allSourceFiles, filteredMessages, undefined); } private computeUiDataCurrentMessageIndex() { if (!this.entry) { + this.uiData.currentMessageIndex = undefined; return; } - this.uiData.currentMessageIndex = ArrayUtils.binarySearchLowerOrEqual( - this.originalIndicesOfFilteredOutputMessages, - this.entry.currentMessageIndex - ); + if (this.originalIndicesOfFilteredOutputMessages.length === 0) { + this.uiData.currentMessageIndex = undefined; + return; + } + + this.uiData.currentMessageIndex = + ArrayUtils.binarySearchFirstGreaterOrEqual( + this.originalIndicesOfFilteredOutputMessages, + this.entry.getIndex() + ) ?? this.originalIndicesOfFilteredOutputMessages.length - 1; } - private getUniqueMessageValues( - messages: LogMessage[], - getValue: (message: LogMessage) => string - ): string[] { + private getUniqueMessageValues(getValue: (message: LogMessage) => string): string[] { const uniqueValues = new Set(); - messages.forEach((message) => { - uniqueValues.add(getValue(message)); + this.trace.forEachEntry((entry) => { + uniqueValues.add(getValue(entry.getValue())); }); const result = [...uniqueValues]; result.sort(); return result; } - - private entry?: ProtoLogTraceEntry; - private originalIndicesOfFilteredOutputMessages: number[]; - private uiData: UiData; - private readonly notifyUiDataCallback: (data: UiData) => void; - - private tags: string[] = []; - private files: string[] = []; - private levels: string[] = []; - private searchString = ''; } diff --git a/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts b/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts index 983a78c0c..cf461d816 100644 --- a/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts +++ b/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts @@ -13,7 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {LogMessage, ProtoLogTraceEntry} from 'trace/protolog'; + +import {TracesBuilder} from 'test/unit/traces_builder'; +import {TraceBuilder} from 'test/unit/trace_builder'; +import {LogMessage} from 'trace/protolog'; +import {RealTimestamp} from 'trace/timestamp'; +import {Trace} from 'trace/trace'; +import {Traces} from 'trace/traces'; +import {TracePosition} from 'trace/trace_position'; import {TraceType} from 'trace/trace_type'; import {Presenter} from './presenter'; import {UiData} from './ui_data'; @@ -21,41 +28,56 @@ import {UiData} from './ui_data'; describe('ViewerProtoLogPresenter', () => { let presenter: Presenter; let inputMessages: LogMessage[]; - let inputTraceEntries: Map; + let trace: Trace; + let position10: TracePosition; + let position11: TracePosition; + let position12: TracePosition; let outputUiData: undefined | UiData; beforeEach(async () => { + const time10 = new RealTimestamp(10n); + const time11 = new RealTimestamp(11n); + const time12 = new RealTimestamp(12n); + inputMessages = [ new LogMessage('text0', 'time', 'tag0', 'level0', 'sourcefile0', 10n), - new LogMessage('text1', 'time', 'tag1', 'level1', 'sourcefile1', 10n), - new LogMessage('text2', 'time', 'tag2', 'level2', 'sourcefile2', 10n), + new LogMessage('text1', 'time', 'tag1', 'level1', 'sourcefile1', 11n), + new LogMessage('text2', 'time', 'tag2', 'level2', 'sourcefile2', 12n), ]; - inputTraceEntries = new Map(); - inputTraceEntries.set(TraceType.PROTO_LOG, [new ProtoLogTraceEntry(inputMessages, 0)]); + trace = new TraceBuilder() + .setEntries(inputMessages) + .setTimestamps([time10, time11, time12]) + .build(); + + position10 = TracePosition.fromTimestamp(time10); + position11 = TracePosition.fromTimestamp(time11); + position12 = TracePosition.fromTimestamp(time12); outputUiData = undefined; - presenter = new Presenter((data: UiData) => { + const traces = new Traces(); + traces.setTrace(TraceType.PROTO_LOG, trace); + presenter = new Presenter(traces, (data: UiData) => { outputUiData = data; }); }); - it('is robust to undefined trace entry', () => { - presenter.notifyCurrentTraceEntries(new Map()); + it('is robust to empty trace', () => { + const traces = new TracesBuilder().setEntries(TraceType.PROTO_LOG, []).build(); + presenter = new Presenter(traces, (data: UiData) => { + outputUiData = data; + }); + + expect(outputUiData!.messages).toEqual([]); + expect(outputUiData!.currentMessageIndex).toBeUndefined(); + + presenter.onTracePositionUpdate(position10); expect(outputUiData!.messages).toEqual([]); expect(outputUiData!.currentMessageIndex).toBeUndefined(); }); - it("ignores undefined trace entry and doesn't discard displayed messages", () => { - presenter.notifyCurrentTraceEntries(inputTraceEntries); - expect(outputUiData!.messages).toEqual(inputMessages); - - presenter.notifyCurrentTraceEntries(new Map()); - expect(outputUiData!.messages).toEqual(inputMessages); - }); - - it('processes current trace entries', () => { - presenter.notifyCurrentTraceEntries(inputTraceEntries); + it('processes trace position updates', () => { + presenter.onTracePositionUpdate(position10); expect(outputUiData!.allLogLevels).toEqual(['level0', 'level1', 'level2']); expect(outputUiData!.allTags).toEqual(['tag0', 'tag1', 'tag2']); @@ -64,8 +86,7 @@ describe('ViewerProtoLogPresenter', () => { expect(outputUiData!.currentMessageIndex).toEqual(0); }); - it('updated displayed messages according to log levels filter', () => { - presenter.notifyCurrentTraceEntries(inputTraceEntries); + it('updates displayed messages according to log levels filter', () => { expect(outputUiData!.messages).toEqual(inputMessages); presenter.onLogLevelsFilterChanged([]); @@ -79,7 +100,6 @@ describe('ViewerProtoLogPresenter', () => { }); it('updates displayed messages according to tags filter', () => { - presenter.notifyCurrentTraceEntries(inputTraceEntries); expect(outputUiData!.messages).toEqual(inputMessages); presenter.onTagsFilterChanged([]); @@ -93,7 +113,6 @@ describe('ViewerProtoLogPresenter', () => { }); it('updates displayed messages according to source files filter', () => { - presenter.notifyCurrentTraceEntries(inputTraceEntries); expect(outputUiData!.messages).toEqual(inputMessages); presenter.onSourceFilesFilterChanged([]); @@ -107,7 +126,6 @@ describe('ViewerProtoLogPresenter', () => { }); it('updates displayed messages according to search string filter', () => { - presenter.notifyCurrentTraceEntries(inputTraceEntries); expect(outputUiData!.messages).toEqual(inputMessages); presenter.onSearchStringFilterChanged(''); @@ -124,7 +142,8 @@ describe('ViewerProtoLogPresenter', () => { }); it('computes current message index', () => { - presenter.notifyCurrentTraceEntries(inputTraceEntries); + // Position -> entry #0 + presenter.onTracePositionUpdate(position10); presenter.onLogLevelsFilterChanged([]); expect(outputUiData!.currentMessageIndex).toEqual(0); @@ -134,8 +153,8 @@ describe('ViewerProtoLogPresenter', () => { presenter.onLogLevelsFilterChanged([]); expect(outputUiData!.currentMessageIndex).toEqual(0); - (inputTraceEntries.get(TraceType.PROTO_LOG)[0] as ProtoLogTraceEntry).currentMessageIndex = 1; - presenter.notifyCurrentTraceEntries(inputTraceEntries); + // Position -> entry #1 + presenter.onTracePositionUpdate(position11); presenter.onLogLevelsFilterChanged([]); expect(outputUiData!.currentMessageIndex).toEqual(1); @@ -147,5 +166,10 @@ describe('ViewerProtoLogPresenter', () => { presenter.onLogLevelsFilterChanged(['level0', 'level1']); expect(outputUiData!.currentMessageIndex).toEqual(1); + + // Position -> entry #2 + presenter.onTracePositionUpdate(position12); + presenter.onLogLevelsFilterChanged([]); + expect(outputUiData!.currentMessageIndex).toEqual(2); }); }); diff --git a/tools/winscope/src/viewers/viewer_protolog/viewer_protolog.ts b/tools/winscope/src/viewers/viewer_protolog/viewer_protolog.ts index 7f3eed48e..c71f390d0 100644 --- a/tools/winscope/src/viewers/viewer_protolog/viewer_protolog.ts +++ b/tools/winscope/src/viewers/viewer_protolog/viewer_protolog.ts @@ -13,6 +13,9 @@ * 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'; @@ -20,10 +23,10 @@ import {Presenter} from './presenter'; import {UiData} from './ui_data'; class ViewerProtoLog implements Viewer { - constructor() { + constructor(traces: Traces) { this.htmlElement = document.createElement('viewer-protolog'); - this.presenter = new Presenter((data: UiData) => { + this.presenter = new Presenter(traces, (data: UiData) => { (this.htmlElement as any).inputData = data; }); @@ -41,8 +44,8 @@ class ViewerProtoLog implements Viewer { }); } - notifyCurrentTraceEntries(entries: Map): void { - this.presenter.notifyCurrentTraceEntries(entries); + onTracePositionUpdate(position: TracePosition) { + this.presenter.onTracePositionUpdate(position); } getViews(): View[] { diff --git a/tools/winscope/src/viewers/viewer_screen_recording/viewer_screen_recording.ts b/tools/winscope/src/viewers/viewer_screen_recording/viewer_screen_recording.ts index 51cd7df6b..db26c900a 100644 --- a/tools/winscope/src/viewers/viewer_screen_recording/viewer_screen_recording.ts +++ b/tools/winscope/src/viewers/viewer_screen_recording/viewer_screen_recording.ts @@ -13,21 +13,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import {assertDefined} from 'common/assert_utils'; import {ScreenRecordingTraceEntry} from 'trace/screen_recording'; +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 {View, Viewer, ViewType} from 'viewers/viewer'; +import {ViewerScreenRecordingComponent} from './viewer_screen_recording_component'; class ViewerScreenRecording implements Viewer { - constructor() { + static readonly DEPENDENCIES: TraceType[] = [TraceType.SCREEN_RECORDING]; + private readonly trace: Trace; + private readonly htmlElement: HTMLElement; + + constructor(traces: Traces) { + this.trace = assertDefined(traces.getTrace(TraceType.SCREEN_RECORDING)); this.htmlElement = document.createElement('viewer-screen-recording'); } - notifyCurrentTraceEntries(entries: Map): void { - const entry: undefined | ScreenRecordingTraceEntry = entries.get(TraceType.SCREEN_RECORDING) - ? entries.get(TraceType.SCREEN_RECORDING)[0] - : undefined; - - (this.htmlElement as any).currentTraceEntry = entry; + onTracePositionUpdate(position: TracePosition) { + const entry = TraceEntryFinder.findCorrespondingEntry(this.trace, position); + (this.htmlElement as unknown as ViewerScreenRecordingComponent).currentTraceEntry = + entry?.getValue(); } getViews(): View[] { @@ -39,9 +49,6 @@ class ViewerScreenRecording implements Viewer { getDependencies(): TraceType[] { return ViewerScreenRecording.DEPENDENCIES; } - - static readonly DEPENDENCIES: TraceType[] = [TraceType.SCREEN_RECORDING]; - private htmlElement: HTMLElement; } export {ViewerScreenRecording}; diff --git a/tools/winscope/src/viewers/viewer_stub.ts b/tools/winscope/src/viewers/viewer_stub.ts index 2f7592dc3..088c5f4ad 100644 --- a/tools/winscope/src/viewers/viewer_stub.ts +++ b/tools/winscope/src/viewers/viewer_stub.ts @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import {TracePosition} from 'trace/trace_position'; import {View, Viewer, ViewType} from './viewer'; class ViewerStub implements Viewer { @@ -27,7 +29,7 @@ class ViewerStub implements Viewer { } } - notifyCurrentTraceEntries(entries: any) { + onTracePositionUpdate(position: TracePosition) { // do nothing } diff --git a/tools/winscope/src/viewers/viewer_surface_flinger/presenter.ts b/tools/winscope/src/viewers/viewer_surface_flinger/presenter.ts index 271c33d61..57edcdb34 100644 --- a/tools/winscope/src/viewers/viewer_surface_flinger/presenter.ts +++ b/tools/winscope/src/viewers/viewer_surface_flinger/presenter.ts @@ -13,9 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import {assertDefined} from 'common/assert_utils'; import {PersistentStoreProxy} from 'common/persistent_store_proxy'; import {FilterType, TreeUtils} from 'common/tree_utils'; -import {Layer, LayerTraceEntry} from 'trace/flickerlib/common'; +import {Layer} from 'trace/flickerlib/layers/Layer'; +import {LayerTraceEntry} from 'trace/flickerlib/layers/LayerTraceEntry'; +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 {Rectangle, RectMatrix, RectTransform} from 'viewers/common/rectangle'; import {TreeGenerator} from 'viewers/common/tree_generator'; @@ -27,12 +34,94 @@ import {UiData} from './ui_data'; type NotifyViewCallbackType = (uiData: UiData) => void; export class Presenter { - constructor(notifyViewCallback: NotifyViewCallbackType, private storage: Storage) { + private readonly notifyViewCallback: NotifyViewCallbackType; + private readonly trace: Trace; + private uiData: UiData; + private hierarchyFilter: FilterType = TreeUtils.makeNodeFilter(''); + private propertiesFilter: FilterType = TreeUtils.makeNodeFilter(''); + private highlightedItems: string[] = []; + private displayIds: number[] = []; + private pinnedItems: HierarchyTreeNode[] = []; + private pinnedIds: string[] = []; + private selectedHierarchyTree: HierarchyTreeNode | null = null; + private selectedLayer: LayerTraceEntry | Layer | null = null; + private previousEntry: LayerTraceEntry | null = null; + private entry: LayerTraceEntry | null = null; + private hierarchyUserOptions: UserOptions = PersistentStoreProxy.new( + 'SfHierarchyOptions', + { + showDiff: { + name: 'Show diff', // TODO: PersistentStoreObject.Ignored("Show diff") or something like that to instruct to not store this info + enabled: false, + }, + simplifyNames: { + name: 'Simplify names', + enabled: true, + }, + onlyVisible: { + name: 'Only visible', + enabled: false, + }, + flat: { + name: 'Flat', + enabled: false, + }, + }, + this.storage + ); + + private propertiesUserOptions: UserOptions = PersistentStoreProxy.new( + 'SfPropertyOptions', + { + showDiff: { + name: 'Show diff', + enabled: false, + }, + showDefaults: { + name: 'Show defaults', + enabled: false, + tooltip: ` + If checked, shows the value of all properties. + Otherwise, hides all properties whose value is + the default for its data type. + `, + }, + }, + this.storage + ); + + constructor( + traces: Traces, + private readonly storage: Storage, + notifyViewCallback: NotifyViewCallbackType + ) { + this.trace = assertDefined(traces.getTrace(TraceType.SURFACE_FLINGER)); this.notifyViewCallback = notifyViewCallback; this.uiData = new UiData([TraceType.SURFACE_FLINGER]); this.copyUiDataAndNotifyView(); } + onTracePositionUpdate(position: TracePosition) { + this.uiData = new UiData(); + this.uiData.hierarchyUserOptions = this.hierarchyUserOptions; + this.uiData.propertiesUserOptions = this.propertiesUserOptions; + + const entry = TraceEntryFinder.findCorrespondingEntry(this.trace, position); + const prevEntry = + entry && entry.getIndex() > 0 ? this.trace.getEntry(entry.getIndex() - 1) : undefined; + + this.entry = entry?.getValue() ?? null; + this.previousEntry = prevEntry?.getValue() ?? null; + if (this.entry) { + this.uiData.highlightedItems = this.highlightedItems; + this.uiData.rects = this.generateRects(); + this.uiData.displayIds = this.displayIds; + this.uiData.tree = this.generateTree(); + } + + this.copyUiDataAndNotifyView(); + } + updatePinnedItems(pinnedItem: HierarchyTreeNode) { const pinnedId = `${pinnedItem.id}`; if (this.pinnedItems.map((item) => `${item.id}`).includes(pinnedId)) { @@ -85,24 +174,6 @@ export class Presenter { this.updateSelectedTreeUiData(); } - notifyCurrentTraceEntries(entries: Map) { - this.uiData = new UiData(); - this.uiData.hierarchyUserOptions = this.hierarchyUserOptions; - this.uiData.propertiesUserOptions = this.propertiesUserOptions; - - const sfEntries = entries.get(TraceType.SURFACE_FLINGER); - if (sfEntries) { - [this.entry, this.previousEntry] = sfEntries; - if (this.entry) { - this.uiData.highlightedItems = this.highlightedItems; - this.uiData.rects = this.generateRects(); - this.uiData.displayIds = this.displayIds; - this.uiData.tree = this.generateTree(); - } - } - this.copyUiDataAndNotifyView(); - } - private generateRects(): Rectangle[] { const displayRects = this.entry.displays.map((display: any) => { @@ -273,59 +344,4 @@ export class Presenter { const copy = Object.assign({}, this.uiData); this.notifyViewCallback(copy); } - - private readonly notifyViewCallback: NotifyViewCallbackType; - private uiData: UiData; - private hierarchyFilter: FilterType = TreeUtils.makeNodeFilter(''); - private propertiesFilter: FilterType = TreeUtils.makeNodeFilter(''); - private highlightedItems: string[] = []; - private displayIds: number[] = []; - private pinnedItems: HierarchyTreeNode[] = []; - private pinnedIds: string[] = []; - private selectedHierarchyTree: HierarchyTreeNode | null = null; - private selectedLayer: LayerTraceEntry | Layer | null = null; - private previousEntry: LayerTraceEntry | null = null; - private entry: LayerTraceEntry | null = null; - private hierarchyUserOptions: UserOptions = PersistentStoreProxy.new( - 'SfHierarchyOptions', - { - showDiff: { - name: 'Show diff', // TODO: PersistentStoreObject.Ignored("Show diff") or something like that to instruct to not store this info - enabled: false, - }, - simplifyNames: { - name: 'Simplify names', - enabled: true, - }, - onlyVisible: { - name: 'Only visible', - enabled: false, - }, - flat: { - name: 'Flat', - enabled: false, - }, - }, - this.storage - ); - - private propertiesUserOptions: UserOptions = PersistentStoreProxy.new( - 'SfPropertyOptions', - { - showDiff: { - name: 'Show diff', - enabled: false, - }, - showDefaults: { - name: 'Show defaults', - enabled: false, - tooltip: ` - If checked, shows the value of all properties. - Otherwise, hides all properties whose value is - the default for its data type. - `, - }, - }, - this.storage - ); } diff --git a/tools/winscope/src/viewers/viewer_surface_flinger/presenter_test.ts b/tools/winscope/src/viewers/viewer_surface_flinger/presenter_test.ts index dab9dd6c3..484852bfe 100644 --- a/tools/winscope/src/viewers/viewer_surface_flinger/presenter_test.ts +++ b/tools/winscope/src/viewers/viewer_surface_flinger/presenter_test.ts @@ -13,10 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import {HierarchyTreeBuilder} from 'test/unit/hierarchy_tree_builder'; import {MockStorage} from 'test/unit/mock_storage'; +import {TraceBuilder} from 'test/unit/trace_builder'; import {UnitTestUtils} from 'test/unit/utils'; -import {LayerTraceEntry} from 'trace/flickerlib/common'; +import {LayerTraceEntry} from 'trace/flickerlib/layers/LayerTraceEntry'; +import {RealTimestamp} from 'trace/timestamp'; +import {Trace} from 'trace/trace'; +import {Traces} from 'trace/traces'; +import {TracePosition} from 'trace/trace_position'; import {TraceType} from 'trace/trace_type'; import {HierarchyTreeNode, PropertiesTreeNode} from 'viewers/common/ui_tree_utils'; import {UserOptions} from 'viewers/common/user_options'; @@ -24,14 +30,23 @@ import {Presenter} from './presenter'; import {UiData} from './ui_data'; describe('PresenterSurfaceFlinger', () => { + let trace: Trace; + let position: TracePosition; + let positionMultiDisplayEntry: TracePosition; let presenter: Presenter; let uiData: UiData; - let entries: Map; let selectedTree: HierarchyTreeNode; beforeAll(async () => { - entries = new Map(); - const entry: LayerTraceEntry = await UnitTestUtils.getLayerTraceEntry(); + trace = new TraceBuilder() + .setEntries([ + await UnitTestUtils.getLayerTraceEntry(), + await UnitTestUtils.getMultiDisplayLayerTraceEntry(), + ]) + .build(); + + position = TracePosition.fromTraceEntry(trace.getEntry(0)); + positionMultiDisplayEntry = TracePosition.fromTraceEntry(trace.getEntry(1)); selectedTree = new HierarchyTreeBuilder() .setName('Dim layer#53') @@ -41,18 +56,24 @@ describe('PresenterSurfaceFlinger', () => { .setDiffType('EffectLayer') .setId(53) .build(); - - entries.set(TraceType.SURFACE_FLINGER, [entry, null]); }); beforeEach(async () => { - presenter = new Presenter((newData: UiData) => { - uiData = newData; - }, new MockStorage()); + presenter = createPresenter(trace); }); - it('processes current trace entries', () => { - presenter.notifyCurrentTraceEntries(entries); + it('is robust to empty trace', () => { + const emptyTrace = new TraceBuilder().setEntries([]).build(); + const presenter = createPresenter(emptyTrace); + + const positionWithoutTraceEntry = TracePosition.fromTimestamp(new RealTimestamp(0n)); + presenter.onTracePositionUpdate(positionWithoutTraceEntry); + expect(uiData.hierarchyUserOptions).toBeTruthy(); + expect(uiData.tree).toBeFalsy(); + }); + + it('processes trace position updates', () => { + presenter.onTracePositionUpdate(position); expect(uiData.rects.length).toBeGreaterThan(0); expect(uiData.highlightedItems?.length).toEqual(0); @@ -68,18 +89,8 @@ describe('PresenterSurfaceFlinger', () => { expect(Object.keys(uiData.tree!).length > 0).toBeTrue(); }); - it('handles unavailable trace entry', () => { - presenter.notifyCurrentTraceEntries(entries); - expect(uiData.tree).toBeDefined(); - expect(Object.keys(uiData.tree!).length > 0).toBeTrue(); - - const emptyEntries = new Map(); - presenter.notifyCurrentTraceEntries(emptyEntries); - expect(uiData.tree).toBeFalsy(); - }); - it('creates input data for rects view', () => { - presenter.notifyCurrentTraceEntries(entries); + presenter.onTracePositionUpdate(position); expect(uiData.rects.length).toBeGreaterThan(0); expect(uiData.rects[0].topLeft).toEqual({x: 0, y: 0}); expect(uiData.rects[0].bottomRight).toEqual({x: 1080, y: 118}); @@ -126,7 +137,7 @@ describe('PresenterSurfaceFlinger', () => { }, }; - presenter.notifyCurrentTraceEntries(entries); + presenter.onTracePositionUpdate(position); expect(uiData.tree?.children.length).toEqual(3); presenter.updateHierarchyTree(userOptions); @@ -154,7 +165,7 @@ describe('PresenterSurfaceFlinger', () => { enabled: true, }, }; - presenter.notifyCurrentTraceEntries(entries); + presenter.onTracePositionUpdate(position); presenter.updateHierarchyTree(userOptions); expect(uiData.tree?.children.length).toEqual(94); presenter.filterHierarchyTree('Wallpaper'); @@ -163,7 +174,7 @@ describe('PresenterSurfaceFlinger', () => { }); it('sets properties tree and associated ui data', () => { - presenter.notifyCurrentTraceEntries(entries); + presenter.onTracePositionUpdate(position); presenter.newPropertiesTree(selectedTree); // does not check specific tree values as tree transformation method may change expect(uiData.propertiesTree).toBeTruthy(); @@ -187,7 +198,7 @@ describe('PresenterSurfaceFlinger', () => { }, }; - presenter.notifyCurrentTraceEntries(entries); + presenter.onTracePositionUpdate(position); presenter.newPropertiesTree(selectedTree); expect(uiData.propertiesTree?.diffType).toBeFalsy(); @@ -197,7 +208,7 @@ describe('PresenterSurfaceFlinger', () => { }); it('filters properties tree', () => { - presenter.notifyCurrentTraceEntries(entries); + presenter.onTracePositionUpdate(position); presenter.newPropertiesTree(selectedTree); let nonTerminalChildren = uiData.propertiesTree?.children?.filter( @@ -215,18 +226,17 @@ describe('PresenterSurfaceFlinger', () => { }); it('handles displays with no visible layers', async () => { - presenter.notifyCurrentTraceEntries(await getEntriesWithMultiDisplaySfTrace()); + presenter.onTracePositionUpdate(positionMultiDisplayEntry); expect(uiData.displayIds.length).toEqual(5); // we want the ids to be sorted expect(uiData.displayIds).toEqual([0, 2, 3, 4, 5]); }); - async function getEntriesWithMultiDisplaySfTrace(): Promise> { - const entries = new Map(); - const entry: LayerTraceEntry = await UnitTestUtils.getMultiDisplayLayerTraceEntry(); - - entries.set(TraceType.SURFACE_FLINGER, [entry, null]); - - return entries; - } + const createPresenter = (trace: Trace): Presenter => { + const traces = new Traces(); + traces.setTrace(TraceType.SURFACE_FLINGER, trace); + return new Presenter(traces, new MockStorage(), (newData: UiData) => { + uiData = newData; + }); + }; }); diff --git a/tools/winscope/src/viewers/viewer_surface_flinger/viewer_surface_flinger.ts b/tools/winscope/src/viewers/viewer_surface_flinger/viewer_surface_flinger.ts index b3e56017d..ff2b12eae 100644 --- a/tools/winscope/src/viewers/viewer_surface_flinger/viewer_surface_flinger.ts +++ b/tools/winscope/src/viewers/viewer_surface_flinger/viewer_surface_flinger.ts @@ -13,6 +13,9 @@ * 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 {ViewerEvents} from 'viewers/common/viewer_events'; import {View, Viewer, ViewType} from 'viewers/viewer'; @@ -21,15 +24,15 @@ import {UiData} from './ui_data'; class ViewerSurfaceFlinger implements Viewer { static readonly DEPENDENCIES: TraceType[] = [TraceType.SURFACE_FLINGER]; - private htmlElement: HTMLElement; - private presenter: Presenter; + private readonly htmlElement: HTMLElement; + private readonly presenter: Presenter; - constructor(storage: Storage) { + constructor(traces: Traces, storage: Storage) { this.htmlElement = document.createElement('viewer-surface-flinger'); - this.presenter = new Presenter((uiData: UiData) => { + this.presenter = new Presenter(traces, storage, (uiData: UiData) => { (this.htmlElement as any).inputData = uiData; - }, storage); + }); this.htmlElement.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) => this.presenter.updatePinnedItems((event as CustomEvent).detail.pinnedItem) @@ -54,8 +57,8 @@ class ViewerSurfaceFlinger implements Viewer { ); } - notifyCurrentTraceEntries(entries: Map): void { - this.presenter.notifyCurrentTraceEntries(entries); + onTracePositionUpdate(position: TracePosition) { + this.presenter.onTracePositionUpdate(position); } getViews(): View[] { diff --git a/tools/winscope/src/viewers/viewer_transactions/presenter.ts b/tools/winscope/src/viewers/viewer_transactions/presenter.ts index 2ed43966b..557ba18de 100644 --- a/tools/winscope/src/viewers/viewer_transactions/presenter.ts +++ b/tools/winscope/src/viewers/viewer_transactions/presenter.ts @@ -13,20 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import {ArrayUtils} from 'common/array_utils'; +import {assertDefined} from 'common/assert_utils'; import {TimeUtils} from 'common/time_utils'; import {ObjectFormatter} from 'trace/flickerlib/ObjectFormatter'; -import {ElapsedTimestamp, RealTimestamp, TimestampType} from 'trace/timestamp'; +import {Trace, TraceEntry} 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 {TransactionsTraceEntry} from 'trace/transactions'; import {PropertiesTreeGenerator} from 'viewers/common/properties_tree_generator'; import {PropertiesTreeNode} from 'viewers/common/ui_tree_utils'; import {UiData, UiDataEntry, UiDataEntryType} from './ui_data'; export class Presenter { - private entry?: TransactionsTraceEntry; + private trace: Trace; + private entry?: TraceEntry; private originalIndicesOfUiDataEntries: number[]; - private uiData: UiData; + private uiData = UiData.EMPTY; private readonly notifyUiDataCallback: (data: UiData) => void; private static readonly VALUE_NA = 'N/A'; private vsyncIdFilter: string[] = []; @@ -36,31 +41,26 @@ export class Presenter { private idFilter: string[] = []; private whatSearchString = ''; - constructor(notifyUiDataCallback: (data: UiData) => void) { + constructor(traces: Traces, notifyUiDataCallback: (data: UiData) => void) { + this.trace = assertDefined(traces.getTrace(TraceType.TRANSACTIONS)); this.notifyUiDataCallback = notifyUiDataCallback; this.originalIndicesOfUiDataEntries = []; - this.uiData = UiData.EMPTY; + this.computeUiData(); this.notifyUiDataCallback(this.uiData); } - //TODO: replace input with something like iterator/cursor (same for other viewers/presenters) - notifyCurrentTraceEntries(entries: Map): void { - this.entry = entries.get(TraceType.TRANSACTIONS) - ? entries.get(TraceType.TRANSACTIONS)[0] - : undefined; - if (this.uiData === UiData.EMPTY) { - this.computeUiData(); - } else { - // update only "position" data - this.uiData.currentEntryIndex = this.computeCurrentEntryIndex(); - this.uiData.selectedEntryIndex = undefined; - this.uiData.scrollToIndex = this.uiData.currentEntryIndex; - this.uiData.currentPropertiesTree = this.computeCurrentPropertiesTree( - this.uiData.entries, - this.uiData.currentEntryIndex, - this.uiData.selectedEntryIndex - ); - } + onTracePositionUpdate(position: TracePosition) { + this.entry = TraceEntryFinder.findCorrespondingEntry(this.trace, position); + + this.uiData.currentEntryIndex = this.computeCurrentEntryIndex(); + this.uiData.selectedEntryIndex = undefined; + this.uiData.scrollToIndex = this.uiData.currentEntryIndex; + this.uiData.currentPropertiesTree = this.computeCurrentPropertiesTree( + this.uiData.entries, + this.uiData.currentEntryIndex, + this.uiData.selectedEntryIndex + ); + this.notifyUiDataCallback(this.uiData); } @@ -119,11 +119,7 @@ export class Presenter { } private computeUiData() { - if (!this.entry) { - return; - } - - const entries = this.makeUiDataEntries(this.entry!); + const entries = this.makeUiDataEntries(); const allVSyncIds = this.getUniqueUiDataEntryValues(entries, (entry: UiDataEntry) => entry.vsyncId.toString() @@ -190,9 +186,15 @@ export class Presenter { return undefined; } - return ArrayUtils.binarySearchLowerOrEqual( - this.originalIndicesOfUiDataEntries, - this.entry.currentEntryIndex + if (this.originalIndicesOfUiDataEntries.length === 0) { + return undefined; + } + + return ( + ArrayUtils.binarySearchFirstGreaterOrEqual( + this.originalIndicesOfUiDataEntries, + this.entry.getIndex() + ) ?? this.originalIndicesOfUiDataEntries.length - 1 ); } @@ -210,23 +212,23 @@ export class Presenter { return undefined; } - private makeUiDataEntries(entry: TransactionsTraceEntry): UiDataEntry[] { - const entriesProto: any[] = entry.entriesProto; - const timestampType = entry.timestampType; - const realToElapsedTimeOffsetNs = entry.realToElapsedTimeOffsetNs; + private makeUiDataEntries(): UiDataEntry[] { const treeGenerator = new PropertiesTreeGenerator(); - const entries: UiDataEntry[] = []; - const formattingOptions = ObjectFormatter.displayDefaults; ObjectFormatter.displayDefaults = false; - for (const [originalIndex, entryProto] of entriesProto.entries()) { + + this.trace.forEachEntry((entry, originalIndex) => { + const timestampType = entry.getTimestamp().getType(); + const entryProto = entry.getValue() as any; + const realToElapsedTimeOffsetNs = entryProto.realToElapsedTimeOffsetNs; + for (const transactionStateProto of entryProto.transactions) { for (const layerStateProto of transactionStateProto.layerChanges) { entries.push( new UiDataEntry( originalIndex, - this.formatTime(entryProto, timestampType, realToElapsedTimeOffsetNs), + TimeUtils.format(entry.getTimestamp()), Number(entryProto.vsyncId), transactionStateProto.pid.toString(), transactionStateProto.uid.toString(), @@ -242,7 +244,7 @@ export class Presenter { entries.push( new UiDataEntry( originalIndex, - this.formatTime(entryProto, timestampType, realToElapsedTimeOffsetNs), + TimeUtils.format(entry.getTimestamp()), Number(entryProto.vsyncId), transactionStateProto.pid.toString(), transactionStateProto.uid.toString(), @@ -259,7 +261,7 @@ export class Presenter { entries.push( new UiDataEntry( originalIndex, - this.formatTime(entryProto, timestampType, realToElapsedTimeOffsetNs), + TimeUtils.format(entry.getTimestamp()), Number(entryProto.vsyncId), Presenter.VALUE_NA, Presenter.VALUE_NA, @@ -278,7 +280,7 @@ export class Presenter { entries.push( new UiDataEntry( originalIndex, - this.formatTime(entryProto, timestampType, realToElapsedTimeOffsetNs), + TimeUtils.format(entry.getTimestamp()), Number(entryProto.vsyncId), Presenter.VALUE_NA, Presenter.VALUE_NA, @@ -294,7 +296,7 @@ export class Presenter { entries.push( new UiDataEntry( originalIndex, - this.formatTime(entryProto, timestampType, realToElapsedTimeOffsetNs), + TimeUtils.format(entry.getTimestamp()), Number(entryProto.vsyncId), Presenter.VALUE_NA, Presenter.VALUE_NA, @@ -310,7 +312,7 @@ export class Presenter { entries.push( new UiDataEntry( originalIndex, - this.formatTime(entryProto, timestampType, realToElapsedTimeOffsetNs), + TimeUtils.format(entry.getTimestamp()), Number(entryProto.vsyncId), Presenter.VALUE_NA, Presenter.VALUE_NA, @@ -326,7 +328,7 @@ export class Presenter { entries.push( new UiDataEntry( originalIndex, - this.formatTime(entryProto, timestampType, realToElapsedTimeOffsetNs), + TimeUtils.format(entry.getTimestamp()), Number(entryProto.vsyncId), Presenter.VALUE_NA, Presenter.VALUE_NA, @@ -340,26 +342,13 @@ export class Presenter { ) ); } - } + }); + ObjectFormatter.displayDefaults = formattingOptions; return entries; } - private formatTime( - entryProto: any, - timestampType: TimestampType, - realToElapsedTimeOffsetNs: bigint | undefined - ): string { - if (timestampType === TimestampType.REAL && realToElapsedTimeOffsetNs !== undefined) { - return TimeUtils.format( - new RealTimestamp(BigInt(entryProto.elapsedRealtimeNanos) + realToElapsedTimeOffsetNs) - ); - } else { - return TimeUtils.format(new ElapsedTimestamp(BigInt(entryProto.elapsedRealtimeNanos))); - } - } - private getUniqueUiDataEntryValues( entries: UiDataEntry[], getValue: (entry: UiDataEntry) => T diff --git a/tools/winscope/src/viewers/viewer_transactions/presenter_test.ts b/tools/winscope/src/viewers/viewer_transactions/presenter_test.ts index 6cc55d3c2..d02880515 100644 --- a/tools/winscope/src/viewers/viewer_transactions/presenter_test.ts +++ b/tools/winscope/src/viewers/viewer_transactions/presenter_test.ts @@ -13,21 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {Parser} from 'parsers/parser'; + +import {TracesBuilder} from 'test/unit/traces_builder'; +import {TraceBuilder} from 'test/unit/trace_builder'; import {UnitTestUtils} from 'test/unit/utils'; -import {Timestamp, TimestampType} from 'trace/timestamp'; +import {Parser} from 'trace/parser'; +import {RealTimestamp, TimestampType} from 'trace/timestamp'; +import {Trace} from 'trace/trace'; +import {Traces} from 'trace/traces'; +import {TracePosition} from 'trace/trace_position'; import {TraceType} from 'trace/trace_type'; -import {TransactionsTraceEntry} from 'trace/transactions'; import {Presenter} from './presenter'; import {UiData, UiDataEntryType} from './ui_data'; -describe('ViewerTransactionsPresenter', () => { - let parser: Parser; +describe('PresenterTransactions', () => { + let parser: Parser; + let trace: Trace; + let traces: Traces; let presenter: Presenter; - let inputTraceEntryElapsed: TransactionsTraceEntry; - let inputTraceEntriesElapsed: Map; - let inputTraceEntryReal: TransactionsTraceEntry; - let inputTraceEntriesReal: Map; let outputUiData: undefined | UiData; const TOTAL_OUTPUT_ENTRIES = 1504; @@ -36,31 +39,24 @@ describe('ViewerTransactionsPresenter', () => { }); beforeEach(() => { - const elapsedTimestamp = new Timestamp(TimestampType.ELAPSED, 2450981445n); - inputTraceEntryElapsed = parser.getTraceEntry(elapsedTimestamp)!; - inputTraceEntriesElapsed = new Map(); - inputTraceEntriesElapsed.set(TraceType.TRANSACTIONS, [inputTraceEntryElapsed]); - - const realTimestamp = new Timestamp(TimestampType.REAL, 16595075386004995520n); - inputTraceEntryReal = parser.getTraceEntry(realTimestamp)!; - inputTraceEntriesReal = new Map(); - inputTraceEntriesReal.set(TraceType.TRANSACTIONS, [inputTraceEntryReal]); - outputUiData = undefined; - - presenter = new Presenter((data: UiData) => { - outputUiData = data; - }); + setUpTestEnvironment(TimestampType.ELAPSED); }); - it('is robust to undefined trace entry', () => { - inputTraceEntriesElapsed = new Map(); - presenter.notifyCurrentTraceEntries(inputTraceEntriesElapsed); + it('is robust to empty trace', () => { + const traces = new TracesBuilder().setEntries(TraceType.TRANSACTIONS, []).build(); + presenter = new Presenter(traces, (data: UiData) => { + outputUiData = data; + }); + + expect(outputUiData).toEqual(UiData.EMPTY); + + presenter.onTracePositionUpdate(TracePosition.fromTimestamp(new RealTimestamp(10n))); expect(outputUiData).toEqual(UiData.EMPTY); }); - it('processes trace entry and computes output UI data', () => { - presenter.notifyCurrentTraceEntries(inputTraceEntriesElapsed); + it('processes trace position update and computes output UI data', () => { + presenter.onTracePositionUpdate(createTracePosition(0)); expect(outputUiData!.allPids).toEqual([ 'N/A', @@ -90,30 +86,17 @@ describe('ViewerTransactionsPresenter', () => { expect(outputUiData?.currentPropertiesTree).toBeDefined(); }); - it("ignores undefined trace entry and doesn't discard previously computed UI data", () => { - presenter.notifyCurrentTraceEntries(inputTraceEntriesElapsed); - expect(outputUiData!.entries.length).toEqual(TOTAL_OUTPUT_ENTRIES); - - presenter.notifyCurrentTraceEntries(new Map()); - expect(outputUiData!.entries.length).toEqual(TOTAL_OUTPUT_ENTRIES); - }); - - it('processes trace entry and updates current entry and scroll position', () => { - presenter.notifyCurrentTraceEntries(inputTraceEntriesElapsed); + it('processes trace position update and updates current entry and scroll position', () => { + presenter.onTracePositionUpdate(createTracePosition(0)); expect(outputUiData!.currentEntryIndex).toEqual(0); expect(outputUiData!.scrollToIndex).toEqual(0); - ( - inputTraceEntriesElapsed.get(TraceType.TRANSACTIONS)[0] as TransactionsTraceEntry - ).currentEntryIndex = 10; - presenter.notifyCurrentTraceEntries(inputTraceEntriesElapsed); + presenter.onTracePositionUpdate(createTracePosition(10)); expect(outputUiData!.currentEntryIndex).toEqual(13); expect(outputUiData!.scrollToIndex).toEqual(13); }); it('filters entries according to VSYNC ID filter', () => { - presenter.notifyCurrentTraceEntries(inputTraceEntriesElapsed); - presenter.onVSyncIdFilterChanged([]); expect(outputUiData!.entries.length).toEqual(TOTAL_OUTPUT_ENTRIES); @@ -127,8 +110,6 @@ describe('ViewerTransactionsPresenter', () => { }); it('filters entries according to PID filter', () => { - presenter.notifyCurrentTraceEntries(inputTraceEntriesElapsed); - presenter.onPidFilterChanged([]); expect(new Set(outputUiData!.entries.map((entry) => entry.pid))).toEqual( new Set(['N/A', '0', '515', '1593', '2022', '2322', '2463', '3300']) @@ -142,8 +123,6 @@ describe('ViewerTransactionsPresenter', () => { }); it('filters entries according to UID filter', () => { - presenter.notifyCurrentTraceEntries(inputTraceEntriesElapsed); - presenter.onUidFilterChanged([]); expect(new Set(outputUiData!.entries.map((entry) => entry.uid))).toEqual( new Set(['N/A', '1000', '1003', '10169', '10235', '10239']) @@ -159,8 +138,6 @@ describe('ViewerTransactionsPresenter', () => { }); it('filters entries according to type filter', () => { - presenter.notifyCurrentTraceEntries(inputTraceEntriesElapsed); - presenter.onTypeFilterChanged([]); expect(new Set(outputUiData!.entries.map((entry) => entry.type))).toEqual( new Set([ @@ -184,8 +161,6 @@ describe('ViewerTransactionsPresenter', () => { }); it('filters entries according to ID filter', () => { - presenter.notifyCurrentTraceEntries(inputTraceEntriesElapsed); - presenter.onIdFilterChanged([]); expect(new Set(outputUiData!.entries.map((entry) => entry.id)).size).toBeGreaterThan(20); @@ -197,7 +172,6 @@ describe('ViewerTransactionsPresenter', () => { }); it('filters entries according to "what" search string', () => { - presenter.notifyCurrentTraceEntries(inputTraceEntriesElapsed); expect(outputUiData!.entries.length).toEqual(TOTAL_OUTPUT_ENTRIES); presenter.onWhatSearchStringChanged(''); @@ -211,7 +185,7 @@ describe('ViewerTransactionsPresenter', () => { }); it('updates selected entry and properties tree when entry is clicked', () => { - presenter.notifyCurrentTraceEntries(inputTraceEntriesElapsed); + presenter.onTracePositionUpdate(createTracePosition(0)); expect(outputUiData!.currentEntryIndex).toEqual(0); expect(outputUiData!.selectedEntryIndex).toBeUndefined(); expect(outputUiData!.scrollToIndex).toEqual(0); @@ -232,21 +206,15 @@ describe('ViewerTransactionsPresenter', () => { }); it('computes current entry index', () => { - presenter.notifyCurrentTraceEntries(inputTraceEntriesElapsed); + presenter.onTracePositionUpdate(createTracePosition(0)); expect(outputUiData!.currentEntryIndex).toEqual(0); - ( - inputTraceEntriesElapsed.get(TraceType.TRANSACTIONS)[0] as TransactionsTraceEntry - ).currentEntryIndex = 10; - presenter.notifyCurrentTraceEntries(inputTraceEntriesElapsed); + presenter.onTracePositionUpdate(createTracePosition(10)); expect(outputUiData!.currentEntryIndex).toEqual(13); }); it('updates current entry index when filters change', () => { - ( - inputTraceEntriesElapsed.get(TraceType.TRANSACTIONS)[0] as TransactionsTraceEntry - ).currentEntryIndex = 10; - presenter.notifyCurrentTraceEntries(inputTraceEntriesElapsed); + presenter.onTracePositionUpdate(createTracePosition(10)); presenter.onPidFilterChanged([]); expect(outputUiData!.currentEntryIndex).toEqual(13); @@ -262,20 +230,27 @@ describe('ViewerTransactionsPresenter', () => { }); it('formats real time', () => { - ( - inputTraceEntriesReal.get(TraceType.TRANSACTIONS)[0] as TransactionsTraceEntry - ).currentEntryIndex = 10; - presenter.notifyCurrentTraceEntries(inputTraceEntriesReal); - + setUpTestEnvironment(TimestampType.REAL); expect(outputUiData!.entries[0].time).toEqual('2022-08-03T06:19:01.051480997'); }); it('formats elapsed time', () => { - ( - inputTraceEntriesElapsed.get(TraceType.TRANSACTIONS)[0] as TransactionsTraceEntry - ).currentEntryIndex = 10; - presenter.notifyCurrentTraceEntries(inputTraceEntriesElapsed); - + setUpTestEnvironment(TimestampType.ELAPSED); expect(outputUiData!.entries[0].time).toEqual('2s450ms981445ns'); }); + + const setUpTestEnvironment = (timestampType: TimestampType) => { + trace = new TraceBuilder().setParser(parser).setTimestampType(timestampType).build(); + + traces = new Traces(); + traces.setTrace(TraceType.TRANSACTIONS, trace); + + presenter = new Presenter(traces, (data: UiData) => { + outputUiData = data; + }); + }; + + const createTracePosition = (entryIndex: number): TracePosition => { + return TracePosition.fromTraceEntry(trace.getEntry(entryIndex)); + }; }); diff --git a/tools/winscope/src/viewers/viewer_transactions/viewer_transactions.ts b/tools/winscope/src/viewers/viewer_transactions/viewer_transactions.ts index abec5dd34..e1c31ac70 100644 --- a/tools/winscope/src/viewers/viewer_transactions/viewer_transactions.ts +++ b/tools/winscope/src/viewers/viewer_transactions/viewer_transactions.ts @@ -13,6 +13,9 @@ * 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'; @@ -20,10 +23,10 @@ import {Presenter} from './presenter'; import {UiData} from './ui_data'; class ViewerTransactions implements Viewer { - constructor() { + constructor(traces: Traces) { this.htmlElement = document.createElement('viewer-transactions'); - this.presenter = new Presenter((data: UiData) => { + this.presenter = new Presenter(traces, (data: UiData) => { (this.htmlElement as any).inputData = data; }); @@ -56,8 +59,8 @@ class ViewerTransactions implements Viewer { }); } - notifyCurrentTraceEntries(entries: Map): void { - this.presenter.notifyCurrentTraceEntries(entries); + onTracePositionUpdate(position: TracePosition) { + this.presenter.onTracePositionUpdate(position); } getViews(): View[] { diff --git a/tools/winscope/src/viewers/viewer_window_manager/presenter.ts b/tools/winscope/src/viewers/viewer_window_manager/presenter.ts index 96bf1cc31..f62a6fa12 100644 --- a/tools/winscope/src/viewers/viewer_window_manager/presenter.ts +++ b/tools/winscope/src/viewers/viewer_window_manager/presenter.ts @@ -13,9 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import {assertDefined} from 'common/assert_utils'; import {PersistentStoreProxy} from 'common/persistent_store_proxy'; import {FilterType, TreeUtils} from 'common/tree_utils'; import {DisplayContent} from 'trace/flickerlib/windows/DisplayContent'; +import {WindowManagerState} from 'trace/flickerlib/windows/WindowManagerState'; +import {Trace} from 'trace/trace'; +import {Traces} from 'trace/traces'; +import {TraceEntryFinder} from 'trace/trace_entry_finder'; +import {TracePosition} from 'trace/trace_position'; import {TraceTreeNode} from 'trace/trace_tree_node'; import {TraceType} from 'trace/trace_type'; import {Rectangle, RectMatrix, RectTransform} from 'viewers/common/rectangle'; @@ -28,7 +35,66 @@ import {UiData} from './ui_data'; type NotifyViewCallbackType = (uiData: UiData) => void; export class Presenter { - constructor(notifyViewCallback: NotifyViewCallbackType, private storage: Storage) { + private readonly trace: Trace; + private readonly notifyViewCallback: NotifyViewCallbackType; + private uiData: UiData; + private hierarchyFilter: FilterType = TreeUtils.makeNodeFilter(''); + private propertiesFilter: FilterType = TreeUtils.makeNodeFilter(''); + private highlightedItems: string[] = []; + private displayIds: number[] = []; + private pinnedItems: HierarchyTreeNode[] = []; + private pinnedIds: string[] = []; + private selectedHierarchyTree: HierarchyTreeNode | null = null; + private previousEntry: TraceTreeNode | null = null; + private entry: TraceTreeNode | null = null; + private hierarchyUserOptions: UserOptions = PersistentStoreProxy.new( + 'WmHierarchyOptions', + { + showDiff: { + name: 'Show diff', + enabled: false, + }, + simplifyNames: { + name: 'Simplify names', + enabled: true, + }, + onlyVisible: { + name: 'Only visible', + enabled: false, + }, + flat: { + name: 'Flat', + enabled: false, + }, + }, + this.storage + ); + private propertiesUserOptions: UserOptions = PersistentStoreProxy.new( + 'WmPropertyOptions', + { + showDiff: { + name: 'Show diff', + enabled: false, + }, + showDefaults: { + name: 'Show defaults', + enabled: false, + tooltip: ` + If checked, shows the value of all properties. + Otherwise, hides all properties whose value is + the default for its data type. + `, + }, + }, + this.storage + ); + + constructor( + traces: Traces, + private storage: Storage, + notifyViewCallback: NotifyViewCallbackType + ) { + this.trace = assertDefined(traces.getTrace(TraceType.WINDOW_MANAGER)); this.notifyViewCallback = notifyViewCallback; this.uiData = new UiData([TraceType.WINDOW_MANAGER]); this.notifyViewCallback(this.uiData); @@ -86,20 +152,22 @@ export class Presenter { this.updateSelectedTreeUiData(); } - notifyCurrentTraceEntries(entries: Map) { + onTracePositionUpdate(position: TracePosition) { this.uiData = new UiData(); this.uiData.hierarchyUserOptions = this.hierarchyUserOptions; this.uiData.propertiesUserOptions = this.propertiesUserOptions; - const wmEntries = entries.get(TraceType.WINDOW_MANAGER); - if (wmEntries) { - [this.entry, this.previousEntry] = wmEntries; - if (this.entry) { - this.uiData.highlightedItems = this.highlightedItems; - this.uiData.rects = this.generateRects(); - this.uiData.displayIds = this.displayIds; - this.uiData.tree = this.generateTree(); - } + const entry = TraceEntryFinder.findCorrespondingEntry(this.trace, position); + const prevEntry = + entry && entry.getIndex() > 0 ? this.trace.getEntry(entry.getIndex() - 1) : undefined; + + this.entry = entry?.getValue() ?? null; + this.previousEntry = prevEntry?.getValue() ?? null; + if (this.entry) { + this.uiData.highlightedItems = this.highlightedItems; + this.uiData.rects = this.generateRects(); + this.uiData.displayIds = this.displayIds; + this.uiData.tree = this.generateTree(); } this.notifyViewCallback(this.uiData); @@ -224,57 +292,4 @@ export class Presenter { const transformedTree = transformer.transform(); return transformedTree; } - - private readonly notifyViewCallback: NotifyViewCallbackType; - private uiData: UiData; - private hierarchyFilter: FilterType = TreeUtils.makeNodeFilter(''); - private propertiesFilter: FilterType = TreeUtils.makeNodeFilter(''); - private highlightedItems: string[] = []; - private displayIds: number[] = []; - private pinnedItems: HierarchyTreeNode[] = []; - private pinnedIds: string[] = []; - private selectedHierarchyTree: HierarchyTreeNode | null = null; - private previousEntry: TraceTreeNode | null = null; - private entry: TraceTreeNode | null = null; - private hierarchyUserOptions: UserOptions = PersistentStoreProxy.new( - 'WmHierarchyOptions', - { - showDiff: { - name: 'Show diff', - enabled: false, - }, - simplifyNames: { - name: 'Simplify names', - enabled: true, - }, - onlyVisible: { - name: 'Only visible', - enabled: false, - }, - flat: { - name: 'Flat', - enabled: false, - }, - }, - this.storage - ); - private propertiesUserOptions: UserOptions = PersistentStoreProxy.new( - 'WmPropertyOptions', - { - showDiff: { - name: 'Show diff', - enabled: false, - }, - showDefaults: { - name: 'Show defaults', - enabled: false, - tooltip: ` - If checked, shows the value of all properties. - Otherwise, hides all properties whose value is - the default for its data type. - `, - }, - }, - this.storage - ); } diff --git a/tools/winscope/src/viewers/viewer_window_manager/presenter_test.ts b/tools/winscope/src/viewers/viewer_window_manager/presenter_test.ts index 9680d2eba..fc87d093a 100644 --- a/tools/winscope/src/viewers/viewer_window_manager/presenter_test.ts +++ b/tools/winscope/src/viewers/viewer_window_manager/presenter_test.ts @@ -13,10 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import {HierarchyTreeBuilder} from 'test/unit/hierarchy_tree_builder'; import {MockStorage} from 'test/unit/mock_storage'; +import {TraceBuilder} from 'test/unit/trace_builder'; import {UnitTestUtils} from 'test/unit/utils'; import {WindowManagerState} from 'trace/flickerlib/common'; +import {RealTimestamp} from 'trace/timestamp'; +import {Trace} from 'trace/trace'; +import {Traces} from 'trace/traces'; +import {TracePosition} from 'trace/trace_position'; import {TraceType} from 'trace/trace_type'; import {VISIBLE_CHIP} from 'viewers/common/chip'; import {HierarchyTreeNode, PropertiesTreeNode} from 'viewers/common/ui_tree_utils'; @@ -25,14 +31,18 @@ import {Presenter} from './presenter'; import {UiData} from './ui_data'; describe('PresenterWindowManager', () => { + let trace: Trace; + let position: TracePosition; let presenter: Presenter; let uiData: UiData; - let entries: Map; let selectedTree: HierarchyTreeNode; beforeAll(async () => { - entries = new Map(); - const entry: WindowManagerState = await UnitTestUtils.getWindowManagerState(); + trace = new TraceBuilder() + .setEntries([await UnitTestUtils.getWindowManagerState()]) + .build(); + + position = TracePosition.fromTraceEntry(trace.getEntry(0)); selectedTree = new HierarchyTreeBuilder() .setName('ScreenDecorOverlayBottom') @@ -45,18 +55,26 @@ describe('PresenterWindowManager', () => { .setIsVisible(true) .setChips([VISIBLE_CHIP]) .build(); - - entries.set(TraceType.WINDOW_MANAGER, [entry, null]); }); beforeEach(async () => { - presenter = new Presenter((newData: UiData) => { - uiData = newData; - }, new MockStorage()); + presenter = createPresenter(trace); }); - it('processes current trace entries', () => { - presenter.notifyCurrentTraceEntries(entries); + it('is robust to empty trace', () => { + const emptyTrace = new TraceBuilder().setEntries([]).build(); + const presenter = createPresenter(emptyTrace); + expect(uiData.hierarchyUserOptions).toBeTruthy(); + expect(uiData.tree).toBeFalsy(); + + const positionWithoutTraceEntry = TracePosition.fromTimestamp(new RealTimestamp(0n)); + presenter.onTracePositionUpdate(positionWithoutTraceEntry); + expect(uiData.hierarchyUserOptions).toBeTruthy(); + expect(uiData.tree).toBeFalsy(); + }); + + it('processes trace position update', () => { + presenter.onTracePositionUpdate(position); const filteredUiDataRectLabels = uiData.rects ?.filter((rect) => rect.isVisible !== undefined) .map((rect) => rect.label); @@ -76,23 +94,15 @@ describe('PresenterWindowManager', () => { expect(Object.keys(uiData.tree!).length > 0).toBeTrue(); }); - it('handles unavailable trace entry', () => { - presenter.notifyCurrentTraceEntries(entries); - expect(Object.keys(uiData.tree!).length > 0).toBeTrue(); - const emptyEntries = new Map(); - presenter.notifyCurrentTraceEntries(emptyEntries); - expect(uiData.tree).toBeFalsy(); - }); - it('creates input data for rects view', () => { - presenter.notifyCurrentTraceEntries(entries); + presenter.onTracePositionUpdate(position); expect(uiData.rects.length).toBeGreaterThan(0); expect(uiData.rects[0].topLeft).toEqual({x: 0, y: 2326}); expect(uiData.rects[0].bottomRight).toEqual({x: 1080, y: 2400}); }); it('updates pinned items', () => { - presenter.notifyCurrentTraceEntries(entries); + presenter.onTracePositionUpdate(position); expect(uiData.pinnedItems).toEqual([]); const pinnedItem = new HierarchyTreeBuilder() @@ -133,7 +143,7 @@ describe('PresenterWindowManager', () => { }, }; - presenter.notifyCurrentTraceEntries(entries); + presenter.onTracePositionUpdate(position); expect(uiData.tree?.children.length).toEqual(1); presenter.updateHierarchyTree(userOptions); expect(uiData.hierarchyUserOptions).toEqual(userOptions); @@ -160,7 +170,7 @@ describe('PresenterWindowManager', () => { enabled: true, }, }; - presenter.notifyCurrentTraceEntries(entries); + presenter.onTracePositionUpdate(position); presenter.updateHierarchyTree(userOptions); expect(uiData.tree?.children.length).toEqual(72); presenter.filterHierarchyTree('ScreenDecor'); @@ -169,7 +179,7 @@ describe('PresenterWindowManager', () => { }); it('sets properties tree and associated ui data', () => { - presenter.notifyCurrentTraceEntries(entries); + presenter.onTracePositionUpdate(position); presenter.newPropertiesTree(selectedTree); // does not check specific tree values as tree transformation method may change expect(uiData.propertiesTree).toBeTruthy(); @@ -193,7 +203,7 @@ describe('PresenterWindowManager', () => { }, }; - presenter.notifyCurrentTraceEntries(entries); + presenter.onTracePositionUpdate(position); presenter.newPropertiesTree(selectedTree); expect(uiData.propertiesTree?.diffType).toBeFalsy(); presenter.updatePropertiesTree(userOptions); @@ -203,7 +213,7 @@ describe('PresenterWindowManager', () => { }); it('filters properties tree', () => { - presenter.notifyCurrentTraceEntries(entries); + presenter.onTracePositionUpdate(position); presenter.newPropertiesTree(selectedTree); let nonTerminalChildren = @@ -220,4 +230,12 @@ describe('PresenterWindowManager', () => { ) ?? []; expect(nonTerminalChildren.length).toEqual(1); }); + + const createPresenter = (trace: Trace): Presenter => { + const traces = new Traces(); + traces.setTrace(TraceType.WINDOW_MANAGER, trace); + return new Presenter(traces, new MockStorage(), (newData: UiData) => { + uiData = newData; + }); + }; }); diff --git a/tools/winscope/src/viewers/viewer_window_manager/viewer_window_manager.ts b/tools/winscope/src/viewers/viewer_window_manager/viewer_window_manager.ts index 9c21ece06..7bfea19ff 100644 --- a/tools/winscope/src/viewers/viewer_window_manager/viewer_window_manager.ts +++ b/tools/winscope/src/viewers/viewer_window_manager/viewer_window_manager.ts @@ -13,7 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {MockStorage} from 'test/unit/mock_storage'; + +import {Traces} from 'trace/traces'; +import {TracePosition} from 'trace/trace_position'; import {TraceType} from 'trace/trace_type'; import {ViewerEvents} from 'viewers/common/viewer_events'; import {View, Viewer, ViewType} from 'viewers/viewer'; @@ -21,15 +23,11 @@ import {Presenter} from './presenter'; import {UiData} from './ui_data'; class ViewerWindowManager implements Viewer { - constructor() { + constructor(traces: Traces, storage: Storage) { this.htmlElement = document.createElement('viewer-window-manager'); - this.presenter = new Presenter((uiData: UiData) => { - // Angular does not deep watch @Input properties. Clearing inputData to null before repopulating - // automatically ensures that the UI will change via the Angular change detection cycle. Without - // resetting, Angular does not auto-detect that inputData has changed. - (this.htmlElement as any).inputData = null; + this.presenter = new Presenter(traces, storage, (uiData: UiData) => { (this.htmlElement as any).inputData = uiData; - }, new MockStorage()); + }); this.htmlElement.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) => this.presenter.updatePinnedItems((event as CustomEvent).detail.pinnedItem) ); @@ -53,8 +51,8 @@ class ViewerWindowManager implements Viewer { ); } - notifyCurrentTraceEntries(entries: Map): void { - this.presenter.notifyCurrentTraceEntries(entries); + onTracePositionUpdate(position: TracePosition) { + this.presenter.onTracePositionUpdate(position); } getViews(): View[] {