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: https://github.com/nodejs/node/pull/21741
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: Refael Ackermann <refack@gmail.com>
This commit is contained in:
Anna Henningsen committed 2018-07-15 20:35:44 +02:00
1 parent 45ad8df318
commit 1009118d27
9 files changed
+324

No files matched your search

+37
View File
@@ -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)
@@ -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
+80
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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' }
]
}
]);