v8: add setHeapSnapshotNearHeapLimit

PR-URL: https://github.com/nodejs/node/pull/44420
Refs: https://github.com/nodejs/node/pull/33010
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
This commit is contained in:
theanarkh 2022-09-08 17:33:20 +08:00 committed by GitHub
parent 0b5b5edd86
commit e62f6ce630
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 294 additions and 17 deletions

View File

@ -356,6 +356,20 @@ if (isMainThread) {
}
```
## `v8.setHeapSnapshotNearHeapLimit(limit)`
<!-- YAML
added: REPLACEME
-->
> Stability: 1 - Experimental
* `limit` {integer}
The API is a no-op if `--heapsnapshot-near-heap-limit` is already set from the
command line or the API is called more than once. `limit` must be a positive
integer. See [`--heapsnapshot-near-heap-limit`][] for more information.
## Serialization API
The serialization API provides means of serializing JavaScript values in a way
@ -1020,6 +1034,7 @@ Returns true if the Node.js instance is run to build a snapshot.
[HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
[Hook Callbacks]: #hook-callbacks
[V8]: https://developers.google.com/v8/
[`--heapsnapshot-near-heap-limit`]: cli.md#--heapsnapshot-near-heap-limitmax_count
[`AsyncLocalStorage`]: async_context.md#class-asynclocalstorage
[`Buffer`]: buffer.md
[`DefaultDeserializer`]: #class-v8defaultdeserializer

View File

@ -33,7 +33,7 @@ const {
} = primordials;
const { Buffer } = require('buffer');
const { validateString } = require('internal/validators');
const { validateString, validateUint32 } = require('internal/validators');
const {
Serializer,
Deserializer
@ -59,6 +59,7 @@ const {
} = internalBinding('heap_utils');
const { HeapSnapshotStream } = require('internal/heap_utils');
const promiseHooks = require('internal/promise_hooks');
const { getOptionValue } = require('internal/options');
/**
* Generates a snapshot of the current V8 heap
@ -95,6 +96,7 @@ const {
updateHeapStatisticsBuffer,
updateHeapSpaceStatisticsBuffer,
updateHeapCodeStatisticsBuffer,
setHeapSnapshotNearHeapLimit: _setHeapSnapshotNearHeapLimit,
// Properties for heap statistics buffer extraction.
kTotalHeapSizeIndex,
@ -226,6 +228,18 @@ function getHeapCodeStatistics() {
};
}
let heapSnapshotNearHeapLimitCallbackAdded = false;
function setHeapSnapshotNearHeapLimit(limit) {
validateUint32(limit, 'limit', 1);
if (heapSnapshotNearHeapLimitCallbackAdded ||
getOptionValue('--heapsnapshot-near-heap-limit') > 0
) {
return;
}
heapSnapshotNearHeapLimitCallbackAdded = true;
_setHeapSnapshotNearHeapLimit(limit);
}
/* V8 serialization API */
/* JS methods for the base objects */
@ -387,5 +401,6 @@ module.exports = {
serialize,
writeHeapSnapshot,
promiseHooks,
startupSnapshot
startupSnapshot,
setHeapSnapshotNearHeapLimit,
};

View File

@ -892,6 +892,24 @@ Realm* Environment::principal_realm() const {
return principal_realm_.get();
}
inline void Environment::set_heap_snapshot_near_heap_limit(uint32_t limit) {
heap_snapshot_near_heap_limit_ = limit;
}
inline void Environment::AddHeapSnapshotNearHeapLimitCallback() {
DCHECK(!heapsnapshot_near_heap_limit_callback_added_);
heapsnapshot_near_heap_limit_callback_added_ = true;
isolate_->AddNearHeapLimitCallback(Environment::NearHeapLimitCallback, this);
}
inline void Environment::RemoveHeapSnapshotNearHeapLimitCallback(
size_t heap_limit) {
DCHECK(heapsnapshot_near_heap_limit_callback_added_);
heapsnapshot_near_heap_limit_callback_added_ = false;
isolate_->RemoveNearHeapLimitCallback(Environment::NearHeapLimitCallback,
heap_limit);
}
} // namespace node
// These two files depend on each other. Including base_object-inl.h after this

View File

@ -682,6 +682,9 @@ Environment::Environment(IsolateData* isolate_data,
inspector_host_port_ = std::make_shared<ExclusiveAccess<HostPort>>(
options_->debug_options().host_port);
heap_snapshot_near_heap_limit_ =
static_cast<uint32_t>(options_->heap_snapshot_near_heap_limit);
if (!(flags_ & EnvironmentFlags::kOwnsProcessState)) {
set_abort_on_uncaught_exception(false);
}
@ -797,9 +800,8 @@ Environment::~Environment() {
// FreeEnvironment() should have set this.
CHECK(is_stopping());
if (options_->heap_snapshot_near_heap_limit > heap_limit_snapshot_taken_) {
isolate_->RemoveNearHeapLimitCallback(Environment::NearHeapLimitCallback,
0);
if (heapsnapshot_near_heap_limit_callback_added_) {
RemoveHeapSnapshotNearHeapLimitCallback(0);
}
isolate()->GetHeapProfiler()->RemoveBuildEmbedderGraphCallback(
@ -1788,8 +1790,7 @@ size_t Environment::NearHeapLimitCallback(void* data,
Debug(env,
DebugCategory::DIAGNOSTICS,
"Not generating snapshots because it's too risky.\n");
env->isolate()->RemoveNearHeapLimitCallback(NearHeapLimitCallback,
initial_heap_limit);
env->RemoveHeapSnapshotNearHeapLimitCallback(initial_heap_limit);
// The new limit must be higher than current_heap_limit or V8 might
// crash.
return current_heap_limit + 1;
@ -1809,17 +1810,15 @@ size_t Environment::NearHeapLimitCallback(void* data,
// Remove the callback first in case it's triggered when generating
// the snapshot.
env->isolate()->RemoveNearHeapLimitCallback(NearHeapLimitCallback,
initial_heap_limit);
env->RemoveHeapSnapshotNearHeapLimitCallback(initial_heap_limit);
heap::WriteSnapshot(env, filename.c_str());
env->heap_limit_snapshot_taken_ += 1;
// Don't take more snapshots than the number specified by
// --heapsnapshot-near-heap-limit.
if (env->heap_limit_snapshot_taken_ <
env->options_->heap_snapshot_near_heap_limit) {
env->isolate()->AddNearHeapLimitCallback(NearHeapLimitCallback, env);
if (env->heap_limit_snapshot_taken_ < env->heap_snapshot_near_heap_limit_) {
env->AddHeapSnapshotNearHeapLimitCallback();
}
FPrintF(stderr, "Wrote snapshot to %s\n", filename.c_str());

View File

@ -1040,6 +1040,12 @@ class Environment : public MemoryRetainer {
template <typename T>
void ForEachBaseObject(T&& iterator);
inline void set_heap_snapshot_near_heap_limit(uint32_t limit);
inline void AddHeapSnapshotNearHeapLimitCallback();
inline void RemoveHeapSnapshotNearHeapLimitCallback(size_t heap_limit);
private:
inline void ThrowError(v8::Local<v8::Value> (*fun)(v8::Local<v8::String>),
const char* errmsg);
@ -1097,7 +1103,9 @@ class Environment : public MemoryRetainer {
std::string exec_path_;
bool is_processing_heap_limit_callback_ = false;
int64_t heap_limit_snapshot_taken_ = 0;
uint32_t heap_limit_snapshot_taken_ = 0;
uint32_t heap_snapshot_near_heap_limit_ = 0;
bool heapsnapshot_near_heap_limit_callback_added_ = false;
uint32_t module_id_counter_ = 0;
uint32_t script_id_counter_ = 0;

View File

@ -241,9 +241,8 @@ static void AtomicsWaitCallback(Isolate::AtomicsWaitEvent event,
void Environment::InitializeDiagnostics() {
isolate_->GetHeapProfiler()->AddBuildEmbedderGraphCallback(
Environment::BuildEmbedderGraph, this);
if (options_->heap_snapshot_near_heap_limit > 0) {
isolate_->AddNearHeapLimitCallback(Environment::NearHeapLimitCallback,
this);
if (heap_snapshot_near_heap_limit_ > 0) {
AddHeapSnapshotNearHeapLimitCallback();
}
if (options_->trace_uncaught)
isolate_->SetCaptureStackTraceForUncaughtExceptions(true);

View File

@ -157,6 +157,15 @@ void CachedDataVersionTag(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(result);
}
void SetHeapSnapshotNearHeapLimit(const FunctionCallbackInfo<Value>& args) {
CHECK(args[0]->IsUint32());
Environment* env = Environment::GetCurrent(args);
uint32_t limit = args[0].As<v8::Uint32>()->Value();
CHECK_GT(limit, 0);
env->AddHeapSnapshotNearHeapLimitCallback();
env->set_heap_snapshot_near_heap_limit(limit);
}
void UpdateHeapStatisticsBuffer(const FunctionCallbackInfo<Value>& args) {
BindingData* data = Environment::GetBindingData<BindingData>(args);
HeapStatistics s;
@ -212,6 +221,10 @@ void Initialize(Local<Object> target,
SetMethodNoSideEffect(
context, target, "cachedDataVersionTag", CachedDataVersionTag);
SetMethodNoSideEffect(context,
target,
"setHeapSnapshotNearHeapLimit",
SetHeapSnapshotNearHeapLimit);
SetMethod(context,
target,
"updateHeapStatisticsBuffer",
@ -267,6 +280,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(UpdateHeapCodeStatisticsBuffer);
registry->Register(UpdateHeapSpaceStatisticsBuffer);
registry->Register(SetFlagsFromString);
registry->Register(SetHeapSnapshotNearHeapLimit);
}
} // namespace v8_utils

View File

@ -0,0 +1,9 @@
'use strict';
const path = require('path');
const v8 = require('v8');
v8.setHeapSnapshotNearHeapLimit(+process.env.limit);
if (process.env.limit2) {
v8.setHeapSnapshotNearHeapLimit(+process.env.limit2);
}
require(path.resolve(__dirname, 'grow.js'));

View File

@ -0,0 +1,15 @@
'use strict';
const path = require('path');
const { Worker } = require('worker_threads');
const max_snapshots = parseInt(process.env.TEST_SNAPSHOTS) || 1;
new Worker(path.join(__dirname, 'grow-and-set-near-heap-limit.js'), {
env: {
...process.env,
limit: max_snapshots,
},
resourceLimits: {
maxOldGenerationSizeMb:
parseInt(process.env.TEST_OLD_SPACE_SIZE) || 20
}
});

View File

@ -0,0 +1,41 @@
// Copy from test-heapsnapshot-near-heap-limit-worker.js
'use strict';
require('../common');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const { spawnSync } = require('child_process');
const fixtures = require('../common/fixtures');
const fs = require('fs');
const env = {
...process.env,
NODE_DEBUG_NATIVE: 'diagnostics'
};
{
tmpdir.refresh();
const child = spawnSync(process.execPath, [
fixtures.path('workload', 'grow-worker-and-set-near-heap-limit.js'),
], {
cwd: tmpdir.path,
env: {
TEST_SNAPSHOTS: 1,
TEST_OLD_SPACE_SIZE: 50,
...env
}
});
console.log(child.stdout.toString());
const stderr = child.stderr.toString();
console.log(stderr);
const risky = /Not generating snapshots because it's too risky/.test(stderr);
if (!risky) {
// There should be one snapshot taken and then after the
// snapshot heap limit callback is popped, the OOM callback
// becomes effective.
assert(stderr.includes('ERR_WORKER_OUT_OF_MEMORY'));
const list = fs.readdirSync(tmpdir.path)
.filter((file) => file.endsWith('.heapsnapshot'));
assert.strictEqual(list.length, 1);
}
}

View File

@ -0,0 +1,144 @@
// Copy from test-heapsnapshot-near-heap-limit.js
'use strict';
const common = require('../common');
if (common.isPi) {
common.skip('Too slow for Raspberry Pi devices');
}
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const { spawnSync } = require('child_process');
const fixtures = require('../common/fixtures');
const fs = require('fs');
const v8 = require('v8');
const invalidValues = [-1, '', {}, NaN, undefined];
for (let i = 0; i < invalidValues.length; i++) {
assert.throws(() => v8.setHeapSnapshotNearHeapLimit(invalidValues[i]),
/ERR_INVALID_ARG_TYPE|ERR_OUT_OF_RANGE/);
}
// Set twice
v8.setHeapSnapshotNearHeapLimit(1);
v8.setHeapSnapshotNearHeapLimit(2);
const env = {
...process.env,
NODE_DEBUG_NATIVE: 'diagnostics',
};
{
console.log('\nTesting set by cmd option and api');
tmpdir.refresh();
const child = spawnSync(process.execPath, [
'--trace-gc',
'--heapsnapshot-near-heap-limit=1',
'--max-old-space-size=50',
fixtures.path('workload', 'grow-and-set-near-heap-limit.js'),
], {
cwd: tmpdir.path,
env: {
...env,
limit: 2,
},
});
console.log(child.stdout.toString());
const stderr = child.stderr.toString();
console.log(stderr);
assert(common.nodeProcessAborted(child.status, child.signal),
'process should have aborted, but did not');
const list = fs.readdirSync(tmpdir.path)
.filter((file) => file.endsWith('.heapsnapshot'));
const risky = [...stderr.matchAll(
/Not generating snapshots because it's too risky/g)].length;
assert(list.length + risky > 0 && list.length <= 1,
`Generated ${list.length} snapshots ` +
`and ${risky} was too risky`);
}
{
console.log('\nTesting limit = 1');
tmpdir.refresh();
const child = spawnSync(process.execPath, [
'--trace-gc',
'--max-old-space-size=50',
fixtures.path('workload', 'grow-and-set-near-heap-limit.js'),
], {
cwd: tmpdir.path,
env: {
...env,
limit: 1,
},
});
console.log(child.stdout.toString());
const stderr = child.stderr.toString();
console.log(stderr);
assert(common.nodeProcessAborted(child.status, child.signal),
'process should have aborted, but did not');
const list = fs.readdirSync(tmpdir.path)
.filter((file) => file.endsWith('.heapsnapshot'));
const risky = [...stderr.matchAll(
/Not generating snapshots because it's too risky/g)].length;
assert(list.length + risky > 0 && list.length <= 1,
`Generated ${list.length} snapshots ` +
`and ${risky} was too risky`);
}
{
console.log('\nTesting set limit twice');
tmpdir.refresh();
const child = spawnSync(process.execPath, [
'--trace-gc',
'--max-old-space-size=50',
fixtures.path('workload', 'grow-and-set-near-heap-limit.js'),
], {
cwd: tmpdir.path,
env: {
...env,
limit: 1,
limit2: 2
},
});
console.log(child.stdout.toString());
const stderr = child.stderr.toString();
console.log(stderr);
assert(common.nodeProcessAborted(child.status, child.signal),
'process should have aborted, but did not');
const list = fs.readdirSync(tmpdir.path)
.filter((file) => file.endsWith('.heapsnapshot'));
const risky = [...stderr.matchAll(
/Not generating snapshots because it's too risky/g)].length;
assert(list.length + risky > 0 && list.length <= 1,
`Generated ${list.length} snapshots ` +
`and ${risky} was too risky`);
}
{
console.log('\nTesting limit = 3');
tmpdir.refresh();
const child = spawnSync(process.execPath, [
'--trace-gc',
'--max-old-space-size=50',
fixtures.path('workload', 'grow-and-set-near-heap-limit.js'),
], {
cwd: tmpdir.path,
env: {
...env,
limit: 3,
},
});
console.log(child.stdout.toString());
const stderr = child.stderr.toString();
console.log(stderr);
assert(common.nodeProcessAborted(child.status, child.signal),
'process should have aborted, but did not');
const list = fs.readdirSync(tmpdir.path)
.filter((file) => file.endsWith('.heapsnapshot'));
const risky = [...stderr.matchAll(
/Not generating snapshots because it's too risky/g)].length;
assert(list.length + risky > 0 && list.length <= 3,
`Generated ${list.length} snapshots ` +
`and ${risky} was too risky`);
}

View File

@ -71,7 +71,7 @@ const env = {
.filter((file) => file.endsWith('.heapsnapshot'));
const risky = [...stderr.matchAll(
/Not generating snapshots because it's too risky/g)].length;
assert(list.length + risky > 0 && list.length <= 3,
assert(list.length + risky > 0 && list.length <= 1,
`Generated ${list.length} snapshots ` +
`and ${risky} was too risky`);
}