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