mirror of
https://github.com/nodejs/node.git
synced 2026-10-14 10:26:26 -04:00
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:
9 files changed
+324
No files matched your search
@@ -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` [<string>] Look for this string as the name of heap dump nodes.
|
||||
* `expected` [<Array>] A list of objects, possibly with an `children`
|
||||
property that points to expected other adjacent nodes.
|
||||
* `options` [<Array>]
|
||||
* `loose` [<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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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' }
|
||||
]
|
||||
}
|
||||
]);
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
]);
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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'
|
||||
]
|
||||
}
|
||||
]);
|
||||
@@ -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' }
|
||||
]
|
||||
}
|
||||
]);
|
||||
}));
|
||||
@@ -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();
|
||||
@@ -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' }
|
||||
]
|
||||
}
|
||||
]);
|
||||
Reference in new issue
Block a user