Skip to content

Commit 7352b72

Browse files
addaleaxtargos
authored andcommitted
test: add heap snapshot tests
Add a number of tests that validate that heap snapshots contain what we expect them to contain, and cross-check against a JS version of our own embedder graphs. PR-URL: #21741 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Joyee Cheung <[email protected]> Reviewed-By: Refael Ackermann <[email protected]>
1 parent a9a7186 commit 7352b72

9 files changed

+324
-0
lines changed

test/common/README.md

+37
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ This directory contains modules used to test the Node.js implementation.
1010
* [DNS module](#dns-module)
1111
* [Duplex pair helper](#duplex-pair-helper)
1212
* [Fixtures module](#fixtures-module)
13+
* [Heap dump checker module](#heap-dump-checker-module)
1314
* [HTTP2 module](#http2-module)
1415
* [Internet module](#internet-module)
1516
* [tmpdir module](#tmpdir-module)
@@ -538,6 +539,42 @@ Returns the result of
538539
Returns the result of
539540
`fs.readFileSync(path.join(fixtures.fixturesDir, 'keys', arg), 'enc')`.
540541

542+
## Heap dump checker module
543+
544+
This provides utilities for checking the validity of heap dumps.
545+
This requires the usage of `--expose-internals`.
546+
547+
### heap.recordState()
548+
549+
Create a heap dump and an embedder graph copy for inspection.
550+
The returned object has a `validateSnapshotNodes` function similar to the
551+
one listed below. (`heap.validateSnapshotNodes(...)` is a shortcut for
552+
`heap.recordState().validateSnapshotNodes(...)`.)
553+
554+
### heap.validateSnapshotNodes(name, expected, options)
555+
556+
* `name` [&lt;string>] Look for this string as the name of heap dump nodes.
557+
* `expected` [&lt;Array>] A list of objects, possibly with an `children`
558+
property that points to expected other adjacent nodes.
559+
* `options` [&lt;Array>]
560+
* `loose` [&lt;boolean>] Do not expect an exact listing of occurrences
561+
of nodes with name `name` in `expected`.
562+
563+
Create a heap dump and an embedder graph copy and validate occurrences.
564+
565+
<!-- eslint-disable no-undef, no-unused-vars, node-core/required-modules, strict -->
566+
```js
567+
validateSnapshotNodes('TLSWRAP', [
568+
{
569+
children: [
570+
{ name: 'enc_out' },
571+
{ name: 'enc_in' },
572+
{ name: 'TLSWrap' }
573+
]
574+
}
575+
]);
576+
```
577+
541578
## HTTP/2 Module
542579

543580
The http2.js module provides a handful of utilities for creating mock HTTP/2

test/common/heap.js

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/* eslint-disable node-core/required-modules */
2+
'use strict';
3+
const assert = require('assert');
4+
const util = require('util');
5+
6+
let internalTestHeap;
7+
try {
8+
internalTestHeap = require('internal/test/heap');
9+
} catch (e) {
10+
console.log('using `test/common/heap.js` requires `--expose-internals`');
11+
throw e;
12+
}
13+
const { createJSHeapDump, buildEmbedderGraph } = internalTestHeap;
14+
15+
class State {
16+
constructor() {
17+
this.snapshot = createJSHeapDump();
18+
this.embedderGraph = buildEmbedderGraph();
19+
}
20+
21+
validateSnapshotNodes(name, expected, { loose = false } = {}) {
22+
const snapshot = this.snapshot.filter(
23+
(node) => node.name === 'Node / ' + name && node.type !== 'string');
24+
if (loose)
25+
assert(snapshot.length >= expected.length);
26+
else
27+
assert.strictEqual(snapshot.length, expected.length);
28+
for (const expectedNode of expected) {
29+
if (expectedNode.children) {
30+
for (const expectedChild of expectedNode.children) {
31+
const check = typeof expectedChild === 'function' ?
32+
expectedChild :
33+
(node) => [expectedChild.name, 'Node / ' + expectedChild.name]
34+
.includes(node.name);
35+
36+
assert(snapshot.some((node) => {
37+
return node.outgoingEdges.map((edge) => edge.toNode).some(check);
38+
}), `expected to find child ${util.inspect(expectedChild)} ` +
39+
`in ${util.inspect(snapshot)}`);
40+
}
41+
}
42+
}
43+
44+
const graph = this.embedderGraph.filter((node) => node.name === name);
45+
if (loose)
46+
assert(graph.length >= expected.length);
47+
else
48+
assert.strictEqual(graph.length, expected.length);
49+
for (const expectedNode of expected) {
50+
if (expectedNode.edges) {
51+
for (const expectedChild of expectedNode.children) {
52+
const check = typeof expectedChild === 'function' ?
53+
expectedChild : (node) => {
54+
return node.name === expectedChild.name ||
55+
(node.value &&
56+
node.value.constructor &&
57+
node.value.constructor.name === expectedChild.name);
58+
};
59+
60+
assert(graph.some((node) => node.edges.some(check)),
61+
`expected to find child ${util.inspect(expectedChild)} ` +
62+
`in ${util.inspect(snapshot)}`);
63+
}
64+
}
65+
}
66+
}
67+
}
68+
69+
function recordState() {
70+
return new State();
71+
}
72+
73+
function validateSnapshotNodes(...args) {
74+
return recordState().validateSnapshotNodes(...args);
75+
}
76+
77+
module.exports = {
78+
recordState,
79+
validateSnapshotNodes
80+
};

test/parallel/test-heapdump-dns.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
require('../common');
4+
const { validateSnapshotNodes } = require('../common/heap');
5+
6+
validateSnapshotNodes('DNSCHANNEL', []);
7+
const dns = require('dns');
8+
validateSnapshotNodes('DNSCHANNEL', [{}]);
9+
dns.resolve('localhost', () => {});
10+
validateSnapshotNodes('DNSCHANNEL', [
11+
{
12+
children: [
13+
{ name: 'task list' },
14+
{ name: 'ChannelWrap' }
15+
]
16+
}
17+
]);
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
require('../common');
4+
const { validateSnapshotNodes } = require('../common/heap');
5+
const fs = require('fs').promises;
6+
7+
validateSnapshotNodes('FSREQPROMISE', []);
8+
fs.stat(__filename);
9+
validateSnapshotNodes('FSREQPROMISE', [
10+
{
11+
children: [
12+
{ name: 'FSReqPromise' },
13+
{ name: 'Float64Array' } // Stat array
14+
]
15+
}
16+
]);

test/parallel/test-heapdump-http2.js

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
const common = require('../common');
4+
const { recordState } = require('../common/heap');
5+
const http2 = require('http2');
6+
if (!common.hasCrypto)
7+
common.skip('missing crypto');
8+
9+
{
10+
const state = recordState();
11+
state.validateSnapshotNodes('HTTP2SESSION', []);
12+
state.validateSnapshotNodes('HTTP2STREAM', []);
13+
}
14+
15+
const server = http2.createServer();
16+
server.on('stream', (stream) => {
17+
stream.respondWithFile(__filename);
18+
});
19+
server.listen(0, () => {
20+
const client = http2.connect(`http://localhost:${server.address().port}`);
21+
const req = client.request();
22+
23+
req.on('response', common.mustCall(() => {
24+
const state = recordState();
25+
state.validateSnapshotNodes('HTTP2STREAM', [
26+
{
27+
children: [
28+
{ name: 'Http2Stream' }
29+
]
30+
},
31+
], { loose: true });
32+
state.validateSnapshotNodes('FILEHANDLE', [
33+
{
34+
children: [
35+
{ name: 'FileHandle' }
36+
]
37+
}
38+
]);
39+
state.validateSnapshotNodes('TCPWRAP', [
40+
{
41+
children: [
42+
{ name: 'TCP' }
43+
]
44+
}
45+
], { loose: true });
46+
state.validateSnapshotNodes('TCPSERVERWRAP', [
47+
{
48+
children: [
49+
{ name: 'TCP' }
50+
]
51+
}
52+
], { loose: true });
53+
state.validateSnapshotNodes('STREAMPIPE', [
54+
{
55+
children: [
56+
{ name: 'StreamPipe' }
57+
]
58+
}
59+
]);
60+
state.validateSnapshotNodes('HTTP2SESSION', [
61+
{
62+
children: [
63+
{ name: 'Http2Session' },
64+
{ name: 'streams' }
65+
]
66+
}
67+
], { loose: true });
68+
}));
69+
70+
req.resume();
71+
req.on('end', common.mustCall(() => {
72+
client.close();
73+
server.close();
74+
}));
75+
req.end();
76+
});
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
const common = require('../common');
4+
5+
common.skipIfInspectorDisabled();
6+
7+
const { validateSnapshotNodes } = require('../common/heap');
8+
const inspector = require('inspector');
9+
10+
const session = new inspector.Session();
11+
validateSnapshotNodes('INSPECTORJSBINDING', []);
12+
session.connect();
13+
validateSnapshotNodes('INSPECTORJSBINDING', [
14+
{
15+
children: [
16+
{ name: 'session' },
17+
{ name: 'Connection' },
18+
(node) => node.type === 'closure' || typeof node.value === 'function'
19+
]
20+
}
21+
]);

test/parallel/test-heapdump-tls.js

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
const common = require('../common');
4+
5+
if (!common.hasCrypto)
6+
common.skip('missing crypto');
7+
8+
const { validateSnapshotNodes } = require('../common/heap');
9+
const net = require('net');
10+
const tls = require('tls');
11+
12+
validateSnapshotNodes('TLSWRAP', []);
13+
14+
const server = net.createServer(common.mustCall((c) => {
15+
c.end();
16+
})).listen(0, common.mustCall(() => {
17+
const c = tls.connect({ port: server.address().port });
18+
19+
c.on('error', common.mustCall(() => {
20+
server.close();
21+
}));
22+
c.write('hello');
23+
24+
validateSnapshotNodes('TLSWRAP', [
25+
{
26+
children: [
27+
{ name: 'enc_out' },
28+
{ name: 'enc_in' },
29+
{ name: 'TLSWrap' }
30+
]
31+
}
32+
]);
33+
}));

test/parallel/test-heapdump-worker.js

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Flags: --expose-internals --experimental-worker
2+
'use strict';
3+
require('../common');
4+
const { validateSnapshotNodes } = require('../common/heap');
5+
const { Worker } = require('worker_threads');
6+
7+
validateSnapshotNodes('WORKER', []);
8+
const worker = new Worker('setInterval(() => {}, 100);', { eval: true });
9+
validateSnapshotNodes('WORKER', [
10+
{
11+
children: [
12+
{ name: 'thread_exit_async' },
13+
{ name: 'env' },
14+
{ name: 'MESSAGEPORT' },
15+
{ name: 'Worker' }
16+
]
17+
}
18+
]);
19+
validateSnapshotNodes('MESSAGEPORT', [
20+
{
21+
children: [
22+
{ name: 'data' },
23+
{ name: 'MessagePort' }
24+
]
25+
}
26+
], { loose: true });
27+
worker.terminate();

test/parallel/test-heapdump-zlib.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
require('../common');
4+
const { validateSnapshotNodes } = require('../common/heap');
5+
const zlib = require('zlib');
6+
7+
validateSnapshotNodes('ZLIB', []);
8+
// eslint-disable-next-line no-unused-vars
9+
const gunzip = zlib.createGunzip();
10+
validateSnapshotNodes('ZLIB', [
11+
{
12+
children: [
13+
{ name: 'Zlib' },
14+
{ name: 'zlib memory' }
15+
]
16+
}
17+
]);

0 commit comments

Comments
 (0)