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