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