• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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