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

File tree

9 files changed

+324
-0
lines changed

9 files changed

+324
-0
lines changed

‎test/common/README.md‎

Lines changed: 37 additions & 0 deletions
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‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/* eslint-disable node-core/required-modules */
2+
'use strict';
3+
constassert=require('assert');
4+
constutil=require('util');
5+
6+
letinternalTestHeap;
7+
try{
8+
internalTestHeap=require('internal/test/heap');
9+
}catch(e){
10+
console.log('using `test/common/heap.js` requires `--expose-internals`');
11+
throwe;
12+
}
13+
const{ createJSHeapDump, buildEmbedderGraph }=internalTestHeap;
14+
15+
classState{
16+
constructor(){
17+
this.snapshot=createJSHeapDump();
18+
this.embedderGraph=buildEmbedderGraph();
19+
}
20+
21+
validateSnapshotNodes(name,expected,{ loose =false}={}){
22+
constsnapshot=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(constexpectedNodeofexpected){
29+
if(expectedNode.children){
30+
for(constexpectedChildofexpectedNode.children){
31+
constcheck=typeofexpectedChild==='function' ?
32+
expectedChild :
33+
(node)=>[expectedChild.name,'Node / '+expectedChild.name]
34+
.includes(node.name);
35+
36+
assert(snapshot.some((node)=>{
37+
returnnode.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+
constgraph=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(constexpectedNodeofexpected){
50+
if(expectedNode.edges){
51+
for(constexpectedChildofexpectedNode.children){
52+
constcheck=typeofexpectedChild==='function' ?
53+
expectedChild : (node)=>{
54+
returnnode.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+
functionrecordState(){
70+
returnnewState();
71+
}
72+
73+
functionvalidateSnapshotNodes(...args){
74+
returnrecordState().validateSnapshotNodes(...args);
75+
}
76+
77+
module.exports={
78+
recordState,
79+
validateSnapshotNodes
80+
};

‎test/parallel/test-heapdump-dns.js‎

Lines changed: 17 additions & 0 deletions
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+
constdns=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+
]);
Lines changed: 16 additions & 0 deletions
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+
constfs=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+
]);
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
constcommon=require('../common');
4+
const{ recordState }=require('../common/heap');
5+
consthttp2=require('http2');
6+
if(!common.hasCrypto)
7+
common.skip('missing crypto');
8+
9+
{
10+
conststate=recordState();
11+
state.validateSnapshotNodes('HTTP2SESSION',[]);
12+
state.validateSnapshotNodes('HTTP2STREAM',[]);
13+
}
14+
15+
constserver=http2.createServer();
16+
server.on('stream',(stream)=>{
17+
stream.respondWithFile(__filename);
18+
});
19+
server.listen(0,()=>{
20+
constclient=http2.connect(`http://localhost:${server.address().port}`);
21+
constreq=client.request();
22+
23+
req.on('response',common.mustCall(()=>{
24+
conststate=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+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
constcommon=require('../common');
4+
5+
common.skipIfInspectorDisabled();
6+
7+
const{ validateSnapshotNodes }=require('../common/heap');
8+
constinspector=require('inspector');
9+
10+
constsession=newinspector.Session();
11+
validateSnapshotNodes('INSPECTORJSBINDING',[]);
12+
session.connect();
13+
validateSnapshotNodes('INSPECTORJSBINDING',[
14+
{
15+
children: [
16+
{name: 'session'},
17+
{name: 'Connection'},
18+
(node)=>node.type==='closure'||typeofnode.value==='function'
19+
]
20+
}
21+
]);

‎test/parallel/test-heapdump-tls.js‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
constcommon=require('../common');
4+
5+
if(!common.hasCrypto)
6+
common.skip('missing crypto');
7+
8+
const{ validateSnapshotNodes }=require('../common/heap');
9+
constnet=require('net');
10+
consttls=require('tls');
11+
12+
validateSnapshotNodes('TLSWRAP',[]);
13+
14+
constserver=net.createServer(common.mustCall((c)=>{
15+
c.end();
16+
})).listen(0,common.mustCall(()=>{
17+
constc=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+
}));
Lines changed: 27 additions & 0 deletions
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+
constworker=newWorker('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();
Lines changed: 17 additions & 0 deletions
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+
constzlib=require('zlib');
6+
7+
validateSnapshotNodes('ZLIB',[]);
8+
// eslint-disable-next-line no-unused-vars
9+
constgunzip=zlib.createGunzip();
10+
validateSnapshotNodes('ZLIB',[
11+
{
12+
children: [
13+
{name: 'Zlib'},
14+
{name: 'zlib memory'}
15+
]
16+
}
17+
]);

0 commit comments

Comments
(0)