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 !== 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 } 147 148 // Validate our internal embedded graph representation 149 validateGraph(rootName, expected, { loose = false } = {}) { 150 const rootNodes = this.embedderGraph.filter( 151 (node) => node.name === rootName 152 ); 153 if (loose) { 154 assert(rootNodes.length >= expected.length, 155 `Expect to find at least ${expected.length} '${rootName}', ` + 156 `found ${rootNodes.length}`); 157 } else { 158 assert.strictEqual( 159 rootNodes.length, expected.length, 160 `Expect to find ${expected.length} '${rootName}', ` + 161 `found ${rootNodes.length}`); 162 } 163 for (const expectation of expected) { 164 if (expectation.children) { 165 for (const expectedEdge of expectation.children) { 166 const check = typeof expectedEdge === 'function' ? expectedEdge : 167 (edge) => (isEdge(edge, expectedEdge)); 168 // Don't use assert with a custom message here. Otherwise the 169 // inspection in the message is done eagerly and wastes a lot of CPU 170 // time. 171 const hasChild = rootNodes.some( 172 (node) => node.edges.some(check) 173 ); 174 if (!hasChild) { 175 throw new Error( 176 'expected to find child ' + 177 `${util.inspect(expectedEdge)} in ${inspectNode(rootNodes)}`); 178 } 179 } 180 } 181 } 182 } 183 184 validateSnapshotNodes(rootName, expected, { loose = false } = {}) { 185 this.validateSnapshot(rootName, expected, { loose }); 186 this.validateGraph(rootName, expected, { loose }); 187 } 188} 189 190function recordState(stream = undefined) { 191 return new State(stream); 192} 193 194function validateSnapshotNodes(...args) { 195 return recordState().validateSnapshotNodes(...args); 196} 197 198module.exports = { 199 recordState, 200 validateSnapshotNodes 201}; 202