xref: /aosp_15_r20/development/tools/winscope/src/viewers/common/abstract_hierarchy_viewer_presenter.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
1/*
2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {assertDefined} from 'common/assert_utils';
18import {FunctionUtils} from 'common/function_utils';
19import {parseMap, stringifyMap} from 'common/persistent_store_proxy';
20import {Store} from 'common/store';
21import {
22  TracePositionUpdate,
23  WinscopeEvent,
24  WinscopeEventType,
25} from 'messaging/winscope_event';
26import {
27  EmitEvent,
28  WinscopeEventEmitter,
29} from 'messaging/winscope_event_emitter';
30import {Trace, TraceEntry} from 'trace/trace';
31import {Traces} from 'trace/traces';
32import {TraceEntryFinder} from 'trace/trace_entry_finder';
33import {TraceType} from 'trace/trace_type';
34import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
35import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
36import {PropertiesPresenter} from 'viewers/common/properties_presenter';
37import {RectsPresenter} from 'viewers/common/rects_presenter';
38import {TextFilter} from 'viewers/common/text_filter';
39import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node';
40import {UserOptions} from 'viewers/common/user_options';
41import {HierarchyPresenter} from './hierarchy_presenter';
42import {PresetHierarchy, TextFilterValues} from './preset_hierarchy';
43import {RectShowState} from './rect_show_state';
44import {UiDataHierarchy} from './ui_data_hierarchy';
45import {ViewerEvents} from './viewer_events';
46
47export type NotifyHierarchyViewCallbackType<UiData> = (uiData: UiData) => void;
48
49export abstract class AbstractHierarchyViewerPresenter<
50  UiData extends UiDataHierarchy,
51> implements WinscopeEventEmitter
52{
53  protected emitWinscopeEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC;
54  protected overridePropertiesTree: PropertyTreeNode | undefined;
55  protected overridePropertiesTreeName: string | undefined;
56  protected rectsPresenter?: RectsPresenter;
57  protected abstract hierarchyPresenter: HierarchyPresenter;
58  protected abstract propertiesPresenter: PropertiesPresenter;
59  protected abstract readonly multiTraceType?: TraceType;
60  private highlightedItem = '';
61
62  constructor(
63    private readonly trace: Trace<HierarchyTreeNode> | undefined,
64    protected readonly traces: Traces,
65    protected readonly storage: Readonly<Store>,
66    private readonly notifyViewCallback: NotifyHierarchyViewCallbackType<UiData>,
67    protected readonly uiData: UiData,
68  ) {
69    uiData.isDarkMode = storage.get('dark-mode') === 'true';
70    this.copyUiDataAndNotifyView();
71  }
72
73  setEmitEvent(callback: EmitEvent) {
74    this.emitWinscopeEvent = callback;
75  }
76
77  addEventListeners(htmlElement: HTMLElement) {
78    htmlElement.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) =>
79      this.onPinnedItemChange((event as CustomEvent).detail.pinnedItem),
80    );
81    htmlElement.addEventListener(
82      ViewerEvents.HighlightedIdChange,
83      async (event) =>
84        await this.onHighlightedIdChange((event as CustomEvent).detail.id),
85    );
86    htmlElement.addEventListener(
87      ViewerEvents.HighlightedPropertyChange,
88      (event) =>
89        this.onHighlightedPropertyChange((event as CustomEvent).detail.id),
90    );
91    htmlElement.addEventListener(
92      ViewerEvents.HierarchyUserOptionsChange,
93      async (event) =>
94        await this.onHierarchyUserOptionsChange(
95          (event as CustomEvent).detail.userOptions,
96        ),
97    );
98    htmlElement.addEventListener(
99      ViewerEvents.HierarchyFilterChange,
100      async (event) => {
101        const detail: TextFilter = (event as CustomEvent).detail;
102        await this.onHierarchyFilterChange(detail);
103      },
104    );
105    htmlElement.addEventListener(
106      ViewerEvents.PropertiesUserOptionsChange,
107      async (event) =>
108        await this.onPropertiesUserOptionsChange(
109          (event as CustomEvent).detail.userOptions,
110        ),
111    );
112    htmlElement.addEventListener(
113      ViewerEvents.PropertiesFilterChange,
114      async (event) => {
115        const detail: TextFilter = (event as CustomEvent).detail;
116        await this.onPropertiesFilterChange(detail);
117      },
118    );
119    htmlElement.addEventListener(
120      ViewerEvents.HighlightedNodeChange,
121      async (event) =>
122        await this.onHighlightedNodeChange((event as CustomEvent).detail.node),
123    );
124    htmlElement.addEventListener(
125      ViewerEvents.RectShowStateChange,
126      async (event) => {
127        await this.onRectShowStateChange(
128          (event as CustomEvent).detail.rectId,
129          (event as CustomEvent).detail.state,
130        );
131      },
132    );
133    htmlElement.addEventListener(
134      ViewerEvents.RectsUserOptionsChange,
135      (event) => {
136        this.onRectsUserOptionsChange(
137          (event as CustomEvent).detail.userOptions,
138        );
139      },
140    );
141  }
142
143  onPinnedItemChange(pinnedItem: UiHierarchyTreeNode) {
144    this.hierarchyPresenter.applyPinnedItemChange(pinnedItem);
145    this.uiData.pinnedItems = this.hierarchyPresenter.getPinnedItems();
146    this.copyUiDataAndNotifyView();
147  }
148
149  onHighlightedPropertyChange(id: string) {
150    this.propertiesPresenter.applyHighlightedPropertyChange(id);
151    this.uiData.highlightedProperty =
152      this.propertiesPresenter.getHighlightedProperty();
153    this.copyUiDataAndNotifyView();
154  }
155
156  onRectsUserOptionsChange(userOptions: UserOptions) {
157    if (!this.rectsPresenter) {
158      return;
159    }
160    this.rectsPresenter.applyRectsUserOptionsChange(userOptions);
161
162    this.uiData.rectsUserOptions = this.rectsPresenter.getUserOptions();
163    this.uiData.rectsToDraw = this.rectsPresenter.getRectsToDraw();
164    this.uiData.rectIdToShowState = this.rectsPresenter.getRectIdToShowState();
165
166    this.copyUiDataAndNotifyView();
167  }
168
169  async onHierarchyUserOptionsChange(userOptions: UserOptions) {
170    await this.hierarchyPresenter.applyHierarchyUserOptionsChange(userOptions);
171    this.uiData.hierarchyUserOptions = this.hierarchyPresenter.getUserOptions();
172    this.uiData.hierarchyTrees = this.hierarchyPresenter.getAllFormattedTrees();
173    this.uiData.pinnedItems = this.hierarchyPresenter.getPinnedItems();
174    this.copyUiDataAndNotifyView();
175  }
176
177  async onHierarchyFilterChange(textFilter: TextFilter) {
178    await this.hierarchyPresenter.applyHierarchyFilterChange(textFilter);
179    this.uiData.hierarchyTrees = this.hierarchyPresenter.getAllFormattedTrees();
180    this.uiData.pinnedItems = this.hierarchyPresenter.getPinnedItems();
181    this.copyUiDataAndNotifyView();
182  }
183
184  async onPropertiesUserOptionsChange(userOptions: UserOptions) {
185    this.propertiesPresenter.applyPropertiesUserOptionsChange(userOptions);
186    await this.updatePropertiesTree();
187    this.uiData.propertiesUserOptions =
188      this.propertiesPresenter.getUserOptions();
189    this.uiData.propertiesTree = this.propertiesPresenter.getFormattedTree();
190    this.copyUiDataAndNotifyView();
191  }
192
193  async onPropertiesFilterChange(textFilter: TextFilter) {
194    this.propertiesPresenter.applyPropertiesFilterChange(textFilter);
195    await this.updatePropertiesTree();
196    this.uiData.propertiesTree = this.propertiesPresenter.getFormattedTree();
197    this.copyUiDataAndNotifyView();
198  }
199
200  async onRectShowStateChange(id: string, newShowState: RectShowState) {
201    if (!this.rectsPresenter) {
202      return;
203    }
204    this.rectsPresenter.applyRectShowStateChange(id, newShowState);
205
206    this.uiData.rectsToDraw = this.rectsPresenter.getRectsToDraw();
207    this.uiData.rectIdToShowState = this.rectsPresenter.getRectIdToShowState();
208    this.copyUiDataAndNotifyView();
209  }
210
211  async onAppEvent(event: WinscopeEvent) {
212    await event.visit(
213      WinscopeEventType.TRACE_POSITION_UPDATE,
214      async (event) => {
215        if (this.initializeIfNeeded) await this.initializeIfNeeded(event);
216        await this.applyTracePositionUpdate(event);
217        if (this.processDataAfterPositionUpdate) {
218          await this.processDataAfterPositionUpdate(event);
219        }
220        this.refreshUIData();
221      },
222    );
223    await event.visit(
224      WinscopeEventType.FILTER_PRESET_SAVE_REQUEST,
225      async (event) => {
226        this.saveConfigAsPreset(event.name);
227      },
228    );
229    await event.visit(WinscopeEventType.DARK_MODE_TOGGLED, async (event) => {
230      this.uiData.isDarkMode = event.isDarkMode;
231      this.copyUiDataAndNotifyView();
232    });
233    await event.visit(
234      WinscopeEventType.FILTER_PRESET_APPLY_REQUEST,
235      async (event) => {
236        const filterPresetName = event.name;
237        await this.applyPresetConfig(filterPresetName);
238        this.refreshUIData();
239      },
240    );
241  }
242
243  protected saveConfigAsPreset(storeKey: string) {
244    const preset: PresetHierarchy = {
245      hierarchyUserOptions: this.uiData.hierarchyUserOptions,
246      hierarchyFilter: TextFilterValues.fromTextFilter(
247        this.uiData.hierarchyFilter,
248      ),
249      propertiesUserOptions: this.uiData.propertiesUserOptions,
250      propertiesFilter: TextFilterValues.fromTextFilter(
251        this.uiData.propertiesFilter,
252      ),
253      rectsUserOptions: this.uiData.rectsUserOptions,
254      rectIdToShowState: this.uiData.rectIdToShowState,
255    };
256    this.storage.add(storeKey, JSON.stringify(preset, stringifyMap));
257  }
258
259  protected async applyPresetConfig(storeKey: string) {
260    const preset = this.storage.get(storeKey);
261    if (preset) {
262      const parsedPreset: PresetHierarchy = JSON.parse(preset, parseMap);
263      await this.hierarchyPresenter.applyHierarchyUserOptionsChange(
264        parsedPreset.hierarchyUserOptions,
265      );
266      await this.hierarchyPresenter.applyHierarchyFilterChange(
267        new TextFilter(
268          parsedPreset.hierarchyFilter.filterString,
269          parsedPreset.hierarchyFilter.flags,
270        ),
271      );
272
273      this.propertiesPresenter.applyPropertiesUserOptionsChange(
274        parsedPreset.propertiesUserOptions,
275      );
276      this.propertiesPresenter.applyPropertiesFilterChange(
277        new TextFilter(
278          parsedPreset.propertiesFilter.filterString,
279          parsedPreset.propertiesFilter.flags,
280        ),
281      );
282      await this.updatePropertiesTree();
283
284      if (this.rectsPresenter) {
285        this.rectsPresenter?.applyRectsUserOptionsChange(
286          assertDefined(parsedPreset.rectsUserOptions),
287        );
288        this.rectsPresenter?.updateRectShowStates(
289          parsedPreset.rectIdToShowState,
290        );
291      }
292      this.refreshHierarchyViewerUiData();
293    }
294  }
295
296  protected async applyTracePositionUpdate(event: TracePositionUpdate) {
297    let entries: Array<TraceEntry<HierarchyTreeNode>> = [];
298    if (this.multiTraceType !== undefined) {
299      entries = this.traces
300        .getTraces(this.multiTraceType)
301        .map((trace) => {
302          return TraceEntryFinder.findCorrespondingEntry(
303            trace,
304            event.position,
305          ) as TraceEntry<HierarchyTreeNode> | undefined;
306        })
307        .filter((entry) => entry !== undefined) as Array<
308        TraceEntry<HierarchyTreeNode>
309      >;
310    } else {
311      const entry = TraceEntryFinder.findCorrespondingEntry(
312        assertDefined(this.trace),
313        event.position,
314      );
315      if (entry) entries.push(entry);
316    }
317
318    try {
319      await this.hierarchyPresenter.applyTracePositionUpdate(
320        entries,
321        this.highlightedItem,
322      );
323    } catch (e) {
324      this.hierarchyPresenter.clear();
325      this.rectsPresenter?.clear();
326      this.propertiesPresenter.clear();
327      this.refreshHierarchyViewerUiData();
328      throw e;
329    }
330
331    const propertiesOpts = this.propertiesPresenter.getUserOptions();
332    const hasPreviousEntry = entries.some((e) => e.getIndex() > 0);
333    if (propertiesOpts['showDiff']?.isUnavailable !== undefined) {
334      propertiesOpts['showDiff'].isUnavailable = !hasPreviousEntry;
335    }
336
337    const currentHierarchyTrees =
338      this.hierarchyPresenter.getAllCurrentHierarchyTrees();
339    if (currentHierarchyTrees) {
340      this.rectsPresenter?.applyHierarchyTreesChange(currentHierarchyTrees);
341      await this.updatePropertiesTree();
342    }
343  }
344
345  protected async applyHighlightedNodeChange(node: UiHierarchyTreeNode) {
346    this.updateHighlightedItem(node.id);
347    this.hierarchyPresenter.applyHighlightedNodeChange(node);
348    await this.updatePropertiesTree();
349  }
350
351  protected async applyHighlightedIdChange(newId: string) {
352    this.updateHighlightedItem(newId);
353    this.hierarchyPresenter.applyHighlightedIdChange(newId);
354    await this.updatePropertiesTree();
355  }
356
357  protected async updatePropertiesTree() {
358    if (this.overridePropertiesTree) {
359      this.propertiesPresenter.setPropertiesTree(this.overridePropertiesTree);
360      await this.propertiesPresenter.formatPropertiesTree(
361        undefined,
362        this.overridePropertiesTreeName,
363        false,
364      );
365      return;
366    }
367    const selected = this.hierarchyPresenter.getSelectedTree();
368    if (selected) {
369      const [trace, selectedTree] = selected;
370      const propertiesTree = await selectedTree.getAllProperties();
371      if (
372        this.propertiesPresenter.getUserOptions()['showDiff']?.enabled &&
373        !this.hierarchyPresenter.getPreviousHierarchyTreeForTrace(trace)
374      ) {
375        await this.hierarchyPresenter.updatePreviousHierarchyTrees();
376      }
377      const previousTree =
378        this.hierarchyPresenter.getPreviousHierarchyTreeForTrace(trace);
379      this.propertiesPresenter.setPropertiesTree(propertiesTree);
380      await this.propertiesPresenter.formatPropertiesTree(
381        previousTree,
382        this.getOverrideDisplayName(selected),
383        this.keepCalculated(selectedTree),
384      );
385    } else {
386      this.propertiesPresenter.clear();
387    }
388  }
389
390  protected updateHighlightedItem(id: string) {
391    if (this.highlightedItem === id) {
392      this.highlightedItem = '';
393    } else {
394      this.highlightedItem = id;
395    }
396  }
397
398  protected refreshHierarchyViewerUiData() {
399    this.uiData.highlightedItem = this.highlightedItem;
400    this.uiData.pinnedItems = this.hierarchyPresenter.getPinnedItems();
401    this.uiData.hierarchyUserOptions = this.hierarchyPresenter.getUserOptions();
402    this.uiData.hierarchyTrees = this.hierarchyPresenter.getAllFormattedTrees();
403    this.uiData.hierarchyFilter = this.hierarchyPresenter.getTextFilter();
404
405    this.uiData.propertiesUserOptions =
406      this.propertiesPresenter.getUserOptions();
407    this.uiData.propertiesTree = this.propertiesPresenter.getFormattedTree();
408    this.uiData.highlightedProperty =
409      this.propertiesPresenter.getHighlightedProperty();
410    this.uiData.propertiesFilter = assertDefined(
411      this.propertiesPresenter.getTextFilter(),
412    );
413
414    if (this.rectsPresenter) {
415      this.uiData.rectsToDraw = this.rectsPresenter?.getRectsToDraw();
416      this.uiData.rectIdToShowState =
417        this.rectsPresenter.getRectIdToShowState();
418      this.uiData.displays = this.rectsPresenter.getDisplays();
419      this.uiData.rectsUserOptions = this.rectsPresenter.getUserOptions();
420    }
421
422    this.copyUiDataAndNotifyView();
423  }
424
425  protected getHighlightedItem(): string | undefined {
426    return this.highlightedItem;
427  }
428
429  protected getEntryFormattedTimestamp(
430    entry: TraceEntry<HierarchyTreeNode>,
431  ): string {
432    if (entry.getFullTrace().isDumpWithoutTimestamp()) {
433      return 'Dump';
434    }
435    return entry.getTimestamp().format();
436  }
437
438  private copyUiDataAndNotifyView() {
439    // Create a shallow copy of the data, otherwise the Angular OnPush change detection strategy
440    // won't detect the new input
441    const copy = Object.assign({}, this.uiData);
442    this.notifyViewCallback(copy);
443  }
444
445  abstract onHighlightedNodeChange(node: UiHierarchyTreeNode): Promise<void>;
446  abstract onHighlightedIdChange(id: string): Promise<void>;
447  protected abstract keepCalculated(tree: HierarchyTreeNode): boolean;
448  protected abstract getOverrideDisplayName(
449    selected: [Trace<HierarchyTreeNode>, HierarchyTreeNode],
450  ): string | undefined;
451  protected abstract refreshUIData(): void;
452  protected initializeIfNeeded?(event: TracePositionUpdate): Promise<void>;
453  protected processDataAfterPositionUpdate?(
454    event: TracePositionUpdate,
455  ): Promise<void>;
456}
457