1'use strict'; 2const assert = require('assert'); 3const util = require('util'); 4 5let internalBinding; 6try { 7 internalBinding = require('internal/test/binding').internalBinding; 8} catch (e) { 9 console.log('using `test/common/heap.js` requires `--expose-internals`'); 10 throw e; 11} 12 13const { buildEmbedderGraph } = internalBinding('heap_utils'); 14const { getHeapSnapshot } = require('v8'); 15 16function createJSHeapSnapshot(stream = getHeapSnapshot()) { 17 stream.pause(); 18 const dump = JSON.parse(stream.read()); 19 const meta = dump.snapshot.meta; 20 21 const nodes = 22 readHeapInfo(dump.nodes, meta.node_fields, meta.node_types, dump.strings); 23 const edges = 24 readHeapInfo(dump.edges, meta.edge_fields, meta.edge_types, dump.strings); 25 26 for (const node of nodes) { 27 node.incomingEdges = []; 28 node.outgoingEdges = []; 29 } 30 31 let fromNodeIndex = 0; 32 let edgeIndex = 0; 33 for (const { type, name_or_index, to_node } of edges) { 34 while (edgeIndex === nodes[fromNodeIndex].edge_count) { 35 edgeIndex = 0; 36 fromNodeIndex++; 37 } 38 const toNode = nodes[to_node / meta.node_fields.length]; 39 const fromNode = nodes[fromNodeIndex]; 40 const edge = { 41 type, 42 to: toNode, 43 from: fromNode, 44 name: typeof name_or_index === 'string' ? name_or_index : null, 45 }; 46 toNode.incomingEdges.push(edge); 47 fromNode.outgoingEdges.push(edge); 48 edgeIndex++; 49 } 50 51 for (const node of nodes) { 52 assert.strictEqual(node.edge_count, node.outgoingEdges.length, 53 `${node.edge_count} !== ${node.outgoingEdges.length}`); 54 } 55 return nodes; 56} 57 58function readHeapInfo(raw, fields, types, strings) { 59 const items = []; 60 61 for (let i = 0; i < raw.length; i += fields.length) { 62 const item = {}; 63 for (let j = 0; j < fields.length; j++) { 64 const name = fields[j]; 65 let type = types[j]; 66 if (Array.isArray(type)) { 67 item[name] = type[raw[i + j]]; 68 } else if (name === 'name_or_index') { // type === 'string_or_number' 69 if (item.type === 'element' || item.type === 'hidden') 70 type = 'number'; 71 else 72 type = 'string'; 73 } 74 75 if (type === 'string') { 76 item[name] = strings[raw[i + j]]; 77 } else if (type === 'number' || type === 'node') { 78 item[name] = raw[i + j]; 79 } 80 } 81 items.push(item); 82 } 83 84 return items; 85} 86 87function inspectNode(snapshot) { 88 return util.inspect(snapshot, { depth: 4 }); 89} 90 91function isEdge(edge, { node_name, edge_name }) { 92 if (edge_name !== undefined && edge.name !== edge_name) { 93 return false; 94 } 95 // From our internal embedded graph 96 if (edge.to.value) { 97 if (edge.to.value.constructor.name !== node_name) { 98 return false; 99 } 100 } else if (edge.to.name !== node_name) { 101 return false; 102 } 103 return true; 104} 105 106class State { 107 constructor(stream) { 108 this.snapshot = createJSHeapSnapshot(stream); 109 this.embedderGraph = buildEmbedderGraph(); 110 } 111 112 // Validate the v8 heap snapshot 113 validateSnapshot(rootName, expected, { loose = false } = {}) { 114 const rootNodes = this.snapshot.filter( 115 (node) => node.name === rootName && node.type !== 'string'); 116 if (loose) { 117 assert(rootNodes.length >= expected.length, 118 `Expect to find at least ${expected.length} '${rootName}', ` + 119 `found ${rootNodes.length}`); 120 } else { 121 assert.strictEqual( 122 rootNodes.length, expected.length, 123 `Expect to find ${expected.length} '${rootName}', ` + 124 `found ${rootNodes.length}`); 125 } 126 127 for (const expectation of expected) { 128 if (expectation.children) { 129 for (const expectedEdge of expectation.children) { 130 const check = typeof expectedEdge === 'function' ? expectedEdge : 131 (edge) => (isEdge(edge, expectedEdge)); 132 const hasChild = rootNodes.some( 133 (node) => node.outgoingEdges.some(check), 134 ); 135 // Don't use assert with a custom message here. Otherwise the 136 // inspection in the message is done eagerly and wastes a lot of CPU 137 // time. 138 if (!hasChild) { 139 throw new Error( 140 'expected to find child ' + 141 `${util.inspect(expectedEdge)} in ${inspectNode(rootNodes)}`); 142 } 143 } 144 } 145 146 if (expectation.detachedness !== undefined) { 147 const matchedNodes = rootNodes.filter( 148 (node) => node.detachedness === expectation.detachedness); 149 if (loose) { 150 assert(matchedNodes.length >= rootNodes.length, 151 `Expect to find at least ${rootNodes.length} with ` + 152 `detachedness ${expectation.detachedness}, ` + 153 `found ${matchedNodes.length}`); 154 } else { 155 assert.strictEqual( 156 matchedNodes.length, rootNodes.length, 157 `Expect to find ${rootNodes.length} with detachedness ` + 158 `${expectation.detachedness}, found ${matchedNodes.length}`); 159 } 160 } 161 } 162 } 163 164 // Validate our internal embedded graph representation 165 validateGraph(rootName, expected, { loose = false } = {}) { 166 const rootNodes = this.embedderGraph.filter( 167 (node) => node.name === rootName, 168 ); 169 if (loose) { 170 assert(rootNodes.length >= expected.length, 171 `Expect to find at least ${expected.length} '${rootName}', ` + 172 `found ${rootNodes.length}`); 173 } else { 174 assert.strictEqual( 175 rootNodes.length, expected.length, 176 `Expect to find ${expected.length} '${rootName}', ` + 177 `found ${rootNodes.length}`); 178 } 179 for (const expectation of expected) { 180 if (expectation.children) { 181 for (const expectedEdge of expectation.children) { 182 const check = typeof expectedEdge === 'function' ? expectedEdge : 183 (edge) => (isEdge(edge, expectedEdge)); 184 // Don't use assert with a custom message here. Otherwise the 185 // inspection in the message is done eagerly and wastes a lot of CPU 186 // time. 187 const hasChild = rootNodes.some( 188 (node) => node.edges.some(check), 189 ); 190 if (!hasChild) { 191 throw new Error( 192 'expected to find child ' + 193 `${util.inspect(expectedEdge)} in ${inspectNode(rootNodes)}`); 194 } 195 } 196 } 197 } 198 } 199 200 validateSnapshotNodes(rootName, expected, { loose = false } = {}) { 201 this.validateSnapshot(rootName, expected, { loose }); 202 this.validateGraph(rootName, expected, { loose }); 203 } 204} 205 206function recordState(stream = undefined) { 207 return new State(stream); 208} 209 210function validateSnapshotNodes(...args) { 211 return recordState().validateSnapshotNodes(...args); 212} 213 214module.exports = { 215 recordState, 216 validateSnapshotNodes, 217}; 218