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