1/* 2 * Copyright (C) 2022 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import { AllocationLogic } from './Allocation'; 17import { AllocationFunction, ConstructorComparison, ConstructorItem, ConstructorType } from '../model/UiStruct'; 18import { 19 DetachedNessState, 20 EdgeType, 21 FileStruct, 22 HeapEdge, 23 HeapNode, 24 HeapSample, 25 HeapTraceFunctionInfo, 26 NodeType, 27} from '../model/DatabaseStruct'; 28import { HeapNodeToConstructorItem } from '../utils/Utils'; 29 30const BASE_SYSTEM_DISTANCE = 100000000; 31const CAN_BE_QUERIED = 1; 32const DETACHED_DOM_NODE = 2; 33const PAGE_PROJECT = 4; 34 35export class HeapLoader { 36 private fileId: number; 37 private fileStruct: FileStruct; 38 private allocationLogic: AllocationLogic; 39 private nodeMap: Map<number, HeapNode>; 40 private edges: Array<HeapEdge>; 41 private rootNode: HeapNode | undefined; 42 private nodeCount: number; 43 private edgeCount: number; 44 private nodes: Array<HeapNode>; 45 private retainingNodes: Uint32Array; 46 private retainingEdges: Uint32Array; 47 private firstRetainerIndex: Uint32Array; 48 private dominatorTree: Uint32Array; 49 private firstDominatedNodesIdx: Uint32Array; 50 private dominatedNodes: Uint32Array; 51 private allClasses?: Map<string, ConstructorItem>; 52 private diffToOtherFile: Map<number, Map<string, ConstructorComparison>>; 53 54 constructor(fileStruct: FileStruct) { 55 this.fileStruct = fileStruct; 56 this.fileId = fileStruct.id; 57 this.nodeCount = fileStruct.snapshotStruct.nodeCount; 58 this.edgeCount = fileStruct.snapshotStruct.edgeCount; 59 this.nodeMap = fileStruct.snapshotStruct.nodeMap; 60 this.edges = fileStruct.snapshotStruct.edges; 61 this.allocationLogic = new AllocationLogic(this.fileStruct); 62 this.nodes = new Array<HeapNode>(this.nodeCount); 63 this.nodeMap.forEach((value): void => { 64 this.nodes[value.nodeIndex] = value; 65 }); 66 this.nodes.sort((a, b) => a.nodeIndex - b.nodeIndex); 67 this.rootNode = this.nodes[0]; 68 this.retainingNodes = new Uint32Array(this.edgeCount); //每一个值为node index 69 this.retainingEdges = new Uint32Array(this.edgeCount); // 每一个值为edge index 70 this.firstRetainerIndex = new Uint32Array(this.nodeCount + 1); 71 this.dominatorTree = new Uint32Array(this.nodeCount); 72 this.firstDominatedNodesIdx = new Uint32Array(this.nodeCount + 1); 73 this.dominatedNodes = new Uint32Array(this.nodeCount - 1); 74 this.diffToOtherFile = new Map<number, Map<string, ConstructorComparison>>(); 75 this.preprocess(); 76 } 77 78 get allocation(): AllocationLogic { 79 return this.allocationLogic; 80 } 81 82 private preprocess(): void { 83 if (!this.rootNode) { 84 return; 85 } 86 this.buildNodeEdge(); 87 this.buildRetainers(); 88 this.distributeDOMState(); 89 this.calFlags(); 90 this.calDistances(); 91 this.calRetainedSize(); 92 // use to cal class Retained Size 93 this.buildDominatedNode(); 94 this.buildSamples(); 95 } 96 97 /** 98 * set node parents node 99 * node has multi parent because bottom up combine multi node 100 * @param node selected node 101 */ 102 loadAllocationParent(node: AllocationFunction): void { 103 this.allocationLogic.getParent(node); 104 } 105 106 /** 107 * get Bottom Up Function List 108 * @returns bottomUpList 109 */ 110 getAllocationFunctionList(): Array<AllocationFunction> { 111 return this.allocationLogic.getFunctionList(); 112 } 113 114 /** 115 * get full stack for node 116 * @param allocationNodeId node.traceNodeId 117 * @returns stack list 118 */ 119 getAllocationStack(traceNodeId: number): Array<HeapTraceFunctionInfo> { 120 return this.allocationLogic.getNodeStack(traceNodeId); 121 } 122 123 getFunctionNodeIds(id: number): number[] { 124 return this.allocationLogic.getFunctionNodeIds(id); 125 } 126 127 getAllocation(): AllocationLogic { 128 return this.allocationLogic; 129 } 130 131 private buildNodeEdge(): void { 132 for (let node of this.nodes) { 133 for (let i = 0; i < node.edgeCount; i++) { 134 let edgeIndex = node.firstEdgeIndex + i; 135 if (this.edges.length > edgeIndex) { 136 node.addEdge(this.edges[edgeIndex]); 137 } 138 } 139 } 140 } 141 142 private buildRetainers(): void { 143 // Iterate over edges and count how many times each node is retained by other nodes 144 for (let edge of this.edges) { 145 let toNode = this.nodeMap.get(edge.toNodeId); 146 if (toNode) { 147 ++this.firstRetainerIndex[toNode.nodeIndex]; 148 } 149 } 150 // Assign the first retainer slot index for each node 151 let firstUnusedRetainerSlot = 0; 152 for (let node of this.nodes) { 153 let retainCount = this.firstRetainerIndex[node.nodeIndex]; 154 this.firstRetainerIndex[node.nodeIndex] = firstUnusedRetainerSlot; 155 this.retainingNodes[firstUnusedRetainerSlot] = retainCount; 156 firstUnusedRetainerSlot += retainCount; 157 } 158 this.firstRetainerIndex[this.nodeCount] = this.retainingNodes.length; 159 160 // Fill the retainer slots with the retaining nodes and edges 161 for (let node of this.nodes) { 162 for (let edge of node.edges) { 163 let childNode = this.nodeMap.get(edge.toNodeId); 164 if (!childNode) { 165 continue; 166 } 167 if (node.nodeIndex !== this.rootNode?.nodeIndex) { 168 childNode.retainsNodeIdx.push(node.nodeIndex); 169 childNode.retainsEdgeIdx.push(edge.edgeIndex); 170 } 171 172 let firstRetainerSlotIndex = this.firstRetainerIndex[childNode.nodeIndex]; 173 174 let nextUnusedRetainerSlotIndex = firstRetainerSlotIndex + --this.retainingNodes[firstRetainerSlotIndex]; 175 this.retainingNodes[nextUnusedRetainerSlotIndex] = node.nodeIndex; 176 this.retainingEdges[nextUnusedRetainerSlotIndex] = edge.edgeIndex; 177 } 178 } 179 } 180 181 private distributeDOMState(): void { 182 // 1.存储传播的状态。虽然已知节点的状态已经设置, 183 // 但仍然需要经过处理以调整其名称并将其放入各自的队列中。 184 let domState = new DOMState(this.nodeCount); 185 for (let node of this.nodes) { 186 if (node.detachedness === DetachedNessState.UNKNOWN) { 187 continue; 188 } 189 this.processNode(domState, node, node.detachedness); 190 } 191 192 // 2. 如果父节点被Attached,那么子节点也被Attached。 193 while (domState.attached.length > 0) { 194 let nodeId = domState.attached.pop() as number; 195 let node = this.nodeMap.get(nodeId); 196 if (node) { 197 this.iterateFilteredChildren(domState, node, DetachedNessState.ATTACHED); 198 } 199 } 200 201 // 3. 如果父节点为Attached,那么子节点将继承父节点的状态。 202 while (domState.detached.length > 0) { 203 let nodeId = domState.detached.pop() as number; 204 let node = this.nodeMap.get(nodeId); 205 if (node && node.detachedness !== DetachedNessState.ATTACHED) { 206 this.iterateFilteredChildren(domState, node, DetachedNessState.DETACHED); 207 } 208 } 209 } 210 211 private calFlags(): void { 212 this.markQueryableNodes(); 213 this.markPageOwnedNodes(); 214 } 215 216 private buildOrderIdxOrphans( 217 postOrderIdx: number, 218 visited: Uint8Array, 219 nodeIdx2OrderIdx: Uint32Array, 220 orderIdx2NodeIdx: Uint32Array 221 ): void { 222 if (postOrderIdx !== this.nodeCount) { 223 // Remove root from the result (last node in the array) and put it at the bottom of the stack so that it is 224 // visited after all orphan nodes and their subgraphs. 225 --postOrderIdx; 226 for (let i = 0; i < this.nodeCount; i++) { 227 if (visited[i]) { 228 continue; 229 } 230 // Fix it by giving the node a postorder index anyway. 231 nodeIdx2OrderIdx[i] = postOrderIdx; 232 orderIdx2NodeIdx[postOrderIdx++] = i; 233 } 234 nodeIdx2OrderIdx[this.rootNode!.nodeIndex] = postOrderIdx; 235 orderIdx2NodeIdx[postOrderIdx++] = this.rootNode!.nodeIndex; 236 } 237 } 238 239 private buildOrderIdxRootNode(stackNodes: Uint32Array, stackCurrentEdge: Uint32Array, visited: Uint8Array): number { 240 let stack = 0; 241 stackNodes[0] = this.rootNode!.nodeIndex; 242 stackCurrentEdge[0] = this.nodes[this.rootNode!.nodeIndex + 1].firstEdgeIndex; 243 for (let node of this.nodes) { 244 if (visited[node.nodeIndex] || !this.hasOnlyWeakRetainers(node)) { 245 continue; 246 } 247 stackNodes[++stack] = node.nodeIndex; 248 stackCurrentEdge[stack] = node.firstEdgeIndex; 249 visited[node.nodeIndex] = 1; 250 } 251 return stack; 252 } 253 254 private buildOrderIdxInit(): { 255 stackNodes: Uint32Array; 256 stackCurrentEdge: Uint32Array; 257 orderIdx2NodeIdx: Uint32Array; 258 nodeIdx2OrderIdx: Uint32Array; 259 visited: Uint8Array; 260 } { 261 const state = { 262 stackNodes: new Uint32Array(this.nodeCount), 263 stackCurrentEdge: new Uint32Array(this.nodeCount), 264 orderIdx2NodeIdx: new Uint32Array(this.nodeCount), 265 nodeIdx2OrderIdx: new Uint32Array(this.nodeCount), 266 visited: new Uint8Array(this.nodeCount), 267 }; 268 state.stackNodes[0] = this.rootNode!.nodeIndex; 269 state.visited[this.rootNode!.nodeIndex] = 1; 270 return state; 271 } 272 private buildOrderIdxAndDominateTree(): Uint32Array { 273 const params = this.buildOrderIdxInit(); 274 let iteration = 0; 275 let postOrderIdx = 0; 276 let stack = 0; 277 while (true) { 278 ++iteration; 279 while (stack >= 0 && stack < this.nodeCount) { 280 let nodeIndex = params.stackNodes[stack]; 281 let node = this.nodes[nodeIndex]; 282 if (!node) { 283 continue; 284 } 285 let edgeIndex = params.stackCurrentEdge[stack]; 286 let edgeEnd = node.firstEdgeIndex + node.edgeCount; 287 if (edgeIndex < edgeEnd) { 288 params.stackCurrentEdge[stack] += 1; 289 let edge = this.edges[edgeIndex]; 290 if (!this.isEssentialEdge(edge, node.id)) { 291 continue; 292 } 293 let childNode = this.nodeMap.get(edge.toNodeId); 294 if (!childNode || params.visited[childNode!.nodeIndex]) { 295 continue; 296 } 297 //Skip the edges from non-page-object nodes to page-object nodes 298 let childNodeFlag = childNode.flag & PAGE_PROJECT; 299 if (node.id !== this.rootNode!.id && childNodeFlag && !node.flag) { 300 continue; 301 } 302 ++stack; 303 params.stackNodes[stack] = childNode.nodeIndex; 304 params.stackCurrentEdge[stack] = childNode.firstEdgeIndex; 305 params.visited[childNode.nodeIndex] = 1; 306 } else { 307 params.nodeIdx2OrderIdx[node.nodeIndex] = postOrderIdx; 308 params.orderIdx2NodeIdx[postOrderIdx++] = node.nodeIndex; 309 --stack; 310 } 311 } 312 if (postOrderIdx === this.nodeCount || iteration > 1) { 313 break; 314 } 315 316 // Remove root from the result (last node in the array) and put it at the bottom of the stack so that it is 317 // visited after all orphan nodes and their subgraphs. 318 --postOrderIdx; 319 stack = this.buildOrderIdxRootNode(params.stackNodes, params.stackCurrentEdge, params.visited); 320 } 321 // If we already processed all orphan nodes that have only weak retainers and still have some orphans... 322 this.buildOrderIdxOrphans(postOrderIdx, params.visited, params.nodeIdx2OrderIdx, params.orderIdx2NodeIdx); 323 this.buildDominatorTree(params.orderIdx2NodeIdx, params.nodeIdx2OrderIdx); 324 return params.orderIdx2NodeIdx; 325 } 326 327 private affectedNode(affected: Uint8Array, nodeIdx2OrderIdx: Uint32Array): void { 328 // 标记root节点的子节点为affected. 329 for (let edge of this.rootNode!.edges) { 330 if (!this.isEssentialEdge(edge, this.rootNode!.id)) { 331 continue; 332 } 333 let childNode = this.nodeMap.get(edge.toNodeId); 334 if (childNode) { 335 affected[nodeIdx2OrderIdx[childNode.nodeIndex]] = 1; 336 } 337 } 338 } 339 340 private buildDominatorTreeLogic( 341 nodeIdx: number, 342 rootOrderedIdx: number, 343 dominators: Uint32Array, 344 nodeIdx2OrderIdx: Uint32Array 345 ): number { 346 let node = this.nodes[nodeIdx]; 347 let nodeFlag = node.flag & PAGE_PROJECT; 348 let newDominatorIdx = this.nodeCount; 349 let retainerStart = this.firstRetainerIndex[nodeIdx]; 350 let retainerEnd = this.firstRetainerIndex[nodeIdx + 1]; 351 let orphanNode = true; 352 for (let idx = retainerStart; idx < retainerEnd; idx++) { 353 let retainerNodeIdx = this.retainingNodes[idx]; 354 let retainerEdgeIdx = this.retainingEdges[idx]; 355 let node = this.nodes[retainerNodeIdx]; 356 let edge = this.edges[retainerEdgeIdx]; 357 if (!this.isEssentialEdge(edge, node.id)) { 358 continue; 359 } 360 orphanNode = false; 361 let retainerNodeFlag = node.flag & PAGE_PROJECT; 362 if (retainerNodeIdx !== this.rootNode?.nodeIndex && nodeFlag && !retainerNodeFlag) { 363 continue; 364 } 365 let retainerOrderIdx = nodeIdx2OrderIdx[retainerNodeIdx]; 366 if (dominators[retainerOrderIdx] !== this.nodeCount) { 367 if (newDominatorIdx === this.nodeCount) { 368 newDominatorIdx = retainerOrderIdx; 369 } else { 370 while (retainerOrderIdx !== newDominatorIdx) { 371 while (retainerOrderIdx < newDominatorIdx) { 372 retainerOrderIdx = dominators[retainerOrderIdx]; 373 } 374 while (newDominatorIdx < retainerOrderIdx) { 375 newDominatorIdx = dominators[newDominatorIdx]; 376 } 377 } 378 } 379 // If idom has already reached the root, it doesn't make sense 380 // to check other retainers. 381 if (newDominatorIdx === rootOrderedIdx) { 382 break; 383 } 384 } 385 } 386 // Make root dominator of orphans. 387 if (orphanNode) { 388 newDominatorIdx = rootOrderedIdx; 389 } 390 return newDominatorIdx; 391 } 392 // The algorithm is based on the article: 393 // K. Cooper, T. Harvey and K. Kennedy "A Simple, Fast Dominance Algorithm" 394 // Softw. Pract. Exper. 4 (2001), pp. 1-10. 395 private buildDominatorTree(orderIdx2NodeIdx: Uint32Array, nodeIdx2OrderIdx: Uint32Array): void { 396 let rootOrderedIdx = this.nodeCount - 1; 397 let dominators = new Uint32Array(this.nodeCount).fill(this.nodeCount); 398 dominators[rootOrderedIdx] = rootOrderedIdx; 399 400 // The affected array is used to mark entries which dominators 401 // have to be racalculated because of changes in their retainers. 402 let affected = new Uint8Array(this.nodeCount); 403 404 // 标记root节点的子节点为affected. 405 this.affectedNode(affected, nodeIdx2OrderIdx); 406 407 let changed = true; 408 let nodeIdx; 409 while (changed) { 410 changed = false; 411 for (let orderIdx = rootOrderedIdx - 1; orderIdx >= 0; --orderIdx) { 412 // If dominator of the entry has already been set to root, 413 // then it can't propagate unknown further. 414 if (affected[orderIdx] === 0) { 415 continue; 416 } 417 affected[orderIdx] = 0; 418 if (dominators[orderIdx] === rootOrderedIdx) { 419 continue; 420 } 421 nodeIdx = orderIdx2NodeIdx[orderIdx]; 422 const newDominatorIdx = this.buildDominatorTreeLogic(nodeIdx, rootOrderedIdx, dominators, nodeIdx2OrderIdx); 423 424 if (newDominatorIdx !== this.nodeCount && dominators[orderIdx] !== newDominatorIdx) { 425 dominators[orderIdx] = newDominatorIdx; 426 changed = true; 427 nodeIdx = orderIdx2NodeIdx[orderIdx]; 428 let node = this.nodes[nodeIdx]; 429 for (let edge of node.edges) { 430 let childNode = this.nodeMap.get(edge.toNodeId); 431 if (childNode) { 432 affected[nodeIdx2OrderIdx[childNode.nodeIndex]] = 1; 433 } 434 } 435 } 436 } 437 } 438 439 for (let orderIdx = 0; orderIdx < dominators.length; orderIdx++) { 440 nodeIdx = orderIdx2NodeIdx[orderIdx]; 441 this.dominatorTree[nodeIdx] = orderIdx2NodeIdx[dominators[orderIdx]]; 442 } 443 } 444 445 private calDistances(): void { 446 if (!this.rootNode) { 447 return; 448 } 449 let nodesToVisit = new Uint32Array(this.nodeCount); 450 let nodesToVisitLen = 0; 451 // root node's edges distance is 1 452 for (let edge of this.rootNode.edges) { 453 let node = this.nodeMap.get(edge.toNodeId); 454 if (!node) { 455 continue; 456 } 457 if (node.isUserRoot() || node.isDocumentDOMTreesRoot()) { 458 node.distance = 1; 459 nodesToVisit[nodesToVisitLen++] = node.id; 460 } 461 } 462 this.bfs(nodesToVisit, nodesToVisitLen); 463 464 // 如果roo节点下有能访问到的子节点,将root节点的distance设置为100000000,否则为0 465 this.rootNode.distance = nodesToVisitLen > 0 ? BASE_SYSTEM_DISTANCE : 0; 466 // 设置root节点下访问不到的哪些独立节点的distance,从100000000开始计数 467 nodesToVisit[0] = this.rootNode.id; 468 nodesToVisitLen = 1; 469 this.bfs(nodesToVisit, nodesToVisitLen); 470 } 471 472 private calRetainedSize(): void { 473 let orderIdx2NodeIdx = this.buildOrderIdxAndDominateTree(); 474 // Propagate retained sizes for each node excluding root. 475 for (let idx = 0; idx < this.nodeCount - 1; idx++) { 476 let node = this.nodes[orderIdx2NodeIdx[idx]]; 477 let dominatorNode = this.nodes[this.dominatorTree[node.nodeIndex]]; 478 dominatorNode.retainedSize += node.retainedSize; 479 } 480 } 481 482 private buildDominatedNode(): void { 483 //赋值两个数组: 484 //-dominatedNodes是一个连续数组,其中每个节点拥有一个与相应的被支配节点的间隔(可以为空)。 485 //—indexArray 是dominatedNodes中与_nodeIndex位置相同的索引数组。 486 let fromNodeIdx = 0; 487 let toNodeIdx = this.nodeCount; 488 489 if (this.rootNode?.nodeIndex === 0) { 490 fromNodeIdx = 1; 491 } else if (this.rootNode?.nodeIndex === toNodeIdx - 1) { 492 toNodeIdx -= 1; 493 } else { 494 throw new Error('Root node is expected to be either first or last'); 495 } 496 for (let nodeIdx = fromNodeIdx; nodeIdx < toNodeIdx; ++nodeIdx) { 497 ++this.firstDominatedNodesIdx[this.dominatorTree[nodeIdx]]; 498 } 499 // Put in the first slot of each dominatedNodes slice the count of entries 500 // that will be filled. 501 let firstDominatedNodeIdx = 0; 502 for (let idx = 0; idx < this.nodeCount; idx++) { 503 let dominateCount = (this.dominatedNodes[firstDominatedNodeIdx] = this.firstDominatedNodesIdx[idx]); 504 this.firstDominatedNodesIdx[idx] = firstDominatedNodeIdx; 505 firstDominatedNodeIdx += dominateCount; 506 } 507 this.firstDominatedNodesIdx[this.nodeCount] = this.dominatedNodes.length; 508 // Fill up the dominatedNodes array with indexes of dominated nodes. Skip the root (node at 509 // index 0) as it is the only node that dominates itself. 510 for (let nodeIdx = fromNodeIdx; nodeIdx < toNodeIdx; nodeIdx++) { 511 let dominatorIdx = this.dominatorTree[nodeIdx]; 512 let dominatorRefIdx = this.firstDominatedNodesIdx[dominatorIdx]; 513 dominatorRefIdx += --this.dominatedNodes[dominatorRefIdx]; 514 this.dominatedNodes[dominatorRefIdx] = nodeIdx; 515 } 516 } 517 518 private buildSamples(): void { 519 let samples = this.fileStruct.snapshotStruct.samples; 520 if (!samples.length) { 521 return; 522 } 523 for (let node of this.nodes) { 524 if (node.id % 2 === 0) { 525 continue; 526 } 527 let rangeIdx = this.binarySearchNodeInSamples(node.id, samples); 528 if (rangeIdx === samples.length) { 529 continue; 530 } 531 samples[rangeIdx].size += node.selfSize; 532 } 533 } 534 535 getMinAndMaxNodeId(): { minNodeId: number; maxNodeId: number } { 536 return { 537 minNodeId: this.nodes[0].id, 538 maxNodeId: this.nodes[this.nodeCount - 1].id, 539 }; 540 } 541 542 private binarySearchNodeInSamples(nodeId: number, samples: Array<HeapSample>): number { 543 let left = 0; 544 let right = samples.length - 1; 545 546 while (left <= right) { 547 const mid = Math.floor((left + right) / 2); 548 const currentSample = samples[mid]; 549 if (currentSample.lastAssignedId === nodeId) { 550 return left; 551 } else if (currentSample.lastAssignedId < nodeId) { 552 left = mid + 1; 553 } else { 554 right = mid - 1; 555 } 556 } 557 return left; 558 } 559 560 private filterForBpf(node: HeapNode, edge: HeapEdge): boolean { 561 if (node.type === NodeType.HIDDEN) { 562 return edge.nameOrIndex !== 'sloppy_function_map' || node.name !== 'system / NativeContext'; 563 } 564 if (node.type === NodeType.ARRAY) { 565 if (node.name !== '(map descriptors)') { 566 return true; 567 } 568 const index = parseInt(edge.nameOrIndex, 10); 569 return index < 2 || index % 3 !== 1; 570 } 571 return true; 572 } 573 574 /** 575 * 广度优先算法遍历所有能从root节点访问到的节点,设置节点的的distance 576 * @param nodesToVisit 能被访问到的node id 数组 577 * @param nodesToVisitLen 有效的node数量 578 */ 579 private bfs(nodesToVisit: Uint32Array, nodesToVisitLen: number): void { 580 let index = 0; 581 while (index < nodesToVisitLen) { 582 let nodeId = nodesToVisit[index++]; 583 let node = this.nodeMap.get(nodeId); 584 if (!node) { 585 continue; 586 } 587 let distance = node.distance + 1; 588 589 for (let edge of node.edges) { 590 if (edge.type === EdgeType.WEAK || !this.nodeMap.has(edge.toNodeId)) { 591 continue; 592 } 593 let childNode = this.nodeMap.get(edge.toNodeId); 594 // if distance is set,not set again 595 if (!childNode || childNode.distance !== -5 || !this.filterForBpf(node, edge)) { 596 continue; 597 } 598 childNode.distance = distance; 599 nodesToVisit[nodesToVisitLen++] = childNode.id; 600 } 601 } 602 if (nodesToVisitLen > this.nodeCount) { 603 throw new Error(`BFS failed. Nodes to visit ${nodesToVisitLen} is more than nodes count ${this.nodeCount}`); 604 } 605 } 606 607 private processNode(domState: DOMState, node: HeapNode, newState: number): void { 608 if (domState.visited[node.nodeIndex]) { 609 return; 610 } 611 if (node.type !== NodeType.NATIVE) { 612 domState.visited[node.nodeIndex] = 1; 613 return; 614 } 615 616 node.detachedness = newState; 617 618 if (newState === DetachedNessState.ATTACHED) { 619 domState.attached.push(node.id); 620 } else if (newState === DetachedNessState.DETACHED) { 621 node.displayName = `Detached ${node.name}`; 622 // mark detached dom 623 node.flag |= DETACHED_DOM_NODE; 624 domState.detached.push(node.id); 625 } 626 domState.visited[node.nodeIndex] = 1; 627 } 628 629 /** 630 * Iterates children of a node. 631 */ 632 private iterateFilteredChildren(domState: DOMState, node: HeapNode, newState: number): void { 633 for (let edge of node.edges) { 634 if (edge.type === EdgeType.HIDDEN || edge.type === EdgeType.WEAK || edge.type === EdgeType.INVISIBLE) { 635 continue; 636 } 637 let childNode = this.nodeMap.get(edge.toNodeId); 638 if (childNode) { 639 this.processNode(domState, childNode, newState); 640 } 641 } 642 } 643 644 /** 645 * mark the node can reachable from root node 646 */ 647 private markQueryableNodes(): void { 648 let list: HeapNode[] = []; 649 let flag = CAN_BE_QUERIED; 650 for (let edge of this.rootNode!.edges) { 651 let childNode = this.nodeMap.get(edge.toNodeId); 652 if (childNode && childNode.isUserRoot()) { 653 list.push(childNode); 654 } 655 } 656 657 while (list.length) { 658 let node = list.pop() as HeapNode; 659 if (!node || node.flag & flag) { 660 continue; 661 } 662 node.flag |= flag; 663 for (let edge of node.edges) { 664 let childNode = this.nodeMap.get(edge.toNodeId); 665 if ( 666 !childNode || 667 childNode.flag & flag || 668 // @ts-ignore 669 [EdgeType.HIDDEN, EdgeType.INVISIBLE, EdgeType.INTERNAL, EdgeType.WEAK].includes(edge.type) 670 ) { 671 continue; 672 } 673 list.push(childNode); 674 } 675 } 676 } 677 678 private markPageOwnedNodes(): void { 679 let nodesToVisitLen = 0; 680 let nodesToVisit = new Array<HeapNode>(this.nodeCount); 681 let flag = PAGE_PROJECT; 682 if (!this.rootNode) { 683 return; 684 } 685 686 for (let edge of this.rootNode.edges) { 687 let node = this.nodeMap.get(edge.toNodeId); 688 if (!node) { 689 continue; 690 } 691 if (edge.type === EdgeType.ELEMENT) { 692 if (!node.isDocumentDOMTreesRoot()) { 693 continue; 694 } 695 } else if (edge.type !== EdgeType.SHORTCUT) { 696 continue; 697 } 698 nodesToVisit[nodesToVisitLen++] = node; 699 node.flag |= flag; 700 } 701 702 while (nodesToVisitLen) { 703 let node = nodesToVisit[--nodesToVisitLen]; 704 for (let edge of node.edges) { 705 let childNode = this.nodeMap.get(edge.toNodeId); 706 if (!childNode || childNode.flag & flag || edge.type === EdgeType.WEAK) { 707 continue; 708 } 709 nodesToVisit[nodesToVisitLen++] = childNode; 710 childNode.flag |= flag; 711 } 712 } 713 } 714 715 private isEssentialEdge(edge: HeapEdge, nodeId: number): boolean { 716 return edge.type !== EdgeType.WEAK && (edge.type !== EdgeType.SHORTCUT || nodeId === this.rootNode!.id); 717 } 718 719 private hasOnlyWeakRetainers(node: HeapNode): boolean { 720 let retainerStart = this.firstRetainerIndex[node.nodeIndex]; 721 let retainerEnd = this.firstRetainerIndex[node.nodeIndex + 1]; 722 for (let index = retainerStart; index < retainerEnd; index++) { 723 let retainingEdgeIdx = this.retainingEdges[index]; 724 let edge = this.edges[retainingEdgeIdx]; 725 if (edge.type !== EdgeType.WEAK && edge.type !== EdgeType.SHORTCUT) { 726 return false; 727 } 728 } 729 return true; 730 } 731 732 private initDiff(targetClass: ConstructorItem, baseClass?: ConstructorItem): ConstructorComparison { 733 const diff = new ConstructorComparison(); 734 diff.type = ConstructorType.ComparisonType; 735 diff.fileId = this.fileId; 736 diff.targetFileId = targetClass.fileId; 737 diff.nodeName = targetClass.nodeName; 738 targetClass.classChildren.sort((a, b) => a.id - b.id); 739 if (baseClass) { 740 baseClass.classChildren.sort((a, b) => a.id - b.id); 741 } 742 return diff; 743 } 744 745 private calClassDiff(targetClass: ConstructorItem, baseClass?: ConstructorItem): ConstructorComparison | null { 746 let i = 0; 747 let j = 0; 748 let baseLen = baseClass ? baseClass.childCount : 0; 749 let targetLen = targetClass.childCount; 750 const diff = this.initDiff(targetClass, baseClass); 751 // The overlap between the base class and the target class 752 while (i < targetLen && j < baseLen) { 753 let baseNode = baseClass!.classChildren[j]; 754 let targetNode = targetClass.classChildren[i]; 755 if (targetNode.id < baseNode.id) { 756 diff.deletedIdx.push(targetNode.index); 757 diff.removedCount++; 758 diff.removedSize += targetNode.shallowSize; 759 i++; 760 } else if (targetNode.id > baseNode.id) { 761 diff.addedIndx.push(baseNode.index); 762 diff.addedCount++; 763 diff.addedSize += baseNode.shallowSize; 764 j++; 765 } else { 766 i++; 767 j++; 768 } 769 } 770 // base more then target 771 while (i < targetLen) { 772 let targetNode = targetClass!.classChildren[i]; 773 diff.deletedIdx.push(targetNode.index); 774 diff.removedCount++; 775 diff.removedSize += targetNode.shallowSize; 776 i++; 777 } 778 // 779 while (j < baseLen) { 780 let baseNode = baseClass!.classChildren[j]; 781 diff.addedIndx.push(baseNode.index); 782 diff.addedCount++; 783 diff.addedSize += baseNode.shallowSize; 784 j++; 785 } 786 diff.deltaCount = diff.addedCount - diff.removedCount; 787 diff.deltaSize = diff.addedSize - diff.removedSize; 788 if (diff.addedCount === 0 && diff.removedCount === 0) { 789 return null; 790 } 791 diff.childCount = diff.addedCount + diff.removedCount; 792 diff.hasNext = true; 793 return diff; 794 } 795 796 private calClassRetainedSize(hasFiler: boolean, classes: Map<string, ConstructorItem>, filter: Function): void { 797 // cal class retained size 798 let list = [this.rootNode]; 799 const sizes = [-1]; 800 const classesName = []; 801 let seenClassName = new Map<string, boolean>(); 802 803 while (list.length) { 804 let node = list.pop(); 805 if (!node) { 806 continue; 807 } 808 let nodeClassName = node.className(); 809 let seen = Boolean(seenClassName.get(nodeClassName)); 810 let dominatorFromIdx = this.firstDominatedNodesIdx[node.nodeIndex]; 811 let dominatorToIdx = this.firstDominatedNodesIdx[node.nodeIndex + 1]; 812 813 if (!seen && (!hasFiler || filter(node.id)) && (node.selfSize || node.type === NodeType.NATIVE)) { 814 let classItem = classes.get(nodeClassName); 815 if (classItem) { 816 classItem.retainedSize += node.retainedSize; 817 if (dominatorFromIdx !== dominatorToIdx) { 818 seenClassName.set(nodeClassName, true); 819 sizes.push(list.length); 820 classesName.push(nodeClassName); 821 } 822 } 823 } 824 825 for (let idx = dominatorFromIdx; idx < dominatorToIdx; idx++) { 826 let nodeIdx = this.dominatedNodes[idx]; 827 let domNode = this.nodes[nodeIdx]; 828 list.push(domNode); 829 } 830 831 while (sizes[sizes.length - 1] === list.length) { 832 sizes.pop(); 833 nodeClassName = classesName.pop() as string; 834 seenClassName.set(nodeClassName, false); 835 } 836 } 837 if (!hasFiler) { 838 this.allClasses = classes; 839 } 840 } 841 842 private combineNodeWithClassName(classes: Map<string, ConstructorItem>, filter: Function): void { 843 for (let node of this.nodes) { 844 if (!filter(node.id) || (node.selfSize === 0 && node.type !== NodeType.NATIVE)) { 845 continue; 846 } 847 848 if (!classes.has(node.className())) { 849 let classItem = HeapNodeToConstructorItem(node); 850 classItem.fileId = this.fileId; 851 classItem.childCount = 1; 852 classItem.type = ConstructorType.ClassType; 853 classItem.retainedSize = 0; 854 classItem.nodeName = node.className(); 855 classItem.hasNext = true; 856 classes.set(node.className(), classItem); 857 858 let instanceItem = classItem.clone(); 859 instanceItem.type = ConstructorType.InstanceType; 860 instanceItem.id = node.id; 861 instanceItem.index = node.nodeIndex; 862 instanceItem.childCount = node.edgeCount; 863 instanceItem.retainedSize = node.retainedSize; 864 instanceItem.hasNext = instanceItem.childCount > 0; 865 instanceItem.traceNodeId = node.traceNodeId; 866 classItem.classChildren.push(instanceItem); 867 } else { 868 let classItem = classes.get(node.className()); 869 if (!classItem) { 870 continue; 871 } 872 // set min node distance to class distance 873 classItem.distance = Math.min(classItem.distance, node.distance); 874 ++classItem.childCount; 875 classItem.shallowSize += node.selfSize; 876 877 let nodeItem = HeapNodeToConstructorItem(node); 878 nodeItem.fileId = this.fileId; 879 nodeItem.type = ConstructorType.InstanceType; 880 nodeItem.id = node.id; 881 nodeItem.index = node.nodeIndex; 882 nodeItem.childCount = node.edgeCount; 883 nodeItem.hasNext = nodeItem.childCount > 0; 884 885 classItem.classChildren.push(nodeItem); 886 } 887 } 888 } 889 890 public getClassesForSummary(minNodeId?: number, maxNodeId?: number): Map<string, ConstructorItem> { 891 let hasFiler = typeof minNodeId === 'number' && typeof maxNodeId === 'number'; 892 function filter(nodeId: number): boolean { 893 if (hasFiler) { 894 if (hasFiler && nodeId >= minNodeId! && nodeId <= maxNodeId!) { 895 return true; 896 } else { 897 return false; 898 } 899 } else { 900 return true; 901 } 902 } 903 if (!hasFiler && this.allClasses) { 904 return this.allClasses; 905 } 906 let classes = new Map<string, ConstructorItem>(); 907 this.combineNodeWithClassName(classes, filter); 908 this.calClassRetainedSize(hasFiler, classes, filter); 909 return classes; 910 } 911 912 /** 913 * compare base file and target file, calculate delta size and count to target class 914 * @param targetFileId to compare file's id 915 * @param targetFileClasses to compare file's constructor 916 */ 917 public getClassesForComparison( 918 targetFileId: number, 919 targetFileClasses: Map<string, ConstructorItem> 920 ): Map<string, ConstructorComparison> | undefined { 921 // Return the result if it has been obtained before 922 if (this.diffToOtherFile.has(targetFileId)) { 923 return this.diffToOtherFile.get(targetFileId); 924 } 925 // get base file class if not init before 926 if (!this.allClasses) { 927 this.allClasses = this.getClassesForSummary(); 928 } 929 930 //deal target class 931 let diffMap = new Map<string, ConstructorComparison>(); 932 for (let targetClass of targetFileClasses.values()) { 933 let classes = this.allClasses.get(targetClass.nodeName); 934 let different = this.calClassDiff(targetClass, classes); 935 if (different) { 936 diffMap.set(targetClass.nodeName, different); 937 } 938 } 939 940 // deal base class which is not in target 941 for (let classItem of this.allClasses.values()) { 942 if (targetFileClasses.has(classItem.nodeName)) { 943 continue; 944 } 945 let different = this.calClassDiff(classItem); 946 if (different) { 947 diffMap.set(classItem.nodeName, different); 948 } 949 } 950 this.diffToOtherFile.set(targetFileId, diffMap); 951 return diffMap; 952 } 953 954 /** 955 * Summary get Node Children 956 * @param item select Node 957 * @returns child Nodes 958 */ 959 public getNextNode(item: ConstructorItem): Array<ConstructorItem> { 960 if (item.children.length > 0) { 961 return item.children; 962 } 963 // get children from edge 964 let node = this.nodes[item.index]; 965 let childNodes: ConstructorItem[] = []; 966 for (let edge of node.edges) { 967 let childNode = this.nodeMap.get(edge.toNodeId); 968 if (!childNode) { 969 continue; 970 } 971 let instanceItem = HeapNodeToConstructorItem(childNode); 972 instanceItem.childCount = instanceItem.edgeCount = childNode.edgeCount; 973 instanceItem.edgeName = edge.nameOrIndex; 974 instanceItem.hasNext = instanceItem.childCount > 0; 975 instanceItem.traceNodeId = childNode.traceNodeId; 976 instanceItem.type = ConstructorType.FiledType; 977 instanceItem.parent = item; 978 childNodes.push(instanceItem); 979 } 980 981 let clickNode = childNodes[0].parent; 982 // If there are duplicate IDs in the third layer and beyond, they will not be expanded again 983 if (clickNode!.type === ConstructorType.FiledType) { 984 this.findParentsFunc(childNodes, clickNode); 985 } 986 let filterChildNodes: ConstructorItem[] = []; 987 for (let item of childNodes) { 988 if (item.id !== this.rootNode!.id) { 989 filterChildNodes.push(item); 990 } 991 } 992 item.children = filterChildNodes; 993 return filterChildNodes; 994 } 995 996 private findParentsFunc(childNodes: ConstructorItem[], clickNode: ConstructorItem): void { 997 function findParents(clickNode: unknown, parents: unknown): unknown { 998 //@ts-ignore 999 if (!clickNode.parent) { 1000 return parents; 1001 } 1002 // add the parent of the current node to the result array 1003 //@ts-ignore 1004 parents.push(clickNode); 1005 for (let childNode of childNodes) { 1006 //@ts-ignore 1007 for (let heapParent of parents) { 1008 if (heapParent.id === childNode!.id) { 1009 childNode.hasNext = false; 1010 } 1011 } 1012 } //@ts-ignore 1013 return findParents(clickNode.parent, parents); 1014 } 1015 findParents(clickNode, []); 1016 } 1017 1018 private combineRetains(node: HeapNode, item: ConstructorItem, retains: Array<ConstructorItem>): void { 1019 for (let i = 0; i < node.retainsNodeIdx.length; i++) { 1020 let retainsNode = this.nodes[node.retainsNodeIdx[i]]; 1021 let retainEdge = this.edges[node.retainsEdgeIdx[i]]; 1022 1023 if (retainEdge.type === EdgeType.WEAK) { 1024 continue; 1025 } 1026 let retainsItem = HeapNodeToConstructorItem(retainsNode); 1027 retainsItem.edgeName = retainEdge.nameOrIndex; 1028 retainsItem.edgeType = retainEdge.type; 1029 retainsItem.type = ConstructorType.RetainersType; 1030 retainsItem.childCount = retainsNode.retainsNodeIdx.length; 1031 retainsItem.hasNext = retainsNode.retainsNodeIdx.length > 0; 1032 if (item!.type === ConstructorType.RetainersType) { 1033 retainsItem.parent = item; 1034 } 1035 retains.push(retainsItem); 1036 } 1037 } 1038 1039 /** 1040 * get nodes which referenced this node 1041 * @param constructor current node 1042 * @returns reference nodes 1043 */ 1044 public getRetains(item: ConstructorItem): Array<ConstructorItem> { 1045 if (item.retains.length > 0) { 1046 return item.retains; 1047 } 1048 if (item.type === ConstructorType.ClassType) { 1049 return []; 1050 } 1051 let node = this.nodes[item.index]; 1052 let retains: ConstructorItem[] = []; 1053 if (node && node.retainsEdgeIdx.length === node.retainsNodeIdx.length) { 1054 this.combineRetains(node, item, retains); 1055 } 1056 1057 // Because the node with id 1 needs to be deleted, there is only one child and id 1 does not need to expand the symbol 1058 for (let childNode of retains) { 1059 let node = this.nodes[childNode.index]; 1060 if (node && node.retainsEdgeIdx.length === node.retainsNodeIdx.length) { 1061 for (let i = 0; i < node.retainsNodeIdx.length; i++) { 1062 let retainsNode = this.nodes[node.retainsNodeIdx[i]]; 1063 if (node.retainsNodeIdx.length === 1 && retainsNode.id === this.rootNode!.id) { 1064 childNode.hasNext = false; 1065 } 1066 } 1067 } 1068 } 1069 1070 if (retains.length > 0 && retains[0].parent) { 1071 let clickNode = retains[0].parent; 1072 // If there are duplicate IDs in the third layer and beyond, they will not be expanded again 1073 if (clickNode!.type === ConstructorType.RetainersType) { 1074 this.findParentsFunc(retains, clickNode); 1075 } 1076 } 1077 1078 retains.sort(function (a: unknown, b: unknown) { 1079 //@ts-ignore 1080 return a.distance - b.distance; 1081 }); 1082 1083 let filterRetains: ConstructorItem[] = []; 1084 for (let item of retains) { 1085 if (item.id !== this.rootNode!.id) { 1086 filterRetains.push(item); 1087 } 1088 } 1089 return filterRetains; 1090 } 1091 1092 public getNodes(): Array<HeapNode> { 1093 return this.nodes; 1094 } 1095 1096 public clear(): void { 1097 this.allClasses?.clear(); 1098 this.diffToOtherFile.clear(); 1099 this.nodeMap.clear(); 1100 this.edges.length = 0; 1101 this.nodes.length = 0; 1102 } 1103} 1104 1105class DOMState { 1106 visited: Uint8Array; 1107 attached: Array<number>; 1108 detached: Array<number>; 1109 1110 constructor(nodeSize: number) { 1111 this.visited = new Uint8Array(nodeSize); 1112 this.attached = []; 1113 this.detached = []; 1114 } 1115} 1116