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 <anna@addaleax.net>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
This commit is contained in:
Joyee Cheung 2022-10-13 14:37:22 +02:00
parent 59938e3180
commit b872d30d19
No known key found for this signature in database
GPG Key ID: 92B78A53C8303B8D
16 changed files with 322 additions and 33 deletions

View File

@ -61,13 +61,23 @@ following properties:
}
```
## `v8.getHeapSnapshot()`
## `v8.getHeapSnapshot([options])`
<!-- YAML
added: v11.13.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/44989
description: Support options to configure the heap snapshot.
-->
* 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]])`
<!-- YAML
added: v11.13.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/44989
description: Support options to configure the heap snapshot.
- version: v18.0.0
pr-url: https://github.com/nodejs/node/pull/41373
description: An exception will now be thrown if the file could not be written.
@ -308,6 +321,11 @@ changes:
generated, where `{pid}` will be the PID of the Node.js process,
`{thread_id}` will be `0` when `writeHeapSnapshot()` is called from
the main Node.js thread or the id of a worker thread.
* `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: {string} The filename where the snapshot was saved.
Generates a snapshot of the current V8 heap and writes it to a JSON

View File

@ -1067,14 +1067,23 @@ added: v10.5.0
The `'online'` event is emitted when the worker thread has started executing
JavaScript code.
### `worker.getHeapSnapshot()`
### `worker.getHeapSnapshot([options])`
<!-- YAML
added:
- v13.9.0
- v12.17.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/44989
description: Support options to configure the 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: {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

View File

@ -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,
};

View File

@ -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));
};
});

View File

@ -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);
}

View File

@ -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,

View File

@ -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<void> WriteSnapshot(Environment* env, const char* filename) {
Maybe<void> WriteSnapshot(Environment* env,
const char* filename,
HeapProfiler::HeapSnapshotOptions options) {
uv_fs_t req;
int err;
@ -365,7 +371,7 @@ Maybe<void> 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<void>();
@ -410,10 +416,28 @@ BaseObjectPtr<AsyncWrap> CreateHeapSnapshotStream(
return MakeBaseObject<HeapSnapshotStream>(env, std::move(snapshot), obj);
}
HeapProfiler::HeapSnapshotOptions GetHeapSnapshotOptions(
Local<Value> options_value) {
CHECK(options_value->IsUint8Array());
Local<Uint8Array> arr = options_value.As<Uint8Array>();
uint8_t* options =
static_cast<uint8_t*>(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<Value>& 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<AsyncWrap> stream =
CreateHeapSnapshotStream(env, std::move(snapshot));
@ -424,13 +448,13 @@ void CreateHeapSnapshotStream(const FunctionCallbackInfo<Value>& args) {
void TriggerHeapSnapshot(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = args.GetIsolate();
CHECK_EQ(args.Length(), 2);
Local<Value> 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<Value>& 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);
}

View File

@ -382,7 +382,9 @@ class DiagnosticFilename {
};
namespace heap {
v8::Maybe<void> WriteSnapshot(Environment* env, const char* filename);
v8::Maybe<void> 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<v8::Value> options);
} // namespace heap
} // namespace node
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

View File

@ -778,6 +778,8 @@ class WorkerHeapSnapshotTaker : public AsyncWrap {
void Worker::TakeHeapSnapshot(const FunctionCallbackInfo<Value>& 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<Value>& 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

View File

@ -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
};

18
test/fixtures/klass-with-fields.js vendored Normal file
View File

@ -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);
}

View File

@ -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());

View File

@ -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);
}

View File

@ -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();

View File

@ -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());

View File

@ -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);
}