@memlab/heap-analysis
Version: 
heap analysis plugins for memlab
719 lines (718 loc) • 25.9 kB
JavaScript
;
/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @format
 * @oncall memory_lab
 */
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const core_1 = require("@memlab/core");
const chalk_1 = __importDefault(require("chalk"));
const core_2 = require("@memlab/core");
const HeapConfig_1 = __importDefault(require("./HeapConfig"));
const nodeNameBlockList = new Set([
    '(Startup object cache)',
    '(Global handles)',
    '(External strings)',
    '(Builtins)',
]);
const nodeTypeBlockList = new Set([
    'array',
    'native',
    'code',
    'synthetic',
    'hidden',
]);
const defaultAnalysisArgs = { args: { _: [] } };
function isNodeWorthInspecting(node) {
    // exclude meta objects like GC roots etc.
    if (node.id <= 3) {
        return false;
    }
    if (nodeTypeBlockList.has(node.type)) {
        return false;
    }
    if (nodeNameBlockList.has(node.name)) {
        return false;
    }
    return true;
}
/**
 * filter out dominators that have a similar size, for example if
 * input is [A, B] and A is the dominator of B, then this function
 * throw away A if the size of A is close to the size of B
 * @param nodeList an array of heap nodes
 * @returns an array of heap nodes with dominators that have similar size removed
 */
function filterOutDominators(nodeList) {
    const candidateIdSet = new Set(nodeList.map(node => node.id));
    const childrenSizeInList = new Map();
    for (const node of nodeList) {
        const visitedIds = new Set();
        let curNode = node;
        inner: while (!visitedIds.has(curNode.id)) {
            curNode = node.dominatorNode;
            if (!curNode || curNode.id === node.id) {
                break inner;
            }
            // record the size of the children node in the candidate list
            // and associate the children size with its dominator in the candidate list
            if (candidateIdSet.has(curNode.id)) {
                let childrenSize = node.retainedSize;
                if (childrenSizeInList.has(curNode.id)) {
                    childrenSize += childrenSizeInList.get(curNode.id)[1];
                }
                childrenSizeInList.set(curNode.id, [
                    curNode.retainedSize,
                    childrenSize,
                ]);
                break inner;
            }
            visitedIds.add(curNode.id);
        }
    }
    // remove the dominator node from the candidate set
    // if the dominator node's size is similar to the child node
    for (const [dominatorId, [dominatorSize, childrenSize],] of childrenSizeInList) {
        if (dominatorSize - childrenSize < 500000) {
            candidateIdSet.delete(dominatorId);
        }
    }
    return nodeList.filter(node => candidateIdSet.has(node.id));
}
// Note: be cautious when using printRef = true, it may cause infinite loop
function getNodeRecord(node, printRef = false) {
    const refs = node.references.slice(0, MAX_NUM_OF_EDGES_TO_PRINT);
    return {
        id: node.id,
        name: node.name,
        type: node.type,
        selfsize: node.self_size,
        retainedSize: node.retainedSize,
        traceNodeId: node.trace_node_id,
        nodeIndex: node.nodeIndex,
        references: printRef
            ? refs.map(edge => getEdgeRecord(edge))
            : refs.map(edge => ({
                name: edge.name_or_index.toString(),
                toNode: edge.toNode.id,
            })),
        referrers: node.referrers.slice(0, MAX_NUM_OF_EDGES_TO_PRINT).map(edge => ({
            name: edge.name_or_index.toString(),
            fromNode: edge.fromNode.id,
        })),
    };
}
function getEdgeRecord(edge) {
    return {
        nameOrIndex: edge.name_or_index,
        type: edge.type,
        edgeIndex: edge.edgeIndex,
        toNode: getNodeRecord(edge.toNode),
        fromNode: getNodeRecord(edge.fromNode),
    };
}
// Note: be cautious when setting printReferences to true,
// it may cause infinite loop
function printNodeListInTerminal(nodeList, options = {}) {
    const dot = chalk_1.default.grey('· ');
    const indent = options.indent || '';
    const printRef = !!options.printReferences;
    if (!options.printAll) {
        nodeList = filterOutDominators(nodeList);
    }
    if (core_1.config.outputFormat === core_1.OutputFormat.Json) {
        const jsonNodes = nodeList.map(node => getNodeRecord(node, printRef));
        core_2.info.writeOutput(JSON.stringify(jsonNodes));
        core_2.info.writeOutput('\n');
        return;
    }
    for (const node of nodeList) {
        const nodeInfo = getHeapObjectString(node);
        core_2.info.topLevel(`${indent}${dot}${nodeInfo}`);
        if (printRef) {
            printReferencesInTerminal(node.references, { indent: indent + '  ' });
        }
    }
}
function printNodeInTerminal(node) {
    const nodeRecord = getNodeRecord(node);
    core_2.info.writeOutput(JSON.stringify(nodeRecord));
    core_2.info.writeOutput('\n');
}
function isNumeric(v) {
    if (typeof v === 'number') {
        return true;
    }
    if (parseInt(v, 10) + '' === v + '') {
        return true;
    }
    if (parseFloat(v) + '' === v + '') {
        return true;
    }
    return false;
}
function getObjectReferenceNames(node) {
    const referrers = node.referrers;
    const names = new Set();
    const visited = new Set();
    for (const edge of referrers) {
        const name = edge.name_or_index;
        // in case infinite loop
        if (visited.has(edge.edgeIndex)) {
            continue;
        }
        visited.add(edge.edgeIndex);
        // numeric index is not informative
        if (isNumeric(name) || name === '') {
            continue;
        }
        // context and previous references are not informative
        if ((name === 'previous' || name === 'context') &&
            edge.type === 'internal') {
            for (const ref of edge.fromNode.referrers) {
                referrers.push(ref);
            }
            continue;
        }
        names.add(name);
    }
    const refs = Array.from(names).slice(0, 10).join(chalk_1.default.grey(', '));
    return 'refs: ' + chalk_1.default.grey('[') + refs + chalk_1.default.grey(']');
}
function getHeapObjectString(node) {
    const colon = chalk_1.default.grey(':');
    const comma = chalk_1.default.grey(',');
    const serializeOpt = { color: true, compact: true };
    const shapeStr = core_2.serializer.summarizeNodeShape(node, serializeOpt);
    const edgeCount = getObjectOutgoingEdgeCount(node);
    const fanout = `${edgeCount} edges`;
    const bytes = core_2.utils.getReadableBytes(node.retainedSize);
    const nodeId = chalk_1.default.grey(`@${node.id}`);
    const type = node.type === 'object' ? '' : ` ${node.type}`;
    const refs = getObjectReferenceNames(node);
    return (`${nodeId}${type} ${shapeStr}${colon} ` +
        `${fanout}${comma} ${bytes}${comma} ${refs}`);
}
const MAX_NUM_OF_EDGES_TO_PRINT = 50;
function getReferenceString(edge) {
    const edgeName = chalk_1.default.green(edge.name_or_index);
    const objectInfo = getHeapObjectString(edge.toNode);
    return ` --${edgeName}--> ${objectInfo}`;
}
function printReferencesInTerminal(edgeList, options = {}) {
    if (core_1.config.outputFormat === core_1.OutputFormat.Json) {
        printEdgesJson(edgeList);
        return;
    }
    const dot = chalk_1.default.grey('· ');
    const indent = options.indent || '';
    let n = 0;
    for (const edge of edgeList) {
        if (!core_1.config.verbose && n >= MAX_NUM_OF_EDGES_TO_PRINT) {
            break;
        }
        ++n;
        const refStr = getReferenceString(edge);
        core_2.info.topLevel(`${indent}${dot}${refStr}`);
    }
    if (n < edgeList.length) {
        core_2.info.lowLevel(`${edgeList.length - n} more references...`);
    }
}
function getReferrerString(edge) {
    const edgeName = chalk_1.default.green(edge.name_or_index);
    const objectInfo = getHeapObjectString(edge.fromNode);
    return ` ${objectInfo} --${edgeName}--> `;
}
function printReferrersInTerminal(edgeList, options = {}) {
    if (core_1.config.outputFormat === core_1.OutputFormat.Json) {
        printEdgesJson(edgeList);
        return;
    }
    const dot = chalk_1.default.grey('· ');
    const indent = options.indent || '';
    let n = 0;
    for (const edge of edgeList) {
        if (!core_1.config.verbose && n >= MAX_NUM_OF_EDGES_TO_PRINT) {
            break;
        }
        ++n;
        const refStr = getReferrerString(edge);
        core_2.info.topLevel(`${indent}${dot}${refStr}`);
    }
    if (n < edgeList.length) {
        core_2.info.lowLevel(`${edgeList.length - n} more referrers...`);
    }
}
function printEdgesJson(edgeList) {
    const jsonEdges = edgeList.map(edge => getEdgeRecord(edge));
    core_2.info.writeOutput(JSON.stringify(jsonEdges));
    core_2.info.writeOutput('\n');
}
function getObjectOutgoingEdgeCount(node) {
    if (node.name === 'Set' || node.name === 'Map') {
        const edge = core_2.utils.getEdgeByNameAndType(node, 'table');
        if (!edge) {
            return node.edge_count;
        }
        return edge.toNode.edge_count;
    }
    return node.edge_count;
}
/**
 * Get the heap snapshot file's absolute path passed to the hosting heap
 * analysis via `HeapAnalysisOptions`.
 *
 * This API is supposed to be used within the overridden `process` method
 * of an `BaseAnalysis` instance.
 *
 * @param options this is the auto-generated input passed to all the `BaseAnalysis` instances
 * @returns the absolute path of the heap snapshot file
 * * **Examples:**
 * ```typescript
 * import type {IHeapSnapshot} from '@memlab/core';
 * import type {HeapAnalysisOptions} from '@memlab/heap-analysis';
 * import {getSnapshotFileForAnalysis, BaseAnalysis} from '@memlab/heap-analysis';
 *
 * class ExampleAnalysis extends BaseAnalysis {
 *   public getCommandName(): string {
 *     return 'example-analysis';
 *   }
 *
 *   public getDescription(): string {
 *     return 'an example analysis for demo';
 *   }
 *
 *   async process(options: HeapAnalysisOptions): Promise<void> {
 *     const file = getSnapshotFileForAnalysis(options);
 *   }
 * }
 * ```
 *
 * Use the following code to invoke the heap analysis:
 * ```typescript
 * const analysis = new ExampleAnalysis();
 * // any .heapsnapshot file recorded by memlab or saved manually from Chrome
 * await analysis.analyzeSnapshotFromFile(snapshotFile);
 * ```
 * The new heap analysis can also be used with {@link analyze}, in that case
 * `getSnapshotFileForAnalysis` will use the last heap snapshot in alphanumerically
 * ascending order from {@link BrowserInteractionResultReader}.
 */
function getSnapshotFileForAnalysis(options) {
    const args = options.args;
    if (args.snapshot) {
        return args.snapshot;
    }
    return core_2.utils.getSingleSnapshotFileForAnalysis();
}
/**
 * Get the absolute path of the directory holding all the heap snapshot files
 * passed to the hosting heap analysis via `HeapAnalysisOptions`.
 *
 * This API is supposed to be used within the overridden `process` method
 * of an `BaseAnalysis` instance.
 *
 * @param options this is the auto-generated input passed
 * to all the `BaseAnalysis` instances
 * @returns the absolute path of the directory
 * * **Examples:**
 * ```typescript
 * import type {IHeapSnapshot} from '@memlab/core';
 * import type {HeapAnalysisOptions} from '@memlab/heap-analysis';
 * import {getSnapshotFileForAnalysis, BaseAnalysis} from '@memlab/heap-analysis';
 *
 * class ExampleAnalysis extends BaseAnalysis {
 *   public getCommandName(): string {
 *     return 'example-analysis';
 *   }
 *
 *   public getDescription(): string {
 *     return 'an example analysis for demo';
 *   }
 *
 *   async process(options: HeapAnalysisOptions): Promise<void> {
 *     const directory = getSnapshotDirForAnalysis(options);
 *   }
 * }
 * ```
 *
 * Use the following code to invoke the heap analysis:
 * ```typescript
 * const analysis = new ExampleAnalysis();
 * // any .heapsnapshot file recorded by memlab or saved manually from Chrome
 * await analysis.analyzeSnapshotFromFile(snapshotFile);
 * ```
 * The new heap analysis can also be used with {@link analyze}, in that case
 * `getSnapshotDirForAnalysis` use the snapshot directory from
 * {@link BrowserInteractionResultReader}.
 */
function getSnapshotDirForAnalysis(options) {
    const args = options.args;
    if (args['snapshot-dir']) {
        return args['snapshot-dir'];
    }
    if (core_1.config.externalSnapshotDir) {
        return core_1.config.externalSnapshotDir;
    }
    return null;
}
/**
 * Load the heap graph based on the single JavaScript heap snapshot
 * passed to the hosting heap analysis via `HeapAnalysisOptions`.
 *
 * This API is supposed to be used within the `process` implementation
 * of an `BaseAnalysis` instance.
 *
 * @param options this is the auto-generated input passed to all the `BaseAnalysis` instances
 * @returns the graph representation of the heap
 * * **Examples:**
 * ```typescript
 * import type {IHeapSnapshot} from '@memlab/core';
 * import type {HeapAnalysisOptions} from '@memlab/heap-analysis';
 * import {loadHeapSnapshot, BaseAnalysis} from '@memlab/heap-analysis';
 *
 * class ExampleAnalysis extends BaseAnalysis {
 *   public getCommandName(): string {
 *     return 'example-analysis';
 *   }
 *
 *   public getDescription(): string {
 *     return 'an example analysis for demo';
 *   }
 *
 *   async process(options: HeapAnalysisOptions): Promise<void> {
 *     const heap = await loadHeapSnapshot(options);
 *     // doing heap analysis
 *   }
 * }
 * ```
 *
 * Use the following code to invoke the heap analysis:
 * ```typescript
 * const analysis = new ExampleAnalysis();
 * // any .heapsnapshot file recorded by memlab or saved manually from Chrome
 * await analysis.analyzeSnapshotFromFile(snapshotFile);
 * ```
 * The new heap analysis can also be used with {@link analyze}, in that case
 * `loadHeapSnapshot` will use the last heap snapshot in alphanumerically
 * ascending order from {@link BrowserInteractionResultReader}.
 */
function loadHeapSnapshot(options) {
    return __awaiter(this, void 0, void 0, function* () {
        if (HeapConfig_1.default.isCliInteractiveMode) {
            if (!HeapConfig_1.default.currentHeap) {
                const file = getSnapshotFileForAnalysis(options);
                const heap = yield loadProcessedSnapshot({ file });
                HeapConfig_1.default.currentHeapFile = file;
                HeapConfig_1.default.currentHeap = heap;
            }
            return HeapConfig_1.default.currentHeap;
        }
        else {
            const file = getSnapshotFileForAnalysis(options);
            return loadProcessedSnapshot({ file });
        }
    });
}
/**
 * Load and parse a `.heapsnapshot` file and calculate meta data like
 * dominator nodes and retained sizes.
 * @param file the absolute path of the `.heapsnapshot` file
 * @returns the heap graph representation instance that supports querying
 * the heap
 * * **Examples**:
 * ```typescript
 * import {dumpNodeHeapSnapshot} from '@memlab/core';
 * import {getFullHeapFromFile} from '@memlab/heap-analysis';
 *
 * (async function (){
 *   const heapFile = dumpNodeHeapSnapshot();
 *   const heap = await getFullHeapFromFile(heapFile);
 * })();
 * ```
 */
function getFullHeapFromFile(file) {
    return __awaiter(this, void 0, void 0, function* () {
        return yield loadProcessedSnapshot({ file });
    });
}
/**
 * Take a heap snapshot of the current program state
 * and parse it as {@link IHeapSnapshot}. This
 * API also calculates some heap analysis meta data
 * for heap analysis. But this also means slower heap parsing
 * comparing with {@link takeNodeMinimalHeap}.
 *
 * @returns heap representation with heap analysis meta data.
 *
 * * **Examples:**
 * ```typescript
 * import type {IHeapSnapshot} from '@memlab/core';
 * import type {takeNodeFullHeap} from '@memlab/heap-analysis';
 *
 * (async function () {
 *   const heap: IHeapSnapshot = await takeNodeFullHeap();
 * })();
 * ```
 */
function takeNodeFullHeap() {
    return __awaiter(this, void 0, void 0, function* () {
        const heap = yield (0, core_1.takeNodeMinimalHeap)();
        core_2.analysis.preparePathFinder(heap);
        core_2.info.flush();
        return heap;
    });
}
/** @deprecated */
function getHeapFromFile(file) {
    return __awaiter(this, void 0, void 0, function* () {
        return yield loadProcessedSnapshot({ file });
    });
}
function loadProcessedSnapshot(options = {}) {
    return __awaiter(this, void 0, void 0, function* () {
        const opt = { buildNodeIdIndex: true, verbose: true };
        const file = options.file || core_2.utils.getSnapshotFilePathWithTabType(/.*/);
        const snapshot = yield core_2.utils.getSnapshotFromFile(file, opt);
        core_2.analysis.preparePathFinder(snapshot);
        core_2.info.flush();
        return snapshot;
    });
}
/**
 * When a heap analysis is taking multiple heap snapshots as input for memory
 * analysis (e.g., finding which object keeps growing in size in a series of
 * heap snapshots), this API could be used to do
 * [MapRedue](https://en.wikipedia.org/wiki/MapReduce) on all heap snapshots.
 *
 * This API is supposed to be used within the `process` implementation
 * of an `BaseAnalysis` instance that is designed to analyze multiple heap
 * snapshots (as an example, finding which object keeps growing overtime)
 *
 * @param mapCallback the map function in MapReduce, the function will be applied
 * to each heap snapshot
 * @param reduceCallback the reduce function in MapReduce, the function will take
 * as input all intermediate results from all map function calls
 * @typeParam T1 - the type of the intermediate result from each map function call
 * @typeParam T2 - the type of the final result of the reduce function call
 * @param options this is the auto-generated input passed to all the `BaseAnalysis` instances
 * @returns the return value of your reduce function
 * * **Examples:**
 * ```typescript
 * import type {IHeapSnapshot} from '@memlab/core';
 * import type {HeapAnalysisOptions} from '@memlab/heap-analysis';
 * import {snapshotMapReduce, BaseAnalysis} from '@memlab/heap-analysis';
 *
 * class ExampleAnalysis extends BaseAnalysis {
 *   public getCommandName(): string {
 *     return 'example-analysis';
 *   }
 *
 *   public getDescription(): string {
 *     return 'an example analysis for demo';
 *   }
 *
 *   async process(options: HeapAnalysisOptions): Promise<void> {
 *     // check if the number of heap objects keeps growing overtime
 *     const isMonotonicIncreasing = await snapshotMapReduce(
 *       (heap) => heap.nodes.length,
 *       (nodeCounts) =>
 *         nodeCounts[0] < nodeCounts[nodeCounts.length - 1] &&
 *         nodeCounts.every((count, i) => i === 0 || count >= nodeCounts[i - 1]),
 *       options,
 *     );
 *   }
 * }
 * ```
 *
 * Use the following code to invoke the heap analysis:
 * ```typescript
 * const analysis = new ExampleAnalysis();
 * // snapshotDir includes a series of .heapsnapshot files recorded by
 * // memlab or saved manually from Chrome, those files will be loaded
 * // in alphanumerically ascending order
 * await analysis.analyzeSnapshotsInDirectory(snapshotDir);
 * ```
 * The new heap analysis can also be used with {@link analyze}, in that case
 * `snapshotMapReduce` will use all the heap snapshot in alphanumerically
 * ascending order from {@link BrowserInteractionResultReader}.
 *
 * **Why not passing in all heap snapshots as an array of {@link IHeapSnapshot}s?**
 * Each heap snapshot could be non-trivial in size, loading them all at once
 * may not be possible.
 */
function snapshotMapReduce(mapCallback, reduceCallback, options) {
    return __awaiter(this, void 0, void 0, function* () {
        const snapshotDir = getSnapshotDirForAnalysis(options);
        core_2.utils.checkSnapshots({ snapshotDir });
        const snapshotFiles = snapshotDir
            ? // load snapshots from a directory
                core_2.utils.getSnapshotFilesInDir(snapshotDir)
            : // load snapshots based on the visit sequence meta data
                core_2.utils.getSnapshotFilesFromTabsOrder();
        const intermediateResults = [];
        for (let i = 0; i < snapshotFiles.length; ++i) {
            const file = snapshotFiles[i];
            // force GC before loading each snapshot
            if (global.gc) {
                global.gc();
            }
            const snapshot = yield loadProcessedSnapshot({ file });
            intermediateResults.push(mapCallback(snapshot, i, file));
        }
        return reduceCallback(intermediateResults);
    });
}
/**
 * This API aggregates metrics from the
 * [dominator nodes](https://firefox-source-docs.mozilla.org/devtools-user/memory/dominators/index.html)
 * of the set of input heap objects.
 *
 * @param ids Set of ids of heap objects (or nodes)
 * @param snapshot heap graph loaded from a heap snapshot
 * @param checkNodeCb filter callback to exclude some heap object/nodes
 * before calculating the dominator nodes
 * @param nodeMetricsCb callback to calculate metrics from each dominator node
 * @returns the aggregated metrics
 */
function aggregateDominatorMetrics(ids, snapshot, checkNodeCb, nodeMetricsCb) {
    let ret = 0;
    const dominators = core_2.utils.getConditionalDominatorIds(ids, snapshot, checkNodeCb);
    core_2.utils.applyToNodes(dominators, snapshot, node => {
        ret += nodeMetricsCb(node);
    });
    return ret;
}
/**
 * This API calculate the set of
 * [dominator nodes](https://firefox-source-docs.mozilla.org/devtools-user/memory/dominators/index.html)
 * of the set of input heap objects.
 * @param ids Set of ids of heap objects (or nodes)
 * @param snapshot heap loaded from a heap snapshot
 * @returns the set of dominator nodes/objects
 * * * **Examples**:
 * ```typescript
 * import {dumpNodeHeapSnapshot} from '@memlab/core';
 * import {getFullHeapFromFile, getDominatorNodes} from '@memlab/heap-analysis';
 *
 * class TestObject {}
 *
 * (async function () {
 *   const t1 = new TestObject();
 *   const t2 = new TestObject();
 *
 *   // dump the heap of this running JavaScript program
 *   const heapFile = dumpNodeHeapSnapshot();
 *   const heap = await getFullHeapFromFile(heapFile);
 *
 *   // find the heap node for TestObject
 *   let nodes = [];
 *   heap.nodes.forEach(node => {
 *     if (node.name === 'TestObject' && node.type === 'object') {
 *       nodes.push(node);
 *     }
 *   });
 *
 *   // get the dominator nodes
 *   const dominatorIds = getDominatorNodes(
 *     new Set(nodes.map(node => node.id)),
 *     heap,
 *   );
 * })();
 * ```
 */
function getDominatorNodes(ids, snapshot) {
    return core_2.utils.getConditionalDominatorIds(ids, snapshot, () => true);
}
function filterOutLargestObjects(snapshot, objectFilter, listSize = 50) {
    let largeObjects = [];
    snapshot.nodes.forEach(node => {
        if (!objectFilter(node)) {
            return;
        }
        const size = node.retainedSize;
        let i;
        for (i = largeObjects.length - 1; i >= 0; --i) {
            if (largeObjects[i].retainedSize >= size) {
                largeObjects.splice(i + 1, 0, node);
                break;
            }
        }
        if (i < 0) {
            largeObjects.unshift(node);
        }
        largeObjects = largeObjects.slice(0, listSize);
    });
    return largeObjects;
}
function calculateRetainedSizes(snapshot) {
    const finder = new core_2.TraceFinder();
    // dominator and retained size
    finder.calculateAllNodesRetainedSizes(snapshot);
}
function isCollectObject(node) {
    if (node.type !== 'object') {
        return false;
    }
    return (node.name === 'Map' ||
        node.name === 'Set' ||
        node.name === 'WeakMap' ||
        node.name === 'WeakSet');
}
function getCollectionFanout(node) {
    if (node.type !== 'object') {
        return 0;
    }
    if (node.name === 'Array') {
        return node.edge_count;
    }
    else if (node.name === 'Map' ||
        node.name === 'Set' ||
        node.name === 'WeakMap' ||
        node.name === 'WeakSet') {
        const table = node.getReferenceNode('table');
        if (table) {
            return table.edge_count;
        }
    }
    return 0;
}
exports.default = {
    aggregateDominatorMetrics,
    calculateRetainedSizes,
    defaultAnalysisArgs,
    filterOutLargestObjects,
    getCollectionFanout,
    getDominatorNodes,
    getObjectOutgoingEdgeCount,
    getSnapshotDirForAnalysis,
    getSnapshotFileForAnalysis,
    isCollectObject,
    isNodeWorthInspecting,
    loadHeapSnapshot,
    getHeapFromFile,
    getFullHeapFromFile,
    printNodeListInTerminal,
    printReferencesInTerminal,
    printReferrersInTerminal,
    printNodeInTerminal,
    snapshotMapReduce,
    takeNodeFullHeap,
};