UNPKG

@memlab/core

Version:
540 lines (539 loc) 23.2 kB
"use strict"; /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format * @oncall memory_lab */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const Config_1 = __importDefault(require("../lib/Config")); const Console_1 = __importDefault(require("../lib/Console")); const Utils_1 = __importDefault(require("../lib/Utils")); const NumericSet_1 = __importDefault(require("../lib/heap-data/utils/NumericSet")); const ROOT_NODE_INDEX = 0; const PAGE_OBJECT_FLAG = 1; class TraceFinder { getRootNodeList(snapshot, opt = {}) { const highPri = []; const lowPri = []; if (opt.prioritize) { snapshot.nodes.forEach(node => { if (Utils_1.default.isRootNode(node, { excludeBlinkRoot: true, excludePendingActivity: true, })) { highPri.push(node); } else if (Utils_1.default.isRootNode(node)) { lowPri.push(node); } }); } else { snapshot.nodes.forEach(node => { if (Utils_1.default.isRootNode(node)) { highPri.push(node); } }); } return [highPri, lowPri]; } visitReachableNodesbyDFS(snapshot, nodeVisitor, edgeVisitor) { const [queue] = this.getRootNodeList(snapshot); const queuedIDs = new NumericSet_1.default(queue.map(n => n.id)); const visitedIDs = new NumericSet_1.default(); const traverseOption = { visited: visitedIDs, queued: queuedIDs, excludeWeakMapEdge: false, // do not exclude all weak maps isForward: true, }; while (queue.length > 0) { const node = queue.pop(); if (!node || visitedIDs.has(node.id)) { continue; } if (nodeVisitor && nodeVisitor(node) === false) { continue; } visitedIDs.add(node.id); for (const edge of node.references) { if (!this.shouldTraverseEdge(edge, snapshot, traverseOption)) { continue; } const nextNode = edge.toNode; // deal with weak map specifically if (Utils_1.default.isWeakMapEdgeToKey(edge)) { const weakMapKeyObjectId = Utils_1.default.getWeakMapEdgeKeyId(edge); // in weak map keys are weakly referenced if (weakMapKeyObjectId === nextNode.id) { continue; } } if (edgeVisitor && edgeVisitor(edge) === false) { continue; } queue.push(nextNode); queuedIDs.add(nextNode.id); } } } flagReachableNodesFromWindow(snapshot, flags, flag) { const nodesCount = snapshot.nodes.length; const nodesToVisit = new Uint32Array(nodesCount); let nodesToVisitLength = 0; const node = snapshot.nodes.get(ROOT_NODE_INDEX); for (const edge of node.references) { const toNode = edge.toNode; const type = edge.type; if (type === 'element') { if (Utils_1.default.isDocumentDOMTreesRoot(toNode)) { continue; } } else if (type === 'shortcut') { continue; } const childNodeIndex = toNode.nodeIndex; nodesToVisit[nodesToVisitLength++] = childNodeIndex; flags[childNodeIndex] |= flag; } // flag all heap objects reachable from the root while (nodesToVisitLength > 0) { const nodeIndex = nodesToVisit[--nodesToVisitLength]; const node = snapshot.nodes.get(nodeIndex); for (const edge of node.references) { const childNode = edge.toNode; const childNodeIndex = childNode.nodeIndex; if (flags[childNodeIndex] & flag) { continue; } if (edge.type === 'weak') { continue; } nodesToVisit[nodesToVisitLength++] = childNodeIndex; flags[childNodeIndex] |= flag; } } } // Build the dominator tree and retained sizes using the Lengauer-Tarjan // algorithm: // Thomas Lengauer and Robert Endre Tarjan. 1979. A fast algorithm for // finding dominators in a flowgraph. ACM Trans. Program. Lang. Syst. 1, 1 // (July 1979), 121-141. https://doi.org/10.1145/357062.357071 // // This is the same near-linear, single-pass algorithm Chrome DevTools uses. // It replaces the previous iterative Cooper-Harvey-Kennedy fix-point, which // had two problems on real browser snapshots: // 1. It could spin forever. The fix-point's two-finger intersect() walk // assumes every step climbs toward the root, but the post-order that was // assigned to orphan / weakly-reachable nodes (unreachable from the GC // root) could violate that, producing a cycle in the provisional // dominator pointers or a walk onto the out-of-bounds "empty" sentinel. // The walk then never terminated -- one CPU pinned at 100% with a flat // heap, stuck at "calculating dominators and retained sizes". // 2. Even when it converged it needed O(graph-depth) sweeps. // // Lengauer-Tarjan avoids both: it assigns every node a valid DFS number, // treats orphan and mutually-retaining "clique" nodes as retained by the // root, and computes dominators in a single pass with no fix-point. // // Vertices are numbered 1..nodeCount (0 is the invalid/empty value the // algorithm relies on); ordinal === vertex - 1 maps a vertex back to its heap // node index. computeDominatorsAndRetainedSizes(snapshot, flags) { const nodes = snapshot.nodes; const edges = snapshot.edges; const nodeCount = nodes.length; const flag = PAGE_OBJECT_FLAG; // Offset of each node's first outgoing edge in the flat edge list. const firstEdgeIndexes = new Uint32Array(nodeCount + 1); firstEdgeIndexes[nodeCount] = edges.length; for (let ordinal = 0, edgeIndex = 0; ordinal < nodeCount; ++ordinal) { firstEdgeIndexes[ordinal] = edgeIndex; edgeIndex += nodes.get(ordinal).edge_count; } // A single essential-edge predicate used for BOTH the forward DFS and the // backward retainer scan, so the two passes agree on exactly which edges // exist. (A disagreement between them -- the forward pass omitting a filter // the backward pass applied -- is what corrupted the old dominator relation // and caused the hang.) Mirrors Chrome DevTools' computeIsEssentialEdge: // skip weak edges, non-root shortcut edges, self edges, and edges from a // non-page-owned node into a page-owned node (otherwise the debugger's own // references would perturb product-object dominators). const isEssential = (fromOrdinal, edgeType, toOrdinal) => { if (fromOrdinal === toOrdinal) { return false; } if (!Utils_1.default.isEssentialEdge(fromOrdinal, edgeType, ROOT_NODE_INDEX)) { return false; } if (fromOrdinal !== ROOT_NODE_INDEX && flags[toOrdinal] & flag && !(flags[fromOrdinal] & flag)) { return false; } return true; }; // Lengauer-Tarjan working arrays, 1-indexed (index 0 == invalid). const arrayLength = nodeCount + 1; const parent = new Uint32Array(arrayLength); const ancestor = new Uint32Array(arrayLength); const vertex = new Uint32Array(arrayLength); const label = new Uint32Array(arrayLength); const semi = new Uint32Array(arrayLength); const dom = new Uint32Array(arrayLength); const bucket = new Array(arrayLength); // Resumable per-node edge cursor for the iterative DFS. const nextEdgeIndex = new Uint32Array(arrayLength); let n = 0; // Iterative DFS (a recursive version overflows the stack on large heaps). const dfs = (root) => { const rootOrdinal = root - 1; nextEdgeIndex[rootOrdinal] = firstEdgeIndexes[rootOrdinal]; let v = root; while (v !== 0) { // Number v the first time it is reached. if (semi[v] === 0) { semi[v] = ++n; vertex[n] = label[v] = v; } // The next node to visit is v's first unprocessed essential successor, // else v's parent (backtrack). let vNext = parent[v]; const vOrdinal = v - 1; const edgesEnd = firstEdgeIndexes[vOrdinal + 1]; for (; nextEdgeIndex[vOrdinal] < edgesEnd; ++nextEdgeIndex[vOrdinal]) { const edge = edges.get(nextEdgeIndex[vOrdinal]); const wOrdinal = edge.toNode.nodeIndex; if (!isEssential(vOrdinal, edge.type, wOrdinal)) { continue; } const w = wOrdinal + 1; if (semi[w] === 0) { parent[w] = v; nextEdgeIndex[wOrdinal] = firstEdgeIndexes[wOrdinal]; vNext = w; break; } } v = vNext; } }; // eval/link with path compression, iterative to avoid deep recursion. const compressionStack = new Uint32Array(arrayLength); const compress = (node) => { let v = node; let stackPointer = 0; while (ancestor[ancestor[v]] !== 0) { compressionStack[++stackPointer] = v; v = ancestor[v]; } while (stackPointer > 0) { const w = compressionStack[stackPointer--]; if (semi[label[ancestor[w]]] < semi[label[w]]) { label[w] = label[ancestor[w]]; } ancestor[w] = ancestor[ancestor[w]]; } }; const evaluate = (v) => { if (ancestor[v] === 0) { return v; } compress(v); return label[v]; }; const link = (v, w) => { ancestor[w] = v; }; const r = ROOT_NODE_INDEX + 1; // Step 1: DFS from the root. dfs(r); // Step 2: nodes may remain unreachable from the root. First bring in // orphans that have only weak retainers, DFS-ing from each. if (n < nodeCount) { for (let v = 1; v <= nodeCount; ++v) { if (semi[v] === 0 && Utils_1.default.hasOnlyWeakReferrers(nodes.get(v - 1))) { parent[v] = r; dfs(v); } } } // Step 3: whatever is still unreachable is a clique of nodes retained only // by one another. Attach each directly under the root and give it a DFS // number so the algorithm is well-defined for every node. if (n < nodeCount) { for (let v = 1; v <= nodeCount; ++v) { if (semi[v] === 0) { parent[v] = r; semi[v] = ++n; vertex[n] = label[v] = v; } } } // Step 4: main loop, processing vertices in decreasing DFS number. for (let i = n; i >= 2; --i) { const w = vertex[i]; const wOrdinal = w - 1; // Compute semidominator of w from its (essential) predecessors. let isOrphanNode = true; nodes.get(wOrdinal).forEachReferrer((edge) => { const vOrdinal = edge.fromNode.nodeIndex; if (!isEssential(vOrdinal, edge.type, wOrdinal)) { return; } isOrphanNode = false; const u = evaluate(vOrdinal + 1); if (semi[u] < semi[w]) { semi[w] = semi[u]; } }); // Treat an orphan as retained by the root; semi[r] is <= any other semi. if (isOrphanNode) { semi[w] = semi[r]; } const semidominator = vertex[semi[w]]; if (bucket[semidominator] === undefined) { bucket[semidominator] = new Set(); } bucket[semidominator].add(w); link(parent[w], w); // Process the vertices in bucket(parent(w)). const parentBucket = bucket[parent[w]]; if (parentBucket !== undefined) { for (const v of parentBucket) { const u = evaluate(v); dom[v] = semi[u] < semi[v] ? u : parent[w]; } parentBucket.clear(); } } // Step 5: fill in the immediate dominators not computed explicitly above. // The root is treated as its own dominator, which also propagates the root // as the dominator of any unreachable node. dom[0] = dom[r] = r; for (let i = 2; i <= n; ++i) { const w = vertex[i]; if (dom[w] !== vertex[semi[w]]) { dom[w] = dom[dom[w]]; } } // Convert to ordinal-indexed dominators and seed retained sizes with self // sizes. const dominatorOrdinals = new Uint32Array(nodeCount); const retainedSizes = new Float64Array(nodeCount); for (let ordinal = 0; ordinal < nodeCount; ++ordinal) { dominatorOrdinals[ordinal] = dom[ordinal + 1] - 1; retainedSizes[ordinal] = nodes.get(ordinal).self_size; } // Propagate retained sizes up the dominator tree in reverse DFS order, so // each node is accumulated into its dominator before the dominator itself. // vertex[1] is the root, so stop at i > 1. for (let i = n; i > 1; --i) { const ordinal = vertex[i] - 1; retainedSizes[dominatorOrdinals[ordinal]] += retainedSizes[ordinal]; } return { dominatorOrdinals, retainedSizes }; } shouldIgnoreEdgeInTraceFinding(edge) { const fromNode = edge.fromNode; const toNode = edge.toNode; const isDetachedNode = Utils_1.default.isDetachedDOMNode(toNode); if (Config_1.default.hideBrowserLeak && Utils_1.default.isBlinkRootNode(fromNode) && isDetachedNode) { return true; } if (!Config_1.default.reportLeaksInTimers && Utils_1.default.isPendingActivityNode(fromNode) && isDetachedNode) { return true; } return false; } shouldTraverseEdge(edge, snapshot, options = {}) { var _a; const shouldTraverseByDefault = this.shouldTraverseNodeByInternalStandard(edge, options); const externalFilter = (_a = Config_1.default.externalLeakFilter) === null || _a === void 0 ? void 0 : _a.retainerReferenceFilter; if (externalFilter != null) { return externalFilter(edge, snapshot, shouldTraverseByDefault); } return shouldTraverseByDefault; } shouldTraverseNodeByInternalStandard(edge, options = {}) { if (this.isBlockListedEdge(edge)) { return false; } return Utils_1.default.isMeaningfulEdge(edge, Object.assign({ includeString: true }, options)); } // remove edges that are already part of reported leaked paths isBlockListedEdge(edge) { const nameOrIndex = edge.name_or_index; if (!Config_1.default.traverseDevToolsConsole && edge.type === 'internal' && typeof nameOrIndex === 'string' && nameOrIndex.indexOf('DevTools console') >= 0) { return true; } if (Config_1.default.edgeNameBlockList.has(String(nameOrIndex))) { return true; } if (Config_1.default.nodeNameBlockList.has(edge.toNode.name)) { return true; } if (Config_1.default.nodeNameBlockList.has(edge.fromNode.name)) { return true; } return false; } isLessPreferableEdge(edge) { const fromNode = edge.fromNode; const toNode = edge.toNode; // pending activities -> DOM element is less preferrable if (Utils_1.default.isPendingActivityNode(fromNode) && Utils_1.default.isDOMNodeIncomplete(toNode)) { return true; } // detached DOM node -> non-detached DOM node is less preferable if (Utils_1.default.isDetachedDOMNode(fromNode) && Utils_1.default.isDOMNodeIncomplete(toNode) && !Utils_1.default.isDetachedDOMNode(toNode)) { return true; } // non-detached DOM node -> detached DOM node is less preferable if (Utils_1.default.isDOMNodeIncomplete(fromNode) && !Utils_1.default.isDetachedDOMNode(fromNode) && Utils_1.default.isDetachedDOMNode(toNode)) { return true; } return Config_1.default.edgeNameGreyList.has(String(edge.name_or_index)); } isLessPreferableNode(node) { return Config_1.default.nodeNameGreyList.has(node.name) || Utils_1.default.isCppRootsNode(node); } // each edge is indexed by fromNode's ID, toNode's ID, edge name, and edge type getEdgeKey(edge) { const fromNode = edge.fromNode; const toNode = edge.toNode; return `${fromNode.id}|${edge.name_or_index}|${edge.type}|${toNode.id}`; } calculateAllNodesRetainedSizes(snapshot) { Console_1.default.overwrite('calculating dominators and retained sizes .'); const nodes = snapshot.nodes; // Flag nodes owned by the page (reachable from the window) so the dominator // pass can ignore edges the debugger adds into page-owned objects. const flags = new Uint32Array(nodes.length); this.flagReachableNodesFromWindow(snapshot, flags, PAGE_OBJECT_FLAG); Console_1.default.overwrite('calculating dominators and retained sizes ..'); const { dominatorOrdinals, retainedSizes } = this.computeDominatorsAndRetainedSizes(snapshot, flags); // assign retained sizes and dominators to nodes Console_1.default.overwrite('calculating dominators and retained sizes ...'); for (let ordinal = 0; ordinal < retainedSizes.length; ++ordinal) { const node = nodes.get(ordinal); node.retainedSize = retainedSizes[ordinal]; node.dominatorNode = nodes.get(dominatorOrdinals[ordinal]); } } annotateShortestPaths(snapshot, excludeKeySet) { snapshot.clearShortestPathInfo(); Console_1.default.overwrite('annotating shortest path for all nodes'); const [nodeRootLists, lowPriRootLists] = this.getRootNodeList(snapshot, { prioritize: true, }); const nodeCount = snapshot.nodes.length; const visited = new Uint8Array(nodeCount); const queued = new Uint8Array(nodeCount); const traverseOption = { visited, queued, excludeWeakMapEdge: true, isForward: true, }; let curQueue = nodeRootLists; const postponeQueue = []; while (curQueue.length > 0) { const nextQueue = []; while (curQueue.length > 0) { const node = curQueue.pop(); visited[node.nodeIndex] = 1; for (const edge of node.references) { const toNode = edge.toNode; // skip nodes that already have a parent if (toNode.hasPathEdge) { continue; } if (!this.shouldTraverseEdge(edge, snapshot, traverseOption)) { continue; } if (this.shouldIgnoreEdgeInTraceFinding(edge)) { continue; } if (Utils_1.default.isWeakMapEdge(edge) && excludeKeySet) { const weakMapKeyObjectId = Utils_1.default.getWeakMapEdgeKeyId(edge); if (excludeKeySet.has(weakMapKeyObjectId)) { continue; } } // postpone traversing edges and nodes that are less preferable if (this.isLessPreferableEdge(edge) || this.isLessPreferableNode(toNode)) { postponeQueue.push(edge); } else { toNode.pathEdge = edge; nextQueue.push(toNode); } queued[toNode.nodeIndex] = 1; } } // if no other preferable traces available // traverse the postpone queue while (nextQueue.length === 0 && postponeQueue.length > 0) { const edge = postponeQueue.pop(); const toNode = edge.toNode; if (toNode.hasPathEdge) { continue; } toNode.pathEdge = edge; nextQueue.push(toNode); } // if no other preferable traces available // consider the low priority root nodes while (nextQueue.length === 0 && lowPriRootLists.length > 0) { const root = lowPriRootLists.pop(); if (root.hasPathEdge) { continue; } nextQueue.push(root); } curQueue = nextQueue; } } getPathToGCRoots(_snapshot, node) { if (!node || !node.hasPathEdge) { return null; } const visited = new Set([node.id]); let path = { node }; while (node && node.hasPathEdge) { const edge = node.pathEdge; const fromNode = edge.fromNode; if (visited.has(fromNode.id)) { return null; } visited.add(fromNode.id); path = { node: fromNode, edge, next: path }; node = edge.fromNode; } return path; } } exports.default = TraceFinder;