UNPKG

@memlab/core

Version:
471 lines (470 loc) 21.5 kB
/** * 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 * @lightSyntaxTransform * @oncall memory_lab */ 'use strict'; 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("../Config")); const Console_1 = __importDefault(require("../Console")); const HeapNode_1 = __importDefault(require("./HeapNode")); const HeapEdge_1 = __importDefault(require("./HeapEdge")); const HeapUtils_1 = require("./HeapUtils"); const MemLabTagStore_1 = __importDefault(require("./MemLabTagStore")); const NumericDictionary_1 = __importDefault(require("./utils/NumericDictionary")); const EMPTY_UINT8_ARRAY = new Uint8Array(0); const EMPTY_UINT32_ARRAY = new Uint32Array(0); const EMPTY_BIG_UINT64_ARRAY = new BigUint64Array(0); const EMPTY_NUMERIC_DICTIONARY = new NumericDictionary_1.default(0); class HeapSnapshot { // eslint-disable-next-line @typescript-eslint/no-unused-vars constructor(snapshot, _options = {}) { this.isProcessed = false; this._nodeCount = -1; this._edgeCount = -1; this._nodeId2NodeIdx = EMPTY_NUMERIC_DICTIONARY; this._nodeIdxHasPathEdge = EMPTY_UINT8_ARRAY; this._nodeIdx2PathEdgeIdx = EMPTY_UINT32_ARRAY; this._nodeIdx2DominatorNodeIdx = EMPTY_UINT32_ARRAY; this._nodeIdxHasDominatorNode = EMPTY_UINT8_ARRAY; this._nodeIdx2RetainedSize = EMPTY_BIG_UINT64_ARRAY; this._additionalAttributes = EMPTY_UINT8_ARRAY; this._nodeDetachednessOffset = -1; this._externalDetachedness = EMPTY_UINT8_ARRAY; this._nodeIdx2LocationIdx = EMPTY_UINT32_ARRAY; this._locationFieldsCount = -1; this._locationCount = -1; this._locationObjectIndexOffset = -1; this._nodeFieldsCount = -1; this._nodeIdOffset = -1; this.forwardEdges = []; this._nodeTypeOffset = -1; this._nodeNameOffset = -1; this._nodeSelfSizeOffset = -1; this._nodeEdgeCountOffset = -1; this._nodeTraceNodeIdOffset = -1; this._nodeTypes = []; this._nodeArrayType = -1; this._nodeHiddenType = -1; this._nodeObjectType = -1; this._nodeNativeType = -1; this._nodeConsStringType = -1; this._nodeSlicedStringType = -1; this._nodeCodeType = -1; this._nodeSyntheticType = -1; this._edgeFieldsCount = -1; this._edgeTypeOffset = -1; this._edgeNameOrIndexOffset = -1; this._edgeToNodeOffset = -1; this._edgeTypes = []; this._edgeElementType = -1; this._edgeHiddenType = -1; this._edgeInternalType = -1; this._edgeShortcutType = -1; this._edgeWeakType = -1; this._edgeInvisibleType = -1; this._locationScriptIdOffset = -1; this._locationLineOffset = -1; this._locationColumnOffset = -1; this._firstEdgePointers = EMPTY_UINT32_ARRAY; this._retainingEdgeIndex2EdgeIndex = EMPTY_UINT32_ARRAY; this._firstRetainerIndex = EMPTY_UINT32_ARRAY; this._edgeIndex2SrcNodeIndex = EMPTY_UINT32_ARRAY; this.snapshot = snapshot; this._metaNode = snapshot.snapshot.meta; this._buildMetaData(); // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; // virtual nodes this.nodes = { length: self._nodeCount, get(idx) { if (idx < 0 || idx >= self._nodeCount) { return null; } return new HeapNode_1.default(self, idx); }, forEach(cb) { for (let i = 0; i < this.length; i++) { const ret = cb(this.get(i), i); if (ret === false) { break; } } }, forEachTraceable(cb) { for (let i = 0; i < this.length; i++) { const node = this.get(i); if (!node.hasPathEdge) { continue; } const ret = cb(node, i); if (ret === false) { break; } } }, }; // virtual edges this.edges = { length: self._edgeCount, get(idx) { if (idx < 0 || idx >= self._edgeCount) { return null; } return new HeapEdge_1.default(self, idx); }, forEach(cb) { for (let i = 0; i < this.length; i++) { const ret = cb(this.get(i), i); if (ret === false) { break; } } }, }; } getJSONifyableObject() { return Object.assign({}, this.snapshot.snapshot); } toJSONString(...args) { const rep = this.getJSONifyableObject(); return JSON.stringify(rep, ...args); } hasObjectWithClassName(className) { let detected = false; this.nodes.forEach((node) => { if (node.name === className && node.type === 'object') { detected = true; return false; } }); return detected; } getAnyObjectWithClassName(className) { let ret = null; this.nodes.forEach((node) => { if (node.name === className && node.type === 'object') { ret = node; return false; } }); return ret; } hasObjectWithPropertyName(nameOrIndex) { let detected = false; this.edges.forEach((edge) => { if (edge.name_or_index === nameOrIndex && edge.type === 'property') { detected = true; return false; } }); return detected; } hasObjectWithTag(tag) { return MemLabTagStore_1.default.hasObjectWithTag(this, tag); } getNodeById(id) { const idx = this._nodeId2NodeIdx.get(id); return idx == null ? null : new HeapNode_1.default(this, idx); } getNodesByIds(ids) { const ret = []; ids.forEach(id => { const idx = this._nodeId2NodeIdx.get(id); ret.push(idx == null ? null : new HeapNode_1.default(this, idx)); }); return ret; } getNodesByIdSet(ids) { const ret = new Set(); ids.forEach(id => { const idx = this._nodeId2NodeIdx.get(id); if (idx != null) { ret.add(new HeapNode_1.default(this, idx)); } }); return ret; } clearShortestPathInfo() { this._nodeIdxHasPathEdge = new Uint8Array(this._nodeCount); } _buildMetaData() { this._calculateBasicInfo(); this._buildReferencesIndex(); this._buildReferrersIndex(); this._propagateDetachednessState(); this._buildNodeIdx(); this._buildLocationIdx(); this._buildExtraMetaData(); } _buildExtraMetaData() { Console_1.default.overwrite('building extra meta info...'); // maps node index to the edge index (from edge) of its shortest path to GC root this._nodeIdx2PathEdgeIdx = new Uint32Array(this._nodeCount); this._nodeIdxHasPathEdge = new Uint8Array(this._nodeCount); // dominator node info this._nodeIdx2DominatorNodeIdx = new Uint32Array(this._nodeCount); this._nodeIdxHasDominatorNode = new Uint8Array(this._nodeCount); // retained size info this._nodeIdx2RetainedSize = new BigUint64Array(this._nodeCount); // additional attributes this._additionalAttributes = new Uint8Array(this._nodeCount); // additional detachedness info if (this._nodeDetachednessOffset < 0) { this._externalDetachedness = new Uint8Array(this._nodeCount); } } _buildLocationIdx() { Console_1.default.overwrite('building location index...'); this._nodeIdx2LocationIdx = new Uint32Array(this._nodeCount); // use the total location count as a guard value for no mapping this._nodeIdx2LocationIdx.fill(this._locationCount); // iterate over locations const locations = this.snapshot.locations; const locationFieldsCount = this._locationFieldsCount; let locationIdx = 0; while (locationIdx < this._locationCount) { const nodeIndex = locations[locationIdx * locationFieldsCount + this._locationObjectIndexOffset]; this._nodeIdx2LocationIdx[nodeIndex] = locationIdx; ++locationIdx; } } _buildNodeIdx() { Console_1.default.overwrite('building node index...'); this._nodeId2NodeIdx = new NumericDictionary_1.default(this._nodeCount, { fastStoreSize: Config_1.default.heapParserDictFastStoreSize, }); // iterate over each node const nodeValues = this.snapshot.nodes; const nodeFieldsCount = this._nodeFieldsCount; let nodeIdx = 0; while (nodeIdx < this._nodeCount) { const id = nodeValues[nodeIdx * nodeFieldsCount + this._nodeIdOffset]; this._nodeId2NodeIdx.set(id, nodeIdx); ++nodeIdx; } } _calculateBasicInfo() { Console_1.default.overwrite('calculating basic meta info...'); this.forwardEdges = this.snapshot.edges; const meta = this._metaNode; const nodeFields = meta.node_fields; this._nodeTypeOffset = nodeFields.indexOf('type'); this._nodeNameOffset = nodeFields.indexOf('name'); this._nodeIdOffset = nodeFields.indexOf('id'); this._nodeSelfSizeOffset = nodeFields.indexOf('self_size'); this._nodeEdgeCountOffset = nodeFields.indexOf('edge_count'); this._nodeTraceNodeIdOffset = nodeFields.indexOf('trace_node_id'); this._nodeDetachednessOffset = nodeFields.indexOf('detachedness'); Config_1.default.snapshotHasDetachedness = this._nodeDetachednessOffset >= 0; this._nodeFieldsCount = nodeFields.length; const nodeTypes = meta.node_types[this._nodeTypeOffset]; this._nodeTypes = nodeTypes; this._nodeArrayType = nodeTypes.indexOf('array'); this._nodeHiddenType = nodeTypes.indexOf('hidden'); this._nodeObjectType = nodeTypes.indexOf('object'); this._nodeNativeType = nodeTypes.indexOf('native'); this._nodeConsStringType = nodeTypes.indexOf('concatenated string'); this._nodeSlicedStringType = nodeTypes.indexOf('sliced string'); this._nodeCodeType = nodeTypes.indexOf('code'); this._nodeSyntheticType = nodeTypes.indexOf('synthetic'); const edgeFields = meta.edge_fields; this._edgeFieldsCount = edgeFields.length; this._edgeTypeOffset = edgeFields.indexOf('type'); this._edgeNameOrIndexOffset = edgeFields.indexOf('name_or_index'); this._edgeToNodeOffset = edgeFields.indexOf('to_node'); const edgeTypes = meta.edge_types[this._edgeTypeOffset]; edgeFields.push('invisible'); this._edgeTypes = edgeTypes; this._edgeElementType = edgeFields.indexOf('element'); this._edgeHiddenType = edgeFields.indexOf('hidden'); this._edgeInternalType = edgeFields.indexOf('internal'); this._edgeShortcutType = edgeFields.indexOf('shortcut'); this._edgeWeakType = edgeFields.indexOf('weak'); this._edgeInvisibleType = edgeFields.indexOf('invisible'); const locationFields = meta.location_fields || []; this._locationObjectIndexOffset = locationFields.indexOf('object_index'); this._locationScriptIdOffset = locationFields.indexOf('script_id'); this._locationLineOffset = locationFields.indexOf('line'); this._locationColumnOffset = locationFields.indexOf('column'); this._locationFieldsCount = locationFields.length; const snapshot = this.snapshot; this._nodeCount = snapshot.nodes.length / this._nodeFieldsCount; this._edgeCount = this.forwardEdges.length / this._edgeFieldsCount; this._locationCount = snapshot.locations.length / this._locationFieldsCount; } // create array that maps node index in node list to // the node's first edge index in forward edge list _buildReferencesIndex() { Console_1.default.overwrite('building reference index...'); this._firstEdgePointers = new Uint32Array(this._nodeCount + 1); const nodes = this.snapshot.nodes; const nodeCount = this._nodeCount; const firstEdgePointers = this._firstEdgePointers; const nodeFieldsCount = this._nodeFieldsCount; const edgeFieldsCount = this._edgeFieldsCount; const nodeEdgeCountOffset = this._nodeEdgeCountOffset; firstEdgePointers[nodeCount] = this.forwardEdges.length; let edgePointer = 0; for (let nodeIndex = 0; nodeIndex < nodeCount; ++nodeIndex) { firstEdgePointers[nodeIndex] = edgePointer; const nodeEdgeCountPointer = nodeIndex * nodeFieldsCount + nodeEdgeCountOffset; edgePointer += nodes[nodeEdgeCountPointer] * edgeFieldsCount; } } // build indexes for each node's retaining edge and retaining node _buildReferrersIndex() { Console_1.default.overwrite('building referrers index...'); // init data const retainingEdgeCount = new Uint32Array(this._edgeCount); this._retainingEdgeIndex2EdgeIndex = new Uint32Array(this._edgeCount); // index of the first retainer edge in _retainingEdgeIndex2EdgeIndex this._firstRetainerIndex = new Uint32Array(this._nodeCount + 1); // map edge index to source node index this._edgeIndex2SrcNodeIndex = new Uint32Array(this._edgeCount); // calculate retainers const edgeIndex2SrcNodeIndex = this._edgeIndex2SrcNodeIndex; const retainingEdgeIndex2EdgeIndex = this._retainingEdgeIndex2EdgeIndex; const firstRetainerIndex = this._firstRetainerIndex; const forwardEdges = this.forwardEdges; const edgeFieldsCount = this._edgeFieldsCount; const nodeFieldsCount = this._nodeFieldsCount; const edgeToNodeOffset = this._edgeToNodeOffset; const firstEdgePointers = this._firstEdgePointers; const nodeCount = this._nodeCount; // calculate the number of retainer nodes for each node // temporarily store the information in firstRetainerIndex for (let toNodeFieldIndex = edgeToNodeOffset; toNodeFieldIndex < forwardEdges.length; toNodeFieldIndex += edgeFieldsCount) { const toNodeIndex = forwardEdges[toNodeFieldIndex]; if (toNodeIndex % nodeFieldsCount) { (0, HeapUtils_1.throwError)(new Error('Invalid toNodeIndex ' + toNodeIndex)); } ++firstRetainerIndex[toNodeIndex / nodeFieldsCount]; } // calculate the actual first retainer node index info // temporarily store the number of retainers in retainingNodes (sparsely) for (let i = 0, firstUnusedRetainerSlot = 0; i < nodeCount; i++) { const retainersCount = firstRetainerIndex[i]; firstRetainerIndex[i] = firstUnusedRetainerSlot; retainingEdgeCount[firstUnusedRetainerSlot] = retainersCount; firstUnusedRetainerSlot += retainersCount; } firstRetainerIndex[nodeCount] = retainingEdgeCount.length; // calculate a compact array containing retainer node and retainer edge let nextNodeFirstEdgePointer = firstEdgePointers[0]; for (let srcNodeIndex = 0; srcNodeIndex < nodeCount; ++srcNodeIndex) { const firstEdgePointer = nextNodeFirstEdgePointer; nextNodeFirstEdgePointer = firstEdgePointers[srcNodeIndex + 1]; for (let edgePointer = firstEdgePointer; edgePointer < nextNodeFirstEdgePointer; edgePointer += edgeFieldsCount) { const toNodePointer = forwardEdges[edgePointer + edgeToNodeOffset]; if (toNodePointer % nodeFieldsCount) { (0, HeapUtils_1.throwError)(Error('Invalid toNodeIndex ' + toNodePointer)); } const toNodeIndex = toNodePointer / nodeFieldsCount; const firstRetainerSlotIndex = firstRetainerIndex[toNodeIndex]; const nextUnusedRetainerSlotIndex = firstRetainerSlotIndex + --retainingEdgeCount[firstRetainerSlotIndex]; const edgeIndex = edgePointer / edgeFieldsCount; retainingEdgeIndex2EdgeIndex[nextUnusedRetainerSlotIndex] = edgeIndex; edgeIndex2SrcNodeIndex[edgeIndex] = srcNodeIndex; } } } // a helper function that iterates the direct children of a given node _iterateDirectChildren(nodeIndex, edgeFilterCallback, childCallback) { const beginEdgePointer = this._firstEdgePointers[nodeIndex]; const endEdgePointer = this._firstEdgePointers[nodeIndex + 1]; for (let edgePointer = beginEdgePointer; edgePointer < endEdgePointer; edgePointer += this._edgeFieldsCount) { const childNodePointer = this.forwardEdges[edgePointer + this._edgeToNodeOffset]; const childNodeIndex = childNodePointer / this._nodeFieldsCount; const type = this.forwardEdges[edgePointer + this._edgeTypeOffset]; if (!edgeFilterCallback(type)) { continue; } childCallback(childNodeIndex); } } // initial detachedness state is available at entry point to native node // this helper function propagate the detachedness state to connected // native node (mainly DOM elements) _propagateDetachednessState() { if (this._nodeDetachednessOffset === -1) { return; } Console_1.default.overwrite('propagating detachedness state...'); const visited = new Uint8Array(this._nodeCount); const attached = []; const detached = []; // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; const setNodeDetachState = function (snapshot, nodeIndex, detachedness) { if (visited[nodeIndex] === 1) { return; } const nodePointer = nodeIndex * self._nodeFieldsCount; // Do not propagate the state (and name change) through JavaScript. // From V8: Every entry point into embedder code is a node that knows // its own state. All embedder nodes have their node type set to native. if (snapshot.nodes[nodePointer + self._nodeTypeOffset] !== self._nodeNativeType) { visited[nodeIndex] = 1; return; } snapshot.nodes[nodePointer + self._nodeDetachednessOffset] = detachedness; if (detachedness === HeapUtils_1.NodeDetachState.Attached) { attached.push(nodeIndex); } else if (detachedness === HeapUtils_1.NodeDetachState.Detached) { detached.push(nodeIndex); } visited[nodeIndex] = 1; }; const hiddenEdgeTypes = [ self._edgeHiddenType, self._edgeInvisibleType, self._edgeWeakType, ]; const filterEdge = function (edgeType) { return hiddenEdgeTypes.indexOf(edgeType) < 0; }; const propagate = function (snapshot, nodeIndex, detachedness) { self._iterateDirectChildren(nodeIndex, filterEdge, childNodeIndex => setNodeDetachState(snapshot, childNodeIndex, detachedness)); }; // add attached and detached nodes to queue for (let nodeIndex = 0; nodeIndex < this._nodeCount; ++nodeIndex) { const state = this.snapshot.nodes[nodeIndex * this._nodeFieldsCount + this._nodeDetachednessOffset]; if (state === HeapUtils_1.NodeDetachState.Unknown) { continue; } setNodeDetachState(this.snapshot, nodeIndex, state); } // if the parent is attached, then the child is also attached while (attached.length > 0) { const nodeIndex = attached.pop(); if (nodeIndex != null) { propagate(this.snapshot, nodeIndex, HeapUtils_1.NodeDetachState.Attached); } } // if the parent is not attached, then the child inherits the parent's state while (detached.length > 0) { const nodeIndex = detached.pop(); if (nodeIndex == null) { continue; // make TS code strict mode happy } const detachedness = this.snapshot.nodes[nodeIndex * this._nodeFieldsCount + this._nodeDetachednessOffset]; if (detachedness === HeapUtils_1.NodeDetachState.Attached) { continue; } propagate(this.snapshot, nodeIndex, HeapUtils_1.NodeDetachState.Detached); } } } exports.default = HeapSnapshot;