xref: /aosp_15_r20/development/tools/winscope/src/viewers/common/hierarchy_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 {Trace, TraceEntry} from 'trace/trace';
19import {TraceType} from 'trace/trace_type';
20import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
21import {Operation} from 'trace/tree_node/operations/operation';
22import {
23  PropertySource,
24  PropertyTreeNode,
25} from 'trace/tree_node/property_tree_node';
26import {TreeNode} from 'trace/tree_node/tree_node';
27import {IsModifiedCallbackType} from 'viewers/common/add_diffs';
28import {TextFilter} from 'viewers/common/text_filter';
29import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node';
30import {TreeNodeFilter, UiTreeUtils} from 'viewers/common/ui_tree_utils';
31import {UserOptions} from 'viewers/common/user_options';
32import {SimplifyNamesVc} from 'viewers/viewer_view_capture/operations/simplify_names';
33import {AddDiffsHierarchyTree} from './add_diffs_hierarchy_tree';
34import {AddChips} from './operations/add_chips';
35import {Filter} from './operations/filter';
36import {FlattenChildren} from './operations/flatten_children';
37import {SimplifyNames} from './operations/simplify_names';
38import {PropertiesPresenter} from './properties_presenter';
39import {UiTreeFormatter} from './ui_tree_formatter';
40
41export type GetHierarchyTreeNameType = (
42  entry: TraceEntry<HierarchyTreeNode>,
43  tree: HierarchyTreeNode,
44) => string;
45
46export class HierarchyPresenter {
47  private hierarchyFilter: TreeNodeFilter;
48  private pinnedItems: UiHierarchyTreeNode[] = [];
49  private pinnedIds: string[] = [];
50
51  private previousEntries:
52    | Map<Trace<HierarchyTreeNode>, TraceEntry<HierarchyTreeNode>>
53    | undefined;
54  private previousHierarchyTrees? = new Map<
55    Trace<HierarchyTreeNode>,
56    HierarchyTreeNode
57  >();
58
59  private currentEntries:
60    | Map<Trace<HierarchyTreeNode>, TraceEntry<HierarchyTreeNode>>
61    | undefined;
62  private currentHierarchyTrees? = new Map<
63    Trace<HierarchyTreeNode>,
64    HierarchyTreeNode[]
65  >();
66  private currentHierarchyTreeNames:
67    | Map<Trace<HierarchyTreeNode>, string[]>
68    | undefined;
69  private currentFormattedTrees:
70    | Map<Trace<HierarchyTreeNode>, UiHierarchyTreeNode[]>
71    | undefined;
72  private selectedHierarchyTree:
73    | [Trace<HierarchyTreeNode>, HierarchyTreeNode]
74    | undefined;
75
76  constructor(
77    private userOptions: UserOptions,
78    private textFilter: TextFilter,
79    private denylistProperties: string[],
80    private showHeadings: boolean,
81    private forceSelectFirstNode: boolean,
82    private getHierarchyTreeNameStrategy?: GetHierarchyTreeNameType,
83    private customOperations?: Array<
84      [TraceType, Array<Operation<UiHierarchyTreeNode>>]
85    >,
86  ) {
87    this.hierarchyFilter = UiTreeUtils.makeNodeFilter(
88      textFilter.getFilterPredicate(),
89    );
90  }
91
92  getUserOptions(): UserOptions {
93    return this.userOptions;
94  }
95
96  getCurrentEntryForTrace(
97    trace: Trace<HierarchyTreeNode>,
98  ): TraceEntry<HierarchyTreeNode> | undefined {
99    return this.currentEntries?.get(trace);
100  }
101
102  getCurrentHierarchyTreesForTrace(
103    trace: Trace<HierarchyTreeNode>,
104  ): HierarchyTreeNode[] | undefined {
105    return this.currentHierarchyTrees?.get(trace);
106  }
107
108  getAllCurrentHierarchyTrees():
109    | Array<[Trace<HierarchyTreeNode>, HierarchyTreeNode[]]>
110    | undefined {
111    return this.currentHierarchyTrees
112      ? Array.from(this.currentHierarchyTrees.entries())
113      : undefined;
114  }
115
116  getCurrentHierarchyTreeNames(
117    trace: Trace<HierarchyTreeNode>,
118  ): string[] | undefined {
119    return this.currentHierarchyTreeNames?.get(trace);
120  }
121
122  async addCurrentHierarchyTrees(
123    value: [Trace<HierarchyTreeNode>, HierarchyTreeNode[]],
124    highlightedItem: string | undefined,
125  ) {
126    const [trace, trees] = value;
127    if (!this.currentHierarchyTrees) {
128      this.currentHierarchyTrees = new Map();
129    }
130    const curr = this.currentHierarchyTrees.get(trace);
131    if (curr) {
132      curr.push(...trees);
133    } else {
134      this.currentHierarchyTrees.set(trace, trees);
135    }
136
137    if (!this.currentFormattedTrees) {
138      this.currentFormattedTrees = new Map();
139    }
140    if (!this.currentFormattedTrees.get(trace)) {
141      this.currentFormattedTrees.set(trace, []);
142    }
143
144    for (let i = 0; i < trees.length; i++) {
145      const tree = trees[i];
146      const formattedTree = await this.formatTreeAndUpdatePinnedItems(
147        trace,
148        tree,
149        i,
150      );
151      assertDefined(this.currentFormattedTrees.get(trace)).push(formattedTree);
152    }
153
154    if (!this.selectedHierarchyTree && highlightedItem) {
155      this.applyHighlightedIdChange(highlightedItem);
156    }
157  }
158
159  getPreviousHierarchyTreeForTrace(
160    trace: Trace<HierarchyTreeNode>,
161  ): HierarchyTreeNode | undefined {
162    return this.previousHierarchyTrees?.get(trace);
163  }
164
165  getPinnedItems(): UiHierarchyTreeNode[] {
166    return this.pinnedItems;
167  }
168
169  getAllFormattedTrees(): UiHierarchyTreeNode[] | undefined {
170    if (!this.currentFormattedTrees || this.currentFormattedTrees.size === 0) {
171      return undefined;
172    }
173    return Array.from(this.currentFormattedTrees.values()).flat();
174  }
175
176  getFormattedTreesByTrace(
177    trace: Trace<HierarchyTreeNode>,
178  ): UiHierarchyTreeNode[] | undefined {
179    return this.currentFormattedTrees?.get(trace);
180  }
181
182  getSelectedTree(): [Trace<HierarchyTreeNode>, HierarchyTreeNode] | undefined {
183    return this.selectedHierarchyTree;
184  }
185
186  setSelectedTree(
187    value: [Trace<HierarchyTreeNode>, HierarchyTreeNode] | undefined,
188  ) {
189    this.selectedHierarchyTree = value;
190  }
191
192  async updatePreviousHierarchyTrees() {
193    if (!this.previousEntries) {
194      this.previousHierarchyTrees = undefined;
195      return;
196    }
197    const previousTrees = new Map<
198      Trace<HierarchyTreeNode>,
199      HierarchyTreeNode
200    >();
201    for (const previousEntry of this.previousEntries.values()) {
202      const trace = previousEntry.getFullTrace();
203      const previousTree = await previousEntry.getValue();
204      previousTrees.set(trace, previousTree);
205    }
206    this.previousHierarchyTrees = previousTrees;
207  }
208
209  async applyTracePositionUpdate(
210    entries: Array<TraceEntry<HierarchyTreeNode>>,
211    highlightedItem: string | undefined,
212  ): Promise<void> {
213    const currEntries = new Map<
214      Trace<HierarchyTreeNode>,
215      TraceEntry<HierarchyTreeNode>
216    >();
217    const currTrees = new Map<Trace<HierarchyTreeNode>, HierarchyTreeNode[]>();
218    const prevEntries = new Map<
219      Trace<HierarchyTreeNode>,
220      TraceEntry<HierarchyTreeNode>
221    >();
222
223    for (const entry of entries) {
224      const trace = entry.getFullTrace();
225      currEntries.set(trace, entry);
226
227      const tree = await entry.getValue();
228      currTrees.set(trace, [tree]);
229
230      const entryIndex = entry.getIndex();
231      if (entryIndex > 0) {
232        prevEntries.set(trace, trace.getEntry(entryIndex - 1));
233      }
234    }
235    this.currentEntries = currEntries.size > 0 ? currEntries : undefined;
236    this.currentHierarchyTrees = currTrees.size > 0 ? currTrees : undefined;
237    this.previousEntries = prevEntries.size > 0 ? prevEntries : undefined;
238    this.previousHierarchyTrees =
239      prevEntries.size > 0
240        ? new Map<Trace<HierarchyTreeNode>, HierarchyTreeNode>()
241        : undefined;
242    this.selectedHierarchyTree = undefined;
243
244    const names = new Map<Trace<HierarchyTreeNode>, string[]>();
245    if (this.getHierarchyTreeNameStrategy && entries.length > 0) {
246      entries.forEach((entry) => {
247        const trace = entry.getFullTrace();
248        const trees = this.currentHierarchyTrees?.get(trace);
249        if (trees) {
250          names.set(
251            entry.getFullTrace(),
252            trees.map((tree) =>
253              assertDefined(this.getHierarchyTreeNameStrategy)(entry, tree),
254            ),
255          );
256        }
257      });
258    }
259    this.currentHierarchyTreeNames = names;
260
261    if (this.userOptions['showDiff']?.isUnavailable !== undefined) {
262      this.userOptions['showDiff'].isUnavailable =
263        this.previousEntries === undefined;
264    }
265
266    if (this.currentHierarchyTrees) {
267      this.currentFormattedTrees = assertDefined(
268        await this.formatHierarchyTreesAndUpdatePinnedItems(
269          this.currentHierarchyTrees,
270        ),
271      );
272
273      if (!highlightedItem && this.forceSelectFirstNode) {
274        const firstTrees = Array.from(this.currentHierarchyTrees.entries())[0];
275        this.selectedHierarchyTree = [firstTrees[0], firstTrees[1][0]];
276      } else if (highlightedItem && this.currentFormattedTrees) {
277        this.applyHighlightedIdChange(highlightedItem);
278      }
279    }
280  }
281
282  applyHighlightedIdChange(newId: string) {
283    if (!this.currentHierarchyTrees) {
284      return;
285    }
286    const idMatchFilter = UiTreeUtils.makeIdMatchFilter(newId);
287    for (const [trace, trees] of this.currentHierarchyTrees) {
288      let highlightedNode: HierarchyTreeNode | undefined;
289      trees.find((t) => {
290        const target = t.findDfs(idMatchFilter);
291        if (target) {
292          highlightedNode = target;
293          return true;
294        }
295        return false;
296      });
297      if (highlightedNode) {
298        this.selectedHierarchyTree = [trace, highlightedNode];
299        break;
300      }
301    }
302  }
303
304  applyHighlightedNodeChange(selectedTree: UiHierarchyTreeNode) {
305    if (!this.currentHierarchyTrees) {
306      return;
307    }
308    if (UiTreeUtils.shouldGetProperties(selectedTree)) {
309      const idMatchFilter = UiTreeUtils.makeIdMatchFilter(selectedTree.id);
310      for (const [trace, trees] of this.currentHierarchyTrees) {
311        const hasTree = trees.find((t) => t.findDfs(idMatchFilter));
312        if (hasTree) {
313          this.selectedHierarchyTree = [trace, selectedTree];
314          break;
315        }
316      }
317    }
318  }
319
320  async applyHierarchyUserOptionsChange(userOptions: UserOptions) {
321    this.userOptions = userOptions;
322    if (this.currentHierarchyTrees) {
323      this.currentFormattedTrees =
324        await this.formatHierarchyTreesAndUpdatePinnedItems(
325          this.currentHierarchyTrees,
326        );
327    }
328  }
329
330  async applyHierarchyFilterChange(textFilter: TextFilter) {
331    this.textFilter = textFilter;
332    this.hierarchyFilter = UiTreeUtils.makeNodeFilter(
333      textFilter.getFilterPredicate(),
334    );
335    if (this.currentHierarchyTrees) {
336      this.currentFormattedTrees =
337        await this.formatHierarchyTreesAndUpdatePinnedItems(
338          this.currentHierarchyTrees,
339        );
340    }
341  }
342
343  getTextFilter(): TextFilter {
344    return this.textFilter;
345  }
346
347  applyPinnedItemChange(pinnedItem: UiHierarchyTreeNode) {
348    const pinnedId = pinnedItem.id;
349    if (this.pinnedItems.map((item) => item.id).includes(pinnedId)) {
350      this.pinnedItems = this.pinnedItems.filter(
351        (pinned) => pinned.id !== pinnedId,
352      );
353    } else {
354      // Angular change detection requires new array as input
355      this.pinnedItems = this.pinnedItems.concat([pinnedItem]);
356    }
357    this.updatePinnedIds(pinnedId);
358  }
359
360  clear() {
361    this.previousEntries = undefined;
362    this.previousHierarchyTrees = undefined;
363    this.currentEntries = undefined;
364    this.currentHierarchyTrees = undefined;
365    this.currentHierarchyTreeNames = undefined;
366    this.currentFormattedTrees = undefined;
367    this.selectedHierarchyTree = undefined;
368  }
369
370  private updatePinnedIds(newId: string) {
371    if (this.pinnedIds.includes(newId)) {
372      this.pinnedIds = this.pinnedIds.filter((pinned) => pinned !== newId);
373    } else {
374      this.pinnedIds.push(newId);
375    }
376  }
377
378  private async formatHierarchyTreesAndUpdatePinnedItems(
379    hierarchyTrees: Map<Trace<HierarchyTreeNode>, HierarchyTreeNode[]>,
380  ): Promise<Map<Trace<HierarchyTreeNode>, UiHierarchyTreeNode[]> | undefined> {
381    this.pinnedItems = [];
382    const formattedTrees = new Map<
383      Trace<HierarchyTreeNode>,
384      UiHierarchyTreeNode[]
385    >();
386
387    for (const [trace, trees] of hierarchyTrees.entries()) {
388      const formatted = [];
389      for (let i = 0; i < trees.length; i++) {
390        const tree = trees[i];
391        const formattedTree = await this.formatTreeAndUpdatePinnedItems(
392          trace,
393          tree,
394          i,
395        );
396        formatted.push(formattedTree);
397      }
398      formattedTrees.set(trace, formatted);
399    }
400    return formattedTrees;
401  }
402
403  private async formatTreeAndUpdatePinnedItems(
404    trace: Trace<HierarchyTreeNode>,
405    hierarchyTree: HierarchyTreeNode,
406    hierarchyTreeIndex: number | undefined,
407  ): Promise<UiHierarchyTreeNode> {
408    const formattedTree = await this.formatTree(
409      trace,
410      hierarchyTree,
411      hierarchyTreeIndex,
412    );
413    this.pinnedItems.push(...this.extractPinnedItems(formattedTree));
414    const filteredTree = this.filterTree(formattedTree);
415    return filteredTree;
416  }
417
418  private async formatTree(
419    trace: Trace<HierarchyTreeNode>,
420    hierarchyTree: HierarchyTreeNode,
421    hierarchyTreeIndex: number | undefined,
422  ): Promise<UiHierarchyTreeNode> {
423    const uiTree = UiHierarchyTreeNode.from(hierarchyTree);
424
425    if (!this.showHeadings) {
426      uiTree.forEachNodeDfs((node) => node.setShowHeading(false));
427    }
428    if (hierarchyTreeIndex !== undefined) {
429      const displayName = this.currentHierarchyTreeNames
430        ?.get(trace)
431        ?.at(hierarchyTreeIndex);
432      if (displayName) uiTree.setDisplayName(displayName);
433    }
434
435    const formatter = new UiTreeFormatter<UiHierarchyTreeNode>().setUiTree(
436      uiTree,
437    );
438
439    if (
440      this.userOptions['showDiff']?.enabled &&
441      !this.userOptions['showDiff']?.isUnavailable
442    ) {
443      let prevTree = this.previousHierarchyTrees?.get(trace);
444      if (this.previousHierarchyTrees && !prevTree) {
445        prevTree = await this.previousEntries?.get(trace)?.getValue();
446        if (prevTree) this.previousHierarchyTrees.set(trace, prevTree);
447      }
448      const prevEntryUiTree = prevTree
449        ? UiHierarchyTreeNode.from(prevTree)
450        : undefined;
451      await new AddDiffsHierarchyTree(
452        HierarchyPresenter.isHierarchyTreeModified,
453        this.denylistProperties,
454      ).executeInPlace(uiTree, prevEntryUiTree);
455    }
456
457    if (this.userOptions['flat']?.enabled) {
458      formatter.addOperation(new FlattenChildren());
459    }
460
461    formatter.addOperation(new AddChips());
462
463    if (this.userOptions['simplifyNames']?.enabled) {
464      formatter.addOperation(
465        trace.type === TraceType.VIEW_CAPTURE
466          ? new SimplifyNamesVc()
467          : new SimplifyNames(),
468      );
469    }
470    this.customOperations?.forEach((traceAndOperations) => {
471      const [traceType, operations] = traceAndOperations;
472      if (trace.type === traceType) {
473        operations.forEach((op) => formatter.addOperation(op));
474      }
475    });
476
477    return formatter.format();
478  }
479
480  private extractPinnedItems(tree: UiHierarchyTreeNode): UiHierarchyTreeNode[] {
481    const pinnedNodes = [];
482
483    if (this.pinnedIds.includes(tree.id)) {
484      pinnedNodes.push(tree);
485    }
486
487    for (const child of tree.getAllChildren()) {
488      pinnedNodes.push(...this.extractPinnedItems(child));
489    }
490
491    return pinnedNodes;
492  }
493
494  private filterTree(formattedTree: UiHierarchyTreeNode): UiHierarchyTreeNode {
495    const formatter = new UiTreeFormatter<UiHierarchyTreeNode>().setUiTree(
496      formattedTree,
497    );
498    const predicates = [this.hierarchyFilter];
499    if (this.userOptions['showOnlyVisible']?.enabled) {
500      predicates.push(UiTreeUtils.isVisible);
501    }
502    return formatter.addOperation(new Filter(predicates, true)).format();
503  }
504
505  static isHierarchyTreeModified: IsModifiedCallbackType = async (
506    newTree: TreeNode,
507    oldTree: TreeNode,
508    denylistProperties: string[],
509  ) => {
510    if ((newTree as UiHierarchyTreeNode).isRoot()) return false;
511    const newProperties = await (
512      newTree as UiHierarchyTreeNode
513    ).getAllProperties();
514    const oldProperties = await (
515      oldTree as UiHierarchyTreeNode
516    ).getAllProperties();
517
518    return await HierarchyPresenter.isChildPropertyModified(
519      newProperties,
520      oldProperties,
521      denylistProperties,
522    );
523  };
524
525  private static async isChildPropertyModified(
526    newProperties: PropertyTreeNode,
527    oldProperties: PropertyTreeNode,
528    denylistProperties: string[],
529  ): Promise<boolean> {
530    for (const newProperty of newProperties
531      .getAllChildren()
532      .slice()
533      .sort(HierarchyPresenter.sortChildren)) {
534      if (denylistProperties.includes(newProperty.name)) {
535        continue;
536      }
537      if (newProperty.source === PropertySource.CALCULATED) {
538        continue;
539      }
540
541      const oldProperty = oldProperties.getChildByName(newProperty.name);
542      if (!oldProperty) {
543        return true;
544      }
545
546      if (newProperty.getAllChildren().length === 0) {
547        if (
548          await PropertiesPresenter.isPropertyNodeModified(
549            newProperty,
550            oldProperty,
551            denylistProperties,
552          )
553        ) {
554          return true;
555        }
556      } else {
557        const childrenModified =
558          await HierarchyPresenter.isChildPropertyModified(
559            newProperty,
560            oldProperty,
561            denylistProperties,
562          );
563        if (childrenModified) return true;
564      }
565    }
566    return false;
567  }
568
569  private static sortChildren(
570    a: PropertyTreeNode,
571    b: PropertyTreeNode,
572  ): number {
573    return a.name < b.name ? -1 : 1;
574  }
575}
576