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