From 7352b72fc9a740c21dc848a3bb7b0f67313f5776 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Tue, 10 Jul 2018 22:12:23 +0200 Subject: [PATCH] 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 Reviewed-By: Joyee Cheung Reviewed-By: Refael Ackermann --- test/common/README.md | 37 +++++++++++ test/common/heap.js | 80 +++++++++++++++++++++++ test/parallel/test-heapdump-dns.js | 17 +++++ test/parallel/test-heapdump-fs-promise.js | 16 +++++ test/parallel/test-heapdump-http2.js | 76 +++++++++++++++++++++ test/parallel/test-heapdump-inspector.js | 21 ++++++ test/parallel/test-heapdump-tls.js | 33 ++++++++++ test/parallel/test-heapdump-worker.js | 27 ++++++++ test/parallel/test-heapdump-zlib.js | 17 +++++ 9 files changed, 324 insertions(+) create mode 100644 test/common/heap.js create mode 100644 test/parallel/test-heapdump-dns.js create mode 100644 test/parallel/test-heapdump-fs-promise.js create mode 100644 test/parallel/test-heapdump-http2.js create mode 100644 test/parallel/test-heapdump-inspector.js create mode 100644 test/parallel/test-heapdump-tls.js create mode 100644 test/parallel/test-heapdump-worker.js create mode 100644 test/parallel/test-heapdump-zlib.js diff --git a/test/common/README.md b/test/common/README.md index 111ce45b00360c..c0051ad9f7ca6e 100644 --- a/test/common/README.md +++ b/test/common/README.md @@ -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. + + +```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 diff --git a/test/common/heap.js b/test/common/heap.js new file mode 100644 index 00000000000000..a02de9a60651f4 --- /dev/null +++ b/test/common/heap.js @@ -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 +}; diff --git a/test/parallel/test-heapdump-dns.js b/test/parallel/test-heapdump-dns.js new file mode 100644 index 00000000000000..011503f5874d5a --- /dev/null +++ b/test/parallel/test-heapdump-dns.js @@ -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' } + ] + } +]); diff --git a/test/parallel/test-heapdump-fs-promise.js b/test/parallel/test-heapdump-fs-promise.js new file mode 100644 index 00000000000000..be44b3d8731bc1 --- /dev/null +++ b/test/parallel/test-heapdump-fs-promise.js @@ -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 + ] + } +]); diff --git a/test/parallel/test-heapdump-http2.js b/test/parallel/test-heapdump-http2.js new file mode 100644 index 00000000000000..19a70d8c44b15d --- /dev/null +++ b/test/parallel/test-heapdump-http2.js @@ -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(); +}); diff --git a/test/parallel/test-heapdump-inspector.js b/test/parallel/test-heapdump-inspector.js new file mode 100644 index 00000000000000..355b8d0d0a1d51 --- /dev/null +++ b/test/parallel/test-heapdump-inspector.js @@ -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' + ] + } +]); diff --git a/test/parallel/test-heapdump-tls.js b/test/parallel/test-heapdump-tls.js new file mode 100644 index 00000000000000..be14b7b5f7ca64 --- /dev/null +++ b/test/parallel/test-heapdump-tls.js @@ -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' } + ] + } + ]); +})); diff --git a/test/parallel/test-heapdump-worker.js b/test/parallel/test-heapdump-worker.js new file mode 100644 index 00000000000000..68d2ccd1abbc29 --- /dev/null +++ b/test/parallel/test-heapdump-worker.js @@ -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(); diff --git a/test/parallel/test-heapdump-zlib.js b/test/parallel/test-heapdump-zlib.js new file mode 100644 index 00000000000000..7a749902f5aaf6 --- /dev/null +++ b/test/parallel/test-heapdump-zlib.js @@ -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' } + ] + } +]);