Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v8: add setHeapSnapshotNearHeapLimit #44420

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 15 additions & 0 deletions doc/api/v8.md
Expand Up @@ -356,6 +356,20 @@ if (isMainThread) {
}
```

## `v8.setHeapSnapshotNearHeapLimit(limit)`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental

theanarkh marked this conversation as resolved.
Show resolved Hide resolved
* `limit` {integer}

The API is a no-op if `--heapsnapshot-near-heap-limit` is already set from the
theanarkh marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down Expand Up @@ -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
Expand Down
19 changes: 17 additions & 2 deletions lib/v8.js
Expand Up @@ -33,7 +33,7 @@ const {
} = primordials;

const { Buffer } = require('buffer');
const { validateString } = require('internal/validators');
const { validateString, validateUint32 } = require('internal/validators');
const {
Serializer,
Deserializer
Expand All @@ -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
Expand Down Expand Up @@ -95,6 +96,7 @@ const {
updateHeapStatisticsBuffer,
updateHeapSpaceStatisticsBuffer,
updateHeapCodeStatisticsBuffer,
setHeapSnapshotNearHeapLimit: _setHeapSnapshotNearHeapLimit,

// Properties for heap statistics buffer extraction.
kTotalHeapSizeIndex,
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -387,5 +401,6 @@ module.exports = {
serialize,
writeHeapSnapshot,
promiseHooks,
startupSnapshot
startupSnapshot,
setHeapSnapshotNearHeapLimit,
};
18 changes: 18 additions & 0 deletions src/env-inl.h
Expand Up @@ -914,6 +914,24 @@ v8::Local<v8::Context> Environment::context() const {
return PersistentToLocal::Strong(context_);
}

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
Expand Down
19 changes: 9 additions & 10 deletions src/env.cc
Expand Up @@ -720,6 +720,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);
}
Expand Down Expand Up @@ -834,9 +837,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(
Expand Down Expand Up @@ -1952,8 +1954,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;
Expand All @@ -1973,17 +1974,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());
Expand Down
10 changes: 9 additions & 1 deletion src/env.h
Expand Up @@ -1489,6 +1489,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);
Expand Down Expand Up @@ -1546,7 +1552,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;
Expand Down
5 changes: 2 additions & 3 deletions src/node.cc
Expand Up @@ -273,9 +273,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);
Expand Down
14 changes: 14 additions & 0 deletions src/node_v8.cc
Expand Up @@ -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;
Expand Down Expand Up @@ -212,6 +221,10 @@ void Initialize(Local<Object> target,

SetMethodNoSideEffect(
context, target, "cachedDataVersionTag", CachedDataVersionTag);
SetMethodNoSideEffect(context,
target,
"setHeapSnapshotNearHeapLimit",
SetHeapSnapshotNearHeapLimit);
SetMethod(context,
target,
"updateHeapStatisticsBuffer",
Expand Down Expand Up @@ -267,6 +280,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(UpdateHeapCodeStatisticsBuffer);
registry->Register(UpdateHeapSpaceStatisticsBuffer);
registry->Register(SetFlagsFromString);
registry->Register(SetHeapSnapshotNearHeapLimit);
}

} // namespace v8_utils
Expand Down
9 changes: 9 additions & 0 deletions test/fixtures/workload/grow-and-set-near-heap-limit.js
@@ -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'));
15 changes: 15 additions & 0 deletions test/fixtures/workload/grow-worker-and-set-near-heap-limit.js
@@ -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
}
});

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