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.js'; 17import { AllocationFunction, ConstructorComparison, ConstructorItem, ConstructorType } from '../model/UiStruct.js'; 18import { 19 DetachedNessState, 20 EdgeType, 21 FileStruct, 22 HeapEdge, 23 HeapNode, 24 HeapSample, 25 HeapTraceFunctionInfo, 26 NodeType, 27} from '../model/DatabaseStruct.js'; 28import { HeapNodeToConstructorItem } from '../utils/Utils.js'; 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) => { 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() { 79 return this.allocationLogic; 80 } 81 82 private preprocess() { 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) { 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) { 124 return this.allocationLogic.getFunctionNodeIds(id); 125 } 126 127 getAllocation() { 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 buildOrderIdxAndDominateTree(): Uint32Array { 217 let stackNodes = new Uint32Array(this.nodeCount); // node index 218 let stackCurrentEdge = new Uint32Array(this.nodeCount); // edge index 219 let orderIdx2NodeIdx = new Uint32Array(this.nodeCount); 220 let nodeIdx2OrderIdx = new Uint32Array(this.nodeCount); 221 let visited = new Uint8Array(this.nodeCount); 222 let postOrderIdx = 0; 223 let stack = 0; 224 stackNodes[0] = this.rootNode!.nodeIndex; 225 visited[this.rootNode!.nodeIndex] = 1; 226 227 let iteration = 0; 228 while (true) { 229 ++iteration; 230 while (stack >= 0 && stack < this.nodeCount) { 231 let nodeIndex = stackNodes[stack]; 232 let node = this.nodes[nodeIndex]; 233 if (!node) { 234 continue; 235 } 236 let edgeIndex = stackCurrentEdge[stack]; 237 let edgeEnd = node.firstEdgeIndex + node.edgeCount; 238 if (edgeIndex < edgeEnd) { 239 stackCurrentEdge[stack] += 1; 240 let edge = this.edges[edgeIndex]; 241 if (!this.isEssentialEdge(edge, node.id)) { 242 continue; 243 } 244 let childNode = this.nodeMap.get(edge.toNodeId); 245 if (!childNode || visited[childNode!.nodeIndex]) { 246 continue; 247 } 248 //Skip the edges from non-page-object nodes to page-object nodes 249 let childNodeFlag = childNode.flag & PAGE_PROJECT; 250 if (node.id != this.rootNode!.id && childNodeFlag && !node.flag) { 251 continue; 252 } 253 ++stack; 254 stackNodes[stack] = childNode.nodeIndex; 255 stackCurrentEdge[stack] = childNode.firstEdgeIndex; 256 visited[childNode.nodeIndex] = 1; 257 } else { 258 nodeIdx2OrderIdx[node.nodeIndex] = postOrderIdx; 259 orderIdx2NodeIdx[postOrderIdx++] = node.nodeIndex; 260 --stack; 261 } 262 } 263 if (postOrderIdx == this.nodeCount || iteration > 1) { 264 break; 265 } 266 267 // Remove root from the result (last node in the array) and put it at the bottom of the stack so that it is 268 // visited after all orphan nodes and their subgraphs. 269 --postOrderIdx; 270 stack = 0; 271 stackNodes[0] = this.rootNode!.nodeIndex; 272 stackCurrentEdge[0] = this.nodes[this.rootNode!.nodeIndex + 1].firstEdgeIndex; 273 for (let node of this.nodes) { 274 if (visited[node.nodeIndex] || !this.hasOnlyWeakRetainers(node)) { 275 continue; 276 } 277 stackNodes[++stack] = node.nodeIndex; 278 stackCurrentEdge[stack] = node.firstEdgeIndex; 279 visited[node.nodeIndex] = 1; 280 } 281 } 282 // If we already processed all orphan nodes that have only weak retainers and still have some orphans... 283 if (postOrderIdx !== this.nodeCount) { 284 // Remove root from the result (last node in the array) and put it at the bottom of the stack so that it is 285 // visited after all orphan nodes and their subgraphs. 286 --postOrderIdx; 287 for (let i = 0; i < this.nodeCount; i++) { 288 if (visited[i]) { 289 continue; 290 } 291 // Fix it by giving the node a postorder index anyway. 292 nodeIdx2OrderIdx[i] = postOrderIdx; 293 orderIdx2NodeIdx[postOrderIdx++] = i; 294 } 295 nodeIdx2OrderIdx[this.rootNode!.nodeIndex] = postOrderIdx; 296 orderIdx2NodeIdx[postOrderIdx++] = this.rootNode!.nodeIndex; 297 } 298 this.buildDominatorTree(orderIdx2NodeIdx, nodeIdx2OrderIdx); 299 return orderIdx2NodeIdx; 300 } 301 302 // The algorithm is based on the article: 303 // K. Cooper, T. Harvey and K. Kennedy "A Simple, Fast Dominance Algorithm" 304 // Softw. Pract. Exper. 4 (2001), pp. 1-10. 305 private buildDominatorTree(orderIdx2NodeIdx: Uint32Array, nodeIdx2OrderIdx: Uint32Array): void { 306 let rootOrderedIdx = this.nodeCount - 1; 307 let dominators = new Uint32Array(this.nodeCount).fill(this.nodeCount); 308 dominators[rootOrderedIdx] = rootOrderedIdx; 309 310 // The affected array is used to mark entries which dominators 311 // have to be racalculated because of changes in their retainers. 312 let affected = new Uint8Array(this.nodeCount); 313 314 // 标记root节点的子节点为affected. 315 for (let edge of this.rootNode!.edges) { 316 if (!this.isEssentialEdge(edge, this.rootNode!.id)) { 317 continue; 318 } 319 let childNode = this.nodeMap.get(edge.toNodeId); 320 if (childNode) { 321 affected[nodeIdx2OrderIdx[childNode.nodeIndex]] = 1; 322 } 323 } 324 325 let changed = true; 326 let nodeIdx; 327 while (changed) { 328 changed = false; 329 for (let orderIdx = rootOrderedIdx - 1; orderIdx >= 0; --orderIdx) { 330 // If dominator of the entry has already been set to root, 331 // then it can't propagate any further. 332 if (affected[orderIdx] === 0) { 333 continue; 334 } 335 affected[orderIdx] = 0; 336 if (dominators[orderIdx] === rootOrderedIdx) { 337 continue; 338 } 339 nodeIdx = orderIdx2NodeIdx[orderIdx]; 340 let node = this.nodes[nodeIdx]; 341 let nodeFlag = node.flag & PAGE_PROJECT; 342 let newDominatorIdx = this.nodeCount; 343 let retainerStart = this.firstRetainerIndex[nodeIdx]; 344 let retainerEnd = this.firstRetainerIndex[nodeIdx + 1]; 345 let orphanNode = true; 346 for (let idx = retainerStart; idx < retainerEnd; idx++) { 347 let retainerNodeIdx = this.retainingNodes[idx]; 348 let retainerEdgeIdx = this.retainingEdges[idx]; 349 let node = this.nodes[retainerNodeIdx]; 350 let edge = this.edges[retainerEdgeIdx]; 351 if (!this.isEssentialEdge(edge, node.id)) { 352 continue; 353 } 354 orphanNode = false; 355 let retainerNodeFlag = node.flag & PAGE_PROJECT; 356 if (retainerNodeIdx !== this.rootNode?.nodeIndex && nodeFlag && !retainerNodeFlag) { 357 continue; 358 } 359 let retainerOrderIdx = nodeIdx2OrderIdx[retainerNodeIdx]; 360 if (dominators[retainerOrderIdx] !== this.nodeCount) { 361 if (newDominatorIdx === this.nodeCount) { 362 newDominatorIdx = retainerOrderIdx; 363 } else { 364 while (retainerOrderIdx !== newDominatorIdx) { 365 while (retainerOrderIdx < newDominatorIdx) { 366 retainerOrderIdx = dominators[retainerOrderIdx]; 367 } 368 while (newDominatorIdx < retainerOrderIdx) { 369 newDominatorIdx = dominators[newDominatorIdx]; 370 } 371 } 372 } 373 // If idom has already reached the root, it doesn't make sense 374 // to check other retainers. 375 if (newDominatorIdx === rootOrderedIdx) { 376 break; 377 } 378 } 379 } 380 // Make root dominator of orphans. 381 if (orphanNode) { 382 newDominatorIdx = rootOrderedIdx; 383 } 384 if (newDominatorIdx !== this.nodeCount && dominators[orderIdx] !== newDominatorIdx) { 385 dominators[orderIdx] = newDominatorIdx; 386 changed = true; 387 nodeIdx = orderIdx2NodeIdx[orderIdx]; 388 let node = this.nodes[nodeIdx]; 389 for (let edge of node.edges) { 390 let childNode = this.nodeMap.get(edge.toNodeId); 391 if (childNode) { 392 affected[nodeIdx2OrderIdx[childNode.nodeIndex]] = 1; 393 } 394 } 395 } 396 } 397 } 398 399 for (let orderIdx = 0; orderIdx < dominators.length; orderIdx++) { 400 nodeIdx = orderIdx2NodeIdx[orderIdx]; 401 this.dominatorTree[nodeIdx] = orderIdx2NodeIdx[dominators[orderIdx]]; 402 } 403 } 404 405 private calDistances(): void { 406 if (!this.rootNode) return; 407 let nodesToVisit = new Uint32Array(this.nodeCount); 408 let nodesToVisitLen = 0; 409 // root node's edges distance is 1 410 for (let edge of this.rootNode.edges) { 411 let node = this.nodeMap.get(edge.toNodeId); 412 if (!node) { 413 continue; 414 } 415 if (node.isUserRoot() || node.isDocumentDOMTreesRoot()) { 416 node.distance = 1; 417 nodesToVisit[nodesToVisitLen++] = node.id; 418 } 419 } 420 this.bfs(nodesToVisit, nodesToVisitLen); 421 422 // 如果roo节点下有能访问到的子节点,将root节点的distance设置为100000000,否则为0 423 this.rootNode.distance = nodesToVisitLen > 0 ? BASE_SYSTEM_DISTANCE : 0; 424 // 设置root节点下访问不到的哪些独立节点的distance,从100000000开始计数 425 nodesToVisit[0] = this.rootNode.id; 426 nodesToVisitLen = 1; 427 this.bfs(nodesToVisit, nodesToVisitLen); 428 } 429 430 private calRetainedSize(): void { 431 let orderIdx2NodeIdx = this.buildOrderIdxAndDominateTree(); 432 // Propagate retained sizes for each node excluding root. 433 for (let idx = 0; idx < this.nodeCount - 1; idx++) { 434 let node = this.nodes[orderIdx2NodeIdx[idx]]; 435 let dominatorNode = this.nodes[this.dominatorTree[node.nodeIndex]]; 436 dominatorNode.retainedSize += node.retainedSize; 437 } 438 } 439 440 private buildDominatedNode() { 441 //赋值两个数组: 442 //-dominatedNodes是一个连续数组,其中每个节点拥有一个与相应的被支配节点的间隔(可以为空)。 443 //—indexArray 是dominatedNodes中与_nodeIndex位置相同的索引数组。 444 let fromNodeIdx = 0; 445 let toNodeIdx = this.nodeCount; 446 447 if (this.rootNode?.nodeIndex === 0) { 448 fromNodeIdx = 1; 449 } else if (this.rootNode?.nodeIndex == toNodeIdx - 1) { 450 toNodeIdx -= 1; 451 } else { 452 throw new Error('Root node is expected to be either first or last'); 453 } 454 for (let nodeIdx = fromNodeIdx; nodeIdx < toNodeIdx; ++nodeIdx) { 455 ++this.firstDominatedNodesIdx[this.dominatorTree[nodeIdx]]; 456 } 457 // Put in the first slot of each dominatedNodes slice the count of entries 458 // that will be filled. 459 let firstDominatedNodeIdx = 0; 460 for (let idx = 0; idx < this.nodeCount; idx++) { 461 let dominateCount = (this.dominatedNodes[firstDominatedNodeIdx] = this.firstDominatedNodesIdx[idx]); 462 this.firstDominatedNodesIdx[idx] = firstDominatedNodeIdx; 463 firstDominatedNodeIdx += dominateCount; 464 } 465 this.firstDominatedNodesIdx[this.nodeCount] = this.dominatedNodes.length; 466 // Fill up the dominatedNodes array with indexes of dominated nodes. Skip the root (node at 467 // index 0) as it is the only node that dominates itself. 468 for (let nodeIdx = fromNodeIdx; nodeIdx < toNodeIdx; nodeIdx++) { 469 let dominatorIdx = this.dominatorTree[nodeIdx]; 470 let dominatorRefIdx = this.firstDominatedNodesIdx[dominatorIdx]; 471 dominatorRefIdx += --this.dominatedNodes[dominatorRefIdx]; 472 this.dominatedNodes[dominatorRefIdx] = nodeIdx; 473 } 474 } 475 476 private buildSamples() { 477 let samples = this.fileStruct.snapshotStruct.samples; 478 if (!samples.length) { 479 return; 480 } 481 for (let node of this.nodes) { 482 if (node.id % 2 === 0) { 483 continue; 484 } 485 let rangeIdx = this.binarySearchNodeInSamples(node.id, samples); 486 if (rangeIdx === samples.length) { 487 continue; 488 } 489 samples[rangeIdx].size += node.selfSize; 490 } 491 } 492 493 getMinAndMaxNodeId() { 494 return { 495 minNodeId: this.nodes[0].id, 496 maxNodeId: this.nodes[this.nodeCount - 1].id, 497 }; 498 } 499 500 private binarySearchNodeInSamples(nodeId: number, samples: Array<HeapSample>): number { 501 let left = 0; 502 let right = samples.length - 1; 503 504 while (left <= right) { 505 const mid = Math.floor((left + right) / 2); 506 const currentSample = samples[mid]; 507 if (currentSample.lastAssignedId === nodeId) { 508 return left; 509 } else if (currentSample.lastAssignedId < nodeId) { 510 left = mid + 1; 511 } else { 512 right = mid - 1; 513 } 514 } 515 return left; 516 } 517 518 private filterForBpf(node: HeapNode, edge: HeapEdge): boolean { 519 if (node.type === NodeType.HIDDEN) { 520 return edge.nameOrIndex !== 'sloppy_function_map' || node.name !== 'system / NativeContext'; 521 } 522 if (node.type === NodeType.ARRAY) { 523 if (node.name !== '(map descriptors)') { 524 return true; 525 } 526 const index = parseInt(edge.nameOrIndex, 10); 527 return index < 2 || index % 3 !== 1; 528 } 529 return true; 530 } 531 532 /** 533 * 广度优先算法遍历所有能从root节点访问到的节点,设置节点的的distance 534 * @param nodesToVisit 能被访问到的node id 数组 535 * @param nodesToVisitLen 有效的node数量 536 */ 537 private bfs(nodesToVisit: Uint32Array, nodesToVisitLen: number) { 538 let index = 0; 539 while (index < nodesToVisitLen) { 540 let nodeId = nodesToVisit[index++]; 541 let node = this.nodeMap.get(nodeId); 542 if (!node) { 543 continue; 544 } 545 let distance = node.distance + 1; 546 547 for (let edge of node.edges) { 548 if (edge.type === EdgeType.WEAK || !this.nodeMap.has(edge.toNodeId)) { 549 continue; 550 } 551 let childNode = this.nodeMap.get(edge.toNodeId); 552 // if distance is set,not set again 553 if (!childNode || childNode.distance != -5 || !this.filterForBpf(node, edge)) { 554 continue; 555 } 556 childNode.distance = distance; 557 nodesToVisit[nodesToVisitLen++] = childNode.id; 558 } 559 } 560 if (nodesToVisitLen > this.nodeCount) { 561 throw new Error('BFS failed. Nodes to visit ' + nodesToVisitLen + ' is more than nodes count ' + this.nodeCount); 562 } 563 } 564 565 private processNode(domState: DOMState, node: HeapNode, newState: number) { 566 if (domState.visited[node.nodeIndex]) { 567 return; 568 } 569 if (node.type !== NodeType.NATIVE) { 570 domState.visited[node.nodeIndex] = 1; 571 return; 572 } 573 574 node.detachedness = newState; 575 576 if (newState === DetachedNessState.ATTACHED) { 577 domState.attached.push(node.id); 578 } else if (newState === DetachedNessState.DETACHED) { 579 node.displayName = 'Detached ' + node.name; 580 // mark detached dom 581 node.flag |= DETACHED_DOM_NODE; 582 domState.detached.push(node.id); 583 } 584 domState.visited[node.nodeIndex] = 1; 585 } 586 587 /** 588 * Iterates children of a node. 589 */ 590 private iterateFilteredChildren(domState: DOMState, node: HeapNode, newState: number): void { 591 for (let edge of node.edges) { 592 if (edge.type === EdgeType.HIDDEN || edge.type === EdgeType.WEAK || edge.type === EdgeType.INVISIBLE) { 593 continue; 594 } 595 let childNode = this.nodeMap.get(edge.toNodeId); 596 if (childNode) { 597 this.processNode(domState, childNode, newState); 598 } 599 } 600 } 601 602 /** 603 * mark the node can reachable from root node 604 */ 605 private markQueryableNodes(): void { 606 let list = new Array<HeapNode>(); 607 let flag = CAN_BE_QUERIED; 608 for (let edge of this.rootNode!.edges) { 609 let childNode = this.nodeMap.get(edge.toNodeId); 610 if (childNode && childNode.isUserRoot()) { 611 list.push(childNode); 612 } 613 } 614 615 while (list.length) { 616 let node = list.pop() as HeapNode; 617 if (!node || node.flag & flag) { 618 continue; 619 } 620 node.flag |= flag; 621 for (let edge of node.edges) { 622 let childNode = this.nodeMap.get(edge.toNodeId); 623 if ( 624 !childNode || 625 childNode.flag & flag || 626 // @ts-ignore 627 [EdgeType.HIDDEN, EdgeType.INVISIBLE, EdgeType.INTERNAL, EdgeType.WEAK].includes(edge.type) 628 ) { 629 continue; 630 } 631 list.push(childNode); 632 } 633 } 634 } 635 636 private markPageOwnedNodes(): void { 637 let nodesToVisitLen = 0; 638 let nodesToVisit = new Array<HeapNode>(this.nodeCount); 639 let flag = PAGE_PROJECT; 640 if (!this.rootNode) { 641 return; 642 } 643 644 for (let edge of this.rootNode.edges) { 645 let node = this.nodeMap.get(edge.toNodeId); 646 if (!node) { 647 continue; 648 } 649 if (edge.type === EdgeType.ELEMENT) { 650 if (!node.isDocumentDOMTreesRoot()) { 651 continue; 652 } 653 } else if (edge.type !== EdgeType.SHORTCUT) { 654 continue; 655 } 656 nodesToVisit[nodesToVisitLen++] = node; 657 node.flag |= flag; 658 } 659 660 while (nodesToVisitLen) { 661 let node = nodesToVisit[--nodesToVisitLen]; 662 for (let edge of node.edges) { 663 let childNode = this.nodeMap.get(edge.toNodeId); 664 if (!childNode || childNode.flag & flag || edge.type === EdgeType.WEAK) { 665 continue; 666 } 667 nodesToVisit[nodesToVisitLen++] = childNode; 668 childNode.flag |= flag; 669 } 670 } 671 } 672 673 private isEssentialEdge(edge: HeapEdge, nodeId: number): boolean { 674 return edge.type !== EdgeType.WEAK && (edge.type !== EdgeType.SHORTCUT || nodeId === this.rootNode!.id); 675 } 676 677 private hasOnlyWeakRetainers(node: HeapNode): boolean { 678 let retainerStart = this.firstRetainerIndex[node.nodeIndex]; 679 let retainerEnd = this.firstRetainerIndex[node.nodeIndex + 1]; 680 for (let index = retainerStart; index < retainerEnd; index++) { 681 let retainingEdgeIdx = this.retainingEdges[index]; 682 let edge = this.edges[retainingEdgeIdx]; 683 if (edge.type !== EdgeType.WEAK && edge.type !== EdgeType.SHORTCUT) { 684 return false; 685 } 686 } 687 return true; 688 } 689 690 private calClassDiff(targetClass: ConstructorItem, baseClass?: ConstructorItem) { 691 let diff = new ConstructorComparison(); 692 diff.type = ConstructorType.ComparisonType; 693 diff.fileId = this.fileId; 694 diff.targetFileId = targetClass.fileId; 695 diff.nodeName = targetClass.nodeName; 696 let i = 0; 697 let j = 0; 698 let baseLen = baseClass ? baseClass.childCount : 0; 699 let targetLen = targetClass.childCount; 700 targetClass.classChildren.sort((a, b) => a.id - b.id); 701 if (baseClass) { 702 baseClass.classChildren.sort((a, b) => a.id - b.id); 703 } 704 // The overlap between the base class and the target class 705 while (i < targetLen && j < baseLen) { 706 let baseNode = baseClass!.classChildren[j]; 707 let targetNode = targetClass.classChildren[i]; 708 if (targetNode.id < baseNode.id) { 709 diff.deletedIdx.push(targetNode.index); 710 diff.removedCount++; 711 diff.removedSize += targetNode.shallowSize; 712 i++; 713 } else if (targetNode.id > baseNode.id) { 714 diff.addedIndx.push(baseNode.index); 715 diff.addedCount++; 716 diff.addedSize += baseNode.shallowSize; 717 j++; 718 } else { 719 i++; 720 j++; 721 } 722 } 723 // base more then target 724 while (i < targetLen) { 725 let targetNode = targetClass!.classChildren[i]; 726 diff.deletedIdx.push(targetNode.index); 727 diff.removedCount++; 728 diff.removedSize += targetNode.shallowSize; 729 i++; 730 } 731 // 732 while (j < baseLen) { 733 let baseNode = baseClass!.classChildren[j]; 734 diff.addedIndx.push(baseNode.index); 735 diff.addedCount++; 736 diff.addedSize += baseNode.shallowSize; 737 j++; 738 } 739 diff.deltaCount = diff.addedCount - diff.removedCount; 740 diff.deltaSize = diff.addedSize - diff.removedSize; 741 if (diff.addedCount == 0 && diff.removedCount == 0) { 742 return null; 743 } 744 diff.childCount = diff.addedCount + diff.removedCount; 745 diff.hasNext = true; 746 return diff; 747 } 748 749 public getClassesForSummary(minNodeId?: number, maxNodeId?: number): Map<string, ConstructorItem> { 750 let hasFiler = typeof minNodeId === 'number' && typeof maxNodeId === 'number'; 751 function filter(nodeId: number): boolean { 752 if (hasFiler) { 753 if (hasFiler && nodeId >= minNodeId! && nodeId <= maxNodeId!) { 754 return true; 755 } else { 756 return false; 757 } 758 } else { 759 return true; 760 } 761 } 762 if (!hasFiler && this.allClasses) { 763 return this.allClasses; 764 } 765 let classes = new Map<string, ConstructorItem>(); 766 // combine node with className 767 for (let node of this.nodes) { 768 if (!filter(node.id) || (node.selfSize === 0 && node.type !== NodeType.NATIVE)) { 769 continue; 770 } 771 772 if (!classes.has(node.className())) { 773 let classItem = HeapNodeToConstructorItem(node); 774 classItem.fileId = this.fileId; 775 classItem.childCount = 1; 776 classItem.type = ConstructorType.ClassType; 777 classItem.retainedSize = 0; 778 classItem.nodeName = node.className(); 779 classItem.hasNext = true; 780 classes.set(node.className(), classItem); 781 782 let instanceItem = classItem.clone(); 783 instanceItem.type = ConstructorType.InstanceType; 784 instanceItem.id = node.id; 785 instanceItem.index = node.nodeIndex; 786 instanceItem.childCount = node.edgeCount; 787 instanceItem.retainedSize = node.retainedSize; 788 instanceItem.hasNext = instanceItem.childCount > 0; 789 instanceItem.traceNodeId = node.traceNodeId; 790 classItem.classChildren.push(instanceItem); 791 } else { 792 let classItem = classes.get(node.className()); 793 if (!classItem) { 794 continue; 795 } 796 // set min node distance to class distance 797 classItem.distance = Math.min(classItem.distance, node.distance); 798 ++classItem.childCount; 799 classItem.shallowSize += node.selfSize; 800 801 let nodeItem = HeapNodeToConstructorItem(node); 802 nodeItem.fileId = this.fileId; 803 nodeItem.type = ConstructorType.InstanceType; 804 nodeItem.id = node.id; 805 nodeItem.index = node.nodeIndex; 806 nodeItem.childCount = node.edgeCount; 807 nodeItem.hasNext = nodeItem.childCount > 0; 808 809 classItem.classChildren.push(nodeItem); 810 } 811 } 812 813 // cal class retained size 814 let list = [this.rootNode]; 815 const sizes = [-1]; 816 const classesName = []; 817 let seenClassName = new Map<string, boolean>(); 818 819 while (list.length) { 820 let node = list.pop(); 821 if (!node) { 822 continue; 823 } 824 let nodeClassName = node.className(); 825 let seen = Boolean(seenClassName.get(nodeClassName)); 826 let dominatorFromIdx = this.firstDominatedNodesIdx[node.nodeIndex]; 827 let dominatorToIdx = this.firstDominatedNodesIdx[node.nodeIndex + 1]; 828 829 if (!seen && (!hasFiler || filter(node.id)) && (node.selfSize || node.type === NodeType.NATIVE)) { 830 let classItem = classes.get(nodeClassName); 831 if (classItem) { 832 classItem.retainedSize += node.retainedSize; 833 if (dominatorFromIdx !== dominatorToIdx) { 834 seenClassName.set(nodeClassName, true); 835 sizes.push(list.length); 836 classesName.push(nodeClassName); 837 } 838 } 839 } 840 841 for (let idx = dominatorFromIdx; idx < dominatorToIdx; idx++) { 842 let nodeIdx = this.dominatedNodes[idx]; 843 let domNode = this.nodes[nodeIdx]; 844 list.push(domNode); 845 } 846 847 while (sizes[sizes.length - 1] === list.length) { 848 sizes.pop(); 849 nodeClassName = classesName.pop() as string; 850 seenClassName.set(nodeClassName, false); 851 } 852 } 853 if (!hasFiler) { 854 this.allClasses = classes; 855 } 856 return classes; 857 } 858 859 /** 860 * compare base file and target file, calculate delta size and count to target class 861 * @param targetFileId to compare file's id 862 * @param targetFileClasses to compare file's constructor 863 */ 864 public getClassesForComparison(targetFileId: number, targetFileClasses: Map<string, ConstructorItem>) { 865 // Return the result if it has been obtained before 866 if (this.diffToOtherFile.has(targetFileId)) { 867 return this.diffToOtherFile.get(targetFileId); 868 } 869 // get base file class if not init before 870 if (!this.allClasses) { 871 this.allClasses = this.getClassesForSummary(); 872 } 873 874 //deal target class 875 let diffMap = new Map<string, ConstructorComparison>(); 876 for (let targetClass of targetFileClasses.values()) { 877 let classes = this.allClasses.get(targetClass.nodeName); 878 let different = this.calClassDiff(targetClass, classes); 879 if (different) { 880 diffMap.set(targetClass.nodeName, different); 881 } 882 } 883 884 // deal base class which is not in target 885 for (let classItem of this.allClasses.values()) { 886 if (targetFileClasses.has(classItem.nodeName)) { 887 continue; 888 } 889 let different = this.calClassDiff(classItem); 890 if (different) { 891 diffMap.set(classItem.nodeName, different); 892 } 893 } 894 this.diffToOtherFile.set(targetFileId, diffMap); 895 return diffMap; 896 } 897 898 /** 899 * Summary get Node Children 900 * @param item select Node 901 * @returns child Nodes 902 */ 903 public getNextNode(item: ConstructorItem): Array<ConstructorItem> { 904 if (item.children.length > 0) { 905 return item.children; 906 } 907 // get children from edge 908 let node = this.nodes[item.index]; 909 let childNodes = new Array<ConstructorItem>(); 910 for (let edge of node.edges) { 911 let childNode = this.nodeMap.get(edge.toNodeId); 912 if (!childNode) { 913 continue; 914 } 915 let instanceItem = HeapNodeToConstructorItem(childNode); 916 instanceItem.childCount = instanceItem.edgeCount = childNode.edgeCount; 917 instanceItem.edgeName = edge.nameOrIndex; 918 instanceItem.hasNext = instanceItem.childCount > 0; 919 instanceItem.traceNodeId = childNode.traceNodeId; 920 instanceItem.type = ConstructorType.FiledType; 921 instanceItem.parent = item; 922 childNodes.push(instanceItem); 923 } 924 925 let clickNode = childNodes[0].parent; 926 // If there are duplicate IDs in the third layer and beyond, they will not be expanded again 927 if (clickNode!.type == ConstructorType.FiledType) { 928 function findParents(clickNode: any, parents: any): any { 929 if (!clickNode.parent) { 930 return parents; 931 } 932 // add the parent of the current node to the result array 933 parents.push(clickNode); 934 for (let childNode of childNodes) { 935 for (let p of parents) { 936 if (p.id === childNode!.id) { 937 childNode.hasNext = false; 938 } 939 } 940 } 941 return findParents(clickNode.parent, parents); 942 } 943 findParents(clickNode, []); 944 } 945 let filterChildNodes = new Array<ConstructorItem>(); 946 for (let item of childNodes) { 947 if (item.id !== this.rootNode!.id) { 948 filterChildNodes.push(item); 949 } 950 } 951 item.children = filterChildNodes; 952 return filterChildNodes; 953 } 954 955 /** 956 * get nodes which referenced this node 957 * @param constructor current node 958 * @returns reference nodes 959 */ 960 public getRetains(item: ConstructorItem): Array<ConstructorItem> { 961 if (item.retains.length > 0) { 962 return item.retains; 963 } 964 if (item.type === ConstructorType.ClassType) { 965 return []; 966 } 967 let node = this.nodes[item.index]; 968 let retains = new Array<ConstructorItem>(); 969 if (node && node.retainsEdgeIdx.length === node.retainsNodeIdx.length) { 970 for (let i = 0; i < node.retainsNodeIdx.length; i++) { 971 let retainsNode = this.nodes[node.retainsNodeIdx[i]]; 972 let retainEdge = this.edges[node.retainsEdgeIdx[i]]; 973 974 if (retainEdge.type == EdgeType.WEAK) { 975 continue; 976 } 977 let retainsItem = HeapNodeToConstructorItem(retainsNode); 978 retainsItem.edgeName = retainEdge.nameOrIndex; 979 retainsItem.edgeType = retainEdge.type; 980 retainsItem.type = ConstructorType.RetainersType; 981 retainsItem.childCount = retainsNode.retainsNodeIdx.length; 982 retainsItem.hasNext = retainsNode.retainsNodeIdx.length > 0; 983 if (item!.type == ConstructorType.RetainersType) { 984 retainsItem.parent = item; 985 } 986 retains.push(retainsItem); 987 } 988 } 989 990 // 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 991 for (let childNode of retains) { 992 let node = this.nodes[childNode.index]; 993 if (node && node.retainsEdgeIdx.length === node.retainsNodeIdx.length) { 994 for (let i = 0; i < node.retainsNodeIdx.length; i++) { 995 let retainsNode = this.nodes[node.retainsNodeIdx[i]]; 996 if (node.retainsNodeIdx.length == 1 && retainsNode.id == this.rootNode!.id) { 997 childNode.hasNext = false; 998 } 999 } 1000 } 1001 } 1002 1003 if (retains.length > 0 && retains[0].parent) { 1004 let clickNode = retains[0].parent; 1005 // If there are duplicate IDs in the third layer and beyond, they will not be expanded again 1006 if (clickNode!.type == ConstructorType.RetainersType) { 1007 function findParents(clickNode: any, parents: any): any { 1008 if (!clickNode.parent) { 1009 return parents; 1010 } 1011 // add the parent of the current node to the result array 1012 parents.push(clickNode); 1013 for (let childNode of retains) { 1014 for (let p of parents) { 1015 if (p.id === childNode!.id) { 1016 childNode.hasNext = false; 1017 } 1018 } 1019 } 1020 return findParents(clickNode.parent, parents); 1021 } 1022 findParents(clickNode, []); 1023 } 1024 } 1025 1026 retains.sort(function (a: any, b: any) { 1027 return a.distance - b.distance; 1028 }); 1029 1030 let filterRetains = new Array<ConstructorItem>(); 1031 for (let item of retains) { 1032 if (item.id !== this.rootNode!.id) { 1033 filterRetains.push(item); 1034 } 1035 } 1036 return filterRetains; 1037 } 1038 1039 public getNodes(): Array<HeapNode> { 1040 return this.nodes; 1041 } 1042 1043 public clear() { 1044 this.allClasses?.clear(); 1045 this.diffToOtherFile.clear(); 1046 this.nodeMap.clear(); 1047 this.edges.length = 0; 1048 this.nodes.length = 0; 1049 } 1050} 1051 1052class DOMState { 1053 visited: Uint8Array; 1054 attached: Array<number>; 1055 detached: Array<number>; 1056 1057 constructor(nodeSize: number) { 1058 this.visited = new Uint8Array(nodeSize); 1059 this.attached = new Array<number>(); 1060 this.detached = new Array<number>(); 1061 } 1062} 1063