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