• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {InMemoryStorage} from 'common/store/in_memory_storage';
20import {parseMap, stringifyMap} from 'common/store/persistent_store_proxy';
21import {Store} from 'common/store/store';
22import {Analytics} from 'logging/analytics';
23import {
24  TracePositionUpdate,
25  WinscopeEvent,
26  WinscopeEventType,
27} from 'messaging/winscope_event';
28import {EmitEvent} from 'messaging/winscope_event_emitter';
29import {Trace, TraceEntry} from 'trace/trace';
30import {Traces} from 'trace/traces';
31import {TraceEntryFinder} from 'trace/trace_entry_finder';
32import {TRACE_INFO} from 'trace/trace_info';
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 {UserOption, UserOptions} from 'viewers/common/user_options';
41import {HierarchyPresenter, SelectedTree} 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> {
52  protected emitWinscopeEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC;
53  protected overridePropertiesTree: PropertyTreeNode | undefined;
54  protected overridePropertiesTreeName: string | undefined;
55  protected rectsPresenter?: RectsPresenter;
56  protected abstract hierarchyPresenter: HierarchyPresenter;
57  protected abstract propertiesPresenter: PropertiesPresenter;
58  protected abstract readonly multiTraceType?: TraceType;
59  private highlightedItem = '';
60
61  constructor(
62    private readonly trace: Trace<HierarchyTreeNode> | undefined,
63    protected readonly traces: Traces,
64    protected readonly storage: Readonly<Store>,
65    private readonly notifyViewCallback: NotifyHierarchyViewCallbackType<UiData>,
66    protected readonly uiData: UiData,
67  ) {
68    uiData.isDarkMode = storage.get('dark-mode') === 'true';
69    this.copyUiDataAndNotifyView();
70  }
71
72  setEmitEvent(callback: EmitEvent) {
73    this.emitWinscopeEvent = callback;
74  }
75
76  addEventListeners(htmlElement: HTMLElement) {
77    htmlElement.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) =>
78      this.onPinnedItemChange((event as CustomEvent).detail.pinnedItem),
79    );
80    htmlElement.addEventListener(
81      ViewerEvents.HighlightedIdChange,
82      async (event) =>
83        await this.onHighlightedIdChange((event as CustomEvent).detail.id),
84    );
85    htmlElement.addEventListener(
86      ViewerEvents.ArrowDownPress,
87      async (event) =>
88        await this.onArrowPress((event as CustomEvent).detail, false),
89    );
90    htmlElement.addEventListener(
91      ViewerEvents.ArrowUpPress,
92      async (event) =>
93        await this.onArrowPress((event as CustomEvent).detail, true),
94    );
95    htmlElement.addEventListener(
96      ViewerEvents.HighlightedPropertyChange,
97      (event) =>
98        this.onHighlightedPropertyChange((event as CustomEvent).detail.id),
99    );
100    htmlElement.addEventListener(
101      ViewerEvents.HierarchyUserOptionsChange,
102      async (event) =>
103        await this.onHierarchyUserOptionsChange(
104          (event as CustomEvent).detail.userOptions,
105        ),
106    );
107    htmlElement.addEventListener(
108      ViewerEvents.HierarchyFilterChange,
109      async (event) => {
110        const detail: TextFilter = (event as CustomEvent).detail;
111        await this.onHierarchyFilterChange(detail);
112      },
113    );
114    htmlElement.addEventListener(
115      ViewerEvents.PropertiesUserOptionsChange,
116      async (event) =>
117        await this.onPropertiesUserOptionsChange(
118          (event as CustomEvent).detail.userOptions,
119        ),
120    );
121    htmlElement.addEventListener(
122      ViewerEvents.PropertiesFilterChange,
123      async (event) => {
124        const detail: TextFilter = (event as CustomEvent).detail;
125        await this.onPropertiesFilterChange(detail);
126      },
127    );
128    htmlElement.addEventListener(
129      ViewerEvents.HighlightedNodeChange,
130      async (event) =>
131        await this.onHighlightedNodeChange((event as CustomEvent).detail.node),
132    );
133    htmlElement.addEventListener(
134      ViewerEvents.RectShowStateChange,
135      async (event) => {
136        await this.onRectShowStateChange(
137          (event as CustomEvent).detail.rectId,
138          (event as CustomEvent).detail.state,
139        );
140      },
141    );
142    htmlElement.addEventListener(
143      ViewerEvents.RectsUserOptionsChange,
144      (event) => {
145        this.onRectsUserOptionsChange(
146          (event as CustomEvent).detail.userOptions,
147        );
148      },
149    );
150    this.addViewerSpecificListeners(htmlElement);
151  }
152
153  onPinnedItemChange(pinnedItem: UiHierarchyTreeNode) {
154    this.hierarchyPresenter.applyPinnedItemChange(pinnedItem);
155    this.uiData.pinnedItems = this.hierarchyPresenter.getPinnedItems();
156    this.copyUiDataAndNotifyView();
157  }
158
159  async onArrowPress(storage: InMemoryStorage, getPrevious: boolean) {
160    const newNode = this.hierarchyPresenter.getAdjacentVisibleNode(
161      storage,
162      getPrevious,
163    );
164    if (newNode) {
165      await this.onHighlightedNodeChange(newNode);
166    }
167  }
168
169  onHighlightedPropertyChange(id: string) {
170    this.propertiesPresenter.applyHighlightedPropertyChange(id);
171    this.uiData.highlightedProperty =
172      this.propertiesPresenter.getHighlightedProperty();
173    this.copyUiDataAndNotifyView();
174  }
175
176  onRectsUserOptionsChange(userOptions: UserOptions) {
177    if (!this.rectsPresenter) {
178      return;
179    }
180    this.rectsPresenter.applyRectsUserOptionsChange(userOptions);
181
182    this.uiData.rectsUserOptions = this.rectsPresenter.getUserOptions();
183    this.uiData.rectsToDraw = this.rectsPresenter.getRectsToDraw();
184    this.uiData.rectIdToShowState = this.rectsPresenter.getRectIdToShowState();
185
186    this.copyUiDataAndNotifyView();
187  }
188
189  async onHierarchyUserOptionsChange(userOptions: UserOptions) {
190    await this.hierarchyPresenter.applyHierarchyUserOptionsChange(userOptions);
191    this.uiData.hierarchyUserOptions = this.hierarchyPresenter.getUserOptions();
192    this.uiData.hierarchyTrees = this.hierarchyPresenter.getAllFormattedTrees();
193    this.uiData.pinnedItems = this.hierarchyPresenter.getPinnedItems();
194    this.copyUiDataAndNotifyView();
195  }
196
197  async onHierarchyFilterChange(textFilter: TextFilter) {
198    await this.hierarchyPresenter.applyHierarchyFilterChange(textFilter);
199    this.uiData.hierarchyTrees = this.hierarchyPresenter.getAllFormattedTrees();
200    this.uiData.pinnedItems = this.hierarchyPresenter.getPinnedItems();
201    this.copyUiDataAndNotifyView();
202  }
203
204  async onPropertiesUserOptionsChange(userOptions: UserOptions) {
205    this.propertiesPresenter.applyPropertiesUserOptionsChange(userOptions);
206    await this.updatePropertiesTree();
207    this.uiData.propertiesUserOptions =
208      this.propertiesPresenter.getUserOptions();
209    this.uiData.propertiesTree = this.propertiesPresenter.getFormattedTree();
210    this.copyUiDataAndNotifyView();
211  }
212
213  async onPropertiesFilterChange(textFilter: TextFilter) {
214    this.propertiesPresenter.applyPropertiesFilterChange(textFilter);
215    await this.updatePropertiesTree();
216    this.uiData.propertiesTree = this.propertiesPresenter.getFormattedTree();
217    this.copyUiDataAndNotifyView();
218  }
219
220  async onRectShowStateChange(id: string, newShowState: RectShowState) {
221    if (!this.rectsPresenter) {
222      return;
223    }
224    this.rectsPresenter.applyRectShowStateChange(id, newShowState);
225
226    this.uiData.rectsToDraw = this.rectsPresenter.getRectsToDraw();
227    this.uiData.rectIdToShowState = this.rectsPresenter.getRectIdToShowState();
228    this.copyUiDataAndNotifyView();
229  }
230
231  async onAppEvent(event: WinscopeEvent) {
232    await event.visit(
233      WinscopeEventType.TRACE_POSITION_UPDATE,
234      async (event) => {
235        if (this.initializeIfNeeded) await this.initializeIfNeeded(event);
236        await this.applyTracePositionUpdate(event);
237        if (this.processDataAfterPositionUpdate) {
238          await this.processDataAfterPositionUpdate(event);
239        }
240        this.refreshUIData();
241      },
242    );
243    await event.visit(
244      WinscopeEventType.FILTER_PRESET_SAVE_REQUEST,
245      async (event) => {
246        this.saveConfigAsPreset(event.name);
247      },
248    );
249    await event.visit(WinscopeEventType.DARK_MODE_TOGGLED, async (event) => {
250      this.uiData.isDarkMode = event.isDarkMode;
251      this.copyUiDataAndNotifyView();
252    });
253    await event.visit(
254      WinscopeEventType.FILTER_PRESET_APPLY_REQUEST,
255      async (event) => {
256        const filterPresetName = event.name;
257        await this.applyPresetConfig(filterPresetName);
258        this.refreshUIData();
259      },
260    );
261    await this.onViewerSpecificWinscopeEvent(event);
262  }
263
264  protected async onViewerSpecificWinscopeEvent(event: WinscopeEvent) {
265    // do nothing
266  }
267
268  protected addViewerSpecificListeners(htmlElement: HTMLElement) {
269    // do nothing;
270  }
271
272  protected saveConfigAsPreset(storeKey: string) {
273    const preset: PresetHierarchy = {
274      hierarchyUserOptions: this.uiData.hierarchyUserOptions,
275      hierarchyFilter: TextFilterValues.fromTextFilter(
276        this.uiData.hierarchyFilter,
277      ),
278      propertiesUserOptions: this.uiData.propertiesUserOptions,
279      propertiesFilter: TextFilterValues.fromTextFilter(
280        this.uiData.propertiesFilter,
281      ),
282      rectsUserOptions: this.uiData.rectsUserOptions,
283      rectIdToShowState: this.uiData.rectIdToShowState,
284    };
285    this.storage.add(storeKey, JSON.stringify(preset, stringifyMap));
286  }
287
288  protected async applyPresetConfig(storeKey: string) {
289    const preset = this.storage.get(storeKey);
290    if (preset) {
291      const parsedPreset: PresetHierarchy = JSON.parse(preset, parseMap);
292      await this.hierarchyPresenter.applyHierarchyUserOptionsChange(
293        parsedPreset.hierarchyUserOptions,
294      );
295      await this.hierarchyPresenter.applyHierarchyFilterChange(
296        new TextFilter(
297          parsedPreset.hierarchyFilter.filterString,
298          parsedPreset.hierarchyFilter.flags,
299        ),
300      );
301
302      this.propertiesPresenter.applyPropertiesUserOptionsChange(
303        parsedPreset.propertiesUserOptions,
304      );
305      this.propertiesPresenter.applyPropertiesFilterChange(
306        new TextFilter(
307          parsedPreset.propertiesFilter.filterString,
308          parsedPreset.propertiesFilter.flags,
309        ),
310      );
311      await this.updatePropertiesTree();
312
313      if (this.rectsPresenter) {
314        this.rectsPresenter?.applyRectsUserOptionsChange(
315          assertDefined(parsedPreset.rectsUserOptions),
316        );
317        this.rectsPresenter?.updateRectShowStates(
318          parsedPreset.rectIdToShowState,
319        );
320      }
321      this.refreshHierarchyViewerUiData();
322    }
323  }
324
325  protected async applyTracePositionUpdate(event: TracePositionUpdate) {
326    const hierarchyStartTime = Date.now();
327
328    let entries: Array<TraceEntry<HierarchyTreeNode>> = [];
329    if (this.multiTraceType !== undefined) {
330      entries = this.traces
331        .getTraces(this.multiTraceType)
332        .map((trace) => {
333          return TraceEntryFinder.findCorrespondingEntry(
334            trace,
335            event.position,
336          ) as TraceEntry<HierarchyTreeNode> | undefined;
337        })
338        .filter((entry) => entry !== undefined) as Array<
339        TraceEntry<HierarchyTreeNode>
340      >;
341    } else {
342      const entry = TraceEntryFinder.findCorrespondingEntry(
343        assertDefined(this.trace),
344        event.position,
345      );
346      if (entry) entries.push(entry);
347    }
348
349    try {
350      await this.hierarchyPresenter.applyTracePositionUpdate(
351        entries,
352        this.highlightedItem,
353      );
354      const showDiff = this.hierarchyPresenter.getUserOptions()['showDiff'];
355      this.logFetchComponentData(hierarchyStartTime, 'hierarchy', showDiff);
356    } catch (e) {
357      this.hierarchyPresenter.clear();
358      this.rectsPresenter?.clear();
359      this.propertiesPresenter.clear();
360      this.refreshHierarchyViewerUiData();
361      throw e;
362    }
363
364    const propertiesOpts = this.propertiesPresenter.getUserOptions();
365    const hasPreviousEntry = entries.some((e) => e.getIndex() > 0);
366    if (propertiesOpts['showDiff']?.isUnavailable !== undefined) {
367      propertiesOpts['showDiff'].isUnavailable = !hasPreviousEntry;
368    }
369
370    const currentHierarchyTrees =
371      this.hierarchyPresenter.getAllCurrentHierarchyTrees();
372    if (currentHierarchyTrees) {
373      const rectStartTime = Date.now();
374      this.rectsPresenter?.applyHierarchyTreesChange(currentHierarchyTrees);
375      this.logFetchComponentData(rectStartTime, 'rects');
376
377      await this.updatePropertiesTree();
378    }
379  }
380
381  protected async applyHighlightedNodeChange(node: UiHierarchyTreeNode) {
382    this.updateHighlightedItem(node.id);
383    this.hierarchyPresenter.applyHighlightedNodeChange(node);
384    await this.updatePropertiesTree();
385  }
386
387  protected async applyHighlightedIdChange(newId: string) {
388    this.updateHighlightedItem(newId);
389    this.hierarchyPresenter.applyHighlightedIdChange(newId);
390    await this.updatePropertiesTree();
391  }
392
393  protected async updatePropertiesTree() {
394    const showDiff = this.propertiesPresenter.getUserOptions()['showDiff'];
395    const propertiesStartTime = Date.now();
396
397    if (this.overridePropertiesTree) {
398      this.propertiesPresenter.setPropertiesTree(this.overridePropertiesTree);
399      await this.propertiesPresenter.formatPropertiesTree(
400        undefined,
401        this.overridePropertiesTreeName,
402        false,
403      );
404      this.logFetchComponentData(propertiesStartTime, 'properties', showDiff);
405      return;
406    }
407    const selected = this.hierarchyPresenter.getSelectedTree();
408    if (selected) {
409      const {trace, tree: selectedTree} = selected;
410      const propertiesTree = await selectedTree.getAllProperties();
411      if (
412        showDiff?.enabled &&
413        !this.hierarchyPresenter.getPreviousHierarchyTreeForTrace(trace)
414      ) {
415        await this.hierarchyPresenter.updatePreviousHierarchyTrees();
416      }
417      const previousTree =
418        this.hierarchyPresenter.getPreviousHierarchyTreeForTrace(trace);
419      this.propertiesPresenter.setPropertiesTree(propertiesTree);
420      await this.propertiesPresenter.formatPropertiesTree(
421        previousTree,
422        this.getOverrideDisplayName(selected),
423        this.keepCalculated(selectedTree),
424        trace.type,
425      );
426      this.logFetchComponentData(propertiesStartTime, 'properties', showDiff);
427    } else {
428      this.propertiesPresenter.clear();
429    }
430  }
431
432  protected updateHighlightedItem(id: string) {
433    if (this.highlightedItem === id) {
434      this.highlightedItem = '';
435    } else {
436      this.highlightedItem = id;
437    }
438  }
439
440  protected refreshHierarchyViewerUiData() {
441    this.uiData.highlightedItem = this.highlightedItem;
442    this.uiData.pinnedItems = this.hierarchyPresenter.getPinnedItems();
443    this.uiData.hierarchyUserOptions = this.hierarchyPresenter.getUserOptions();
444    this.uiData.hierarchyTrees = this.hierarchyPresenter.getAllFormattedTrees();
445    this.uiData.hierarchyFilter = this.hierarchyPresenter.getTextFilter();
446
447    this.uiData.propertiesUserOptions =
448      this.propertiesPresenter.getUserOptions();
449    this.uiData.propertiesTree = this.propertiesPresenter.getFormattedTree();
450    this.uiData.highlightedProperty =
451      this.propertiesPresenter.getHighlightedProperty();
452    this.uiData.propertiesFilter = assertDefined(
453      this.propertiesPresenter.getTextFilter(),
454    );
455
456    if (this.rectsPresenter) {
457      this.uiData.rectsToDraw = this.rectsPresenter?.getRectsToDraw();
458      this.uiData.rectIdToShowState =
459        this.rectsPresenter.getRectIdToShowState();
460      this.uiData.displays = this.rectsPresenter.getDisplays();
461      this.uiData.rectsUserOptions = this.rectsPresenter.getUserOptions();
462    }
463
464    this.copyUiDataAndNotifyView();
465  }
466
467  protected getHighlightedItem(): string | undefined {
468    return this.highlightedItem;
469  }
470
471  protected getEntryFormattedTimestamp(
472    entry: TraceEntry<HierarchyTreeNode>,
473  ): string {
474    if (entry.getFullTrace().isDumpWithoutTimestamp()) {
475      return 'Dump';
476    }
477    return entry.getTimestamp().format();
478  }
479
480  private copyUiDataAndNotifyView() {
481    // Create a shallow copy of the data, otherwise the Angular OnPush change detection strategy
482    // won't detect the new input
483    const copy = Object.assign({}, this.uiData);
484    this.notifyViewCallback(copy);
485  }
486
487  private logFetchComponentData(
488    startTimeMs: number,
489    component: 'hierarchy' | 'properties' | 'rects',
490    showDiffs?: UserOption,
491  ) {
492    const traceName =
493      TRACE_INFO[this.trace?.type ?? assertDefined(this.multiTraceType)].name;
494    Analytics.Navigation.logFetchComponentDataTime(
495      component,
496      traceName,
497      showDiffs !== undefined && showDiffs.enabled && !showDiffs.isUnavailable,
498      Date.now() - startTimeMs,
499    );
500  }
501
502  abstract onHighlightedNodeChange(node: UiHierarchyTreeNode): Promise<void>;
503  abstract onHighlightedIdChange(id: string): Promise<void>;
504  protected abstract keepCalculated(tree: HierarchyTreeNode): boolean;
505  protected abstract getOverrideDisplayName(
506    selected: SelectedTree,
507  ): string | undefined;
508  protected abstract refreshUIData(): void;
509  protected initializeIfNeeded?(event: TracePositionUpdate): Promise<void>;
510  protected processDataAfterPositionUpdate?(
511    event: TracePositionUpdate,
512  ): Promise<void>;
513}
514