/* eslint-disable node-core/require-common-first, node-core/required-modules */ 'use strict'; const assert = require('assert'); const util = require('util'); let internalBinding; try { internalBinding = require('internal/test/binding').internalBinding; } catch (e) { console.log('using `test/common/heap.js` requires `--expose-internals`'); throw e; } const { buildEmbedderGraph } = internalBinding('heap_utils'); const { getHeapSnapshot } = require('v8'); function createJSHeapSnapshot() { const stream = getHeapSnapshot(); stream.pause(); const dump = JSON.parse(stream.read()); const meta = dump.snapshot.meta; const nodes = readHeapInfo(dump.nodes, meta.node_fields, meta.node_types, dump.strings); const edges = readHeapInfo(dump.edges, meta.edge_fields, meta.edge_types, dump.strings); for (const node of nodes) { node.incomingEdges = []; node.outgoingEdges = []; } let fromNodeIndex = 0; let edgeIndex = 0; for (const { type, name_or_index, to_node } of edges) { while (edgeIndex === nodes[fromNodeIndex].edge_count) { edgeIndex = 0; fromNodeIndex++; } const toNode = nodes[to_node / meta.node_fields.length]; const fromNode = nodes[fromNodeIndex]; const edge = { type, to: toNode, from: fromNode, name: typeof name_or_index === 'string' ? name_or_index : null }; toNode.incomingEdges.push(edge); fromNode.outgoingEdges.push(edge); edgeIndex++; } for (const node of nodes) { assert.strictEqual(node.edge_count, node.outgoingEdges.length, `${node.edge_count} !== ${node.outgoingEdges.length}`); } return nodes; } function readHeapInfo(raw, fields, types, strings) { const items = []; for (let i = 0; i < raw.length; i += fields.length) { const item = {}; for (let j = 0; j < fields.length; j++) { const name = fields[j]; let type = types[j]; if (Array.isArray(type)) { item[name] = type[raw[i + j]]; } else if (name === 'name_or_index') { // type === 'string_or_number' if (item.type === 'element' || item.type === 'hidden') type = 'number'; else type = 'string'; } if (type === 'string') { item[name] = strings[raw[i + j]]; } else if (type === 'number' || type === 'node') { item[name] = raw[i + j]; } } items.push(item); } return items; } function inspectNode(snapshot) { return util.inspect(snapshot, { depth: 4 }); } function isEdge(edge, { node_name, edge_name }) { if (edge.name !== edge_name) { return false; } // From our internal embedded graph if (edge.to.value) { if (edge.to.value.constructor.name !== node_name) { return false; } } else if (edge.to.name !== node_name) { return false; } return true; } class State { constructor() { this.snapshot = createJSHeapSnapshot(); this.embedderGraph = buildEmbedderGraph(); } // Validate the v8 heap snapshot validateSnapshot(rootName, expected, { loose = false } = {}) { const rootNodes = this.snapshot.filter( (node) => node.name === rootName && node.type !== 'string'); if (loose) { assert(rootNodes.length >= expected.length, `Expect to find at least ${expected.length} '${rootName}', ` + `found ${rootNodes.length}`); } else { assert.strictEqual( rootNodes.length, expected.length, `Expect to find ${expected.length} '${rootName}', ` + `found ${rootNodes.length}`); } for (const expectation of expected) { if (expectation.children) { for (const expectedEdge of expectation.children) { const check = typeof expectedEdge === 'function' ? expectedEdge : (edge) => (isEdge(edge, expectedEdge)); const hasChild = rootNodes.some( (node) => node.outgoingEdges.some(check) ); // Don't use assert with a custom message here. Otherwise the // inspection in the message is done eagerly and wastes a lot of CPU // time. if (!hasChild) { throw new Error( 'expected to find child ' + `${util.inspect(expectedEdge)} in ${inspectNode(rootNodes)}`); } } } } } // Validate our internal embedded graph representation validateGraph(rootName, expected, { loose = false } = {}) { const rootNodes = this.embedderGraph.filter( (node) => node.name === rootName ); if (loose) { assert(rootNodes.length >= expected.length, `Expect to find at least ${expected.length} '${rootName}', ` + `found ${rootNodes.length}`); } else { assert.strictEqual( rootNodes.length, expected.length, `Expect to find ${expected.length} '${rootName}', ` + `found ${rootNodes.length}`); } for (const expectation of expected) { if (expectation.children) { for (const expectedEdge of expectation.children) { const check = typeof expectedEdge === 'function' ? expectedEdge : (edge) => (isEdge(edge, expectedEdge)); // Don't use assert with a custom message here. Otherwise the // inspection in the message is done eagerly and wastes a lot of CPU // time. const hasChild = rootNodes.some( (node) => node.edges.some(check) ); if (!hasChild) { throw new Error( 'expected to find child ' + `${util.inspect(expectedEdge)} in ${inspectNode(rootNodes)}`); } } } } } validateSnapshotNodes(rootName, expected, { loose = false } = {}) { this.validateSnapshot(rootName, expected, { loose }); this.validateGraph(rootName, expected, { loose }); } } function recordState() { return new State(); } function validateSnapshotNodes(...args) { return recordState().validateSnapshotNodes(...args); } module.exports = { recordState, validateSnapshotNodes };