UNPKG

@memlab/core

Version:
1,577 lines 69.3 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 __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.runShell = exports.resolveSnapshotFilePath = void 0; const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const child_process_1 = __importDefault(require("child_process")); const process_1 = __importDefault(require("process")); const Config_1 = __importStar(require("./Config")); const Console_1 = __importDefault(require("./Console")); const Constant_1 = __importDefault(require("./Constant")); const HeapParser_1 = __importDefault(require("./HeapParser")); const memCache = Object.create(null); const FileManager_1 = __importDefault(require("./FileManager")); const __1 = require(".."); const HeapNode_1 = require("./heap-data/HeapNode"); // For more details see ReactWorkTags.js of React const reactWorkTag = { FunctionComponent: 0, ClassComponent: 1, IndeterminateComponent: 2, HostRoot: 3, HostPortal: 4, HostComponent: 5, HostText: 6, Fragment: 7, Mode: 8, ContextConsumer: 9, ContextProvider: 10, ForwardRef: 11, Profiler: 12, SuspenseComponent: 13, MemoComponent: 14, SimpleMemoComponent: 15, LazyComponent: 16, IncompleteClassComponent: 17, DehydratedFragment: 18, SuspenseListComponent: 19, ScopeComponent: 21, OffscreenComponent: 22, LegacyHiddenComponent: 23, CacheComponent: 24, }; const reactTagIdToName = []; Object.entries(reactWorkTag).forEach(workTag => (reactTagIdToName[workTag[1]] = workTag[0])); function _getReactWorkTagName(tagId) { if (typeof tagId === 'string') { tagId = parseInt(tagId, 10); } if (typeof tagId !== 'number' || tagId !== tagId) { return null; } return reactTagIdToName[tagId]; } function isHermesInternalObject(node) { return (node.type === 'number' || node.name === 'HiddenClass' || node.name === 'Environment' || node.name === 'ArrayStorage' || node.name === 'SegmentedArray' || node.name === 'WeakValueMap' || node.name === 'HashMapEntry'); } function isStackTraceFrame(node) { if (!node || node.type !== 'hidden') { return false; } return node.name === 'system / StackTraceFrame'; } // returns true if it is detached DOM element or detached FiberNode // NOTE: Doesn't work for FiberNode without detachedness field function isDetached(node) { if (Config_1.default.snapshotHasDetachedness) { return node.is_detached; } return node.name.startsWith('Detached '); } function isFiberNode(node) { if (!node || node.type !== 'object') { return false; } const name = node.name; return name === 'FiberNode' || name === 'Detached FiberNode'; } // quickly check the detachedness field // need to call markDetachedFiberNode(node) before this function // does not traverse and check the existance of HostRoot // NOTE: Doesn't work for FiberNode without detachedness field function isDetachedFiberNode(node) { return isFiberNode(node) && isDetached(node); } // return true if this node is InternalNode (native) function isDOMInternalNode(node) { if (node == null) { return false; } return (node.type === 'native' && (node.name === 'InternalNode' || node.name === 'Detached InternalNode')); } // return true the the nodee is a global handles node function isGlobalHandlesNode(node) { if (node == null) { return false; } return node.name === '(Global handles)' && node.type === 'synthetic'; } // this function returns a more general sense of DOM nodes. Specifically, // any detached DOM nodes (e.g., HTMLXXElement, IntersectionObserver etc.) // that are not internal nodes. function isDetachedDOMNode(node, args = {}) { let name = null; if (!node || typeof (name = node.name) !== 'string') { return false; } if (isFiberNode(node)) { return false; } if (name === 'Detached InternalNode' && args.ignoreInternalNode) { return false; } return isDetached(node); } function isWeakMapEdge(edge) { if (!edge || typeof edge.name_or_index !== 'string') { return false; } if (edge.name_or_index.indexOf('WeakMap') < 0) { return false; } return true; } function isWeakMapEdgeToKey(edge) { if (!isWeakMapEdge(edge)) { return false; } const weakMapKeyObjectId = getWeakMapEdgeKeyId(edge); const toNodeObjectId = edge.toNode.id; // in WeakMap, keys are weakly referenced if (weakMapKeyObjectId === toNodeObjectId) { return true; } return false; } function isWeakMapEdgeToValue(edge) { if (!isWeakMapEdge(edge)) { return false; } const weakMapKeyObjectId = getWeakMapEdgeKeyId(edge); const toNodeObjectId = edge.toNode.id; // in WeakMap, keys are weakly referenced if (weakMapKeyObjectId !== toNodeObjectId) { return true; } return false; } function isEssentialEdge(nodeIndex, edgeType, rootNodeIndex) { // According to Chrome Devtools, most shortcut edges are non-essential // except at the root node, which have special meaning of marking user // global objects // NOTE: However, bound function may have a shortcut edge to the bound // host object return (edgeType !== 'weak' && (edgeType !== 'shortcut' || nodeIndex === rootNodeIndex)); } function isFiberNodeDeletionsEdge(edge) { if (!edge || !edge.fromNode || !edge.toNode) { return false; } if (!isFiberNode(edge.fromNode)) { return false; } return edge.name_or_index === 'deletions'; } function isBlinkRootNode(node) { if (!node || !node.name) { return false; } return (node.type === 'synthetic' && (node.name === 'Blink cross-thread roots' || node.name === 'Blink roots')); } function isPendingActivityNode(node) { if (!node || !node.name) { return false; } if (node.type !== 'synthetic' && node.type !== 'native') { return false; } return node.name === 'Pending activities'; } const htmlElementRegex = /^HTML.*Element$/; const svgElementRegex = /^SVG.*Element$/; const htmlCollectionRegex = /^HTML.*Collection$/; const cssElementRegex = /^CSS/; const styleSheetRegex = /StyleSheet/; const newDOMNodeRegex = /^<[a-zA-Z]+.*>$/; // special DOM element names that are not // included in the previous regex definitions const domElementSpecialNames = new Set([ 'DOMTokenList', 'HTMLDocument', 'InternalNode', 'Text', 'XMLDocument', ]); // check the node against a curated list of known HTML Elements // the list may be incomplete function isDOMNodeIncomplete(node) { if (node.type !== 'native') { return false; } let name = node.name; const detachedPrefix = 'Detached '; if (name.startsWith(detachedPrefix)) { name = name.substring(detachedPrefix.length); } name = name.trim(); return (htmlElementRegex.test(name) || svgElementRegex.test(name) || cssElementRegex.test(name) || styleSheetRegex.test(name) || htmlCollectionRegex.test(name) || domElementSpecialNames.has(name) || newDOMNodeRegex.test(name)); } function isXMLDocumentNode(node) { return node.type === 'native' && node.name === 'XMLDocument'; } function isHTMLDocumentNode(node) { return node.type === 'native' && node.name === 'HTMLDocument'; } function isDOMTextNode(node) { return node.type === 'native' && node.name === 'Text'; } // check if this is a [C++ roots] (synthetic) node function isCppRootsNode(node) { return node.name === 'C++ roots' && node.type === 'synthetic'; } function isRootNode(node, opt = {}) { if (!node) { return false; } // consider Hermes snapshot GC roots if (Config_1.default.jsEngine === 'hermes') { return node.name === '(GC roots)' || node.name === '(GC Roots)'; } if (node.id === 0 || node.id === 1) { return true; } // the window object if (node.type === 'native' && node.name.indexOf('Window') === 0) { return true; } if (node.type === 'synthetic' && node.name === '(GC roots)') { return true; } if (!opt.excludeBlinkRoot && isBlinkRootNode(node)) { return true; } if (!opt.excludePendingActivity && isPendingActivityNode(node)) { return true; } return false; } // in Hermes engine, directProp edge is a shortcut reference // and is less useful for debugging leak trace const directPropRegex = /^directProp\d+$/; function isDirectPropEdge(edge) { return directPropRegex.test(`${edge.name_or_index}`); } function isReturnEdge(edge) { if (!edge) { return false; } if (typeof edge.name_or_index !== 'string') { return false; } return edge.name_or_index.startsWith('return'); } function isReactPropsEdge(edge) { if (!edge) { return false; } if (typeof edge.name_or_index !== 'string') { return false; } return edge.name_or_index.startsWith('__reactProps$'); } function isReactFiberEdge(edge) { if (!edge) { return false; } if (typeof edge.name_or_index !== 'string') { return false; } return edge.name_or_index.startsWith('__reactFiber$'); } function hasReactEdges(node) { if (!node) { return false; } let ret = false; node.forEachReference((edge) => { if (isReactFiberEdge(edge) || isReactPropsEdge(edge)) { ret = true; } return { stop: true }; }); return ret; } // HostRoot's stateNode should be a FiberRootNode function isHostRoot(node) { if (!isFiberNode(node)) { return false; } const stateNode = getToNodeByEdge(node, 'stateNode', 'property'); return !!stateNode && stateNode.name === 'FiberRootNode'; } function getReactFiberNode(node, propName) { if (!node || !isFiberNode(node)) { return; } const targetNode = getToNodeByEdge(node, propName, 'property'); return isFiberNode(targetNode) ? targetNode : void 0; } // check if the current node's parent has the node as a child function checkIsChildOfParent(node) { const parent = getToNodeByEdge(node, 'return', 'property'); let matched = false; iterateChildFiberNodes(parent, child => { if (child.id === node.id) { matched = true; return { stop: true }; } }); return matched; } // iterate through immediate children function iterateChildFiberNodes(node, cb) { if (!isFiberNode(node)) { return; } const visited = new Set(); let cur = getReactFiberNode(node, 'child'); while (cur && isFiberNode(cur) && !visited.has(cur.id)) { const ret = cb(cur); visited.add(cur.id); if (ret && ret.stop) { break; } cur = getReactFiberNode(cur, 'sibling'); } } function iterateDescendantFiberNodes(node, iteratorCB) { if (!isFiberNode(node)) { return; } const visited = new Set(); const stack = [node]; while (stack.length > 0) { const cur = stack.pop(); if (!cur) { continue; } const ret = iteratorCB(cur); visited.add(cur.id); if (ret && ret.stop) { break; } iterateChildFiberNodes(cur, child => { if (visited.has(child.id)) { return; } stack.push(child); }); } } function getNodesIdSet(snapshot) { const set = new Set(); snapshot.nodes.forEach(node => { set.add(node.id); }); return set; } // given a set of nodes S, return a minimal subset S' where // no nodes are dominated by nodes in S function getConditionalDominatorIds(ids, snapshot, condCb) { const dominatorIds = new Set(); const fullDominatorIds = new Set(); // set all node ids applyToNodes(ids, snapshot, node => { if (condCb(node)) { dominatorIds.add(node.id); fullDominatorIds.add(node.id); } }); // traverse the dominators and remove the node // if one of it's dominators is already in the set applyToNodes(ids, snapshot, node => { const visited = new Set([node.id]); let cur = node.dominatorNode; while (cur) { if (visited.has(cur.id)) { break; } if (fullDominatorIds.has(cur.id)) { dominatorIds.delete(node.id); break; } visited.add(cur.id); cur = cur.dominatorNode; } }); return dominatorIds; } const ALTERNATE_NODE_FLAG = 0b1; const REGULAR_NODE_FLAG = 0b10; function setFiberNodeAttribute(node, flag) { if (!node || !isFiberNode(node)) { return; } node.attributes |= flag; } function hasFiberNodeAttribute(node, flag) { if (!isFiberNode(node)) { return false; } return !!(node.attributes & flag); } function setIsAlternateNode(node) { setFiberNodeAttribute(node, ALTERNATE_NODE_FLAG); } function isAlternateNode(node) { return hasFiberNodeAttribute(node, ALTERNATE_NODE_FLAG); } function setIsRegularFiberNode(node) { setFiberNodeAttribute(node, REGULAR_NODE_FLAG); } function isRegularFiberNode(node) { return hasFiberNodeAttribute(node, REGULAR_NODE_FLAG); } // The Fiber tree starts with a special type of Fiber node (HostRoot). function hasHostRoot(node) { if (node && node.is_detached) { return false; } let cur = node; const visitedIds = new Set(); const visitedNodes = new Set(); while (cur && isFiberNode(cur)) { if (cur.id == null || visitedIds.has(cur.id)) { break; } visitedNodes.add(cur); visitedIds.add(cur.id); if (isHostRoot(cur)) { return true; } cur = getReactFiberNode(cur, 'return'); } return false; } // The Fiber tree starts with a special type of Fiber node (HostRoot). // return true if the node is a mounted Fiber node function markDetachedFiberNode(node) { if (node && node.is_detached) { return false; } let cur = node; const visitedIds = new Set(); const visitedNodes = new Set(); while (cur && isFiberNode(cur)) { if (cur.id == null || visitedIds.has(cur.id)) { break; } visitedNodes.add(cur); // if a Fiber node whose dominator is neither root nor // another Fiber node, then consider it as detached Fiber node if (cur.dominatorNode && cur.dominatorNode.id !== 1) { if (isDOMNodeIncomplete(cur.dominatorNode) && !isDetachedDOMNode(cur.dominatorNode)) { // skip the direct marking of detached DOM nodes here // if the Fiber Node is dominated by an attached DOM element } else if (!isFiberNode(cur.dominatorNode)) { cur.markAsDetached(); } } visitedIds.add(cur.id); if (isHostRoot(cur)) { return true; } cur = getReactFiberNode(cur, 'return'); } for (const visitedNode of visitedNodes) { visitedNode.markAsDetached(); } return false; } function filterNodesInPlace(idSet, snapshot, cb) { const ids = Array.from(idSet); for (const id of ids) { const node = snapshot.getNodeById(id); if (node && !cb(node, snapshot)) { idSet.delete(id); } } } function applyToNodes(idSet, snapshot, cb, options = {}) { let ids = Array.from(idSet); if (options.shuffle) { ids.sort(() => Math.random() - 0.5); } else if (options.reverse) { ids = ids.reverse(); } for (const id of ids) { const node = snapshot.getNodeById(id); if (!node) { if (Config_1.default.verbose) { Console_1.default.warning(`node @${id} is not found`); } return; } cb(node, snapshot); } } function checkScenarioInstance(s) { if (typeof s !== 'object' || typeof s.url !== 'function' || (s.action && typeof s.action !== 'function') || (s.back && typeof s.back !== 'function') || (s.repeat && typeof s.repeat !== 'function') || (s.isPageLoaded && typeof s.isPageLoaded !== 'function') || (s.leakFilter && typeof s.leakFilter !== 'function') || (s.beforeLeakFilter && typeof s.beforeLeakFilter !== 'function') || (s.beforeInitialPageLoad && typeof s.beforeInitialPageLoad !== 'function') || (s.setup && typeof s.setup !== 'function')) { throw new Error('Invalid scenario'); } return s; } function loadLeakFilter(filename) { const filepath = resolveFilePath(filename); if (!filepath || !fs_1.default.existsSync(filepath)) { // add a throw to silent the type error throw haltOrThrow(`Leak filter definition file doesn't exist: ${filepath}`); } try { // eslint-disable-next-line @typescript-eslint/no-var-requires let filter = require(filepath); if (typeof filter === 'function') { return { leakFilter: filter }; } filter = (filter === null || filter === void 0 ? void 0 : filter.default) || filter; if (typeof filter === 'function') { return { leakFilter: filter }; } if (typeof (filter === null || filter === void 0 ? void 0 : filter.leakFilter) === 'function' || typeof (filter === null || filter === void 0 ? void 0 : filter.retainerReferenceFilter) === 'function') { return filter; } throw haltOrThrow(`Invalid leak filter in ${filepath}`); } catch (ex) { throw haltOrThrow('Invalid leak filter definition file: ' + filename); } } function loadScenario(filename) { const filepath = resolveFilePath(filename); if (!filepath || !fs_1.default.existsSync(filepath)) { // add a throw to silent the type error throw haltOrThrow(`Scenario file doesn't exist: ${filepath}`); } let scenario; try { scenario = require(filepath); scenario = checkScenarioInstance(scenario); if (scenario.name == null) { scenario.name = () => path_1.default.basename(filename); } return scenario; } catch (ex) { throw haltOrThrow('Invalid scenario file: ' + filename); } } function getScenarioName(scenario) { if (!scenario.name) { return Constant_1.default.namePrefixForScenarioFromFile; } if (Constant_1.default.namePrefixForScenarioFromFile.length > 0) { return Constant_1.default.namePrefixForScenarioFromFile + '-' + scenario.name(); } return scenario.name(); } function handleSnapshotError(e) { haltOrThrow(e, { primaryMessageToPrint: 'Error parsing heap snapshot', secondaryMessageToPrint: 'Please pass in a valid heap snapshot file', }); } function getSnapshotFromFile(filename, options) { return __awaiter(this, void 0, void 0, function* () { const heapConfig = Config_1.default.heapConfig; if (heapConfig && heapConfig.currentHeapFile === filename && heapConfig.currentHeap) { return heapConfig.currentHeap; } Console_1.default.overwrite('parsing ' + filename + ' ...'); let ret = null; try { ret = yield HeapParser_1.default.parse(filename, options); } catch (e) { handleSnapshotError(getError(e)); } Console_1.default.flush(); return ret; }); } function getSnapshotNodeIdsFromFile(filename, options) { return __awaiter(this, void 0, void 0, function* () { Console_1.default.overwrite('lightweight parsing ' + filename + ' ...'); let ret = new Set(); try { ret = yield HeapParser_1.default.getNodeIdsFromFile(filename, options); } catch (e) { handleSnapshotError(getError(e)); } return ret; }); } const weakMapKeyRegExp = /@(\d+)\) ->/; function getWeakMapEdgeKeyId(edge) { const name = edge.name_or_index; if (typeof name !== 'string') { return -1; } const ret = name.match(weakMapKeyRegExp); if (!ret) { return -1; } return Number(ret[1]); } function isDocumentDOMTreesRoot(node) { if (!node) { return false; } return node.type === 'synthetic' && node.name === '(Document DOM trees)'; } function getEdgeByNameAndType(node, edgeName, type) { if (!node) { return null; } return node.findAnyReference((edge) => edge.name_or_index === edgeName && (type === void 0 || edge.type === type)); } function getEdgeStartsWithName(node, prefix) { if (!node) { return null; } return node.findAnyReference(edge => typeof edge.name_or_index === 'string' && edge.name_or_index.startsWith(prefix)); } function isStringNode(node) { return (0, HeapNode_1.isHeapStringType)(node.type); } function isSlicedStringNode(node) { return node.type === 'sliced string'; } function getStringNodeValue(node) { var _a, _b, _c; if (!node) { return ''; } if (node.type === 'concatenated string') { const firstNode = (_a = getEdgeByNameAndType(node, 'first')) === null || _a === void 0 ? void 0 : _a.toNode; const secondNode = (_b = getEdgeByNameAndType(node, 'second')) === null || _b === void 0 ? void 0 : _b.toNode; return getStringNodeValue(firstNode) + getStringNodeValue(secondNode); } if (isSlicedStringNode(node)) { const parentNode = (_c = getEdgeByNameAndType(node, 'parent')) === null || _c === void 0 ? void 0 : _c.toNode; return getStringNodeValue(parentNode); } return node.name; } function extractClosureNodeInfo(node) { let name = _extractClosureNodeInfo(node); // replace all [, ], (, and ) name = name.replace(/[[\]()]/g, ''); return name; } function _extractClosureNodeInfo(node) { if (!node) { return ''; } const name = node.name === '' ? '<anonymous>' : node.name; if (node.type !== 'closure') { return name; } // node.shared const sharedEdge = getEdgeByNameAndType(node, 'shared'); if (!sharedEdge) { return name; } // node.shared.function_data const sharedNode = sharedEdge.toNode; const functionDataEdge = getEdgeByNameAndType(sharedNode, 'function_data'); if (!functionDataEdge) { return name; } // node.shared.function_data[0] const functionDataNode = functionDataEdge.toNode; const displaynameEdge = getEdgeByNameAndType(functionDataNode, 0, 'hidden'); if (!displaynameEdge) { return name; } // extract display name const displayNameNode = displaynameEdge.toNode; if (displayNameNode.type === 'concatenated string' || displayNameNode.type === 'string' || displayNameNode.type === 'sliced string') { const str = getStringNodeValue(displayNameNode); if (str !== '') { return `${name} ${str}`; } } return name; } function extractFiberNodeInfo(node) { let name = _extractFiberNodeInfo(node); const tagName = _extractFiberNodeTagInfo(node); if (tagName) { name += ` ${tagName}`; } // simplify redundant pattern: // "(Detached )FiberNode X from X.react" -> "(Detached )FiberNode X" const detachedPrefix = 'Detached '; let prefix = ''; if (name.startsWith(detachedPrefix)) { prefix = detachedPrefix; name = name.substring(detachedPrefix.length); } const matches = name.match(/^FiberNode (\w+) \[from (\w+)\.react\]$/); if (matches && matches[1] === matches[2]) { name = `FiberNode ${matches[1]}`; } // replace all [, ], (, and ) name = name.replace(/[[\]()]/g, ''); return prefix + name; } function getSimplifiedDOMNodeName(node) { if (isDetachedDOMNode(node) || isDOMNodeIncomplete(node)) { return simplifyTagAttributes(node.name); } return node.name; } function limitStringLength(str, len) { if (str.length > len) { return str.substring(0, len) + '...'; } return str; } function simplifyTagAttributes(str, prioritizedAttributes = Config_1.default.defaultPrioritizedHTMLTagAttributes) { const outputLengthLimit = 100; const prefixEnd = str.indexOf('<'); if (prefixEnd <= 0) { return str; } try { const prefix = str.substring(0, prefixEnd); const tagStr = str.substring(prefixEnd).trim(); const parsedTag = parseHTMLTags(tagStr)[0]; if (parsedTag == null) { return limitStringLength(str, outputLengthLimit); } // Build maps for quick lookup const attrMap = new Map(parsedTag.attributes.map(attr => [attr.key, attr])); const prioritized = []; for (const key of prioritizedAttributes) { const attr = attrMap.get(key); if (attr != null) { prioritized.push(attr); attrMap.delete(key); } } const remaining = parsedTag.attributes.filter(attr => attrMap.has(attr.key)); parsedTag.attributes = [...prioritized, ...remaining]; const finalStr = prefix + serializeParsedTags([parsedTag]); return limitStringLength(finalStr, outputLengthLimit); } catch (_a) { return limitStringLength(str, outputLengthLimit); } } function parseHTMLTags(html) { const result = []; let i = 0; while (i < html.length) { if (html[i] === '<') { i++; // skip '<' // Determine if this is a closing tag let isClosing = false; if (html[i] === '/') { isClosing = true; i++; } // Extract tag name let tagName = ''; while (i < html.length && /[a-zA-Z0-9:-]/.test(html[i])) { tagName += html[i++]; } // Skip whitespace while (i < html.length && /\s/.test(html[i])) i++; // Parse attributes const attributes = []; while (i < html.length && html[i] !== '>' && html[i] !== '/') { // Extract key let key = ''; while (i < html.length && /[^\s=>]/.test(html[i])) { key += html[i++]; } // Skip whitespace while (i < html.length && /\s/.test(html[i])) i++; // Extract value let value = true; if (html[i] === '=') { i++; // skip '=' while (i < html.length && /\s/.test(html[i])) i++; if (html[i] === '"' || html[i] === "'") { const quote = html[i++]; value = ''; while (i < html.length && html[i] !== quote) { value += html[i++]; } i++; // skip closing quote } else { value = ''; while (i < html.length && /[^\s>]/.test(html[i])) { value += html[i++]; } } } if (key) { attributes.push({ key, value }); } // Skip whitespace while (i < html.length && /\s/.test(html[i])) i++; } // Check for self-closing let isSelfClosing = false; if (html[i] === '/') { isSelfClosing = true; i++; // skip '/' } // Skip '>' if (html[i] === '>') i++; const type = isClosing ? 'closing' : isSelfClosing ? 'self-closing' : 'opening'; result.push({ tagName, attributes, type }); } else { i++; } } return result; } function serializeParsedTags(tags) { return tags .map(tag => { if (tag.type === 'closing') { return `</${tag.tagName}>`; } const attrString = tag.attributes .map(({ key, value }) => { if (value === true) return key; const escaped = String(value).replace(/"/g, '&quot;'); return `${key}="${escaped}"`; }) .join(' '); const space = attrString ? ' ' : ''; return tag.type === 'self-closing' ? `<${tag.tagName}${space}${attrString}/>` : `<${tag.tagName}${space}${attrString}>`; }) .join(''); } // remove all attributes from the tag name // so Detached <div prop1="xyz" prop2="xyz" ...> // becomes Detached <div> function stripTagAttributes(str) { let result = ''; let i = 0; while (i < str.length) { const open = str.indexOf('<', i); if (open === -1) { result += str.slice(i); break; } const close = str.indexOf('>', open); if (close === -1) { result += str.slice(i); break; } // Find the tag name const space = str.indexOf(' ', open); if (space !== -1 && space < close) { const tagName = str.slice(open + 1, space); result += str.slice(i, open) + `<${tagName}>`; } else { result += str.slice(i, close + 1); } i = close + 1; } return result; } function getNumberNodeValue(node) { if (!node) { return null; } if (Config_1.default.jsEngine === 'hermes') { return +node.name; } const valueNode = getToNodeByEdge(node, 'value', 'internal'); if (!valueNode) { return null; } return +valueNode.name; } function getBooleanNodeValue(node) { if (node === null || node === void 0) { return null; } if (Config_1.default.jsEngine === 'hermes') { return node.name === 'true'; } const valueNode = getToNodeByEdge(node, 'value', 'internal'); if (valueNode === null || valueNode === void 0) { return null; } return valueNode.name === 'true'; } function _extractFiberNodeTagInfo(node) { if (!node) { return null; } if (!isFiberNode(node)) { return null; } const tagNode = getToNodeByEdge(node, 'tag', 'property'); if (!tagNode) { return null; } if (tagNode.type !== 'number') { return null; } const tagId = getNumberNodeValue(tagNode); return _getReactWorkTagName(tagId); } function getToNodeByEdge(node, propName, propType) { const edge = getEdgeByNameAndType(node, propName, propType); if (!edge) { return null; } return edge.toNode; } function getSymbolNodeValue(node) { if (!node || node.name !== 'symbol') { return null; } const nameNode = getToNodeByEdge(node, 'name'); if (!nameNode) { return null; } return nameNode.name; } function _extractFiberNodeInfo(node) { if (!node) { return ''; } const name = node.name; if (!isFiberNode(node)) { return name; } // extract FiberNode.type const typeNode = getToNodeByEdge(node, 'type', 'property'); if (!typeNode) { return name; } if (typeNode.type === 'string') { return `${name} ${typeNode.name}`; } // extract FiberNode.type.render const renderNode = getToNodeByEdge(typeNode, 'render'); if (renderNode && renderNode.name) { return `${name} ${renderNode.name}`; } // if FiberNode.type or FiberNode.elementType is a symbol let value = getSymbolNodeValue(typeNode); if (value) { return `${name} ${value}`; } const elementTypeNode = getToNodeByEdge(node, 'elementType', 'property'); value = getSymbolNodeValue(elementTypeNode); if (value) { return `${name} ${value}`; } // extract FiberNode.elementType.$$typeof const typeofNode = getToNodeByEdge(elementTypeNode, '$$typeof', 'property'); value = getSymbolNodeValue(typeofNode); if (value) { return `${name} ${value}`; } // extract FiberNode.type.displayName const displayNameNode = getToNodeByEdge(typeNode, 'displayName'); if (!displayNameNode) { return name; } if (displayNameNode.type === 'string') { return `${name} ${displayNameNode.name}`; } if (displayNameNode.type === 'concatenated string') { return `${name} ${getStringNodeValue(displayNameNode)}`; } return name; } function extractHTMLElementNodeInfo(node) { if (!node) { return ''; } const reactFiberEdge = getEdgeStartsWithName(node, '__reactFiber$'); if (!reactFiberEdge) { return node.name; } return `${node.name} ${extractFiberNodeInfo(reactFiberEdge.toNode)}`; } function hasOnlyWeakReferrers(node) { const referrer = node.findAnyReferrer( // shortcut references are added by JS engine // GC won't consider shortcut as a retaining edge (edge) => edge.type !== 'weak' && edge.type !== 'shortcut'); return referrer == null; } function getSnapshotSequenceFilePath() { if (!Config_1.default.useExternalSnapshot) { // load the snapshot sequence meta file from the default location return Config_1.default.snapshotSequenceFile; } if (Config_1.default.externalSnapshotDir) { // try to load the snap-seq.json file from the specified external dir const metaFile = path_1.default.join(Config_1.default.externalSnapshotDir, 'snap-seq.json'); if (fs_1.default.existsSync(metaFile)) { return metaFile; } } // otherwise return the default meta file for external snapshots return Config_1.default.externalSnapshotVisitOrderFile; } // this should be called only after exploration function loadTabsOrder(metaFile = void 0) { try { const file = metaFile != null && fs_1.default.existsSync(metaFile) ? metaFile : getSnapshotSequenceFilePath(); const content = fs_1.default.readFileSync(file, 'UTF-8'); return JSON.parse(content); } catch (_a) { throw haltOrThrow('snapshot meta data invalid or missing'); } } // return true if the heap node represents JS object or closure function isObjectNode(node) { if (isPlainJSObjectNode(node)) { return true; } return node.type === 'closure'; } // return true if the heap node represents JS object function isPlainJSObjectNode(node) { if (!node) { return false; } if (Config_1.default.jsEngine === 'hermes') { return node.name === 'Object' || node.name.startsWith('Object('); } return node.name === 'Object'; } function pathHasDetachedHTMLNode(path) { if (!path) { return false; } let p = path; while (p) { if (p.node && isDetachedDOMNode(p.node)) { return true; } p = p.next; } return false; } function pathHasEdgeWithIndex(path, idx) { if (!path || typeof idx !== 'number') { return false; } let p = path; while (p) { if (p.edge && p.edge.edgeIndex === idx) { return true; } p = p.next; } return false; } function pathHasEdgeWithName(path, edgeName) { let p = path; while (p) { if (p.edge && p.edge.name_or_index === edgeName) { return true; } p = p.next; } return false; } function pathHasNodeOrEdgeWithName(path, name) { if (name == null) { return true; } name = name.toLowerCase(); let p = path; while (p) { if (p.edge && `${p.edge.name_or_index}`.toLowerCase().includes(name)) { return true; } if (p.node && `${p.node.name}`.toLowerCase().includes(name)) { return true; } p = p.next; } return false; } function getLastNodeId(path) { if (!path) { return -1; } let p = path; while (p) { if (!p.next && p.node) { return p.node.id; } p = p.next; } return -1; } function getReadablePercent(num) { if (Number.isNaN(num)) { return `${num}%`; } const v = num * 100; let str = v.toFixed(2); if (str.endsWith('.00')) { str = str.slice(0, -3); } else if (str.endsWith('0')) { str = str.slice(0, -1); } return str + '%'; } function getReadableBytes(bytes) { let n, suffix; if (bytes === void 0 || bytes === null) { return ''; } if (bytes >= 1e12) { n = ((bytes / 1e11) | 0) / 10; suffix = 'TB'; } else if (bytes >= 1e9) { n = ((bytes / 1e8) | 0) / 10; suffix = 'GB'; } else if (bytes >= 1e6) { n = ((bytes / 1e5) | 0) / 10; suffix = 'MB'; } else if (bytes >= 1e3) { n = ((bytes / 1e2) | 0) / 10; suffix = 'KB'; } else if (bytes > 1) { n = bytes; suffix = ' bytes'; } else if (bytes >= 0) { n = bytes; suffix = ' byte'; } else { return ''; } return n + suffix; } function p1(n, divide) { return (((n * 10) / divide) | 0) / 10; } function getReadableTime(ms) { let time = ms; if (time < 1000) { return `${time}ms`; } time /= 1000; if (time < 60) { return `${p1(time, 1)}s`; } time /= 60; if (time < 60) { return `${p1(time, 1)}min`; } time /= 60; if (time < 24) { return `${p1(time, 1)}hr`; } time /= 24; return `${p1(time, 1)} days`; } function shouldShowMoreInfo(node) { if (!node || !node.name) { return false; } if (!Config_1.default.nodeToShowMoreInfo) { return false; } return Config_1.default.nodeToShowMoreInfo.has(node.name); } function isDebuggableNode(node) { if (!node) { return false; } if (node.type === 'native' && !isDetachedDOMNode(node)) { return false; } if (node.type === 'hidden' || node.type === 'array' || node.type === 'string' || node.type === 'number' || node.type === 'concatenated string' || node.type === 'sliced string' || node.type === 'code' || node.name === 'system / Context') { return false; } return true; } function throwError(error) { if (error) { error.stack; } throw error; } function callAsync(f) { const promise = f(); if (promise && promise.catch) { promise.catch((e) => { var _a; const parsedError = getError(e); Console_1.default.error(parsedError.message); Console_1.default.lowLevel((_a = parsedError.stack) !== null && _a !== void 0 ? _a : '', { annotation: Console_1.default.annotations.STACK_TRACE, }); }); } } function checkUninstalledLibrary(ex) { var _a; const stackStr = (_a = ex.stack) === null || _a === void 0 ? void 0 : _a.toString(); if (stackStr === null || stackStr === void 0 ? void 0 : stackStr.includes('cannot open shared object file')) { haltOrThrow(ex, { primaryMessageToPrint: 'Could not launch Chrome. To run MemLab on a CentOS 8 devserver, please run the following command:\n', secondaryMessageToPrint: 'sudo dnf install nss libwayland-client libwayland-egl egl-wayland libpng15 mesa-libGL atk java-atk-wrapper at-spi2-atk gtk3 libXt', }); } } function closePuppeteer(browser, pages, options = {}) { return __awaiter(this, void 0, void 0, function* () { if (Config_1.default.isLocalPuppeteer && !options.warmup) { yield Promise.all(pages.map(page => page.close())); yield browser.disconnect(); } else if (Config_1.default.skipBrowserCloseWait) { browser.close(); } else { yield browser.close(); } }); } function camelCaseToReadableString(str, options = {}) { let ret = ''; const strToProcess = str.trim(); const isUpperCase = (c) => /^[A-Z]$/.test(c); for (const c of strToProcess) { if (isUpperCase(c)) { ret += ret.length > 0 ? ' ' : ''; ret += c.toLowerCase(); } else { ret += c; } } if (options.capitalizeFirstWord && ret.length > 0) { ret = ret[0].toUpperCase() + ret.slice(1); } return ret; } // Given a file path (relative or absolute), // this function tries to resolve to a absolute path that exists // in MemLab's directories. // if nothing is found, it returns null. function resolveFilePath(file) { if (!file) { return null; } const dirs = [ Config_1.default.curDataDir, Config_1.default.persistentDataDir, Config_1.default.monoRepoDir, ]; const paths = [file].concat(dirs.map(d => path_1.default.join(d, file))); for (const p of paths) { const filepath = path_1.default.resolve(p); if (fs_1.default.existsSync(filepath)) { return filepath; } } return null; } const snapshotNamePattern = /^s(\d+)\.heapsnapshot$/; function compareSnapshotName(f1, f2) { // if file name follows the 's{\d+}.heapsnapshot' pattern // then order based on the ascending order of the number const m1 = f1.match(snapshotNamePattern); const m2 = f2.match(snapshotNamePattern); if (m1 && m2) { return parseInt(m1[1], 10) - parseInt(m2[1], 10); } // otherwise sort in alpha numeric order return f1 < f2 ? -1 : f1 === f2 ? 0 : 1; } function getSnapshotFilesInDir(dir) { try { return fs_1.default .readdirSync(dir) .filter(file => file.endsWith('.heapsnapshot')) .sort(compareSnapshotName) .map(file => path_1.default.join(dir, file)); } catch (ex) { throw __1.utils.haltOrThrow(__1.utils.getError(ex)); } } function getSnapshotFilesFromTabsOrder(options = {}) { const tabsOrder = loadTabsOrder(); const ret = []; const typesSeen = new Set(); for (let i = 0; i < tabsOrder.length; i++) { const tab = tabsOrder[i]; if (!tab.snapshot) { continue; } if (tab.type) { typesSeen.add(tab.type); } if (options.skipBeforeTabType && !typesSeen.has(options.skipBeforeTabType)) { continue; } ret.push(getSnapshotFilePath(tab)); } return ret; } // checks if the snapshots along with their meta data are complete function checkSnapshots(options = {}) { if (Config_1.default.skipSnapshot) { haltOrThrow('This command is run with `--no-snapshot`, skip snapshot check.'); } let snapshotDir; if (options.snapshotDir) { snapshotDir = options.snapshotDir; } else if (Config_1.default.useExternalSnapshot) { snapshotDir = Config_1.default.externalSnapshotDir || '<missing>'; } else { snapshotDir = FileManager_1.default.getCurDataDir({ workDir: Config_1.default.workDir }); } if (options.snapshotDir) { const snapshots = getSnapshotFilesInDir(snapshotDir); const min = options.minSnapshots || 0; if (snapshots.length < min) { __1.utils.haltOrThrow(`Directory has < ${min} snapshot files: ${options.snapshotDir}`); } return; } // check if any snapshot file is missing const tabsOrder = loadTabsOrder(); const missingTabs = Object.create(null); let miss = 0; for (const tab of tabsOrder) { if (!tab.snapshot) { continue; } const file = getSnapshotFilePath(tab); if (!fs_1.default.existsSync(file)) { ++miss; missingTabs[tab.idx] = { name: tab.name, url: tab.url, type: tab.type, }; } } if (miss > 0) { const msg = 'snapshot for the following tabs are missing:'; const printCallback = () => { Console_1.default.warning(msg); Console_1.default.table(missingTabs); }; haltOrThrow(msg + JSON.stringify(missingTabs, null, 2), { printCallback, }); } } function resolveSnapshotFilePath(snapshotFile) { const file = resolveFilePath(snapshotFile); if (!file) { throw haltOrThrow(new Error(`Error: snapshot file doesn't exist ${snapshotFile}`)); } return file; } exports.resolveSnapshotFilePath = resolveSnapshotFilePath; function getSnapshotDirForAnalysis() { const dir = Config_1.default.externalSnapshotDir; if (!dir) { throw __1.utils.haltOrThrow(new Error('external snapshot file not set')); } return dir; } function getSingleSnapshotFileForAnalysis() { let path = null; // if an external snapshot file is specified if (Config_1.default.useExternalSnapshot && Config_1.default.externalSnapshotFilePaths.length > 0) { path = Config_1.default.externalSnapshotFilePaths[Config_1.default.externalSnapshotFilePaths.length - 1]; // if running in interactive heap analysis mode } else if (Config_1.default.heapConfig && Config_1.default.heapConfig.isCliInteractiveMode && Config_1.default.heapConfig.currentHeapFile) { path = Config_1.default.heapConfig.currentHeapFile; // search for snapshot labeled as baseline, target, or final } else { path = getSnapshotFilePathWithTabType(/(final)|(target)|(baseline)/); } return resolveSnapshotFilePath(path); } function getSnapshotFilePath(tab, options = {}) { if (tab.snapshotFile) { return path_1.default.isAbsolute(tab.snapshotFile) ? tab.snapshotFile : path_1.default.join(FileManager_1.default.getCurDataDir(options), tab.snapshotFile); } const fileName = `s${tab.idx}.heapsnapshot`; if (options.workDir) { return path_1.default.join(FileManager_1.default.getCurDataDir(options), fileName); } if (!Config_1.default.useExternalSnapshot) { return path_1.default.join(Config_1.default.curDataDir, fileName); } // if we are loading snapshot from external snapshot dir if (Config_1.default.externalSnapshotDir) { return path_1.default.join(Config_1.default.externalSnapshotDir, fileName); } return Config_1.default.externalSnapshotFilePaths[tab.idx - 1]; } // eslint-disable-next-line @typescript-eslint/no-explicit-any function equalOrMatch(v1, v2) { const t1 = typeof v1; const t2 = typeof v2; if (t1 === t2) { return v1 === v2; } if (t1 === 'string' && v2 instanceof RegExp) { return v2.test(v1); } if (t2 === 'string' && v1 instanceof RegExp) { return v1.test(v2); } return false; } function getSnapshotFilePathWithTabType(type) { checkSnapshots(); const tabsOrder = loadTabsOrder(); for (let i = tabsOrder.length - 1; i >= 0; --i) { const tab = tabsOrder[i]; if (!tab.snapshot) { continue; } if (equalOrMatch(tab.type, type)) { return getSnapshotFilePath(tab); } } return null; } function isMeaningfulNode(node) { if (!node) { return false; } const nodeName