From 0478e4063f8464164efd05f1133edfa207ee02f2 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Thu, 13 Oct 2022 14:37:22 +0200 Subject: [PATCH] lib: add options to the heap snapshot APIs Support configuration of the HeapSnapshotMode and NumericsMode fields inf HeapSnapshotOptions in the JS APIs for heap snapshots. PR-URL: https://github.com/nodejs/node/pull/44989 Reviewed-By: Anna Henningsen Reviewed-By: Chengzhong Wu --- doc/api/v8.md | 24 +++++++-- doc/api/worker_threads.md | 13 ++++- lib/internal/heap_utils.js | 19 ++++++- lib/internal/worker.js | 10 ++-- lib/v8.js | 23 +++++++-- src/env.cc | 6 ++- src/heap_utils.cc | 47 ++++++++++++----- src/node_internals.h | 10 +++- src/node_worker.cc | 8 +-- test/common/heap.js | 34 ++++++++++++- test/fixtures/klass-with-fields.js | 18 +++++++ test/parallel/test-worker-heapdump-failure.js | 15 ++++++ .../test-get-heapsnapshot-options.js | 39 +++++++++++++++ test/sequential/test-heapdump.js | 18 +++++++ .../test-worker-heapsnapshot-options.js | 21 ++++++++ .../test-write-heapsnapshot-options.js | 50 +++++++++++++++++++ 16 files changed, 322 insertions(+), 33 deletions(-) create mode 100644 test/fixtures/klass-with-fields.js create mode 100644 test/sequential/test-get-heapsnapshot-options.js create mode 100644 test/sequential/test-worker-heapsnapshot-options.js create mode 100644 test/sequential/test-write-heapsnapshot-options.js diff --git a/doc/api/v8.md b/doc/api/v8.md index ed63f44f01b97b..1d26ff953b49ed 100644 --- a/doc/api/v8.md +++ b/doc/api/v8.md @@ -61,13 +61,23 @@ following properties: } ``` -## `v8.getHeapSnapshot()` +## `v8.getHeapSnapshot([options])` -* Returns: {stream.Readable} A Readable Stream containing the V8 heap snapshot +* `options` {Object} + * `exposeInternals` {boolean} If true, expose internals in the heap snapshot. + **Default:** `false`. + * `exposeNumericValues` {boolean} If true, expose numeric values in + artificial fields. **Default:** `false`. + +* Returns: {stream.Readable} A Readable containing the V8 heap snapshot. Generates a snapshot of the current V8 heap and returns a Readable Stream that may be used to read the JSON serialized representation. @@ -289,11 +299,14 @@ by [`NODE_V8_COVERAGE`][]. When the process is about to exit, one last coverage will still be written to disk unless [`v8.stopCoverage()`][] is invoked before the process exits. -## `v8.writeHeapSnapshot([filename])` +## `v8.writeHeapSnapshot([filename[,options]])` +* `options` {Object} + * `exposeInternals` {boolean} If true, expose internals in the heap snapshot. + **Default:** `false`. + * `exposeNumericValues` {boolean} If true, expose numeric values in + artificial fields. **Default:** `false`. * Returns: {Promise} A promise for a Readable Stream containing a V8 heap snapshot @@ -1379,7 +1388,7 @@ thread spawned will spawn another until the application crashes. [`require('node:worker_threads').threadId`]: #workerthreadid [`require('node:worker_threads').workerData`]: #workerworkerdata [`trace_events`]: tracing.md -[`v8.getHeapSnapshot()`]: v8.md#v8getheapsnapshot +[`v8.getHeapSnapshot()`]: v8.md#v8getheapsnapshotoptions [`vm`]: vm.md [`worker.SHARE_ENV`]: #workershare_env [`worker.on('message')`]: #event-message_1 diff --git a/lib/internal/heap_utils.js b/lib/internal/heap_utils.js index 126fe3f7e46a0c..3e789845c7b1a1 100644 --- a/lib/internal/heap_utils.js +++ b/lib/internal/heap_utils.js @@ -1,6 +1,7 @@ 'use strict'; const { - Symbol + Symbol, + Uint8Array, } = primordials; const { kUpdateTimer, @@ -8,9 +9,22 @@ const { } = require('internal/stream_base_commons'); const { owner_symbol } = require('internal/async_hooks').symbols; const { Readable } = require('stream'); +const { validateObject, validateBoolean } = require('internal/validators'); +const { kEmptyObject } = require('internal/util'); const kHandle = Symbol('kHandle'); +function getHeapSnapshotOptions(options = kEmptyObject) { + validateObject(options, 'options'); + const { + exposeInternals = false, + exposeNumericValues = false, + } = options; + validateBoolean(exposeInternals, 'options.exposeInternals'); + validateBoolean(exposeNumericValues, 'options.exposeNumericValues'); + return new Uint8Array([+exposeInternals, +exposeNumericValues]); +} + class HeapSnapshotStream extends Readable { constructor(handle) { super({ autoDestroy: true }); @@ -37,5 +51,6 @@ class HeapSnapshotStream extends Readable { } module.exports = { - HeapSnapshotStream + getHeapSnapshotOptions, + HeapSnapshotStream, }; diff --git a/lib/internal/worker.js b/lib/internal/worker.js index d88170ab9cd9cf..3cc589c996703c 100644 --- a/lib/internal/worker.js +++ b/lib/internal/worker.js @@ -416,12 +416,16 @@ class Worker extends EventEmitter { return makeResourceLimits(this[kHandle].getResourceLimits()); } - getHeapSnapshot() { - const heapSnapshotTaker = this[kHandle] && this[kHandle].takeHeapSnapshot(); + getHeapSnapshot(options) { + const { + HeapSnapshotStream, + getHeapSnapshotOptions + } = require('internal/heap_utils'); + const optionsArray = getHeapSnapshotOptions(options); + const heapSnapshotTaker = this[kHandle]?.takeHeapSnapshot(optionsArray); return new Promise((resolve, reject) => { if (!heapSnapshotTaker) return reject(new ERR_WORKER_NOT_RUNNING()); heapSnapshotTaker.ondone = (handle) => { - const { HeapSnapshotStream } = require('internal/heap_utils'); resolve(new HeapSnapshotStream(handle)); }; }); diff --git a/lib/v8.js b/lib/v8.js index 479e8b13efc96d..70956192d7d34f 100644 --- a/lib/v8.js +++ b/lib/v8.js @@ -57,7 +57,10 @@ const { createHeapSnapshotStream, triggerHeapSnapshot } = internalBinding('heap_utils'); -const { HeapSnapshotStream } = require('internal/heap_utils'); +const { + HeapSnapshotStream, + getHeapSnapshotOptions +} = require('internal/heap_utils'); const promiseHooks = require('internal/promise_hooks'); const { getOptionValue } = require('internal/options'); @@ -65,23 +68,33 @@ const { getOptionValue } = require('internal/options'); * Generates a snapshot of the current V8 heap * and writes it to a JSON file. * @param {string} [filename] + * @param {{ + * exposeInternals?: boolean, + * exposeNumericValues?: boolean + * }} [options] * @returns {string} */ -function writeHeapSnapshot(filename) { +function writeHeapSnapshot(filename, options) { if (filename !== undefined) { filename = getValidatedPath(filename); filename = toNamespacedPath(filename); } - return triggerHeapSnapshot(filename); + const optionArray = getHeapSnapshotOptions(options); + return triggerHeapSnapshot(filename, optionArray); } /** * Generates a snapshot of the current V8 heap * and returns a Readable Stream. + * @param {{ + * exposeInternals?: boolean, + * exposeNumericValues?: boolean + * }} [options] * @returns {import('./stream.js').Readable} */ -function getHeapSnapshot() { - const handle = createHeapSnapshotStream(); +function getHeapSnapshot(options) { + const optionArray = getHeapSnapshotOptions(options); + const handle = createHeapSnapshotStream(optionArray); assert(handle); return new HeapSnapshotStream(handle); } diff --git a/src/env.cc b/src/env.cc index 01cce4650b6eee..be5d4b0723c578 100644 --- a/src/env.cc +++ b/src/env.cc @@ -39,6 +39,7 @@ using v8::EscapableHandleScope; using v8::Function; using v8::FunctionTemplate; using v8::HandleScope; +using v8::HeapProfiler; using v8::HeapSpaceStatistics; using v8::Integer; using v8::Isolate; @@ -1790,7 +1791,10 @@ size_t Environment::NearHeapLimitCallback(void* data, Debug(env, DebugCategory::DIAGNOSTICS, "Start generating %s...\n", *name); - heap::WriteSnapshot(env, filename.c_str()); + HeapProfiler::HeapSnapshotOptions options; + options.numerics_mode = HeapProfiler::NumericsMode::kExposeNumericValues; + options.snapshot_mode = HeapProfiler::HeapSnapshotMode::kExposeInternals; + heap::WriteSnapshot(env, filename.c_str(), options); env->heap_limit_snapshot_taken_ += 1; Debug(env, diff --git a/src/heap_utils.cc b/src/heap_utils.cc index 8556b58f151092..14bcdd14433e7d 100644 --- a/src/heap_utils.cc +++ b/src/heap_utils.cc @@ -25,6 +25,7 @@ using v8::FunctionCallbackInfo; using v8::FunctionTemplate; using v8::Global; using v8::HandleScope; +using v8::HeapProfiler; using v8::HeapSnapshot; using v8::Isolate; using v8::JustVoid; @@ -36,6 +37,7 @@ using v8::Number; using v8::Object; using v8::ObjectTemplate; using v8::String; +using v8::Uint8Array; using v8::Value; namespace node { @@ -340,15 +342,19 @@ class HeapSnapshotStream : public AsyncWrap, HeapSnapshotPointer snapshot_; }; -inline void TakeSnapshot(Environment* env, v8::OutputStream* out) { - HeapSnapshotPointer snapshot { - env->isolate()->GetHeapProfiler()->TakeHeapSnapshot() }; +inline void TakeSnapshot(Environment* env, + v8::OutputStream* out, + HeapProfiler::HeapSnapshotOptions options) { + HeapSnapshotPointer snapshot{ + env->isolate()->GetHeapProfiler()->TakeHeapSnapshot(options)}; snapshot->Serialize(out, HeapSnapshot::kJSON); } } // namespace -Maybe WriteSnapshot(Environment* env, const char* filename) { +Maybe WriteSnapshot(Environment* env, + const char* filename, + HeapProfiler::HeapSnapshotOptions options) { uv_fs_t req; int err; @@ -365,7 +371,7 @@ Maybe WriteSnapshot(Environment* env, const char* filename) { } FileOutputStream stream(fd, &req); - TakeSnapshot(env, &stream); + TakeSnapshot(env, &stream, options); if ((err = stream.status()) < 0) { env->ThrowUVException(err, "write", nullptr, filename); return Nothing(); @@ -410,10 +416,28 @@ BaseObjectPtr CreateHeapSnapshotStream( return MakeBaseObject(env, std::move(snapshot), obj); } +HeapProfiler::HeapSnapshotOptions GetHeapSnapshotOptions( + Local options_value) { + CHECK(options_value->IsUint8Array()); + Local arr = options_value.As(); + uint8_t* options = + static_cast(arr->Buffer()->Data()) + arr->ByteOffset(); + HeapProfiler::HeapSnapshotOptions result; + result.snapshot_mode = options[0] + ? HeapProfiler::HeapSnapshotMode::kExposeInternals + : HeapProfiler::HeapSnapshotMode::kRegular; + result.numerics_mode = options[1] + ? HeapProfiler::NumericsMode::kExposeNumericValues + : HeapProfiler::NumericsMode::kHideNumericValues; + return result; +} + void CreateHeapSnapshotStream(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); - HeapSnapshotPointer snapshot { - env->isolate()->GetHeapProfiler()->TakeHeapSnapshot() }; + CHECK_EQ(args.Length(), 1); + auto options = GetHeapSnapshotOptions(args[0]); + HeapSnapshotPointer snapshot{ + env->isolate()->GetHeapProfiler()->TakeHeapSnapshot(options)}; CHECK(snapshot); BaseObjectPtr stream = CreateHeapSnapshotStream(env, std::move(snapshot)); @@ -424,13 +448,13 @@ void CreateHeapSnapshotStream(const FunctionCallbackInfo& args) { void TriggerHeapSnapshot(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); Isolate* isolate = args.GetIsolate(); - + CHECK_EQ(args.Length(), 2); Local filename_v = args[0]; + auto options = GetHeapSnapshotOptions(args[1]); if (filename_v->IsUndefined()) { DiagnosticFilename name(env, "Heap", "heapsnapshot"); - if (WriteSnapshot(env, *name).IsNothing()) - return; + if (WriteSnapshot(env, *name, options).IsNothing()) return; if (String::NewFromUtf8(isolate, *name).ToLocal(&filename_v)) { args.GetReturnValue().Set(filename_v); } @@ -439,8 +463,7 @@ void TriggerHeapSnapshot(const FunctionCallbackInfo& args) { BufferValue path(isolate, filename_v); CHECK_NOT_NULL(*path); - if (WriteSnapshot(env, *path).IsNothing()) - return; + if (WriteSnapshot(env, *path, options).IsNothing()) return; return args.GetReturnValue().Set(filename_v); } diff --git a/src/node_internals.h b/src/node_internals.h index 2dd2b6038ea2b2..e47b180b192c9a 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -382,7 +382,9 @@ class DiagnosticFilename { }; namespace heap { -v8::Maybe WriteSnapshot(Environment* env, const char* filename); +v8::Maybe WriteSnapshot(Environment* env, + const char* filename, + v8::HeapProfiler::HeapSnapshotOptions options); } namespace heap { @@ -423,6 +425,12 @@ std::ostream& operator<<(std::ostream& output, } bool linux_at_secure(); + +namespace heap { +v8::HeapProfiler::HeapSnapshotOptions GetHeapSnapshotOptions( + v8::Local options); +} // namespace heap + } // namespace node #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/node_worker.cc b/src/node_worker.cc index 571160a14ebb1e..6b0ca484ace83a 100644 --- a/src/node_worker.cc +++ b/src/node_worker.cc @@ -778,6 +778,8 @@ class WorkerHeapSnapshotTaker : public AsyncWrap { void Worker::TakeHeapSnapshot(const FunctionCallbackInfo& args) { Worker* w; ASSIGN_OR_RETURN_UNWRAP(&w, args.This()); + CHECK_EQ(args.Length(), 1); + auto options = heap::GetHeapSnapshotOptions(args[0]); Debug(w, "Worker %llu taking heap snapshot", w->thread_id_.id); @@ -797,10 +799,10 @@ void Worker::TakeHeapSnapshot(const FunctionCallbackInfo& args) { // Interrupt the worker thread and take a snapshot, then schedule a call // on the parent thread that turns that snapshot into a readable stream. - bool scheduled = w->RequestInterrupt([taker = std::move(taker), - env](Environment* worker_env) mutable { + bool scheduled = w->RequestInterrupt([taker = std::move(taker), env, options]( + Environment* worker_env) mutable { heap::HeapSnapshotPointer snapshot{ - worker_env->isolate()->GetHeapProfiler()->TakeHeapSnapshot()}; + worker_env->isolate()->GetHeapProfiler()->TakeHeapSnapshot(options)}; CHECK(snapshot); // Here, the worker thread temporarily owns the WorkerHeapSnapshotTaker diff --git a/test/common/heap.js b/test/common/heap.js index 6e5e55b000341b..1c22c274af125a 100644 --- a/test/common/heap.js +++ b/test/common/heap.js @@ -211,7 +211,39 @@ function validateSnapshotNodes(...args) { return recordState().validateSnapshotNodes(...args); } +function getHeapSnapshotOptionTests() { + const fixtures = require('../common/fixtures'); + const cases = [ + { + options: { exposeInternals: true }, + expected: [{ + children: [ + // We don't have anything special to test here yet + // because we don't use cppgc or embedder heap tracer. + { edge_name: 'nonNumeric', node_name: 'test' }, + ] + }] + }, + { + options: { exposeNumericValues: true }, + expected: [{ + children: [ + { edge_name: 'numeric', node_name: 'smi number' }, + ] + }] + }, + ]; + return { + fixtures: fixtures.path('klass-with-fields.js'), + check(snapshot, expected) { + snapshot.validateSnapshot('Klass', expected, { loose: true }); + }, + cases, + }; +} + module.exports = { recordState, - validateSnapshotNodes + validateSnapshotNodes, + getHeapSnapshotOptionTests }; diff --git a/test/fixtures/klass-with-fields.js b/test/fixtures/klass-with-fields.js new file mode 100644 index 00000000000000..c5b000d9f3ff3e --- /dev/null +++ b/test/fixtures/klass-with-fields.js @@ -0,0 +1,18 @@ +'use strict'; + +const { + parentPort, + isMainThread +} = require('node:worker_threads'); + +class Klass { + numeric = 1234; + nonNumeric = 'test'; +} + +globalThis.obj = new Klass(); + +if (!isMainThread) { + parentPort.postMessage('ready'); + setInterval(() => {}, 100); +} diff --git a/test/parallel/test-worker-heapdump-failure.js b/test/parallel/test-worker-heapdump-failure.js index 06e260374cd3a4..c5d24cdcf658a2 100644 --- a/test/parallel/test-worker-heapdump-failure.js +++ b/test/parallel/test-worker-heapdump-failure.js @@ -13,3 +13,18 @@ const { once } = require('events'); code: 'ERR_WORKER_NOT_RUNNING' }); })().then(common.mustCall()); + +(async function() { + const worker = new Worker('setInterval(() => {}, 1000);', { eval: true }); + await once(worker, 'online'); + + [1, true, [], null, Infinity, NaN].forEach((i) => { + assert.throws(() => worker.getHeapSnapshot(i), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options" argument must be of type object.' + + common.invalidArgTypeHelper(i) + }); + }); + await worker.terminate(); +})().then(common.mustCall()); diff --git a/test/sequential/test-get-heapsnapshot-options.js b/test/sequential/test-get-heapsnapshot-options.js new file mode 100644 index 00000000000000..91ba6112722ed5 --- /dev/null +++ b/test/sequential/test-get-heapsnapshot-options.js @@ -0,0 +1,39 @@ +'use strict'; + +// Flags: --expose-internals + +require('../common'); + +const { getHeapSnapshotOptionTests, recordState } = require('../common/heap'); + +const tests = getHeapSnapshotOptionTests(); +if (process.argv[2] === 'child') { + const { getHeapSnapshot } = require('v8'); + require(tests.fixtures); + const { options, expected } = tests.cases[parseInt(process.argv[3])]; + const snapshot = recordState(getHeapSnapshot(options)); + console.log('Snapshot nodes', snapshot.snapshot.length); + console.log('Searching for', expected[0].children); + tests.check(snapshot, expected); + delete globalThis.obj; // To pass the leaked global tests. + return; +} + +const { spawnSync } = require('child_process'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +for (let i = 0; i < tests.cases.length; ++i) { + const child = spawnSync( + process.execPath, + ['--expose-internals', __filename, 'child', i + ''], + { + cwd: tmpdir.path + }); + const stderr = child.stderr.toString(); + const stdout = child.stdout.toString(); + console.log('[STDERR]', stderr); + console.log('[STDOUT]', stdout); + assert.strictEqual(child.status, 0); +} diff --git a/test/sequential/test-heapdump.js b/test/sequential/test-heapdump.js index cb84bca4cd96da..1388623e61f939 100644 --- a/test/sequential/test-heapdump.js +++ b/test/sequential/test-heapdump.js @@ -47,6 +47,24 @@ process.chdir(tmpdir.path); }); }); +[1, true, [], null, Infinity, NaN].forEach((i) => { + assert.throws(() => writeHeapSnapshot('test.heapsnapshot', i), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options" argument must be of type object.' + + common.invalidArgTypeHelper(i) + }); +}); + +[1, true, [], null, Infinity, NaN].forEach((i) => { + assert.throws(() => getHeapSnapshot(i), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options" argument must be of type object.' + + common.invalidArgTypeHelper(i) + }); +}); + { let data = ''; const snapshot = getHeapSnapshot(); diff --git a/test/sequential/test-worker-heapsnapshot-options.js b/test/sequential/test-worker-heapsnapshot-options.js new file mode 100644 index 00000000000000..ca0ab190514b6c --- /dev/null +++ b/test/sequential/test-worker-heapsnapshot-options.js @@ -0,0 +1,21 @@ +// Flags: --expose-internals +'use strict'; +const common = require('../common'); +const { recordState, getHeapSnapshotOptionTests } = require('../common/heap'); +const { Worker } = require('worker_threads'); +const { once } = require('events'); + +(async function() { + const tests = getHeapSnapshotOptionTests(); + const w = new Worker(tests.fixtures); + + await once(w, 'message'); + + for (const { options, expected } of tests.cases) { + const stream = await w.getHeapSnapshot(options); + const snapshot = recordState(stream); + tests.check(snapshot, expected); + } + + await w.terminate(); +})().then(common.mustCall()); diff --git a/test/sequential/test-write-heapsnapshot-options.js b/test/sequential/test-write-heapsnapshot-options.js new file mode 100644 index 00000000000000..cc3b4b19f4d724 --- /dev/null +++ b/test/sequential/test-write-heapsnapshot-options.js @@ -0,0 +1,50 @@ +'use strict'; + +// Flags: --expose-internals + +require('../common'); + +const fs = require('fs'); +const { getHeapSnapshotOptionTests, recordState } = require('../common/heap'); + +class ReadStream { + constructor(filename) { + this._content = fs.readFileSync(filename, 'utf-8'); + } + pause() {} + read() { return this._content; } +} + +const tests = getHeapSnapshotOptionTests(); +if (process.argv[2] === 'child') { + const { writeHeapSnapshot } = require('v8'); + require(tests.fixtures); + const { options, expected } = tests.cases[parseInt(process.argv[3])]; + const filename = writeHeapSnapshot(undefined, options); + const snapshot = recordState(new ReadStream(filename)); + console.log('Snapshot nodes', snapshot.snapshot.length); + console.log('Searching for', expected[0].children); + tests.check(snapshot, expected); + delete globalThis.obj; // To pass the leaked global tests. + return; +} + +const { spawnSync } = require('child_process'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +// Start child processes to prevent the heap from growing too big. +for (let i = 0; i < tests.cases.length; ++i) { + const child = spawnSync( + process.execPath, + ['--expose-internals', __filename, 'child', i + ''], + { + cwd: tmpdir.path + }); + const stderr = child.stderr.toString(); + const stdout = child.stdout.toString(); + console.log('[STDERR]', stderr); + console.log('[STDOUT]', stdout); + assert.strictEqual(child.status, 0); +}