diff --git a/.github/workflows/memtest.yml b/.github/workflows/memtest.yml index d6a3f70ad..4ea9f30bd 100644 --- a/.github/workflows/memtest.yml +++ b/.github/workflows/memtest.yml @@ -57,11 +57,12 @@ jobs: with: key: docker-images-${{ runner.os }}-${{ steps.hash-docker-images.outputs.result }} - name: Test - uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3 env: E2E_GATEWAY_RUNNER: ${{matrix.e2e_runner}} + run: yarn test:mem ${{matrix.test_name}} + - name: Upload heap snapshots + if: failure() + uses: actions/upload-artifact@v4 with: - timeout_minutes: 30 - max_attempts: 5 - command: yarn test:mem ${{matrix.test_name}} - # TODO: publish heap allocation sampling profile to artifact + name: ${{matrix.test_name}}-heap-snapshots + path: e2e/${{matrix.test_name}}/*.heapsnapshot diff --git a/.gitignore b/.gitignore index 08339b767..0ee4dce98 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules/ .DS_Store dist/ +!/internal/heapsnapshot/dist/ bundle/ .yarn/* !.yarn/patches diff --git a/.prettierignore b/.prettierignore index d2c6c10b1..3b5250631 100644 --- a/.prettierignore +++ b/.prettierignore @@ -13,3 +13,4 @@ __generated__ /e2e/config-syntax-error/gateway.config.ts /e2e/config-syntax-error/custom-resolvers.ts CHANGELOG.md +/internal/heapsnapshot/dist/ diff --git a/DEPS_RESOLUTIONS_NOTES.md b/DEPS_RESOLUTIONS_NOTES.md index 3d46565a5..776a89e1d 100644 --- a/DEPS_RESOLUTIONS_NOTES.md +++ b/DEPS_RESOLUTIONS_NOTES.md @@ -14,8 +14,3 @@ Here we collect reasons and write explanations about why some resolutions or pat ### vitest-tsconfig-paths 1. Resolve tsconfig paths in modules that have been [inlined](https://vitest.dev/config/#server-deps-inline). - -### @memlab/core - -1. Define package.json#export for `@memlab/core/Types` -1. Define package.json#export for `@memlab/core/Utils` diff --git a/e2e/auto-type-merging/auto-type-merging.memtest.ts b/e2e/auto-type-merging/auto-type-merging.memtest.ts index b8e204dcd..1079b1eb6 100644 --- a/e2e/auto-type-merging/auto-type-merging.memtest.ts +++ b/e2e/auto-type-merging/auto-type-merging.memtest.ts @@ -28,10 +28,6 @@ memtest( } } `, - expectedHeavyFrame: (frame) => - // allocates a lot but all is freed confirmed through heap snapshot - frame.name === 'set' && - frame.callstack.some((frame) => frame.name === 'subschemaExecutor'), }, async () => gateway({ diff --git a/e2e/federation-subscriptions-passthrough/federation-subscriptions-passthrough.memtest.ts b/e2e/federation-subscriptions-passthrough/federation-subscriptions-passthrough.memtest.ts index 78f27af2a..cd2e015f2 100644 --- a/e2e/federation-subscriptions-passthrough/federation-subscriptions-passthrough.memtest.ts +++ b/e2e/federation-subscriptions-passthrough/federation-subscriptions-passthrough.memtest.ts @@ -23,14 +23,6 @@ describe('upstream subscriptions via websockets', () => { } } `, - expectedHeavyFrame: (frame) => - // allocates a lot but all is freed confirmed through heap snapshot - frame.name === 'set' && - frame.callstack.some( - (frame) => - frame.name.includes('stitchingInfo') || - frame.name.includes('batch'), - ), }, async () => gateway({ diff --git a/e2e/interface-additional-resolvers/interface-additional-resolvers.memtest.ts b/e2e/interface-additional-resolvers/interface-additional-resolvers.memtest.ts index fca208732..b506593f6 100644 --- a/e2e/interface-additional-resolvers/interface-additional-resolvers.memtest.ts +++ b/e2e/interface-additional-resolvers/interface-additional-resolvers.memtest.ts @@ -24,10 +24,6 @@ memtest( } } `, - expectedHeavyFrame: (frame) => - // allocates a lot but all is freed confirmed through heap snapshot - frame.name === 'set' && - frame.callstack.some((frame) => frame.name === 'createBatchingExecutor'), }, async () => await gateway({ diff --git a/internal/heapsnapshot/README.md b/internal/heapsnapshot/README.md new file mode 100644 index 000000000..f5e5d922d --- /dev/null +++ b/internal/heapsnapshot/README.md @@ -0,0 +1,5 @@ +Fork of [ChromeDevTools/devtools-frontend/[...]/heap_snapshot_worker](https://github.com/ChromeDevTools/devtools-frontend/blob/dd60dc9c8add93357dcffcfc3e2a9e5a31864413/front_end/entrypoints/heap_snapshot_worker) without the browser requirements, adapted for Node, with utilities for quick setup, parsing and analysis. + +Please make sure to build and commit any changes done inside this package to allow for "buildless" testing. + +This is the only package in the project that is used built (it's not listed in `tsconfig.json#paths`) because it uses worker threads and they need Node-ready JavaScript. diff --git a/internal/heapsnapshot/dist/HeapSnapshotLoader-CpV_0rIo.js b/internal/heapsnapshot/dist/HeapSnapshotLoader-CpV_0rIo.js new file mode 100644 index 000000000..109c75ee0 --- /dev/null +++ b/internal/heapsnapshot/dist/HeapSnapshotLoader-CpV_0rIo.js @@ -0,0 +1,4541 @@ +var HeapSnapshotLoader$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + get HeapSnapshotLoader () { return HeapSnapshotLoader; } +}); + +const HeapSnapshotProgressEvent = { + Update: "ProgressUpdate", + BrokenSnapshot: "BrokenSnapshot" +}; +const baseSystemDistance = 1e8; +const baseUnreachableDistance = baseSystemDistance * 2; +class AllocationNodeCallers { + nodesWithSingleCaller; + branchingCallers; + constructor(nodesWithSingleCaller, branchingCallers) { + this.nodesWithSingleCaller = nodesWithSingleCaller; + this.branchingCallers = branchingCallers; + } +} +class SerializedAllocationNode { + id; + name; + scriptName; + scriptId; + line; + column; + count; + size; + liveCount; + liveSize; + hasChildren; + constructor(nodeId, functionName, scriptName, scriptId, line, column, count, size, liveCount, liveSize, hasChildren) { + this.id = nodeId; + this.name = functionName; + this.scriptName = scriptName; + this.scriptId = scriptId; + this.line = line; + this.column = column; + this.count = count; + this.size = size; + this.liveCount = liveCount; + this.liveSize = liveSize; + this.hasChildren = hasChildren; + } +} +class AllocationStackFrame { + functionName; + scriptName; + scriptId; + line; + column; + constructor(functionName, scriptName, scriptId, line, column) { + this.functionName = functionName; + this.scriptName = scriptName; + this.scriptId = scriptId; + this.line = line; + this.column = column; + } +} +class Node { + id; + name; + distance; + nodeIndex; + retainedSize; + selfSize; + type; + canBeQueried; + detachedDOMTreeNode; + isAddedNotRemoved; + ignored; + constructor(id, name, distance, nodeIndex, retainedSize, selfSize, type) { + this.id = id; + this.name = name; + this.distance = distance; + this.nodeIndex = nodeIndex; + this.retainedSize = retainedSize; + this.selfSize = selfSize; + this.type = type; + this.canBeQueried = false; + this.detachedDOMTreeNode = false; + this.isAddedNotRemoved = null; + this.ignored = false; + } +} +class Edge { + name; + node; + type; + edgeIndex; + isAddedNotRemoved; + constructor(name, node, type, edgeIndex) { + this.name = name; + this.node = node; + this.type = type; + this.edgeIndex = edgeIndex; + this.isAddedNotRemoved = null; + } +} +class Aggregate { + count; + distance; + self; + maxRet; + name; + idxs; + constructor() { + } +} +class AggregateForDiff { + name; + indexes; + ids; + selfSizes; + constructor() { + this.name = ""; + this.indexes = []; + this.ids = []; + this.selfSizes = []; + } +} +class Diff { + name; + addedCount; + removedCount; + addedSize; + removedSize; + deletedIndexes; + addedIndexes; + countDelta; + sizeDelta; + constructor(name) { + this.name = name; + this.addedCount = 0; + this.removedCount = 0; + this.addedSize = 0; + this.removedSize = 0; + this.deletedIndexes = []; + this.addedIndexes = []; + } +} +class DiffForClass { + name; + addedCount; + removedCount; + addedSize; + removedSize; + deletedIndexes; + addedIndexes; + countDelta; + sizeDelta; + constructor() { + } +} +class ComparatorConfig { + fieldName1; + ascending1; + fieldName2; + ascending2; + constructor(fieldName1, ascending1, fieldName2, ascending2) { + this.fieldName1 = fieldName1; + this.ascending1 = ascending1; + this.fieldName2 = fieldName2; + this.ascending2 = ascending2; + } +} +class WorkerCommand { + callId; + disposition; + objectId; + newObjectId; + methodName; + // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration + // eslint-disable-next-line @typescript-eslint/no-explicit-any + methodArguments; + source; + constructor() { + } +} +class ItemsRange { + startPosition; + endPosition; + totalLength; + items; + constructor(startPosition, endPosition, totalLength, items) { + this.startPosition = startPosition; + this.endPosition = endPosition; + this.totalLength = totalLength; + this.items = items; + } +} +class StaticData { + nodeCount; + rootNodeIndex; + totalSize; + maxJSObjectId; + constructor(nodeCount, rootNodeIndex, totalSize, maxJSObjectId) { + this.nodeCount = nodeCount; + this.rootNodeIndex = rootNodeIndex; + this.totalSize = totalSize; + this.maxJSObjectId = maxJSObjectId; + } +} +class NodeFilter { + minNodeId; + maxNodeId; + allocationNodeId; + filterName; + constructor(minNodeId, maxNodeId) { + this.minNodeId = minNodeId; + this.maxNodeId = maxNodeId; + } + equals(o) { + return this.minNodeId === o.minNodeId && this.maxNodeId === o.maxNodeId && this.allocationNodeId === o.allocationNodeId && this.filterName === o.filterName; + } +} +class SearchConfig { + query; + caseSensitive; + isRegex; + shouldJump; + jumpBackward; + constructor(query, caseSensitive, isRegex, shouldJump, jumpBackward) { + this.query = query; + this.caseSensitive = caseSensitive; + this.isRegex = isRegex; + this.shouldJump = shouldJump; + this.jumpBackward = jumpBackward; + } + toSearchRegex(_global) { + throw new Error("Unsupported operation on search config"); + } +} +class Samples { + timestamps; + lastAssignedIds; + sizes; + constructor(timestamps, lastAssignedIds, sizes) { + this.timestamps = timestamps; + this.lastAssignedIds = lastAssignedIds; + this.sizes = sizes; + } +} +class Location { + scriptId; + lineNumber; + columnNumber; + constructor(scriptId, lineNumber, columnNumber) { + this.scriptId = scriptId; + this.lineNumber = lineNumber; + this.columnNumber = columnNumber; + } +} + +var HeapSnapshotModel = /*#__PURE__*/Object.freeze({ + __proto__: null, + Aggregate: Aggregate, + AggregateForDiff: AggregateForDiff, + AllocationNodeCallers: AllocationNodeCallers, + AllocationStackFrame: AllocationStackFrame, + ComparatorConfig: ComparatorConfig, + Diff: Diff, + DiffForClass: DiffForClass, + Edge: Edge, + HeapSnapshotProgressEvent: HeapSnapshotProgressEvent, + ItemsRange: ItemsRange, + Location: Location, + Node: Node, + NodeFilter: NodeFilter, + Samples: Samples, + SearchConfig: SearchConfig, + SerializedAllocationNode: SerializedAllocationNode, + StaticData: StaticData, + WorkerCommand: WorkerCommand, + baseSystemDistance: baseSystemDistance, + baseUnreachableDistance: baseUnreachableDistance +}); + +class AllocationProfile { + #strings; + #nextNodeId; + #functionInfos; + #idToNode; + #idToTopDownNode; + #collapsedTopNodeIdToFunctionInfo; + #traceTops; + constructor(profile, liveObjectStats) { + this.#strings = profile.strings; + this.#nextNodeId = 1; + this.#functionInfos = []; + this.#idToNode = {}; + this.#idToTopDownNode = {}; + this.#collapsedTopNodeIdToFunctionInfo = {}; + this.#traceTops = null; + this.#buildFunctionAllocationInfos(profile); + this.#buildAllocationTree(profile, liveObjectStats); + } + #buildFunctionAllocationInfos(profile) { + const strings = this.#strings; + const functionInfoFields = profile.snapshot.meta.trace_function_info_fields; + const functionNameOffset = functionInfoFields.indexOf("name"); + const scriptNameOffset = functionInfoFields.indexOf("script_name"); + const scriptIdOffset = functionInfoFields.indexOf("script_id"); + const lineOffset = functionInfoFields.indexOf("line"); + const columnOffset = functionInfoFields.indexOf("column"); + const functionInfoFieldCount = functionInfoFields.length; + const rawInfos = profile.trace_function_infos; + const infoLength = rawInfos.length; + const functionInfos = this.#functionInfos = new Array( + infoLength / functionInfoFieldCount + ); + let index = 0; + for (let i = 0; i < infoLength; i += functionInfoFieldCount) { + functionInfos[index++] = new FunctionAllocationInfo( + strings[rawInfos[i + functionNameOffset]], + strings[rawInfos[i + scriptNameOffset]], + rawInfos[i + scriptIdOffset], + rawInfos[i + lineOffset], + rawInfos[i + columnOffset] + ); + } + } + #buildAllocationTree(profile, liveObjectStats) { + const traceTreeRaw = profile.trace_tree; + const functionInfos = this.#functionInfos; + const idToTopDownNode = this.#idToTopDownNode; + const traceNodeFields = profile.snapshot.meta.trace_node_fields; + const nodeIdOffset = traceNodeFields.indexOf("id"); + const functionInfoIndexOffset = traceNodeFields.indexOf( + "function_info_index" + ); + const allocationCountOffset = traceNodeFields.indexOf("count"); + const allocationSizeOffset = traceNodeFields.indexOf("size"); + const childrenOffset = traceNodeFields.indexOf("children"); + const nodeFieldCount = traceNodeFields.length; + function traverseNode(rawNodeArray, nodeOffset, parent) { + const functionInfo = functionInfos[rawNodeArray[nodeOffset + functionInfoIndexOffset]]; + const id = rawNodeArray[nodeOffset + nodeIdOffset]; + const stats = liveObjectStats[id]; + const liveCount = stats ? stats.count : 0; + const liveSize = stats ? stats.size : 0; + const result = new TopDownAllocationNode( + id, + functionInfo, + rawNodeArray[nodeOffset + allocationCountOffset], + rawNodeArray[nodeOffset + allocationSizeOffset], + liveCount, + liveSize, + parent + ); + idToTopDownNode[id] = result; + functionInfo.addTraceTopNode(result); + const rawChildren = rawNodeArray[nodeOffset + childrenOffset]; + for (let i = 0; i < rawChildren.length; i += nodeFieldCount) { + result.children.push(traverseNode(rawChildren, i, result)); + } + return result; + } + return traverseNode(traceTreeRaw, 0, null); + } + serializeTraceTops() { + if (this.#traceTops) { + return this.#traceTops; + } + const result = this.#traceTops = []; + const functionInfos = this.#functionInfos; + for (let i = 0; i < functionInfos.length; i++) { + const info = functionInfos[i]; + if (info.totalCount === 0) { + continue; + } + const nodeId = this.#nextNodeId++; + const isRoot = i === 0; + result.push( + this.#serializeNode( + nodeId, + info, + info.totalCount, + info.totalSize, + info.totalLiveCount, + info.totalLiveSize, + !isRoot + ) + ); + this.#collapsedTopNodeIdToFunctionInfo[nodeId] = info; + } + result.sort(function(a, b) { + return b.size - a.size; + }); + return result; + } + serializeCallers(nodeId) { + let node = this.#ensureBottomUpNode(nodeId); + const nodesWithSingleCaller = []; + while (node.callers().length === 1) { + node = node.callers()[0]; + nodesWithSingleCaller.push(this.#serializeCaller(node)); + } + const branchingCallers = []; + const callers = node.callers(); + for (let i = 0; i < callers.length; i++) { + branchingCallers.push(this.#serializeCaller(callers[i])); + } + return new AllocationNodeCallers( + nodesWithSingleCaller, + branchingCallers + ); + } + serializeAllocationStack(traceNodeId) { + let node = this.#idToTopDownNode[traceNodeId]; + const result = []; + while (node) { + const functionInfo = node.functionInfo; + result.push( + new AllocationStackFrame( + functionInfo.functionName, + functionInfo.scriptName, + functionInfo.scriptId, + functionInfo.line, + functionInfo.column + ) + ); + node = node.parent; + } + return result; + } + traceIds(allocationNodeId) { + return this.#ensureBottomUpNode(allocationNodeId).traceTopIds; + } + #ensureBottomUpNode(nodeId) { + let node = this.#idToNode[nodeId]; + if (!node) { + const functionInfo = this.#collapsedTopNodeIdToFunctionInfo[nodeId]; + node = functionInfo.bottomUpRoot(); + delete this.#collapsedTopNodeIdToFunctionInfo[nodeId]; + this.#idToNode[nodeId] = node; + } + return node; + } + #serializeCaller(node) { + const callerId = this.#nextNodeId++; + this.#idToNode[callerId] = node; + return this.#serializeNode( + callerId, + node.functionInfo, + node.allocationCount, + node.allocationSize, + node.liveCount, + node.liveSize, + node.hasCallers() + ); + } + #serializeNode(nodeId, functionInfo, count, size, liveCount, liveSize, hasChildren) { + return new SerializedAllocationNode( + nodeId, + functionInfo.functionName, + functionInfo.scriptName, + functionInfo.scriptId, + functionInfo.line, + functionInfo.column, + count, + size, + liveCount, + liveSize, + hasChildren + ); + } +} +class TopDownAllocationNode { + id; + functionInfo; + allocationCount; + allocationSize; + liveCount; + liveSize; + parent; + children; + constructor(id, functionInfo, count, size, liveCount, liveSize, parent) { + this.id = id; + this.functionInfo = functionInfo; + this.allocationCount = count; + this.allocationSize = size; + this.liveCount = liveCount; + this.liveSize = liveSize; + this.parent = parent; + this.children = []; + } +} +class BottomUpAllocationNode { + functionInfo; + allocationCount; + allocationSize; + liveCount; + liveSize; + traceTopIds; + #callersInternal; + constructor(functionInfo) { + this.functionInfo = functionInfo; + this.allocationCount = 0; + this.allocationSize = 0; + this.liveCount = 0; + this.liveSize = 0; + this.traceTopIds = []; + this.#callersInternal = []; + } + addCaller(traceNode) { + const functionInfo = traceNode.functionInfo; + let result; + for (let i = 0; i < this.#callersInternal.length; i++) { + const caller = this.#callersInternal[i]; + if (caller.functionInfo === functionInfo) { + result = caller; + break; + } + } + if (!result) { + result = new BottomUpAllocationNode(functionInfo); + this.#callersInternal.push(result); + } + return result; + } + callers() { + return this.#callersInternal; + } + hasCallers() { + return this.#callersInternal.length > 0; + } +} +class FunctionAllocationInfo { + functionName; + scriptName; + scriptId; + line; + column; + totalCount; + totalSize; + totalLiveCount; + totalLiveSize; + #traceTops; + #bottomUpTree; + constructor(functionName, scriptName, scriptId, line, column) { + this.functionName = functionName; + this.scriptName = scriptName; + this.scriptId = scriptId; + this.line = line; + this.column = column; + this.totalCount = 0; + this.totalSize = 0; + this.totalLiveCount = 0; + this.totalLiveSize = 0; + this.#traceTops = []; + } + addTraceTopNode(node) { + if (node.allocationCount === 0) { + return; + } + this.#traceTops.push(node); + this.totalCount += node.allocationCount; + this.totalSize += node.allocationSize; + this.totalLiveCount += node.liveCount; + this.totalLiveSize += node.liveSize; + } + bottomUpRoot() { + if (!this.#traceTops.length) { + return null; + } + if (!this.#bottomUpTree) { + this.#buildAllocationTraceTree(); + } + return this.#bottomUpTree; + } + #buildAllocationTraceTree() { + this.#bottomUpTree = new BottomUpAllocationNode(this); + for (let i = 0; i < this.#traceTops.length; i++) { + let node = this.#traceTops[i]; + let bottomUpNode = this.#bottomUpTree; + const count = node.allocationCount; + const size = node.allocationSize; + const liveCount = node.liveCount; + const liveSize = node.liveSize; + const traceId = node.id; + while (true) { + bottomUpNode.allocationCount += count; + bottomUpNode.allocationSize += size; + bottomUpNode.liveCount += liveCount; + bottomUpNode.liveSize += liveSize; + bottomUpNode.traceTopIds.push(traceId); + node = node.parent; + if (node === null) { + break; + } + bottomUpNode = bottomUpNode.addCaller(node); + } + } + } +} + +var AllocationProfile$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + AllocationProfile: AllocationProfile, + BottomUpAllocationNode: BottomUpAllocationNode, + FunctionAllocationInfo: FunctionAllocationInfo, + TopDownAllocationNode: TopDownAllocationNode +}); + +function createExpandableBigUint32Array() { + return new ExpandableBigUint32ArrayImpl(); +} +function createFixedBigUint32Array(length, maxLengthForTesting) { + try { + if (maxLengthForTesting !== void 0 && length > maxLengthForTesting) ; + return new BasicBigUint32ArrayImpl(length); + } catch { + return new SplitBigUint32ArrayImpl(length, maxLengthForTesting); + } +} +class BasicBigUint32ArrayImpl extends Uint32Array { + getValue(index) { + return this[index]; + } + setValue(index, value) { + this[index] = value; + } + asUint32ArrayOrFail() { + return this; + } + asArrayOrFail() { + throw new Error("Not an array"); + } +} +class SplitBigUint32ArrayImpl { + #data; + #partLength; + length; + constructor(length, maxLengthForTesting) { + this.#data = []; + this.length = length; + let partCount = 1; + while (true) { + partCount *= 2; + this.#partLength = Math.ceil(length / partCount); + try { + if (maxLengthForTesting !== void 0 && this.#partLength > maxLengthForTesting) { + throw new RangeError(); + } + for (let i = 0; i < partCount; ++i) { + this.#data[i] = new Uint32Array(this.#partLength); + } + return; + } catch (e) { + if (this.#partLength < 1e6) { + throw e; + } + } + } + } + getValue(index) { + if (index >= 0 && index < this.length) { + const partLength = this.#partLength; + return this.#data[Math.floor(index / partLength)][index % partLength]; + } + return this.#data[0][-1]; + } + setValue(index, value) { + if (index >= 0 && index < this.length) { + const partLength = this.#partLength; + this.#data[Math.floor(index / partLength)][index % partLength] = value; + } + } + asUint32ArrayOrFail() { + throw new Error("Not a Uint32Array"); + } + asArrayOrFail() { + throw new Error("Not an array"); + } +} +class ExpandableBigUint32ArrayImpl extends Array { + getValue(index) { + return this[index]; + } + setValue(index, value) { + this[index] = value; + } + asUint32ArrayOrFail() { + throw new Error("Not a Uint32Array"); + } + asArrayOrFail() { + return this; + } +} +function createBitVector(lengthOrBuffer) { + return new BitVectorImpl(lengthOrBuffer); +} +class BitVectorImpl extends Uint8Array { + constructor(lengthOrBuffer) { + if (typeof lengthOrBuffer === "number") { + super(Math.ceil(lengthOrBuffer / 8)); + } else { + super(lengthOrBuffer); + } + } + getBit(index) { + const value = this[index >> 3] & 1 << (index & 7); + return value !== 0; + } + setBit(index) { + this[index >> 3] |= 1 << (index & 7); + } + clearBit(index) { + this[index >> 3] &= ~(1 << (index & 7)); + } + previous(index) { + while (index !== index >> 3 << 3) { + --index; + if (this.getBit(index)) { + return index; + } + } + let byteIndex = (index >> 3) - 1; + while (byteIndex >= 0 && this[byteIndex] === 0) { + --byteIndex; + } + if (byteIndex < 0) { + return -1; + } + for (index = (byteIndex << 3) + 7; index >= byteIndex << 3; --index) { + if (this.getBit(index)) { + return index; + } + } + throw new Error("Unreachable"); + } +} + +function swap(array, i1, i2) { + const temp = array[i1]; + array[i1] = array[i2]; + array[i2] = temp; +} +function partition(array, comparator, left, right, pivotIndex) { + const pivotValue = array[pivotIndex]; + swap(array, right, pivotIndex); + let storeIndex = left; + for (let i = left; i < right; ++i) { + if (comparator(array[i], pivotValue) < 0) { + swap(array, storeIndex, i); + ++storeIndex; + } + } + swap(array, right, storeIndex); + return storeIndex; +} +function quickSortRange(array, comparator, left, right, sortWindowLeft, sortWindowRight) { + if (right <= left) { + return; + } + const pivotIndex = Math.floor(Math.random() * (right - left)) + left; + const pivotNewIndex = partition(array, comparator, left, right, pivotIndex); + if (sortWindowLeft < pivotNewIndex) { + quickSortRange( + array, + comparator, + left, + pivotNewIndex - 1, + sortWindowLeft, + sortWindowRight + ); + } + if (pivotNewIndex < sortWindowRight) { + quickSortRange( + array, + comparator, + pivotNewIndex + 1, + right, + sortWindowLeft, + sortWindowRight + ); + } +} +function sortRange(array, comparator, leftBound, rightBound, sortWindowLeft, sortWindowRight) { + if (leftBound === 0 && rightBound === array.length - 1 && sortWindowLeft === 0 && sortWindowRight >= rightBound) { + array.sort(comparator); + } else { + quickSortRange( + array, + comparator, + leftBound, + rightBound, + sortWindowLeft, + sortWindowRight + ); + } + return array; +} +const DEFAULT_COMPARATOR = (a, b) => { + return a < b ? -1 : a > b ? 1 : 0; +}; +function lowerBound(array, needle, comparator, left, right) { + let l = 0; + let r = array.length; + while (l < r) { + const m = l + r >> 1; + if (comparator(needle, array[m]) > 0) { + l = m + 1; + } else { + r = m; + } + } + return r; +} + +const SPECIAL_REGEX_CHARACTERS = "^[]{}()\\.^$*+?|-,"; +const regexSpecialCharacters = function() { + return SPECIAL_REGEX_CHARACTERS; +}; +const createPlainTextSearchRegex = function(query, flags) { + let regex = ""; + for (let i = 0; i < query.length; ++i) { + const c = query.charAt(i); + if (regexSpecialCharacters().indexOf(c) !== -1) { + regex += "\\"; + } + regex += c; + } + return new RegExp(regex, flags); +}; + +class Multimap { + map = /* @__PURE__ */ new Map(); + set(key, value) { + let set = this.map.get(key); + if (!set) { + set = /* @__PURE__ */ new Set(); + this.map.set(key, set); + } + set.add(value); + } + get(key) { + return this.map.get(key) || /* @__PURE__ */ new Set(); + } + has(key) { + return this.map.has(key); + } + hasValue(key, value) { + const set = this.map.get(key); + if (!set) { + return false; + } + return set.has(value); + } + get size() { + return this.map.size; + } + delete(key, value) { + const values = this.get(key); + if (!values) { + return false; + } + const result = values.delete(value); + if (!values.size) { + this.map.delete(key); + } + return result; + } + deleteAll(key) { + this.map.delete(key); + } + keysArray() { + return [...this.map.keys()]; + } + keys() { + return this.map.keys(); + } + valuesArray() { + const result = []; + for (const set of this.map.values()) { + result.push(...set.values()); + } + return result; + } + clear() { + this.map.clear(); + } +} + +function withResolvers() { + if (Promise.withResolvers) return Promise.withResolvers(); + let resolveFn; + let rejectFn; + const promise = new Promise(function deferredPromiseExecutor(resolve, reject) { + resolveFn = resolve; + rejectFn = reject; + }); + return { + promise, + get resolve() { + return resolveFn; + }, + get reject() { + return rejectFn; + } + }; +} + +class HeapSnapshotEdge { + snapshot; + edges; + edgeIndex; + constructor(snapshot, edgeIndex) { + this.snapshot = snapshot; + this.edges = snapshot.containmentEdges; + this.edgeIndex = edgeIndex || 0; + } + clone() { + return new HeapSnapshotEdge(this.snapshot, this.edgeIndex); + } + hasStringName() { + throw new Error("Not implemented"); + } + name() { + throw new Error("Not implemented"); + } + node() { + return this.snapshot.createNode(this.nodeIndex()); + } + nodeIndex() { + if (typeof this.snapshot.edgeToNodeOffset === "undefined") { + throw new Error("edgeToNodeOffset is undefined"); + } + return this.edges.getValue(this.edgeIndex + this.snapshot.edgeToNodeOffset); + } + toString() { + return "HeapSnapshotEdge: " + this.name(); + } + type() { + return this.snapshot.edgeTypes[this.rawType()]; + } + itemIndex() { + return this.edgeIndex; + } + serialize() { + return new Edge( + this.name(), + this.node().serialize(), + this.type(), + this.edgeIndex + ); + } + rawType() { + if (typeof this.snapshot.edgeTypeOffset === "undefined") { + throw new Error("edgeTypeOffset is undefined"); + } + return this.edges.getValue(this.edgeIndex + this.snapshot.edgeTypeOffset); + } + isInternal() { + throw new Error("Not implemented"); + } + isInvisible() { + throw new Error("Not implemented"); + } + isWeak() { + throw new Error("Not implemented"); + } + getValueForSorting(_fieldName) { + throw new Error("Not implemented"); + } + nameIndex() { + throw new Error("Not implemented"); + } +} +class HeapSnapshotNodeIndexProvider { + #node; + constructor(snapshot) { + this.#node = snapshot.createNode(); + } + itemForIndex(index) { + this.#node.nodeIndex = index; + return this.#node; + } +} +class HeapSnapshotEdgeIndexProvider { + #edge; + constructor(snapshot) { + this.#edge = snapshot.createEdge(0); + } + itemForIndex(index) { + this.#edge.edgeIndex = index; + return this.#edge; + } +} +class HeapSnapshotRetainerEdgeIndexProvider { + #retainerEdge; + constructor(snapshot) { + this.#retainerEdge = snapshot.createRetainingEdge(0); + } + itemForIndex(index) { + this.#retainerEdge.setRetainerIndex(index); + return this.#retainerEdge; + } +} +class HeapSnapshotEdgeIterator { + #sourceNode; + edge; + constructor(node) { + this.#sourceNode = node; + this.edge = node.snapshot.createEdge(node.edgeIndexesStart()); + } + hasNext() { + return this.edge.edgeIndex < this.#sourceNode.edgeIndexesEnd(); + } + item() { + return this.edge; + } + next() { + if (typeof this.edge.snapshot.edgeFieldsCount === "undefined") { + throw new Error("edgeFieldsCount is undefined"); + } + this.edge.edgeIndex += this.edge.snapshot.edgeFieldsCount; + } +} +class HeapSnapshotRetainerEdge { + snapshot; + #retainerIndexInternal; + #globalEdgeIndex; + #retainingNodeIndex; + #edgeInstance; + #nodeInstance; + constructor(snapshot, retainerIndex) { + this.snapshot = snapshot; + this.setRetainerIndex(retainerIndex); + } + clone() { + return new HeapSnapshotRetainerEdge(this.snapshot, this.retainerIndex()); + } + hasStringName() { + return this.edge().hasStringName(); + } + name() { + return this.edge().name(); + } + nameIndex() { + return this.edge().nameIndex(); + } + node() { + return this.nodeInternal(); + } + nodeIndex() { + if (typeof this.#retainingNodeIndex === "undefined") { + throw new Error("retainingNodeIndex is undefined"); + } + return this.#retainingNodeIndex; + } + retainerIndex() { + return this.#retainerIndexInternal; + } + setRetainerIndex(retainerIndex) { + if (retainerIndex === this.#retainerIndexInternal) { + return; + } + if (!this.snapshot.retainingEdges || !this.snapshot.retainingNodes) { + throw new Error( + "Snapshot does not contain retaining edges or retaining nodes" + ); + } + this.#retainerIndexInternal = retainerIndex; + this.#globalEdgeIndex = this.snapshot.retainingEdges[retainerIndex]; + this.#retainingNodeIndex = this.snapshot.retainingNodes[retainerIndex]; + this.#edgeInstance = null; + this.#nodeInstance = null; + } + set edgeIndex(edgeIndex) { + this.setRetainerIndex(edgeIndex); + } + nodeInternal() { + if (!this.#nodeInstance) { + this.#nodeInstance = this.snapshot.createNode(this.#retainingNodeIndex); + } + return this.#nodeInstance; + } + edge() { + if (!this.#edgeInstance) { + this.#edgeInstance = this.snapshot.createEdge(this.#globalEdgeIndex); + } + return this.#edgeInstance; + } + toString() { + return this.edge().toString(); + } + itemIndex() { + return this.#retainerIndexInternal; + } + serialize() { + const node = this.node(); + const serializedNode = node.serialize(); + serializedNode.distance = this.#distance(); + serializedNode.ignored = this.snapshot.isNodeIgnoredInRetainersView( + node.nodeIndex + ); + return new Edge( + this.name(), + serializedNode, + this.type(), + this.#globalEdgeIndex + ); + } + type() { + return this.edge().type(); + } + isInternal() { + return this.edge().isInternal(); + } + getValueForSorting(fieldName) { + if (fieldName === "!edgeDistance") { + return this.#distance(); + } + throw new Error("Invalid field name"); + } + #distance() { + if (this.snapshot.isEdgeIgnoredInRetainersView(this.#globalEdgeIndex)) { + return baseUnreachableDistance; + } + return this.node().distanceForRetainersView(); + } +} +class HeapSnapshotRetainerEdgeIterator { + #retainersEnd; + retainer; + constructor(retainedNode) { + const snapshot = retainedNode.snapshot; + const retainedNodeOrdinal = retainedNode.ordinal(); + if (!snapshot.firstRetainerIndex) { + throw new Error("Snapshot does not contain firstRetainerIndex"); + } + const retainerIndex = snapshot.firstRetainerIndex[retainedNodeOrdinal]; + this.#retainersEnd = snapshot.firstRetainerIndex[retainedNodeOrdinal + 1]; + this.retainer = snapshot.createRetainingEdge(retainerIndex); + } + hasNext() { + return this.retainer.retainerIndex() < this.#retainersEnd; + } + item() { + return this.retainer; + } + next() { + this.retainer.setRetainerIndex(this.retainer.retainerIndex() + 1); + } +} +class HeapSnapshotNode { + snapshot; + nodeIndex; + constructor(snapshot, nodeIndex) { + this.snapshot = snapshot; + this.nodeIndex = nodeIndex || 0; + } + distance() { + return this.snapshot.nodeDistances[this.nodeIndex / this.snapshot.nodeFieldCount]; + } + distanceForRetainersView() { + return this.snapshot.getDistanceForRetainersView(this.nodeIndex); + } + className() { + return this.snapshot.strings[this.classIndex()]; + } + classIndex() { + return this.#detachednessAndClassIndex() >>> SHIFT_FOR_CLASS_INDEX; + } + // Returns a key which can uniquely describe both the class name for this node + // and its Location, if relevant. These keys are meant to be cheap to produce, + // so that building aggregates is fast. These keys are NOT the same as the + // keys exposed to the frontend by functions such as aggregatesWithFilter and + // aggregatesForDiff. + classKeyInternal() { + if (this.rawType() !== this.snapshot.nodeObjectType) { + return this.classIndex(); + } + const location = this.snapshot.getLocation(this.nodeIndex); + return location ? `${location.scriptId},${location.lineNumber},${location.columnNumber},${this.className()}` : this.classIndex(); + } + setClassIndex(index) { + let value = this.#detachednessAndClassIndex(); + value &= BITMASK_FOR_DOM_LINK_STATE; + value |= index << SHIFT_FOR_CLASS_INDEX; + this.#setDetachednessAndClassIndex(value); + if (this.classIndex() !== index) { + throw new Error("String index overflow"); + } + } + dominatorIndex() { + const nodeFieldCount = this.snapshot.nodeFieldCount; + return this.snapshot.dominatorsTree[this.nodeIndex / this.snapshot.nodeFieldCount] * nodeFieldCount; + } + edges() { + return new HeapSnapshotEdgeIterator(this); + } + edgesCount() { + return (this.edgeIndexesEnd() - this.edgeIndexesStart()) / this.snapshot.edgeFieldsCount; + } + id() { + throw new Error("Not implemented"); + } + rawName() { + return this.snapshot.strings[this.rawNameIndex()]; + } + isRoot() { + return this.nodeIndex === this.snapshot.rootNodeIndex; + } + isUserRoot() { + throw new Error("Not implemented"); + } + isHidden() { + throw new Error("Not implemented"); + } + isArray() { + throw new Error("Not implemented"); + } + isSynthetic() { + throw new Error("Not implemented"); + } + isDocumentDOMTreesRoot() { + throw new Error("Not implemented"); + } + name() { + return this.rawName(); + } + retainedSize() { + return this.snapshot.retainedSizes[this.ordinal()]; + } + retainers() { + return new HeapSnapshotRetainerEdgeIterator(this); + } + retainersCount() { + const snapshot = this.snapshot; + const ordinal = this.ordinal(); + return snapshot.firstRetainerIndex[ordinal + 1] - snapshot.firstRetainerIndex[ordinal]; + } + selfSize() { + const snapshot = this.snapshot; + return snapshot.nodes.getValue( + this.nodeIndex + snapshot.nodeSelfSizeOffset + ); + } + type() { + return this.snapshot.nodeTypes[this.rawType()]; + } + traceNodeId() { + const snapshot = this.snapshot; + return snapshot.nodes.getValue( + this.nodeIndex + snapshot.nodeTraceNodeIdOffset + ); + } + itemIndex() { + return this.nodeIndex; + } + serialize() { + return new Node( + this.id(), + this.name(), + this.distance(), + this.nodeIndex, + this.retainedSize(), + this.selfSize(), + this.type() + ); + } + rawNameIndex() { + const snapshot = this.snapshot; + return snapshot.nodes.getValue(this.nodeIndex + snapshot.nodeNameOffset); + } + edgeIndexesStart() { + return this.snapshot.firstEdgeIndexes[this.ordinal()]; + } + edgeIndexesEnd() { + return this.snapshot.firstEdgeIndexes[this.ordinal() + 1]; + } + ordinal() { + return this.nodeIndex / this.snapshot.nodeFieldCount; + } + nextNodeIndex() { + return this.nodeIndex + this.snapshot.nodeFieldCount; + } + rawType() { + const snapshot = this.snapshot; + return snapshot.nodes.getValue(this.nodeIndex + snapshot.nodeTypeOffset); + } + isFlatConsString() { + if (this.rawType() !== this.snapshot.nodeConsStringType) { + return false; + } + for (let iter = this.edges(); iter.hasNext(); iter.next()) { + const edge = iter.edge; + if (!edge.isInternal()) { + continue; + } + const edgeName = edge.name(); + if ((edgeName === "first" || edgeName === "second") && edge.node().name() === "") { + return true; + } + } + return false; + } + #detachednessAndClassIndex() { + const { snapshot, nodeIndex } = this; + const nodeDetachednessAndClassIndexOffset = snapshot.nodeDetachednessAndClassIndexOffset; + return nodeDetachednessAndClassIndexOffset !== -1 ? snapshot.nodes.getValue(nodeIndex + nodeDetachednessAndClassIndexOffset) : snapshot.detachednessAndClassIndexArray[nodeIndex / snapshot.nodeFieldCount]; + } + #setDetachednessAndClassIndex(value) { + const { snapshot, nodeIndex } = this; + const nodeDetachednessAndClassIndexOffset = snapshot.nodeDetachednessAndClassIndexOffset; + if (nodeDetachednessAndClassIndexOffset !== -1) { + snapshot.nodes.setValue( + nodeIndex + nodeDetachednessAndClassIndexOffset, + value + ); + } else { + snapshot.detachednessAndClassIndexArray[nodeIndex / snapshot.nodeFieldCount] = value; + } + } + detachedness() { + return this.#detachednessAndClassIndex() & BITMASK_FOR_DOM_LINK_STATE; + } + setDetachedness(detachedness) { + let value = this.#detachednessAndClassIndex(); + value &= -4; + value |= detachedness; + this.#setDetachednessAndClassIndex(value); + } +} +class HeapSnapshotNodeIterator { + node; + #nodesLength; + constructor(node) { + this.node = node; + this.#nodesLength = node.snapshot.nodes.length; + } + hasNext() { + return this.node.nodeIndex < this.#nodesLength; + } + item() { + return this.node; + } + next() { + this.node.nodeIndex = this.node.nextNodeIndex(); + } +} +class HeapSnapshotIndexRangeIterator { + #itemProvider; + #indexes; + #position; + constructor(itemProvider, indexes) { + this.#itemProvider = itemProvider; + this.#indexes = indexes; + this.#position = 0; + } + hasNext() { + return this.#position < this.#indexes.length; + } + item() { + const index = this.#indexes[this.#position]; + return this.#itemProvider.itemForIndex(index); + } + next() { + ++this.#position; + } +} +class HeapSnapshotFilteredIterator { + #iterator; + #filter; + constructor(iterator, filter) { + this.#iterator = iterator; + this.#filter = filter; + this.skipFilteredItems(); + } + hasNext() { + return this.#iterator.hasNext(); + } + item() { + return this.#iterator.item(); + } + next() { + this.#iterator.next(); + this.skipFilteredItems(); + } + skipFilteredItems() { + while (this.#iterator.hasNext() && this.#filter && !this.#filter(this.#iterator.item())) { + this.#iterator.next(); + } + } +} +function serializeUIString(string, values = {}) { + const serializedMessage = { string, values }; + return JSON.stringify(serializedMessage); +} +class HeapSnapshotProgress { + #dispatcher; + constructor(dispatcher) { + this.#dispatcher = dispatcher; + } + updateStatus(status) { + this.sendUpdateEvent(serializeUIString(status)); + } + updateProgress(title, value, total) { + const percentValue = ((total ? value / total : 0) * 100).toFixed(0); + this.sendUpdateEvent(serializeUIString(title, { PH1: percentValue })); + } + reportProblem(error) { + if (this.#dispatcher) { + this.#dispatcher.sendEvent( + HeapSnapshotProgressEvent.BrokenSnapshot, + error + ); + } + } + sendUpdateEvent(serializedText) { + if (this.#dispatcher) { + this.#dispatcher.sendEvent( + HeapSnapshotProgressEvent.Update, + serializedText + ); + } + } +} +function appendToProblemReport(report, messageOrNodeIndex) { + if (report.length > 100) { + return; + } + report.push(messageOrNodeIndex); +} +function formatProblemReport(snapshot, report) { + const node = snapshot.rootNode(); + return report.map((messageOrNodeIndex) => { + if (typeof messageOrNodeIndex === "string") { + return messageOrNodeIndex; + } + node.nodeIndex = messageOrNodeIndex; + return `${node.name()} @${node.id()}`; + }).join("\n "); +} +function reportProblemToPrimaryWorker(problemReport, port) { + port.postMessage({ problemReport }); +} +class SecondaryInitManager { + argsStep1; + argsStep2; + argsStep3; + constructor(port) { + const { promise: argsStep1, resolve: resolveArgsStep1 } = withResolvers(); + this.argsStep1 = argsStep1; + const { promise: argsStep2, resolve: resolveArgsStep2 } = withResolvers(); + this.argsStep2 = argsStep2; + const { promise: argsStep3, resolve: resolveArgsStep3 } = withResolvers(); + this.argsStep3 = argsStep3; + port.on("message", (data) => { + switch (data.step) { + case 1: + resolveArgsStep1(data.args); + break; + case 2: + resolveArgsStep2(data.args); + break; + case 3: + resolveArgsStep3(data.args); + break; + } + }); + void this.initialize(port); + } + async getNodeSelfSizes() { + return (await this.argsStep3).nodeSelfSizes; + } + async initialize(port) { + try { + const argsStep1 = await this.argsStep1; + const retainers = HeapSnapshot.buildRetainers(argsStep1); + const argsStep2 = await this.argsStep2; + const args = { + ...argsStep2, + ...argsStep1, + ...retainers, + essentialEdges: createBitVector( + argsStep2.essentialEdgesBuffer + ), + port, + nodeSelfSizesPromise: this.getNodeSelfSizes() + }; + const dominatorsAndRetainedSizes = await HeapSnapshot.calculateDominatorsAndRetainedSizes(args); + const dominatedNodesOutputs = HeapSnapshot.buildDominatedNodes({ + ...args, + ...dominatorsAndRetainedSizes + }); + const results = { + ...retainers, + ...dominatorsAndRetainedSizes, + ...dominatedNodesOutputs + }; + port.postMessage({ resultsFromSecondWorker: results }, [ + // TODO: node should be ok with this + results.dominatorsTree.buffer, + results.firstRetainerIndex.buffer, + results.retainedSizes.buffer, + results.retainingEdges.buffer, + results.retainingNodes.buffer, + results.dominatedNodes.buffer, + results.firstDominatedNodeIndex.buffer + ]); + } catch (e) { + port.postMessage({ error: e + "\n" + e?.stack }); + } + } +} +const BITMASK_FOR_DOM_LINK_STATE = 3; +const SHIFT_FOR_CLASS_INDEX = 2; +const MIN_INTERFACE_PROPERTY_COUNT = 1; +const MAX_INTERFACE_NAME_LENGTH = 120; +const MIN_OBJECT_COUNT_PER_INTERFACE = 2; +const MIN_OBJECT_PROPORTION_PER_INTERFACE = 1e3; +class HeapSnapshot { + nodes; + containmentEdges; + #metaNode; + #rawSamples; + #samples = null; + strings; + #locations; + #progress; + #noDistance = -5; + rootNodeIndexInternal = 0; + #snapshotDiffs = {}; + #aggregatesForDiffInternal; + #aggregates = {}; + #aggregatesSortedFlags = {}; + profile; + nodeTypeOffset; + nodeNameOffset; + nodeIdOffset; + nodeSelfSizeOffset; + #nodeEdgeCountOffset; + nodeTraceNodeIdOffset; + nodeFieldCount; + nodeTypes; + nodeArrayType; + nodeHiddenType; + nodeObjectType; + nodeNativeType; + nodeStringType; + nodeConsStringType; + nodeSlicedStringType; + nodeCodeType; + nodeSyntheticType; + nodeClosureType; + nodeRegExpType; + edgeFieldsCount; + edgeTypeOffset; + edgeNameOffset; + edgeToNodeOffset; + edgeTypes; + edgeElementType; + edgeHiddenType; + edgeInternalType; + edgeShortcutType; + edgeWeakType; + edgeInvisibleType; + edgePropertyType; + #locationIndexOffset; + #locationScriptIdOffset; + #locationLineOffset; + #locationColumnOffset; + #locationFieldCount; + nodeCount; + #edgeCount; + retainedSizes; + firstEdgeIndexes; + retainingNodes; + retainingEdges; + firstRetainerIndex; + nodeDistances; + firstDominatedNodeIndex; + dominatedNodes; + dominatorsTree; + #allocationProfile; + nodeDetachednessAndClassIndexOffset; + #locationMap; + #ignoredNodesInRetainersView = /* @__PURE__ */ new Set(); + #ignoredEdgesInRetainersView = /* @__PURE__ */ new Set(); + #nodeDistancesForRetainersView; + #edgeNamesThatAreNotWeakMaps; + detachednessAndClassIndexArray; + #interfaceNames = /* @__PURE__ */ new Map(); + #interfaceDefinitions; + constructor(profile, progress) { + this.nodes = profile.nodes; + this.containmentEdges = profile.edges; + this.#metaNode = profile.snapshot.meta; + this.#rawSamples = profile.samples; + this.strings = profile.strings; + this.#locations = profile.locations; + this.#progress = progress; + if (profile.snapshot.root_index) { + this.rootNodeIndexInternal = profile.snapshot.root_index; + } + this.profile = profile; + this.#edgeNamesThatAreNotWeakMaps = createBitVector(this.strings.length); + } + async initialize(secondWorker) { + const meta = this.#metaNode; + this.nodeTypeOffset = meta.node_fields.indexOf("type"); + this.nodeNameOffset = meta.node_fields.indexOf("name"); + this.nodeIdOffset = meta.node_fields.indexOf("id"); + this.nodeSelfSizeOffset = meta.node_fields.indexOf("self_size"); + this.#nodeEdgeCountOffset = meta.node_fields.indexOf("edge_count"); + this.nodeTraceNodeIdOffset = meta.node_fields.indexOf("trace_node_id"); + this.nodeDetachednessAndClassIndexOffset = meta.node_fields.indexOf("detachedness"); + this.nodeFieldCount = meta.node_fields.length; + this.nodeTypes = meta.node_types[this.nodeTypeOffset]; + this.nodeArrayType = this.nodeTypes.indexOf("array"); + this.nodeHiddenType = this.nodeTypes.indexOf("hidden"); + this.nodeObjectType = this.nodeTypes.indexOf("object"); + this.nodeNativeType = this.nodeTypes.indexOf("native"); + this.nodeStringType = this.nodeTypes.indexOf("string"); + this.nodeConsStringType = this.nodeTypes.indexOf("concatenated string"); + this.nodeSlicedStringType = this.nodeTypes.indexOf("sliced string"); + this.nodeCodeType = this.nodeTypes.indexOf("code"); + this.nodeSyntheticType = this.nodeTypes.indexOf("synthetic"); + this.nodeClosureType = this.nodeTypes.indexOf("closure"); + this.nodeRegExpType = this.nodeTypes.indexOf("regexp"); + this.edgeFieldsCount = meta.edge_fields.length; + this.edgeTypeOffset = meta.edge_fields.indexOf("type"); + this.edgeNameOffset = meta.edge_fields.indexOf("name_or_index"); + this.edgeToNodeOffset = meta.edge_fields.indexOf("to_node"); + this.edgeTypes = meta.edge_types[this.edgeTypeOffset]; + this.edgeTypes.push("invisible"); + this.edgeElementType = this.edgeTypes.indexOf("element"); + this.edgeHiddenType = this.edgeTypes.indexOf("hidden"); + this.edgeInternalType = this.edgeTypes.indexOf("internal"); + this.edgeShortcutType = this.edgeTypes.indexOf("shortcut"); + this.edgeWeakType = this.edgeTypes.indexOf("weak"); + this.edgeInvisibleType = this.edgeTypes.indexOf("invisible"); + this.edgePropertyType = this.edgeTypes.indexOf("property"); + const locationFields = meta.location_fields || []; + this.#locationIndexOffset = locationFields.indexOf("object_index"); + this.#locationScriptIdOffset = locationFields.indexOf("script_id"); + this.#locationLineOffset = locationFields.indexOf("line"); + this.#locationColumnOffset = locationFields.indexOf("column"); + this.#locationFieldCount = locationFields.length; + this.nodeCount = this.nodes.length / this.nodeFieldCount; + this.#edgeCount = this.containmentEdges.length / this.edgeFieldsCount; + this.#progress.updateStatus("Building edge indexes\u2026"); + this.firstEdgeIndexes = new Uint32Array(this.nodeCount + 1); + this.buildEdgeIndexes(); + this.#progress.updateStatus("Building retainers\u2026"); + const resultsFromSecondWorker = this.startInitStep1InSecondThread(secondWorker); + this.#progress.updateStatus("Propagating DOM state\u2026"); + this.propagateDOMState(); + this.#progress.updateStatus("Calculating node flags\u2026"); + this.calculateFlags(); + this.#progress.updateStatus("Building dominated nodes\u2026"); + this.startInitStep2InSecondThread(secondWorker); + this.#progress.updateStatus("Calculating shallow sizes\u2026"); + this.calculateShallowSizes(); + this.#progress.updateStatus("Calculating retained sizes\u2026"); + this.startInitStep3InSecondThread(secondWorker); + this.#progress.updateStatus("Calculating distances\u2026"); + this.nodeDistances = new Int32Array(this.nodeCount); + this.calculateDistances( + /* isForRetainersView=*/ + false + ); + this.#progress.updateStatus("Calculating object names\u2026"); + this.calculateObjectNames(); + this.applyInterfaceDefinitions(this.inferInterfaceDefinitions()); + this.#progress.updateStatus("Calculating samples\u2026"); + this.buildSamples(); + this.#progress.updateStatus("Building locations\u2026"); + this.buildLocationMap(); + this.#progress.updateStatus("Calculating retained sizes\u2026"); + await this.installResultsFromSecondThread(resultsFromSecondWorker); + this.#progress.updateStatus("Calculating statistics\u2026"); + this.calculateStatistics(); + if (this.profile.snapshot.trace_function_count) { + this.#progress.updateStatus("Building allocation statistics\u2026"); + const nodes = this.nodes; + const nodesLength = nodes.length; + const nodeFieldCount = this.nodeFieldCount; + const node = this.rootNode(); + const liveObjects = {}; + for (let nodeIndex = 0; nodeIndex < nodesLength; nodeIndex += nodeFieldCount) { + node.nodeIndex = nodeIndex; + const traceNodeId = node.traceNodeId(); + let stats = liveObjects[traceNodeId]; + if (!stats) { + liveObjects[traceNodeId] = stats = { count: 0, size: 0, ids: [] }; + } + stats.count++; + stats.size += node.selfSize(); + stats.ids.push(node.id()); + } + this.#allocationProfile = new AllocationProfile( + this.profile, + liveObjects + ); + } + this.#progress.updateStatus("Finished processing."); + } + startInitStep1InSecondThread(secondWorker) { + const resultsFromSecondWorker = new Promise( + (resolve, reject) => { + secondWorker.on("message", (data) => { + if (data?.problemReport) { + const problemReport = data.problemReport; + this.#progress.reportProblem( + formatProblemReport(this, problemReport) + ); + } else if (data?.resultsFromSecondWorker) { + const resultsFromSecondWorker2 = data.resultsFromSecondWorker; + resolve(resultsFromSecondWorker2); + } else if (data?.error) { + reject(data.error); + } + }); + } + ); + const edgeCount = this.#edgeCount; + const { + containmentEdges, + edgeToNodeOffset, + edgeFieldsCount, + nodeFieldCount + } = this; + const edgeToNodeOrdinals = new Uint32Array(edgeCount); + for (let edgeOrdinal = 0; edgeOrdinal < edgeCount; ++edgeOrdinal) { + const toNodeIndex = containmentEdges.getValue( + edgeOrdinal * edgeFieldsCount + edgeToNodeOffset + ); + if (toNodeIndex % nodeFieldCount) { + throw new Error("Invalid toNodeIndex " + toNodeIndex); + } + edgeToNodeOrdinals[edgeOrdinal] = toNodeIndex / nodeFieldCount; + } + const args = { + edgeToNodeOrdinals, + firstEdgeIndexes: this.firstEdgeIndexes, + nodeCount: this.nodeCount, + edgeFieldsCount: this.edgeFieldsCount, + nodeFieldCount: this.nodeFieldCount + }; + secondWorker.postMessage({ step: 1, args }, [edgeToNodeOrdinals.buffer]); + return resultsFromSecondWorker; + } + startInitStep2InSecondThread(secondWorker) { + const rootNodeOrdinal = this.rootNodeIndexInternal / this.nodeFieldCount; + const essentialEdges = this.initEssentialEdges(); + const args = { + rootNodeOrdinal, + essentialEdgesBuffer: essentialEdges.buffer + }; + secondWorker.postMessage({ step: 2, args }, [essentialEdges.buffer]); + } + startInitStep3InSecondThread(secondWorker) { + const { nodes, nodeFieldCount, nodeSelfSizeOffset, nodeCount } = this; + const nodeSelfSizes = new Uint32Array(nodeCount); + for (let nodeOrdinal = 0; nodeOrdinal < nodeCount; ++nodeOrdinal) { + nodeSelfSizes[nodeOrdinal] = nodes.getValue( + nodeOrdinal * nodeFieldCount + nodeSelfSizeOffset + ); + } + const args = { nodeSelfSizes }; + secondWorker.postMessage({ step: 3, args }, [nodeSelfSizes.buffer]); + } + async installResultsFromSecondThread(resultsFromSecondWorker) { + const results = await resultsFromSecondWorker; + this.dominatedNodes = results.dominatedNodes; + this.dominatorsTree = results.dominatorsTree; + this.firstDominatedNodeIndex = results.firstDominatedNodeIndex; + this.firstRetainerIndex = results.firstRetainerIndex; + this.retainedSizes = results.retainedSizes; + this.retainingEdges = results.retainingEdges; + this.retainingNodes = results.retainingNodes; + } + buildEdgeIndexes() { + const nodes = this.nodes; + const nodeCount = this.nodeCount; + const firstEdgeIndexes = this.firstEdgeIndexes; + const nodeFieldCount = this.nodeFieldCount; + const edgeFieldsCount = this.edgeFieldsCount; + const nodeEdgeCountOffset = this.#nodeEdgeCountOffset; + firstEdgeIndexes[nodeCount] = this.containmentEdges.length; + for (let nodeOrdinal = 0, edgeIndex = 0; nodeOrdinal < nodeCount; ++nodeOrdinal) { + firstEdgeIndexes[nodeOrdinal] = edgeIndex; + edgeIndex += nodes.getValue(nodeOrdinal * nodeFieldCount + nodeEdgeCountOffset) * edgeFieldsCount; + } + } + static buildRetainers(inputs) { + const { + edgeToNodeOrdinals, + firstEdgeIndexes, + nodeCount, + edgeFieldsCount, + nodeFieldCount + } = inputs; + const edgeCount = edgeToNodeOrdinals.length; + const retainingNodes = new Uint32Array(edgeCount); + const retainingEdges = new Uint32Array(edgeCount); + const firstRetainerIndex = new Uint32Array(nodeCount + 1); + for (let edgeOrdinal = 0; edgeOrdinal < edgeCount; ++edgeOrdinal) { + const toNodeOrdinal = edgeToNodeOrdinals[edgeOrdinal]; + ++firstRetainerIndex[toNodeOrdinal]; + } + for (let i = 0, firstUnusedRetainerSlot = 0; i < nodeCount; i++) { + const retainersCount = firstRetainerIndex[i]; + firstRetainerIndex[i] = firstUnusedRetainerSlot; + retainingNodes[firstUnusedRetainerSlot] = retainersCount; + firstUnusedRetainerSlot += retainersCount; + } + firstRetainerIndex[nodeCount] = retainingNodes.length; + let nextNodeFirstEdgeIndex = firstEdgeIndexes[0]; + for (let srcNodeOrdinal = 0; srcNodeOrdinal < nodeCount; ++srcNodeOrdinal) { + const firstEdgeIndex = nextNodeFirstEdgeIndex; + nextNodeFirstEdgeIndex = firstEdgeIndexes[srcNodeOrdinal + 1]; + const srcNodeIndex = srcNodeOrdinal * nodeFieldCount; + for (let edgeIndex = firstEdgeIndex; edgeIndex < nextNodeFirstEdgeIndex; edgeIndex += edgeFieldsCount) { + const toNodeOrdinal = edgeToNodeOrdinals[edgeIndex / edgeFieldsCount]; + const firstRetainerSlotIndex = firstRetainerIndex[toNodeOrdinal]; + const nextUnusedRetainerSlotIndex = firstRetainerSlotIndex + --retainingNodes[firstRetainerSlotIndex]; + retainingNodes[nextUnusedRetainerSlotIndex] = srcNodeIndex; + retainingEdges[nextUnusedRetainerSlotIndex] = edgeIndex; + } + } + return { + retainingNodes, + retainingEdges, + firstRetainerIndex + }; + } + allNodes() { + return new HeapSnapshotNodeIterator(this.rootNode()); + } + rootNode() { + return this.createNode(this.rootNodeIndexInternal); + } + get rootNodeIndex() { + return this.rootNodeIndexInternal; + } + get totalSize() { + return this.rootNode().retainedSize() + (this.profile.snapshot.extra_native_bytes ?? 0); + } + createFilter(nodeFilter) { + const { minNodeId, maxNodeId, allocationNodeId, filterName } = nodeFilter; + let filter; + if (typeof allocationNodeId === "number") { + filter = this.createAllocationStackFilter(allocationNodeId); + if (!filter) { + throw new Error("Unable to create filter"); + } + filter.key = "AllocationNodeId: " + allocationNodeId; + } else if (typeof minNodeId === "number" && typeof maxNodeId === "number") { + filter = this.createNodeIdFilter(minNodeId, maxNodeId); + filter.key = "NodeIdRange: " + minNodeId + ".." + maxNodeId; + } else if (filterName !== void 0) { + filter = this.createNamedFilter(filterName); + filter.key = "NamedFilter: " + filterName; + } + return filter; + } + search(searchConfig, nodeFilter) { + const query = searchConfig.query; + function filterString(matchedStringIndexes, string, index) { + if (string.indexOf(query) !== -1) { + matchedStringIndexes.add(index); + } + return matchedStringIndexes; + } + const regexp = searchConfig.isRegex ? new RegExp(query) : createPlainTextSearchRegex(query, "i"); + function filterRegexp(matchedStringIndexes, string, index) { + if (regexp.test(string)) { + matchedStringIndexes.add(index); + } + return matchedStringIndexes; + } + const useRegExp = searchConfig.isRegex || !searchConfig.caseSensitive; + const stringFilter = useRegExp ? filterRegexp : filterString; + const stringIndexes = this.strings.reduce(stringFilter, /* @__PURE__ */ new Set()); + const filter = this.createFilter(nodeFilter); + const nodeIds = []; + const nodesLength = this.nodes.length; + const nodes = this.nodes; + const nodeNameOffset = this.nodeNameOffset; + const nodeIdOffset = this.nodeIdOffset; + const nodeFieldCount = this.nodeFieldCount; + const node = this.rootNode(); + for (let nodeIndex = 0; nodeIndex < nodesLength; nodeIndex += nodeFieldCount) { + node.nodeIndex = nodeIndex; + if (filter && !filter(node)) { + continue; + } + if (node.selfSize() === 0) { + continue; + } + const name = node.name(); + if (name === node.rawName()) { + if (stringIndexes.has(nodes.getValue(nodeIndex + nodeNameOffset))) { + nodeIds.push(nodes.getValue(nodeIndex + nodeIdOffset)); + } + } else if (useRegExp ? regexp.test(name) : name.indexOf(query) !== -1) { + nodeIds.push(nodes.getValue(nodeIndex + nodeIdOffset)); + } + } + return nodeIds; + } + aggregatesWithFilter(nodeFilter) { + const filter = this.createFilter(nodeFilter); + const key = filter ? filter.key : "allObjects"; + return this.getAggregatesByClassKey(false, key, filter); + } + createNodeIdFilter(minNodeId, maxNodeId) { + function nodeIdFilter(node) { + const id = node.id(); + return id > minNodeId && id <= maxNodeId; + } + return nodeIdFilter; + } + createAllocationStackFilter(bottomUpAllocationNodeId) { + if (!this.#allocationProfile) { + throw new Error("No Allocation Profile provided"); + } + const traceIds = this.#allocationProfile.traceIds(bottomUpAllocationNodeId); + if (!traceIds.length) { + return void 0; + } + const set = {}; + for (let i = 0; i < traceIds.length; i++) { + set[traceIds[i]] = true; + } + function traceIdFilter(node) { + return Boolean(set[node.traceNodeId()]); + } + return traceIdFilter; + } + createNamedFilter(filterName) { + const bitmap = createBitVector(this.nodeCount); + const getBit = (node) => { + const ordinal = node.nodeIndex / this.nodeFieldCount; + return bitmap.getBit(ordinal); + }; + const traverse = (filter) => { + const distances = new Int32Array(this.nodeCount); + for (let i = 0; i < this.nodeCount; ++i) { + distances[i] = this.#noDistance; + } + const nodesToVisit = new Uint32Array(this.nodeCount); + distances[this.rootNode().ordinal()] = 0; + nodesToVisit[0] = this.rootNode().nodeIndex; + const nodesToVisitLength = 1; + this.bfs(nodesToVisit, nodesToVisitLength, distances, filter); + for (let i = 0; i < this.nodeCount; ++i) { + if (distances[i] !== this.#noDistance) { + bitmap.setBit(i); + } + } + }; + const markUnreachableNodes = () => { + for (let i = 0; i < this.nodeCount; ++i) { + if (this.nodeDistances[i] === this.#noDistance) { + bitmap.setBit(i); + } + } + }; + switch (filterName) { + case "objectsRetainedByDetachedDomNodes": + traverse((node, edge) => { + return edge.node().detachedness() !== 2 /* DETACHED */; + }); + markUnreachableNodes(); + return (node) => !getBit(node); + case "objectsRetainedByConsole": + traverse((node, edge) => { + return !(node.isSynthetic() && edge.hasStringName() && edge.name().endsWith(" / DevTools console")); + }); + markUnreachableNodes(); + return (node) => !getBit(node); + case "duplicatedStrings": { + const stringToNodeIndexMap = /* @__PURE__ */ new Map(); + const node = this.createNode(0); + for (let i = 0; i < this.nodeCount; ++i) { + node.nodeIndex = i * this.nodeFieldCount; + const rawType = node.rawType(); + if (rawType === this.nodeStringType || rawType === this.nodeConsStringType) { + if (node.isFlatConsString()) { + continue; + } + const name = node.name(); + const alreadyVisitedNodeIndex = stringToNodeIndexMap.get(name); + if (alreadyVisitedNodeIndex === void 0) { + stringToNodeIndexMap.set(name, node.nodeIndex); + } else { + bitmap.setBit(alreadyVisitedNodeIndex / this.nodeFieldCount); + bitmap.setBit(node.nodeIndex / this.nodeFieldCount); + } + } + } + return getBit; + } + } + throw new Error("Invalid filter name"); + } + getAggregatesByClassKey(sortedIndexes, key, filter) { + let aggregates; + if (key && this.#aggregates[key]) { + aggregates = this.#aggregates[key]; + } else { + const aggregatesMap = this.buildAggregates(filter); + this.calculateClassesRetainedSize(aggregatesMap, filter); + aggregates = /* @__PURE__ */ Object.create(null); + for (const [classKey, aggregate] of aggregatesMap.entries()) { + const newKey = this.classKeyFromClassKeyInternal(classKey); + aggregates[newKey] = aggregate; + } + if (key) { + this.#aggregates[key] = aggregates; + } + } + if (sortedIndexes && (!key || !this.#aggregatesSortedFlags[key])) { + this.sortAggregateIndexes(aggregates); + if (key) { + this.#aggregatesSortedFlags[key] = sortedIndexes; + } + } + return aggregates; + } + allocationTracesTops() { + return this.#allocationProfile.serializeTraceTops(); + } + allocationNodeCallers(nodeId) { + return this.#allocationProfile.serializeCallers(nodeId); + } + allocationStack(nodeIndex) { + const node = this.createNode(nodeIndex); + const allocationNodeId = node.traceNodeId(); + if (!allocationNodeId) { + return null; + } + return this.#allocationProfile.serializeAllocationStack(allocationNodeId); + } + aggregatesForDiff(interfaceDefinitions) { + if (this.#aggregatesForDiffInternal?.interfaceDefinitions === interfaceDefinitions) { + return this.#aggregatesForDiffInternal.aggregates; + } + const originalInterfaceDefinitions = this.#interfaceDefinitions; + this.applyInterfaceDefinitions( + JSON.parse(interfaceDefinitions) + ); + const aggregates = this.getAggregatesByClassKey(true, "allObjects"); + this.applyInterfaceDefinitions(originalInterfaceDefinitions ?? []); + const result = {}; + const node = this.createNode(); + for (const classKey in aggregates) { + const aggregate = aggregates[classKey]; + const indexes = aggregate.idxs; + const ids = new Array(indexes.length); + const selfSizes = new Array(indexes.length); + for (let i = 0; i < indexes.length; i++) { + node.nodeIndex = indexes[i]; + ids[i] = node.id(); + selfSizes[i] = node.selfSize(); + } + result[classKey] = { name: node.className(), indexes, ids, selfSizes }; + } + this.#aggregatesForDiffInternal = { + interfaceDefinitions, + aggregates: result + }; + return result; + } + isUserRoot(_node) { + return true; + } + calculateShallowSizes() { + } + calculateDistances(isForRetainersView, filter) { + const nodeCount = this.nodeCount; + if (isForRetainersView) { + const originalFilter = filter; + filter = (node, edge) => { + return !this.#ignoredNodesInRetainersView.has(edge.nodeIndex()) && (!originalFilter || originalFilter(node, edge)); + }; + if (this.#nodeDistancesForRetainersView === void 0) { + this.#nodeDistancesForRetainersView = new Int32Array(nodeCount); + } + } + const distances = isForRetainersView ? this.#nodeDistancesForRetainersView : this.nodeDistances; + const noDistance = this.#noDistance; + for (let i = 0; i < nodeCount; ++i) { + distances[i] = noDistance; + } + const nodesToVisit = new Uint32Array(this.nodeCount); + let nodesToVisitLength = 0; + for (let iter = this.rootNode().edges(); iter.hasNext(); iter.next()) { + const node = iter.edge.node(); + if (this.isUserRoot(node)) { + distances[node.ordinal()] = 1; + nodesToVisit[nodesToVisitLength++] = node.nodeIndex; + } + } + this.bfs(nodesToVisit, nodesToVisitLength, distances, filter); + distances[this.rootNode().ordinal()] = nodesToVisitLength > 0 ? baseSystemDistance : 0; + nodesToVisit[0] = this.rootNode().nodeIndex; + nodesToVisitLength = 1; + this.bfs(nodesToVisit, nodesToVisitLength, distances, filter); + } + bfs(nodesToVisit, nodesToVisitLength, distances, filter) { + const edgeFieldsCount = this.edgeFieldsCount; + const nodeFieldCount = this.nodeFieldCount; + const containmentEdges = this.containmentEdges; + const firstEdgeIndexes = this.firstEdgeIndexes; + const edgeToNodeOffset = this.edgeToNodeOffset; + const edgeTypeOffset = this.edgeTypeOffset; + const nodeCount = this.nodeCount; + const edgeWeakType = this.edgeWeakType; + const noDistance = this.#noDistance; + let index = 0; + const edge = this.createEdge(0); + const node = this.createNode(0); + while (index < nodesToVisitLength) { + const nodeIndex = nodesToVisit[index++]; + const nodeOrdinal = nodeIndex / nodeFieldCount; + const distance = distances[nodeOrdinal] + 1; + const firstEdgeIndex = firstEdgeIndexes[nodeOrdinal]; + const edgesEnd = firstEdgeIndexes[nodeOrdinal + 1]; + node.nodeIndex = nodeIndex; + for (let edgeIndex = firstEdgeIndex; edgeIndex < edgesEnd; edgeIndex += edgeFieldsCount) { + const edgeType = containmentEdges.getValue(edgeIndex + edgeTypeOffset); + if (edgeType === edgeWeakType) { + continue; + } + const childNodeIndex = containmentEdges.getValue( + edgeIndex + edgeToNodeOffset + ); + const childNodeOrdinal = childNodeIndex / nodeFieldCount; + if (distances[childNodeOrdinal] !== noDistance) { + continue; + } + edge.edgeIndex = edgeIndex; + if (filter && !filter(node, edge)) { + continue; + } + distances[childNodeOrdinal] = distance; + nodesToVisit[nodesToVisitLength++] = childNodeIndex; + } + } + if (nodesToVisitLength > nodeCount) { + throw new Error( + "BFS failed. Nodes to visit (" + nodesToVisitLength + ") is more than nodes count (" + nodeCount + ")" + ); + } + } + buildAggregates(filter) { + const aggregates = /* @__PURE__ */ new Map(); + const nodes = this.nodes; + const nodesLength = nodes.length; + const nodeFieldCount = this.nodeFieldCount; + const selfSizeOffset = this.nodeSelfSizeOffset; + const node = this.rootNode(); + const nodeDistances = this.nodeDistances; + for (let nodeIndex = 0; nodeIndex < nodesLength; nodeIndex += nodeFieldCount) { + node.nodeIndex = nodeIndex; + if (filter && !filter(node)) { + continue; + } + const selfSize = nodes.getValue(nodeIndex + selfSizeOffset); + if (!selfSize) { + continue; + } + const classKey = node.classKeyInternal(); + const nodeOrdinal = nodeIndex / nodeFieldCount; + const distance = nodeDistances[nodeOrdinal]; + let aggregate = aggregates.get(classKey); + if (!aggregate) { + aggregate = { + count: 1, + distance, + self: selfSize, + maxRet: 0, + name: node.className(), + idxs: [nodeIndex] + }; + aggregates.set(classKey, aggregate); + } else { + aggregate.distance = Math.min(aggregate.distance, distance); + ++aggregate.count; + aggregate.self += selfSize; + aggregate.idxs.push(nodeIndex); + } + } + for (const aggregate of aggregates.values()) { + aggregate.idxs = aggregate.idxs.slice(); + } + return aggregates; + } + calculateClassesRetainedSize(aggregates, filter) { + const rootNodeIndex = this.rootNodeIndexInternal; + const node = this.createNode(rootNodeIndex); + const list = [rootNodeIndex]; + const sizes = [-1]; + const classKeys = []; + const seenClassKeys = /* @__PURE__ */ new Map(); + const nodeFieldCount = this.nodeFieldCount; + const dominatedNodes = this.dominatedNodes; + const firstDominatedNodeIndex = this.firstDominatedNodeIndex; + while (list.length) { + const nodeIndex = list.pop(); + node.nodeIndex = nodeIndex; + let classKey = node.classKeyInternal(); + const seen = Boolean(seenClassKeys.get(classKey)); + const nodeOrdinal = nodeIndex / nodeFieldCount; + const dominatedIndexFrom = firstDominatedNodeIndex[nodeOrdinal]; + const dominatedIndexTo = firstDominatedNodeIndex[nodeOrdinal + 1]; + if (!seen && (!filter || filter(node)) && node.selfSize()) { + aggregates.get(classKey).maxRet += node.retainedSize(); + if (dominatedIndexFrom !== dominatedIndexTo) { + seenClassKeys.set(classKey, true); + sizes.push(list.length); + classKeys.push(classKey); + } + } + for (let i = dominatedIndexFrom; i < dominatedIndexTo; i++) { + list.push(dominatedNodes[i]); + } + const l = list.length; + while (sizes[sizes.length - 1] === l) { + sizes.pop(); + classKey = classKeys.pop(); + seenClassKeys.set(classKey, false); + } + } + } + sortAggregateIndexes(aggregates) { + const nodeA = this.createNode(); + const nodeB = this.createNode(); + for (const clss in aggregates) { + aggregates[clss].idxs.sort((idxA, idxB) => { + nodeA.nodeIndex = idxA; + nodeB.nodeIndex = idxB; + return nodeA.id() < nodeB.id() ? -1 : 1; + }); + } + } + tryParseWeakMapEdgeName(edgeNameIndex) { + const previousResult = this.#edgeNamesThatAreNotWeakMaps.getBit(edgeNameIndex); + if (previousResult) { + return void 0; + } + const edgeName = this.strings[edgeNameIndex]; + const ephemeronNameRegex = /^\d+(? \/ part of key \(.*? @\d+\) -> value \(.*? @\d+\) pair in WeakMap \(table @(?\d+)\))$/; + const match = edgeName.match(ephemeronNameRegex); + if (!match) { + this.#edgeNamesThatAreNotWeakMaps.setBit(edgeNameIndex); + return void 0; + } + return match.groups; + } + computeIsEssentialEdge(nodeIndex, edgeIndex, userObjectsMapAndFlag) { + const edgeType = this.containmentEdges.getValue( + edgeIndex + this.edgeTypeOffset + ); + if (edgeType === this.edgeInternalType) { + const edgeNameIndex = this.containmentEdges.getValue( + edgeIndex + this.edgeNameOffset + ); + const match = this.tryParseWeakMapEdgeName(edgeNameIndex); + if (match) { + const nodeId = this.nodes.getValue(nodeIndex + this.nodeIdOffset); + if (nodeId === parseInt(match.tableId, 10)) { + return false; + } + } + } + if (edgeType === this.edgeWeakType) { + return false; + } + const childNodeIndex = this.containmentEdges.getValue( + edgeIndex + this.edgeToNodeOffset + ); + if (nodeIndex === childNodeIndex) { + return false; + } + if (nodeIndex !== this.rootNodeIndex) { + if (edgeType === this.edgeShortcutType) { + return false; + } + const flags = userObjectsMapAndFlag ? userObjectsMapAndFlag.map : null; + const userObjectFlag = userObjectsMapAndFlag ? userObjectsMapAndFlag.flag : 0; + const nodeOrdinal = nodeIndex / this.nodeFieldCount; + const childNodeOrdinal = childNodeIndex / this.nodeFieldCount; + const nodeFlag = !flags || flags[nodeOrdinal] & userObjectFlag; + const childNodeFlag = !flags || flags[childNodeOrdinal] & userObjectFlag; + if (childNodeFlag && !nodeFlag) { + return false; + } + } + return true; + } + // Returns a bitmap indicating whether each edge should be considered when building the dominator tree. + initEssentialEdges() { + const essentialEdges = createBitVector( + this.#edgeCount + ); + const { nodes, nodeFieldCount, edgeFieldsCount } = this; + const userObjectsMapAndFlag = this.userObjectsMapAndFlag(); + const endNodeIndex = nodes.length; + const node = this.createNode(0); + for (let nodeIndex = 0; nodeIndex < endNodeIndex; nodeIndex += nodeFieldCount) { + node.nodeIndex = nodeIndex; + const edgeIndexesEnd = node.edgeIndexesEnd(); + for (let edgeIndex = node.edgeIndexesStart(); edgeIndex < edgeIndexesEnd; edgeIndex += edgeFieldsCount) { + if (this.computeIsEssentialEdge( + nodeIndex, + edgeIndex, + userObjectsMapAndFlag + )) { + essentialEdges.setBit(edgeIndex / edgeFieldsCount); + } + } + } + return essentialEdges; + } + static hasOnlyWeakRetainers(inputs, nodeOrdinal) { + const { + retainingEdges, + edgeFieldsCount, + firstRetainerIndex, + essentialEdges + } = inputs; + const beginRetainerIndex = firstRetainerIndex[nodeOrdinal]; + const endRetainerIndex = firstRetainerIndex[nodeOrdinal + 1]; + for (let retainerIndex = beginRetainerIndex; retainerIndex < endRetainerIndex; ++retainerIndex) { + const retainerEdgeIndex = retainingEdges[retainerIndex]; + if (essentialEdges.getBit(retainerEdgeIndex / edgeFieldsCount)) { + return false; + } + } + return true; + } + // The algorithm for building the dominator tree is from the paper: + // 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 + static async calculateDominatorsAndRetainedSizes(inputs) { + const { + nodeCount, + firstEdgeIndexes, + edgeFieldsCount, + nodeFieldCount, + firstRetainerIndex, + retainingEdges, + retainingNodes, + edgeToNodeOrdinals, + rootNodeOrdinal, + essentialEdges, + nodeSelfSizesPromise, + port + } = inputs; + function isEssentialEdge(edgeIndex) { + return essentialEdges.getBit(edgeIndex / edgeFieldsCount); + } + 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 bucket = new Array(arrayLength); + let n = 0; + const nextEdgeIndex = new Uint32Array(arrayLength); + const dfs = (root) => { + const rootOrdinal = root - 1; + nextEdgeIndex[rootOrdinal] = firstEdgeIndexes[rootOrdinal]; + let v = root; + while (v !== 0) { + if (semi[v] === 0) { + semi[v] = ++n; + vertex[n] = label[v] = v; + } + let vNext = parent[v]; + const vOrdinal = v - 1; + for (; nextEdgeIndex[vOrdinal] < firstEdgeIndexes[vOrdinal + 1]; nextEdgeIndex[vOrdinal] += edgeFieldsCount) { + const edgeIndex = nextEdgeIndex[vOrdinal]; + if (!isEssentialEdge(edgeIndex)) { + continue; + } + const wOrdinal = edgeToNodeOrdinals[edgeIndex / edgeFieldsCount]; + const w = wOrdinal + 1; + if (semi[w] === 0) { + parent[w] = v; + nextEdgeIndex[wOrdinal] = firstEdgeIndexes[wOrdinal]; + vNext = w; + break; + } + } + v = vNext; + } + }; + const compressionStack = new Uint32Array(arrayLength); + const compress = (v) => { + 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 = rootNodeOrdinal + 1; + n = 0; + const dom = new Uint32Array(arrayLength); + dfs(r); + if (n < nodeCount) { + const errors = [ + `Heap snapshot: ${nodeCount - n} nodes are unreachable from the root.` + ]; + appendToProblemReport( + errors, + "The following nodes have only weak retainers:" + ); + for (let v = 1; v <= nodeCount; v++) { + const vOrdinal = v - 1; + if (semi[v] === 0 && HeapSnapshot.hasOnlyWeakRetainers(inputs, vOrdinal)) { + appendToProblemReport(errors, vOrdinal * nodeFieldCount); + parent[v] = r; + dfs(v); + } + } + reportProblemToPrimaryWorker(errors, port); + } + if (n < nodeCount) { + const errors = [ + `Heap snapshot: Still found ${nodeCount - n} unreachable nodes:` + ]; + for (let v = 1; v <= nodeCount; v++) { + if (semi[v] === 0) { + const vOrdinal = v - 1; + appendToProblemReport(errors, vOrdinal * nodeFieldCount); + parent[v] = r; + semi[v] = ++n; + vertex[n] = label[v] = v; + } + } + reportProblemToPrimaryWorker(errors, port); + } + for (let i = n; i >= 2; --i) { + const w = vertex[i]; + const wOrdinal = w - 1; + let isOrphanNode = true; + for (let retainerIndex = firstRetainerIndex[wOrdinal]; retainerIndex < firstRetainerIndex[wOrdinal + 1]; retainerIndex++) { + if (!isEssentialEdge(retainingEdges[retainerIndex])) { + continue; + } + isOrphanNode = false; + const vOrdinal = retainingNodes[retainerIndex] / nodeFieldCount; + const v = vOrdinal + 1; + const u = evaluate(v); + if (semi[u] < semi[w]) { + semi[w] = semi[u]; + } + } + if (isOrphanNode) { + semi[w] = semi[r]; + } + if (bucket[vertex[semi[w]]] === void 0) { + bucket[vertex[semi[w]]] = /* @__PURE__ */ new Set(); + } + bucket[vertex[semi[w]]].add(w); + link(parent[w], w); + if (bucket[parent[w]] !== void 0) { + for (const v of bucket[parent[w]]) { + const u = evaluate(v); + dom[v] = semi[u] < semi[v] ? u : parent[w]; + } + bucket[parent[w]].clear(); + } + } + 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]]; + } + } + const dominatorsTree = new Uint32Array(nodeCount); + const retainedSizes = new Float64Array(nodeCount); + const nodeSelfSizes = await nodeSelfSizesPromise; + for (let nodeOrdinal = 0; nodeOrdinal < nodeCount; nodeOrdinal++) { + dominatorsTree[nodeOrdinal] = dom[nodeOrdinal + 1] - 1; + retainedSizes[nodeOrdinal] = nodeSelfSizes[nodeOrdinal]; + } + for (let i = n; i > 1; i--) { + const nodeOrdinal = vertex[i] - 1; + const dominatorOrdinal = dominatorsTree[nodeOrdinal]; + retainedSizes[dominatorOrdinal] += retainedSizes[nodeOrdinal]; + } + return { dominatorsTree, retainedSizes }; + } + static buildDominatedNodes(inputs) { + const { nodeCount, dominatorsTree, rootNodeOrdinal, nodeFieldCount } = inputs; + const indexArray = new Uint32Array(nodeCount + 1); + const dominatedNodes = new Uint32Array(nodeCount - 1); + let fromNodeOrdinal = 0; + let toNodeOrdinal = nodeCount; + if (rootNodeOrdinal === fromNodeOrdinal) { + fromNodeOrdinal = 1; + } else if (rootNodeOrdinal === toNodeOrdinal - 1) { + toNodeOrdinal = toNodeOrdinal - 1; + } else { + throw new Error("Root node is expected to be either first or last"); + } + for (let nodeOrdinal = fromNodeOrdinal; nodeOrdinal < toNodeOrdinal; ++nodeOrdinal) { + ++indexArray[dominatorsTree[nodeOrdinal]]; + } + let firstDominatedNodeIndex = 0; + for (let i = 0, l = nodeCount; i < l; ++i) { + const dominatedCount = dominatedNodes[firstDominatedNodeIndex] = indexArray[i]; + indexArray[i] = firstDominatedNodeIndex; + firstDominatedNodeIndex += dominatedCount; + } + indexArray[nodeCount] = dominatedNodes.length; + for (let nodeOrdinal = fromNodeOrdinal; nodeOrdinal < toNodeOrdinal; ++nodeOrdinal) { + const dominatorOrdinal = dominatorsTree[nodeOrdinal]; + let dominatedRefIndex = indexArray[dominatorOrdinal]; + dominatedRefIndex += --dominatedNodes[dominatedRefIndex]; + dominatedNodes[dominatedRefIndex] = nodeOrdinal * nodeFieldCount; + } + return { firstDominatedNodeIndex: indexArray, dominatedNodes }; + } + calculateObjectNames() { + const { + nodes, + nodeCount, + nodeNameOffset, + nodeNativeType, + nodeHiddenType, + nodeObjectType, + nodeCodeType, + nodeClosureType, + nodeRegExpType + } = this; + if (this.nodeDetachednessAndClassIndexOffset === -1) { + this.detachednessAndClassIndexArray = new Uint32Array(nodeCount); + } + const stringTable = /* @__PURE__ */ new Map(); + const getIndexForString = (s) => { + let index = stringTable.get(s); + if (index === void 0) { + index = this.addString(s); + stringTable.set(s, index); + } + return index; + }; + const hiddenClassIndex = getIndexForString("(system)"); + const codeClassIndex = getIndexForString("(compiled code)"); + const functionClassIndex = getIndexForString("Function"); + const regExpClassIndex = getIndexForString("RegExp"); + function getNodeClassIndex(node2) { + switch (node2.rawType()) { + case nodeHiddenType: + return hiddenClassIndex; + case nodeObjectType: + case nodeNativeType: { + let name = node2.rawName(); + if (name.startsWith("<")) { + const firstSpace = name.indexOf(" "); + if (firstSpace !== -1) { + name = name.substring(0, firstSpace) + ">"; + } + return getIndexForString(name); + } + if (name.startsWith("Detached <")) { + const firstSpace = name.indexOf(" ", 10); + if (firstSpace !== -1) { + name = name.substring(0, firstSpace) + ">"; + } + return getIndexForString(name); + } + return nodes.getValue(node2.nodeIndex + nodeNameOffset); + } + case nodeCodeType: + return codeClassIndex; + case nodeClosureType: + return functionClassIndex; + case nodeRegExpType: + return regExpClassIndex; + default: + return getIndexForString("(" + node2.type() + ")"); + } + } + const node = this.createNode(0); + for (let i = 0; i < nodeCount; ++i) { + node.setClassIndex(getNodeClassIndex(node)); + node.nodeIndex = node.nextNodeIndex(); + } + } + interfaceDefinitions() { + return JSON.stringify(this.#interfaceDefinitions ?? []); + } + isPlainJSObject(node) { + return node.rawType() === this.nodeObjectType && node.rawName() === "Object"; + } + inferInterfaceDefinitions() { + const { edgePropertyType } = this; + const candidates = /* @__PURE__ */ new Map(); + let totalObjectCount = 0; + for (let it = this.allNodes(); it.hasNext(); it.next()) { + const node = it.item(); + if (!this.isPlainJSObject(node)) { + continue; + } + ++totalObjectCount; + let interfaceName = "{"; + const properties = []; + for (let edgeIt = node.edges(); edgeIt.hasNext(); edgeIt.next()) { + const edge = edgeIt.item(); + const edgeName = edge.name(); + if (edge.rawType() !== edgePropertyType || edgeName === "__proto__") { + continue; + } + const formattedEdgeName = JSHeapSnapshotNode.formatPropertyName(edgeName); + if (interfaceName.length > MIN_INTERFACE_PROPERTY_COUNT && interfaceName.length + formattedEdgeName.length > MAX_INTERFACE_NAME_LENGTH) { + break; + } + if (interfaceName.length !== 1) { + interfaceName += ", "; + } + interfaceName += formattedEdgeName; + properties.push(edgeName); + } + if (properties.length === 0) { + continue; + } + interfaceName += "}"; + const candidate = candidates.get(interfaceName); + if (candidate) { + ++candidate.count; + } else { + candidates.set(interfaceName, { + name: interfaceName, + properties, + count: 1 + }); + } + } + const sortedCandidates = Array.from(candidates.values()); + sortedCandidates.sort((a, b) => b.count - a.count); + const result = []; + const minCount = Math.max( + MIN_OBJECT_COUNT_PER_INTERFACE, + totalObjectCount / MIN_OBJECT_PROPORTION_PER_INTERFACE + ); + for (let i = 0; i < sortedCandidates.length; ++i) { + const candidate = sortedCandidates[i]; + if (candidate.count < minCount) { + break; + } + result.push(candidate); + } + return result; + } + applyInterfaceDefinitions(definitions) { + const { edgePropertyType } = this; + this.#interfaceDefinitions = definitions; + this.#aggregates = {}; + this.#aggregatesSortedFlags = {}; + function selectBetterMatch(a, b) { + if (!b || a.propertyCount > b.propertyCount) { + return a; + } + if (b.propertyCount > a.propertyCount) { + return b; + } + return a.index <= b.index ? a : b; + } + const propertyTree = { + next: /* @__PURE__ */ new Map(), + matchInfo: null, + greatestNext: null + }; + for (let interfaceIndex = 0; interfaceIndex < definitions.length; ++interfaceIndex) { + const definition = definitions[interfaceIndex]; + const properties = [...definition.properties].sort(); + let currentNode = propertyTree; + for (const property of properties) { + const nextMap = currentNode.next; + let nextNode = nextMap.get(property); + if (!nextNode) { + nextNode = { + next: /* @__PURE__ */ new Map(), + matchInfo: null, + greatestNext: null + }; + nextMap.set(property, nextNode); + if (currentNode.greatestNext === null || currentNode.greatestNext < property) { + currentNode.greatestNext = property; + } + } + currentNode = nextNode; + } + if (!currentNode.matchInfo) { + currentNode.matchInfo = { + name: definition.name, + propertyCount: properties.length, + index: interfaceIndex + }; + } + } + const initialMatch = { + name: "Object", + propertyCount: 0, + index: Infinity + }; + for (let it = this.allNodes(); it.hasNext(); it.next()) { + const node = it.item(); + if (!this.isPlainJSObject(node)) { + continue; + } + const properties = []; + for (let edgeIt = node.edges(); edgeIt.hasNext(); edgeIt.next()) { + const edge = edgeIt.item(); + if (edge.rawType() === edgePropertyType) { + properties.push(edge.name()); + } + } + properties.sort(); + const states = /* @__PURE__ */ new Set(); + states.add(propertyTree); + let match = selectBetterMatch(initialMatch, propertyTree.matchInfo); + for (const property of properties) { + for (const currentState of Array.from(states.keys())) { + if (currentState.greatestNext === null || property >= currentState.greatestNext) { + states.delete(currentState); + } + const nextState = currentState.next.get(property); + if (nextState) { + states.add(nextState); + match = selectBetterMatch(match, nextState.matchInfo); + } + } + } + let classIndex = match === initialMatch ? node.rawNameIndex() : this.#interfaceNames.get(match.name); + if (classIndex === void 0) { + classIndex = this.addString(match.name); + this.#interfaceNames.set(match.name, classIndex); + } + node.setClassIndex(classIndex); + } + } + /** + * Iterates children of a node. + */ + iterateFilteredChildren(nodeOrdinal, edgeFilterCallback, childCallback) { + const beginEdgeIndex = this.firstEdgeIndexes[nodeOrdinal]; + const endEdgeIndex = this.firstEdgeIndexes[nodeOrdinal + 1]; + for (let edgeIndex = beginEdgeIndex; edgeIndex < endEdgeIndex; edgeIndex += this.edgeFieldsCount) { + const childNodeIndex = this.containmentEdges.getValue( + edgeIndex + this.edgeToNodeOffset + ); + const childNodeOrdinal = childNodeIndex / this.nodeFieldCount; + const type = this.containmentEdges.getValue( + edgeIndex + this.edgeTypeOffset + ); + if (!edgeFilterCallback(type)) { + continue; + } + childCallback(childNodeOrdinal); + } + } + /** + * Adds a string to the snapshot. + */ + addString(string) { + this.strings.push(string); + return this.strings.length - 1; + } + /** + * The phase propagates whether a node is attached or detached through the + * graph and adjusts the low-level representation of nodes. + * + * State propagation: + * 1. Any object reachable from an attached object is itself attached. + * 2. Any object reachable from a detached object that is not already + * attached is considered detached. + * + * Representation: + * - Name of any detached node is changed from """ to + * "Detached ". + */ + propagateDOMState() { + if (this.nodeDetachednessAndClassIndexOffset === -1) { + return; + } + const visited = new Uint8Array(this.nodeCount); + const attached = []; + const detached = []; + const stringIndexCache = /* @__PURE__ */ new Map(); + const node = this.createNode(0); + const addDetachedPrefixToNodeName = function(snapshot, nodeIndex) { + const oldStringIndex = snapshot.nodes.getValue( + nodeIndex + snapshot.nodeNameOffset + ); + let newStringIndex = stringIndexCache.get(oldStringIndex); + if (newStringIndex === void 0) { + newStringIndex = snapshot.addString( + "Detached " + snapshot.strings[oldStringIndex] + ); + stringIndexCache.set(oldStringIndex, newStringIndex); + } + snapshot.nodes.setValue( + nodeIndex + snapshot.nodeNameOffset, + newStringIndex + ); + }; + const processNode = function(snapshot, nodeOrdinal, newState) { + if (visited[nodeOrdinal]) { + return; + } + const nodeIndex = nodeOrdinal * snapshot.nodeFieldCount; + if (snapshot.nodes.getValue(nodeIndex + snapshot.nodeTypeOffset) !== snapshot.nodeNativeType) { + visited[nodeOrdinal] = 1; + return; + } + node.nodeIndex = nodeIndex; + node.setDetachedness(newState); + if (newState === 1 /* ATTACHED */) { + attached.push(nodeOrdinal); + } else if (newState === 2 /* DETACHED */) { + addDetachedPrefixToNodeName(snapshot, nodeIndex); + detached.push(nodeOrdinal); + } + visited[nodeOrdinal] = 1; + }; + const propagateState = function(snapshot, parentNodeOrdinal, newState) { + snapshot.iterateFilteredChildren( + parentNodeOrdinal, + (edgeType) => ![ + snapshot.edgeHiddenType, + snapshot.edgeInvisibleType, + snapshot.edgeWeakType + ].includes(edgeType), + (nodeOrdinal) => processNode(snapshot, nodeOrdinal, newState) + ); + }; + for (let nodeOrdinal = 0; nodeOrdinal < this.nodeCount; ++nodeOrdinal) { + node.nodeIndex = nodeOrdinal * this.nodeFieldCount; + const state = node.detachedness(); + if (state === 0 /* UNKNOWN */) { + continue; + } + processNode(this, nodeOrdinal, state); + } + while (attached.length !== 0) { + const nodeOrdinal = attached.pop(); + propagateState(this, nodeOrdinal, 1 /* ATTACHED */); + } + while (detached.length !== 0) { + const nodeOrdinal = detached.pop(); + node.nodeIndex = nodeOrdinal * this.nodeFieldCount; + const nodeState = node.detachedness(); + if (nodeState === 1 /* ATTACHED */) { + continue; + } + propagateState(this, nodeOrdinal, 2 /* DETACHED */); + } + } + buildSamples() { + const samples = this.#rawSamples; + if (!samples?.length) { + return; + } + const sampleCount = samples.length / 2; + const sizeForRange = new Array(sampleCount); + const timestamps = new Array(sampleCount); + const lastAssignedIds = new Array(sampleCount); + const timestampOffset = this.#metaNode.sample_fields.indexOf("timestamp_us"); + const lastAssignedIdOffset = this.#metaNode.sample_fields.indexOf("last_assigned_id"); + for (let i = 0; i < sampleCount; i++) { + sizeForRange[i] = 0; + timestamps[i] = samples[2 * i + timestampOffset] / 1e3; + lastAssignedIds[i] = samples[2 * i + lastAssignedIdOffset]; + } + const nodes = this.nodes; + const nodesLength = nodes.length; + const nodeFieldCount = this.nodeFieldCount; + const node = this.rootNode(); + for (let nodeIndex = 0; nodeIndex < nodesLength; nodeIndex += nodeFieldCount) { + node.nodeIndex = nodeIndex; + const nodeId = node.id(); + if (nodeId % 2 === 0) { + continue; + } + const rangeIndex = lowerBound( + lastAssignedIds, + nodeId, + DEFAULT_COMPARATOR + ); + if (rangeIndex === sampleCount) { + continue; + } + sizeForRange[rangeIndex] += node.selfSize(); + } + this.#samples = new Samples( + timestamps, + lastAssignedIds, + sizeForRange + ); + } + buildLocationMap() { + const map = /* @__PURE__ */ new Map(); + const locations = this.#locations; + for (let i = 0; i < locations.length; i += this.#locationFieldCount) { + const nodeIndex = locations[i + this.#locationIndexOffset]; + const scriptId = locations[i + this.#locationScriptIdOffset]; + const line = locations[i + this.#locationLineOffset]; + const col = locations[i + this.#locationColumnOffset]; + map.set(nodeIndex, new Location(scriptId, line, col)); + } + this.#locationMap = map; + } + getLocation(nodeIndex) { + return this.#locationMap.get(nodeIndex) || null; + } + getSamples() { + return this.#samples; + } + calculateFlags() { + throw new Error("Not implemented"); + } + calculateStatistics() { + throw new Error("Not implemented"); + } + userObjectsMapAndFlag() { + throw new Error("Not implemented"); + } + calculateSnapshotDiff(baseSnapshotId, baseSnapshotAggregates) { + let snapshotDiff = this.#snapshotDiffs[baseSnapshotId]; + if (snapshotDiff) { + return snapshotDiff; + } + snapshotDiff = {}; + const aggregates = this.getAggregatesByClassKey(true, "allObjects"); + for (const classKey in baseSnapshotAggregates) { + const baseAggregate = baseSnapshotAggregates[classKey]; + const diff = this.calculateDiffForClass( + baseAggregate, + aggregates[classKey] + ); + if (diff) { + snapshotDiff[classKey] = diff; + } + } + const emptyBaseAggregate = new AggregateForDiff(); + for (const classKey in aggregates) { + if (classKey in baseSnapshotAggregates) { + continue; + } + const classDiff = this.calculateDiffForClass( + emptyBaseAggregate, + aggregates[classKey] + ); + if (classDiff) { + snapshotDiff[classKey] = classDiff; + } + } + this.#snapshotDiffs[baseSnapshotId] = snapshotDiff; + return snapshotDiff; + } + calculateDiffForClass(baseAggregate, aggregate) { + const baseIds = baseAggregate.ids; + const baseIndexes = baseAggregate.indexes; + const baseSelfSizes = baseAggregate.selfSizes; + const indexes = aggregate ? aggregate.idxs : []; + let i = 0; + let j = 0; + const l = baseIds.length; + const m = indexes.length; + const diff = new Diff( + aggregate ? aggregate.name : baseAggregate.name + ); + const nodeB = this.createNode(indexes[j]); + while (i < l && j < m) { + const nodeAId = baseIds[i]; + if (nodeAId < nodeB.id()) { + diff.deletedIndexes.push(baseIndexes[i]); + diff.removedCount++; + diff.removedSize += baseSelfSizes[i]; + ++i; + } else if (nodeAId > nodeB.id()) { + diff.addedIndexes.push(indexes[j]); + diff.addedCount++; + diff.addedSize += nodeB.selfSize(); + nodeB.nodeIndex = indexes[++j]; + } else { + ++i; + nodeB.nodeIndex = indexes[++j]; + } + } + while (i < l) { + diff.deletedIndexes.push(baseIndexes[i]); + diff.removedCount++; + diff.removedSize += baseSelfSizes[i]; + ++i; + } + while (j < m) { + diff.addedIndexes.push(indexes[j]); + diff.addedCount++; + diff.addedSize += nodeB.selfSize(); + nodeB.nodeIndex = indexes[++j]; + } + diff.countDelta = diff.addedCount - diff.removedCount; + diff.sizeDelta = diff.addedSize - diff.removedSize; + if (!diff.addedCount && !diff.removedCount) { + return null; + } + return diff; + } + nodeForSnapshotObjectId(snapshotObjectId) { + for (let it = this.allNodes(); it.hasNext(); it.next()) { + if (it.node.id() === snapshotObjectId) { + return it.node; + } + } + return null; + } + // Converts an internal class key, suitable for categorizing within this + // snapshot, to a public class key, which can be used in comparisons + // between multiple snapshots. + classKeyFromClassKeyInternal(key) { + return typeof key === "number" ? "," + this.strings[key] : key; + } + nodeClassKey(snapshotObjectId) { + const node = this.nodeForSnapshotObjectId(snapshotObjectId); + if (node) { + return this.classKeyFromClassKeyInternal(node.classKeyInternal()); + } + return null; + } + idsOfObjectsWithName(name) { + const ids = []; + for (let it = this.allNodes(); it.hasNext(); it.next()) { + if (it.item().name() === name) { + ids.push(it.item().id()); + } + } + return ids; + } + createEdgesProvider(nodeIndex) { + const node = this.createNode(nodeIndex); + const filter = this.containmentEdgesFilter(); + const indexProvider = new HeapSnapshotEdgeIndexProvider(this); + return new HeapSnapshotEdgesProvider( + this, + filter, + node.edges(), + indexProvider + ); + } + createEdgesProviderForTest(nodeIndex, filter) { + const node = this.createNode(nodeIndex); + const indexProvider = new HeapSnapshotEdgeIndexProvider(this); + return new HeapSnapshotEdgesProvider( + this, + filter, + node.edges(), + indexProvider + ); + } + retainingEdgesFilter() { + return null; + } + containmentEdgesFilter() { + return null; + } + createRetainingEdgesProvider(nodeIndex) { + const node = this.createNode(nodeIndex); + const filter = this.retainingEdgesFilter(); + const indexProvider = new HeapSnapshotRetainerEdgeIndexProvider(this); + return new HeapSnapshotEdgesProvider( + this, + filter, + node.retainers(), + indexProvider + ); + } + createAddedNodesProvider(baseSnapshotId, classKey) { + const snapshotDiff = this.#snapshotDiffs[baseSnapshotId]; + const diffForClass = snapshotDiff[classKey]; + return new HeapSnapshotNodesProvider(this, diffForClass.addedIndexes); + } + createDeletedNodesProvider(nodeIndexes) { + return new HeapSnapshotNodesProvider(this, nodeIndexes); + } + createNodesProviderForClass(classKey, nodeFilter) { + return new HeapSnapshotNodesProvider( + this, + this.aggregatesWithFilter(nodeFilter)[classKey].idxs + ); + } + maxJsNodeId() { + const nodeFieldCount = this.nodeFieldCount; + const nodes = this.nodes; + const nodesLength = nodes.length; + let id = 0; + for (let nodeIndex = this.nodeIdOffset; nodeIndex < nodesLength; nodeIndex += nodeFieldCount) { + const nextId = nodes.getValue(nodeIndex); + if (nextId % 2 === 0) { + continue; + } + if (id < nextId) { + id = nextId; + } + } + return id; + } + updateStaticData() { + return new StaticData( + this.nodeCount, + this.rootNodeIndexInternal, + this.totalSize, + this.maxJsNodeId() + ); + } + ignoreNodeInRetainersView(nodeIndex) { + this.#ignoredNodesInRetainersView.add(nodeIndex); + this.calculateDistances( + /* isForRetainersView=*/ + true + ); + this.#updateIgnoredEdgesInRetainersView(); + } + unignoreNodeInRetainersView(nodeIndex) { + this.#ignoredNodesInRetainersView.delete(nodeIndex); + if (this.#ignoredNodesInRetainersView.size === 0) { + this.#nodeDistancesForRetainersView = void 0; + } else { + this.calculateDistances( + /* isForRetainersView=*/ + true + ); + } + this.#updateIgnoredEdgesInRetainersView(); + } + unignoreAllNodesInRetainersView() { + this.#ignoredNodesInRetainersView.clear(); + this.#nodeDistancesForRetainersView = void 0; + this.#updateIgnoredEdgesInRetainersView(); + } + #updateIgnoredEdgesInRetainersView() { + const distances = this.#nodeDistancesForRetainersView; + this.#ignoredEdgesInRetainersView.clear(); + if (distances === void 0) { + return; + } + const unreachableWeakMapEdges = new Multimap(); + const noDistance = this.#noDistance; + const { nodeCount, nodeFieldCount } = this; + const node = this.createNode(0); + for (let nodeOrdinal = 0; nodeOrdinal < nodeCount; ++nodeOrdinal) { + if (distances[nodeOrdinal] !== noDistance) { + continue; + } + node.nodeIndex = nodeOrdinal * nodeFieldCount; + for (let iter = node.edges(); iter.hasNext(); iter.next()) { + const edge = iter.edge; + if (!edge.isInternal()) { + continue; + } + const match = this.tryParseWeakMapEdgeName(edge.nameIndex()); + if (match) { + unreachableWeakMapEdges.set(edge.nodeIndex(), match.duplicatedPart); + } + } + } + for (const targetNodeIndex of unreachableWeakMapEdges.keys()) { + node.nodeIndex = targetNodeIndex; + for (let it = node.retainers(); it.hasNext(); it.next()) { + const reverseEdge = it.item(); + if (!reverseEdge.isInternal()) { + continue; + } + const match = this.tryParseWeakMapEdgeName(reverseEdge.nameIndex()); + if (match && unreachableWeakMapEdges.hasValue( + targetNodeIndex, + match.duplicatedPart + )) { + const forwardEdgeIndex = this.retainingEdges[reverseEdge.itemIndex()]; + this.#ignoredEdgesInRetainersView.add(forwardEdgeIndex); + } + } + } + } + areNodesIgnoredInRetainersView() { + return this.#ignoredNodesInRetainersView.size > 0; + } + getDistanceForRetainersView(nodeIndex) { + const nodeOrdinal = nodeIndex / this.nodeFieldCount; + const distances = this.#nodeDistancesForRetainersView ?? this.nodeDistances; + const distance = distances[nodeOrdinal]; + if (distance === this.#noDistance) { + return Math.max(0, this.nodeDistances[nodeOrdinal]) + baseUnreachableDistance; + } + return distance; + } + isNodeIgnoredInRetainersView(nodeIndex) { + return this.#ignoredNodesInRetainersView.has(nodeIndex); + } + isEdgeIgnoredInRetainersView(edgeIndex) { + return this.#ignoredEdgesInRetainersView.has(edgeIndex); + } +} +class HeapSnapshotItemProvider { + iterator; + #indexProvider; + #isEmptyInternal; + iterationOrder; + currentComparator; + #sortedPrefixLength; + #sortedSuffixLength; + constructor(iterator, indexProvider) { + this.iterator = iterator; + this.#indexProvider = indexProvider; + this.#isEmptyInternal = !iterator.hasNext(); + this.iterationOrder = null; + this.currentComparator = null; + this.#sortedPrefixLength = 0; + this.#sortedSuffixLength = 0; + } + createIterationOrder() { + if (this.iterationOrder) { + return; + } + this.iterationOrder = []; + for (let iterator = this.iterator; iterator.hasNext(); iterator.next()) { + this.iterationOrder.push(iterator.item().itemIndex()); + } + } + isEmpty() { + return this.#isEmptyInternal; + } + serializeItemsRange(begin, end) { + this.createIterationOrder(); + if (begin > end) { + throw new Error("Start position > end position: " + begin + " > " + end); + } + if (!this.iterationOrder) { + throw new Error("Iteration order undefined"); + } + if (end > this.iterationOrder.length) { + end = this.iterationOrder.length; + } + if (this.#sortedPrefixLength < end && begin < this.iterationOrder.length - this.#sortedSuffixLength && this.currentComparator) { + const currentComparator = this.currentComparator; + this.sort( + currentComparator, + this.#sortedPrefixLength, + this.iterationOrder.length - 1 - this.#sortedSuffixLength, + begin, + end - 1 + ); + if (begin <= this.#sortedPrefixLength) { + this.#sortedPrefixLength = end; + } + if (end >= this.iterationOrder.length - this.#sortedSuffixLength) { + this.#sortedSuffixLength = this.iterationOrder.length - begin; + } + } + let position = begin; + const count = end - begin; + const result = new Array(count); + for (let i = 0; i < count; ++i) { + const itemIndex = this.iterationOrder[position++]; + const item = this.#indexProvider.itemForIndex(itemIndex); + result[i] = item.serialize(); + } + return new ItemsRange( + begin, + end, + this.iterationOrder.length, + result + ); + } + sortAndRewind(comparator) { + this.currentComparator = comparator; + this.#sortedPrefixLength = 0; + this.#sortedSuffixLength = 0; + } +} +class HeapSnapshotEdgesProvider extends HeapSnapshotItemProvider { + snapshot; + constructor(snapshot, filter, edgesIter, indexProvider) { + const iter = filter ? new HeapSnapshotFilteredIterator( + edgesIter, + filter + ) : edgesIter; + super(iter, indexProvider); + this.snapshot = snapshot; + } + sort(comparator, leftBound, rightBound, windowLeft, windowRight) { + const fieldName1 = comparator.fieldName1; + const fieldName2 = comparator.fieldName2; + const ascending1 = comparator.ascending1; + const ascending2 = comparator.ascending2; + const edgeA = this.iterator.item().clone(); + const edgeB = edgeA.clone(); + const nodeA = this.snapshot.createNode(); + const nodeB = this.snapshot.createNode(); + function compareEdgeField(fieldName, ascending, indexA, indexB) { + edgeA.edgeIndex = indexA; + edgeB.edgeIndex = indexB; + let result = 0; + if (fieldName === "!edgeName") { + if (edgeB.name() === "__proto__") { + return -1; + } + if (edgeA.name() === "__proto__") { + return 1; + } + result = edgeA.hasStringName() === edgeB.hasStringName() ? edgeA.name() < edgeB.name() ? -1 : edgeA.name() > edgeB.name() ? 1 : 0 : edgeA.hasStringName() ? -1 : 1; + } else { + result = edgeA.getValueForSorting(fieldName) - edgeB.getValueForSorting(fieldName); + } + return ascending ? result : -result; + } + function compareNodeField(fieldName, ascending, indexA, indexB) { + edgeA.edgeIndex = indexA; + nodeA.nodeIndex = edgeA.nodeIndex(); + const valueA = nodeA[fieldName](); + edgeB.edgeIndex = indexB; + nodeB.nodeIndex = edgeB.nodeIndex(); + const valueB = nodeB[fieldName](); + const result = valueA < valueB ? -1 : valueA > valueB ? 1 : 0; + return ascending ? result : -result; + } + function compareEdgeAndEdge(indexA, indexB) { + let result = compareEdgeField(fieldName1, ascending1, indexA, indexB); + if (result === 0) { + result = compareEdgeField(fieldName2, ascending2, indexA, indexB); + } + if (result === 0) { + return indexA - indexB; + } + return result; + } + function compareEdgeAndNode(indexA, indexB) { + let result = compareEdgeField(fieldName1, ascending1, indexA, indexB); + if (result === 0) { + result = compareNodeField(fieldName2, ascending2, indexA, indexB); + } + if (result === 0) { + return indexA - indexB; + } + return result; + } + function compareNodeAndEdge(indexA, indexB) { + let result = compareNodeField(fieldName1, ascending1, indexA, indexB); + if (result === 0) { + result = compareEdgeField(fieldName2, ascending2, indexA, indexB); + } + if (result === 0) { + return indexA - indexB; + } + return result; + } + function compareNodeAndNode(indexA, indexB) { + let result = compareNodeField(fieldName1, ascending1, indexA, indexB); + if (result === 0) { + result = compareNodeField(fieldName2, ascending2, indexA, indexB); + } + if (result === 0) { + return indexA - indexB; + } + return result; + } + if (!this.iterationOrder) { + throw new Error("Iteration order not defined"); + } + function isEdgeFieldName(fieldName) { + return fieldName.startsWith("!edge"); + } + if (isEdgeFieldName(fieldName1)) { + if (isEdgeFieldName(fieldName2)) { + sortRange( + this.iterationOrder, + compareEdgeAndEdge, + leftBound, + rightBound, + windowLeft, + windowRight + ); + } else { + sortRange( + this.iterationOrder, + compareEdgeAndNode, + leftBound, + rightBound, + windowLeft, + windowRight + ); + } + } else if (isEdgeFieldName(fieldName2)) { + sortRange( + this.iterationOrder, + compareNodeAndEdge, + leftBound, + rightBound, + windowLeft, + windowRight + ); + } else { + sortRange( + this.iterationOrder, + compareNodeAndNode, + leftBound, + rightBound, + windowLeft, + windowRight + ); + } + } +} +class HeapSnapshotNodesProvider extends HeapSnapshotItemProvider { + snapshot; + constructor(snapshot, nodeIndexes) { + const indexProvider = new HeapSnapshotNodeIndexProvider(snapshot); + const it = new HeapSnapshotIndexRangeIterator(indexProvider, nodeIndexes); + super(it, indexProvider); + this.snapshot = snapshot; + } + nodePosition(snapshotObjectId) { + this.createIterationOrder(); + const node = this.snapshot.createNode(); + let i = 0; + if (!this.iterationOrder) { + throw new Error("Iteration order not defined"); + } + for (; i < this.iterationOrder.length; i++) { + node.nodeIndex = this.iterationOrder[i]; + if (node.id() === snapshotObjectId) { + break; + } + } + if (i === this.iterationOrder.length) { + return -1; + } + const targetNodeIndex = this.iterationOrder[i]; + let smallerCount = 0; + const currentComparator = this.currentComparator; + const compare = this.buildCompareFunction(currentComparator); + for (let i2 = 0; i2 < this.iterationOrder.length; i2++) { + if (compare(this.iterationOrder[i2], targetNodeIndex) < 0) { + ++smallerCount; + } + } + return smallerCount; + } + buildCompareFunction(comparator) { + const nodeA = this.snapshot.createNode(); + const nodeB = this.snapshot.createNode(); + const fieldAccessor1 = nodeA[comparator.fieldName1]; + const fieldAccessor2 = nodeA[comparator.fieldName2]; + const ascending1 = comparator.ascending1 ? 1 : -1; + const ascending2 = comparator.ascending2 ? 1 : -1; + function sortByNodeField(fieldAccessor, ascending) { + const valueA = fieldAccessor.call(nodeA); + const valueB = fieldAccessor.call(nodeB); + return valueA < valueB ? -ascending : valueA > valueB ? ascending : 0; + } + function sortByComparator(indexA, indexB) { + nodeA.nodeIndex = indexA; + nodeB.nodeIndex = indexB; + let result = sortByNodeField(fieldAccessor1, ascending1); + if (result === 0) { + result = sortByNodeField(fieldAccessor2, ascending2); + } + return result || indexA - indexB; + } + return sortByComparator; + } + sort(comparator, leftBound, rightBound, windowLeft, windowRight) { + if (!this.iterationOrder) { + throw new Error("Iteration order not defined"); + } + sortRange( + this.iterationOrder, + this.buildCompareFunction(comparator), + leftBound, + rightBound, + windowLeft, + windowRight + ); + } +} +class JSHeapSnapshot extends HeapSnapshot { + nodeFlags; + flags; + #statistics; + constructor(profile, progress) { + super(profile, progress); + this.nodeFlags = { + // bit flags in 8-bit value + canBeQueried: 1, + detachedDOMTreeNode: 2, + pageObject: 4 + // The idea is to track separately the objects owned by the page and the objects owned by debugger. + }; + } + createNode(nodeIndex) { + return new JSHeapSnapshotNode( + this, + nodeIndex === void 0 ? -1 : nodeIndex + ); + } + createEdge(edgeIndex) { + return new JSHeapSnapshotEdge(this, edgeIndex); + } + createRetainingEdge(retainerIndex) { + return new JSHeapSnapshotRetainerEdge(this, retainerIndex); + } + containmentEdgesFilter() { + return (edge) => !edge.isInvisible(); + } + retainingEdgesFilter() { + const containmentEdgesFilter = this.containmentEdgesFilter(); + function filter(edge) { + return containmentEdgesFilter(edge) && !edge.node().isRoot() && !edge.isWeak(); + } + return filter; + } + calculateFlags() { + this.flags = new Uint8Array(this.nodeCount); + this.markDetachedDOMTreeNodes(); + this.markQueriableHeapObjects(); + this.markPageOwnedNodes(); + } + #hasUserRoots() { + for (let iter = this.rootNode().edges(); iter.hasNext(); iter.next()) { + if (this.isUserRoot(iter.edge.node())) { + return true; + } + } + return false; + } + // Updates the shallow sizes for "owned" objects of types kArray or kHidden to + // zero, and add their sizes to the "owner" object instead. + calculateShallowSizes() { + if (!this.#hasUserRoots()) { + return; + } + const { nodeCount, nodes, nodeFieldCount, nodeSelfSizeOffset } = this; + const kUnvisited = 4294967295; + const kHasMultipleOwners = 4294967294; + if (nodeCount >= kHasMultipleOwners) { + throw new Error("Too many nodes for calculateShallowSizes"); + } + const owners = new Uint32Array(nodeCount); + const worklist = []; + const node = this.createNode(0); + for (let i = 0; i < nodeCount; ++i) { + if (node.isHidden() || node.isArray() || node.isNative() && node.rawName() === "system / ExternalStringData") { + owners[i] = kUnvisited; + } else { + owners[i] = i; + worklist.push(i); + } + node.nodeIndex = node.nextNodeIndex(); + } + while (worklist.length !== 0) { + const id = worklist.pop(); + const owner = owners[id]; + node.nodeIndex = id * nodeFieldCount; + for (let iter = node.edges(); iter.hasNext(); iter.next()) { + const edge = iter.edge; + if (edge.isWeak()) { + continue; + } + const targetId = edge.nodeIndex() / nodeFieldCount; + switch (owners[targetId]) { + case kUnvisited: + owners[targetId] = owner; + worklist.push(targetId); + break; + case targetId: + case owner: + case kHasMultipleOwners: + break; + default: + owners[targetId] = kHasMultipleOwners; + worklist.push(targetId); + break; + } + } + } + for (let i = 0; i < nodeCount; ++i) { + const ownerId = owners[i]; + switch (ownerId) { + case kUnvisited: + case kHasMultipleOwners: + case i: + break; + default: { + const ownedNodeIndex = i * nodeFieldCount; + const ownerNodeIndex = ownerId * nodeFieldCount; + node.nodeIndex = ownerNodeIndex; + if (node.isSynthetic() || node.isRoot()) { + break; + } + const sizeToTransfer = nodes.getValue( + ownedNodeIndex + nodeSelfSizeOffset + ); + nodes.setValue(ownedNodeIndex + nodeSelfSizeOffset, 0); + nodes.setValue( + ownerNodeIndex + nodeSelfSizeOffset, + nodes.getValue(ownerNodeIndex + nodeSelfSizeOffset) + sizeToTransfer + ); + break; + } + } + } + } + calculateDistances(isForRetainersView) { + const pendingEphemeronEdges = /* @__PURE__ */ new Set(); + const snapshot = this; + function filter(node, edge) { + if (node.isHidden() && edge.name() === "sloppy_function_map" && node.rawName() === "system / NativeContext") { + return false; + } + if (node.isArray() && node.rawName() === "(map descriptors)") { + const index = parseInt(edge.name(), 10); + return index < 2 || index % 3 !== 1; + } + if (edge.isInternal()) { + const match = snapshot.tryParseWeakMapEdgeName(edge.nameIndex()); + if (match) { + if (!pendingEphemeronEdges.delete(match.duplicatedPart)) { + pendingEphemeronEdges.add(match.duplicatedPart); + return false; + } + } + } + return true; + } + super.calculateDistances(isForRetainersView, filter); + } + isUserRoot(node) { + return node.isUserRoot() || node.isDocumentDOMTreesRoot(); + } + userObjectsMapAndFlag() { + return { map: this.flags, flag: this.nodeFlags.pageObject }; + } + flagsOfNode(node) { + return this.flags[node.nodeIndex / this.nodeFieldCount]; + } + markDetachedDOMTreeNodes() { + const nodes = this.nodes; + const nodesLength = nodes.length; + const nodeFieldCount = this.nodeFieldCount; + const nodeNativeType = this.nodeNativeType; + const nodeTypeOffset = this.nodeTypeOffset; + const flag = this.nodeFlags.detachedDOMTreeNode; + const node = this.rootNode(); + for (let nodeIndex = 0, ordinal = 0; nodeIndex < nodesLength; nodeIndex += nodeFieldCount, ordinal++) { + const nodeType = nodes.getValue(nodeIndex + nodeTypeOffset); + if (nodeType !== nodeNativeType) { + continue; + } + node.nodeIndex = nodeIndex; + if (node.name().startsWith("Detached ")) { + this.flags[ordinal] |= flag; + } + } + } + markQueriableHeapObjects() { + const flag = this.nodeFlags.canBeQueried; + const hiddenEdgeType = this.edgeHiddenType; + const internalEdgeType = this.edgeInternalType; + const invisibleEdgeType = this.edgeInvisibleType; + const weakEdgeType = this.edgeWeakType; + const edgeToNodeOffset = this.edgeToNodeOffset; + const edgeTypeOffset = this.edgeTypeOffset; + const edgeFieldsCount = this.edgeFieldsCount; + const containmentEdges = this.containmentEdges; + const nodeFieldCount = this.nodeFieldCount; + const firstEdgeIndexes = this.firstEdgeIndexes; + const flags = this.flags; + const list = []; + for (let iter = this.rootNode().edges(); iter.hasNext(); iter.next()) { + if (iter.edge.node().isUserRoot()) { + list.push(iter.edge.node().nodeIndex / nodeFieldCount); + } + } + while (list.length) { + const nodeOrdinal = list.pop(); + if (flags[nodeOrdinal] & flag) { + continue; + } + flags[nodeOrdinal] |= flag; + const beginEdgeIndex = firstEdgeIndexes[nodeOrdinal]; + const endEdgeIndex = firstEdgeIndexes[nodeOrdinal + 1]; + for (let edgeIndex = beginEdgeIndex; edgeIndex < endEdgeIndex; edgeIndex += edgeFieldsCount) { + const childNodeIndex = containmentEdges.getValue( + edgeIndex + edgeToNodeOffset + ); + const childNodeOrdinal = childNodeIndex / nodeFieldCount; + if (flags[childNodeOrdinal] & flag) { + continue; + } + const type = containmentEdges.getValue(edgeIndex + edgeTypeOffset); + if (type === hiddenEdgeType || type === invisibleEdgeType || type === internalEdgeType || type === weakEdgeType) { + continue; + } + list.push(childNodeOrdinal); + } + } + } + markPageOwnedNodes() { + const edgeShortcutType = this.edgeShortcutType; + const edgeElementType = this.edgeElementType; + const edgeToNodeOffset = this.edgeToNodeOffset; + const edgeTypeOffset = this.edgeTypeOffset; + const edgeFieldsCount = this.edgeFieldsCount; + const edgeWeakType = this.edgeWeakType; + const firstEdgeIndexes = this.firstEdgeIndexes; + const containmentEdges = this.containmentEdges; + const nodeFieldCount = this.nodeFieldCount; + const nodesCount = this.nodeCount; + const flags = this.flags; + const pageObjectFlag = this.nodeFlags.pageObject; + const nodesToVisit = new Uint32Array(nodesCount); + let nodesToVisitLength = 0; + const rootNodeOrdinal = this.rootNodeIndexInternal / nodeFieldCount; + const node = this.rootNode(); + for (let edgeIndex = firstEdgeIndexes[rootNodeOrdinal], endEdgeIndex = firstEdgeIndexes[rootNodeOrdinal + 1]; edgeIndex < endEdgeIndex; edgeIndex += edgeFieldsCount) { + const edgeType = containmentEdges.getValue(edgeIndex + edgeTypeOffset); + const nodeIndex = containmentEdges.getValue(edgeIndex + edgeToNodeOffset); + if (edgeType === edgeElementType) { + node.nodeIndex = nodeIndex; + if (!node.isDocumentDOMTreesRoot()) { + continue; + } + } else if (edgeType !== edgeShortcutType) { + continue; + } + const nodeOrdinal = nodeIndex / nodeFieldCount; + nodesToVisit[nodesToVisitLength++] = nodeOrdinal; + flags[nodeOrdinal] |= pageObjectFlag; + } + while (nodesToVisitLength) { + const nodeOrdinal = nodesToVisit[--nodesToVisitLength]; + const beginEdgeIndex = firstEdgeIndexes[nodeOrdinal]; + const endEdgeIndex = firstEdgeIndexes[nodeOrdinal + 1]; + for (let edgeIndex = beginEdgeIndex; edgeIndex < endEdgeIndex; edgeIndex += edgeFieldsCount) { + const childNodeIndex = containmentEdges.getValue( + edgeIndex + edgeToNodeOffset + ); + const childNodeOrdinal = childNodeIndex / nodeFieldCount; + if (flags[childNodeOrdinal] & pageObjectFlag) { + continue; + } + const type = containmentEdges.getValue(edgeIndex + edgeTypeOffset); + if (type === edgeWeakType) { + continue; + } + nodesToVisit[nodesToVisitLength++] = childNodeOrdinal; + flags[childNodeOrdinal] |= pageObjectFlag; + } + } + } + calculateStatistics() { + const nodeFieldCount = this.nodeFieldCount; + const nodes = this.nodes; + const nodesLength = nodes.length; + const nodeTypeOffset = this.nodeTypeOffset; + const nodeSizeOffset = this.nodeSelfSizeOffset; + const nodeNativeType = this.nodeNativeType; + const nodeCodeType = this.nodeCodeType; + const nodeConsStringType = this.nodeConsStringType; + const nodeSlicedStringType = this.nodeSlicedStringType; + const nodeHiddenType = this.nodeHiddenType; + const nodeStringType = this.nodeStringType; + let sizeNative = this.profile.snapshot.extra_native_bytes ?? 0; + let sizeTypedArrays = 0; + let sizeCode = 0; + let sizeStrings = 0; + let sizeJSArrays = 0; + let sizeSystem = 0; + const node = this.rootNode(); + for (let nodeIndex = 0; nodeIndex < nodesLength; nodeIndex += nodeFieldCount) { + const nodeSize = nodes.getValue(nodeIndex + nodeSizeOffset); + const nodeType = nodes.getValue(nodeIndex + nodeTypeOffset); + if (nodeType === nodeHiddenType) { + sizeSystem += nodeSize; + continue; + } + node.nodeIndex = nodeIndex; + if (nodeType === nodeNativeType) { + sizeNative += nodeSize; + if (node.rawName() === "system / JSArrayBufferData") { + sizeTypedArrays += nodeSize; + } + } else if (nodeType === nodeCodeType) { + sizeCode += nodeSize; + } else if (nodeType === nodeConsStringType || nodeType === nodeSlicedStringType || nodeType === nodeStringType) { + sizeStrings += nodeSize; + } else if (node.rawName() === "Array") { + sizeJSArrays += this.calculateArraySize(node); + } + } + this.#statistics = { + total: this.totalSize, + native: { + total: sizeNative, + typedArrays: sizeTypedArrays + }, + v8heap: { + total: this.totalSize - sizeNative, + code: sizeCode, + jsArrays: sizeJSArrays, + strings: sizeStrings, + system: sizeSystem + } + }; + } + calculateArraySize(node) { + let size = node.selfSize(); + const beginEdgeIndex = node.edgeIndexesStart(); + const endEdgeIndex = node.edgeIndexesEnd(); + const containmentEdges = this.containmentEdges; + const strings = this.strings; + const edgeToNodeOffset = this.edgeToNodeOffset; + const edgeTypeOffset = this.edgeTypeOffset; + const edgeNameOffset = this.edgeNameOffset; + const edgeFieldsCount = this.edgeFieldsCount; + const edgeInternalType = this.edgeInternalType; + for (let edgeIndex = beginEdgeIndex; edgeIndex < endEdgeIndex; edgeIndex += edgeFieldsCount) { + const edgeType = containmentEdges.getValue(edgeIndex + edgeTypeOffset); + if (edgeType !== edgeInternalType) { + continue; + } + const edgeName = strings[containmentEdges.getValue(edgeIndex + edgeNameOffset)]; + if (edgeName !== "elements") { + continue; + } + const elementsNodeIndex = containmentEdges.getValue( + edgeIndex + edgeToNodeOffset + ); + node.nodeIndex = elementsNodeIndex; + if (node.retainersCount() === 1) { + size += node.selfSize(); + } + break; + } + return size; + } + getStatistics() { + return this.#statistics; + } +} +class JSHeapSnapshotNode extends HeapSnapshotNode { + constructor(snapshot, nodeIndex) { + super(snapshot, nodeIndex); + } + canBeQueried() { + const snapshot = this.snapshot; + const flags = snapshot.flagsOfNode(this); + return Boolean(flags & snapshot.nodeFlags.canBeQueried); + } + name() { + const snapshot = this.snapshot; + if (this.rawType() === snapshot.nodeConsStringType) { + return this.consStringName(); + } + if (this.rawType() === snapshot.nodeObjectType && this.rawName() === "Object") { + return this.#plainObjectName(); + } + return this.rawName(); + } + consStringName() { + const snapshot = this.snapshot; + const consStringType = snapshot.nodeConsStringType; + const edgeInternalType = snapshot.edgeInternalType; + const edgeFieldsCount = snapshot.edgeFieldsCount; + const edgeToNodeOffset = snapshot.edgeToNodeOffset; + const edgeTypeOffset = snapshot.edgeTypeOffset; + const edgeNameOffset = snapshot.edgeNameOffset; + const strings = snapshot.strings; + const edges = snapshot.containmentEdges; + const firstEdgeIndexes = snapshot.firstEdgeIndexes; + const nodeFieldCount = snapshot.nodeFieldCount; + const nodeTypeOffset = snapshot.nodeTypeOffset; + const nodeNameOffset = snapshot.nodeNameOffset; + const nodes = snapshot.nodes; + const nodesStack = []; + nodesStack.push(this.nodeIndex); + let name = ""; + while (nodesStack.length && name.length < 1024) { + const nodeIndex = nodesStack.pop(); + if (nodes.getValue(nodeIndex + nodeTypeOffset) !== consStringType) { + name += strings[nodes.getValue(nodeIndex + nodeNameOffset)]; + continue; + } + const nodeOrdinal = nodeIndex / nodeFieldCount; + const beginEdgeIndex = firstEdgeIndexes[nodeOrdinal]; + const endEdgeIndex = firstEdgeIndexes[nodeOrdinal + 1]; + let firstNodeIndex = 0; + let secondNodeIndex = 0; + for (let edgeIndex = beginEdgeIndex; edgeIndex < endEdgeIndex && (!firstNodeIndex || !secondNodeIndex); edgeIndex += edgeFieldsCount) { + const edgeType = edges.getValue(edgeIndex + edgeTypeOffset); + if (edgeType === edgeInternalType) { + const edgeName = strings[edges.getValue(edgeIndex + edgeNameOffset)]; + if (edgeName === "first") { + firstNodeIndex = edges.getValue(edgeIndex + edgeToNodeOffset); + } else if (edgeName === "second") { + secondNodeIndex = edges.getValue(edgeIndex + edgeToNodeOffset); + } + } + } + nodesStack.push(secondNodeIndex); + nodesStack.push(firstNodeIndex); + } + return name; + } + // Creates a name for plain JS objects, which looks something like + // '{propName, otherProp, thirdProp, ..., secondToLastProp, lastProp}'. + // A variable number of property names is included, depending on the length + // of the property names, so that the result fits nicely in a reasonably + // sized DevTools window. + #plainObjectName() { + const snapshot = this.snapshot; + const { edgeFieldsCount, edgePropertyType } = snapshot; + const edge = snapshot.createEdge(0); + let categoryNameStart = "{"; + let categoryNameEnd = "}"; + let edgeIndexFromStart = this.edgeIndexesStart(); + let edgeIndexFromEnd = this.edgeIndexesEnd() - edgeFieldsCount; + let nextFromEnd = false; + while (edgeIndexFromStart <= edgeIndexFromEnd) { + edge.edgeIndex = nextFromEnd ? edgeIndexFromEnd : edgeIndexFromStart; + if (edge.rawType() !== edgePropertyType || edge.name() === "__proto__") { + if (nextFromEnd) { + edgeIndexFromEnd -= edgeFieldsCount; + } else { + edgeIndexFromStart += edgeFieldsCount; + } + continue; + } + const formatted = JSHeapSnapshotNode.formatPropertyName(edge.name()); + if (categoryNameStart.length > 1 && categoryNameStart.length + categoryNameEnd.length + formatted.length > 100) { + break; + } + if (nextFromEnd) { + edgeIndexFromEnd -= edgeFieldsCount; + if (categoryNameEnd.length > 1) { + categoryNameEnd = ", " + categoryNameEnd; + } + categoryNameEnd = formatted + categoryNameEnd; + } else { + edgeIndexFromStart += edgeFieldsCount; + if (categoryNameStart.length > 1) { + categoryNameStart += ", "; + } + categoryNameStart += formatted; + } + nextFromEnd = !nextFromEnd; + } + if (edgeIndexFromStart <= edgeIndexFromEnd) { + categoryNameStart += ", ..."; + } + if (categoryNameEnd.length > 1) { + categoryNameStart += ", "; + } + return categoryNameStart + categoryNameEnd; + } + static formatPropertyName(name) { + if (/[,'"{}]/.test(name)) { + name = JSON.stringify({ [name]: 0 }); + name = name.substring(1, name.length - 3); + } + return name; + } + id() { + const snapshot = this.snapshot; + return snapshot.nodes.getValue(this.nodeIndex + snapshot.nodeIdOffset); + } + isHidden() { + return this.rawType() === this.snapshot.nodeHiddenType; + } + isArray() { + return this.rawType() === this.snapshot.nodeArrayType; + } + isSynthetic() { + return this.rawType() === this.snapshot.nodeSyntheticType; + } + isNative() { + return this.rawType() === this.snapshot.nodeNativeType; + } + isUserRoot() { + return !this.isSynthetic(); + } + isDocumentDOMTreesRoot() { + return this.isSynthetic() && this.rawName() === "(Document DOM trees)"; + } + serialize() { + const result = super.serialize(); + const snapshot = this.snapshot; + const flags = snapshot.flagsOfNode(this); + if (flags & snapshot.nodeFlags.canBeQueried) { + result.canBeQueried = true; + } + if (flags & snapshot.nodeFlags.detachedDOMTreeNode) { + result.detachedDOMTreeNode = true; + } + return result; + } +} +class JSHeapSnapshotEdge extends HeapSnapshotEdge { + constructor(snapshot, edgeIndex) { + super(snapshot, edgeIndex); + } + clone() { + const snapshot = this.snapshot; + return new JSHeapSnapshotEdge(snapshot, this.edgeIndex); + } + hasStringName() { + if (!this.isShortcut()) { + return this.hasStringNameInternal(); + } + return isNaN(parseInt(this.nameInternal(), 10)); + } + isElement() { + return this.rawType() === this.snapshot.edgeElementType; + } + isHidden() { + return this.rawType() === this.snapshot.edgeHiddenType; + } + isWeak() { + return this.rawType() === this.snapshot.edgeWeakType; + } + isInternal() { + return this.rawType() === this.snapshot.edgeInternalType; + } + isInvisible() { + return this.rawType() === this.snapshot.edgeInvisibleType; + } + isShortcut() { + return this.rawType() === this.snapshot.edgeShortcutType; + } + name() { + const name = this.nameInternal(); + if (!this.isShortcut()) { + return String(name); + } + const numName = parseInt(name, 10); + return String(isNaN(numName) ? name : numName); + } + toString() { + const name = this.name(); + switch (this.type()) { + case "context": + return "->" + name; + case "element": + return "[" + name + "]"; + case "weak": + return "[[" + name + "]]"; + case "property": + return name.indexOf(" ") === -1 ? "." + name : '["' + name + '"]'; + case "shortcut": + if (typeof name === "string") { + return name.indexOf(" ") === -1 ? "." + name : '["' + name + '"]'; + } + return "[" + name + "]"; + case "internal": + case "hidden": + case "invisible": + return "{" + name + "}"; + } + return "?" + name + "?"; + } + hasStringNameInternal() { + const type = this.rawType(); + const snapshot = this.snapshot; + return type !== snapshot.edgeElementType && type !== snapshot.edgeHiddenType; + } + nameInternal() { + return this.hasStringNameInternal() ? this.snapshot.strings[this.nameOrIndex()] : this.nameOrIndex(); + } + nameOrIndex() { + return this.edges.getValue(this.edgeIndex + this.snapshot.edgeNameOffset); + } + rawType() { + return this.edges.getValue(this.edgeIndex + this.snapshot.edgeTypeOffset); + } + nameIndex() { + if (!this.hasStringNameInternal()) { + throw new Error("Edge does not have string name"); + } + return this.nameOrIndex(); + } +} +class JSHeapSnapshotRetainerEdge extends HeapSnapshotRetainerEdge { + constructor(snapshot, retainerIndex) { + super(snapshot, retainerIndex); + } + clone() { + const snapshot = this.snapshot; + return new JSHeapSnapshotRetainerEdge(snapshot, this.retainerIndex()); + } + isHidden() { + return this.edge().isHidden(); + } + isInvisible() { + return this.edge().isInvisible(); + } + isShortcut() { + return this.edge().isShortcut(); + } + isWeak() { + return this.edge().isWeak(); + } +} + +var HeapSnapshot$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + HeapSnapshot: HeapSnapshot, + HeapSnapshotEdge: HeapSnapshotEdge, + HeapSnapshotEdgeIndexProvider: HeapSnapshotEdgeIndexProvider, + HeapSnapshotEdgeIterator: HeapSnapshotEdgeIterator, + HeapSnapshotEdgesProvider: HeapSnapshotEdgesProvider, + HeapSnapshotFilteredIterator: HeapSnapshotFilteredIterator, + HeapSnapshotIndexRangeIterator: HeapSnapshotIndexRangeIterator, + HeapSnapshotItemProvider: HeapSnapshotItemProvider, + HeapSnapshotNode: HeapSnapshotNode, + HeapSnapshotNodeIndexProvider: HeapSnapshotNodeIndexProvider, + HeapSnapshotNodeIterator: HeapSnapshotNodeIterator, + HeapSnapshotNodesProvider: HeapSnapshotNodesProvider, + HeapSnapshotProgress: HeapSnapshotProgress, + HeapSnapshotRetainerEdge: HeapSnapshotRetainerEdge, + HeapSnapshotRetainerEdgeIndexProvider: HeapSnapshotRetainerEdgeIndexProvider, + HeapSnapshotRetainerEdgeIterator: HeapSnapshotRetainerEdgeIterator, + JSHeapSnapshot: JSHeapSnapshot, + JSHeapSnapshotEdge: JSHeapSnapshotEdge, + JSHeapSnapshotNode: JSHeapSnapshotNode, + JSHeapSnapshotRetainerEdge: JSHeapSnapshotRetainerEdge, + SecondaryInitManager: SecondaryInitManager, + serializeUIString: serializeUIString +}); + +class HeapSnapshotWorkerDispatcher { + // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + #objects; + #postMessage; + constructor(postMessage) { + this.#objects = []; + this.#postMessage = postMessage; + } + sendEvent(name, data2) { + this.#postMessage({ eventName: name, data: data2 }); + } + async dispatchMessage({ + data, + ports + }) { + const response = { + callId: data.callId, + result: null, + error: void 0, + errorCallStack: void 0, + errorMethodName: void 0 + }; + try { + switch (data.disposition) { + case "createLoader": + this.#objects[data.objectId] = new HeapSnapshotLoader(this); + break; + case "dispose": { + delete this.#objects[data.objectId]; + break; + } + case "getter": { + const object = this.#objects[data.objectId]; + const result = object[data.methodName]; + response.result = result; + break; + } + case "factory": { + const object = this.#objects[data.objectId]; + const args = data.methodArguments.slice(); + args.push(...ports); + const result = await object[data.methodName].apply(object, args); + if (result) { + this.#objects[data.newObjectId] = result; + } + response.result = Boolean(result); + break; + } + case "method": { + const object = this.#objects[data.objectId]; + response.result = object[data.methodName].apply( + object, + data.methodArguments + ); + break; + } + case "evaluateForTest": { + try { + globalThis.HeapSnapshotWorker = { + AllocationProfile: AllocationProfile$1, + HeapSnapshot: HeapSnapshot$1, + HeapSnapshotLoader: HeapSnapshotLoader$1 + }; + globalThis.HeapSnapshotModel = HeapSnapshotModel; + response.result = await eval(data.source); + } catch (error) { + response.result = error.toString(); + } + break; + } + case "setupForSecondaryInit": { + this.#objects[data.objectId] = new SecondaryInitManager( + ports[0] + ); + } + } + } catch (error) { + response.error = error.toString(); + response.errorCallStack = error.stack; + if (data.methodName) { + response.errorMethodName = data.methodName; + } + } + this.#postMessage(response); + } +} + +class BalancedJSONTokenizer { + callback; + index; + balance; + buffer; + findMultiple; + closingDoubleQuoteRegex; + lastBalancedIndex; + constructor(callback, findMultiple) { + this.callback = callback; + this.index = 0; + this.balance = 0; + this.buffer = ""; + this.findMultiple = findMultiple || false; + this.closingDoubleQuoteRegex = /[^\\](?:\\\\)*"/g; + } + write(chunk) { + this.buffer += chunk; + const lastIndex = this.buffer.length; + const buffer = this.buffer; + let index; + for (index = this.index; index < lastIndex; ++index) { + const character = buffer[index]; + if (character === '"') { + this.closingDoubleQuoteRegex.lastIndex = index; + if (!this.closingDoubleQuoteRegex.test(buffer)) { + break; + } + index = this.closingDoubleQuoteRegex.lastIndex - 1; + } else if (character === "{") { + ++this.balance; + } else if (character === "}") { + --this.balance; + if (this.balance < 0) { + this.reportBalanced(); + return false; + } + if (!this.balance) { + this.lastBalancedIndex = index + 1; + if (!this.findMultiple) { + break; + } + } + } else if (character === "]" && !this.balance) { + this.reportBalanced(); + return false; + } + } + this.index = index; + this.reportBalanced(); + return true; + } + reportBalanced() { + if (!this.lastBalancedIndex) { + return; + } + this.callback(this.buffer.slice(0, this.lastBalancedIndex)); + this.buffer = this.buffer.slice(this.lastBalancedIndex); + this.index -= this.lastBalancedIndex; + this.lastBalancedIndex = 0; + } + remainder() { + return this.buffer; + } +} + +class HeapSnapshotLoader { + #progress; + #buffer; + #dataCallback; + #done; + // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + #snapshot; + #array; + #arrayIndex; + #json = ""; + parsingComplete; + constructor(dispatcherOrProgress) { + this.#reset(); + if (dispatcherOrProgress instanceof HeapSnapshotWorkerDispatcher) { + this.#progress = new HeapSnapshotProgress(dispatcherOrProgress); + } else { + this.#progress = dispatcherOrProgress; + } + this.#buffer = []; + this.#dataCallback = null; + this.#done = false; + this.parsingComplete = this.#parseInput(); + } + dispose() { + this.#reset(); + } + #reset() { + this.#json = ""; + this.#snapshot = void 0; + } + close() { + this.#done = true; + if (this.#dataCallback) { + this.#dataCallback(""); + } + } + async buildSnapshot(secondWorker) { + this.#snapshot = this.#snapshot || {}; + this.#progress.updateStatus("Processing snapshot\u2026"); + const result = new JSHeapSnapshot( + this.#snapshot, + this.#progress + ); + await result.initialize(secondWorker); + this.#reset(); + return result; + } + #parseUintArray() { + let index = 0; + const char0 = "0".charCodeAt(0); + const char9 = "9".charCodeAt(0); + const closingBracket = "]".charCodeAt(0); + const length = this.#json.length; + while (true) { + while (index < length) { + const code = this.#json.charCodeAt(index); + if (char0 <= code && code <= char9) { + break; + } else if (code === closingBracket) { + this.#json = this.#json.slice(index + 1); + return false; + } + ++index; + } + if (index === length) { + this.#json = ""; + return true; + } + let nextNumber = 0; + const startIndex = index; + while (index < length) { + const code = this.#json.charCodeAt(index); + if (char0 > code || code > char9) { + break; + } + nextNumber *= 10; + nextNumber += code - char0; + ++index; + } + if (index === length) { + this.#json = this.#json.slice(startIndex); + return true; + } + if (!this.#array) { + throw new Error("Array not instantiated"); + } + this.#array.setValue(this.#arrayIndex++, nextNumber); + } + } + #parseStringsArray() { + this.#progress.updateStatus("Parsing strings\u2026"); + const closingBracketIndex = this.#json.lastIndexOf("]"); + if (closingBracketIndex === -1) { + throw new Error("Incomplete JSON"); + } + this.#json = this.#json.slice(0, closingBracketIndex + 1); + if (!this.#snapshot) { + throw new Error("No snapshot in parseStringsArray"); + } + this.#snapshot.strings = JSON.parse(this.#json); + } + write(chunk) { + this.#buffer.push(chunk); + if (!this.#dataCallback) { + return; + } + this.#dataCallback(this.#buffer.shift()); + this.#dataCallback = null; + } + #fetchChunk() { + if (this.#buffer.length > 0) { + return Promise.resolve(this.#buffer.shift()); + } + const { promise, resolve } = withResolvers(); + this.#dataCallback = resolve; + return promise; + } + async #findToken(token, startIndex) { + while (true) { + const pos = this.#json.indexOf(token, startIndex || 0); + if (pos !== -1) { + return pos; + } + startIndex = this.#json.length - token.length + 1; + this.#json += await this.#fetchChunk(); + } + } + async #parseArray(name, title, length) { + const nameIndex = await this.#findToken(name); + const bracketIndex = await this.#findToken("[", nameIndex); + this.#json = this.#json.slice(bracketIndex + 1); + this.#array = length === void 0 ? createExpandableBigUint32Array() : createFixedBigUint32Array(length); + this.#arrayIndex = 0; + while (this.#parseUintArray()) { + if (length) { + this.#progress.updateProgress( + title, + this.#arrayIndex, + this.#array.length + ); + } else { + this.#progress.updateStatus(title); + } + this.#json += await this.#fetchChunk(); + } + const result = this.#array; + this.#array = null; + return result; + } + async #parseInput() { + const snapshotToken = '"snapshot"'; + const snapshotTokenIndex = await this.#findToken(snapshotToken); + if (snapshotTokenIndex === -1) { + throw new Error("Snapshot token not found"); + } + this.#progress.updateStatus("Loading snapshot info\u2026"); + const json = this.#json.slice( + snapshotTokenIndex + snapshotToken.length + 1 + ); + let jsonTokenizerDone = false; + const jsonTokenizer = new BalancedJSONTokenizer((metaJSON) => { + this.#json = jsonTokenizer.remainder(); + jsonTokenizerDone = true; + this.#snapshot = this.#snapshot || {}; + this.#snapshot.snapshot = JSON.parse(metaJSON); + }); + jsonTokenizer.write(json); + while (!jsonTokenizerDone) { + jsonTokenizer.write(await this.#fetchChunk()); + } + this.#snapshot = this.#snapshot || {}; + const nodes = await this.#parseArray( + '"nodes"', + "Loading nodes\u2026 {PH1}%", + this.#snapshot.snapshot.meta.node_fields.length * this.#snapshot.snapshot.node_count + ); + this.#snapshot.nodes = nodes; + const edges = await this.#parseArray( + '"edges"', + "Loading edges\u2026 {PH1}%", + this.#snapshot.snapshot.meta.edge_fields.length * this.#snapshot.snapshot.edge_count + ); + this.#snapshot.edges = edges; + if (this.#snapshot.snapshot.trace_function_count) { + const traceFunctionInfos = await this.#parseArray( + '"trace_function_infos"', + "Loading allocation traces\u2026 {PH1}%", + this.#snapshot.snapshot.meta.trace_function_info_fields.length * this.#snapshot.snapshot.trace_function_count + ); + this.#snapshot.trace_function_infos = traceFunctionInfos.asUint32ArrayOrFail(); + const thisTokenEndIndex = await this.#findToken(":"); + const nextTokenIndex = await this.#findToken('"', thisTokenEndIndex); + const openBracketIndex = this.#json.indexOf("["); + const closeBracketIndex = this.#json.lastIndexOf("]", nextTokenIndex); + this.#snapshot.trace_tree = JSON.parse( + this.#json.substring(openBracketIndex, closeBracketIndex + 1) + ); + this.#json = this.#json.slice(closeBracketIndex + 1); + } + if (this.#snapshot.snapshot.meta.sample_fields) { + const samples = await this.#parseArray('"samples"', "Loading samples\u2026"); + this.#snapshot.samples = samples.asArrayOrFail(); + } + if (this.#snapshot.snapshot.meta["location_fields"]) { + const locations = await this.#parseArray( + '"locations"', + "Loading locations\u2026" + ); + this.#snapshot.locations = locations.asArrayOrFail(); + } else { + this.#snapshot.locations = []; + } + this.#progress.updateStatus("Loading strings\u2026"); + const stringsTokenIndex = await this.#findToken('"strings"'); + const bracketIndex = await this.#findToken("[", stringsTokenIndex); + this.#json = this.#json.slice(bracketIndex); + while (this.#buffer.length > 0 || !this.#done) { + this.#json += await this.#fetchChunk(); + } + this.#parseStringsArray(); + } +} + +export { AllocationNodeCallers as A, AggregateForDiff as B, DiffForClass as C, Diff as D, Edge as E, ComparatorConfig as F, StaticData as G, HeapSnapshotLoader as H, ItemsRange as I, JSHeapSnapshot as J, NodeFilter as K, SearchConfig as L, Samples as M, Node as N, Location as O, HeapSnapshotWorkerDispatcher as P, SecondaryInitManager as S, WorkerCommand as W, HeapSnapshotProgress as a, HeapSnapshotEdge as b, HeapSnapshotNodeIndexProvider as c, HeapSnapshotEdgeIndexProvider as d, HeapSnapshotRetainerEdgeIndexProvider as e, HeapSnapshotEdgeIterator as f, HeapSnapshotRetainerEdge as g, HeapSnapshotRetainerEdgeIterator as h, HeapSnapshotNode as i, HeapSnapshotNodeIterator as j, HeapSnapshotIndexRangeIterator as k, HeapSnapshotFilteredIterator as l, HeapSnapshot as m, HeapSnapshotItemProvider as n, HeapSnapshotEdgesProvider as o, HeapSnapshotNodesProvider as p, JSHeapSnapshotNode as q, JSHeapSnapshotEdge as r, serializeUIString as s, JSHeapSnapshotRetainerEdge as t, HeapSnapshotProgressEvent as u, baseSystemDistance as v, baseUnreachableDistance as w, SerializedAllocationNode as x, AllocationStackFrame as y, Aggregate as z }; diff --git a/internal/heapsnapshot/dist/heap_snapshot_worker-entrypoint.js b/internal/heapsnapshot/dist/heap_snapshot_worker-entrypoint.js new file mode 100755 index 000000000..ddb62a6bb --- /dev/null +++ b/internal/heapsnapshot/dist/heap_snapshot_worker-entrypoint.js @@ -0,0 +1,8 @@ +import { parentPort } from 'node:worker_threads'; +import { P as HeapSnapshotWorkerDispatcher } from './HeapSnapshotLoader-CpV_0rIo.js'; + +const dispatcher = new HeapSnapshotWorkerDispatcher( + parentPort.postMessage.bind(parentPort) +); +parentPort.on("message", dispatcher.dispatchMessage.bind(dispatcher)); +parentPort.postMessage("workerReady"); diff --git a/internal/heapsnapshot/dist/index.d.ts b/internal/heapsnapshot/dist/index.d.ts new file mode 100644 index 000000000..7882e7c89 --- /dev/null +++ b/internal/heapsnapshot/dist/index.d.ts @@ -0,0 +1,736 @@ +import { MessagePort } from 'node:worker_threads'; +import { Readable } from 'node:stream'; + +declare const HeapSnapshotProgressEvent: { + Update: string; + BrokenSnapshot: string; +}; +declare const baseSystemDistance = 100000000; +declare const baseUnreachableDistance: number; +declare class AllocationNodeCallers { + nodesWithSingleCaller: SerializedAllocationNode[]; + branchingCallers: SerializedAllocationNode[]; + constructor(nodesWithSingleCaller: SerializedAllocationNode[], branchingCallers: SerializedAllocationNode[]); +} +declare class SerializedAllocationNode { + id: number; + name: string; + scriptName: string; + scriptId: number; + line: number; + column: number; + count: number; + size: number; + liveCount: number; + liveSize: number; + hasChildren: boolean; + constructor(nodeId: number, functionName: string, scriptName: string, scriptId: number, line: number, column: number, count: number, size: number, liveCount: number, liveSize: number, hasChildren: boolean); +} +declare class AllocationStackFrame { + functionName: string; + scriptName: string; + scriptId: number; + line: number; + column: number; + constructor(functionName: string, scriptName: string, scriptId: number, line: number, column: number); +} +declare class Node { + id: number; + name: string; + distance: number; + nodeIndex: number; + retainedSize: number; + selfSize: number; + type: string; + canBeQueried: boolean; + detachedDOMTreeNode: boolean; + isAddedNotRemoved: boolean | null; + ignored: boolean; + constructor(id: number, name: string, distance: number, nodeIndex: number, retainedSize: number, selfSize: number, type: string); +} +declare class Edge { + name: string; + node: Node; + type: string; + edgeIndex: number; + isAddedNotRemoved: boolean | null; + constructor(name: string, node: Node, type: string, edgeIndex: number); +} +declare class Aggregate { + count: number; + distance: number; + self: number; + maxRet: number; + name: string; + idxs: number[]; + constructor(); +} +declare class AggregateForDiff { + name: string; + indexes: number[]; + ids: number[]; + selfSizes: number[]; + constructor(); +} +declare class Diff { + name: string; + addedCount: number; + removedCount: number; + addedSize: number; + removedSize: number; + deletedIndexes: number[]; + addedIndexes: number[]; + countDelta: number; + sizeDelta: number; + constructor(name: string); +} +declare class DiffForClass { + name: string; + addedCount: number; + removedCount: number; + addedSize: number; + removedSize: number; + deletedIndexes: number[]; + addedIndexes: number[]; + countDelta: number; + sizeDelta: number; + constructor(); +} +declare class ComparatorConfig { + fieldName1: string; + ascending1: boolean; + fieldName2: string; + ascending2: boolean; + constructor(fieldName1: string, ascending1: boolean, fieldName2: string, ascending2: boolean); +} +declare class WorkerCommand { + callId: number; + disposition: string; + objectId: number; + newObjectId: number; + methodName: string; + methodArguments: any[]; + source: string; + constructor(); +} +declare class ItemsRange { + startPosition: number; + endPosition: number; + totalLength: number; + items: Array; + constructor(startPosition: number, endPosition: number, totalLength: number, items: Array); +} +declare class StaticData { + nodeCount: number; + rootNodeIndex: number; + totalSize: number; + maxJSObjectId: number; + constructor(nodeCount: number, rootNodeIndex: number, totalSize: number, maxJSObjectId: number); +} +interface Statistics { + total: number; + native: { + total: number; + typedArrays: number; + }; + v8heap: { + total: number; + code: number; + jsArrays: number; + strings: number; + system: number; + }; +} +declare class NodeFilter { + minNodeId: number | undefined; + maxNodeId: number | undefined; + allocationNodeId: number | undefined; + filterName: string | undefined; + constructor(minNodeId?: number, maxNodeId?: number); + equals(o: NodeFilter): boolean; +} +declare class SearchConfig { + query: string; + caseSensitive: boolean; + isRegex: boolean; + shouldJump: boolean; + jumpBackward: boolean; + constructor(query: string, caseSensitive: boolean, isRegex: boolean, shouldJump: boolean, jumpBackward: boolean); + toSearchRegex(_global?: boolean): { + regex: RegExp; + fromQuery: boolean; + }; +} +declare class Samples { + timestamps: number[]; + lastAssignedIds: number[]; + sizes: number[]; + constructor(timestamps: number[], lastAssignedIds: number[], sizes: number[]); +} +declare class Location { + scriptId: number; + lineNumber: number; + columnNumber: number; + constructor(scriptId: number, lineNumber: number, columnNumber: number); +} + +declare class HeapSnapshotWorkerDispatcher { + #private; + constructor(postMessage: MessagePort['postMessage']); + sendEvent(name: string, data: unknown): void; + dispatchMessage({ data, ports, }: { + data: WorkerCommand; + ports: readonly MessagePort[]; + }): Promise; +} + +/** + * An object which provides functionality similar to Uint32Array. It may be + * implemented as: + * 1. A Uint32Array, + * 2. An array of Uint32Arrays, to support more data than Uint32Array, or + * 3. A plain array, in which case the length may change by setting values. + */ +interface BigUint32Array { + get length(): number; + getValue(index: number): number; + setValue(index: number, value: number): void; + asUint32ArrayOrFail(): Uint32Array; + asArrayOrFail(): number[]; +} +interface BitVector { + getBit(index: number): boolean; + setBit(index: number): void; + clearBit(index: number): void; + previous(index: number): number; + get buffer(): ArrayBuffer; +} + +interface HeapSnapshotItem { + itemIndex(): number; + serialize(): Object; +} +declare class HeapSnapshotEdge implements HeapSnapshotItem { + snapshot: HeapSnapshot; + protected readonly edges: BigUint32Array; + edgeIndex: number; + constructor(snapshot: HeapSnapshot, edgeIndex?: number); + clone(): HeapSnapshotEdge; + hasStringName(): boolean; + name(): string; + node(): HeapSnapshotNode; + nodeIndex(): number; + toString(): string; + type(): string; + itemIndex(): number; + serialize(): Edge; + rawType(): number; + isInternal(): boolean; + isInvisible(): boolean; + isWeak(): boolean; + getValueForSorting(_fieldName: string): number; + nameIndex(): number; +} +interface HeapSnapshotItemIterator { + hasNext(): boolean; + item(): HeapSnapshotItem; + next(): void; +} +interface HeapSnapshotItemIndexProvider { + itemForIndex(newIndex: number): HeapSnapshotItem; +} +declare class HeapSnapshotNodeIndexProvider implements HeapSnapshotItemIndexProvider { + #private; + constructor(snapshot: HeapSnapshot); + itemForIndex(index: number): HeapSnapshotNode; +} +declare class HeapSnapshotEdgeIndexProvider implements HeapSnapshotItemIndexProvider { + #private; + constructor(snapshot: HeapSnapshot); + itemForIndex(index: number): HeapSnapshotEdge; +} +declare class HeapSnapshotRetainerEdgeIndexProvider implements HeapSnapshotItemIndexProvider { + #private; + constructor(snapshot: HeapSnapshot); + itemForIndex(index: number): HeapSnapshotRetainerEdge; +} +declare class HeapSnapshotEdgeIterator implements HeapSnapshotItemIterator { + #private; + edge: JSHeapSnapshotEdge; + constructor(node: HeapSnapshotNode); + hasNext(): boolean; + item(): HeapSnapshotEdge; + next(): void; +} +declare class HeapSnapshotRetainerEdge implements HeapSnapshotItem { + #private; + protected snapshot: HeapSnapshot; + constructor(snapshot: HeapSnapshot, retainerIndex: number); + clone(): HeapSnapshotRetainerEdge; + hasStringName(): boolean; + name(): string; + nameIndex(): number; + node(): HeapSnapshotNode; + nodeIndex(): number; + retainerIndex(): number; + setRetainerIndex(retainerIndex: number): void; + set edgeIndex(edgeIndex: number); + private nodeInternal; + protected edge(): JSHeapSnapshotEdge; + toString(): string; + itemIndex(): number; + serialize(): Edge; + type(): string; + isInternal(): boolean; + getValueForSorting(fieldName: string): number; +} +declare class HeapSnapshotRetainerEdgeIterator implements HeapSnapshotItemIterator { + #private; + retainer: JSHeapSnapshotRetainerEdge; + constructor(retainedNode: HeapSnapshotNode); + hasNext(): boolean; + item(): HeapSnapshotRetainerEdge; + next(): void; +} +declare class HeapSnapshotNode implements HeapSnapshotItem { + #private; + snapshot: HeapSnapshot; + nodeIndex: number; + constructor(snapshot: HeapSnapshot, nodeIndex?: number); + distance(): number; + distanceForRetainersView(): number; + className(): string; + classIndex(): number; + classKeyInternal(): string | number; + setClassIndex(index: number): void; + dominatorIndex(): number; + edges(): HeapSnapshotEdgeIterator; + edgesCount(): number; + id(): number; + rawName(): string; + isRoot(): boolean; + isUserRoot(): boolean; + isHidden(): boolean; + isArray(): boolean; + isSynthetic(): boolean; + isDocumentDOMTreesRoot(): boolean; + name(): string; + retainedSize(): number; + retainers(): HeapSnapshotRetainerEdgeIterator; + retainersCount(): number; + selfSize(): number; + type(): string; + traceNodeId(): number; + itemIndex(): number; + serialize(): Node; + rawNameIndex(): number; + edgeIndexesStart(): number; + edgeIndexesEnd(): number; + ordinal(): number; + nextNodeIndex(): number; + rawType(): number; + isFlatConsString(): boolean; + detachedness(): DOMLinkState; + setDetachedness(detachedness: DOMLinkState): void; +} +declare class HeapSnapshotNodeIterator implements HeapSnapshotItemIterator { + #private; + node: HeapSnapshotNode; + constructor(node: HeapSnapshotNode); + hasNext(): boolean; + item(): HeapSnapshotNode; + next(): void; +} +declare class HeapSnapshotIndexRangeIterator implements HeapSnapshotItemIterator { + #private; + constructor(itemProvider: HeapSnapshotItemIndexProvider, indexes: number[] | Uint32Array); + hasNext(): boolean; + item(): HeapSnapshotItem; + next(): void; +} +declare class HeapSnapshotFilteredIterator implements HeapSnapshotItemIterator { + #private; + constructor(iterator: HeapSnapshotItemIterator, filter?: (arg0: HeapSnapshotItem) => boolean); + hasNext(): boolean; + item(): HeapSnapshotItem; + next(): void; + private skipFilteredItems; +} +declare function serializeUIString(string: string, values?: Record): string; +declare class HeapSnapshotProgress { + #private; + constructor(dispatcher?: HeapSnapshotWorkerDispatcher); + updateStatus(status: string): void; + updateProgress(title: string, value: number, total: number): void; + reportProblem(error: string): void; + private sendUpdateEvent; +} +interface Profile { + root_index: number; + nodes: BigUint32Array; + edges: BigUint32Array; + snapshot: HeapSnapshotHeader; + samples: number[]; + strings: string[]; + locations: number[]; + trace_function_infos: Uint32Array; + trace_tree: Object; +} +interface LiveObjects { + [x: number]: { + count: number; + size: number; + ids: number[]; + }; +} +interface SecondaryInitArgumentsStep1 { + edgeToNodeOrdinals: Uint32Array; + firstEdgeIndexes: Uint32Array; + nodeCount: number; + edgeFieldsCount: number; + nodeFieldCount: number; +} +interface SecondaryInitArgumentsStep2 { + rootNodeOrdinal: number; + essentialEdgesBuffer: ArrayBuffer; +} +interface SecondaryInitArgumentsStep3 { + nodeSelfSizes: Uint32Array; +} +type ArgumentsToBuildRetainers = SecondaryInitArgumentsStep1; +interface Retainers { + firstRetainerIndex: Uint32Array; + retainingNodes: Uint32Array; + retainingEdges: Uint32Array; +} +interface ArgumentsToComputeDominatorsAndRetainedSizes extends SecondaryInitArgumentsStep1, Retainers, SecondaryInitArgumentsStep2 { + essentialEdges: BitVector; + port: MessagePort; + nodeSelfSizesPromise: Promise; +} +interface DominatorsAndRetainedSizes { + dominatorsTree: Uint32Array; + retainedSizes: Float64Array; +} +interface ArgumentsToBuildDominatedNodes extends ArgumentsToComputeDominatorsAndRetainedSizes, DominatorsAndRetainedSizes { +} +interface DominatedNodes { + firstDominatedNodeIndex: Uint32Array; + dominatedNodes: Uint32Array; +} +declare class SecondaryInitManager { + argsStep1: Promise; + argsStep2: Promise; + argsStep3: Promise; + constructor(port: MessagePort); + private getNodeSelfSizes; + private initialize; +} +/** + * DOM node link state. + */ +declare const enum DOMLinkState { + UNKNOWN = 0, + ATTACHED = 1, + DETACHED = 2 +} +declare abstract class HeapSnapshot { + #private; + nodes: BigUint32Array; + containmentEdges: BigUint32Array; + strings: string[]; + rootNodeIndexInternal: number; + profile: Profile; + nodeTypeOffset: number; + nodeNameOffset: number; + nodeIdOffset: number; + nodeSelfSizeOffset: number; + nodeTraceNodeIdOffset: number; + nodeFieldCount: number; + nodeTypes: string[]; + nodeArrayType: number; + nodeHiddenType: number; + nodeObjectType: number; + nodeNativeType: number; + nodeStringType: number; + nodeConsStringType: number; + nodeSlicedStringType: number; + nodeCodeType: number; + nodeSyntheticType: number; + nodeClosureType: number; + nodeRegExpType: number; + edgeFieldsCount: number; + edgeTypeOffset: number; + edgeNameOffset: number; + edgeToNodeOffset: number; + edgeTypes: string[]; + edgeElementType: number; + edgeHiddenType: number; + edgeInternalType: number; + edgeShortcutType: number; + edgeWeakType: number; + edgeInvisibleType: number; + edgePropertyType: number; + nodeCount: number; + retainedSizes: Float64Array; + firstEdgeIndexes: Uint32Array; + retainingNodes: Uint32Array; + retainingEdges: Uint32Array; + firstRetainerIndex: Uint32Array; + nodeDistances: Int32Array; + firstDominatedNodeIndex: Uint32Array; + dominatedNodes: Uint32Array; + dominatorsTree: Uint32Array; + nodeDetachednessAndClassIndexOffset: number; + detachednessAndClassIndexArray?: Uint32Array; + constructor(profile: Profile, progress: HeapSnapshotProgress); + initialize(secondWorker: MessagePort): Promise; + private startInitStep1InSecondThread; + private startInitStep2InSecondThread; + private startInitStep3InSecondThread; + private installResultsFromSecondThread; + private buildEdgeIndexes; + static buildRetainers(inputs: ArgumentsToBuildRetainers): Retainers; + abstract createNode(_nodeIndex?: number): HeapSnapshotNode; + abstract createEdge(_edgeIndex: number): JSHeapSnapshotEdge; + abstract createRetainingEdge(_retainerIndex: number): JSHeapSnapshotRetainerEdge; + private allNodes; + rootNode(): HeapSnapshotNode; + get rootNodeIndex(): number; + get totalSize(): number; + private createFilter; + search(searchConfig: SearchConfig, nodeFilter: NodeFilter): number[]; + aggregatesWithFilter(nodeFilter: NodeFilter): { + [x: string]: Aggregate; + }; + private createNodeIdFilter; + private createAllocationStackFilter; + private createNamedFilter; + getAggregatesByClassKey(sortedIndexes: boolean, key?: string, filter?: (arg0: HeapSnapshotNode) => boolean): { + [x: string]: Aggregate; + }; + allocationTracesTops(): SerializedAllocationNode[]; + allocationNodeCallers(nodeId: number): AllocationNodeCallers; + allocationStack(nodeIndex: number): AllocationStackFrame[] | null; + aggregatesForDiff(interfaceDefinitions: string): { + [x: string]: AggregateForDiff; + }; + isUserRoot(_node: HeapSnapshotNode): boolean; + calculateShallowSizes(): void; + calculateDistances(isForRetainersView: boolean, filter?: (arg0: HeapSnapshotNode, arg1: HeapSnapshotEdge) => boolean): void; + private bfs; + private buildAggregates; + private calculateClassesRetainedSize; + private sortAggregateIndexes; + tryParseWeakMapEdgeName(edgeNameIndex: number): { + duplicatedPart: string; + tableId: string; + } | undefined; + private computeIsEssentialEdge; + private initEssentialEdges; + static hasOnlyWeakRetainers(inputs: ArgumentsToComputeDominatorsAndRetainedSizes, nodeOrdinal: number): boolean; + static calculateDominatorsAndRetainedSizes(inputs: ArgumentsToComputeDominatorsAndRetainedSizes): Promise; + static buildDominatedNodes(inputs: ArgumentsToBuildDominatedNodes): DominatedNodes; + private calculateObjectNames; + interfaceDefinitions(): string; + private isPlainJSObject; + private inferInterfaceDefinitions; + private applyInterfaceDefinitions; + /** + * Iterates children of a node. + */ + private iterateFilteredChildren; + /** + * Adds a string to the snapshot. + */ + private addString; + /** + * The phase propagates whether a node is attached or detached through the + * graph and adjusts the low-level representation of nodes. + * + * State propagation: + * 1. Any object reachable from an attached object is itself attached. + * 2. Any object reachable from a detached object that is not already + * attached is considered detached. + * + * Representation: + * - Name of any detached node is changed from """ to + * "Detached ". + */ + private propagateDOMState; + private buildSamples; + private buildLocationMap; + getLocation(nodeIndex: number): Location | null; + getSamples(): Samples | null; + calculateFlags(): void; + calculateStatistics(): void; + userObjectsMapAndFlag(): { + map: Uint8Array; + flag: number; + } | null; + calculateSnapshotDiff(baseSnapshotId: string, baseSnapshotAggregates: { + [x: string]: AggregateForDiff; + }): { + [x: string]: Diff; + }; + private calculateDiffForClass; + private nodeForSnapshotObjectId; + classKeyFromClassKeyInternal(key: string | number): string; + nodeClassKey(snapshotObjectId: number): string | null; + idsOfObjectsWithName(name: string): number[]; + createEdgesProvider(nodeIndex: number): HeapSnapshotEdgesProvider; + createEdgesProviderForTest(nodeIndex: number, filter: ((arg0: HeapSnapshotEdge) => boolean) | null): HeapSnapshotEdgesProvider; + retainingEdgesFilter(): ((arg0: HeapSnapshotEdge) => boolean) | null; + containmentEdgesFilter(): ((arg0: HeapSnapshotEdge) => boolean) | null; + createRetainingEdgesProvider(nodeIndex: number): HeapSnapshotEdgesProvider; + createAddedNodesProvider(baseSnapshotId: string, classKey: string): HeapSnapshotNodesProvider; + createDeletedNodesProvider(nodeIndexes: number[]): HeapSnapshotNodesProvider; + createNodesProviderForClass(classKey: string, nodeFilter: NodeFilter): HeapSnapshotNodesProvider; + private maxJsNodeId; + updateStaticData(): StaticData; + ignoreNodeInRetainersView(nodeIndex: number): void; + unignoreNodeInRetainersView(nodeIndex: number): void; + unignoreAllNodesInRetainersView(): void; + areNodesIgnoredInRetainersView(): boolean; + getDistanceForRetainersView(nodeIndex: number): number; + isNodeIgnoredInRetainersView(nodeIndex: number): boolean; + isEdgeIgnoredInRetainersView(edgeIndex: number): boolean; +} +interface HeapSnapshotMetaInfo { + location_fields: string[]; + node_fields: string[]; + node_types: string[][]; + edge_fields: string[]; + edge_types: string[][]; + trace_function_info_fields: string[]; + trace_node_fields: string[]; + sample_fields: string[]; + type_strings: { + [key: string]: string; + }; +} +interface HeapSnapshotHeader { + title: string; + meta: HeapSnapshotMetaInfo; + node_count: number; + edge_count: number; + trace_function_count: number; + root_index: number; + extra_native_bytes?: number; +} +declare abstract class HeapSnapshotItemProvider { + #private; + protected readonly iterator: HeapSnapshotItemIterator; + protected iterationOrder: number[] | null; + protected currentComparator: ComparatorConfig | null; + constructor(iterator: HeapSnapshotItemIterator, indexProvider: HeapSnapshotItemIndexProvider); + protected createIterationOrder(): void; + isEmpty(): boolean; + serializeItemsRange(begin: number, end: number): ItemsRange; + sortAndRewind(comparator: ComparatorConfig): void; + abstract sort(comparator: ComparatorConfig, leftBound: number, rightBound: number, windowLeft: number, windowRight: number): void; +} +declare class HeapSnapshotEdgesProvider extends HeapSnapshotItemProvider { + snapshot: HeapSnapshot; + constructor(snapshot: HeapSnapshot, filter: ((arg0: HeapSnapshotEdge) => boolean) | null, edgesIter: HeapSnapshotEdgeIterator | HeapSnapshotRetainerEdgeIterator, indexProvider: HeapSnapshotItemIndexProvider); + sort(comparator: ComparatorConfig, leftBound: number, rightBound: number, windowLeft: number, windowRight: number): void; +} +declare class HeapSnapshotNodesProvider extends HeapSnapshotItemProvider { + snapshot: HeapSnapshot; + constructor(snapshot: HeapSnapshot, nodeIndexes: number[] | Uint32Array); + nodePosition(snapshotObjectId: number): number; + private buildCompareFunction; + sort(comparator: ComparatorConfig, leftBound: number, rightBound: number, windowLeft: number, windowRight: number): void; +} +declare class JSHeapSnapshot extends HeapSnapshot { + #private; + readonly nodeFlags: { + canBeQueried: number; + detachedDOMTreeNode: number; + pageObject: number; + }; + private flags; + constructor(profile: Profile, progress: HeapSnapshotProgress); + createNode(nodeIndex?: number): JSHeapSnapshotNode; + createEdge(edgeIndex: number): JSHeapSnapshotEdge; + createRetainingEdge(retainerIndex: number): JSHeapSnapshotRetainerEdge; + containmentEdgesFilter(): (arg0: HeapSnapshotEdge) => boolean; + retainingEdgesFilter(): (arg0: HeapSnapshotEdge) => boolean; + calculateFlags(): void; + calculateShallowSizes(): void; + calculateDistances(isForRetainersView: boolean): void; + isUserRoot(node: HeapSnapshotNode): boolean; + userObjectsMapAndFlag(): { + map: Uint8Array; + flag: number; + } | null; + flagsOfNode(node: HeapSnapshotNode): number; + private markDetachedDOMTreeNodes; + private markQueriableHeapObjects; + private markPageOwnedNodes; + calculateStatistics(): void; + private calculateArraySize; + getStatistics(): Statistics; +} +declare class JSHeapSnapshotNode extends HeapSnapshotNode { + #private; + constructor(snapshot: JSHeapSnapshot, nodeIndex?: number); + canBeQueried(): boolean; + name(): string; + private consStringName; + static formatPropertyName(name: string): string; + id(): number; + isHidden(): boolean; + isArray(): boolean; + isSynthetic(): boolean; + isNative(): boolean; + isUserRoot(): boolean; + isDocumentDOMTreesRoot(): boolean; + serialize(): Node; +} +declare class JSHeapSnapshotEdge extends HeapSnapshotEdge { + constructor(snapshot: JSHeapSnapshot, edgeIndex?: number); + clone(): JSHeapSnapshotEdge; + hasStringName(): boolean; + isElement(): boolean; + isHidden(): boolean; + isWeak(): boolean; + isInternal(): boolean; + isInvisible(): boolean; + isShortcut(): boolean; + name(): string; + toString(): string; + private hasStringNameInternal; + private nameInternal; + private nameOrIndex; + rawType(): number; + nameIndex(): number; +} +declare class JSHeapSnapshotRetainerEdge extends HeapSnapshotRetainerEdge { + constructor(snapshot: JSHeapSnapshot, retainerIndex: number); + clone(): JSHeapSnapshotRetainerEdge; + isHidden(): boolean; + isInvisible(): boolean; + isShortcut(): boolean; + isWeak(): boolean; +} +interface AggregatedInfo { + count: number; + distance: number; + self: number; + maxRet: number; + name: string; + idxs: number[]; +} + +interface ParseHeapSnapshotOptions { + /** + * Whether to suppress console output. + * + * @default true + */ + silent?: boolean; +} +declare function parseHeapSnapshot(data: Readable, opts?: ParseHeapSnapshotOptions): Promise; + +export { Aggregate, AggregateForDiff, type AggregatedInfo, AllocationNodeCallers, AllocationStackFrame, ComparatorConfig, Diff, DiffForClass, Edge, HeapSnapshot, HeapSnapshotEdge, HeapSnapshotEdgeIndexProvider, HeapSnapshotEdgeIterator, HeapSnapshotEdgesProvider, HeapSnapshotFilteredIterator, type HeapSnapshotHeader, HeapSnapshotIndexRangeIterator, type HeapSnapshotItem, type HeapSnapshotItemIndexProvider, type HeapSnapshotItemIterator, HeapSnapshotItemProvider, HeapSnapshotNode, HeapSnapshotNodeIndexProvider, HeapSnapshotNodeIterator, HeapSnapshotNodesProvider, HeapSnapshotProgress, HeapSnapshotProgressEvent, HeapSnapshotRetainerEdge, HeapSnapshotRetainerEdgeIndexProvider, HeapSnapshotRetainerEdgeIterator, ItemsRange, JSHeapSnapshot, JSHeapSnapshotEdge, JSHeapSnapshotNode, JSHeapSnapshotRetainerEdge, type LiveObjects, Location, Node, NodeFilter, type ParseHeapSnapshotOptions, type Profile, Samples, SearchConfig, SecondaryInitManager, SerializedAllocationNode, StaticData, type Statistics, WorkerCommand, baseSystemDistance, baseUnreachableDistance, parseHeapSnapshot, serializeUIString }; diff --git a/internal/heapsnapshot/dist/index.js b/internal/heapsnapshot/dist/index.js new file mode 100755 index 000000000..d762afa82 --- /dev/null +++ b/internal/heapsnapshot/dist/index.js @@ -0,0 +1,129 @@ +import { H as HeapSnapshotLoader, a as HeapSnapshotProgress } from './HeapSnapshotLoader-CpV_0rIo.js'; +export { z as Aggregate, B as AggregateForDiff, A as AllocationNodeCallers, y as AllocationStackFrame, F as ComparatorConfig, D as Diff, C as DiffForClass, E as Edge, m as HeapSnapshot, b as HeapSnapshotEdge, d as HeapSnapshotEdgeIndexProvider, f as HeapSnapshotEdgeIterator, o as HeapSnapshotEdgesProvider, l as HeapSnapshotFilteredIterator, k as HeapSnapshotIndexRangeIterator, n as HeapSnapshotItemProvider, i as HeapSnapshotNode, c as HeapSnapshotNodeIndexProvider, j as HeapSnapshotNodeIterator, p as HeapSnapshotNodesProvider, u as HeapSnapshotProgressEvent, g as HeapSnapshotRetainerEdge, e as HeapSnapshotRetainerEdgeIndexProvider, h as HeapSnapshotRetainerEdgeIterator, I as ItemsRange, J as JSHeapSnapshot, r as JSHeapSnapshotEdge, q as JSHeapSnapshotNode, t as JSHeapSnapshotRetainerEdge, O as Location, N as Node, K as NodeFilter, M as Samples, L as SearchConfig, S as SecondaryInitManager, x as SerializedAllocationNode, G as StaticData, W as WorkerCommand, v as baseSystemDistance, w as baseUnreachableDistance, s as serializeUIString } from './HeapSnapshotLoader-CpV_0rIo.js'; +import path from 'node:path'; +import { fileURLToPath, URL } from 'node:url'; +import { Worker } from 'node:worker_threads'; + +var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name); +var __typeError = (msg) => { + throw TypeError(msg); +}; +var __using = (stack, value, async) => { + if (value != null) { + if (typeof value !== "object" && typeof value !== "function") __typeError("Object expected"); + var dispose, inner; + dispose = value[__knownSymbol("asyncDispose")]; + if (dispose === void 0) { + dispose = value[__knownSymbol("dispose")]; + inner = dispose; + } + if (typeof dispose !== "function") __typeError("Object not disposable"); + if (inner) dispose = function() { + try { + inner.call(this); + } catch (e) { + return Promise.reject(e); + } + }; + stack.push([async, dispose, value]); + } else { + stack.push([async]); + } + return value; +}; +var __callDispose = (stack, error, hasError) => { + var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) { + return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _; + }; + var fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e); + var next = (it) => { + while (it = stack.pop()) { + try { + var result = it[1] && it[1].call(it[2]); + if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next())); + } catch (e) { + fail(e); + } + } + if (hasError) throw error; + }; + return next(); +}; +const __dirname = fileURLToPath(new URL(".", import.meta.url)); +async function parseHeapSnapshot(data, opts = {}) { + var _stack = []; + try { + const { silent = true } = opts; + const loader = new HeapSnapshotLoader( + silent ? silentProgress : consoleProgress + ); + await new Promise((resolve, reject) => { + function consume(chunk) { + loader.write(String(chunk)); + } + data.on("data", consume); + function cleanup() { + data.off("data", consume); + data.off("error", reject); + data.off("end", resolve); + } + data.once("error", (e) => { + cleanup(); + reject(e); + }); + data.once("end", () => { + cleanup(); + resolve(); + }); + }); + loader.close(); + await loader.parsingComplete; + const secondWorker = new Worker( + path.join(__dirname, "heap_snapshot_worker-entrypoint.js") + // exists after building + ); + const _ = __using(_stack, { + async [Symbol.asyncDispose]() { + await secondWorker.terminate(); + } + }, true); + const chan = new MessageChannel(); + secondWorker.postMessage( + { + data: { + disposition: "setupForSecondaryInit", + objectId: 0 + }, + ports: [chan.port2] + }, + [chan.port2] + ); + return await loader.buildSnapshot(chan.port1); + } catch (_2) { + var _error = _2, _hasError = true; + } finally { + var _promise = __callDispose(_stack, _error, _hasError); + _promise && await _promise; + } +} +const consoleProgress = new class ConsoleProgress extends HeapSnapshotProgress { + reportProblem(error) { + console.error(error); + } + updateProgress(title, value, total) { + console.log(title, value, total); + } + updateStatus(status) { + console.log(status); + } +}(); +const silentProgress = new class SilentProgress extends HeapSnapshotProgress { + reportProblem() { + } + updateProgress() { + } + updateStatus() { + } +}(); + +export { HeapSnapshotProgress, parseHeapSnapshot }; diff --git a/internal/heapsnapshot/package.json b/internal/heapsnapshot/package.json new file mode 100644 index 000000000..6f79d0f19 --- /dev/null +++ b/internal/heapsnapshot/package.json @@ -0,0 +1,28 @@ +{ + "name": "@internal/heapsnapshot", + "type": "module", + "private": true, + "engines": { + "node": ">=18.0.0" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./heap_snapshot_worker-entrypoint": "./dist/heap_snapshot_worker-entrypoint.js", + "./package.json": "./package.json" + }, + "scripts": { + "build": "pkgroll --clean-dist", + "check:types": "tsc --noEmit", + "start": "yarn build --watch" + }, + "devDependencies": { + "@tsconfig/node18": "^18.2.4", + "@tsconfig/recommended": "^1.0.8", + "@types/node": "^22.15.30", + "pkgroll": "2.14.5", + "typescript": "5.8.3" + } +} diff --git a/internal/heapsnapshot/src/AllocationProfile.ts b/internal/heapsnapshot/src/AllocationProfile.ts new file mode 100644 index 000000000..97987d5f5 --- /dev/null +++ b/internal/heapsnapshot/src/AllocationProfile.ts @@ -0,0 +1,431 @@ +/* + * Copyright (C) 2013 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import type { LiveObjects, Profile } from './HeapSnapshot.js'; +import * as HeapSnapshotModel from './HeapSnapshotModel.js'; + +export class AllocationProfile { + readonly #strings: string[]; + #nextNodeId: number; + #functionInfos: FunctionAllocationInfo[]; + #idToNode: { [x: number]: BottomUpAllocationNode | null }; + readonly #idToTopDownNode: { [x: number]: TopDownAllocationNode }; + #collapsedTopNodeIdToFunctionInfo: { [x: number]: FunctionAllocationInfo }; + #traceTops: HeapSnapshotModel.SerializedAllocationNode[] | null; + + constructor(profile: Profile, liveObjectStats: LiveObjects) { + this.#strings = profile.strings; + + this.#nextNodeId = 1; + this.#functionInfos = []; + + this.#idToNode = {}; + + this.#idToTopDownNode = {}; + + this.#collapsedTopNodeIdToFunctionInfo = {}; + + this.#traceTops = null; + + this.#buildFunctionAllocationInfos(profile); + this.#buildAllocationTree(profile, liveObjectStats); + } + + #buildFunctionAllocationInfos(profile: Profile): void { + const strings = this.#strings; + + const functionInfoFields = profile.snapshot.meta.trace_function_info_fields; + const functionNameOffset = functionInfoFields.indexOf('name'); + const scriptNameOffset = functionInfoFields.indexOf('script_name'); + const scriptIdOffset = functionInfoFields.indexOf('script_id'); + const lineOffset = functionInfoFields.indexOf('line'); + const columnOffset = functionInfoFields.indexOf('column'); + const functionInfoFieldCount = functionInfoFields.length; + + const rawInfos = profile.trace_function_infos; + const infoLength = rawInfos.length; + const functionInfos = (this.#functionInfos = new Array( + infoLength / functionInfoFieldCount, + )); + let index = 0; + for (let i = 0; i < infoLength; i += functionInfoFieldCount) { + functionInfos[index++] = new FunctionAllocationInfo( + strings[rawInfos[i + functionNameOffset]], + strings[rawInfos[i + scriptNameOffset]], + rawInfos[i + scriptIdOffset], + rawInfos[i + lineOffset], + rawInfos[i + columnOffset], + ); + } + } + + #buildAllocationTree( + profile: Profile, + liveObjectStats: LiveObjects, + ): TopDownAllocationNode { + const traceTreeRaw = profile.trace_tree; + const functionInfos = this.#functionInfos; + const idToTopDownNode = this.#idToTopDownNode; + + const traceNodeFields = profile.snapshot.meta.trace_node_fields; + const nodeIdOffset = traceNodeFields.indexOf('id'); + const functionInfoIndexOffset = traceNodeFields.indexOf( + 'function_info_index', + ); + const allocationCountOffset = traceNodeFields.indexOf('count'); + const allocationSizeOffset = traceNodeFields.indexOf('size'); + const childrenOffset = traceNodeFields.indexOf('children'); + const nodeFieldCount = traceNodeFields.length; + + function traverseNode( + // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + rawNodeArray: any, + nodeOffset: any, + parent: TopDownAllocationNode | null, + ): TopDownAllocationNode { + const functionInfo = + functionInfos[rawNodeArray[nodeOffset + functionInfoIndexOffset]]; + const id = rawNodeArray[nodeOffset + nodeIdOffset]; + const stats = liveObjectStats[id]; + const liveCount = stats ? stats.count : 0; + const liveSize = stats ? stats.size : 0; + const result = new TopDownAllocationNode( + id, + functionInfo, + rawNodeArray[nodeOffset + allocationCountOffset], + rawNodeArray[nodeOffset + allocationSizeOffset], + liveCount, + liveSize, + parent, + ); + idToTopDownNode[id] = result; + functionInfo.addTraceTopNode(result); + + const rawChildren = rawNodeArray[nodeOffset + childrenOffset]; + for (let i = 0; i < rawChildren.length; i += nodeFieldCount) { + result.children.push(traverseNode(rawChildren, i, result)); + } + + return result; + } + + return traverseNode(traceTreeRaw, 0, null); + } + + serializeTraceTops(): HeapSnapshotModel.SerializedAllocationNode[] { + if (this.#traceTops) { + return this.#traceTops; + } + + const result: HeapSnapshotModel.SerializedAllocationNode[] = + (this.#traceTops = []); + const functionInfos = this.#functionInfos; + for (let i = 0; i < functionInfos.length; i++) { + const info = functionInfos[i]; + if (info.totalCount === 0) { + continue; + } + const nodeId = this.#nextNodeId++; + const isRoot = i === 0; + result.push( + this.#serializeNode( + nodeId, + info, + info.totalCount, + info.totalSize, + info.totalLiveCount, + info.totalLiveSize, + !isRoot, + ), + ); + this.#collapsedTopNodeIdToFunctionInfo[nodeId] = info; + } + result.sort(function (a, b) { + return b.size - a.size; + }); + return result; + } + + serializeCallers(nodeId: number): HeapSnapshotModel.AllocationNodeCallers { + let node = this.#ensureBottomUpNode(nodeId); + const nodesWithSingleCaller = []; + while (node.callers().length === 1) { + node = node.callers()[0]; + nodesWithSingleCaller.push(this.#serializeCaller(node)); + } + + const branchingCallers = []; + const callers = node.callers(); + for (let i = 0; i < callers.length; i++) { + branchingCallers.push(this.#serializeCaller(callers[i])); + } + + return new HeapSnapshotModel.AllocationNodeCallers( + nodesWithSingleCaller, + branchingCallers, + ); + } + + serializeAllocationStack( + traceNodeId: number, + ): HeapSnapshotModel.AllocationStackFrame[] { + let node: (TopDownAllocationNode | null) | TopDownAllocationNode = + this.#idToTopDownNode[traceNodeId]; + const result = []; + while (node) { + const functionInfo = node.functionInfo; + result.push( + new HeapSnapshotModel.AllocationStackFrame( + functionInfo.functionName, + functionInfo.scriptName, + functionInfo.scriptId, + functionInfo.line, + functionInfo.column, + ), + ); + node = node.parent; + } + return result; + } + + traceIds(allocationNodeId: number): number[] { + return this.#ensureBottomUpNode(allocationNodeId).traceTopIds; + } + + #ensureBottomUpNode(nodeId: number): BottomUpAllocationNode { + let node = this.#idToNode[nodeId]; + if (!node) { + const functionInfo = this.#collapsedTopNodeIdToFunctionInfo[nodeId]; + node = functionInfo.bottomUpRoot(); + delete this.#collapsedTopNodeIdToFunctionInfo[nodeId]; + this.#idToNode[nodeId] = node; + } + return node as BottomUpAllocationNode; + } + + #serializeCaller( + node: BottomUpAllocationNode, + ): HeapSnapshotModel.SerializedAllocationNode { + const callerId = this.#nextNodeId++; + this.#idToNode[callerId] = node; + return this.#serializeNode( + callerId, + node.functionInfo, + node.allocationCount, + node.allocationSize, + node.liveCount, + node.liveSize, + node.hasCallers(), + ); + } + + #serializeNode( + nodeId: number, + functionInfo: FunctionAllocationInfo, + count: number, + size: number, + liveCount: number, + liveSize: number, + hasChildren: boolean, + ): HeapSnapshotModel.SerializedAllocationNode { + return new HeapSnapshotModel.SerializedAllocationNode( + nodeId, + functionInfo.functionName, + functionInfo.scriptName, + functionInfo.scriptId, + functionInfo.line, + functionInfo.column, + count, + size, + liveCount, + liveSize, + hasChildren, + ); + } +} + +export class TopDownAllocationNode { + id: number; + functionInfo: FunctionAllocationInfo; + allocationCount: number; + allocationSize: number; + liveCount: number; + liveSize: number; + parent: TopDownAllocationNode | null; + children: TopDownAllocationNode[]; + constructor( + id: number, + functionInfo: FunctionAllocationInfo, + count: number, + size: number, + liveCount: number, + liveSize: number, + parent: TopDownAllocationNode | null, + ) { + this.id = id; + this.functionInfo = functionInfo; + this.allocationCount = count; + this.allocationSize = size; + this.liveCount = liveCount; + this.liveSize = liveSize; + this.parent = parent; + + this.children = []; + } +} + +export class BottomUpAllocationNode { + functionInfo: FunctionAllocationInfo; + allocationCount: number; + allocationSize: number; + liveCount: number; + liveSize: number; + traceTopIds: number[]; + readonly #callersInternal: BottomUpAllocationNode[]; + constructor(functionInfo: FunctionAllocationInfo) { + this.functionInfo = functionInfo; + this.allocationCount = 0; + this.allocationSize = 0; + this.liveCount = 0; + this.liveSize = 0; + + this.traceTopIds = []; + + this.#callersInternal = []; + } + + addCaller(traceNode: TopDownAllocationNode): BottomUpAllocationNode { + const functionInfo = traceNode.functionInfo; + let result; + for (let i = 0; i < this.#callersInternal.length; i++) { + const caller = this.#callersInternal[i]; + if (caller.functionInfo === functionInfo) { + result = caller; + break; + } + } + if (!result) { + result = new BottomUpAllocationNode(functionInfo); + this.#callersInternal.push(result); + } + return result; + } + + callers(): BottomUpAllocationNode[] { + return this.#callersInternal; + } + + hasCallers(): boolean { + return this.#callersInternal.length > 0; + } +} + +export class FunctionAllocationInfo { + functionName: string; + scriptName: string; + scriptId: number; + line: number; + column: number; + totalCount: number; + totalSize: number; + totalLiveCount: number; + totalLiveSize: number; + #traceTops: TopDownAllocationNode[]; + #bottomUpTree?: BottomUpAllocationNode; + constructor( + functionName: string, + scriptName: string, + scriptId: number, + line: number, + column: number, + ) { + this.functionName = functionName; + this.scriptName = scriptName; + this.scriptId = scriptId; + this.line = line; + this.column = column; + this.totalCount = 0; + this.totalSize = 0; + this.totalLiveCount = 0; + this.totalLiveSize = 0; + + this.#traceTops = []; + } + + addTraceTopNode(node: TopDownAllocationNode): void { + if (node.allocationCount === 0) { + return; + } + this.#traceTops.push(node); + this.totalCount += node.allocationCount; + this.totalSize += node.allocationSize; + this.totalLiveCount += node.liveCount; + this.totalLiveSize += node.liveSize; + } + + bottomUpRoot(): BottomUpAllocationNode | null { + if (!this.#traceTops.length) { + return null; + } + if (!this.#bottomUpTree) { + this.#buildAllocationTraceTree(); + } + return this.#bottomUpTree as BottomUpAllocationNode; + } + + #buildAllocationTraceTree(): void { + this.#bottomUpTree = new BottomUpAllocationNode(this); + + for (let i = 0; i < this.#traceTops.length; i++) { + let node: (TopDownAllocationNode | null) | TopDownAllocationNode = + this.#traceTops[i]; + let bottomUpNode: BottomUpAllocationNode = this.#bottomUpTree; + const count = node.allocationCount; + const size = node.allocationSize; + const liveCount = node.liveCount; + const liveSize = node.liveSize; + const traceId = node.id; + while (true) { + bottomUpNode.allocationCount += count; + bottomUpNode.allocationSize += size; + bottomUpNode.liveCount += liveCount; + bottomUpNode.liveSize += liveSize; + bottomUpNode.traceTopIds.push(traceId); + node = node.parent; + if (node === null) { + break; + } + + bottomUpNode = bottomUpNode.addCaller(node); + } + } + } +} diff --git a/internal/heapsnapshot/src/HeapSnapshot.ts b/internal/heapsnapshot/src/HeapSnapshot.ts new file mode 100644 index 000000000..8cf9a8bae --- /dev/null +++ b/internal/heapsnapshot/src/HeapSnapshot.ts @@ -0,0 +1,4594 @@ +/* + * Copyright (C) 2011 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* eslint-disable rulesdir/prefer-private-class-members */ + +import { MessagePort } from 'node:worker_threads'; +import { AllocationProfile } from './AllocationProfile.js'; +import * as HeapSnapshotModel from './HeapSnapshotModel.js'; +import { HeapSnapshotWorkerDispatcher } from './HeapSnapshotWorkerDispatcher.js'; +import * as Platform from './platform/index.js'; + +export interface HeapSnapshotItem { + itemIndex(): number; + + serialize(): Object; +} + +export class HeapSnapshotEdge implements HeapSnapshotItem { + snapshot: HeapSnapshot; + protected readonly edges: Platform.TypedArrayUtilities.BigUint32Array; + edgeIndex: number; + constructor(snapshot: HeapSnapshot, edgeIndex?: number) { + this.snapshot = snapshot; + this.edges = snapshot.containmentEdges; + this.edgeIndex = edgeIndex || 0; + } + + clone(): HeapSnapshotEdge { + return new HeapSnapshotEdge(this.snapshot, this.edgeIndex); + } + + hasStringName(): boolean { + throw new Error('Not implemented'); + } + + name(): string { + throw new Error('Not implemented'); + } + + node(): HeapSnapshotNode { + return this.snapshot.createNode(this.nodeIndex()); + } + + nodeIndex(): number { + if (typeof this.snapshot.edgeToNodeOffset === 'undefined') { + throw new Error('edgeToNodeOffset is undefined'); + } + + return this.edges.getValue(this.edgeIndex + this.snapshot.edgeToNodeOffset); + } + + toString(): string { + return 'HeapSnapshotEdge: ' + this.name(); + } + + type(): string { + return this.snapshot.edgeTypes[this.rawType()]; + } + + itemIndex(): number { + return this.edgeIndex; + } + + serialize(): HeapSnapshotModel.Edge { + return new HeapSnapshotModel.Edge( + this.name(), + this.node().serialize(), + this.type(), + this.edgeIndex, + ); + } + + rawType(): number { + if (typeof this.snapshot.edgeTypeOffset === 'undefined') { + throw new Error('edgeTypeOffset is undefined'); + } + + return this.edges.getValue(this.edgeIndex + this.snapshot.edgeTypeOffset); + } + + isInternal(): boolean { + throw new Error('Not implemented'); + } + + isInvisible(): boolean { + throw new Error('Not implemented'); + } + + isWeak(): boolean { + throw new Error('Not implemented'); + } + + getValueForSorting(_fieldName: string): number { + throw new Error('Not implemented'); + } + + nameIndex(): number { + throw new Error('Not implemented'); + } +} + +export interface HeapSnapshotItemIterator { + hasNext(): boolean; + + item(): HeapSnapshotItem; + + next(): void; +} + +export interface HeapSnapshotItemIndexProvider { + itemForIndex(newIndex: number): HeapSnapshotItem; +} + +export class HeapSnapshotNodeIndexProvider + implements HeapSnapshotItemIndexProvider +{ + #node: HeapSnapshotNode; + constructor(snapshot: HeapSnapshot) { + this.#node = snapshot.createNode(); + } + + itemForIndex(index: number): HeapSnapshotNode { + this.#node.nodeIndex = index; + return this.#node; + } +} + +export class HeapSnapshotEdgeIndexProvider + implements HeapSnapshotItemIndexProvider +{ + #edge: JSHeapSnapshotEdge; + constructor(snapshot: HeapSnapshot) { + this.#edge = snapshot.createEdge(0); + } + + itemForIndex(index: number): HeapSnapshotEdge { + this.#edge.edgeIndex = index; + return this.#edge; + } +} + +export class HeapSnapshotRetainerEdgeIndexProvider + implements HeapSnapshotItemIndexProvider +{ + readonly #retainerEdge: JSHeapSnapshotRetainerEdge; + constructor(snapshot: HeapSnapshot) { + this.#retainerEdge = snapshot.createRetainingEdge(0); + } + + itemForIndex(index: number): HeapSnapshotRetainerEdge { + this.#retainerEdge.setRetainerIndex(index); + return this.#retainerEdge; + } +} + +export class HeapSnapshotEdgeIterator implements HeapSnapshotItemIterator { + readonly #sourceNode: HeapSnapshotNode; + edge: JSHeapSnapshotEdge; + constructor(node: HeapSnapshotNode) { + this.#sourceNode = node; + this.edge = node.snapshot.createEdge(node.edgeIndexesStart()); + } + + hasNext(): boolean { + return this.edge.edgeIndex < this.#sourceNode.edgeIndexesEnd(); + } + + item(): HeapSnapshotEdge { + return this.edge; + } + + next(): void { + if (typeof this.edge.snapshot.edgeFieldsCount === 'undefined') { + throw new Error('edgeFieldsCount is undefined'); + } + this.edge.edgeIndex += this.edge.snapshot.edgeFieldsCount; + } +} + +export class HeapSnapshotRetainerEdge implements HeapSnapshotItem { + protected snapshot: HeapSnapshot; + #retainerIndexInternal!: number; + #globalEdgeIndex!: number; + #retainingNodeIndex?: number; + #edgeInstance?: JSHeapSnapshotEdge | null; + #nodeInstance?: HeapSnapshotNode | null; + constructor(snapshot: HeapSnapshot, retainerIndex: number) { + this.snapshot = snapshot; + this.setRetainerIndex(retainerIndex); + } + + clone(): HeapSnapshotRetainerEdge { + return new HeapSnapshotRetainerEdge(this.snapshot, this.retainerIndex()); + } + + hasStringName(): boolean { + return this.edge().hasStringName(); + } + + name(): string { + return this.edge().name(); + } + + nameIndex(): number { + return this.edge().nameIndex(); + } + + node(): HeapSnapshotNode { + return this.nodeInternal(); + } + + nodeIndex(): number { + if (typeof this.#retainingNodeIndex === 'undefined') { + throw new Error('retainingNodeIndex is undefined'); + } + + return this.#retainingNodeIndex; + } + + retainerIndex(): number { + return this.#retainerIndexInternal; + } + + setRetainerIndex(retainerIndex: number): void { + if (retainerIndex === this.#retainerIndexInternal) { + return; + } + + if (!this.snapshot.retainingEdges || !this.snapshot.retainingNodes) { + throw new Error( + 'Snapshot does not contain retaining edges or retaining nodes', + ); + } + + this.#retainerIndexInternal = retainerIndex; + this.#globalEdgeIndex = this.snapshot.retainingEdges[retainerIndex]; + this.#retainingNodeIndex = this.snapshot.retainingNodes[retainerIndex]; + this.#edgeInstance = null; + this.#nodeInstance = null; + } + + set edgeIndex(edgeIndex: number) { + this.setRetainerIndex(edgeIndex); + } + + private nodeInternal(): HeapSnapshotNode { + if (!this.#nodeInstance) { + this.#nodeInstance = this.snapshot.createNode(this.#retainingNodeIndex); + } + return this.#nodeInstance; + } + + protected edge(): JSHeapSnapshotEdge { + if (!this.#edgeInstance) { + this.#edgeInstance = this.snapshot.createEdge(this.#globalEdgeIndex); + } + return this.#edgeInstance; + } + + toString(): string { + return this.edge().toString(); + } + + itemIndex(): number { + return this.#retainerIndexInternal; + } + + serialize(): HeapSnapshotModel.Edge { + const node = this.node(); + const serializedNode = node.serialize(); + serializedNode.distance = this.#distance(); + serializedNode.ignored = this.snapshot.isNodeIgnoredInRetainersView( + node.nodeIndex, + ); + + return new HeapSnapshotModel.Edge( + this.name(), + serializedNode, + this.type(), + this.#globalEdgeIndex, + ); + } + + type(): string { + return this.edge().type(); + } + + isInternal(): boolean { + return this.edge().isInternal(); + } + + getValueForSorting(fieldName: string): number { + if (fieldName === '!edgeDistance') { + return this.#distance(); + } + throw new Error('Invalid field name'); + } + + #distance(): number { + if (this.snapshot.isEdgeIgnoredInRetainersView(this.#globalEdgeIndex)) { + return HeapSnapshotModel.baseUnreachableDistance; + } + return this.node().distanceForRetainersView(); + } +} + +export class HeapSnapshotRetainerEdgeIterator + implements HeapSnapshotItemIterator +{ + readonly #retainersEnd: number; + retainer: JSHeapSnapshotRetainerEdge; + constructor(retainedNode: HeapSnapshotNode) { + const snapshot = retainedNode.snapshot; + const retainedNodeOrdinal = retainedNode.ordinal(); + if (!snapshot.firstRetainerIndex) { + throw new Error('Snapshot does not contain firstRetainerIndex'); + } + const retainerIndex = snapshot.firstRetainerIndex[retainedNodeOrdinal]; + this.#retainersEnd = snapshot.firstRetainerIndex[retainedNodeOrdinal + 1]; + this.retainer = snapshot.createRetainingEdge(retainerIndex); + } + + hasNext(): boolean { + return this.retainer.retainerIndex() < this.#retainersEnd; + } + + item(): HeapSnapshotRetainerEdge { + return this.retainer; + } + + next(): void { + this.retainer.setRetainerIndex(this.retainer.retainerIndex() + 1); + } +} + +export class HeapSnapshotNode implements HeapSnapshotItem { + snapshot: HeapSnapshot; + nodeIndex: number; + constructor(snapshot: HeapSnapshot, nodeIndex?: number) { + this.snapshot = snapshot; + this.nodeIndex = nodeIndex || 0; + } + + distance(): number { + return this.snapshot.nodeDistances[ + this.nodeIndex / this.snapshot.nodeFieldCount + ]; + } + + distanceForRetainersView(): number { + return this.snapshot.getDistanceForRetainersView(this.nodeIndex); + } + + className(): string { + return this.snapshot.strings[this.classIndex()]; + } + + classIndex(): number { + return this.#detachednessAndClassIndex() >>> SHIFT_FOR_CLASS_INDEX; + } + + // Returns a key which can uniquely describe both the class name for this node + // and its Location, if relevant. These keys are meant to be cheap to produce, + // so that building aggregates is fast. These keys are NOT the same as the + // keys exposed to the frontend by functions such as aggregatesWithFilter and + // aggregatesForDiff. + classKeyInternal(): string | number { + // It is common for multiple JavaScript constructors to have the same + // name, so the class key includes the location if available for nodes of + // type 'object'. + // + // JavaScript Functions (node type 'closure') also have locations, but it + // would not be helpful to split them into categories by location because + // many of those categories would have only one instance. + if (this.rawType() !== this.snapshot.nodeObjectType) { + return this.classIndex(); + } + const location = this.snapshot.getLocation(this.nodeIndex); + return location + ? `${location.scriptId},${location.lineNumber},${location.columnNumber},${this.className()}` + : this.classIndex(); + } + + setClassIndex(index: number): void { + let value = this.#detachednessAndClassIndex(); + value &= BITMASK_FOR_DOM_LINK_STATE; // Clear previous class index. + value |= index << SHIFT_FOR_CLASS_INDEX; // Set new class index. + this.#setDetachednessAndClassIndex(value); + if (this.classIndex() !== index) { + throw new Error('String index overflow'); + } + } + + dominatorIndex(): number { + const nodeFieldCount = this.snapshot.nodeFieldCount; + return ( + this.snapshot.dominatorsTree[ + this.nodeIndex / this.snapshot.nodeFieldCount + ] * nodeFieldCount + ); + } + + edges(): HeapSnapshotEdgeIterator { + return new HeapSnapshotEdgeIterator(this); + } + + edgesCount(): number { + return ( + (this.edgeIndexesEnd() - this.edgeIndexesStart()) / + this.snapshot.edgeFieldsCount + ); + } + + id(): number { + throw new Error('Not implemented'); + } + + rawName(): string { + return this.snapshot.strings[this.rawNameIndex()]; + } + + isRoot(): boolean { + return this.nodeIndex === this.snapshot.rootNodeIndex; + } + + isUserRoot(): boolean { + throw new Error('Not implemented'); + } + + isHidden(): boolean { + throw new Error('Not implemented'); + } + + isArray(): boolean { + throw new Error('Not implemented'); + } + + isSynthetic(): boolean { + throw new Error('Not implemented'); + } + + isDocumentDOMTreesRoot(): boolean { + throw new Error('Not implemented'); + } + + name(): string { + return this.rawName(); + } + + retainedSize(): number { + return this.snapshot.retainedSizes[this.ordinal()]; + } + + retainers(): HeapSnapshotRetainerEdgeIterator { + return new HeapSnapshotRetainerEdgeIterator(this); + } + + retainersCount(): number { + const snapshot = this.snapshot; + const ordinal = this.ordinal(); + return ( + snapshot.firstRetainerIndex[ordinal + 1] - + snapshot.firstRetainerIndex[ordinal] + ); + } + + selfSize(): number { + const snapshot = this.snapshot; + return snapshot.nodes.getValue( + this.nodeIndex + snapshot.nodeSelfSizeOffset, + ); + } + + type(): string { + return this.snapshot.nodeTypes[this.rawType()]; + } + + traceNodeId(): number { + const snapshot = this.snapshot; + return snapshot.nodes.getValue( + this.nodeIndex + snapshot.nodeTraceNodeIdOffset, + ); + } + + itemIndex(): number { + return this.nodeIndex; + } + + serialize(): HeapSnapshotModel.Node { + return new HeapSnapshotModel.Node( + this.id(), + this.name(), + this.distance(), + this.nodeIndex, + this.retainedSize(), + this.selfSize(), + this.type(), + ); + } + + rawNameIndex(): number { + const snapshot = this.snapshot; + return snapshot.nodes.getValue(this.nodeIndex + snapshot.nodeNameOffset); + } + + edgeIndexesStart(): number { + return this.snapshot.firstEdgeIndexes[this.ordinal()]; + } + + edgeIndexesEnd(): number { + return this.snapshot.firstEdgeIndexes[this.ordinal() + 1]; + } + + ordinal(): number { + return this.nodeIndex / this.snapshot.nodeFieldCount; + } + + nextNodeIndex(): number { + return this.nodeIndex + this.snapshot.nodeFieldCount; + } + + rawType(): number { + const snapshot = this.snapshot; + return snapshot.nodes.getValue(this.nodeIndex + snapshot.nodeTypeOffset); + } + + isFlatConsString(): boolean { + if (this.rawType() !== this.snapshot.nodeConsStringType) { + return false; + } + for (let iter = this.edges(); iter.hasNext(); iter.next()) { + const edge = iter.edge; + if (!edge.isInternal()) { + continue; + } + const edgeName = edge.name(); + if ( + (edgeName === 'first' || edgeName === 'second') && + edge.node().name() === '' + ) { + return true; + } + } + return false; + } + + #detachednessAndClassIndex(): number { + const { snapshot, nodeIndex } = this; + const nodeDetachednessAndClassIndexOffset = + snapshot.nodeDetachednessAndClassIndexOffset; + return nodeDetachednessAndClassIndexOffset !== -1 + ? snapshot.nodes.getValue(nodeIndex + nodeDetachednessAndClassIndexOffset) + : (snapshot.detachednessAndClassIndexArray as Uint32Array)[ + nodeIndex / snapshot.nodeFieldCount + ]; + } + + #setDetachednessAndClassIndex(value: number): void { + const { snapshot, nodeIndex } = this; + const nodeDetachednessAndClassIndexOffset = + snapshot.nodeDetachednessAndClassIndexOffset; + if (nodeDetachednessAndClassIndexOffset !== -1) { + snapshot.nodes.setValue( + nodeIndex + nodeDetachednessAndClassIndexOffset, + value, + ); + } else { + (snapshot.detachednessAndClassIndexArray as Uint32Array)[ + nodeIndex / snapshot.nodeFieldCount + ] = value; + } + } + + detachedness(): DOMLinkState { + return this.#detachednessAndClassIndex() & BITMASK_FOR_DOM_LINK_STATE; + } + + setDetachedness(detachedness: DOMLinkState): void { + let value = this.#detachednessAndClassIndex(); + value &= ~BITMASK_FOR_DOM_LINK_STATE; // Clear the old bits. + value |= detachedness; // Set the new bits. + this.#setDetachednessAndClassIndex(value); + } +} + +export class HeapSnapshotNodeIterator implements HeapSnapshotItemIterator { + node: HeapSnapshotNode; + readonly #nodesLength: number; + constructor(node: HeapSnapshotNode) { + this.node = node; + this.#nodesLength = node.snapshot.nodes.length; + } + + hasNext(): boolean { + return this.node.nodeIndex < this.#nodesLength; + } + + item(): HeapSnapshotNode { + return this.node; + } + + next(): void { + this.node.nodeIndex = this.node.nextNodeIndex(); + } +} + +export class HeapSnapshotIndexRangeIterator + implements HeapSnapshotItemIterator +{ + readonly #itemProvider: HeapSnapshotItemIndexProvider; + readonly #indexes: number[] | Uint32Array; + #position: number; + constructor( + itemProvider: HeapSnapshotItemIndexProvider, + indexes: number[] | Uint32Array, + ) { + this.#itemProvider = itemProvider; + this.#indexes = indexes; + this.#position = 0; + } + + hasNext(): boolean { + return this.#position < this.#indexes.length; + } + + item(): HeapSnapshotItem { + const index = this.#indexes[this.#position]; + return this.#itemProvider.itemForIndex(index); + } + + next(): void { + ++this.#position; + } +} + +export class HeapSnapshotFilteredIterator implements HeapSnapshotItemIterator { + #iterator: HeapSnapshotItemIterator; + #filter: ((arg0: HeapSnapshotItem) => boolean) | undefined; + constructor( + iterator: HeapSnapshotItemIterator, + filter?: (arg0: HeapSnapshotItem) => boolean, + ) { + this.#iterator = iterator; + this.#filter = filter; + this.skipFilteredItems(); + } + + hasNext(): boolean { + return this.#iterator.hasNext(); + } + + item(): HeapSnapshotItem { + return this.#iterator.item(); + } + + next(): void { + this.#iterator.next(); + this.skipFilteredItems(); + } + + private skipFilteredItems(): void { + while ( + this.#iterator.hasNext() && + this.#filter && + !this.#filter(this.#iterator.item()) + ) { + this.#iterator.next(); + } + } +} + +export function serializeUIString( + string: string, + values: Record = {}, +): string { + const serializedMessage = { string, values }; + return JSON.stringify(serializedMessage); +} + +export class HeapSnapshotProgress { + readonly #dispatcher: HeapSnapshotWorkerDispatcher | undefined; + constructor(dispatcher?: HeapSnapshotWorkerDispatcher) { + this.#dispatcher = dispatcher; + } + + updateStatus(status: string): void { + this.sendUpdateEvent(serializeUIString(status)); + } + + updateProgress(title: string, value: number, total: number): void { + const percentValue = ((total ? value / total : 0) * 100).toFixed(0); + this.sendUpdateEvent(serializeUIString(title, { PH1: percentValue })); + } + + reportProblem(error: string): void { + // May be undefined in tests. + if (this.#dispatcher) { + this.#dispatcher.sendEvent( + HeapSnapshotModel.HeapSnapshotProgressEvent.BrokenSnapshot, + error, + ); + } + } + + private sendUpdateEvent(serializedText: string): void { + // May be undefined in tests. + if (this.#dispatcher) { + this.#dispatcher.sendEvent( + HeapSnapshotModel.HeapSnapshotProgressEvent.Update, + serializedText, + ); + } + } +} + +// An "interface" to be used when classifying plain JS objects in the snapshot. +// An object matches the interface if it contains every listed property (even +// if it also contains extra properties). +interface InterfaceDefinition { + name: string; + properties: string[]; +} + +type HeapSnapshotProblemReport = Array; +function appendToProblemReport( + report: HeapSnapshotProblemReport, + messageOrNodeIndex: string | number, +): void { + if (report.length > 100) { + return; + } + report.push(messageOrNodeIndex); +} +function formatProblemReport( + snapshot: HeapSnapshot, + report: HeapSnapshotProblemReport, +): string { + const node = snapshot.rootNode(); + return report + .map((messageOrNodeIndex) => { + if (typeof messageOrNodeIndex === 'string') { + return messageOrNodeIndex; + } + node.nodeIndex = messageOrNodeIndex; + return `${node.name()} @${node.id()}`; + }) + .join('\n '); +} +function reportProblemToPrimaryWorker( + problemReport: HeapSnapshotProblemReport, + port: MessagePort, +): void { + port.postMessage({ problemReport }); +} + +export interface Profile { + /* eslint-disable @typescript-eslint/naming-convention */ + root_index: number; + nodes: Platform.TypedArrayUtilities.BigUint32Array; + edges: Platform.TypedArrayUtilities.BigUint32Array; + snapshot: HeapSnapshotHeader; + samples: number[]; + strings: string[]; + locations: number[]; + trace_function_infos: Uint32Array; + trace_tree: Object; + /* eslint-enable @typescript-eslint/naming-convention */ +} + +export interface LiveObjects { + [x: number]: { count: number; size: number; ids: number[] }; +} + +// The first batch of data sent from the primary worker to the secondary. +interface SecondaryInitArgumentsStep1 { + // For each edge ordinal, this array contains the ordinal of the pointed-to node. + edgeToNodeOrdinals: Uint32Array; + // A copy of HeapSnapshot.firstEdgeIndexes. For each node ordinal, this array + // contains the edge index of the first outgoing edge. + firstEdgeIndexes: Uint32Array; + nodeCount: number; + edgeFieldsCount: number; + nodeFieldCount: number; +} + +// The second batch of data sent from the primary worker to the secondary. +interface SecondaryInitArgumentsStep2 { + rootNodeOrdinal: number; + // An array with one bit per edge, where each bit indicates whether the edge + // should be used when computing dominators. + essentialEdgesBuffer: ArrayBuffer; +} + +// The third batch of data sent from the primary worker to the secondary. +interface SecondaryInitArgumentsStep3 { + // For each node ordinal, this array contains the node's shallow size. + nodeSelfSizes: Uint32Array; +} + +type ArgumentsToBuildRetainers = SecondaryInitArgumentsStep1; + +interface Retainers { + // For each node ordinal, this array contains the index of the first retaining edge + // in the retainingEdges and retainingNodes arrays. + firstRetainerIndex: Uint32Array; + // For each retaining edge, this array contains the "from" node's index. + retainingNodes: Uint32Array; + // For each retaining edge, this array contains the index in containmentEdges + // where you can find other info about the edge, such as its type and name. + retainingEdges: Uint32Array; +} + +interface ArgumentsToComputeDominatorsAndRetainedSizes + extends SecondaryInitArgumentsStep1, + Retainers, + SecondaryInitArgumentsStep2 { + // For each edge ordinal, this bit vector contains whether the edge + // should be used when computing dominators. + essentialEdges: Platform.TypedArrayUtilities.BitVector; + // A message port for reporting problems to the primary worker. + port: MessagePort; + // For each node ordinal, this array will contain the node's shallow size. + nodeSelfSizesPromise: Promise; +} + +interface DominatorsAndRetainedSizes { + // For each node ordinal, this array contains the ordinal of its immediate dominating node. + dominatorsTree: Uint32Array; + // For each node ordinal, this array contains the size of the subgraph it dominates, including its own size. + retainedSizes: Float64Array; +} + +interface ArgumentsToBuildDominatedNodes + extends ArgumentsToComputeDominatorsAndRetainedSizes, + DominatorsAndRetainedSizes {} + +interface DominatedNodes { + // For each node ordinal, the index of its first child node in dominatedNodes. + // Together with dominatedNodes, this allows traversing down the dominators tree, + // whereas dominatorsTree allows upward traversal. + firstDominatedNodeIndex: Uint32Array; + // Node indexes of child nodes in the dominator tree. + dominatedNodes: Uint32Array; +} + +// The data transferred from the secondary worker to the primary. +interface ResultsFromSecondWorker + extends Retainers, + DominatorsAndRetainedSizes, + DominatedNodes {} + +// Initialization work is split into two threads. This class is the entry point +// for work done by the second thread. +export class SecondaryInitManager { + argsStep1: Promise; + argsStep2: Promise; + argsStep3: Promise; + constructor(port: MessagePort) { + const { promise: argsStep1, resolve: resolveArgsStep1 } = + Platform.PromiseUtilities.withResolvers(); + this.argsStep1 = argsStep1; + const { promise: argsStep2, resolve: resolveArgsStep2 } = + Platform.PromiseUtilities.withResolvers(); + this.argsStep2 = argsStep2; + const { promise: argsStep3, resolve: resolveArgsStep3 } = + Platform.PromiseUtilities.withResolvers(); + this.argsStep3 = argsStep3; + port.on('message', (data) => { + switch (data.step) { + case 1: + resolveArgsStep1(data.args); + break; + case 2: + resolveArgsStep2(data.args); + break; + case 3: + resolveArgsStep3(data.args); + break; + } + }); + void this.initialize(port); + } + + private async getNodeSelfSizes(): Promise { + return (await this.argsStep3).nodeSelfSizes; + } + + private async initialize(port: MessagePort): Promise { + try { + const argsStep1 = await this.argsStep1; + const retainers = HeapSnapshot.buildRetainers(argsStep1); + const argsStep2 = await this.argsStep2; + const args = { + ...argsStep2, + ...argsStep1, + ...retainers, + essentialEdges: Platform.TypedArrayUtilities.createBitVector( + argsStep2.essentialEdgesBuffer, + ), + port, + nodeSelfSizesPromise: this.getNodeSelfSizes(), + }; + const dominatorsAndRetainedSizes = + await HeapSnapshot.calculateDominatorsAndRetainedSizes(args); + const dominatedNodesOutputs = HeapSnapshot.buildDominatedNodes({ + ...args, + ...dominatorsAndRetainedSizes, + }); + const results: ResultsFromSecondWorker = { + ...retainers, + ...dominatorsAndRetainedSizes, + ...dominatedNodesOutputs, + }; + port.postMessage({ resultsFromSecondWorker: results }, [ + // TODO: node should be ok with this + results.dominatorsTree.buffer, + results.firstRetainerIndex.buffer, + results.retainedSizes.buffer, + results.retainingEdges.buffer, + results.retainingNodes.buffer, + results.dominatedNodes.buffer, + results.firstDominatedNodeIndex.buffer, + ]); + } catch (e: any) { + port.postMessage({ error: e + '\n' + e?.stack }); + } + } +} + +/** + * DOM node link state. + */ +const enum DOMLinkState { + UNKNOWN = 0, + ATTACHED = 1, + DETACHED = 2, +} +const BITMASK_FOR_DOM_LINK_STATE = 3; + +// The class index is stored in the upper 30 bits of the detachedness field. +const SHIFT_FOR_CLASS_INDEX = 2; + +// After this many properties, inferInterfaceDefinitions can stop adding more +// properties to an interface definition if the name is getting too long. +const MIN_INTERFACE_PROPERTY_COUNT = 1; + +// The maximum length of an interface name produced by inferInterfaceDefinitions. +// This limit can be exceeded if the first MIN_INTERFACE_PROPERTY_COUNT property +// names are long. +const MAX_INTERFACE_NAME_LENGTH = 120; + +// Each interface definition produced by inferInterfaceDefinitions will match at +// least this many objects. There's no point in defining interfaces which match +// only a single object. +const MIN_OBJECT_COUNT_PER_INTERFACE = 2; + +// Each interface definition produced by inferInterfaceDefinitions should +// match at least 1 out of 1000 Objects in the heap. Otherwise, we end up with a +// long tail of unpopular interfaces that don't help analysis. +const MIN_OBJECT_PROPORTION_PER_INTERFACE = 1000; + +export abstract class HeapSnapshot { + nodes: Platform.TypedArrayUtilities.BigUint32Array; + containmentEdges: Platform.TypedArrayUtilities.BigUint32Array; + readonly #metaNode: HeapSnapshotMetaInfo; + readonly #rawSamples: number[]; + #samples: HeapSnapshotModel.Samples | null = null; + strings: string[]; + readonly #locations: number[]; + readonly #progress: HeapSnapshotProgress; + readonly #noDistance = -5; + rootNodeIndexInternal = 0; + #snapshotDiffs: { + [x: string]: { + [x: string]: HeapSnapshotModel.Diff; + }; + } = {}; + #aggregatesForDiffInternal?: { + interfaceDefinitions: string; + aggregates: { + [x: string]: HeapSnapshotModel.AggregateForDiff; + }; + }; + #aggregates: { + [x: string]: { + [x: string]: AggregatedInfo; + }; + } = {}; + #aggregatesSortedFlags: { + [x: string]: boolean; + } = {}; + profile: Profile; + nodeTypeOffset!: number; + nodeNameOffset!: number; + nodeIdOffset!: number; + nodeSelfSizeOffset!: number; + #nodeEdgeCountOffset!: number; + nodeTraceNodeIdOffset!: number; + nodeFieldCount!: number; + nodeTypes!: string[]; + nodeArrayType!: number; + nodeHiddenType!: number; + nodeObjectType!: number; + nodeNativeType!: number; + nodeStringType!: number; + nodeConsStringType!: number; + nodeSlicedStringType!: number; + nodeCodeType!: number; + nodeSyntheticType!: number; + nodeClosureType!: number; + nodeRegExpType!: number; + edgeFieldsCount!: number; + edgeTypeOffset!: number; + edgeNameOffset!: number; + edgeToNodeOffset!: number; + edgeTypes!: string[]; + edgeElementType!: number; + edgeHiddenType!: number; + edgeInternalType!: number; + edgeShortcutType!: number; + edgeWeakType!: number; + edgeInvisibleType!: number; + edgePropertyType!: number; + #locationIndexOffset!: number; + #locationScriptIdOffset!: number; + #locationLineOffset!: number; + #locationColumnOffset!: number; + #locationFieldCount!: number; + nodeCount!: number; + #edgeCount!: number; + retainedSizes!: Float64Array; + firstEdgeIndexes!: Uint32Array; + retainingNodes!: Uint32Array; + retainingEdges!: Uint32Array; + firstRetainerIndex!: Uint32Array; + nodeDistances!: Int32Array; + firstDominatedNodeIndex!: Uint32Array; + dominatedNodes!: Uint32Array; + dominatorsTree!: Uint32Array; + #allocationProfile!: AllocationProfile; + nodeDetachednessAndClassIndexOffset!: number; + #locationMap!: Map; + #ignoredNodesInRetainersView = new Set(); + #ignoredEdgesInRetainersView = new Set(); + #nodeDistancesForRetainersView: Int32Array | undefined; + #edgeNamesThatAreNotWeakMaps: Platform.TypedArrayUtilities.BitVector; + detachednessAndClassIndexArray?: Uint32Array; + #interfaceNames = new Map(); + #interfaceDefinitions?: InterfaceDefinition[]; + + constructor(profile: Profile, progress: HeapSnapshotProgress) { + this.nodes = profile.nodes; + this.containmentEdges = profile.edges; + this.#metaNode = profile.snapshot.meta; + this.#rawSamples = profile.samples; + this.strings = profile.strings; + this.#locations = profile.locations; + this.#progress = progress; + + if (profile.snapshot.root_index) { + this.rootNodeIndexInternal = profile.snapshot.root_index; + } + + this.profile = profile; + this.#edgeNamesThatAreNotWeakMaps = + Platform.TypedArrayUtilities.createBitVector(this.strings.length); + } + + async initialize(secondWorker: MessagePort): Promise { + const meta = this.#metaNode; + + this.nodeTypeOffset = meta.node_fields.indexOf('type'); + this.nodeNameOffset = meta.node_fields.indexOf('name'); + this.nodeIdOffset = meta.node_fields.indexOf('id'); + this.nodeSelfSizeOffset = meta.node_fields.indexOf('self_size'); + this.#nodeEdgeCountOffset = meta.node_fields.indexOf('edge_count'); + this.nodeTraceNodeIdOffset = meta.node_fields.indexOf('trace_node_id'); + this.nodeDetachednessAndClassIndexOffset = + meta.node_fields.indexOf('detachedness'); + this.nodeFieldCount = meta.node_fields.length; + + this.nodeTypes = meta.node_types[this.nodeTypeOffset]; + this.nodeArrayType = this.nodeTypes.indexOf('array'); + this.nodeHiddenType = this.nodeTypes.indexOf('hidden'); + this.nodeObjectType = this.nodeTypes.indexOf('object'); + this.nodeNativeType = this.nodeTypes.indexOf('native'); + this.nodeStringType = this.nodeTypes.indexOf('string'); + this.nodeConsStringType = this.nodeTypes.indexOf('concatenated string'); + this.nodeSlicedStringType = this.nodeTypes.indexOf('sliced string'); + this.nodeCodeType = this.nodeTypes.indexOf('code'); + this.nodeSyntheticType = this.nodeTypes.indexOf('synthetic'); + this.nodeClosureType = this.nodeTypes.indexOf('closure'); + this.nodeRegExpType = this.nodeTypes.indexOf('regexp'); + + this.edgeFieldsCount = meta.edge_fields.length; + this.edgeTypeOffset = meta.edge_fields.indexOf('type'); + this.edgeNameOffset = meta.edge_fields.indexOf('name_or_index'); + this.edgeToNodeOffset = meta.edge_fields.indexOf('to_node'); + + this.edgeTypes = meta.edge_types[this.edgeTypeOffset]; + this.edgeTypes.push('invisible'); + this.edgeElementType = this.edgeTypes.indexOf('element'); + this.edgeHiddenType = this.edgeTypes.indexOf('hidden'); + this.edgeInternalType = this.edgeTypes.indexOf('internal'); + this.edgeShortcutType = this.edgeTypes.indexOf('shortcut'); + this.edgeWeakType = this.edgeTypes.indexOf('weak'); + this.edgeInvisibleType = this.edgeTypes.indexOf('invisible'); + this.edgePropertyType = this.edgeTypes.indexOf('property'); + + const locationFields = meta.location_fields || []; + + this.#locationIndexOffset = locationFields.indexOf('object_index'); + this.#locationScriptIdOffset = locationFields.indexOf('script_id'); + this.#locationLineOffset = locationFields.indexOf('line'); + this.#locationColumnOffset = locationFields.indexOf('column'); + this.#locationFieldCount = locationFields.length; + + this.nodeCount = this.nodes.length / this.nodeFieldCount; + this.#edgeCount = this.containmentEdges.length / this.edgeFieldsCount; + + this.#progress.updateStatus('Building edge indexes…'); + this.firstEdgeIndexes = new Uint32Array(this.nodeCount + 1); + this.buildEdgeIndexes(); + this.#progress.updateStatus('Building retainers…'); + const resultsFromSecondWorker = + this.startInitStep1InSecondThread(secondWorker); + this.#progress.updateStatus('Propagating DOM state…'); + this.propagateDOMState(); + this.#progress.updateStatus('Calculating node flags…'); + this.calculateFlags(); + this.#progress.updateStatus('Building dominated nodes…'); + this.startInitStep2InSecondThread(secondWorker); + this.#progress.updateStatus('Calculating shallow sizes…'); + this.calculateShallowSizes(); + this.#progress.updateStatus('Calculating retained sizes…'); + this.startInitStep3InSecondThread(secondWorker); + this.#progress.updateStatus('Calculating distances…'); + this.nodeDistances = new Int32Array(this.nodeCount); + this.calculateDistances(/* isForRetainersView=*/ false); + this.#progress.updateStatus('Calculating object names…'); + this.calculateObjectNames(); + this.applyInterfaceDefinitions(this.inferInterfaceDefinitions()); + this.#progress.updateStatus('Calculating samples…'); + this.buildSamples(); + this.#progress.updateStatus('Building locations…'); + this.buildLocationMap(); + this.#progress.updateStatus('Calculating retained sizes…'); + await this.installResultsFromSecondThread(resultsFromSecondWorker); + this.#progress.updateStatus('Calculating statistics…'); + this.calculateStatistics(); + + if (this.profile.snapshot.trace_function_count) { + this.#progress.updateStatus('Building allocation statistics…'); + const nodes = this.nodes; + const nodesLength = nodes.length; + const nodeFieldCount = this.nodeFieldCount; + const node = this.rootNode(); + const liveObjects: LiveObjects = {}; + for ( + let nodeIndex = 0; + nodeIndex < nodesLength; + nodeIndex += nodeFieldCount + ) { + node.nodeIndex = nodeIndex; + const traceNodeId = node.traceNodeId(); + let stats: { + count: number; + size: number; + ids: number[]; + } = liveObjects[traceNodeId]; + if (!stats) { + liveObjects[traceNodeId] = stats = { count: 0, size: 0, ids: [] }; + } + stats.count++; + stats.size += node.selfSize(); + stats.ids.push(node.id()); + } + this.#allocationProfile = new AllocationProfile( + this.profile, + liveObjects, + ); + } + + this.#progress.updateStatus('Finished processing.'); + } + + private startInitStep1InSecondThread( + secondWorker: MessagePort, + ): Promise { + const resultsFromSecondWorker = new Promise( + (resolve, reject) => { + secondWorker.on('message', (data) => { + if (data?.problemReport) { + const problemReport: HeapSnapshotProblemReport = data.problemReport; + this.#progress.reportProblem( + formatProblemReport(this, problemReport), + ); + } else if (data?.resultsFromSecondWorker) { + const resultsFromSecondWorker: ResultsFromSecondWorker = + data.resultsFromSecondWorker; + resolve(resultsFromSecondWorker); + } else if (data?.error) { + reject(data.error); + } + }); + }, + ); + const edgeCount = this.#edgeCount; + const { + containmentEdges, + edgeToNodeOffset, + edgeFieldsCount, + nodeFieldCount, + } = this; + const edgeToNodeOrdinals = new Uint32Array(edgeCount); + for (let edgeOrdinal = 0; edgeOrdinal < edgeCount; ++edgeOrdinal) { + const toNodeIndex = containmentEdges.getValue( + edgeOrdinal * edgeFieldsCount + edgeToNodeOffset, + ); + if (toNodeIndex % nodeFieldCount) { + throw new Error('Invalid toNodeIndex ' + toNodeIndex); + } + edgeToNodeOrdinals[edgeOrdinal] = toNodeIndex / nodeFieldCount; + } + const args: SecondaryInitArgumentsStep1 = { + edgeToNodeOrdinals, + firstEdgeIndexes: this.firstEdgeIndexes, + nodeCount: this.nodeCount, + edgeFieldsCount: this.edgeFieldsCount, + nodeFieldCount: this.nodeFieldCount, + }; + // Note that firstEdgeIndexes is not transferred; each thread needs its own copy. + secondWorker.postMessage({ step: 1, args }, [edgeToNodeOrdinals.buffer]); + return resultsFromSecondWorker; + } + + private startInitStep2InSecondThread(secondWorker: MessagePort): void { + const rootNodeOrdinal = this.rootNodeIndexInternal / this.nodeFieldCount; + const essentialEdges = this.initEssentialEdges(); + const args: SecondaryInitArgumentsStep2 = { + rootNodeOrdinal, + essentialEdgesBuffer: essentialEdges.buffer, + }; + secondWorker.postMessage({ step: 2, args }, [essentialEdges.buffer]); + } + + private startInitStep3InSecondThread(secondWorker: MessagePort): void { + const { nodes, nodeFieldCount, nodeSelfSizeOffset, nodeCount } = this; + const nodeSelfSizes = new Uint32Array(nodeCount); + for (let nodeOrdinal = 0; nodeOrdinal < nodeCount; ++nodeOrdinal) { + nodeSelfSizes[nodeOrdinal] = nodes.getValue( + nodeOrdinal * nodeFieldCount + nodeSelfSizeOffset, + ); + } + const args: SecondaryInitArgumentsStep3 = { nodeSelfSizes }; + secondWorker.postMessage({ step: 3, args }, [nodeSelfSizes.buffer]); + } + + private async installResultsFromSecondThread( + resultsFromSecondWorker: Promise, + ): Promise { + const results = await resultsFromSecondWorker; + this.dominatedNodes = results.dominatedNodes; + this.dominatorsTree = results.dominatorsTree; + this.firstDominatedNodeIndex = results.firstDominatedNodeIndex; + this.firstRetainerIndex = results.firstRetainerIndex; + this.retainedSizes = results.retainedSizes; + this.retainingEdges = results.retainingEdges; + this.retainingNodes = results.retainingNodes; + } + + private buildEdgeIndexes(): void { + const nodes = this.nodes; + const nodeCount = this.nodeCount; + const firstEdgeIndexes = this.firstEdgeIndexes; + const nodeFieldCount = this.nodeFieldCount; + const edgeFieldsCount = this.edgeFieldsCount; + const nodeEdgeCountOffset = this.#nodeEdgeCountOffset; + firstEdgeIndexes[nodeCount] = this.containmentEdges.length; + for ( + let nodeOrdinal = 0, edgeIndex = 0; + nodeOrdinal < nodeCount; + ++nodeOrdinal + ) { + firstEdgeIndexes[nodeOrdinal] = edgeIndex; + edgeIndex += + nodes.getValue(nodeOrdinal * nodeFieldCount + nodeEdgeCountOffset) * + edgeFieldsCount; + } + } + + static buildRetainers(inputs: ArgumentsToBuildRetainers): Retainers { + const { + edgeToNodeOrdinals, + firstEdgeIndexes, + nodeCount, + edgeFieldsCount, + nodeFieldCount, + } = inputs; + const edgeCount = edgeToNodeOrdinals.length; + const retainingNodes = new Uint32Array(edgeCount); + const retainingEdges = new Uint32Array(edgeCount); + const firstRetainerIndex = new Uint32Array(nodeCount + 1); + + for (let edgeOrdinal = 0; edgeOrdinal < edgeCount; ++edgeOrdinal) { + const toNodeOrdinal = edgeToNodeOrdinals[edgeOrdinal]; + ++firstRetainerIndex[toNodeOrdinal]; + } + for (let i = 0, firstUnusedRetainerSlot = 0; i < nodeCount; i++) { + const retainersCount = firstRetainerIndex[i]; + firstRetainerIndex[i] = firstUnusedRetainerSlot; + retainingNodes[firstUnusedRetainerSlot] = retainersCount; + firstUnusedRetainerSlot += retainersCount; + } + firstRetainerIndex[nodeCount] = retainingNodes.length; + + let nextNodeFirstEdgeIndex: number = firstEdgeIndexes[0]; + for (let srcNodeOrdinal = 0; srcNodeOrdinal < nodeCount; ++srcNodeOrdinal) { + const firstEdgeIndex = nextNodeFirstEdgeIndex; + nextNodeFirstEdgeIndex = firstEdgeIndexes[srcNodeOrdinal + 1]; + const srcNodeIndex = srcNodeOrdinal * nodeFieldCount; + for ( + let edgeIndex = firstEdgeIndex; + edgeIndex < nextNodeFirstEdgeIndex; + edgeIndex += edgeFieldsCount + ) { + const toNodeOrdinal = edgeToNodeOrdinals[edgeIndex / edgeFieldsCount]; + const firstRetainerSlotIndex = firstRetainerIndex[toNodeOrdinal]; + const nextUnusedRetainerSlotIndex = + firstRetainerSlotIndex + --retainingNodes[firstRetainerSlotIndex]; + retainingNodes[nextUnusedRetainerSlotIndex] = srcNodeIndex; + retainingEdges[nextUnusedRetainerSlotIndex] = edgeIndex; + } + } + + return { + retainingNodes, + retainingEdges, + firstRetainerIndex, + }; + } + + abstract createNode(_nodeIndex?: number): HeapSnapshotNode; + abstract createEdge(_edgeIndex: number): JSHeapSnapshotEdge; + abstract createRetainingEdge( + _retainerIndex: number, + ): JSHeapSnapshotRetainerEdge; + + private allNodes(): HeapSnapshotNodeIterator { + return new HeapSnapshotNodeIterator(this.rootNode()); + } + + rootNode(): HeapSnapshotNode { + return this.createNode(this.rootNodeIndexInternal); + } + + get rootNodeIndex(): number { + return this.rootNodeIndexInternal; + } + + get totalSize(): number { + return ( + this.rootNode().retainedSize() + + (this.profile.snapshot.extra_native_bytes ?? 0) + ); + } + + private createFilter( + nodeFilter: HeapSnapshotModel.NodeFilter, + ): ((arg0: HeapSnapshotNode) => boolean) | undefined { + const { minNodeId, maxNodeId, allocationNodeId, filterName } = nodeFilter; + let filter; + if (typeof allocationNodeId === 'number') { + filter = this.createAllocationStackFilter(allocationNodeId); + if (!filter) { + throw new Error('Unable to create filter'); + } + // @ts-expect-error key can be added as a static property + filter.key = 'AllocationNodeId: ' + allocationNodeId; + } else if (typeof minNodeId === 'number' && typeof maxNodeId === 'number') { + filter = this.createNodeIdFilter(minNodeId, maxNodeId); + // @ts-expect-error key can be added as a static property + filter.key = 'NodeIdRange: ' + minNodeId + '..' + maxNodeId; + } else if (filterName !== undefined) { + filter = this.createNamedFilter(filterName); + // @ts-expect-error key can be added as a static property + filter.key = 'NamedFilter: ' + filterName; + } + return filter; + } + + search( + searchConfig: HeapSnapshotModel.SearchConfig, + nodeFilter: HeapSnapshotModel.NodeFilter, + ): number[] { + const query = searchConfig.query; + + function filterString( + matchedStringIndexes: Set, + string: string, + index: number, + ): Set { + if (string.indexOf(query) !== -1) { + matchedStringIndexes.add(index); + } + return matchedStringIndexes; + } + + const regexp = searchConfig.isRegex + ? new RegExp(query) + : Platform.StringUtilities.createPlainTextSearchRegex(query, 'i'); + + function filterRegexp( + matchedStringIndexes: Set, + string: string, + index: number, + ): Set { + if (regexp.test(string)) { + matchedStringIndexes.add(index); + } + return matchedStringIndexes; + } + + const useRegExp = searchConfig.isRegex || !searchConfig.caseSensitive; + const stringFilter = useRegExp ? filterRegexp : filterString; + const stringIndexes = this.strings.reduce(stringFilter, new Set()); + + const filter = this.createFilter(nodeFilter); + const nodeIds = []; + const nodesLength = this.nodes.length; + const nodes = this.nodes; + const nodeNameOffset = this.nodeNameOffset; + const nodeIdOffset = this.nodeIdOffset; + const nodeFieldCount = this.nodeFieldCount; + const node = this.rootNode(); + + for ( + let nodeIndex = 0; + nodeIndex < nodesLength; + nodeIndex += nodeFieldCount + ) { + node.nodeIndex = nodeIndex; + if (filter && !filter(node)) { + continue; + } + if (node.selfSize() === 0) { + // Nodes with size zero are omitted in the data grid, so avoid returning + // search results that can't be navigated to. + continue; + } + const name = node.name(); + if (name === node.rawName()) { + // If the string displayed to the user matches the raw name from the + // snapshot, then we can use the Set computed above. This avoids + // repeated work when multiple nodes have the same name. + if (stringIndexes.has(nodes.getValue(nodeIndex + nodeNameOffset))) { + nodeIds.push(nodes.getValue(nodeIndex + nodeIdOffset)); + } + // If the node is displaying a customized name, then we must perform the + // full string search within that name here. + } else if (useRegExp ? regexp.test(name) : name.indexOf(query) !== -1) { + nodeIds.push(nodes.getValue(nodeIndex + nodeIdOffset)); + } + } + return nodeIds; + } + + aggregatesWithFilter(nodeFilter: HeapSnapshotModel.NodeFilter): { + [x: string]: HeapSnapshotModel.Aggregate; + } { + const filter = this.createFilter(nodeFilter); + // @ts-expect-error key is added in createFilter + const key = filter ? filter.key : 'allObjects'; + return this.getAggregatesByClassKey(false, key, filter); + } + + private createNodeIdFilter( + minNodeId: number, + maxNodeId: number, + ): (arg0: HeapSnapshotNode) => boolean { + function nodeIdFilter(node: HeapSnapshotNode): boolean { + const id = node.id(); + return id > minNodeId && id <= maxNodeId; + } + return nodeIdFilter; + } + + private createAllocationStackFilter( + bottomUpAllocationNodeId: number, + ): ((arg0: HeapSnapshotNode) => boolean) | undefined { + if (!this.#allocationProfile) { + throw new Error('No Allocation Profile provided'); + } + + const traceIds = this.#allocationProfile.traceIds(bottomUpAllocationNodeId); + if (!traceIds.length) { + return undefined; + } + + const set: { [x: number]: boolean } = {}; + for (let i = 0; i < traceIds.length; i++) { + set[traceIds[i]] = true; + } + function traceIdFilter(node: HeapSnapshotNode): boolean { + return Boolean(set[node.traceNodeId()]); + } + return traceIdFilter; + } + + private createNamedFilter( + filterName: string, + ): (node: HeapSnapshotNode) => boolean { + // Allocate an array with a single bit per node, which can be used by each + // specific filter implemented below. + const bitmap = Platform.TypedArrayUtilities.createBitVector(this.nodeCount); + const getBit = (node: HeapSnapshotNode): boolean => { + const ordinal = node.nodeIndex / this.nodeFieldCount; + return bitmap.getBit(ordinal); + }; + + // Traverses the graph in breadth-first order with the given filter, and + // sets the bit in `bitmap` for every visited node. + const traverse = ( + filter: (node: HeapSnapshotNode, edge: HeapSnapshotEdge) => boolean, + ): void => { + const distances = new Int32Array(this.nodeCount); + for (let i = 0; i < this.nodeCount; ++i) { + distances[i] = this.#noDistance; + } + const nodesToVisit = new Uint32Array(this.nodeCount); + distances[this.rootNode().ordinal()] = 0; + nodesToVisit[0] = this.rootNode().nodeIndex; + const nodesToVisitLength = 1; + this.bfs(nodesToVisit, nodesToVisitLength, distances, filter); + for (let i = 0; i < this.nodeCount; ++i) { + if (distances[i] !== this.#noDistance) { + bitmap.setBit(i); + } + } + }; + + const markUnreachableNodes = (): void => { + for (let i = 0; i < this.nodeCount; ++i) { + if (this.nodeDistances[i] === this.#noDistance) { + bitmap.setBit(i); + } + } + }; + + switch (filterName) { + case 'objectsRetainedByDetachedDomNodes': + // Traverse the graph, avoiding detached nodes. + traverse((node: HeapSnapshotNode, edge: HeapSnapshotEdge) => { + return edge.node().detachedness() !== DOMLinkState.DETACHED; + }); + markUnreachableNodes(); + return (node: HeapSnapshotNode) => !getBit(node); + case 'objectsRetainedByConsole': + // Traverse the graph, avoiding edges that represent globals owned by + // the DevTools console. + traverse((node: HeapSnapshotNode, edge: HeapSnapshotEdge) => { + return !( + node.isSynthetic() && + edge.hasStringName() && + edge.name().endsWith(' / DevTools console') + ); + }); + markUnreachableNodes(); + return (node: HeapSnapshotNode) => !getBit(node); + case 'duplicatedStrings': { + const stringToNodeIndexMap = new Map(); + const node = this.createNode(0); + for (let i = 0; i < this.nodeCount; ++i) { + node.nodeIndex = i * this.nodeFieldCount; + const rawType = node.rawType(); + if ( + rawType === this.nodeStringType || + rawType === this.nodeConsStringType + ) { + // Check whether the cons string is already "flattened", meaning + // that one of its two parts is the empty string. If so, we should + // skip it. We don't help anyone by reporting a flattened cons + // string as a duplicate with its own content, since V8 controls + // that behavior internally. + if (node.isFlatConsString()) { + continue; + } + const name = node.name(); + const alreadyVisitedNodeIndex = stringToNodeIndexMap.get(name); + if (alreadyVisitedNodeIndex === undefined) { + stringToNodeIndexMap.set(name, node.nodeIndex); + } else { + bitmap.setBit(alreadyVisitedNodeIndex / this.nodeFieldCount); + bitmap.setBit(node.nodeIndex / this.nodeFieldCount); + } + } + } + return getBit; + } + } + throw new Error('Invalid filter name'); + } + + getAggregatesByClassKey( + sortedIndexes: boolean, + key?: string, + filter?: (arg0: HeapSnapshotNode) => boolean, + ): { [x: string]: HeapSnapshotModel.Aggregate } { + let aggregates: { + [x: string]: HeapSnapshotModel.Aggregate; + }; + if (key && this.#aggregates[key]) { + aggregates = this.#aggregates[key]; + } else { + const aggregatesMap = this.buildAggregates(filter); + this.calculateClassesRetainedSize(aggregatesMap, filter); + + // In the two previous steps, we used class keys that were simple and + // could be produced quickly. For many objects, this meant using the index + // of the string containing its class name. However, string indices are + // not consistent across snapshots, and this aggregate data might end up + // being used in a comparison, so here we convert to a more durable format + // for class keys. + aggregates = Object.create(null); + for (const [classKey, aggregate] of aggregatesMap.entries()) { + const newKey = this.classKeyFromClassKeyInternal(classKey); + aggregates[newKey] = aggregate; + } + if (key) { + this.#aggregates[key] = aggregates; + } + } + + if (sortedIndexes && (!key || !this.#aggregatesSortedFlags[key])) { + this.sortAggregateIndexes(aggregates); + if (key) { + this.#aggregatesSortedFlags[key] = sortedIndexes; + } + } + + return aggregates as { + [x: string]: HeapSnapshotModel.Aggregate; + }; + } + + allocationTracesTops(): HeapSnapshotModel.SerializedAllocationNode[] { + return this.#allocationProfile.serializeTraceTops(); + } + + allocationNodeCallers( + nodeId: number, + ): HeapSnapshotModel.AllocationNodeCallers { + return this.#allocationProfile.serializeCallers(nodeId); + } + + allocationStack( + nodeIndex: number, + ): HeapSnapshotModel.AllocationStackFrame[] | null { + const node = this.createNode(nodeIndex); + const allocationNodeId = node.traceNodeId(); + if (!allocationNodeId) { + return null; + } + return this.#allocationProfile.serializeAllocationStack(allocationNodeId); + } + + aggregatesForDiff(interfaceDefinitions: string): { + [x: string]: HeapSnapshotModel.AggregateForDiff; + } { + if ( + this.#aggregatesForDiffInternal?.interfaceDefinitions === + interfaceDefinitions + ) { + return this.#aggregatesForDiffInternal.aggregates; + } + + // Temporarily apply the interface definitions from the other snapshot. + const originalInterfaceDefinitions = this.#interfaceDefinitions; + this.applyInterfaceDefinitions( + JSON.parse(interfaceDefinitions) as InterfaceDefinition[], + ); + const aggregates = this.getAggregatesByClassKey(true, 'allObjects'); + this.applyInterfaceDefinitions(originalInterfaceDefinitions ?? []); + const result: { + [x: string]: HeapSnapshotModel.AggregateForDiff; + } = {}; + + const node = this.createNode(); + for (const classKey in aggregates) { + const aggregate = aggregates[classKey]; + const indexes = aggregate.idxs; + const ids = new Array(indexes.length); + const selfSizes = new Array(indexes.length); + for (let i = 0; i < indexes.length; i++) { + node.nodeIndex = indexes[i]; + ids[i] = node.id(); + selfSizes[i] = node.selfSize(); + } + + result[classKey] = { name: node.className(), indexes, ids, selfSizes }; + } + + this.#aggregatesForDiffInternal = { + interfaceDefinitions, + aggregates: result, + }; + return result; + } + + isUserRoot(_node: HeapSnapshotNode): boolean { + return true; + } + + calculateShallowSizes(): void {} + + calculateDistances( + isForRetainersView: boolean, + filter?: (arg0: HeapSnapshotNode, arg1: HeapSnapshotEdge) => boolean, + ): void { + const nodeCount = this.nodeCount; + + if (isForRetainersView) { + const originalFilter = filter; + filter = (node: HeapSnapshotNode, edge: HeapSnapshotEdge) => { + return ( + !this.#ignoredNodesInRetainersView.has(edge.nodeIndex()) && + (!originalFilter || originalFilter(node, edge)) + ); + }; + if (this.#nodeDistancesForRetainersView === undefined) { + this.#nodeDistancesForRetainersView = new Int32Array(nodeCount); + } + } + + const distances = isForRetainersView + ? (this.#nodeDistancesForRetainersView as Int32Array) + : this.nodeDistances; + const noDistance = this.#noDistance; + for (let i = 0; i < nodeCount; ++i) { + distances[i] = noDistance; + } + + const nodesToVisit = new Uint32Array(this.nodeCount); + let nodesToVisitLength = 0; + + // BFS for user root objects. + for (let iter = this.rootNode().edges(); iter.hasNext(); iter.next()) { + const node = iter.edge.node(); + if (this.isUserRoot(node)) { + distances[node.ordinal()] = 1; + nodesToVisit[nodesToVisitLength++] = node.nodeIndex; + } + } + this.bfs(nodesToVisit, nodesToVisitLength, distances, filter); + + // BFS for objects not reached from user roots. + distances[this.rootNode().ordinal()] = + nodesToVisitLength > 0 ? HeapSnapshotModel.baseSystemDistance : 0; + nodesToVisit[0] = this.rootNode().nodeIndex; + nodesToVisitLength = 1; + this.bfs(nodesToVisit, nodesToVisitLength, distances, filter); + } + + private bfs( + nodesToVisit: Uint32Array, + nodesToVisitLength: number, + distances: Int32Array, + filter?: (arg0: HeapSnapshotNode, arg1: HeapSnapshotEdge) => boolean, + ): void { + // Preload fields into local variables for better performance. + const edgeFieldsCount = this.edgeFieldsCount; + const nodeFieldCount = this.nodeFieldCount; + const containmentEdges = this.containmentEdges; + const firstEdgeIndexes = this.firstEdgeIndexes; + const edgeToNodeOffset = this.edgeToNodeOffset; + const edgeTypeOffset = this.edgeTypeOffset; + const nodeCount = this.nodeCount; + const edgeWeakType = this.edgeWeakType; + const noDistance = this.#noDistance; + + let index = 0; + const edge = this.createEdge(0); + const node = this.createNode(0); + while (index < nodesToVisitLength) { + const nodeIndex = nodesToVisit[index++]; // shift generates too much garbage. + const nodeOrdinal = nodeIndex / nodeFieldCount; + const distance = distances[nodeOrdinal] + 1; + const firstEdgeIndex = firstEdgeIndexes[nodeOrdinal]; + const edgesEnd = firstEdgeIndexes[nodeOrdinal + 1]; + node.nodeIndex = nodeIndex; + for ( + let edgeIndex = firstEdgeIndex; + edgeIndex < edgesEnd; + edgeIndex += edgeFieldsCount + ) { + const edgeType = containmentEdges.getValue(edgeIndex + edgeTypeOffset); + if (edgeType === edgeWeakType) { + continue; + } + const childNodeIndex = containmentEdges.getValue( + edgeIndex + edgeToNodeOffset, + ); + const childNodeOrdinal = childNodeIndex / nodeFieldCount; + if (distances[childNodeOrdinal] !== noDistance) { + continue; + } + edge.edgeIndex = edgeIndex; + if (filter && !filter(node, edge)) { + continue; + } + distances[childNodeOrdinal] = distance; + nodesToVisit[nodesToVisitLength++] = childNodeIndex; + } + } + if (nodesToVisitLength > nodeCount) { + throw new Error( + 'BFS failed. Nodes to visit (' + + nodesToVisitLength + + ') is more than nodes count (' + + nodeCount + + ')', + ); + } + } + + private buildAggregates( + filter?: (arg0: HeapSnapshotNode) => boolean, + ): Map { + const aggregates = new Map(); + + const nodes = this.nodes; + const nodesLength = nodes.length; + const nodeFieldCount = this.nodeFieldCount; + const selfSizeOffset = this.nodeSelfSizeOffset; + const node = this.rootNode(); + const nodeDistances = this.nodeDistances; + + for ( + let nodeIndex = 0; + nodeIndex < nodesLength; + nodeIndex += nodeFieldCount + ) { + node.nodeIndex = nodeIndex; + if (filter && !filter(node)) { + continue; + } + const selfSize = nodes.getValue(nodeIndex + selfSizeOffset); + if (!selfSize) { + continue; + } + const classKey = node.classKeyInternal(); + const nodeOrdinal = nodeIndex / nodeFieldCount; + const distance = nodeDistances[nodeOrdinal]; + let aggregate = aggregates.get(classKey); + if (!aggregate) { + aggregate = { + count: 1, + distance, + self: selfSize, + maxRet: 0, + name: node.className(), + idxs: [nodeIndex], + }; + aggregates.set(classKey, aggregate); + } else { + aggregate.distance = Math.min(aggregate.distance, distance); + ++aggregate.count; + aggregate.self += selfSize; + aggregate.idxs.push(nodeIndex); + } + } + + // Shave off provisionally allocated space. + for (const aggregate of aggregates.values()) { + aggregate.idxs = aggregate.idxs.slice(); + } + + return aggregates; + } + + private calculateClassesRetainedSize( + aggregates: Map, + filter?: (arg0: HeapSnapshotNode) => boolean, + ): void { + const rootNodeIndex = this.rootNodeIndexInternal; + const node = this.createNode(rootNodeIndex); + const list = [rootNodeIndex]; + const sizes = [-1]; + const classKeys: Array = []; + + const seenClassKeys = new Map(); + const nodeFieldCount = this.nodeFieldCount; + const dominatedNodes = this.dominatedNodes; + const firstDominatedNodeIndex = this.firstDominatedNodeIndex; + + while (list.length) { + const nodeIndex = list.pop() as number; + node.nodeIndex = nodeIndex; + let classKey = node.classKeyInternal(); + const seen = Boolean(seenClassKeys.get(classKey)); + const nodeOrdinal = nodeIndex / nodeFieldCount; + const dominatedIndexFrom = firstDominatedNodeIndex[nodeOrdinal]; + const dominatedIndexTo = firstDominatedNodeIndex[nodeOrdinal + 1]; + + if (!seen && (!filter || filter(node)) && node.selfSize()) { + (aggregates.get(classKey) as AggregatedInfo).maxRet += + node.retainedSize(); + if (dominatedIndexFrom !== dominatedIndexTo) { + seenClassKeys.set(classKey, true); + sizes.push(list.length); + classKeys.push(classKey); + } + } + for (let i = dominatedIndexFrom; i < dominatedIndexTo; i++) { + list.push(dominatedNodes[i]); + } + + const l = list.length; + while (sizes[sizes.length - 1] === l) { + sizes.pop(); + classKey = classKeys.pop() as string; + seenClassKeys.set(classKey, false); + } + } + } + + private sortAggregateIndexes(aggregates: { + [x: string]: AggregatedInfo; + }): void { + const nodeA = this.createNode(); + const nodeB = this.createNode(); + + for (const clss in aggregates) { + aggregates[clss].idxs.sort((idxA, idxB) => { + nodeA.nodeIndex = idxA; + nodeB.nodeIndex = idxB; + return nodeA.id() < nodeB.id() ? -1 : 1; + }); + } + } + + tryParseWeakMapEdgeName( + edgeNameIndex: number, + ): { duplicatedPart: string; tableId: string } | undefined { + const previousResult = + this.#edgeNamesThatAreNotWeakMaps.getBit(edgeNameIndex); + if (previousResult) { + return undefined; + } + const edgeName = this.strings[edgeNameIndex]; + const ephemeronNameRegex = + /^\d+(? \/ part of key \(.*? @\d+\) -> value \(.*? @\d+\) pair in WeakMap \(table @(?\d+)\))$/; + const match = edgeName.match(ephemeronNameRegex); + if (!match) { + this.#edgeNamesThatAreNotWeakMaps.setBit(edgeNameIndex); + return undefined; + } + return match.groups as { duplicatedPart: string; tableId: string }; + } + + private computeIsEssentialEdge( + nodeIndex: number, + edgeIndex: number, + userObjectsMapAndFlag: { map: Uint8Array; flag: number } | null, + ): boolean { + const edgeType = this.containmentEdges.getValue( + edgeIndex + this.edgeTypeOffset, + ); + + // Values in WeakMaps are retained by the key and table together. Removing + // either the key or the table would be sufficient to remove the edge from + // the other one, so we needn't use both of those edges when computing + // dominators. We've found that the edge from the key generally produces + // more useful results, so here we skip the edge from the table. + if (edgeType === this.edgeInternalType) { + const edgeNameIndex = this.containmentEdges.getValue( + edgeIndex + this.edgeNameOffset, + ); + const match = this.tryParseWeakMapEdgeName(edgeNameIndex); + if (match) { + const nodeId = this.nodes.getValue(nodeIndex + this.nodeIdOffset); + if (nodeId === parseInt(match.tableId, 10)) { + return false; + } + } + } + + // Weak edges never retain anything. + if (edgeType === this.edgeWeakType) { + return false; + } + + const childNodeIndex = this.containmentEdges.getValue( + edgeIndex + this.edgeToNodeOffset, + ); + // Ignore self edges. + if (nodeIndex === childNodeIndex) { + return false; + } + + if (nodeIndex !== this.rootNodeIndex) { + // Shortcuts at the root node have special meaning of marking user global objects. + if (edgeType === this.edgeShortcutType) { + return false; + } + + const flags = userObjectsMapAndFlag ? userObjectsMapAndFlag.map : null; + const userObjectFlag = userObjectsMapAndFlag + ? userObjectsMapAndFlag.flag + : 0; + const nodeOrdinal = nodeIndex / this.nodeFieldCount; + const childNodeOrdinal = childNodeIndex / this.nodeFieldCount; + const nodeFlag = !flags || flags[nodeOrdinal] & userObjectFlag; + const childNodeFlag = !flags || flags[childNodeOrdinal] & userObjectFlag; + // We are skipping the edges from non-page-owned nodes to page-owned nodes. + // Otherwise the dominators for the objects that also were retained by debugger would be affected. + if (childNodeFlag && !nodeFlag) { + return false; + } + } + + return true; + } + + // Returns a bitmap indicating whether each edge should be considered when building the dominator tree. + private initEssentialEdges(): Platform.TypedArrayUtilities.BitVector { + const essentialEdges = Platform.TypedArrayUtilities.createBitVector( + this.#edgeCount, + ); + const { nodes, nodeFieldCount, edgeFieldsCount } = this; + const userObjectsMapAndFlag = this.userObjectsMapAndFlag(); + const endNodeIndex = nodes.length; + const node = this.createNode(0); + for ( + let nodeIndex = 0; + nodeIndex < endNodeIndex; + nodeIndex += nodeFieldCount + ) { + node.nodeIndex = nodeIndex; + const edgeIndexesEnd = node.edgeIndexesEnd(); + for ( + let edgeIndex = node.edgeIndexesStart(); + edgeIndex < edgeIndexesEnd; + edgeIndex += edgeFieldsCount + ) { + if ( + this.computeIsEssentialEdge( + nodeIndex, + edgeIndex, + userObjectsMapAndFlag, + ) + ) { + essentialEdges.setBit(edgeIndex / edgeFieldsCount); + } + } + } + return essentialEdges; + } + + static hasOnlyWeakRetainers( + inputs: ArgumentsToComputeDominatorsAndRetainedSizes, + nodeOrdinal: number, + ): boolean { + const { + retainingEdges, + edgeFieldsCount, + firstRetainerIndex, + essentialEdges, + } = inputs; + const beginRetainerIndex = firstRetainerIndex[nodeOrdinal]; + const endRetainerIndex = firstRetainerIndex[nodeOrdinal + 1]; + for ( + let retainerIndex = beginRetainerIndex; + retainerIndex < endRetainerIndex; + ++retainerIndex + ) { + const retainerEdgeIndex = retainingEdges[retainerIndex]; + if (essentialEdges.getBit(retainerEdgeIndex / edgeFieldsCount)) { + return false; + } + } + return true; + } + + // The algorithm for building the dominator tree is from the paper: + // 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 + static async calculateDominatorsAndRetainedSizes( + inputs: ArgumentsToComputeDominatorsAndRetainedSizes, + ): Promise { + // Preload fields into local variables for better performance. + const { + nodeCount, + firstEdgeIndexes, + edgeFieldsCount, + nodeFieldCount, + firstRetainerIndex, + retainingEdges, + retainingNodes, + edgeToNodeOrdinals, + rootNodeOrdinal, + essentialEdges, + nodeSelfSizesPromise, + port, + } = inputs; + function isEssentialEdge(edgeIndex: number): boolean { + return essentialEdges.getBit(edgeIndex / edgeFieldsCount); + } + + // The Lengauer-Tarjan algorithm expects vectors to be numbered from 1 to n + // and uses 0 as an invalid value, so use 1-indexing for all the arrays. + // Convert between ordinals and vertex numbers by adding/subtracting 1. + 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 bucket = new Array>(arrayLength); + let n = 0; + + // Iterative DFS since the recursive version can cause stack overflows. + // Use an array to keep track of the next edge index to be examined for each node. + const nextEdgeIndex = new Uint32Array(arrayLength); + const dfs = (root: number): void => { + const rootOrdinal = root - 1; + nextEdgeIndex[rootOrdinal] = firstEdgeIndexes[rootOrdinal]; + let v = root; + while (v !== 0) { + // First process v if not done already. + if (semi[v] === 0) { + semi[v] = ++n; + vertex[n] = label[v] = v; + } + + // The next node to process is the first unprocessed successor w of v, + // or parent[v] if all of v's successors have already been processed. + let vNext = parent[v]; + const vOrdinal = v - 1; + for ( + ; + nextEdgeIndex[vOrdinal] < firstEdgeIndexes[vOrdinal + 1]; + nextEdgeIndex[vOrdinal] += edgeFieldsCount + ) { + const edgeIndex = nextEdgeIndex[vOrdinal]; + if (!isEssentialEdge(edgeIndex)) { + continue; + } + const wOrdinal = edgeToNodeOrdinals[edgeIndex / edgeFieldsCount]; + const w = wOrdinal + 1; + if (semi[w] === 0) { + parent[w] = v; + nextEdgeIndex[wOrdinal] = firstEdgeIndexes[wOrdinal]; + vNext = w; + break; + } + } + v = vNext; + } + }; + + // Iterative version since the recursive version can cause stack overflows. + // Preallocate a stack since compress() is called several times. + // The stack cannot grow larger than the number of nodes since we walk up + // the tree represented by the ancestor array. + const compressionStack = new Uint32Array(arrayLength); + const compress = (v: number): void => { + 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]]; + } + }; + + // Simple versions of eval and link from the paper. + const evaluate = (v: number): number => { + if (ancestor[v] === 0) { + return v; + } + compress(v); + return label[v]; + }; + + const link = (v: number, w: number): void => { + ancestor[w] = v; + }; + + // Algorithm begins here. The variable names are as per the paper. + const r = rootNodeOrdinal + 1; + n = 0; + const dom = new Uint32Array(arrayLength); + + // First perform DFS from the root. + dfs(r); + + // Then perform DFS from orphan nodes (ones with only weak retainers) if any. + if (n < nodeCount) { + const errors: HeapSnapshotProblemReport = [ + `Heap snapshot: ${nodeCount - n} nodes are unreachable from the root.`, + ]; + appendToProblemReport( + errors, + 'The following nodes have only weak retainers:', + ); + for (let v = 1; v <= nodeCount; v++) { + const vOrdinal = v - 1; + if ( + semi[v] === 0 && + HeapSnapshot.hasOnlyWeakRetainers(inputs, vOrdinal) + ) { + appendToProblemReport(errors, vOrdinal * nodeFieldCount); + parent[v] = r; + dfs(v); + } + } + reportProblemToPrimaryWorker(errors, port); + } + + // If there are unreachable nodes still, visit them individually from the root. + // This can happen when there is a clique of nodes retained by one another. + if (n < nodeCount) { + const errors: HeapSnapshotProblemReport = [ + `Heap snapshot: Still found ${nodeCount - n} unreachable nodes:`, + ]; + for (let v = 1; v <= nodeCount; v++) { + if (semi[v] === 0) { + const vOrdinal = v - 1; + appendToProblemReport(errors, vOrdinal * nodeFieldCount); + parent[v] = r; + semi[v] = ++n; + vertex[n] = label[v] = v; + } + } + reportProblemToPrimaryWorker(errors, port); + } + + // Main loop. Process the vertices in decreasing order by DFS number. + for (let i = n; i >= 2; --i) { + const w = vertex[i]; + // Iterate over all predecessors v of w. + const wOrdinal = w - 1; + let isOrphanNode = true; + for ( + let retainerIndex = firstRetainerIndex[wOrdinal]; + retainerIndex < firstRetainerIndex[wOrdinal + 1]; + retainerIndex++ + ) { + if (!isEssentialEdge(retainingEdges[retainerIndex])) { + continue; + } + isOrphanNode = false; + const vOrdinal = retainingNodes[retainerIndex] / nodeFieldCount; + const v = vOrdinal + 1; + const u = evaluate(v); + if (semi[u] < semi[w]) { + semi[w] = semi[u]; + } + } + if (isOrphanNode) { + // We treat orphan nodes as having a single predecessor - the root. + // semi[r] is always less than any semi[w] so set it unconditionally. + semi[w] = semi[r]; + } + + if (bucket[vertex[semi[w]]] === undefined) { + bucket[vertex[semi[w]]] = new Set(); + } + bucket[vertex[semi[w]]].add(w); + link(parent[w], w); + + // Process all vertices v in bucket(parent(w)). + if (bucket[parent[w]] !== undefined) { + for (const v of bucket[parent[w]]) { + const u = evaluate(v); + dom[v] = semi[u] < semi[v] ? u : parent[w]; + } + bucket[parent[w]].clear(); + } + } + + // Final step. Fill in the immediate dominators not explicitly computed above. + // Unlike the paper, we consider the root to be its own dominator and + // set dom[0] to r to propagate the root as the dominator of unreachable nodes. + 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]]; + } + } + // Algorithm ends here. + + // Transform the dominators into an ordinal-indexed array and populate the self sizes. + const dominatorsTree = new Uint32Array(nodeCount); + const retainedSizes = new Float64Array(nodeCount); + const nodeSelfSizes = await nodeSelfSizesPromise; + for (let nodeOrdinal = 0; nodeOrdinal < nodeCount; nodeOrdinal++) { + dominatorsTree[nodeOrdinal] = dom[nodeOrdinal + 1] - 1; + retainedSizes[nodeOrdinal] = nodeSelfSizes[nodeOrdinal]; + } + + // Then propagate up the retained sizes for each traversed node excluding the root. + for (let i = n; i > 1; i--) { + const nodeOrdinal = vertex[i] - 1; + const dominatorOrdinal = dominatorsTree[nodeOrdinal]; + retainedSizes[dominatorOrdinal] += retainedSizes[nodeOrdinal]; + } + + return { dominatorsTree, retainedSizes }; + } + + static buildDominatedNodes( + inputs: ArgumentsToBuildDominatedNodes, + ): DominatedNodes { + const { nodeCount, dominatorsTree, rootNodeOrdinal, nodeFieldCount } = + inputs; + + // Builds up two arrays: + // - "dominatedNodes" is a continuous array, where each node owns an + // interval (can be empty) with corresponding dominated nodes. + // - "indexArray" is an array of indexes in the "dominatedNodes" + // with the same positions as in the _nodeIndex. + const indexArray = new Uint32Array(nodeCount + 1); + // All nodes except the root have dominators. + const dominatedNodes = new Uint32Array(nodeCount - 1); + + // Count the number of dominated nodes for each node. Skip the root (node at + // index 0) as it is the only node that dominates itself. + + let fromNodeOrdinal = 0; + let toNodeOrdinal: number = nodeCount; + if (rootNodeOrdinal === fromNodeOrdinal) { + fromNodeOrdinal = 1; + } else if (rootNodeOrdinal === toNodeOrdinal - 1) { + toNodeOrdinal = toNodeOrdinal - 1; + } else { + throw new Error('Root node is expected to be either first or last'); + } + for ( + let nodeOrdinal = fromNodeOrdinal; + nodeOrdinal < toNodeOrdinal; + ++nodeOrdinal + ) { + ++indexArray[dominatorsTree[nodeOrdinal]]; + } + // Put in the first slot of each dominatedNodes slice the count of entries + // that will be filled. + let firstDominatedNodeIndex = 0; + for (let i = 0, l = nodeCount; i < l; ++i) { + const dominatedCount = (dominatedNodes[firstDominatedNodeIndex] = + indexArray[i]); + indexArray[i] = firstDominatedNodeIndex; + firstDominatedNodeIndex += dominatedCount; + } + indexArray[nodeCount] = dominatedNodes.length; + // Fill up the dominatedNodes array with indexes of dominated nodes. Skip the root (node at + // index 0) as it is the only node that dominates itself. + for ( + let nodeOrdinal = fromNodeOrdinal; + nodeOrdinal < toNodeOrdinal; + ++nodeOrdinal + ) { + const dominatorOrdinal = dominatorsTree[nodeOrdinal]; + let dominatedRefIndex = indexArray[dominatorOrdinal]; + dominatedRefIndex += --dominatedNodes[dominatedRefIndex]; + dominatedNodes[dominatedRefIndex] = nodeOrdinal * nodeFieldCount; + } + + return { firstDominatedNodeIndex: indexArray, dominatedNodes }; + } + + private calculateObjectNames(): void { + const { + nodes, + nodeCount, + nodeNameOffset, + nodeNativeType, + nodeHiddenType, + nodeObjectType, + nodeCodeType, + nodeClosureType, + nodeRegExpType, + } = this; + + // If the snapshot doesn't contain a detachedness field in each node, then + // allocate a separate array so there is somewhere to store the class index. + if (this.nodeDetachednessAndClassIndexOffset === -1) { + this.detachednessAndClassIndexArray = new Uint32Array(nodeCount); + } + + // We'll add some new values to the `strings` array during the processing below. + // This map lets us easily find the index for each added string. + const stringTable = new Map(); + const getIndexForString = (s: string): number => { + let index = stringTable.get(s); + if (index === undefined) { + index = this.addString(s); + stringTable.set(s, index); + } + return index; + }; + + const hiddenClassIndex = getIndexForString('(system)'); + const codeClassIndex = getIndexForString('(compiled code)'); + const functionClassIndex = getIndexForString('Function'); + const regExpClassIndex = getIndexForString('RegExp'); + + function getNodeClassIndex(node: HeapSnapshotNode): number { + switch (node.rawType()) { + case nodeHiddenType: + return hiddenClassIndex; + case nodeObjectType: + case nodeNativeType: { + let name = node.rawName(); + + // If the node name is (for example) '
', then the class + // name should be just '
'. If the node name is already short + // enough, like '
', we must still call getIndexForString on that + // name, because the names added by getIndexForString are not + // deduplicated with preexisting strings, and we want all objects with + // class name '
' to refer to that class name via the same index. + // Otherwise, object categorization doesn't work. + if (name.startsWith('<')) { + const firstSpace = name.indexOf(' '); + if (firstSpace !== -1) { + name = name.substring(0, firstSpace) + '>'; + } + return getIndexForString(name); + } + if (name.startsWith('Detached <')) { + const firstSpace = name.indexOf(' ', 10); + if (firstSpace !== -1) { + name = name.substring(0, firstSpace) + '>'; + } + return getIndexForString(name); + } + + // Avoid getIndexForString here; the class name index should match the name index. + return nodes.getValue(node.nodeIndex + nodeNameOffset); + } + case nodeCodeType: + return codeClassIndex; + case nodeClosureType: + return functionClassIndex; + case nodeRegExpType: + return regExpClassIndex; + default: + return getIndexForString('(' + node.type() + ')'); + } + } + + const node = this.createNode(0); + for (let i = 0; i < nodeCount; ++i) { + node.setClassIndex(getNodeClassIndex(node)); + node.nodeIndex = node.nextNodeIndex(); + } + } + + interfaceDefinitions(): string { + return JSON.stringify(this.#interfaceDefinitions ?? []); + } + + private isPlainJSObject(node: HeapSnapshotNode): boolean { + return ( + node.rawType() === this.nodeObjectType && node.rawName() === 'Object' + ); + } + + private inferInterfaceDefinitions(): InterfaceDefinition[] { + const { edgePropertyType } = this; + + // First, produce a set of candidate definitions by iterating the properties + // on every plain JS Object in the snapshot. + interface InterfaceDefinitionCandidate extends InterfaceDefinition { + // How many objects start with these properties in this order. + count: number; + } + // A map from interface names to their definitions. + const candidates = new Map(); + let totalObjectCount = 0; + for (let it = this.allNodes(); it.hasNext(); it.next()) { + const node = it.item(); + if (!this.isPlainJSObject(node)) { + continue; + } + ++totalObjectCount; + let interfaceName = '{'; + const properties: string[] = []; + for (let edgeIt = node.edges(); edgeIt.hasNext(); edgeIt.next()) { + const edge = edgeIt.item(); + const edgeName = edge.name(); + if (edge.rawType() !== edgePropertyType || edgeName === '__proto__') { + continue; + } + const formattedEdgeName = + JSHeapSnapshotNode.formatPropertyName(edgeName); + if ( + interfaceName.length > MIN_INTERFACE_PROPERTY_COUNT && + interfaceName.length + formattedEdgeName.length > + MAX_INTERFACE_NAME_LENGTH + ) { + break; // The interface name is getting too long. + } + if (interfaceName.length !== 1) { + interfaceName += ', '; + } + interfaceName += formattedEdgeName; + properties.push(edgeName); + } + // The empty interface is not very meaningful, and can be sort of misleading + // since someone might incorrectly interpret it as objects with no properties. + if (properties.length === 0) { + continue; + } + interfaceName += '}'; + const candidate = candidates.get(interfaceName); + if (candidate) { + ++candidate.count; + } else { + candidates.set(interfaceName, { + name: interfaceName, + properties, + count: 1, + }); + } + } + + // Next, sort the candidates and select the most popular ones. It's possible that + // some candidates represent the same properties in different orders, but that's + // okay: by sorting here, we ensure that the most popular ordering appears first + // in the result list, and the rules for applying interface definitions will prefer + // the first matching definition if multiple matches contain the same properties. + const sortedCandidates = Array.from(candidates.values()); + sortedCandidates.sort((a, b) => b.count - a.count); + const result: InterfaceDefinition[] = []; + const minCount = Math.max( + MIN_OBJECT_COUNT_PER_INTERFACE, + totalObjectCount / MIN_OBJECT_PROPORTION_PER_INTERFACE, + ); + for (let i = 0; i < sortedCandidates.length; ++i) { + const candidate = sortedCandidates[i]; + if (candidate.count < minCount) { + break; + } + result.push(candidate); + } + + return result; + } + + private applyInterfaceDefinitions(definitions: InterfaceDefinition[]): void { + const { edgePropertyType } = this; + this.#interfaceDefinitions = definitions; + + // Any computed aggregate data will be wrong after recategorization, so clear it. + this.#aggregates = {}; + this.#aggregatesSortedFlags = {}; + + // Information about a named interface. + interface MatchInfo { + name: string; + // The number of properties listed in the interface definition. + propertyCount: number; + // The position of the interface definition in the list of definitions. + index: number; + } + + function selectBetterMatch(a: MatchInfo, b: MatchInfo | null): MatchInfo { + if (!b || a.propertyCount > b.propertyCount) { + return a; + } + if (b.propertyCount > a.propertyCount) { + return b; + } + return a.index <= b.index ? a : b; + } + + // A node in the tree which allows us to search for interfaces matching an object. + // Each edge in this tree represents adding a property, starting from an empty + // object. Properties must be iterated in sorted order. + interface PropertyTreeNode { + // All possible successors from this node. Keys are property names. + next: Map; + // If this node corresponds to a named interface, then matchInfo contains that name. + matchInfo: MatchInfo | null; + // The maximum of all keys in `next`. This helps determine when no further transitions + // are possible from this node. + greatestNext: string | null; + } + + // The root node of the tree. + const propertyTree: PropertyTreeNode = { + next: new Map(), + matchInfo: null, + greatestNext: null, + }; + + // Build up the property tree. + for ( + let interfaceIndex = 0; + interfaceIndex < definitions.length; + ++interfaceIndex + ) { + const definition = definitions[interfaceIndex]; + // const properties = definition.properties.toSorted(); not supported on Node v18 + const properties = [...definition.properties].sort(); + let currentNode = propertyTree; + for (const property of properties) { + const nextMap = currentNode.next; + let nextNode = nextMap.get(property); + if (!nextNode) { + nextNode = { + next: new Map(), + matchInfo: null, + greatestNext: null, + }; + nextMap.set(property, nextNode); + if ( + currentNode.greatestNext === null || + currentNode.greatestNext < property + ) { + currentNode.greatestNext = property; + } + } + currentNode = nextNode; + } + // Only set matchInfo on this node if it wasn't already set, to ensure that + // interfaces defined earlier in the list have priority. + if (!currentNode.matchInfo) { + currentNode.matchInfo = { + name: definition.name, + propertyCount: properties.length, + index: interfaceIndex, + }; + } + } + + // The fallback match for objects which don't match any defined interface. + const initialMatch: MatchInfo = { + name: 'Object', + propertyCount: 0, + index: Infinity, + }; + + // Iterate all nodes and check whether they match a named interface, using + // the tree constructed above. Then update the class name for each node. + for (let it = this.allNodes(); it.hasNext(); it.next()) { + const node = it.item(); + if (!this.isPlainJSObject(node)) { + continue; + } + + // Collect and sort the properties of this object. + const properties: string[] = []; + for (let edgeIt = node.edges(); edgeIt.hasNext(); edgeIt.next()) { + const edge = edgeIt.item(); + if (edge.rawType() === edgePropertyType) { + properties.push(edge.name()); + } + } + properties.sort(); + + // We may explore multiple possible paths through the tree, so this set tracks + // all states that match with the properties iterated thus far. + const states = new Set(); + states.add(propertyTree); + + // This variable represents the best match found thus far. We start by checking + // whether there is an interface definition for the empty object. + let match = selectBetterMatch(initialMatch, propertyTree.matchInfo); + + // Traverse the tree to find any matches. + for (const property of properties) { + // Iterate only the states that already exist, not the ones added during the loop below. + for (const currentState of Array.from(states.keys())) { + if ( + currentState.greatestNext === null || + property >= currentState.greatestNext + ) { + // No further transitions are possible from this state. + states.delete(currentState); + } + const nextState = currentState.next.get(property); + if (nextState) { + states.add(nextState); + match = selectBetterMatch(match, nextState.matchInfo); + } + } + } + + // Update the node's class name accordingly. + let classIndex = + match === initialMatch + ? node.rawNameIndex() + : this.#interfaceNames.get(match.name); + if (classIndex === undefined) { + classIndex = this.addString(match.name); + this.#interfaceNames.set(match.name, classIndex); + } + node.setClassIndex(classIndex); + } + } + + /** + * Iterates children of a node. + */ + private iterateFilteredChildren( + nodeOrdinal: number, + edgeFilterCallback: (arg0: number) => boolean, + childCallback: (arg0: number) => void, + ): void { + const beginEdgeIndex = this.firstEdgeIndexes[nodeOrdinal]; + const endEdgeIndex = this.firstEdgeIndexes[nodeOrdinal + 1]; + for ( + let edgeIndex = beginEdgeIndex; + edgeIndex < endEdgeIndex; + edgeIndex += this.edgeFieldsCount + ) { + const childNodeIndex = this.containmentEdges.getValue( + edgeIndex + this.edgeToNodeOffset, + ); + const childNodeOrdinal = childNodeIndex / this.nodeFieldCount; + const type = this.containmentEdges.getValue( + edgeIndex + this.edgeTypeOffset, + ); + if (!edgeFilterCallback(type)) { + continue; + } + childCallback(childNodeOrdinal); + } + } + + /** + * Adds a string to the snapshot. + */ + private addString(string: string): number { + this.strings.push(string); + return this.strings.length - 1; + } + + /** + * The phase propagates whether a node is attached or detached through the + * graph and adjusts the low-level representation of nodes. + * + * State propagation: + * 1. Any object reachable from an attached object is itself attached. + * 2. Any object reachable from a detached object that is not already + * attached is considered detached. + * + * Representation: + * - Name of any detached node is changed from """ to + * "Detached ". + */ + private propagateDOMState(): void { + if (this.nodeDetachednessAndClassIndexOffset === -1) { + return; + } + + const visited = new Uint8Array(this.nodeCount); + const attached: number[] = []; + const detached: number[] = []; + + const stringIndexCache = new Map(); + const node = this.createNode(0); + + /** + * Adds a 'Detached ' prefix to the name of a node. + */ + const addDetachedPrefixToNodeName = function ( + snapshot: HeapSnapshot, + nodeIndex: number, + ): void { + const oldStringIndex = snapshot.nodes.getValue( + nodeIndex + snapshot.nodeNameOffset, + ); + let newStringIndex = stringIndexCache.get(oldStringIndex); + if (newStringIndex === undefined) { + newStringIndex = snapshot.addString( + 'Detached ' + snapshot.strings[oldStringIndex], + ); + stringIndexCache.set(oldStringIndex, newStringIndex); + } + snapshot.nodes.setValue( + nodeIndex + snapshot.nodeNameOffset, + newStringIndex, + ); + }; + + /** + * Processes a node represented by nodeOrdinal: + * - Changes its name based on newState. + * - Puts it onto working sets for attached or detached nodes. + */ + const processNode = function ( + snapshot: HeapSnapshot, + nodeOrdinal: number, + newState: number, + ): void { + if (visited[nodeOrdinal]) { + return; + } + + const nodeIndex = nodeOrdinal * snapshot.nodeFieldCount; + + // Early bailout: Do not propagate the state (and name change) through JavaScript. 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.getValue(nodeIndex + snapshot.nodeTypeOffset) !== + snapshot.nodeNativeType + ) { + visited[nodeOrdinal] = 1; + return; + } + + node.nodeIndex = nodeIndex; + node.setDetachedness(newState); + + if (newState === DOMLinkState.ATTACHED) { + attached.push(nodeOrdinal); + } else if (newState === DOMLinkState.DETACHED) { + // Detached state: Rewire node name. + addDetachedPrefixToNodeName(snapshot, nodeIndex); + detached.push(nodeOrdinal); + } + + visited[nodeOrdinal] = 1; + }; + + const propagateState = function ( + snapshot: HeapSnapshot, + parentNodeOrdinal: number, + newState: number, + ): void { + snapshot.iterateFilteredChildren( + parentNodeOrdinal, + (edgeType) => + ![ + snapshot.edgeHiddenType, + snapshot.edgeInvisibleType, + snapshot.edgeWeakType, + ].includes(edgeType), + (nodeOrdinal) => processNode(snapshot, nodeOrdinal, newState), + ); + }; + + // 1. We re-use the deserialized field to store the propagated state. While + // the state for known nodes is already set, they still need to go + // through processing to have their name adjusted and them enqueued in + // the respective queues. + for (let nodeOrdinal = 0; nodeOrdinal < this.nodeCount; ++nodeOrdinal) { + node.nodeIndex = nodeOrdinal * this.nodeFieldCount; + const state = node.detachedness(); + // Bail out for objects that have no known state. For all other objects set that state. + if (state === DOMLinkState.UNKNOWN) { + continue; + } + processNode(this, nodeOrdinal, state); + } + // 2. If the parent is attached, then the child is also attached. + while (attached.length !== 0) { + const nodeOrdinal = attached.pop() as number; + propagateState(this, nodeOrdinal, DOMLinkState.ATTACHED); + } + // 3. If the parent is not attached, then the child inherits the parent's state. + while (detached.length !== 0) { + const nodeOrdinal = detached.pop() as number; + node.nodeIndex = nodeOrdinal * this.nodeFieldCount; + const nodeState = node.detachedness(); + // Ignore if the node has been found through propagating forward attached state. + if (nodeState === DOMLinkState.ATTACHED) { + continue; + } + propagateState(this, nodeOrdinal, DOMLinkState.DETACHED); + } + } + + private buildSamples(): void { + const samples = this.#rawSamples; + if (!samples?.length) { + return; + } + const sampleCount = samples.length / 2; + const sizeForRange = new Array(sampleCount); + const timestamps = new Array(sampleCount); + const lastAssignedIds = new Array(sampleCount); + + const timestampOffset = + this.#metaNode.sample_fields.indexOf('timestamp_us'); + const lastAssignedIdOffset = + this.#metaNode.sample_fields.indexOf('last_assigned_id'); + for (let i = 0; i < sampleCount; i++) { + sizeForRange[i] = 0; + timestamps[i] = samples[2 * i + timestampOffset] / 1000; + lastAssignedIds[i] = samples[2 * i + lastAssignedIdOffset]; + } + + const nodes = this.nodes; + const nodesLength = nodes.length; + const nodeFieldCount = this.nodeFieldCount; + const node = this.rootNode(); + for ( + let nodeIndex = 0; + nodeIndex < nodesLength; + nodeIndex += nodeFieldCount + ) { + node.nodeIndex = nodeIndex; + + const nodeId = node.id(); + // JS objects have odd ids, skip native objects. + if (nodeId % 2 === 0) { + continue; + } + const rangeIndex = Platform.ArrayUtilities.lowerBound( + lastAssignedIds, + nodeId, + Platform.ArrayUtilities.DEFAULT_COMPARATOR, + ); + if (rangeIndex === sampleCount) { + // TODO: make heap profiler not allocate while taking snapshot + continue; + } + sizeForRange[rangeIndex] += node.selfSize(); + } + this.#samples = new HeapSnapshotModel.Samples( + timestamps, + lastAssignedIds, + sizeForRange, + ); + } + + private buildLocationMap(): void { + const map = new Map(); + const locations = this.#locations; + + for (let i = 0; i < locations.length; i += this.#locationFieldCount) { + const nodeIndex = locations[i + this.#locationIndexOffset]; + const scriptId = locations[i + this.#locationScriptIdOffset]; + const line = locations[i + this.#locationLineOffset]; + const col = locations[i + this.#locationColumnOffset]; + map.set(nodeIndex, new HeapSnapshotModel.Location(scriptId, line, col)); + } + + this.#locationMap = map; + } + + getLocation(nodeIndex: number): HeapSnapshotModel.Location | null { + return this.#locationMap.get(nodeIndex) || null; + } + + getSamples(): HeapSnapshotModel.Samples | null { + return this.#samples; + } + + calculateFlags(): void { + throw new Error('Not implemented'); + } + + calculateStatistics(): void { + throw new Error('Not implemented'); + } + + userObjectsMapAndFlag(): { map: Uint8Array; flag: number } | null { + throw new Error('Not implemented'); + } + + calculateSnapshotDiff( + baseSnapshotId: string, + baseSnapshotAggregates: { + [x: string]: HeapSnapshotModel.AggregateForDiff; + }, + ): { [x: string]: HeapSnapshotModel.Diff } { + let snapshotDiff: + | { [x: string]: HeapSnapshotModel.Diff } + | { + [x: string]: HeapSnapshotModel.Diff; + } = this.#snapshotDiffs[baseSnapshotId]; + if (snapshotDiff) { + return snapshotDiff; + } + snapshotDiff = {} as { + [x: string]: HeapSnapshotModel.Diff; + }; + + const aggregates = this.getAggregatesByClassKey(true, 'allObjects'); + for (const classKey in baseSnapshotAggregates) { + const baseAggregate = baseSnapshotAggregates[classKey]; + const diff = this.calculateDiffForClass( + baseAggregate, + aggregates[classKey], + ); + if (diff) { + snapshotDiff[classKey] = diff; + } + } + const emptyBaseAggregate = new HeapSnapshotModel.AggregateForDiff(); + for (const classKey in aggregates) { + if (classKey in baseSnapshotAggregates) { + continue; + } + const classDiff = this.calculateDiffForClass( + emptyBaseAggregate, + aggregates[classKey], + ); + if (classDiff) { + snapshotDiff[classKey] = classDiff; + } + } + + this.#snapshotDiffs[baseSnapshotId] = snapshotDiff; + return snapshotDiff; + } + + private calculateDiffForClass( + baseAggregate: HeapSnapshotModel.AggregateForDiff, + aggregate?: HeapSnapshotModel.Aggregate, + ): HeapSnapshotModel.Diff | null { + const baseIds = baseAggregate.ids; + const baseIndexes = baseAggregate.indexes; + const baseSelfSizes = baseAggregate.selfSizes; + + const indexes = aggregate ? aggregate.idxs : []; + + let i = 0; + let j = 0; + const l = baseIds.length; + const m = indexes.length; + const diff = new HeapSnapshotModel.Diff( + aggregate ? aggregate.name : baseAggregate.name, + ); + + const nodeB = this.createNode(indexes[j]); + while (i < l && j < m) { + const nodeAId = baseIds[i]; + if (nodeAId < nodeB.id()) { + diff.deletedIndexes.push(baseIndexes[i]); + diff.removedCount++; + diff.removedSize += baseSelfSizes[i]; + ++i; + } else if (nodeAId > nodeB.id()) { + // Native nodes(e.g. dom groups) may have ids less than max JS object id in the base snapshot + diff.addedIndexes.push(indexes[j]); + diff.addedCount++; + diff.addedSize += nodeB.selfSize(); + nodeB.nodeIndex = indexes[++j]; + } else { + // nodeAId === nodeB.id() + ++i; + nodeB.nodeIndex = indexes[++j]; + } + } + while (i < l) { + diff.deletedIndexes.push(baseIndexes[i]); + diff.removedCount++; + diff.removedSize += baseSelfSizes[i]; + ++i; + } + while (j < m) { + diff.addedIndexes.push(indexes[j]); + diff.addedCount++; + diff.addedSize += nodeB.selfSize(); + nodeB.nodeIndex = indexes[++j]; + } + diff.countDelta = diff.addedCount - diff.removedCount; + diff.sizeDelta = diff.addedSize - diff.removedSize; + if (!diff.addedCount && !diff.removedCount) { + return null; + } + return diff; + } + + private nodeForSnapshotObjectId( + snapshotObjectId: number, + ): HeapSnapshotNode | null { + for (let it = this.allNodes(); it.hasNext(); it.next()) { + if (it.node.id() === snapshotObjectId) { + return it.node; + } + } + return null; + } + + // Converts an internal class key, suitable for categorizing within this + // snapshot, to a public class key, which can be used in comparisons + // between multiple snapshots. + classKeyFromClassKeyInternal(key: string | number): string { + return typeof key === 'number' ? ',' + this.strings[key] : key; + } + + nodeClassKey(snapshotObjectId: number): string | null { + const node = this.nodeForSnapshotObjectId(snapshotObjectId); + if (node) { + return this.classKeyFromClassKeyInternal(node.classKeyInternal()); + } + return null; + } + + idsOfObjectsWithName(name: string): number[] { + const ids = []; + for (let it = this.allNodes(); it.hasNext(); it.next()) { + if (it.item().name() === name) { + ids.push(it.item().id()); + } + } + return ids; + } + + createEdgesProvider(nodeIndex: number): HeapSnapshotEdgesProvider { + const node = this.createNode(nodeIndex); + const filter = this.containmentEdgesFilter(); + const indexProvider = new HeapSnapshotEdgeIndexProvider(this); + return new HeapSnapshotEdgesProvider( + this, + filter, + node.edges(), + indexProvider, + ); + } + + createEdgesProviderForTest( + nodeIndex: number, + filter: ((arg0: HeapSnapshotEdge) => boolean) | null, + ): HeapSnapshotEdgesProvider { + const node = this.createNode(nodeIndex); + const indexProvider = new HeapSnapshotEdgeIndexProvider(this); + return new HeapSnapshotEdgesProvider( + this, + filter, + node.edges(), + indexProvider, + ); + } + + retainingEdgesFilter(): ((arg0: HeapSnapshotEdge) => boolean) | null { + return null; + } + + containmentEdgesFilter(): ((arg0: HeapSnapshotEdge) => boolean) | null { + return null; + } + + createRetainingEdgesProvider(nodeIndex: number): HeapSnapshotEdgesProvider { + const node = this.createNode(nodeIndex); + const filter = this.retainingEdgesFilter(); + const indexProvider = new HeapSnapshotRetainerEdgeIndexProvider(this); + return new HeapSnapshotEdgesProvider( + this, + filter, + node.retainers(), + indexProvider, + ); + } + + createAddedNodesProvider( + baseSnapshotId: string, + classKey: string, + ): HeapSnapshotNodesProvider { + const snapshotDiff = this.#snapshotDiffs[baseSnapshotId]; + const diffForClass = snapshotDiff[classKey]; + return new HeapSnapshotNodesProvider(this, diffForClass.addedIndexes); + } + + createDeletedNodesProvider(nodeIndexes: number[]): HeapSnapshotNodesProvider { + return new HeapSnapshotNodesProvider(this, nodeIndexes); + } + + createNodesProviderForClass( + classKey: string, + nodeFilter: HeapSnapshotModel.NodeFilter, + ): HeapSnapshotNodesProvider { + return new HeapSnapshotNodesProvider( + this, + this.aggregatesWithFilter(nodeFilter)[classKey].idxs, + ); + } + + private maxJsNodeId(): number { + const nodeFieldCount = this.nodeFieldCount; + const nodes = this.nodes; + const nodesLength = nodes.length; + let id = 0; + for ( + let nodeIndex = this.nodeIdOffset; + nodeIndex < nodesLength; + nodeIndex += nodeFieldCount + ) { + const nextId = nodes.getValue(nodeIndex); + // JS objects have odd ids, skip native objects. + if (nextId % 2 === 0) { + continue; + } + if (id < nextId) { + id = nextId; + } + } + return id; + } + + updateStaticData(): HeapSnapshotModel.StaticData { + return new HeapSnapshotModel.StaticData( + this.nodeCount, + this.rootNodeIndexInternal, + this.totalSize, + this.maxJsNodeId(), + ); + } + + ignoreNodeInRetainersView(nodeIndex: number): void { + this.#ignoredNodesInRetainersView.add(nodeIndex); + this.calculateDistances(/* isForRetainersView=*/ true); + this.#updateIgnoredEdgesInRetainersView(); + } + + unignoreNodeInRetainersView(nodeIndex: number): void { + this.#ignoredNodesInRetainersView.delete(nodeIndex); + if (this.#ignoredNodesInRetainersView.size === 0) { + this.#nodeDistancesForRetainersView = undefined; + } else { + this.calculateDistances(/* isForRetainersView=*/ true); + } + this.#updateIgnoredEdgesInRetainersView(); + } + + unignoreAllNodesInRetainersView(): void { + this.#ignoredNodesInRetainersView.clear(); + this.#nodeDistancesForRetainersView = undefined; + this.#updateIgnoredEdgesInRetainersView(); + } + + #updateIgnoredEdgesInRetainersView(): void { + const distances = this.#nodeDistancesForRetainersView; + this.#ignoredEdgesInRetainersView.clear(); + if (distances === undefined) { + return; + } + + // To retain a value in a WeakMap, both the WeakMap and the corresponding + // key must stay alive. If one of those two retainers is unreachable due to + // the user ignoring some nodes, then the other retainer edge should also be + // shown as unreachable, since it would be insufficient on its own to retain + // the value. + const unreachableWeakMapEdges = new Platform.MapUtilities.Multimap< + number, + string + >(); + const noDistance = this.#noDistance; + const { nodeCount, nodeFieldCount } = this; + const node = this.createNode(0); + + // Populate unreachableWeakMapEdges. + for (let nodeOrdinal = 0; nodeOrdinal < nodeCount; ++nodeOrdinal) { + if (distances[nodeOrdinal] !== noDistance) { + continue; + } + node.nodeIndex = nodeOrdinal * nodeFieldCount; + for (let iter = node.edges(); iter.hasNext(); iter.next()) { + const edge = iter.edge; + if (!edge.isInternal()) { + continue; + } + const match = this.tryParseWeakMapEdgeName(edge.nameIndex()); + if (match) { + unreachableWeakMapEdges.set(edge.nodeIndex(), match.duplicatedPart); + } + } + } + + // Iterate the retaining edges for the target nodes found in the previous + // step and mark any relevant WeakMap edges as ignored. + for (const targetNodeIndex of unreachableWeakMapEdges.keys()) { + node.nodeIndex = targetNodeIndex; + for (let it = node.retainers(); it.hasNext(); it.next()) { + const reverseEdge = it.item(); + if (!reverseEdge.isInternal()) { + continue; + } + const match = this.tryParseWeakMapEdgeName(reverseEdge.nameIndex()); + if ( + match && + unreachableWeakMapEdges.hasValue( + targetNodeIndex, + match.duplicatedPart, + ) + ) { + const forwardEdgeIndex = this.retainingEdges[reverseEdge.itemIndex()]; + this.#ignoredEdgesInRetainersView.add(forwardEdgeIndex); + } + } + } + } + + areNodesIgnoredInRetainersView(): boolean { + return this.#ignoredNodesInRetainersView.size > 0; + } + + getDistanceForRetainersView(nodeIndex: number): number { + const nodeOrdinal = nodeIndex / this.nodeFieldCount; + const distances = this.#nodeDistancesForRetainersView ?? this.nodeDistances; + const distance = distances[nodeOrdinal]; + if (distance === this.#noDistance) { + // An unreachable node should be sorted to the end, not the beginning. + // To give such nodes a reasonable sorting order, we add a very large + // number to the original distance computed without ignoring any nodes. + return ( + Math.max(0, this.nodeDistances[nodeOrdinal]) + + HeapSnapshotModel.baseUnreachableDistance + ); + } + return distance; + } + + isNodeIgnoredInRetainersView(nodeIndex: number): boolean { + return this.#ignoredNodesInRetainersView.has(nodeIndex); + } + + isEdgeIgnoredInRetainersView(edgeIndex: number): boolean { + return this.#ignoredEdgesInRetainersView.has(edgeIndex); + } +} + +interface HeapSnapshotMetaInfo { + /* eslint-disable @typescript-eslint/naming-convention */ + location_fields: string[]; + node_fields: string[]; + node_types: string[][]; + edge_fields: string[]; + edge_types: string[][]; + trace_function_info_fields: string[]; + trace_node_fields: string[]; + sample_fields: string[]; + type_strings: { [key: string]: string }; + /* eslint-enable @typescript-eslint/naming-convention */ +} + +export interface HeapSnapshotHeader { + /* eslint-disable @typescript-eslint/naming-convention */ + title: string; + meta: HeapSnapshotMetaInfo; + node_count: number; + edge_count: number; + trace_function_count: number; + root_index: number; + extra_native_bytes?: number; + /* eslint-enable @typescript-eslint/naming-convention */ +} + +export abstract class HeapSnapshotItemProvider { + protected readonly iterator: HeapSnapshotItemIterator; + readonly #indexProvider: HeapSnapshotItemIndexProvider; + readonly #isEmptyInternal: boolean; + protected iterationOrder: number[] | null; + protected currentComparator: HeapSnapshotModel.ComparatorConfig | null; + #sortedPrefixLength: number; + #sortedSuffixLength: number; + constructor( + iterator: HeapSnapshotItemIterator, + indexProvider: HeapSnapshotItemIndexProvider, + ) { + this.iterator = iterator; + this.#indexProvider = indexProvider; + this.#isEmptyInternal = !iterator.hasNext(); + this.iterationOrder = null; + this.currentComparator = null; + this.#sortedPrefixLength = 0; + this.#sortedSuffixLength = 0; + } + + protected createIterationOrder(): void { + if (this.iterationOrder) { + return; + } + this.iterationOrder = []; + for (let iterator = this.iterator; iterator.hasNext(); iterator.next()) { + this.iterationOrder.push(iterator.item().itemIndex()); + } + } + + isEmpty(): boolean { + return this.#isEmptyInternal; + } + + serializeItemsRange( + begin: number, + end: number, + ): HeapSnapshotModel.ItemsRange { + this.createIterationOrder(); + if (begin > end) { + throw new Error('Start position > end position: ' + begin + ' > ' + end); + } + + if (!this.iterationOrder) { + throw new Error('Iteration order undefined'); + } + + if (end > this.iterationOrder.length) { + end = this.iterationOrder.length; + } + if ( + this.#sortedPrefixLength < end && + begin < this.iterationOrder.length - this.#sortedSuffixLength && + this.currentComparator + ) { + const currentComparator = this.currentComparator; + this.sort( + currentComparator, + this.#sortedPrefixLength, + this.iterationOrder.length - 1 - this.#sortedSuffixLength, + begin, + end - 1, + ); + if (begin <= this.#sortedPrefixLength) { + this.#sortedPrefixLength = end; + } + if (end >= this.iterationOrder.length - this.#sortedSuffixLength) { + this.#sortedSuffixLength = this.iterationOrder.length - begin; + } + } + let position = begin; + const count = end - begin; + const result = new Array(count); + for (let i = 0; i < count; ++i) { + const itemIndex = this.iterationOrder[position++]; + const item = this.#indexProvider.itemForIndex(itemIndex); + result[i] = item.serialize(); + } + return new HeapSnapshotModel.ItemsRange( + begin, + end, + this.iterationOrder.length, + result, + ); + } + + sortAndRewind(comparator: HeapSnapshotModel.ComparatorConfig): void { + this.currentComparator = comparator; + this.#sortedPrefixLength = 0; + this.#sortedSuffixLength = 0; + } + + abstract sort( + comparator: HeapSnapshotModel.ComparatorConfig, + leftBound: number, + rightBound: number, + windowLeft: number, + windowRight: number, + ): void; +} + +export class HeapSnapshotEdgesProvider extends HeapSnapshotItemProvider { + snapshot: HeapSnapshot; + constructor( + snapshot: HeapSnapshot, + filter: ((arg0: HeapSnapshotEdge) => boolean) | null, + edgesIter: HeapSnapshotEdgeIterator | HeapSnapshotRetainerEdgeIterator, + indexProvider: HeapSnapshotItemIndexProvider, + ) { + const iter = filter + ? new HeapSnapshotFilteredIterator( + edgesIter, + filter as (arg0: HeapSnapshotItem) => boolean, + ) + : edgesIter; + super(iter, indexProvider); + this.snapshot = snapshot; + } + + sort( + comparator: HeapSnapshotModel.ComparatorConfig, + leftBound: number, + rightBound: number, + windowLeft: number, + windowRight: number, + ): void { + const fieldName1 = comparator.fieldName1; + const fieldName2 = comparator.fieldName2; + const ascending1 = comparator.ascending1; + const ascending2 = comparator.ascending2; + + const edgeA = ( + this.iterator.item() as HeapSnapshotEdge | HeapSnapshotRetainerEdge + ).clone(); + const edgeB = edgeA.clone(); + const nodeA = this.snapshot.createNode(); + const nodeB = this.snapshot.createNode(); + + function compareEdgeField( + fieldName: string, + ascending: boolean, + indexA: number, + indexB: number, + ): number { + edgeA.edgeIndex = indexA; + edgeB.edgeIndex = indexB; + let result = 0; + if (fieldName === '!edgeName') { + if (edgeB.name() === '__proto__') { + return -1; + } + if (edgeA.name() === '__proto__') { + return 1; + } + result = + edgeA.hasStringName() === edgeB.hasStringName() + ? edgeA.name() < edgeB.name() + ? -1 + : edgeA.name() > edgeB.name() + ? 1 + : 0 + : edgeA.hasStringName() + ? -1 + : 1; + } else { + result = + edgeA.getValueForSorting(fieldName) - + edgeB.getValueForSorting(fieldName); + } + return ascending ? result : -result; + } + + function compareNodeField( + fieldName: string, + ascending: boolean, + indexA: number, + indexB: number, + ): number { + edgeA.edgeIndex = indexA; + nodeA.nodeIndex = edgeA.nodeIndex(); + // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const valueA = (nodeA as any)[fieldName](); + + edgeB.edgeIndex = indexB; + nodeB.nodeIndex = edgeB.nodeIndex(); + // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const valueB = (nodeB as any)[fieldName](); + + const result = valueA < valueB ? -1 : valueA > valueB ? 1 : 0; + return ascending ? result : -result; + } + + function compareEdgeAndEdge(indexA: number, indexB: number): number { + let result = compareEdgeField(fieldName1, ascending1, indexA, indexB); + if (result === 0) { + result = compareEdgeField(fieldName2, ascending2, indexA, indexB); + } + if (result === 0) { + return indexA - indexB; + } + return result; + } + + function compareEdgeAndNode(indexA: number, indexB: number): number { + let result = compareEdgeField(fieldName1, ascending1, indexA, indexB); + if (result === 0) { + result = compareNodeField(fieldName2, ascending2, indexA, indexB); + } + if (result === 0) { + return indexA - indexB; + } + return result; + } + + function compareNodeAndEdge(indexA: number, indexB: number): number { + let result = compareNodeField(fieldName1, ascending1, indexA, indexB); + if (result === 0) { + result = compareEdgeField(fieldName2, ascending2, indexA, indexB); + } + if (result === 0) { + return indexA - indexB; + } + return result; + } + + function compareNodeAndNode(indexA: number, indexB: number): number { + let result = compareNodeField(fieldName1, ascending1, indexA, indexB); + if (result === 0) { + result = compareNodeField(fieldName2, ascending2, indexA, indexB); + } + if (result === 0) { + return indexA - indexB; + } + return result; + } + + if (!this.iterationOrder) { + throw new Error('Iteration order not defined'); + } + + function isEdgeFieldName(fieldName: string): boolean { + return fieldName.startsWith('!edge'); + } + + if (isEdgeFieldName(fieldName1)) { + if (isEdgeFieldName(fieldName2)) { + Platform.ArrayUtilities.sortRange( + this.iterationOrder, + compareEdgeAndEdge, + leftBound, + rightBound, + windowLeft, + windowRight, + ); + } else { + Platform.ArrayUtilities.sortRange( + this.iterationOrder, + compareEdgeAndNode, + leftBound, + rightBound, + windowLeft, + windowRight, + ); + } + } else if (isEdgeFieldName(fieldName2)) { + Platform.ArrayUtilities.sortRange( + this.iterationOrder, + compareNodeAndEdge, + leftBound, + rightBound, + windowLeft, + windowRight, + ); + } else { + Platform.ArrayUtilities.sortRange( + this.iterationOrder, + compareNodeAndNode, + leftBound, + rightBound, + windowLeft, + windowRight, + ); + } + } +} + +export class HeapSnapshotNodesProvider extends HeapSnapshotItemProvider { + snapshot: HeapSnapshot; + constructor(snapshot: HeapSnapshot, nodeIndexes: number[] | Uint32Array) { + const indexProvider = new HeapSnapshotNodeIndexProvider(snapshot); + const it = new HeapSnapshotIndexRangeIterator(indexProvider, nodeIndexes); + super(it, indexProvider); + this.snapshot = snapshot; + } + + nodePosition(snapshotObjectId: number): number { + this.createIterationOrder(); + const node = this.snapshot.createNode(); + let i = 0; + if (!this.iterationOrder) { + throw new Error('Iteration order not defined'); + } + + for (; i < this.iterationOrder.length; i++) { + node.nodeIndex = this.iterationOrder[i]; + if (node.id() === snapshotObjectId) { + break; + } + } + if (i === this.iterationOrder.length) { + return -1; + } + const targetNodeIndex = this.iterationOrder[i]; + let smallerCount = 0; + + const currentComparator = this + .currentComparator as HeapSnapshotModel.ComparatorConfig; + const compare = this.buildCompareFunction(currentComparator); + for (let i = 0; i < this.iterationOrder.length; i++) { + if (compare(this.iterationOrder[i], targetNodeIndex) < 0) { + ++smallerCount; + } + } + return smallerCount; + } + + private buildCompareFunction( + comparator: HeapSnapshotModel.ComparatorConfig, + ): (arg0: number, arg1: number) => number { + const nodeA = this.snapshot.createNode(); + const nodeB = this.snapshot.createNode(); + // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const fieldAccessor1 = (nodeA as any)[comparator.fieldName1]; + // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const fieldAccessor2 = (nodeA as any)[comparator.fieldName2]; + const ascending1 = comparator.ascending1 ? 1 : -1; + const ascending2 = comparator.ascending2 ? 1 : -1; + + function sortByNodeField( + fieldAccessor: () => void, + ascending: number, + ): number { + const valueA = fieldAccessor.call(nodeA); + const valueB = fieldAccessor.call(nodeB); + return valueA < valueB ? -ascending : valueA > valueB ? ascending : 0; + } + + function sortByComparator(indexA: number, indexB: number): number { + nodeA.nodeIndex = indexA; + nodeB.nodeIndex = indexB; + let result = sortByNodeField(fieldAccessor1, ascending1); + if (result === 0) { + result = sortByNodeField(fieldAccessor2, ascending2); + } + return result || indexA - indexB; + } + + return sortByComparator; + } + + sort( + comparator: HeapSnapshotModel.ComparatorConfig, + leftBound: number, + rightBound: number, + windowLeft: number, + windowRight: number, + ): void { + if (!this.iterationOrder) { + throw new Error('Iteration order not defined'); + } + + Platform.ArrayUtilities.sortRange( + this.iterationOrder, + this.buildCompareFunction(comparator), + leftBound, + rightBound, + windowLeft, + windowRight, + ); + } +} + +export class JSHeapSnapshot extends HeapSnapshot { + readonly nodeFlags: { + // bit flags in 8-bit value + canBeQueried: number; + detachedDOMTreeNode: number; + pageObject: number; // The idea is to track separately the objects owned by the page and the objects owned by debugger. + }; + private flags!: Uint8Array; + #statistics?: HeapSnapshotModel.Statistics; + constructor(profile: Profile, progress: HeapSnapshotProgress) { + super(profile, progress); + this.nodeFlags = { + // bit flags in 8-bit value + canBeQueried: 1, + detachedDOMTreeNode: 2, + pageObject: 4, // The idea is to track separately the objects owned by the page and the objects owned by debugger. + }; + } + + createNode(nodeIndex?: number): JSHeapSnapshotNode { + return new JSHeapSnapshotNode( + this, + nodeIndex === undefined ? -1 : nodeIndex, + ); + } + + createEdge(edgeIndex: number): JSHeapSnapshotEdge { + return new JSHeapSnapshotEdge(this, edgeIndex); + } + + createRetainingEdge(retainerIndex: number): JSHeapSnapshotRetainerEdge { + return new JSHeapSnapshotRetainerEdge(this, retainerIndex); + } + + override containmentEdgesFilter(): (arg0: HeapSnapshotEdge) => boolean { + return (edge: HeapSnapshotEdge): boolean => !edge.isInvisible(); + } + + override retainingEdgesFilter(): (arg0: HeapSnapshotEdge) => boolean { + const containmentEdgesFilter = this.containmentEdgesFilter(); + function filter(edge: HeapSnapshotEdge): boolean { + return ( + containmentEdgesFilter(edge) && !edge.node().isRoot() && !edge.isWeak() + ); + } + return filter; + } + + override calculateFlags(): void { + this.flags = new Uint8Array(this.nodeCount); + this.markDetachedDOMTreeNodes(); + this.markQueriableHeapObjects(); + this.markPageOwnedNodes(); + } + + #hasUserRoots(): boolean { + for (let iter = this.rootNode().edges(); iter.hasNext(); iter.next()) { + if (this.isUserRoot(iter.edge.node())) { + return true; + } + } + return false; + } + + // Updates the shallow sizes for "owned" objects of types kArray or kHidden to + // zero, and add their sizes to the "owner" object instead. + override calculateShallowSizes(): void { + // If there are no user roots, then that means the snapshot was produced with + // the "expose internals" option enabled. In that case, we should faithfully + // represent the actual memory allocations rather than attempting to make the + // output more understandable to web developers. + if (!this.#hasUserRoots()) { + return; + } + + const { nodeCount, nodes, nodeFieldCount, nodeSelfSizeOffset } = this; + + const kUnvisited = 0xffffffff; + const kHasMultipleOwners = 0xfffffffe; + if (nodeCount >= kHasMultipleOwners) { + throw new Error('Too many nodes for calculateShallowSizes'); + } + // For each node in order, `owners` will contain the index of the owning + // node or one of the two values kUnvisited or kHasMultipleOwners. The + // indexes in this array are NOT already multiplied by nodeFieldCount. + const owners = new Uint32Array(nodeCount); + // The worklist contains the indexes of nodes which should be visited during + // the second loop below. The order of visiting doesn't matter. The indexes + // in this array are NOT already multiplied by nodeFieldCount. + const worklist: number[] = []; + + const node = this.createNode(0); + for (let i = 0; i < nodeCount; ++i) { + if ( + node.isHidden() || + node.isArray() || + (node.isNative() && node.rawName() === 'system / ExternalStringData') + ) { + owners[i] = kUnvisited; + } else { + // The node owns itself. + owners[i] = i; + worklist.push(i); + } + node.nodeIndex = node.nextNodeIndex(); + } + + while (worklist.length !== 0) { + const id = worklist.pop() as number; + const owner = owners[id]; + node.nodeIndex = id * nodeFieldCount; + for (let iter = node.edges(); iter.hasNext(); iter.next()) { + const edge = iter.edge; + if (edge.isWeak()) { + continue; + } + const targetId = edge.nodeIndex() / nodeFieldCount; + switch (owners[targetId]) { + case kUnvisited: + owners[targetId] = owner; + worklist.push(targetId); + break; + case targetId: + case owner: + case kHasMultipleOwners: + // There is no change necessary if the target is already marked as: + // * owned by itself, + // * owned by the owner of the current source node, or + // * owned by multiple nodes. + break; + default: + owners[targetId] = kHasMultipleOwners; + // It is possible that this node is already in the worklist + // somewhere, but visiting it an extra time is not harmful. The + // iteration is guaranteed to complete because each node can only be + // added twice to the worklist: once when changing from kUnvisited + // to a specific owner, and a second time when changing from that + // owner to kHasMultipleOwners. + worklist.push(targetId); + break; + } + } + } + + for (let i = 0; i < nodeCount; ++i) { + const ownerId = owners[i]; + switch (ownerId) { + case kUnvisited: + case kHasMultipleOwners: + case i: + break; + default: { + const ownedNodeIndex = i * nodeFieldCount; + const ownerNodeIndex = ownerId * nodeFieldCount; + node.nodeIndex = ownerNodeIndex; + if (node.isSynthetic() || node.isRoot()) { + // Adding shallow size to synthetic or root nodes is not useful. + break; + } + const sizeToTransfer = nodes.getValue( + ownedNodeIndex + nodeSelfSizeOffset, + ); + nodes.setValue(ownedNodeIndex + nodeSelfSizeOffset, 0); + nodes.setValue( + ownerNodeIndex + nodeSelfSizeOffset, + nodes.getValue(ownerNodeIndex + nodeSelfSizeOffset) + + sizeToTransfer, + ); + break; + } + } + } + } + + override calculateDistances(isForRetainersView: boolean): void { + const pendingEphemeronEdges = new Set(); + const snapshot = this; + function filter(node: HeapSnapshotNode, edge: HeapSnapshotEdge): boolean { + if ( + node.isHidden() && + edge.name() === 'sloppy_function_map' && + node.rawName() === 'system / NativeContext' + ) { + return false; + } + if (node.isArray() && node.rawName() === '(map descriptors)') { + // DescriptorArrays are fixed arrays used to hold instance descriptors. + // The format of the these objects is: + // [0]: Number of descriptors + // [1]: Either Smi(0) if uninitialized, or a pointer to small fixed array: + // [0]: pointer to fixed array with enum cache + // [1]: either Smi(0) or pointer to fixed array with indices + // [i*3+2]: i-th key + // [i*3+3]: i-th type + // [i*3+4]: i-th descriptor + // As long as maps may share descriptor arrays some of the descriptor + // links may not be valid for all the maps. We just skip + // all the descriptor links when calculating distances. + // For more details see http://crbug.com/413608 + const index = parseInt(edge.name(), 10); + return index < 2 || index % 3 !== 1; + } + if (edge.isInternal()) { + // Snapshots represent WeakMap values as being referenced by two edges: + // one from the WeakMap, and a second from the corresponding key. To + // avoid the case described in crbug.com/1290800, we should set the + // distance of that value to the greater of (WeakMap+1, key+1). This + // part of the filter skips the first edge in the matched pair of edges, + // so that the distance gets set based on the second, which should be + // greater or equal due to traversal order. + const match = snapshot.tryParseWeakMapEdgeName(edge.nameIndex()); + if (match) { + if (!pendingEphemeronEdges.delete(match.duplicatedPart)) { + pendingEphemeronEdges.add(match.duplicatedPart); + return false; + } + } + } + return true; + } + super.calculateDistances(isForRetainersView, filter); + } + + override isUserRoot(node: HeapSnapshotNode): boolean { + return node.isUserRoot() || node.isDocumentDOMTreesRoot(); + } + + override userObjectsMapAndFlag(): { map: Uint8Array; flag: number } | null { + return { map: this.flags, flag: this.nodeFlags.pageObject }; + } + + flagsOfNode(node: HeapSnapshotNode): number { + return this.flags[node.nodeIndex / this.nodeFieldCount]; + } + + private markDetachedDOMTreeNodes(): void { + const nodes = this.nodes; + const nodesLength = nodes.length; + const nodeFieldCount = this.nodeFieldCount; + const nodeNativeType = this.nodeNativeType; + const nodeTypeOffset = this.nodeTypeOffset; + const flag = this.nodeFlags.detachedDOMTreeNode; + const node = this.rootNode(); + for ( + let nodeIndex = 0, ordinal = 0; + nodeIndex < nodesLength; + nodeIndex += nodeFieldCount, ordinal++ + ) { + const nodeType = nodes.getValue(nodeIndex + nodeTypeOffset); + if (nodeType !== nodeNativeType) { + continue; + } + node.nodeIndex = nodeIndex; + if (node.name().startsWith('Detached ')) { + this.flags[ordinal] |= flag; + } + } + } + + private markQueriableHeapObjects(): void { + // Allow runtime properties query for objects accessible from Window objects + // via regular properties, and for DOM wrappers. Trying to access random objects + // can cause a crash due to inconsistent state of internal properties of wrappers. + const flag = this.nodeFlags.canBeQueried; + const hiddenEdgeType = this.edgeHiddenType; + const internalEdgeType = this.edgeInternalType; + const invisibleEdgeType = this.edgeInvisibleType; + const weakEdgeType = this.edgeWeakType; + const edgeToNodeOffset = this.edgeToNodeOffset; + const edgeTypeOffset = this.edgeTypeOffset; + const edgeFieldsCount = this.edgeFieldsCount; + const containmentEdges = this.containmentEdges; + const nodeFieldCount = this.nodeFieldCount; + const firstEdgeIndexes = this.firstEdgeIndexes; + + const flags = this.flags; + const list: number[] = []; + + for (let iter = this.rootNode().edges(); iter.hasNext(); iter.next()) { + if (iter.edge.node().isUserRoot()) { + list.push(iter.edge.node().nodeIndex / nodeFieldCount); + } + } + + while (list.length) { + const nodeOrdinal = list.pop() as number; + if (flags[nodeOrdinal] & flag) { + continue; + } + flags[nodeOrdinal] |= flag; + const beginEdgeIndex = firstEdgeIndexes[nodeOrdinal]; + const endEdgeIndex = firstEdgeIndexes[nodeOrdinal + 1]; + for ( + let edgeIndex = beginEdgeIndex; + edgeIndex < endEdgeIndex; + edgeIndex += edgeFieldsCount + ) { + const childNodeIndex = containmentEdges.getValue( + edgeIndex + edgeToNodeOffset, + ); + const childNodeOrdinal = childNodeIndex / nodeFieldCount; + if (flags[childNodeOrdinal] & flag) { + continue; + } + const type = containmentEdges.getValue(edgeIndex + edgeTypeOffset); + if ( + type === hiddenEdgeType || + type === invisibleEdgeType || + type === internalEdgeType || + type === weakEdgeType + ) { + continue; + } + list.push(childNodeOrdinal); + } + } + } + + private markPageOwnedNodes(): void { + const edgeShortcutType = this.edgeShortcutType; + const edgeElementType = this.edgeElementType; + const edgeToNodeOffset = this.edgeToNodeOffset; + const edgeTypeOffset = this.edgeTypeOffset; + const edgeFieldsCount = this.edgeFieldsCount; + const edgeWeakType = this.edgeWeakType; + const firstEdgeIndexes = this.firstEdgeIndexes; + const containmentEdges = this.containmentEdges; + const nodeFieldCount = this.nodeFieldCount; + const nodesCount = this.nodeCount; + + const flags = this.flags; + const pageObjectFlag = this.nodeFlags.pageObject; + + const nodesToVisit = new Uint32Array(nodesCount); + let nodesToVisitLength = 0; + + const rootNodeOrdinal = this.rootNodeIndexInternal / nodeFieldCount; + const node = this.rootNode(); + + // Populate the entry points. They are Window objects and DOM Tree Roots. + for ( + let edgeIndex = firstEdgeIndexes[rootNodeOrdinal], + endEdgeIndex = firstEdgeIndexes[rootNodeOrdinal + 1]; + edgeIndex < endEdgeIndex; + edgeIndex += edgeFieldsCount + ) { + const edgeType = containmentEdges.getValue(edgeIndex + edgeTypeOffset); + const nodeIndex = containmentEdges.getValue(edgeIndex + edgeToNodeOffset); + if (edgeType === edgeElementType) { + node.nodeIndex = nodeIndex; + if (!node.isDocumentDOMTreesRoot()) { + continue; + } + } else if (edgeType !== edgeShortcutType) { + continue; + } + const nodeOrdinal = nodeIndex / nodeFieldCount; + nodesToVisit[nodesToVisitLength++] = nodeOrdinal; + flags[nodeOrdinal] |= pageObjectFlag; + } + + // Mark everything reachable with the pageObject flag. + while (nodesToVisitLength) { + const nodeOrdinal = nodesToVisit[--nodesToVisitLength]; + const beginEdgeIndex = firstEdgeIndexes[nodeOrdinal]; + const endEdgeIndex = firstEdgeIndexes[nodeOrdinal + 1]; + for ( + let edgeIndex = beginEdgeIndex; + edgeIndex < endEdgeIndex; + edgeIndex += edgeFieldsCount + ) { + const childNodeIndex = containmentEdges.getValue( + edgeIndex + edgeToNodeOffset, + ); + const childNodeOrdinal = childNodeIndex / nodeFieldCount; + if (flags[childNodeOrdinal] & pageObjectFlag) { + continue; + } + const type = containmentEdges.getValue(edgeIndex + edgeTypeOffset); + if (type === edgeWeakType) { + continue; + } + nodesToVisit[nodesToVisitLength++] = childNodeOrdinal; + flags[childNodeOrdinal] |= pageObjectFlag; + } + } + } + + override calculateStatistics(): void { + const nodeFieldCount = this.nodeFieldCount; + const nodes = this.nodes; + const nodesLength = nodes.length; + const nodeTypeOffset = this.nodeTypeOffset; + const nodeSizeOffset = this.nodeSelfSizeOffset; + const nodeNativeType = this.nodeNativeType; + const nodeCodeType = this.nodeCodeType; + const nodeConsStringType = this.nodeConsStringType; + const nodeSlicedStringType = this.nodeSlicedStringType; + const nodeHiddenType = this.nodeHiddenType; + const nodeStringType = this.nodeStringType; + let sizeNative = this.profile.snapshot.extra_native_bytes ?? 0; + let sizeTypedArrays = 0; + let sizeCode = 0; + let sizeStrings = 0; + let sizeJSArrays = 0; + let sizeSystem = 0; + const node = this.rootNode(); + for ( + let nodeIndex = 0; + nodeIndex < nodesLength; + nodeIndex += nodeFieldCount + ) { + const nodeSize = nodes.getValue(nodeIndex + nodeSizeOffset); + const nodeType = nodes.getValue(nodeIndex + nodeTypeOffset); + if (nodeType === nodeHiddenType) { + sizeSystem += nodeSize; + continue; + } + node.nodeIndex = nodeIndex; + if (nodeType === nodeNativeType) { + sizeNative += nodeSize; + if (node.rawName() === 'system / JSArrayBufferData') { + sizeTypedArrays += nodeSize; + } + } else if (nodeType === nodeCodeType) { + sizeCode += nodeSize; + } else if ( + nodeType === nodeConsStringType || + nodeType === nodeSlicedStringType || + nodeType === nodeStringType + ) { + sizeStrings += nodeSize; + } else if (node.rawName() === 'Array') { + sizeJSArrays += this.calculateArraySize(node); + } + } + this.#statistics = { + total: this.totalSize, + native: { + total: sizeNative, + typedArrays: sizeTypedArrays, + }, + v8heap: { + total: this.totalSize - sizeNative, + code: sizeCode, + jsArrays: sizeJSArrays, + strings: sizeStrings, + system: sizeSystem, + }, + }; + } + + private calculateArraySize(node: HeapSnapshotNode): number { + let size = node.selfSize(); + const beginEdgeIndex = node.edgeIndexesStart(); + const endEdgeIndex = node.edgeIndexesEnd(); + const containmentEdges = this.containmentEdges; + const strings = this.strings; + const edgeToNodeOffset = this.edgeToNodeOffset; + const edgeTypeOffset = this.edgeTypeOffset; + const edgeNameOffset = this.edgeNameOffset; + const edgeFieldsCount = this.edgeFieldsCount; + const edgeInternalType = this.edgeInternalType; + for ( + let edgeIndex = beginEdgeIndex; + edgeIndex < endEdgeIndex; + edgeIndex += edgeFieldsCount + ) { + const edgeType = containmentEdges.getValue(edgeIndex + edgeTypeOffset); + if (edgeType !== edgeInternalType) { + continue; + } + const edgeName = + strings[containmentEdges.getValue(edgeIndex + edgeNameOffset)]; + if (edgeName !== 'elements') { + continue; + } + const elementsNodeIndex = containmentEdges.getValue( + edgeIndex + edgeToNodeOffset, + ); + node.nodeIndex = elementsNodeIndex; + if (node.retainersCount() === 1) { + size += node.selfSize(); + } + break; + } + return size; + } + + getStatistics(): HeapSnapshotModel.Statistics { + return this.#statistics as HeapSnapshotModel.Statistics; + } +} + +export class JSHeapSnapshotNode extends HeapSnapshotNode { + constructor(snapshot: JSHeapSnapshot, nodeIndex?: number) { + super(snapshot, nodeIndex); + } + + canBeQueried(): boolean { + const snapshot = this.snapshot as JSHeapSnapshot; + const flags = snapshot.flagsOfNode(this); + return Boolean(flags & snapshot.nodeFlags.canBeQueried); + } + + override name(): string { + const snapshot = this.snapshot; + if (this.rawType() === snapshot.nodeConsStringType) { + return this.consStringName(); + } + if ( + this.rawType() === snapshot.nodeObjectType && + this.rawName() === 'Object' + ) { + return this.#plainObjectName(); + } + return this.rawName(); + } + + private consStringName(): string { + const snapshot = this.snapshot; + const consStringType = snapshot.nodeConsStringType; + const edgeInternalType = snapshot.edgeInternalType; + const edgeFieldsCount = snapshot.edgeFieldsCount; + const edgeToNodeOffset = snapshot.edgeToNodeOffset; + const edgeTypeOffset = snapshot.edgeTypeOffset; + const edgeNameOffset = snapshot.edgeNameOffset; + const strings = snapshot.strings; + const edges = snapshot.containmentEdges; + const firstEdgeIndexes = snapshot.firstEdgeIndexes; + const nodeFieldCount = snapshot.nodeFieldCount; + const nodeTypeOffset = snapshot.nodeTypeOffset; + const nodeNameOffset = snapshot.nodeNameOffset; + const nodes = snapshot.nodes; + const nodesStack = []; + nodesStack.push(this.nodeIndex); + let name = ''; + + while (nodesStack.length && name.length < 1024) { + const nodeIndex = nodesStack.pop() as number; + if (nodes.getValue(nodeIndex + nodeTypeOffset) !== consStringType) { + name += strings[nodes.getValue(nodeIndex + nodeNameOffset)]; + continue; + } + const nodeOrdinal = nodeIndex / nodeFieldCount; + const beginEdgeIndex = firstEdgeIndexes[nodeOrdinal]; + const endEdgeIndex = firstEdgeIndexes[nodeOrdinal + 1]; + let firstNodeIndex = 0; + let secondNodeIndex = 0; + for ( + let edgeIndex = beginEdgeIndex; + edgeIndex < endEdgeIndex && (!firstNodeIndex || !secondNodeIndex); + edgeIndex += edgeFieldsCount + ) { + const edgeType = edges.getValue(edgeIndex + edgeTypeOffset); + if (edgeType === edgeInternalType) { + const edgeName = strings[edges.getValue(edgeIndex + edgeNameOffset)]; + if (edgeName === 'first') { + firstNodeIndex = edges.getValue(edgeIndex + edgeToNodeOffset); + } else if (edgeName === 'second') { + secondNodeIndex = edges.getValue(edgeIndex + edgeToNodeOffset); + } + } + } + nodesStack.push(secondNodeIndex); + nodesStack.push(firstNodeIndex); + } + return name; + } + + // Creates a name for plain JS objects, which looks something like + // '{propName, otherProp, thirdProp, ..., secondToLastProp, lastProp}'. + // A variable number of property names is included, depending on the length + // of the property names, so that the result fits nicely in a reasonably + // sized DevTools window. + #plainObjectName(): string { + const snapshot = this.snapshot; + const { edgeFieldsCount, edgePropertyType } = snapshot; + const edge = snapshot.createEdge(0); + let categoryNameStart = '{'; + let categoryNameEnd = '}'; + let edgeIndexFromStart = this.edgeIndexesStart(); + let edgeIndexFromEnd = this.edgeIndexesEnd() - edgeFieldsCount; + let nextFromEnd = false; + while (edgeIndexFromStart <= edgeIndexFromEnd) { + edge.edgeIndex = nextFromEnd ? edgeIndexFromEnd : edgeIndexFromStart; + + // Skip non-property edges and the special __proto__ property. + if (edge.rawType() !== edgePropertyType || edge.name() === '__proto__') { + if (nextFromEnd) { + edgeIndexFromEnd -= edgeFieldsCount; + } else { + edgeIndexFromStart += edgeFieldsCount; + } + continue; + } + + const formatted = JSHeapSnapshotNode.formatPropertyName(edge.name()); + + // Always include at least one property, regardless of its length. Beyond that point, + // only include more properties if the name isn't too long. + if ( + categoryNameStart.length > 1 && + categoryNameStart.length + categoryNameEnd.length + formatted.length > + 100 + ) { + break; + } + + if (nextFromEnd) { + edgeIndexFromEnd -= edgeFieldsCount; + if (categoryNameEnd.length > 1) { + categoryNameEnd = ', ' + categoryNameEnd; + } + categoryNameEnd = formatted + categoryNameEnd; + } else { + edgeIndexFromStart += edgeFieldsCount; + if (categoryNameStart.length > 1) { + categoryNameStart += ', '; + } + categoryNameStart += formatted; + } + nextFromEnd = !nextFromEnd; + } + if (edgeIndexFromStart <= edgeIndexFromEnd) { + categoryNameStart += ', ...'; + } + if (categoryNameEnd.length > 1) { + categoryNameStart += ', '; + } + return categoryNameStart + categoryNameEnd; + } + + static formatPropertyName(name: string): string { + // We don't need a strict test for whether a property name follows the + // rules for being a JS identifier, but property names containing commas, + // quotation marks, or braces could cause confusion, so we'll escape those. + if (/[,'"{}]/.test(name)) { + name = JSON.stringify({ [name]: 0 }); + name = name.substring(1, name.length - 3); + } + return name; + } + + override id(): number { + const snapshot = this.snapshot; + return snapshot.nodes.getValue(this.nodeIndex + snapshot.nodeIdOffset); + } + + override isHidden(): boolean { + return this.rawType() === this.snapshot.nodeHiddenType; + } + + override isArray(): boolean { + return this.rawType() === this.snapshot.nodeArrayType; + } + + override isSynthetic(): boolean { + return this.rawType() === this.snapshot.nodeSyntheticType; + } + + isNative(): boolean { + return this.rawType() === this.snapshot.nodeNativeType; + } + + override isUserRoot(): boolean { + return !this.isSynthetic(); + } + + override isDocumentDOMTreesRoot(): boolean { + return this.isSynthetic() && this.rawName() === '(Document DOM trees)'; + } + + override serialize(): HeapSnapshotModel.Node { + const result = super.serialize(); + const snapshot = this.snapshot as JSHeapSnapshot; + const flags = snapshot.flagsOfNode(this); + if (flags & snapshot.nodeFlags.canBeQueried) { + result.canBeQueried = true; + } + if (flags & snapshot.nodeFlags.detachedDOMTreeNode) { + result.detachedDOMTreeNode = true; + } + return result; + } +} + +export class JSHeapSnapshotEdge extends HeapSnapshotEdge { + constructor(snapshot: JSHeapSnapshot, edgeIndex?: number) { + super(snapshot, edgeIndex); + } + + override clone(): JSHeapSnapshotEdge { + const snapshot = this.snapshot as JSHeapSnapshot; + return new JSHeapSnapshotEdge(snapshot, this.edgeIndex); + } + + override hasStringName(): boolean { + if (!this.isShortcut()) { + return this.hasStringNameInternal(); + } + // @ts-expect-error parseInt is successful against numbers. + return isNaN(parseInt(this.nameInternal(), 10)); + } + + isElement(): boolean { + return this.rawType() === this.snapshot.edgeElementType; + } + + isHidden(): boolean { + return this.rawType() === this.snapshot.edgeHiddenType; + } + + override isWeak(): boolean { + return this.rawType() === this.snapshot.edgeWeakType; + } + + override isInternal(): boolean { + return this.rawType() === this.snapshot.edgeInternalType; + } + + override isInvisible(): boolean { + return this.rawType() === this.snapshot.edgeInvisibleType; + } + + isShortcut(): boolean { + return this.rawType() === this.snapshot.edgeShortcutType; + } + + override name(): string { + const name = this.nameInternal(); + if (!this.isShortcut()) { + return String(name); + } + // @ts-expect-error parseInt is successful against numbers. + const numName = parseInt(name, 10); + return String(isNaN(numName) ? name : numName); + } + + override toString(): string { + const name = this.name(); + switch (this.type()) { + case 'context': + return '->' + name; + case 'element': + return '[' + name + ']'; + case 'weak': + return '[[' + name + ']]'; + case 'property': + return name.indexOf(' ') === -1 ? '.' + name : '["' + name + '"]'; + case 'shortcut': + if (typeof name === 'string') { + return name.indexOf(' ') === -1 ? '.' + name : '["' + name + '"]'; + } + return '[' + name + ']'; + case 'internal': + case 'hidden': + case 'invisible': + return '{' + name + '}'; + } + return '?' + name + '?'; + } + + private hasStringNameInternal(): boolean { + const type = this.rawType(); + const snapshot = this.snapshot; + return ( + type !== snapshot.edgeElementType && type !== snapshot.edgeHiddenType + ); + } + + private nameInternal(): string | number { + return this.hasStringNameInternal() + ? this.snapshot.strings[this.nameOrIndex()] + : this.nameOrIndex(); + } + + private nameOrIndex(): number { + return this.edges.getValue(this.edgeIndex + this.snapshot.edgeNameOffset); + } + + override rawType(): number { + return this.edges.getValue(this.edgeIndex + this.snapshot.edgeTypeOffset); + } + + override nameIndex(): number { + if (!this.hasStringNameInternal()) { + throw new Error('Edge does not have string name'); + } + return this.nameOrIndex(); + } +} + +export class JSHeapSnapshotRetainerEdge extends HeapSnapshotRetainerEdge { + constructor(snapshot: JSHeapSnapshot, retainerIndex: number) { + super(snapshot, retainerIndex); + } + + override clone(): JSHeapSnapshotRetainerEdge { + const snapshot = this.snapshot as JSHeapSnapshot; + return new JSHeapSnapshotRetainerEdge(snapshot, this.retainerIndex()); + } + + isHidden(): boolean { + return this.edge().isHidden(); + } + + isInvisible(): boolean { + return this.edge().isInvisible(); + } + + isShortcut(): boolean { + return this.edge().isShortcut(); + } + + isWeak(): boolean { + return this.edge().isWeak(); + } +} +export interface AggregatedInfo { + count: number; + distance: number; + self: number; + maxRet: number; + name: string; + idxs: number[]; +} diff --git a/internal/heapsnapshot/src/HeapSnapshotLoader.ts b/internal/heapsnapshot/src/HeapSnapshotLoader.ts new file mode 100644 index 000000000..e3517af49 --- /dev/null +++ b/internal/heapsnapshot/src/HeapSnapshotLoader.ts @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2012 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import { MessagePort } from 'node:worker_threads'; +import { + HeapSnapshotProgress, + JSHeapSnapshot, + type HeapSnapshotHeader, + type Profile, +} from './HeapSnapshot.js'; +import { HeapSnapshotWorkerDispatcher } from './HeapSnapshotWorkerDispatcher.js'; +import * as Platform from './platform/index.js'; +import * as TextUtils from './TextUtils.js'; + +export class HeapSnapshotLoader { + readonly #progress: HeapSnapshotProgress; + #buffer: string[]; + #dataCallback: ((value: string) => void) | null; + #done: boolean; + // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + #snapshot?: { [x: string]: any }; + #array!: Platform.TypedArrayUtilities.BigUint32Array | null; + #arrayIndex!: number; + #json = ''; + parsingComplete: Promise; + constructor( + dispatcherOrProgress: HeapSnapshotProgress | HeapSnapshotWorkerDispatcher, + ) { + this.#reset(); + if (dispatcherOrProgress instanceof HeapSnapshotWorkerDispatcher) { + this.#progress = new HeapSnapshotProgress(dispatcherOrProgress); + } else { + this.#progress = dispatcherOrProgress; + } + this.#buffer = []; + this.#dataCallback = null; + this.#done = false; + this.parsingComplete = this.#parseInput(); + } + + dispose(): void { + this.#reset(); + } + + #reset(): void { + this.#json = ''; + this.#snapshot = undefined; + } + + close(): void { + this.#done = true; + if (this.#dataCallback) { + this.#dataCallback(''); + } + } + + async buildSnapshot(secondWorker: MessagePort): Promise { + this.#snapshot = this.#snapshot || {}; + + this.#progress.updateStatus('Processing snapshot…'); + const result = new JSHeapSnapshot( + this.#snapshot as Profile, + this.#progress, + ); + await result.initialize(secondWorker); + this.#reset(); + return result; + } + + #parseUintArray(): boolean { + let index = 0; + const char0 = '0'.charCodeAt(0); + const char9 = '9'.charCodeAt(0); + const closingBracket = ']'.charCodeAt(0); + const length = this.#json.length; + while (true) { + while (index < length) { + const code = this.#json.charCodeAt(index); + if (char0 <= code && code <= char9) { + break; + } else if (code === closingBracket) { + this.#json = this.#json.slice(index + 1); + return false; + } + ++index; + } + if (index === length) { + this.#json = ''; + return true; + } + let nextNumber = 0; + const startIndex = index; + while (index < length) { + const code = this.#json.charCodeAt(index); + if (char0 > code || code > char9) { + break; + } + nextNumber *= 10; + nextNumber += code - char0; + ++index; + } + if (index === length) { + this.#json = this.#json.slice(startIndex); + return true; + } + if (!this.#array) { + throw new Error('Array not instantiated'); + } + this.#array.setValue(this.#arrayIndex++, nextNumber); + } + } + + #parseStringsArray(): void { + this.#progress.updateStatus('Parsing strings…'); + const closingBracketIndex = this.#json.lastIndexOf(']'); + if (closingBracketIndex === -1) { + throw new Error('Incomplete JSON'); + } + this.#json = this.#json.slice(0, closingBracketIndex + 1); + + if (!this.#snapshot) { + throw new Error('No snapshot in parseStringsArray'); + } + this.#snapshot.strings = JSON.parse(this.#json); + } + + write(chunk: string): void { + this.#buffer.push(chunk); + if (!this.#dataCallback) { + return; + } + this.#dataCallback(this.#buffer.shift() as string); + this.#dataCallback = null; + } + + #fetchChunk(): Promise { + // This method shoudln't be entered more than once since parsing happens + // sequentially. This means it's fine to stash away a single #dataCallback + // instead of an array of them. + if (this.#buffer.length > 0) { + return Promise.resolve(this.#buffer.shift() as string); + } + + const { promise, resolve } = + Platform.PromiseUtilities.withResolvers(); + this.#dataCallback = resolve; + return promise; + } + + async #findToken(token: string, startIndex?: number): Promise { + while (true) { + const pos = this.#json.indexOf(token, startIndex || 0); + if (pos !== -1) { + return pos; + } + startIndex = this.#json.length - token.length + 1; + this.#json += await this.#fetchChunk(); + } + } + + async #parseArray( + name: string, + title: string, + length?: number, + ): Promise { + const nameIndex = await this.#findToken(name); + const bracketIndex = await this.#findToken('[', nameIndex); + this.#json = this.#json.slice(bracketIndex + 1); + this.#array = + length === undefined + ? Platform.TypedArrayUtilities.createExpandableBigUint32Array() + : Platform.TypedArrayUtilities.createFixedBigUint32Array(length); + this.#arrayIndex = 0; + while (this.#parseUintArray()) { + if (length) { + this.#progress.updateProgress( + title, + this.#arrayIndex, + this.#array.length, + ); + } else { + this.#progress.updateStatus(title); + } + this.#json += await this.#fetchChunk(); + } + const result = this.#array; + this.#array = null; + return result; + } + + async #parseInput(): Promise { + const snapshotToken = '"snapshot"'; + const snapshotTokenIndex = await this.#findToken(snapshotToken); + if (snapshotTokenIndex === -1) { + throw new Error('Snapshot token not found'); + } + + this.#progress.updateStatus('Loading snapshot info…'); + const json = this.#json.slice( + snapshotTokenIndex + snapshotToken.length + 1, + ); + let jsonTokenizerDone = false; + const jsonTokenizer = new TextUtils.BalancedJSONTokenizer((metaJSON) => { + this.#json = jsonTokenizer.remainder(); + jsonTokenizerDone = true; + + this.#snapshot = this.#snapshot || {}; + this.#snapshot.snapshot = JSON.parse(metaJSON) as HeapSnapshotHeader; + }); + jsonTokenizer.write(json); + while (!jsonTokenizerDone) { + jsonTokenizer.write(await this.#fetchChunk()); + } + + this.#snapshot = this.#snapshot || {}; + const nodes = await this.#parseArray( + '"nodes"', + 'Loading nodes… {PH1}%', + this.#snapshot.snapshot.meta.node_fields.length * + this.#snapshot.snapshot.node_count, + ); + this.#snapshot.nodes = nodes; + + const edges = await this.#parseArray( + '"edges"', + 'Loading edges… {PH1}%', + this.#snapshot.snapshot.meta.edge_fields.length * + this.#snapshot.snapshot.edge_count, + ); + this.#snapshot.edges = edges; + + if (this.#snapshot.snapshot.trace_function_count) { + const traceFunctionInfos = await this.#parseArray( + '"trace_function_infos"', + 'Loading allocation traces… {PH1}%', + this.#snapshot.snapshot.meta.trace_function_info_fields.length * + this.#snapshot.snapshot.trace_function_count, + ); + this.#snapshot.trace_function_infos = + traceFunctionInfos.asUint32ArrayOrFail(); + + const thisTokenEndIndex = await this.#findToken(':'); + const nextTokenIndex = await this.#findToken('"', thisTokenEndIndex); + const openBracketIndex = this.#json.indexOf('['); + const closeBracketIndex = this.#json.lastIndexOf(']', nextTokenIndex); + this.#snapshot.trace_tree = JSON.parse( + this.#json.substring(openBracketIndex, closeBracketIndex + 1), + ); + this.#json = this.#json.slice(closeBracketIndex + 1); + } + + if (this.#snapshot.snapshot.meta.sample_fields) { + const samples = await this.#parseArray('"samples"', 'Loading samples…'); + this.#snapshot.samples = samples.asArrayOrFail(); + } + + if (this.#snapshot.snapshot.meta['location_fields']) { + const locations = await this.#parseArray( + '"locations"', + 'Loading locations…', + ); + this.#snapshot.locations = locations.asArrayOrFail(); + } else { + this.#snapshot.locations = []; + } + + this.#progress.updateStatus('Loading strings…'); + const stringsTokenIndex = await this.#findToken('"strings"'); + const bracketIndex = await this.#findToken('[', stringsTokenIndex); + this.#json = this.#json.slice(bracketIndex); + while (this.#buffer.length > 0 || !this.#done) { + this.#json += await this.#fetchChunk(); + } + this.#parseStringsArray(); + } +} diff --git a/internal/heapsnapshot/src/HeapSnapshotModel.ts b/internal/heapsnapshot/src/HeapSnapshotModel.ts new file mode 100644 index 000000000..124e7b591 --- /dev/null +++ b/internal/heapsnapshot/src/HeapSnapshotModel.ts @@ -0,0 +1,367 @@ +/* + * Copyright (C) 2014 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +export const HeapSnapshotProgressEvent = { + Update: 'ProgressUpdate', + BrokenSnapshot: 'BrokenSnapshot', +}; + +export const baseSystemDistance = 100000000; +export const baseUnreachableDistance = baseSystemDistance * 2; + +export class AllocationNodeCallers { + nodesWithSingleCaller: SerializedAllocationNode[]; + branchingCallers: SerializedAllocationNode[]; + constructor( + nodesWithSingleCaller: SerializedAllocationNode[], + branchingCallers: SerializedAllocationNode[], + ) { + this.nodesWithSingleCaller = nodesWithSingleCaller; + this.branchingCallers = branchingCallers; + } +} + +export class SerializedAllocationNode { + id: number; + name: string; + scriptName: string; + scriptId: number; + line: number; + column: number; + count: number; + size: number; + liveCount: number; + liveSize: number; + hasChildren: boolean; + constructor( + nodeId: number, + functionName: string, + scriptName: string, + scriptId: number, + line: number, + column: number, + count: number, + size: number, + liveCount: number, + liveSize: number, + hasChildren: boolean, + ) { + this.id = nodeId; + this.name = functionName; + this.scriptName = scriptName; + this.scriptId = scriptId; + this.line = line; + this.column = column; + this.count = count; + this.size = size; + this.liveCount = liveCount; + this.liveSize = liveSize; + this.hasChildren = hasChildren; + } +} + +export class AllocationStackFrame { + functionName: string; + scriptName: string; + scriptId: number; + line: number; + column: number; + constructor( + functionName: string, + scriptName: string, + scriptId: number, + line: number, + column: number, + ) { + this.functionName = functionName; + this.scriptName = scriptName; + this.scriptId = scriptId; + this.line = line; + this.column = column; + } +} + +export class Node { + id: number; + name: string; + distance: number; + nodeIndex: number; + retainedSize: number; + selfSize: number; + type: string; + canBeQueried: boolean; + detachedDOMTreeNode: boolean; + isAddedNotRemoved: boolean | null; + ignored: boolean; + constructor( + id: number, + name: string, + distance: number, + nodeIndex: number, + retainedSize: number, + selfSize: number, + type: string, + ) { + this.id = id; + this.name = name; + this.distance = distance; + this.nodeIndex = nodeIndex; + this.retainedSize = retainedSize; + this.selfSize = selfSize; + this.type = type; + + this.canBeQueried = false; + this.detachedDOMTreeNode = false; + this.isAddedNotRemoved = null; + this.ignored = false; + } +} + +export class Edge { + name: string; + node: Node; + type: string; + edgeIndex: number; + isAddedNotRemoved: boolean | null; + constructor(name: string, node: Node, type: string, edgeIndex: number) { + this.name = name; + this.node = node; + this.type = type; + this.edgeIndex = edgeIndex; + this.isAddedNotRemoved = null; + } +} + +export class Aggregate { + count!: number; + distance!: number; + self!: number; + maxRet!: number; + name!: string; + idxs!: number[]; + constructor() {} +} + +export class AggregateForDiff { + name: string; + indexes: number[]; + ids: number[]; + selfSizes: number[]; + constructor() { + this.name = ''; + this.indexes = []; + this.ids = []; + this.selfSizes = []; + } +} + +export class Diff { + name: string; + addedCount: number; + removedCount: number; + addedSize: number; + removedSize: number; + deletedIndexes: number[]; + addedIndexes: number[]; + countDelta!: number; + sizeDelta!: number; + constructor(name: string) { + this.name = name; + this.addedCount = 0; + this.removedCount = 0; + this.addedSize = 0; + this.removedSize = 0; + this.deletedIndexes = []; + this.addedIndexes = []; + } +} + +export class DiffForClass { + name!: string; + addedCount!: number; + removedCount!: number; + addedSize!: number; + removedSize!: number; + deletedIndexes!: number[]; + addedIndexes!: number[]; + countDelta!: number; + sizeDelta!: number; + constructor() {} +} + +export class ComparatorConfig { + fieldName1: string; + ascending1: boolean; + fieldName2: string; + ascending2: boolean; + constructor( + fieldName1: string, + ascending1: boolean, + fieldName2: string, + ascending2: boolean, + ) { + this.fieldName1 = fieldName1; + this.ascending1 = ascending1; + this.fieldName2 = fieldName2; + this.ascending2 = ascending2; + } +} + +export class WorkerCommand { + callId!: number; + disposition!: string; + objectId!: number; + newObjectId!: number; + methodName!: string; + // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration + // eslint-disable-next-line @typescript-eslint/no-explicit-any + methodArguments!: any[]; + source!: string; + constructor() {} +} + +export class ItemsRange { + startPosition: number; + endPosition: number; + totalLength: number; + items: Array; + constructor( + startPosition: number, + endPosition: number, + totalLength: number, + items: Array, + ) { + this.startPosition = startPosition; + this.endPosition = endPosition; + this.totalLength = totalLength; + this.items = items; + } +} + +export class StaticData { + nodeCount: number; + rootNodeIndex: number; + totalSize: number; + maxJSObjectId: number; + constructor( + nodeCount: number, + rootNodeIndex: number, + totalSize: number, + maxJSObjectId: number, + ) { + this.nodeCount = nodeCount; + this.rootNodeIndex = rootNodeIndex; + this.totalSize = totalSize; + this.maxJSObjectId = maxJSObjectId; + } +} + +export interface Statistics { + total: number; + native: { total: number; typedArrays: number }; + v8heap: { + total: number; + code: number; + jsArrays: number; + strings: number; + system: number; + }; +} + +export class NodeFilter { + minNodeId: number | undefined; + maxNodeId: number | undefined; + allocationNodeId!: number | undefined; + filterName: string | undefined; + constructor(minNodeId?: number, maxNodeId?: number) { + this.minNodeId = minNodeId; + this.maxNodeId = maxNodeId; + } + + equals(o: NodeFilter): boolean { + return ( + this.minNodeId === o.minNodeId && + this.maxNodeId === o.maxNodeId && + this.allocationNodeId === o.allocationNodeId && + this.filterName === o.filterName + ); + } +} + +export class SearchConfig { + query: string; + caseSensitive: boolean; + isRegex: boolean; + shouldJump: boolean; + jumpBackward: boolean; + constructor( + query: string, + caseSensitive: boolean, + isRegex: boolean, + shouldJump: boolean, + jumpBackward: boolean, + ) { + this.query = query; + this.caseSensitive = caseSensitive; + this.isRegex = isRegex; + this.shouldJump = shouldJump; + this.jumpBackward = jumpBackward; + } + + toSearchRegex(_global?: boolean): { regex: RegExp; fromQuery: boolean } { + throw new Error('Unsupported operation on search config'); + } +} + +export class Samples { + timestamps: number[]; + lastAssignedIds: number[]; + sizes: number[]; + constructor( + timestamps: number[], + lastAssignedIds: number[], + sizes: number[], + ) { + this.timestamps = timestamps; + this.lastAssignedIds = lastAssignedIds; + this.sizes = sizes; + } +} + +export class Location { + scriptId: number; + lineNumber: number; + columnNumber: number; + constructor(scriptId: number, lineNumber: number, columnNumber: number) { + this.scriptId = scriptId; + this.lineNumber = lineNumber; + this.columnNumber = columnNumber; + } +} diff --git a/internal/heapsnapshot/src/HeapSnapshotWorkerDispatcher.ts b/internal/heapsnapshot/src/HeapSnapshotWorkerDispatcher.ts new file mode 100644 index 000000000..c502c03fa --- /dev/null +++ b/internal/heapsnapshot/src/HeapSnapshotWorkerDispatcher.ts @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2011 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +// We mirror what heap_snapshot_worker.ts does, but we can't use it here as we'd have a +// cyclic GN dependency otherwise. + +import { MessagePort } from 'node:worker_threads'; +import * as AllocationProfile from './AllocationProfile.js'; +import * as HeapSnapshot from './HeapSnapshot.js'; +import * as HeapSnapshotLoader from './HeapSnapshotLoader.js'; +import * as HeapSnapshotModel from './HeapSnapshotModel.js'; + +interface DispatcherResponse { + callId?: number; + result: unknown; + error?: string; + errorCallStack?: Object; + errorMethodName?: string; +} + +export class HeapSnapshotWorkerDispatcher { + // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + #objects: any[]; + readonly #postMessage: MessagePort['postMessage']; + constructor(postMessage: MessagePort['postMessage']) { + this.#objects = []; + this.#postMessage = postMessage; + } + + sendEvent(name: string, data: unknown): void { + this.#postMessage({ eventName: name, data }); + } + + async dispatchMessage({ + data, + ports, + }: { + data: HeapSnapshotModel.WorkerCommand; + ports: readonly MessagePort[]; + }): Promise { + const response: DispatcherResponse = { + callId: data.callId, + result: null, + error: undefined, + errorCallStack: undefined, + errorMethodName: undefined, + }; + try { + switch (data.disposition) { + case 'createLoader': + this.#objects[data.objectId] = + new HeapSnapshotLoader.HeapSnapshotLoader(this); + break; + case 'dispose': { + delete this.#objects[data.objectId]; + break; + } + case 'getter': { + const object = this.#objects[data.objectId]; + const result = object[data.methodName]; + response.result = result; + break; + } + case 'factory': { + const object = this.#objects[data.objectId]; + const args = data.methodArguments.slice(); + args.push(...ports); + const result = await object[data.methodName].apply(object, args); + if (result) { + this.#objects[data.newObjectId] = result; + } + response.result = Boolean(result); + break; + } + case 'method': { + const object = this.#objects[data.objectId]; + response.result = object[data.methodName].apply( + object, + data.methodArguments, + ); + break; + } + case 'evaluateForTest': { + try { + // Make 'HeapSnapshotWorker' and 'HeapSnapshotModel' available to web tests. 'eval' can't use 'import'. + // @ts-expect-error + globalThis.HeapSnapshotWorker = { + AllocationProfile, + HeapSnapshot, + HeapSnapshotLoader, + }; + // @ts-expect-error + globalThis.HeapSnapshotModel = HeapSnapshotModel; + response.result = await eval(data.source); + } catch (error: any) { + response.result = error.toString(); + } + break; + } + case 'setupForSecondaryInit': { + this.#objects[data.objectId] = new HeapSnapshot.SecondaryInitManager( + ports[0], + ); + } + } + } catch (error: any) { + response.error = error.toString(); + response.errorCallStack = error.stack; + if (data.methodName) { + response.errorMethodName = data.methodName; + } + } + this.#postMessage(response); + } +} diff --git a/internal/heapsnapshot/src/TextUtils.ts b/internal/heapsnapshot/src/TextUtils.ts new file mode 100644 index 000000000..13621a6da --- /dev/null +++ b/internal/heapsnapshot/src/TextUtils.ts @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2013 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +export class BalancedJSONTokenizer { + private readonly callback: (arg0: string) => void; + private index: number; + private balance: number; + private buffer: string; + private findMultiple: boolean; + private closingDoubleQuoteRegex: RegExp; + private lastBalancedIndex?: number; + constructor(callback: (arg0: string) => void, findMultiple?: boolean) { + this.callback = callback; + this.index = 0; + this.balance = 0; + this.buffer = ''; + this.findMultiple = findMultiple || false; + this.closingDoubleQuoteRegex = /[^\\](?:\\\\)*"/g; + } + + write(chunk: string): boolean { + this.buffer += chunk; + const lastIndex = this.buffer.length; + const buffer = this.buffer; + let index; + for (index = this.index; index < lastIndex; ++index) { + const character = buffer[index]; + if (character === '"') { + this.closingDoubleQuoteRegex.lastIndex = index; + if (!this.closingDoubleQuoteRegex.test(buffer)) { + break; + } + index = this.closingDoubleQuoteRegex.lastIndex - 1; + } else if (character === '{') { + ++this.balance; + } else if (character === '}') { + --this.balance; + if (this.balance < 0) { + this.reportBalanced(); + return false; + } + if (!this.balance) { + this.lastBalancedIndex = index + 1; + if (!this.findMultiple) { + break; + } + } + } else if (character === ']' && !this.balance) { + this.reportBalanced(); + return false; + } + } + this.index = index; + this.reportBalanced(); + return true; + } + + private reportBalanced(): void { + if (!this.lastBalancedIndex) { + return; + } + this.callback(this.buffer.slice(0, this.lastBalancedIndex)); + this.buffer = this.buffer.slice(this.lastBalancedIndex); + this.index -= this.lastBalancedIndex; + this.lastBalancedIndex = 0; + } + + remainder(): string { + return this.buffer; + } +} diff --git a/internal/heapsnapshot/src/heap_snapshot_worker-entrypoint.ts b/internal/heapsnapshot/src/heap_snapshot_worker-entrypoint.ts new file mode 100644 index 000000000..72cb19578 --- /dev/null +++ b/internal/heapsnapshot/src/heap_snapshot_worker-entrypoint.ts @@ -0,0 +1,13 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import { parentPort } from 'node:worker_threads'; +import * as HeapSnapshotWorker from './heap_snapshot_worker.js'; + +const dispatcher = + new HeapSnapshotWorker.HeapSnapshotWorkerDispatcher.HeapSnapshotWorkerDispatcher( + parentPort!.postMessage.bind(parentPort!), + ); +parentPort!.on('message', dispatcher.dispatchMessage.bind(dispatcher)); +parentPort!.postMessage('workerReady'); diff --git a/internal/heapsnapshot/src/heap_snapshot_worker.ts b/internal/heapsnapshot/src/heap_snapshot_worker.ts new file mode 100644 index 000000000..d15c524fd --- /dev/null +++ b/internal/heapsnapshot/src/heap_snapshot_worker.ts @@ -0,0 +1,15 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import * as AllocationProfile from './AllocationProfile.js'; +import * as HeapSnapshot from './HeapSnapshot.js'; +import * as HeapSnapshotLoader from './HeapSnapshotLoader.js'; +import * as HeapSnapshotWorkerDispatcher from './HeapSnapshotWorkerDispatcher.js'; + +export { + AllocationProfile, + HeapSnapshot, + HeapSnapshotLoader, + HeapSnapshotWorkerDispatcher, +}; diff --git a/internal/heapsnapshot/src/index.ts b/internal/heapsnapshot/src/index.ts new file mode 100644 index 000000000..ebd358842 --- /dev/null +++ b/internal/heapsnapshot/src/index.ts @@ -0,0 +1,3 @@ +export * from './HeapSnapshot.js'; +export * from './HeapSnapshotModel.js'; +export * from './parseHeapSnapshot.js'; diff --git a/internal/heapsnapshot/src/parseHeapSnapshot.ts b/internal/heapsnapshot/src/parseHeapSnapshot.ts new file mode 100644 index 000000000..4850b6d3a --- /dev/null +++ b/internal/heapsnapshot/src/parseHeapSnapshot.ts @@ -0,0 +1,101 @@ +import path from 'node:path'; +import { Readable } from 'node:stream'; +import { fileURLToPath, URL } from 'node:url'; +import { Worker } from 'node:worker_threads'; +import { HeapSnapshotProgress, JSHeapSnapshot } from './HeapSnapshot.js'; +import { HeapSnapshotLoader } from './HeapSnapshotLoader.js'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); + +export interface ParseHeapSnapshotOptions { + /** + * Whether to suppress console output. + * + * @default true + */ + silent?: boolean; +} + +export async function parseHeapSnapshot( + data: Readable, + opts: ParseHeapSnapshotOptions = {}, +): Promise { + const { silent = true } = opts; + const loader = new HeapSnapshotLoader( + silent ? silentProgress : consoleProgress, + ); + + await new Promise((resolve, reject) => { + function consume(chunk: string | Buffer) { + loader.write(String(chunk)); + } + data.on('data', consume); + + function cleanup() { + data.off('data', consume); + data.off('error', reject); + data.off('end', resolve); + } + data.once('error', (e) => { + cleanup(); + reject(e); + }); + data.once('end', () => { + cleanup(); + resolve(); + }); + }); + + loader.close(); + + // TODO: this will hang if the snapshot is incomplete or in some cases malformed + await loader.parsingComplete; + + // two workers are used to parse the heap snapshots, this will + // be the main one and the second one initialised in "assistant" mode + const secondWorker = new Worker( + path.join(__dirname, 'heap_snapshot_worker-entrypoint.js'), // exists after building + ); + await using _ = { + async [Symbol.asyncDispose]() { + await secondWorker.terminate(); + }, + }; + const chan = new MessageChannel(); + secondWorker.postMessage( + { + data: { + disposition: 'setupForSecondaryInit', + objectId: 0, + }, + ports: [chan.port2], + }, + [chan.port2], + ); + + return await loader.buildSnapshot(chan.port1); +} + +const consoleProgress = + new (class ConsoleProgress extends HeapSnapshotProgress { + override reportProblem(error: string): void { + console.error(error); + } + override updateProgress(title: string, value: number, total: number): void { + console.log(title, value, total); + } + override updateStatus(status: string): void { + console.log(status); + } + })(); +const silentProgress = new (class SilentProgress extends HeapSnapshotProgress { + override reportProblem() { + // noop + } + override updateProgress() { + // noop + } + override updateStatus() { + // noop + } +})(); diff --git a/internal/heapsnapshot/src/platform/ArrayUtilities.ts b/internal/heapsnapshot/src/platform/ArrayUtilities.ts new file mode 100644 index 000000000..930cd07e8 --- /dev/null +++ b/internal/heapsnapshot/src/platform/ArrayUtilities.ts @@ -0,0 +1,382 @@ +// Copyright (c) 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export const removeElement = ( + array: T[], + element: T, + firstOnly?: boolean, +): boolean => { + let index = array.indexOf(element); + if (index === -1) { + return false; + } + if (firstOnly) { + array.splice(index, 1); + return true; + } + for (let i = index + 1, n = array.length; i < n; ++i) { + if (array[i] !== element) { + array[index++] = array[i]; + } + } + array.length = index; + return true; +}; + +type NumberComparator = (a: number, b: number) => number; + +function swap(array: number[], i1: number, i2: number): void { + const temp = array[i1]; + array[i1] = array[i2]; + array[i2] = temp; +} + +function partition( + array: number[], + comparator: NumberComparator, + left: number, + right: number, + pivotIndex: number, +): number { + const pivotValue = array[pivotIndex]; + swap(array, right, pivotIndex); + let storeIndex = left; + for (let i = left; i < right; ++i) { + if (comparator(array[i], pivotValue) < 0) { + swap(array, storeIndex, i); + ++storeIndex; + } + } + swap(array, right, storeIndex); + return storeIndex; +} + +function quickSortRange( + array: number[], + comparator: NumberComparator, + left: number, + right: number, + sortWindowLeft: number, + sortWindowRight: number, +): void { + if (right <= left) { + return; + } + const pivotIndex = Math.floor(Math.random() * (right - left)) + left; + const pivotNewIndex = partition(array, comparator, left, right, pivotIndex); + if (sortWindowLeft < pivotNewIndex) { + quickSortRange( + array, + comparator, + left, + pivotNewIndex - 1, + sortWindowLeft, + sortWindowRight, + ); + } + if (pivotNewIndex < sortWindowRight) { + quickSortRange( + array, + comparator, + pivotNewIndex + 1, + right, + sortWindowLeft, + sortWindowRight, + ); + } +} + +export function sortRange( + array: number[], + comparator: NumberComparator, + leftBound: number, + rightBound: number, + sortWindowLeft: number, + sortWindowRight: number, +): number[] { + if ( + leftBound === 0 && + rightBound === array.length - 1 && + sortWindowLeft === 0 && + sortWindowRight >= rightBound + ) { + array.sort(comparator); + } else { + quickSortRange( + array, + comparator, + leftBound, + rightBound, + sortWindowLeft, + sortWindowRight, + ); + } + return array; +} +export const binaryIndexOf = ( + array: T[], + value: S, + comparator: (a: S, b: T) => number, +): number => { + const index = lowerBound(array, value, comparator); + return index < array.length && comparator(value, array[index]) === 0 + ? index + : -1; +}; + +function mergeOrIntersect( + array1: T[], + array2: T[], + comparator: (a: T, b: T) => number, + mergeNotIntersect: boolean, +): T[] { + const result = []; + let i = 0; + let j = 0; + while (i < array1.length && j < array2.length) { + const compareValue = comparator(array1[i], array2[j]); + if (mergeNotIntersect || !compareValue) { + result.push(compareValue <= 0 ? array1[i] : array2[j]); + } + if (compareValue <= 0) { + i++; + } + if (compareValue >= 0) { + j++; + } + } + if (mergeNotIntersect) { + while (i < array1.length) { + result.push(array1[i++]); + } + while (j < array2.length) { + result.push(array2[j++]); + } + } + return result; +} + +export const intersectOrdered = ( + array1: T[], + array2: T[], + comparator: (a: T, b: T) => number, +): T[] => { + return mergeOrIntersect(array1, array2, comparator, false); +}; + +export const mergeOrdered = ( + array1: T[], + array2: T[], + comparator: (a: T, b: T) => number, +): T[] => { + return mergeOrIntersect(array1, array2, comparator, true); +}; + +export const DEFAULT_COMPARATOR = ( + a: string | number, + b: string | number, +): -1 | 0 | 1 => { + return a < b ? -1 : a > b ? 1 : 0; +}; + +/** + * Returns the index of the element closest to the needle that is equal to or + * greater than it. Assumes that the provided array is sorted. + * + * If no element is found, the right bound is returned. + * + * Uses the provided comparator function to determine if two items are equal or + * if one is greater than the other. If you are working with strings or + * numbers, you can use ArrayUtilities.DEFAULT_COMPARATOR. Otherwise, you + * should define one that takes the needle element and an element from the + * array and returns a positive or negative number to indicate which is greater + * than the other. + * + * When specified, |left| (inclusive) and |right| (exclusive) indices + * define the search window. + */ +export function lowerBound( + array: Uint32Array | Int32Array, + needle: T, + comparator: (needle: T, b: number) => number, + left?: number, + right?: number, +): number; +export function lowerBound( + array: S[], + needle: T, + comparator: (needle: T, b: S) => number, + left?: number, + right?: number, +): number; +export function lowerBound( + array: readonly S[], + needle: T, + comparator: (needle: T, b: S) => number, + left?: number, + right?: number, +): number; +export function lowerBound( + array: A, + needle: T, + comparator: (needle: T, b: S) => number, + left?: number, + right?: number, +): number { + let l = left || 0; + let r = right !== undefined ? right : array.length; + while (l < r) { + const m = (l + r) >> 1; + if (comparator(needle, array[m]) > 0) { + l = m + 1; + } else { + r = m; + } + } + return r; +} + +/** + * Returns the index of the element closest to the needle that is greater than + * it. Assumes that the provided array is sorted. + * + * If no element is found, the right bound is returned. + * + * Uses the provided comparator function to determine if two items are equal or + * if one is greater than the other. If you are working with strings or + * numbers, you can use ArrayUtilities.DEFAULT_COMPARATOR. Otherwise, you + * should define one that takes the needle element and an element from the + * array and returns a positive or negative number to indicate which is greater + * than the other. + * + * When specified, |left| (inclusive) and |right| (exclusive) indices + * define the search window. + */ +export function upperBound( + array: Uint32Array, + needle: T, + comparator: (needle: T, b: number) => number, + left?: number, + right?: number, +): number; +export function upperBound( + array: S[], + needle: T, + comparator: (needle: T, b: S) => number, + left?: number, + right?: number, +): number; +export function upperBound( + array: A, + needle: T, + comparator: (needle: T, b: S) => number, + left?: number, + right?: number, +): number { + let l = left || 0; + let r = right !== undefined ? right : array.length; + while (l < r) { + const m = (l + r) >> 1; + if (comparator(needle, array[m]) >= 0) { + l = m + 1; + } else { + r = m; + } + } + return r; +} + +const enum NearestSearchStart { + BEGINNING = 'BEGINNING', + END = 'END', +} +/** + * Obtains the first or last item in the array that satisfies the predicate function. + * So, for example, if the array were arr = [2, 4, 6, 8, 10], and you are looking for + * the last item arr[i] such that arr[i] < 5 you would be returned 1, because + * array[1] is 4, the last item in the array that satisfies the + * predicate function. + * + * If instead you were looking for the first item in the same array that satisfies + * arr[i] > 5 you would be returned 2 because array[2] = 6. + * + * Please note: this presupposes that the array is already ordered. + * This function uses a variation of Binary Search. + */ +function nearestIndex( + arr: readonly T[], + predicate: (arrayItem: T) => boolean, + searchStart: NearestSearchStart, +): number | null { + const searchFromEnd = searchStart === NearestSearchStart.END; + if (arr.length === 0) { + return null; + } + + let left = 0; + let right = arr.length - 1; + let pivot = 0; + let matchesPredicate = false; + let moveToTheRight = false; + let middle = 0; + do { + middle = left + (right - left) / 2; + pivot = searchFromEnd ? Math.ceil(middle) : Math.floor(middle); + matchesPredicate = predicate(arr[pivot]); + moveToTheRight = matchesPredicate === searchFromEnd; + if (moveToTheRight) { + left = Math.min(right, pivot + (left === pivot ? 1 : 0)); + } else { + right = Math.max(left, pivot + (right === pivot ? -1 : 0)); + } + } while (right !== left); + + // Special-case: the indexed item doesn't pass the predicate. This + // occurs when none of the items in the array are a match for the + // predicate. + if (!predicate(arr[left])) { + return null; + } + return left; +} + +/** + * Obtains the first item in the array that satisfies the predicate function. + * So, for example, if the array was arr = [2, 4, 6, 8, 10], and you are looking for + * the first item arr[i] such that arr[i] > 5 you would be returned 2, because + * array[2] is 6, the first item in the array that satisfies the + * predicate function. + * + * Please note: this presupposes that the array is already ordered. + */ +export function nearestIndexFromBeginning( + arr: T[], + predicate: (arrayItem: T) => boolean, +): number | null { + return nearestIndex(arr, predicate, NearestSearchStart.BEGINNING); +} + +/** + * Obtains the last item in the array that satisfies the predicate function. + * So, for example, if the array was arr = [2, 4, 6, 8, 10], and you are looking for + * the last item arr[i] such that arr[i] < 5 you would be returned 1, because + * arr[1] is 4, the last item in the array that satisfies the + * predicate function. + * + * Please note: this presupposes that the array is already ordered. + */ + +export function nearestIndexFromEnd( + arr: readonly T[], + predicate: (arrayItem: T) => boolean, +): number | null { + return nearestIndex(arr, predicate, NearestSearchStart.END); +} + +// Type guard for ensuring that `arr` does not contain null or undefined +export function arrayDoesNotContainNullOrUndefined( + arr: (T | null | undefined)[], +): arr is T[] { + return !arr.includes(null) && !arr.includes(undefined); +} diff --git a/internal/heapsnapshot/src/platform/MapUtilities.ts b/internal/heapsnapshot/src/platform/MapUtilities.ts new file mode 100644 index 000000000..1ddaed529 --- /dev/null +++ b/internal/heapsnapshot/src/platform/MapUtilities.ts @@ -0,0 +1,97 @@ +// Copyright (c) 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export const inverse = function (map: Map): Multimap { + const result = new Multimap(); + for (const [key, value] of map.entries()) { + result.set(value, key); + } + return result; +}; + +export class Multimap { + private map = new Map>(); + + set(key: K, value: V): void { + let set = this.map.get(key); + if (!set) { + set = new Set(); + this.map.set(key, set); + } + set.add(value); + } + + get(key: K): Set { + return this.map.get(key) || new Set(); + } + + has(key: K): boolean { + return this.map.has(key); + } + + hasValue(key: K, value: V): boolean { + const set = this.map.get(key); + if (!set) { + return false; + } + return set.has(value); + } + + get size(): number { + return this.map.size; + } + + delete(key: K, value: V): boolean { + const values = this.get(key); + if (!values) { + return false; + } + const result = values.delete(value); + if (!values.size) { + this.map.delete(key); + } + return result; + } + + deleteAll(key: K): void { + this.map.delete(key); + } + + keysArray(): K[] { + return [...this.map.keys()]; + } + + keys(): IterableIterator { + return this.map.keys(); + } + + valuesArray(): V[] { + const result = []; + for (const set of this.map.values()) { + result.push(...set.values()); + } + return result; + } + + clear(): void { + this.map.clear(); + } +} + +/** + * Gets value for key, assigning a default if value is falsy. + */ +export function getWithDefault( + map: WeakMap | Map, + key: K, + defaultValueFactory: (key?: K) => V, +): V { + let value = map.get(key); + if (value === undefined || value === null) { + value = defaultValueFactory(key); + map.set(key, value); + } + + return value; +} diff --git a/internal/heapsnapshot/src/platform/PromiseUtilities.ts b/internal/heapsnapshot/src/platform/PromiseUtilities.ts new file mode 100644 index 000000000..45a698e38 --- /dev/null +++ b/internal/heapsnapshot/src/platform/PromiseUtilities.ts @@ -0,0 +1,28 @@ +export interface DeferredPromise { + promise: Promise; + resolve: (value: T) => void; + reject: (reason: any) => void; +} + +export function withResolvers(): DeferredPromise { + // @ts-expect-error not available in node 18- + if (Promise.withResolvers) return Promise.withResolvers(); + let resolveFn: (value: T) => void; + let rejectFn: (reason: any) => void; + const promise = new Promise(function deferredPromiseExecutor( + resolve, + reject, + ) { + resolveFn = resolve; + rejectFn = reject; + }); + return { + promise, + get resolve() { + return resolveFn; + }, + get reject() { + return rejectFn; + }, + }; +} diff --git a/internal/heapsnapshot/src/platform/StringUtilities.ts b/internal/heapsnapshot/src/platform/StringUtilities.ts new file mode 100644 index 000000000..358372d96 --- /dev/null +++ b/internal/heapsnapshot/src/platform/StringUtilities.ts @@ -0,0 +1,639 @@ +// Copyright (c) 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export const escapeCharacters = ( + inputString: string, + charsToEscape: string, +): string => { + let foundChar = false; + for (let i = 0; i < charsToEscape.length; ++i) { + if (inputString.indexOf(charsToEscape.charAt(i)) !== -1) { + foundChar = true; + break; + } + } + + if (!foundChar) { + return String(inputString); + } + + let result = ''; + for (let i = 0; i < inputString.length; ++i) { + if (charsToEscape.indexOf(inputString.charAt(i)) !== -1) { + result += '\\'; + } + result += inputString.charAt(i); + } + + return result; +}; + +const toHexadecimal = (charCode: number, padToLength: number): string => { + return charCode.toString(16).toUpperCase().padStart(padToLength, '0'); +}; + +// Remember to update the third group in the regexps patternsToEscape and +// patternsToEscapePlusSingleQuote when adding new entries in this map. +const escapedReplacements = new Map([ + ['\b', '\\b'], + ['\f', '\\f'], + ['\n', '\\n'], + ['\r', '\\r'], + ['\t', '\\t'], + ['\v', '\\v'], + ["'", "\\'"], + ['\\', '\\\\'], + ['<------------2-----------> <---------3--------> <-----4----> <------5-----> <-----6----> <7> +// 1: two or more consecutive uppercase letters. This is useful for identifying acronyms +// 2: lookahead assertion that matches a word boundary +// 3: numeronym: single letter followed by number and another letter +// 4: word starting with an optional uppercase letter +// 5: single digit followed by word to handle '3D' or '2px' (this might be controverial) +// 6: single uppercase letter or number +// 7: a dot character. We extract it into a separate word and remove dashes around it later. +// This is makes more sense conceptually and allows accounting for all possible word variants. +// Making dot a part of a word prevent us from handling acronyms or numeronyms after the word +// correctly without making the RegExp prohibitively complicated. +// https://regex101.com/r/FhMVKc/1 +export const toKebabCase = function (input: string): Lowercase { + return (input + .match?.(WORD) + ?.map((w) => w.toLowerCase()) + .join('-') + .replaceAll('-.-', '.') || input) as Lowercase; +}; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +export function toKebabCaseKeys(settingValue: { [x: string]: any }): { + [x: string]: any; +} { + const result: { + [x: string]: any; + } = {}; + for (const [key, value] of Object.entries(settingValue)) { + result[toKebabCase(key)] = value; + } + return result; +} +/* eslint-enable @typescript-eslint/no-explicit-any */ + +// Replaces the last ocurrence of parameter `search` with parameter `replacement` in `input` +export const replaceLast = function ( + input: string, + search: string, + replacement: string, +): string { + const replacementStartIndex = input.lastIndexOf(search); + if (replacementStartIndex === -1) { + return input; + } + + return ( + input.slice(0, replacementStartIndex) + + input.slice(replacementStartIndex).replace(search, replacement) + ); +}; + +export const stringifyWithPrecision = function stringifyWithPrecision( + s: number, + precision = 2, +): string { + if (precision === 0) { + return s.toFixed(0); + } + const string = s.toFixed(precision).replace(/\.?0*$/, ''); + return string === '-0' ? '0' : string; +}; + +/** + * Somewhat efficiently concatenates 2 base64 encoded strings. + */ +export const concatBase64 = function (lhs: string, rhs: string): string { + if (lhs.length === 0 || !lhs.endsWith('=')) { + // Empty string or no padding, we can straight-up concatenate. + return lhs + rhs; + } + const lhsLeaveAsIs = lhs.substring(0, lhs.length - 4); + const lhsToDecode = lhs.substring(lhs.length - 4); + return lhsLeaveAsIs + btoa(atob(lhsToDecode) + atob(rhs)); +}; diff --git a/internal/heapsnapshot/src/platform/TypedArrayUtilities.ts b/internal/heapsnapshot/src/platform/TypedArrayUtilities.ts new file mode 100644 index 000000000..edda8af3f --- /dev/null +++ b/internal/heapsnapshot/src/platform/TypedArrayUtilities.ts @@ -0,0 +1,200 @@ +// Copyright 2024 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * An object which provides functionality similar to Uint32Array. It may be + * implemented as: + * 1. A Uint32Array, + * 2. An array of Uint32Arrays, to support more data than Uint32Array, or + * 3. A plain array, in which case the length may change by setting values. + */ +export interface BigUint32Array { + get length(): number; + getValue(index: number): number; + setValue(index: number, value: number): void; + asUint32ArrayOrFail(): Uint32Array; + asArrayOrFail(): number[]; +} + +/** + * @returns A BigUint32Array implementation which is based on Array. + * This means that its length automatically expands to include the highest index + * used, and asArrayOrFail will succeed. + */ +export function createExpandableBigUint32Array(): BigUint32Array { + return new ExpandableBigUint32ArrayImpl(); +} + +/** + * @returns A BigUint32Array implementation which is based on Uint32Array. + * If the length is small enough to fit in a single Uint32Array, then + * asUint32ArrayOrFail will succeed. Otherwise, it will throw an exception. + */ +export function createFixedBigUint32Array( + length: number, + maxLengthForTesting?: number, +): BigUint32Array { + try { + if (maxLengthForTesting !== undefined && length > maxLengthForTesting) { + // Simulate allocation failure. + throw new RangeError(); + } + return new BasicBigUint32ArrayImpl(length); + } catch { + // We couldn't allocate a big enough ArrayBuffer. + return new SplitBigUint32ArrayImpl(length, maxLengthForTesting); + } +} + +class BasicBigUint32ArrayImpl extends Uint32Array implements BigUint32Array { + getValue(index: number): number { + return this[index]; + } + setValue(index: number, value: number): void { + this[index] = value; + } + asUint32ArrayOrFail(): Uint32Array { + return this; + } + asArrayOrFail(): number[] { + throw new Error('Not an array'); + } +} + +class SplitBigUint32ArrayImpl implements BigUint32Array { + #data: Uint32Array[]; + #partLength: number; + length: number; + + constructor(length: number, maxLengthForTesting?: number) { + this.#data = []; + this.length = length; + let partCount = 1; + while (true) { + partCount *= 2; + this.#partLength = Math.ceil(length / partCount); + try { + if ( + maxLengthForTesting !== undefined && + this.#partLength > maxLengthForTesting + ) { + // Simulate allocation failure. + throw new RangeError(); + } + for (let i = 0; i < partCount; ++i) { + this.#data[i] = new Uint32Array(this.#partLength); + } + return; + } catch (e) { + if (this.#partLength < 1e6) { + // The length per part is already small, so continuing to subdivide it + // will probably not help. + throw e; + } + } + } + } + + getValue(index: number): number { + if (index >= 0 && index < this.length) { + const partLength = this.#partLength; + return this.#data[Math.floor(index / partLength)][index % partLength]; + } + // On out-of-bounds accesses, match the behavior of Uint32Array: return an + // undefined value that's incorrectly typed as number. + return this.#data[0][-1]; + } + + setValue(index: number, value: number): void { + if (index >= 0 && index < this.length) { + const partLength = this.#partLength; + this.#data[Math.floor(index / partLength)][index % partLength] = value; + } + // Attempting to set a value out of bounds does nothing, like Uint32Array. + } + + asUint32ArrayOrFail(): Uint32Array { + throw new Error('Not a Uint32Array'); + } + asArrayOrFail(): number[] { + throw new Error('Not an array'); + } +} + +class ExpandableBigUint32ArrayImpl + extends Array + implements BigUint32Array +{ + getValue(index: number): number { + return this[index]; + } + setValue(index: number, value: number): void { + this[index] = value; + } + asUint32ArrayOrFail(): Uint32Array { + throw new Error('Not a Uint32Array'); + } + asArrayOrFail(): number[] { + return this; + } +} + +export interface BitVector { + getBit(index: number): boolean; + setBit(index: number): void; + clearBit(index: number): void; + // Returns the last bit before `index` which is set, or -1 if there are none. + previous(index: number): number; + get buffer(): ArrayBuffer; +} + +export function createBitVector( + lengthOrBuffer: number | ArrayBuffer, +): BitVector { + return new BitVectorImpl(lengthOrBuffer); +} + +class BitVectorImpl extends Uint8Array { + constructor(lengthOrBuffer: number | ArrayBuffer) { + if (typeof lengthOrBuffer === 'number') { + super(Math.ceil(lengthOrBuffer / 8)); + } else { + super(lengthOrBuffer); + } + } + getBit(index: number): boolean { + const value = this[index >> 3] & (1 << (index & 7)); + return value !== 0; + } + setBit(index: number): void { + this[index >> 3] |= 1 << (index & 7); + } + clearBit(index: number): void { + this[index >> 3] &= ~(1 << (index & 7)); + } + previous(index: number): number { + // First, check for more bits in the current byte. + while (index !== (index >> 3) << 3) { + --index; + if (this.getBit(index)) { + return index; + } + } + // Next, iterate by bytes to skip over ranges of zeros. + let byteIndex: number = (index >> 3) - 1; + while (byteIndex >= 0 && this[byteIndex] === 0) { + --byteIndex; + } + if (byteIndex < 0) { + return -1; + } + // Finally, iterate the nonzero byte to find the highest bit. + for (index = (byteIndex << 3) + 7; index >= byteIndex << 3; --index) { + if (this.getBit(index)) { + return index; + } + } + throw new Error('Unreachable'); + } +} diff --git a/internal/heapsnapshot/src/platform/index.ts b/internal/heapsnapshot/src/platform/index.ts new file mode 100644 index 000000000..23e8eb0c1 --- /dev/null +++ b/internal/heapsnapshot/src/platform/index.ts @@ -0,0 +1,5 @@ +export * as TypedArrayUtilities from './TypedArrayUtilities.js'; +export * as ArrayUtilities from './ArrayUtilities.js'; +export * as StringUtilities from './StringUtilities.js'; +export * as MapUtilities from './MapUtilities.js'; +export * as PromiseUtilities from './PromiseUtilities.js'; diff --git a/internal/heapsnapshot/tsconfig.json b/internal/heapsnapshot/tsconfig.json new file mode 100644 index 000000000..d5ead103f --- /dev/null +++ b/internal/heapsnapshot/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": [ + "@tsconfig/recommended/tsconfig.json", + "@tsconfig/node18/tsconfig.json" + ], + "compilerOptions": { + "outDir": "./dist", + "composite": true, + "types": ["node"] + } +} diff --git a/internal/perf/package.json b/internal/perf/package.json index 8c992af5d..d2e38cdd0 100644 --- a/internal/perf/package.json +++ b/internal/perf/package.json @@ -4,8 +4,6 @@ "private": true, "main": "./src/index.ts", "dependencies": { - "@memlab/core": "^1.1.42", - "@memlab/heap-analysis": "^1.0.39", "@whatwg-node/promise-helpers": "^1.3.0", "canvas": "^3.1.2", "chart.js": "^4.4.7", diff --git a/internal/perf/src/heap.ts b/internal/perf/src/heapsampling.ts similarity index 60% rename from internal/perf/src/heap.ts rename to internal/perf/src/heapsampling.ts index 8e67c5655..49889d509 100644 --- a/internal/perf/src/heap.ts +++ b/internal/perf/src/heapsampling.ts @@ -1,104 +1,10 @@ import { HeapProfiler } from 'inspector'; import path from 'path'; -import { serializer } from '@memlab/core'; -import { getFullHeapFromFile, PluginUtils } from '@memlab/heap-analysis'; import { CallTreeNode, Frame } from 'speedscope/profile'; import { importFromChromeHeapProfile } from 'speedscope/profile/v8heapalloc'; const __project = path.resolve(__dirname, '..', '..', '..') + path.sep; -/** - * Analyses the {@link file heap snapshot file} logging the largest single objects and summed objects. - * - * TODO: Leak detection and return something. - * TODO: innacurate when comparing results to chrome devtools - */ -export async function analyzeHeapSnapshot(file: string) { - const snap = await getFullHeapFromFile(file); - - // these are largest _single_ objects in the heap - const largestSingleObjects = PluginUtils.filterOutLargestObjects( - snap, - PluginUtils.isNodeWorthInspecting, - 5, - ); - console.group('Largest single objects'); - for (const node of largestSingleObjects) { - console.group(`(${node.type}) ${node.name}`.trim()); - - console.log('self size', (node.self_size / 1024).toFixed(2), 'kB'); - console.log( - 'retained size', - (node.retainedSize / (1024 * 1024)).toFixed(2), - 'MB', - ); - - console.groupEnd(); - } - console.groupEnd(); - - // - - // a summed node is a node whose sizes are summed up for all instances of that node - type SummedObject = { - type: string; - name: string; - selfSize: number; - retainedSize: number; - }; - - const summedObjects: { - [key: string]: SummedObject; - } = {}; - - snap.nodes.forEach((node) => { - if (!PluginUtils.isNodeWorthInspecting(node)) { - // we only care about nodes we have control over, like objects and strings - return; - } - - const key = serializer.summarizeNodeShape(node); - - if (summedObjects[key]) { - summedObjects[key].selfSize += node.self_size; - summedObjects[key].retainedSize += node.retainedSize; - } else { - summedObjects[key] = { - type: node.type, - name: node.name, - selfSize: node.self_size, - retainedSize: node.retainedSize, - }; - } - }); - - const largestSummedObjects: SummedObject[] = []; - - for (const object of Object.values(summedObjects)) { - // only the top 10 nodes with the highest retained size - largestSummedObjects.push(object); - largestSummedObjects.sort((n1, n2) => n2.retainedSize - n1.retainedSize); - if (largestSummedObjects.length > 10) { - largestSummedObjects.pop(); - } - } - - console.group('Largest summed objects'); - for (const node of largestSummedObjects) { - console.group(`(${node.type}) ${node.name}`.trim()); - - console.log('self size', (node.selfSize / (1024 * 1024)).toFixed(2), 'MB'); - console.log( - 'retained size', - (node.retainedSize / (1024 * 1024)).toFixed(2), - 'MB', - ); - - console.groupEnd(); - } - console.groupEnd(); -} - export interface HeapSamplingProfileNode { name: string; /** diff --git a/internal/perf/src/heapsnapshot.ts b/internal/perf/src/heapsnapshot.ts new file mode 100644 index 000000000..f292f74fb --- /dev/null +++ b/internal/perf/src/heapsnapshot.ts @@ -0,0 +1,104 @@ +import { createReadStream } from 'fs'; +import { Diff, parseHeapSnapshot } from '@internal/heapsnapshot'; + +export interface HeapSnapshotDiff { + [ctor: string]: Omit; +} + +/** + * Diffs the provided v8 JavaScript {@link files heap snapshot files} + * consecutively and filters the results to only include objects that have a positive + * size delta (grew in size) in **every** snapshot, possibly indicating a leak. + * + * To make accurete results, there should be at least three heap snapshots provided. + * + * Note that this is a heuristic and may not always indicate a leak, some objects may + * legitimately grow in size or count over time. + */ +export async function leakingObjectsInHeapSnapshotFiles( + files: string[], +): Promise { + if (files.length < 3) { + throw new Error( + 'At least three heap snapshot files are required for leak detection.', + ); + } + const snapshotFiles = [...files]; + + const totalGrowingDiff: HeapSnapshotDiff = {}; + + let baseSnap = await parseHeapSnapshot( + createReadStream(snapshotFiles.shift()!), + ); + let firstSnap = true; + while (baseSnap) { + const snapshotFile = snapshotFiles.shift()!; + if (!snapshotFile) { + break; // no more profiles to compare + } + + const snap = await parseHeapSnapshot(createReadStream(snapshotFile)); + + const defs = snap.interfaceDefinitions(); + const aggregates = baseSnap.aggregatesForDiff(defs); + const snapshotDiff = snap.calculateSnapshotDiff('', aggregates); + + // next base snap is this/current snap + baseSnap = snap; + + const growingDiff: HeapSnapshotDiff = {}; + for (const { addedIndexes, deletedIndexes, ...diff } of Object.values( + snapshotDiff, + )) { + if ( + // size just kept growing + diff.sizeDelta > 0 + ) { + growingDiff[diff.name] = diff; + } + } + + if (firstSnap) { + // this is the first snapshot, so we just take the diff as is + firstSnap = false; + Object.assign(totalGrowingDiff, growingDiff); + continue; + } + + for (const diff of Object.values(growingDiff)) { + const totalGrowingDiffForName = totalGrowingDiff[diff.name]; + if (!totalGrowingDiffForName) { + // didnt grow in the previous snapshot, so we skip it + continue; + } + + totalGrowingDiffForName.addedCount += diff.addedCount; + totalGrowingDiffForName.removedCount += diff.removedCount; + totalGrowingDiffForName.addedSize += diff.addedSize; + totalGrowingDiffForName.removedSize += diff.removedSize; + totalGrowingDiffForName.countDelta += diff.countDelta; + totalGrowingDiffForName.sizeDelta += diff.sizeDelta; + } + + // remove everything that the total has but not in the current index, means the thing didnt grow + for (const totalDiffName of Object.keys(totalGrowingDiff)) { + if (!growingDiff[totalDiffName]) { + delete totalGrowingDiff[totalDiffName]; + } + } + } + + return totalGrowingDiff; +} + +/** Converts the provided bytes size to human-readable format (kB, MB, GB). Uses the SI prefix. */ +export function bytesToHuman(size: number) { + if (size < 1_000) { + return `${size}B`; + } else if (size < 1_000_000) { + return `${(size / 1_000).toFixed(2)}kB`; + } else if (size < 1_000_000_000) { + return `${(size / 1_000_000).toFixed(2)}MB`; + } + return `${(size / 1_000_000_000).toFixed(2)}GB`; +} diff --git a/internal/perf/src/index.ts b/internal/perf/src/index.ts index 344059e4a..6326184a8 100644 --- a/internal/perf/src/index.ts +++ b/internal/perf/src/index.ts @@ -1,3 +1,4 @@ export * from './loadtest'; -export * from './heap'; export * from './inspector'; +export * from './heapsnapshot'; +export * from './heapsampling'; diff --git a/internal/perf/src/loadtest.ts b/internal/perf/src/loadtest.ts index aeee1bc38..053148734 100644 --- a/internal/perf/src/loadtest.ts +++ b/internal/perf/src/loadtest.ts @@ -1,6 +1,4 @@ -import fs from 'fs/promises'; import { HeapProfiler } from 'inspector'; -import os from 'os'; import path from 'path'; import { setTimeout } from 'timers/promises'; import { ProcOptions, Server, spawn } from '@internal/proc'; @@ -36,6 +34,16 @@ export interface LoadtestOptions extends ProcOptions { * @default false */ takeHeapSnapshots?: boolean; + /** + * Whether to perform heap allocation sampling during the complete loadtest run. + * This is the "allocation sampling" feature of the V8 memory profiling you may + * find in the Chrome DevTools. It approximates memory allocations by sampling + * long operations with minimal overhead and get a breakdown by JavaScript execution + * stack. + * + * @default false + */ + performHeapSampling?: boolean; /** * Should the loadtest immediatelly error out on the first failed request? * @@ -83,8 +91,8 @@ export interface LoadtestHeapSnapshot { export async function loadtest(opts: LoadtestOptions): Promise<{ samples: LoadtestMemorySample[]; - heapsnapshots: LoadtestHeapSnapshot[]; - profile: HeapProfiler.SamplingHeapProfile; + heapSnapshots: LoadtestHeapSnapshot[]; + heapSamplingProfile: HeapProfiler.SamplingHeapProfile | null; }> { const { cwd, @@ -97,6 +105,7 @@ export async function loadtest(opts: LoadtestOptions): Promise<{ server, query, takeHeapSnapshots, + performHeapSampling, allowFailingRequests, onMemorySample, onHeapSnapshot, @@ -111,6 +120,8 @@ export async function loadtest(opts: LoadtestOptions): Promise<{ throw new Error(`At least one run is necessary, got "${runs}"`); } + const startTime = new Date(); + // make sure the query works before starting the loadtests // the request here matches the request done in loadtest-script.ts const res = await fetch( @@ -149,10 +160,6 @@ export async function loadtest(opts: LoadtestOptions): Promise<{ ctrl.abort('Test run cancelled'); }); - const heapsnapshotCwd = await fs.mkdtemp( - path.join(os.tmpdir(), 'hive-gateway_perf_loadtest_heapsnapshots'), - ); - using inspector = await connectInspector(server); let phase: LoadtestPhase = 'idle'; @@ -194,7 +201,8 @@ export async function loadtest(opts: LoadtestOptions): Promise<{ if (takeHeapSnapshots) { const heapsnapshot = await createHeapSnapshot( - heapsnapshotCwd, + cwd, + startTime, inspector, phase, run, @@ -204,7 +212,9 @@ export async function loadtest(opts: LoadtestOptions): Promise<{ } // start heap sampling after idling (no need to sample anything during the idling phase) - const stopHeapSampling = await inspector.startHeapSampling(); + const stopHeapSampling = performHeapSampling + ? await inspector.startHeapSampling() + : () => null; // no-op if no heap allocation sampling for (; run <= runs; run++) { phase = 'loadtest'; @@ -245,7 +255,8 @@ export async function loadtest(opts: LoadtestOptions): Promise<{ if (takeHeapSnapshots) { const heapsnapshot = await createHeapSnapshot( - heapsnapshotCwd, + cwd, + startTime, inspector, phase, run, @@ -257,19 +268,29 @@ export async function loadtest(opts: LoadtestOptions): Promise<{ return { samples, - heapsnapshots, - profile: await stopHeapSampling(), + heapSnapshots: heapsnapshots, + heapSamplingProfile: await stopHeapSampling(), }; } async function createHeapSnapshot( cwd: string, + startTime: Date, inspector: Inspector, phase: LoadtestPhase, run: number, ): Promise { const time = new Date(); - const file = path.join(cwd, `${phase}-run-${run}-${Date.now()}.heapsnapshot`); + const filenameSafeStartTime = startTime + .toISOString() + // replace time colons with dashes to make it a valid filename + .replaceAll(':', '-') + // remove milliseconds + .split('.')[0]; + const file = path.join( + cwd, + `loadtest-${phase}-run-${run}-${filenameSafeStartTime}.heapsnapshot`, + ); await inspector.writeHeapSnapshot(file); return { phase, run, time, file }; } diff --git a/internal/perf/src/memtest.ts b/internal/perf/src/memtest.ts index e22125e41..9d4a5cc62 100644 --- a/internal/perf/src/memtest.ts +++ b/internal/perf/src/memtest.ts @@ -1,18 +1,23 @@ import fs from 'fs/promises'; import path from 'path'; +import { fileURLToPath } from 'url'; import { Server } from '@internal/proc'; import { getEnvStr, isDebug } from '@internal/testing'; import { it } from 'vitest'; import { createMemorySampleLineChart } from './chart'; import { - getHeaviestFramesFromHeapSamplingProfile, - HeapSamplingProfileFrame, -} from './heap'; + bytesToHuman, + leakingObjectsInHeapSnapshotFiles, +} from './heapsnapshot'; import { loadtest, LoadtestOptions } from './loadtest'; +const __dirname = fileURLToPath(new URL('.', import.meta.url)); +const __project = path.resolve(__dirname, '..', '..', '..'); + const supportedFlags = [ 'short' as const, - 'heapsnaps' as const, + 'cleanheapsnaps' as const, + 'noheapsnaps' as const, 'moreruns' as const, 'chart' as const, 'sampling' as const, @@ -23,10 +28,11 @@ const supportedFlags = [ * * {@link supportedFlags Supported flags} are: * - `short` Runs the loadtest for `30s` and the calmdown for `10s` instead of the defaults. - * - `heapsnaps` Takes heap snapshots instead of the defaults. - * - `moreruns` Does `5` runs instead of the defaults. + * - `cleanheapsnaps` Remove any existing heap snapshot (`*.heapsnapshot`) files before the test. + * - `noheapsnaps` Disable taking heap snapshots. + * - `moreruns` Does `10` runs instead of the defaults. * - `chart` Writes the memory consumption chart. - * - `sampling` Will write the heap allocation sampling profile regardless of whether the test fails. + * - `sampling` Perform and write the heap sampling profile. */ const flags = getEnvStr('MEMTEST') @@ -59,68 +65,59 @@ export interface MemtestOptions * Whether to take heap snapshots on the end of the `idle` phase and then at the end * of the `calmdown` {@link LoadtestPhase phase} in each of the {@link runs}. * - * Ignores the `default` and runs with `true` if {@link flags MEMTEST has the `heapsnaps` flag}. + * Ignores the _@default_ and runs with `false` if {@link flags MEMTEST} has the `noheapsnaps` + * flag provided. * - * @default false + * @default true */ takeHeapSnapshots?: boolean; /** * Idling duration before loadtests {@link runs run} in milliseconds. * - * @default 10_000 + * @default 5_000 */ idle?: number; /** * Duration of the loadtest for each {@link runs run} in milliseconds. * - * Ignores the `default` and runs for `30s` if {@link flags MEMTEST has the `short` flag}. + * Ignores the _@default_ and runs for `10s` if {@link flags MEMTEST} has the `short` + * flag provided. * - * @default 120_000 + * @default 30_000 */ duration?: number; /** * Calmdown duration after loadtesting {@link runs run} in milliseconds. * - * Ignores the `default` and runs for `10s` if {@link flags MEMTEST has the `short` flag}. + * Ignores the _@default_ and runs for `5s` if {@link flags MEMTEST} has the `short` + * flag provided. * - * @default 30_000 + * @default 10_000 */ calmdown?: number; /** * How many times to run the loadtests? * - * Ignores the `default` and does `5` runs if {@link flags MEMTEST has the `moreruns` flag}. + * Ignores the _@default_ and does `10` runs if {@link flags MEMTEST} has the `moreruns` + * flag provided. * - * @default 3 + * @default 5 */ runs?: number; - /** - * The heap allocation sampling profile gathered during the loadtests is analysed - * to find the heaviest frames (frames that allocated most of the memory). These, - * high allocation frames, are often the ones that contain a leak. But not always, - * a frame can simply be heavy... There are some usual suspects which we safely ignore; - * but, if the profile contains any other unexpected heavy frames, the test will fail. - * - * Using this callback check, you can add more "expected" heavy frames for a given test. - * - * BEWARE: Please be diligent when adding expected heavy frames. Carefully analyse the - * heap sampling profile and make sure that the frame you're adding is 100% not leaking. - */ - expectedHeavyFrame?: (frame: HeapSamplingProfileFrame) => boolean; } export function memtest(opts: MemtestOptions, setup: () => Promise) { const { cwd, memorySnapshotWindow = 1_000, - idle = 10_000, - duration = flags.includes('short') ? 30_000 : 120_000, - calmdown = flags.includes('short') ? 10_000 : 30_000, - runs = flags.includes('moreruns') ? 5 : 3, - takeHeapSnapshots = flags.includes('heapsnaps'), + idle = 5_000, + duration = flags.includes('short') ? 10_000 : 30_000, + calmdown = flags.includes('short') ? 5_000 : 10_000, + runs = flags.includes('moreruns') ? 10 : 5, + takeHeapSnapshots = !flags.includes('noheapsnaps'), + performHeapSampling = flags.includes('sampling'), onMemorySample, onHeapSnapshot, - expectedHeavyFrame, ...loadtestOpts } = opts; it( @@ -135,6 +132,15 @@ export function memtest(opts: MemtestOptions, setup: () => Promise) { runs, }, async ({ expect }) => { + if (flags.includes('cleanheapsnaps')) { + const filesInCwd = await fs.readdir(cwd, { withFileTypes: true }); + for (const file of filesInCwd) { + if (file.isFile() && file.name.endsWith('.heapsnapshot')) { + await fs.unlink(path.join(cwd, file.name)); + } + } + } + const server = await setup(); const startTime = new Date() @@ -149,6 +155,7 @@ export function memtest(opts: MemtestOptions, setup: () => Promise) { cwd, memorySnapshotWindow, takeHeapSnapshots, + performHeapSampling, idle, duration, calmdown, @@ -165,104 +172,73 @@ export function memtest(opts: MemtestOptions, setup: () => Promise) { } return onMemorySample?.(samples); }, - async onHeapSnapshot(heapsnapshot) { - if (flags.includes('heapsnaps')) { - await fs.copyFile( - heapsnapshot.file, - path.join( - cwd, - `memtest-run-${heapsnapshot.run}-${heapsnapshot.phase}_${startTime}.heapsnapshot`, - ), - ); - } - return onHeapSnapshot?.(heapsnapshot); - }, }); - // TODO: track failed requests during the loadtest, if any - - const heapSamplingProfileFile = path.join( - cwd, - `memtest_${startTime}.heapprofile`, - ); - if (flags.includes('sampling')) { + if (loadtestResult.heapSamplingProfile) { + const heapSamplingProfileFile = path.join( + cwd, + `memtest_${startTime}.heapprofile`, + ); await fs.writeFile( heapSamplingProfileFile, - JSON.stringify(loadtestResult.profile), + JSON.stringify(loadtestResult.heapSamplingProfile), ); } - // NOTE: memory usage slop trend check is disabled allowing us to run the tests in parallel in the CI - // and we dont want to disable it _only_ in the CI because we want consistant tests locally and in the CI - // import { calculateTrendSlope } from './chart' - // const slope = calculateTrendSlope(loadtestResult.samples.map(({ mem }) => mem)); - // expect - // .soft(slope, 'Consistent memory increase detected') - // .toBeLessThan(10); + if (loadtestResult.heapSnapshots.length) { + const diff = await leakingObjectsInHeapSnapshotFiles( + loadtestResult.heapSnapshots.map(({ file }) => file), + ); - const unexpectedHeavyFrames = getHeaviestFramesFromHeapSamplingProfile( - loadtestResult.profile, - ) - .filter( - (frame) => - // node internals can allocate a lot, but they on their own cannot leak - // if other things triggered by node internals are leaking, they will show up in other frames - !frame.callstack.every( - (stack) => - stack.file?.startsWith('node:') || stack.name === '(root)', - ) && - // memoized functions are usually heavy because they're called a lot, but they're proven to not leak - !( - frame.name === 'set' && - frame.callstack.some((stack) => stack.name === 'memoized') - ) && - // graphql visitor enter is heavy because it's called a lot, but it's proven to not leak - !( - frame.name === 'enter' && - frame.callstack.some((stack) => stack.name === 'visit') - ) && - // graphql visitor leave is heavy because it's called a lot, but it's proven to not leak - !( - frame.name === 'leave' && - frame.callstack.some((stack) => stack.name === 'visit') - ) && - // the (fake)promises themselves cannot leak, things they do can - !( - frame.name === 'then' && - frame.callstack.some( - (stack) => stack.name === 'handleMaybePromise', - ) - ) && - // Anonymous `set` frames are false-positives - !(frame.name === 'set' && frame.file == null), - ) - .filter((frame) => { - if (expectedHeavyFrame) { - // user-provided heavy frames check - return !expectedHeavyFrame(frame); - } - return true; - }); + // growing "(compiled code)" in memory heap snapshots, is typically not a memory leak in the traditional + // sense, but rather a reflection of how the JavaScript engine optimizes your code. It usually + // indicates that the V8 engine is compiling and optimizing more and more JavaScript functions as + // your application runs + // + // TODO: while subtle growth is normal, an excessive and rapid increase in "(compiled code)" could be + // a symptom of an issue where codepaths repeadetly generate and execute new functions that are + // different from the previous ones + delete diff['(compiled code)']; - if (unexpectedHeavyFrames.length) { - let msg = `Unexpected heavy frames detected! In total ${unexpectedHeavyFrames.length} and they are:\n\n`; - let i = 1; - for (const frame of unexpectedHeavyFrames) { - msg += `${i++}. ${frame.name} (${frame.file || ''})\n`; - for (const stack of frame.callstack) { - msg += ` ${stack.name} (${stack.file || ''})\n`; - } - msg += '\n'; - } - msg += `Writing heap sampling profile to ${heapSamplingProfileFile}`; + // "(system)" is a label used to group objects and memory allocations that are managed directly by th + // JavaScript engine's internal systems. a growing "(system)" footprint could signal code bloat or + // inefficient code patterns that force the engine to create many internal data structures, leading + // to an increased "(system)" size + // + // TODO: use it to detect code bloat or inefficient code patterns. optimizing it will lead to better + // JS execution performance and reduced memory usage + delete diff['(system)']; - await fs.writeFile( - heapSamplingProfileFile, - JSON.stringify(loadtestResult.profile), - ); + if (Object.keys(diff).length) { + expect.fail(`Leak detected on ${Object.keys(diff).length} object(s) that kept growing in every snapshot: + ${Object.values(diff) + .map( + ({ + name, + addedSize, + removedSize, + sizeDelta, + addedCount, + removedCount, + countDelta, + }) => + // use SI prefix to convert bytes to MB + `\t- "${name}" allocated ${bytesToHuman(addedSize)}, freed ${removedSize > 0 ? bytesToHuman(removedSize) : 'nothing'} (Δ${bytesToHuman(sizeDelta)}) +\t\t- ${addedCount} instances were added, ${removedCount} were removed (Δ${countDelta})`, + ) + .join('\n')} - expect.fail(msg); +Please load the following heap snapshots respectively in Chrome DevTools for more details: +${loadtestResult.heapSnapshots.map(({ file }, index) => `\t${index + 1}. ${path.relative(__project, file)}`).join('\n')}`); + } + } else { + expect.fail('Expected to diff heap snapshots, but none were taken.'); } + + // no leak, remove the heap snapshots + await Promise.all( + loadtestResult.heapSnapshots.map(({ file }) => fs.unlink(file)), + ); }, ); } diff --git a/internal/perf/tests/__fixtures__/http-server-under-load/1.heapsnapshot.tar.gz b/internal/perf/tests/__fixtures__/http-server-under-load/1.heapsnapshot.tar.gz new file mode 100644 index 000000000..a453c4ba0 Binary files /dev/null and b/internal/perf/tests/__fixtures__/http-server-under-load/1.heapsnapshot.tar.gz differ diff --git a/internal/perf/tests/__fixtures__/http-server-under-load/2.heapsnapshot.tar.gz b/internal/perf/tests/__fixtures__/http-server-under-load/2.heapsnapshot.tar.gz new file mode 100644 index 000000000..e605e3750 Binary files /dev/null and b/internal/perf/tests/__fixtures__/http-server-under-load/2.heapsnapshot.tar.gz differ diff --git a/internal/perf/tests/__fixtures__/http-server-under-load/3.heapsnapshot.tar.gz b/internal/perf/tests/__fixtures__/http-server-under-load/3.heapsnapshot.tar.gz new file mode 100644 index 000000000..a7e470b7d Binary files /dev/null and b/internal/perf/tests/__fixtures__/http-server-under-load/3.heapsnapshot.tar.gz differ diff --git a/internal/perf/tests/__fixtures__/http-server-under-load/4.heapsnapshot.tar.gz b/internal/perf/tests/__fixtures__/http-server-under-load/4.heapsnapshot.tar.gz new file mode 100644 index 000000000..b4a50169e Binary files /dev/null and b/internal/perf/tests/__fixtures__/http-server-under-load/4.heapsnapshot.tar.gz differ diff --git a/internal/perf/tests/__fixtures__/random-grow-and-free/1.heapsnapshot.tar.gz b/internal/perf/tests/__fixtures__/random-grow-and-free/1.heapsnapshot.tar.gz new file mode 100644 index 000000000..bef2521f7 Binary files /dev/null and b/internal/perf/tests/__fixtures__/random-grow-and-free/1.heapsnapshot.tar.gz differ diff --git a/internal/perf/tests/__fixtures__/random-grow-and-free/2.heapsnapshot.tar.gz b/internal/perf/tests/__fixtures__/random-grow-and-free/2.heapsnapshot.tar.gz new file mode 100644 index 000000000..4c6e27b77 Binary files /dev/null and b/internal/perf/tests/__fixtures__/random-grow-and-free/2.heapsnapshot.tar.gz differ diff --git a/internal/perf/tests/__fixtures__/random-grow-and-free/3.heapsnapshot.tar.gz b/internal/perf/tests/__fixtures__/random-grow-and-free/3.heapsnapshot.tar.gz new file mode 100644 index 000000000..7253b3cba Binary files /dev/null and b/internal/perf/tests/__fixtures__/random-grow-and-free/3.heapsnapshot.tar.gz differ diff --git a/internal/perf/tests/__fixtures__/random-grow-and-free/4.heapsnapshot.tar.gz b/internal/perf/tests/__fixtures__/random-grow-and-free/4.heapsnapshot.tar.gz new file mode 100644 index 000000000..5bba85758 Binary files /dev/null and b/internal/perf/tests/__fixtures__/random-grow-and-free/4.heapsnapshot.tar.gz differ diff --git a/internal/perf/tests/__fixtures__/random-grow-and-free/5.heapsnapshot.tar.gz b/internal/perf/tests/__fixtures__/random-grow-and-free/5.heapsnapshot.tar.gz new file mode 100644 index 000000000..59d419320 Binary files /dev/null and b/internal/perf/tests/__fixtures__/random-grow-and-free/5.heapsnapshot.tar.gz differ diff --git a/internal/perf/tests/__fixtures__/random-grow-and-free/6.heapsnapshot.tar.gz b/internal/perf/tests/__fixtures__/random-grow-and-free/6.heapsnapshot.tar.gz new file mode 100644 index 000000000..83ddb0a35 Binary files /dev/null and b/internal/perf/tests/__fixtures__/random-grow-and-free/6.heapsnapshot.tar.gz differ diff --git a/internal/perf/tests/__fixtures__/small-leak-in-growing-array/1.heapsnapshot.tar.gz b/internal/perf/tests/__fixtures__/small-leak-in-growing-array/1.heapsnapshot.tar.gz new file mode 100644 index 000000000..2851e3dfd Binary files /dev/null and b/internal/perf/tests/__fixtures__/small-leak-in-growing-array/1.heapsnapshot.tar.gz differ diff --git a/internal/perf/tests/__fixtures__/small-leak-in-growing-array/2.heapsnapshot.tar.gz b/internal/perf/tests/__fixtures__/small-leak-in-growing-array/2.heapsnapshot.tar.gz new file mode 100644 index 000000000..2c2c0860f Binary files /dev/null and b/internal/perf/tests/__fixtures__/small-leak-in-growing-array/2.heapsnapshot.tar.gz differ diff --git a/internal/perf/tests/__fixtures__/small-leak-in-growing-array/3.heapsnapshot.tar.gz b/internal/perf/tests/__fixtures__/small-leak-in-growing-array/3.heapsnapshot.tar.gz new file mode 100644 index 000000000..fddf21e95 Binary files /dev/null and b/internal/perf/tests/__fixtures__/small-leak-in-growing-array/3.heapsnapshot.tar.gz differ diff --git a/internal/perf/tests/__fixtures__/small-leak-in-growing-array/4.heapsnapshot.tar.gz b/internal/perf/tests/__fixtures__/small-leak-in-growing-array/4.heapsnapshot.tar.gz new file mode 100644 index 000000000..10ea07260 Binary files /dev/null and b/internal/perf/tests/__fixtures__/small-leak-in-growing-array/4.heapsnapshot.tar.gz differ diff --git a/internal/perf/tests/__fixtures__/small-leak-in-growing-array/5.heapsnapshot.tar.gz b/internal/perf/tests/__fixtures__/small-leak-in-growing-array/5.heapsnapshot.tar.gz new file mode 100644 index 000000000..d345fa380 Binary files /dev/null and b/internal/perf/tests/__fixtures__/small-leak-in-growing-array/5.heapsnapshot.tar.gz differ diff --git a/internal/perf/tests/__fixtures__/small-leak-in-growing-array/6.heapsnapshot.tar.gz b/internal/perf/tests/__fixtures__/small-leak-in-growing-array/6.heapsnapshot.tar.gz new file mode 100644 index 000000000..4a0e79479 Binary files /dev/null and b/internal/perf/tests/__fixtures__/small-leak-in-growing-array/6.heapsnapshot.tar.gz differ diff --git a/internal/perf/tests/heapsnapshot.test.ts b/internal/perf/tests/heapsnapshot.test.ts new file mode 100644 index 000000000..899a0c2d9 --- /dev/null +++ b/internal/perf/tests/heapsnapshot.test.ts @@ -0,0 +1,142 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { spawn } from '@internal/proc'; +import { expect, it } from 'vitest'; +import { leakingObjectsInHeapSnapshotFiles } from '../src/heapsnapshot'; + +const __fixtures = path.resolve(__dirname, '__fixtures__'); + +it.skipIf( + // no need to test in bun (also, bun does not support increasing timeouts per test) + globalThis.Bun, +)( + 'should correctly calculate no leaking objects', + { + // parsing snapshots can take a while, so we increase the timeout + timeout: 30_000, + }, + async () => { + await using snaps = await archivedFixtureFiles([ + 'http-server-under-load/1.heapsnapshot', + 'http-server-under-load/2.heapsnapshot', + 'http-server-under-load/3.heapsnapshot', + 'http-server-under-load/4.heapsnapshot', + ]); + await expect(leakingObjectsInHeapSnapshotFiles(snaps.filepaths)).resolves + .toMatchInlineSnapshot(` + {} + `); + }, +); + +it.skipIf( + // no need to test in bun (also, bun does not support increasing timeouts per test) + globalThis.Bun, +)( + 'should correctly detect randomly growing and freeing objects in size', + { + // parsing snapshots can take a while, so we increase the timeout + timeout: 30_000, + }, + async () => { + await using snaps = await archivedFixtureFiles([ + 'random-grow-and-free/1.heapsnapshot', + 'random-grow-and-free/2.heapsnapshot', + 'random-grow-and-free/3.heapsnapshot', + 'random-grow-and-free/4.heapsnapshot', + 'random-grow-and-free/5.heapsnapshot', + 'random-grow-and-free/6.heapsnapshot', + ]); + await expect(leakingObjectsInHeapSnapshotFiles(snaps.filepaths)).resolves + .toMatchInlineSnapshot(` + { + "(compiled code)": { + "addedCount": 16020, + "addedSize": 3015968, + "countDelta": -9728, + "name": "(compiled code)", + "removedCount": 25748, + "removedSize": 1731144, + "sizeDelta": 1284824, + }, + } + `); + }, +); + +it.skipIf( + // no need to test in bun (also, bun does not support increasing timeouts per test) + globalThis.Bun, +)( + 'should detect a small leak in a forever growing array', + { + // parsing snapshots can take a while, so we increase the timeout + timeout: 30_000, + }, + async () => { + await using snaps = await archivedFixtureFiles([ + 'small-leak-in-growing-array/1.heapsnapshot', + 'small-leak-in-growing-array/2.heapsnapshot', + 'small-leak-in-growing-array/3.heapsnapshot', + 'small-leak-in-growing-array/4.heapsnapshot', + 'small-leak-in-growing-array/5.heapsnapshot', + 'small-leak-in-growing-array/6.heapsnapshot', + ]); + await expect(leakingObjectsInHeapSnapshotFiles(snaps.filepaths)).resolves + .toMatchInlineSnapshot(` + { + "(system)": { + "addedCount": 47, + "addedSize": 88040, + "countDelta": -50, + "name": "(system)", + "removedCount": 97, + "removedSize": 38408, + "sizeDelta": 49632, + }, + "{subgraphName}": { + "addedCount": 60597, + "addedSize": 1939104, + "countDelta": 60597, + "name": "{subgraphName}", + "removedCount": 0, + "removedSize": 0, + "sizeDelta": 1939104, + }, + } + `); + }, +); + +/** + * Unarchives the {@link archivedFiles provided fixture files} for using in + * tests and then removes them on disposal. + * + * @param archivedFiles - An array of file names of the archived fixture file (without `.tar.gz`). Is the + * filename of the file inside the archive. + */ +async function archivedFixtureFiles(archivedFiles: string[]) { + const filepaths: string[] = []; + for (const archivedFile of archivedFiles) { + const filepath = path.join(__fixtures, archivedFile); + const [, waitForExit] = await spawn( + { + cwd: __fixtures, + }, + 'tar', + '-xz', + '-f', + archivedFile + '.tar.gz', + '-C', + path.dirname(filepath), + ); + await waitForExit; + filepaths.push(filepath); + } + return { + async [Symbol.asyncDispose]() { + await Promise.all(filepaths.map((filepath) => fs.unlink(filepath))); + }, + filepaths, + }; +} diff --git a/tsconfig.json b/tsconfig.json index 0652a4fcf..1dd0fa0cd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -81,6 +81,8 @@ // paths starting with ~ are used by published packages and will be bundled "~internal/env": ["./internal/env/src/index.ts"], "~internal/env/node": ["./internal/env/src/node.ts"] + // INTENTIONAL: the heapsnapshot package needs to be built to be used (because of the worker script) + // "@internal/heapsnapshot": ["./internal/heapsnapshot/src/index.ts"] } }, "include": [ @@ -101,6 +103,12 @@ "exclude": [ "./packages/importer/tests/fixtures/syntax-error.ts", "./e2e/config-syntax-error/gateway.config.ts", - "./e2e/config-syntax-error/custom-resolvers.ts" + "./e2e/config-syntax-error/custom-resolvers.ts", + "./internal/heapsnapshot" + ], + "references": [ + { + "path": "./internal/heapsnapshot/tsconfig.json" + } ] } diff --git a/yarn.lock b/yarn.lock index 3e03f6c66..3fcc8f4f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1048,7 +1048,23 @@ __metadata: languageName: node linkType: hard -"@azure/core-rest-pipeline@npm:^1.19.0, @azure/core-rest-pipeline@npm:^1.20.0": +"@azure/core-rest-pipeline@npm:^1.19.0": + version: 1.19.1 + resolution: "@azure/core-rest-pipeline@npm:1.19.1" + dependencies: + "@azure/abort-controller": "npm:^2.0.0" + "@azure/core-auth": "npm:^1.8.0" + "@azure/core-tracing": "npm:^1.0.1" + "@azure/core-util": "npm:^1.11.0" + "@azure/logger": "npm:^1.0.0" + http-proxy-agent: "npm:^7.0.0" + https-proxy-agent: "npm:^7.0.0" + tslib: "npm:^2.6.2" + checksum: 10c0/23f2cb9d08e9535bcdca3123aa89bcfe8c5b9d37d6e9c0f87fa8009a089989bebfa11f5bc6fd96ceb6acee210b00b754ec7e02f3f7ee8916e8593e9ef0d610b8 + languageName: node + linkType: hard + +"@azure/core-rest-pipeline@npm:^1.20.0": version: 1.20.0 resolution: "@azure/core-rest-pipeline@npm:1.20.0" dependencies: @@ -2469,9 +2485,11 @@ __metadata: linkType: hard "@babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.5.5": - version: 7.27.1 - resolution: "@babel/runtime@npm:7.27.1" - checksum: 10c0/530a7332f86ac5a7442250456823a930906911d895c0b743bf1852efc88a20a016ed4cd26d442d0ca40ae6d5448111e02a08dd638a4f1064b47d080e2875dc05 + version: 7.26.10 + resolution: "@babel/runtime@npm:7.26.10" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10c0/6dc6d88c7908f505c4f7770fb4677dfa61f68f659b943c2be1f2a99cb6680343462867abf2d49822adc435932919b36c77ac60125793e719ea8745f2073d3745 languageName: node linkType: hard @@ -3493,7 +3511,7 @@ __metadata: languageName: node linkType: hard -"@envelop/core@npm:5.3.0, @envelop/core@npm:^5.0.0, @envelop/core@npm:^5.1.0, @envelop/core@npm:^5.2.3, @envelop/core@npm:^5.3.0": +"@envelop/core@npm:5.3.0, @envelop/core@npm:^5.3.0": version: 5.3.0 resolution: "@envelop/core@npm:5.3.0" dependencies: @@ -3505,6 +3523,18 @@ __metadata: languageName: node linkType: hard +"@envelop/core@npm:^5.0.0, @envelop/core@npm:^5.1.0, @envelop/core@npm:^5.2.3": + version: 5.2.3 + resolution: "@envelop/core@npm:5.2.3" + dependencies: + "@envelop/instrumentation": "npm:^1.0.0" + "@envelop/types": "npm:^5.2.1" + "@whatwg-node/promise-helpers": "npm:^1.2.4" + tslib: "npm:^2.5.0" + checksum: 10c0/77ba5807ddee5d08d6a4f47b2787735f0ad5aef71dcd809d51f5004f937de4c6a0b9a32651f2c6b81a0b9ef0510a917af408813c485e93da151c91d33b453061 + languageName: node + linkType: hard + "@envelop/disable-introspection@npm:^8.0.0": version: 8.0.0 resolution: "@envelop/disable-introspection@npm:8.0.0" @@ -5312,16 +5342,16 @@ __metadata: linkType: hard "@graphql-tools/load@npm:^8.0.1": - version: 8.1.1 - resolution: "@graphql-tools/load@npm:8.1.1" + version: 8.0.19 + resolution: "@graphql-tools/load@npm:8.0.19" dependencies: - "@graphql-tools/schema": "npm:^10.0.24" - "@graphql-tools/utils": "npm:^10.9.0" + "@graphql-tools/schema": "npm:^10.0.23" + "@graphql-tools/utils": "npm:^10.8.6" p-limit: "npm:3.1.0" tslib: "npm:^2.4.0" peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10c0/571435e5c39740855efac46c2d56e4337fbc97227a25f2a42d9db92d4fb0775ddf02d046303116a8e322644fddedfc57a7a47d9f24bd5e53dde6d05a16e71d86 + checksum: 10c0/c83dbd3a6cff3784ce3d2e2c4b2381f9af7469d0ff474d175d180fa9b7d14a25e61d1d75134f8049501523ac1e79261e4d07c6d96c0afab1e5023391df522cc3 languageName: node linkType: hard @@ -5415,7 +5445,7 @@ __metadata: languageName: node linkType: hard -"@graphql-tools/schema@npm:^10.0.11, @graphql-tools/schema@npm:^10.0.23, @graphql-tools/schema@npm:^10.0.24, @graphql-tools/schema@npm:^10.0.5": +"@graphql-tools/schema@npm:^10.0.11, @graphql-tools/schema@npm:^10.0.23, @graphql-tools/schema@npm:^10.0.5": version: 10.0.24 resolution: "@graphql-tools/schema@npm:10.0.24" dependencies: @@ -6046,12 +6076,22 @@ __metadata: languageName: unknown linkType: soft +"@internal/heapsnapshot@workspace:internal/heapsnapshot": + version: 0.0.0-use.local + resolution: "@internal/heapsnapshot@workspace:internal/heapsnapshot" + dependencies: + "@tsconfig/node18": "npm:^18.2.4" + "@tsconfig/recommended": "npm:^1.0.8" + "@types/node": "npm:^22.15.30" + pkgroll: "npm:2.14.5" + typescript: "npm:5.8.3" + languageName: unknown + linkType: soft + "@internal/perf@workspace:internal/perf": version: 0.0.0-use.local resolution: "@internal/perf@workspace:internal/perf" dependencies: - "@memlab/core": "npm:^1.1.42" - "@memlab/heap-analysis": "npm:^1.0.39" "@types/k6": "npm:^1.1.1" "@types/ws": "npm:^8.5.12" "@whatwg-node/promise-helpers": "npm:^1.3.0" @@ -6398,7 +6438,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.2, @jridgewell/gen-mapping@npm:^0.3.5": +"@jridgewell/gen-mapping@npm:^0.3.12": version: 0.3.12 resolution: "@jridgewell/gen-mapping@npm:0.3.12" dependencies: @@ -6408,6 +6448,17 @@ __metadata: languageName: node linkType: hard +"@jridgewell/gen-mapping@npm:^0.3.2, @jridgewell/gen-mapping@npm:^0.3.5": + version: 0.3.8 + resolution: "@jridgewell/gen-mapping@npm:0.3.8" + dependencies: + "@jridgewell/set-array": "npm:^1.2.1" + "@jridgewell/sourcemap-codec": "npm:^1.4.10" + "@jridgewell/trace-mapping": "npm:^0.3.24" + checksum: 10c0/c668feaf86c501d7c804904a61c23c67447b2137b813b9ce03eca82cb9d65ac7006d766c218685d76e3d72828279b6ee26c347aa1119dab23fbaf36aed51585a + languageName: node + linkType: hard + "@jridgewell/resolve-uri@npm:^3.0.3, @jridgewell/resolve-uri@npm:^3.1.0": version: 3.1.2 resolution: "@jridgewell/resolve-uri@npm:3.1.2" @@ -6415,6 +6466,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/set-array@npm:^1.2.1": + version: 1.2.1 + resolution: "@jridgewell/set-array@npm:1.2.1" + checksum: 10c0/2a5aa7b4b5c3464c895c802d8ae3f3d2b92fcbe84ad12f8d0bfbb1f5ad006717e7577ee1fd2eac00c088abe486c7adb27976f45d2941ff6b0b92b2c3302c60f4 + languageName: node + linkType: hard + "@jridgewell/source-map@npm:^0.3.3": version: 0.3.6 resolution: "@jridgewell/source-map@npm:0.3.6" @@ -7636,6 +7694,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm-eabi@npm:4.37.0": + version: 4.37.0 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.37.0" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@rollup/rollup-android-arm-eabi@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-android-arm-eabi@npm:4.44.0" @@ -7643,6 +7708,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm64@npm:4.37.0": + version: 4.37.0 + resolution: "@rollup/rollup-android-arm64@npm:4.37.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-android-arm64@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-android-arm64@npm:4.44.0" @@ -7650,6 +7722,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-arm64@npm:4.37.0": + version: 4.37.0 + resolution: "@rollup/rollup-darwin-arm64@npm:4.37.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-arm64@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-darwin-arm64@npm:4.44.0" @@ -7657,6 +7736,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-x64@npm:4.37.0": + version: 4.37.0 + resolution: "@rollup/rollup-darwin-x64@npm:4.37.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-x64@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-darwin-x64@npm:4.44.0" @@ -7664,6 +7750,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-freebsd-arm64@npm:4.37.0": + version: 4.37.0 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.37.0" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-freebsd-arm64@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-freebsd-arm64@npm:4.44.0" @@ -7671,6 +7764,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-freebsd-x64@npm:4.37.0": + version: 4.37.0 + resolution: "@rollup/rollup-freebsd-x64@npm:4.37.0" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-freebsd-x64@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-freebsd-x64@npm:4.44.0" @@ -7678,6 +7778,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm-gnueabihf@npm:4.37.0": + version: 4.37.0 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.37.0" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-arm-gnueabihf@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.44.0" @@ -7685,6 +7792,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm-musleabihf@npm:4.37.0": + version: 4.37.0 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.37.0" + conditions: os=linux & cpu=arm & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-arm-musleabihf@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.44.0" @@ -7692,6 +7806,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-gnu@npm:4.37.0": + version: 4.37.0 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.37.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-gnu@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.44.0" @@ -7699,6 +7820,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-musl@npm:4.37.0": + version: 4.37.0 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.37.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-musl@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-linux-arm64-musl@npm:4.44.0" @@ -7706,6 +7834,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-loongarch64-gnu@npm:4.37.0": + version: 4.37.0 + resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.37.0" + conditions: os=linux & cpu=loong64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-loongarch64-gnu@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.44.0" @@ -7713,6 +7848,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-powerpc64le-gnu@npm:4.37.0": + version: 4.37.0 + resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.37.0" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-powerpc64le-gnu@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.44.0" @@ -7720,6 +7862,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-riscv64-gnu@npm:4.37.0": + version: 4.37.0 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.37.0" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-riscv64-gnu@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.44.0" @@ -7727,6 +7876,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-riscv64-musl@npm:4.37.0": + version: 4.37.0 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.37.0" + conditions: os=linux & cpu=riscv64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-riscv64-musl@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.44.0" @@ -7734,6 +7890,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-s390x-gnu@npm:4.37.0": + version: 4.37.0 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.37.0" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-s390x-gnu@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.44.0" @@ -7741,6 +7904,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-gnu@npm:4.37.0": + version: 4.37.0 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.37.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-gnu@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-linux-x64-gnu@npm:4.44.0" @@ -7748,6 +7918,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-musl@npm:4.37.0": + version: 4.37.0 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.37.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-musl@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-linux-x64-musl@npm:4.44.0" @@ -7755,6 +7932,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-arm64-msvc@npm:4.37.0": + version: 4.37.0 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.37.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-win32-arm64-msvc@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.44.0" @@ -7762,6 +7946,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-ia32-msvc@npm:4.37.0": + version: 4.37.0 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.37.0" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@rollup/rollup-win32-ia32-msvc@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.44.0" @@ -7769,6 +7960,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-x64-msvc@npm:4.37.0": + version: 4.37.0 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.37.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-win32-x64-msvc@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-win32-x64-msvc@npm:4.44.0" @@ -8500,6 +8698,13 @@ __metadata: languageName: node linkType: hard +"@tsconfig/recommended@npm:^1.0.8": + version: 1.0.10 + resolution: "@tsconfig/recommended@npm:1.0.10" + checksum: 10c0/402ef03da7300ea0f9f9d6e0507dcb8782c6b764c1f45b946a6c34f2bd3153571a1a85f10659606c24d1052aed699c3e32dd0da876e8f7eea577374eeeb80d17 + languageName: node + linkType: hard + "@tsconfig/strictest@npm:2.0.5": version: 2.0.5 resolution: "@tsconfig/strictest@npm:2.0.5" @@ -8690,6 +8895,13 @@ __metadata: languageName: node linkType: hard +"@types/estree@npm:1.0.6": + version: 1.0.6 + resolution: "@types/estree@npm:1.0.6" + checksum: 10c0/cdfd751f6f9065442cd40957c07fd80361c962869aa853c1c2fd03e101af8b9389d8ff4955a43a6fcfa223dd387a089937f95be0f3eec21ca527039fd2d9859a + languageName: node + linkType: hard + "@types/express-serve-static-core@npm:^4.17.30, @types/express-serve-static-core@npm:^4.17.33": version: 4.19.6 resolution: "@types/express-serve-static-core@npm:4.19.6" @@ -8895,11 +9107,11 @@ __metadata: linkType: hard "@types/node@npm:*, @types/node@npm:>=13.7.0": - version: 24.0.3 - resolution: "@types/node@npm:24.0.3" + version: 22.13.13 + resolution: "@types/node@npm:22.13.13" dependencies: - undici-types: "npm:~7.8.0" - checksum: 10c0/9c3c4e87600d1cf11e291c2fd4bfd806a615455463c30a0ef6dc9c801b3423344d9b82b8084e3ccabce485a7421ebb61a66e9676181bd7d9aea4759998a120d5 + undici-types: "npm:~6.20.0" + checksum: 10c0/daf792ba5dcff1316abf4b33680f94b792f8d54d6ae495efc8929531e0ba1284a248d29aab117d2259f9280284d986ad5799b193b0516e2b926d713aab835f7d languageName: node linkType: hard @@ -10077,7 +10289,16 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.11.0, acorn@npm:^8.14.0, acorn@npm:^8.15.0, acorn@npm:^8.4.1": +"acorn@npm:^8.11.0, acorn@npm:^8.14.0, acorn@npm:^8.4.1": + version: 8.14.1 + resolution: "acorn@npm:8.14.1" + bin: + acorn: bin/acorn + checksum: 10c0/dbd36c1ed1d2fa3550140000371fcf721578095b18777b85a79df231ca093b08edc6858d75d6e48c73e431c174dcf9214edbd7e6fa5911b93bd8abfa54e47123 + languageName: node + linkType: hard + +"acorn@npm:^8.15.0": version: 8.15.0 resolution: "acorn@npm:8.15.0" bin: @@ -11953,27 +12174,27 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:4.4.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.3.7, debug@npm:^4.4.0, debug@npm:^4.4.1": - version: 4.4.1 - resolution: "debug@npm:4.4.1" +"debug@npm:4, debug@npm:4.4.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.4.0": + version: 4.4.0 + resolution: "debug@npm:4.4.0" dependencies: ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10c0/d2b44bc1afd912b49bb7ebb0d50a860dc93a4dd7d946e8de94abc957bb63726b7dd5aa48c18c2386c379ec024c46692e15ed3ed97d481729f929201e671fcd55 + checksum: 10c0/db94f1a182bf886f57b4755f85b3a74c39b5114b9377b7ab375dc2cfa3454f09490cc6c30f829df3fc8042bc8b8995f6567ce5cd96f3bc3688bd24027197d9de languageName: node linkType: hard -"debug@npm:4.4.0": - version: 4.4.0 - resolution: "debug@npm:4.4.0" +"debug@npm:4.4.1, debug@npm:^4.3.7, debug@npm:^4.4.1": + version: 4.4.1 + resolution: "debug@npm:4.4.1" dependencies: ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10c0/db94f1a182bf886f57b4755f85b3a74c39b5114b9377b7ab375dc2cfa3454f09490cc6c30f829df3fc8042bc8b8995f6567ce5cd96f3bc3688bd24027197d9de + checksum: 10c0/d2b44bc1afd912b49bb7ebb0d50a860dc93a4dd7d946e8de94abc957bb63726b7dd5aa48c18c2386c379ec024c46692e15ed3ed97d481729f929201e671fcd55 languageName: node linkType: hard @@ -13405,7 +13626,7 @@ __metadata: languageName: node linkType: hard -"fast-xml-parser@npm:5.2.5, fast-xml-parser@npm:^5.0.0": +"fast-xml-parser@npm:5.2.5": version: 5.2.5 resolution: "fast-xml-parser@npm:5.2.5" dependencies: @@ -13416,6 +13637,17 @@ __metadata: languageName: node linkType: hard +"fast-xml-parser@npm:^5.0.0": + version: 5.0.9 + resolution: "fast-xml-parser@npm:5.0.9" + dependencies: + strnum: "npm:^2.0.5" + bin: + fxparser: src/cli/cli.js + checksum: 10c0/29aaa74cb5224ddf755c2777fefce41961514fb525ce153ba9a8cbfd03292c93b67c0c19f3f4fdb5d8fa96a4b70c42dc31504eefc6477a668cea71a11999bc45 + languageName: node + linkType: hard + "fastify@npm:5.4.0, fastify@npm:^5.4.0": version: 5.4.0 resolution: "fastify@npm:5.4.0" @@ -14036,7 +14268,7 @@ __metadata: languageName: node linkType: hard -"get-tsconfig@npm:^4.10.1, get-tsconfig@npm:^4.7.5, get-tsconfig@npm:^4.7.6, get-tsconfig@npm:^4.8.1": +"get-tsconfig@npm:^4.10.1": version: 4.10.1 resolution: "get-tsconfig@npm:4.10.1" dependencies: @@ -14045,6 +14277,15 @@ __metadata: languageName: node linkType: hard +"get-tsconfig@npm:^4.7.5, get-tsconfig@npm:^4.7.6, get-tsconfig@npm:^4.8.1": + version: 4.10.0 + resolution: "get-tsconfig@npm:4.10.0" + dependencies: + resolve-pkg-maps: "npm:^1.0.0" + checksum: 10c0/c9b5572c5118923c491c04285c73bd55b19e214992af957c502a3be0fc0043bb421386ffd45ca3433c0a7fba81221ca300479e8393960acf15d0ed4563f38a86 + languageName: node + linkType: hard + "get-uri@npm:^6.0.1": version: 6.0.4 resolution: "get-uri@npm:6.0.4" @@ -18753,6 +18994,13 @@ __metadata: languageName: node linkType: hard +"regenerator-runtime@npm:^0.14.0": + version: 0.14.1 + resolution: "regenerator-runtime@npm:0.14.1" + checksum: 10c0/1b16eb2c4bceb1665c89de70dcb64126a22bc8eb958feef3cd68fe11ac6d2a4899b5cd1b80b0774c7c03591dc57d16631a7f69d2daa2ec98100e2f29f7ec4cc4 + languageName: node + linkType: hard + "regexp.prototype.flags@npm:^1.5.1, regexp.prototype.flags@npm:^1.5.4": version: 1.5.4 resolution: "regexp.prototype.flags@npm:1.5.4" @@ -18985,7 +19233,7 @@ __metadata: languageName: node linkType: hard -"rollup@npm:4.44.0, rollup@npm:^4.18.1, rollup@npm:^4.34.9": +"rollup@npm:4.44.0, rollup@npm:^4.34.9": version: 4.44.0 resolution: "rollup@npm:4.44.0" dependencies: @@ -19060,6 +19308,81 @@ __metadata: languageName: node linkType: hard +"rollup@npm:^4.18.1": + version: 4.37.0 + resolution: "rollup@npm:4.37.0" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.37.0" + "@rollup/rollup-android-arm64": "npm:4.37.0" + "@rollup/rollup-darwin-arm64": "npm:4.37.0" + "@rollup/rollup-darwin-x64": "npm:4.37.0" + "@rollup/rollup-freebsd-arm64": "npm:4.37.0" + "@rollup/rollup-freebsd-x64": "npm:4.37.0" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.37.0" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.37.0" + "@rollup/rollup-linux-arm64-gnu": "npm:4.37.0" + "@rollup/rollup-linux-arm64-musl": "npm:4.37.0" + "@rollup/rollup-linux-loongarch64-gnu": "npm:4.37.0" + "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.37.0" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.37.0" + "@rollup/rollup-linux-riscv64-musl": "npm:4.37.0" + "@rollup/rollup-linux-s390x-gnu": "npm:4.37.0" + "@rollup/rollup-linux-x64-gnu": "npm:4.37.0" + "@rollup/rollup-linux-x64-musl": "npm:4.37.0" + "@rollup/rollup-win32-arm64-msvc": "npm:4.37.0" + "@rollup/rollup-win32-ia32-msvc": "npm:4.37.0" + "@rollup/rollup-win32-x64-msvc": "npm:4.37.0" + "@types/estree": "npm:1.0.6" + fsevents: "npm:~2.3.2" + dependenciesMeta: + "@rollup/rollup-android-arm-eabi": + optional: true + "@rollup/rollup-android-arm64": + optional: true + "@rollup/rollup-darwin-arm64": + optional: true + "@rollup/rollup-darwin-x64": + optional: true + "@rollup/rollup-freebsd-arm64": + optional: true + "@rollup/rollup-freebsd-x64": + optional: true + "@rollup/rollup-linux-arm-gnueabihf": + optional: true + "@rollup/rollup-linux-arm-musleabihf": + optional: true + "@rollup/rollup-linux-arm64-gnu": + optional: true + "@rollup/rollup-linux-arm64-musl": + optional: true + "@rollup/rollup-linux-loongarch64-gnu": + optional: true + "@rollup/rollup-linux-powerpc64le-gnu": + optional: true + "@rollup/rollup-linux-riscv64-gnu": + optional: true + "@rollup/rollup-linux-riscv64-musl": + optional: true + "@rollup/rollup-linux-s390x-gnu": + optional: true + "@rollup/rollup-linux-x64-gnu": + optional: true + "@rollup/rollup-linux-x64-musl": + optional: true + "@rollup/rollup-win32-arm64-msvc": + optional: true + "@rollup/rollup-win32-ia32-msvc": + optional: true + "@rollup/rollup-win32-x64-msvc": + optional: true + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: 10c0/2e00382e08938636edfe0a7547ea2eaa027205dc0b6ff85d8b82be0fbe55a4ef88a1995fee2a5059e33dbccf12d1376c236825353afb89c96298cc95c5160a46 + languageName: node + linkType: hard + "router@npm:^2.2.0": version: 2.2.0 resolution: "router@npm:2.2.0" @@ -20047,6 +20370,13 @@ __metadata: languageName: node linkType: hard +"strnum@npm:^2.0.5": + version: 2.0.5 + resolution: "strnum@npm:2.0.5" + checksum: 10c0/856026ef65eaf15359d340a313ece25822b6472377b3029201b00f2657a1a3fa1cd7a7ce349dad35afdd00faf451344153dbb3d8478f082b7af8c17a64799ea6 + languageName: node + linkType: hard + "strnum@npm:^2.1.0": version: 2.1.1 resolution: "strnum@npm:2.1.1" @@ -20983,6 +21313,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.20.0": + version: 6.20.0 + resolution: "undici-types@npm:6.20.0" + checksum: 10c0/68e659a98898d6a836a9a59e6adf14a5d799707f5ea629433e025ac90d239f75e408e2e5ff086afc3cace26f8b26ee52155293564593fbb4a2f666af57fc59bf + languageName: node + linkType: hard + "undici-types@npm:~6.21.0": version: 6.21.0 resolution: "undici-types@npm:6.21.0" @@ -20990,13 +21327,6 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~7.8.0": - version: 7.8.0 - resolution: "undici-types@npm:7.8.0" - checksum: 10c0/9d9d246d1dc32f318d46116efe3cfca5a72d4f16828febc1918d94e58f6ffcf39c158aa28bf5b4fc52f410446bc7858f35151367bd7a49f21746cab6497b709b - languageName: node - linkType: hard - "undici@npm:^7.10.0": version: 7.12.0 resolution: "undici@npm:7.12.0"