Skip to content

Commit

Permalink
test: add heap snapshot tests
Browse files Browse the repository at this point in the history
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 <jasnell@gmail.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: Refael Ackermann <refack@gmail.com>
  • Loading branch information
addaleax authored and targos committed Jul 16, 2018
1 parent a9a7186 commit 7352b72
Show file tree
Hide file tree
Showing 9 changed files with 324 additions and 0 deletions.
37 changes: 37 additions & 0 deletions test/common/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This directory contains modules used to test the Node.js implementation.
* [DNS module](#dns-module)
* [Duplex pair helper](#duplex-pair-helper)
* [Fixtures module](#fixtures-module)
* [Heap dump checker module](#heap-dump-checker-module)
* [HTTP2 module](#http2-module)
* [Internet module](#internet-module)
* [tmpdir module](#tmpdir-module)
Expand Down Expand Up @@ -538,6 +539,42 @@ Returns the result of
Returns the result of
`fs.readFileSync(path.join(fixtures.fixturesDir, 'keys', arg), 'enc')`.

## Heap dump checker module

This provides utilities for checking the validity of heap dumps.
This requires the usage of `--expose-internals`.

### heap.recordState()

Create a heap dump and an embedder graph copy for inspection.
The returned object has a `validateSnapshotNodes` function similar to the
one listed below. (`heap.validateSnapshotNodes(...)` is a shortcut for
`heap.recordState().validateSnapshotNodes(...)`.)

### heap.validateSnapshotNodes(name, expected, options)

* `name` [&lt;string>] Look for this string as the name of heap dump nodes.
* `expected` [&lt;Array>] A list of objects, possibly with an `children`
property that points to expected other adjacent nodes.
* `options` [&lt;Array>]
* `loose` [&lt;boolean>] Do not expect an exact listing of occurrences
of nodes with name `name` in `expected`.

Create a heap dump and an embedder graph copy and validate occurrences.

<!-- eslint-disable no-undef, no-unused-vars, node-core/required-modules, strict -->
```js
validateSnapshotNodes('TLSWRAP', [
{
children: [
{ name: 'enc_out' },
{ name: 'enc_in' },
{ name: 'TLSWrap' }
]
}
]);
```

## HTTP/2 Module

The http2.js module provides a handful of utilities for creating mock HTTP/2
Expand Down
80 changes: 80 additions & 0 deletions test/common/heap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/* eslint-disable node-core/required-modules */
'use strict';
const assert = require('assert');
const util = require('util');

let internalTestHeap;
try {
internalTestHeap = require('internal/test/heap');
} catch (e) {
console.log('using `test/common/heap.js` requires `--expose-internals`');
throw e;
}
const { createJSHeapDump, buildEmbedderGraph } = internalTestHeap;

class State {
constructor() {
this.snapshot = createJSHeapDump();
this.embedderGraph = buildEmbedderGraph();
}

validateSnapshotNodes(name, expected, { loose = false } = {}) {
const snapshot = this.snapshot.filter(
(node) => node.name === 'Node / ' + name && node.type !== 'string');
if (loose)
assert(snapshot.length >= expected.length);
else
assert.strictEqual(snapshot.length, expected.length);
for (const expectedNode of expected) {
if (expectedNode.children) {
for (const expectedChild of expectedNode.children) {
const check = typeof expectedChild === 'function' ?
expectedChild :
(node) => [expectedChild.name, 'Node / ' + expectedChild.name]
.includes(node.name);

assert(snapshot.some((node) => {
return node.outgoingEdges.map((edge) => edge.toNode).some(check);
}), `expected to find child ${util.inspect(expectedChild)} ` +
`in ${util.inspect(snapshot)}`);
}
}
}

const graph = this.embedderGraph.filter((node) => node.name === name);
if (loose)
assert(graph.length >= expected.length);
else
assert.strictEqual(graph.length, expected.length);
for (const expectedNode of expected) {
if (expectedNode.edges) {
for (const expectedChild of expectedNode.children) {
const check = typeof expectedChild === 'function' ?
expectedChild : (node) => {
return node.name === expectedChild.name ||
(node.value &&
node.value.constructor &&
node.value.constructor.name === expectedChild.name);
};

assert(graph.some((node) => node.edges.some(check)),
`expected to find child ${util.inspect(expectedChild)} ` +
`in ${util.inspect(snapshot)}`);
}
}
}
}
}

function recordState() {
return new State();
}

function validateSnapshotNodes(...args) {
return recordState().validateSnapshotNodes(...args);
}

module.exports = {
recordState,
validateSnapshotNodes
};
17 changes: 17 additions & 0 deletions test/parallel/test-heapdump-dns.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Flags: --expose-internals
'use strict';
require('../common');
const { validateSnapshotNodes } = require('../common/heap');

validateSnapshotNodes('DNSCHANNEL', []);
const dns = require('dns');
validateSnapshotNodes('DNSCHANNEL', [{}]);
dns.resolve('localhost', () => {});
validateSnapshotNodes('DNSCHANNEL', [
{
children: [
{ name: 'task list' },
{ name: 'ChannelWrap' }
]
}
]);
16 changes: 16 additions & 0 deletions test/parallel/test-heapdump-fs-promise.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Flags: --expose-internals
'use strict';
require('../common');
const { validateSnapshotNodes } = require('../common/heap');
const fs = require('fs').promises;

validateSnapshotNodes('FSREQPROMISE', []);
fs.stat(__filename);
validateSnapshotNodes('FSREQPROMISE', [
{
children: [
{ name: 'FSReqPromise' },
{ name: 'Float64Array' } // Stat array
]
}
]);
76 changes: 76 additions & 0 deletions test/parallel/test-heapdump-http2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Flags: --expose-internals
'use strict';
const common = require('../common');
const { recordState } = require('../common/heap');
const http2 = require('http2');
if (!common.hasCrypto)
common.skip('missing crypto');

{
const state = recordState();
state.validateSnapshotNodes('HTTP2SESSION', []);
state.validateSnapshotNodes('HTTP2STREAM', []);
}

const server = http2.createServer();
server.on('stream', (stream) => {
stream.respondWithFile(__filename);
});
server.listen(0, () => {
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request();

req.on('response', common.mustCall(() => {
const state = recordState();
state.validateSnapshotNodes('HTTP2STREAM', [
{
children: [
{ name: 'Http2Stream' }
]
},
], { loose: true });
state.validateSnapshotNodes('FILEHANDLE', [
{
children: [
{ name: 'FileHandle' }
]
}
]);
state.validateSnapshotNodes('TCPWRAP', [
{
children: [
{ name: 'TCP' }
]
}
], { loose: true });
state.validateSnapshotNodes('TCPSERVERWRAP', [
{
children: [
{ name: 'TCP' }
]
}
], { loose: true });
state.validateSnapshotNodes('STREAMPIPE', [
{
children: [
{ name: 'StreamPipe' }
]
}
]);
state.validateSnapshotNodes('HTTP2SESSION', [
{
children: [
{ name: 'Http2Session' },
{ name: 'streams' }
]
}
], { loose: true });
}));

req.resume();
req.on('end', common.mustCall(() => {
client.close();
server.close();
}));
req.end();
});
21 changes: 21 additions & 0 deletions test/parallel/test-heapdump-inspector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Flags: --expose-internals
'use strict';
const common = require('../common');

common.skipIfInspectorDisabled();

const { validateSnapshotNodes } = require('../common/heap');
const inspector = require('inspector');

const session = new inspector.Session();
validateSnapshotNodes('INSPECTORJSBINDING', []);
session.connect();
validateSnapshotNodes('INSPECTORJSBINDING', [
{
children: [
{ name: 'session' },
{ name: 'Connection' },
(node) => node.type === 'closure' || typeof node.value === 'function'
]
}
]);
33 changes: 33 additions & 0 deletions test/parallel/test-heapdump-tls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Flags: --expose-internals
'use strict';
const common = require('../common');

if (!common.hasCrypto)
common.skip('missing crypto');

const { validateSnapshotNodes } = require('../common/heap');
const net = require('net');
const tls = require('tls');

validateSnapshotNodes('TLSWRAP', []);

const server = net.createServer(common.mustCall((c) => {
c.end();
})).listen(0, common.mustCall(() => {
const c = tls.connect({ port: server.address().port });

c.on('error', common.mustCall(() => {
server.close();
}));
c.write('hello');

validateSnapshotNodes('TLSWRAP', [
{
children: [
{ name: 'enc_out' },
{ name: 'enc_in' },
{ name: 'TLSWrap' }
]
}
]);
}));
27 changes: 27 additions & 0 deletions test/parallel/test-heapdump-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Flags: --expose-internals --experimental-worker
'use strict';
require('../common');
const { validateSnapshotNodes } = require('../common/heap');
const { Worker } = require('worker_threads');

validateSnapshotNodes('WORKER', []);
const worker = new Worker('setInterval(() => {}, 100);', { eval: true });
validateSnapshotNodes('WORKER', [
{
children: [
{ name: 'thread_exit_async' },
{ name: 'env' },
{ name: 'MESSAGEPORT' },
{ name: 'Worker' }
]
}
]);
validateSnapshotNodes('MESSAGEPORT', [
{
children: [
{ name: 'data' },
{ name: 'MessagePort' }
]
}
], { loose: true });
worker.terminate();
17 changes: 17 additions & 0 deletions test/parallel/test-heapdump-zlib.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Flags: --expose-internals
'use strict';
require('../common');
const { validateSnapshotNodes } = require('../common/heap');
const zlib = require('zlib');

validateSnapshotNodes('ZLIB', []);
// eslint-disable-next-line no-unused-vars
const gunzip = zlib.createGunzip();
validateSnapshotNodes('ZLIB', [
{
children: [
{ name: 'Zlib' },
{ name: 'zlib memory' }
]
}
]);

0 comments on commit 7352b72

Please sign in to comment.