From b77352d0a1f07a8d3ed126269cd799dcc7eb1c30 Mon Sep 17 00:00:00 2001 From: theanarkh Date: Sat, 27 Aug 2022 23:01:46 +0800 Subject: [PATCH] v8: add setHeapSnapshotNearHeapLimit --- doc/api/v8.md | 16 ++ lib/v8.js | 12 +- src/env.cc | 13 +- src/env.h | 26 +++ src/node.cc | 3 +- src/node_v8.cc | 29 +++ .../workload/grow-and-set-near-heap-limit.js | 9 + .../grow-worker-and-set-near-heap-limit.js | 15 ++ ...apshot-near-heap-limit-by-api-in-worker.js | 41 ++++ ...est-heapsnapshot-near-heap-limit-by-api.js | 175 ++++++++++++++++++ .../test-heapsnapshot-near-heap-limit.js | 2 +- 11 files changed, 332 insertions(+), 9 deletions(-) create mode 100644 test/fixtures/workload/grow-and-set-near-heap-limit.js create mode 100644 test/fixtures/workload/grow-worker-and-set-near-heap-limit.js create mode 100644 test/parallel/test-heapsnapshot-near-heap-limit-by-api-in-worker.js create mode 100644 test/pummel/test-heapsnapshot-near-heap-limit-by-api.js diff --git a/doc/api/v8.md b/doc/api/v8.md index 02e52619670482..4b55e7c975f25a 100644 --- a/doc/api/v8.md +++ b/doc/api/v8.md @@ -356,6 +356,21 @@ if (isMainThread) { } ``` +## `v8.setHeapSnapshotNearHeapLimit(limit)` + + + +> Stability: 1 - Experimental + +The API is a no-op if `--heapsnapshot-near-heap-limit` is already set from the +command line. And if `--heapsnapshot-near-heap-limit` is not set, the API can +be called with the value greater than 0. After the limit is set, you can call +this API with 0 to tell Node.js stop writing heap snapshot to disk. +Other case is also a no-op. See [`--heapsnapshot-near-heap-limit`][] for +more information. + ## Serialization API The serialization API provides means of serializing JavaScript values in a way @@ -1020,6 +1035,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 diff --git a/lib/v8.js b/lib/v8.js index db3dba59565458..bd38cdac8a5ffe 100644 --- a/lib/v8.js +++ b/lib/v8.js @@ -33,7 +33,7 @@ const { } = primordials; const { Buffer } = require('buffer'); -const { validateString } = require('internal/validators'); +const { validateString, validateNumber } = require('internal/validators'); const { Serializer, Deserializer @@ -59,7 +59,6 @@ const { } = internalBinding('heap_utils'); const { HeapSnapshotStream } = require('internal/heap_utils'); const promiseHooks = require('internal/promise_hooks'); - /** * Generates a snapshot of the current V8 heap * and writes it to a JSON file. @@ -95,6 +94,7 @@ const { updateHeapStatisticsBuffer, updateHeapSpaceStatisticsBuffer, updateHeapCodeStatisticsBuffer, + setHeapSnapshotNearHeapLimit: _setHeapSnapshotNearHeapLimit, // Properties for heap statistics buffer extraction. kTotalHeapSizeIndex, @@ -226,6 +226,11 @@ function getHeapCodeStatistics() { }; } +function setHeapSnapshotNearHeapLimit(limit) { + validateNumber(limit, 'limit', 0); + _setHeapSnapshotNearHeapLimit(limit); +} + /* V8 serialization API */ /* JS methods for the base objects */ @@ -387,5 +392,6 @@ module.exports = { serialize, writeHeapSnapshot, promiseHooks, - startupSnapshot + startupSnapshot, + setHeapSnapshotNearHeapLimit, }; diff --git a/src/env.cc b/src/env.cc index 9bc9346e653fe1..9c2e8329725eee 100644 --- a/src/env.cc +++ b/src/env.cc @@ -720,6 +720,8 @@ Environment::Environment(IsolateData* isolate_data, inspector_host_port_ = std::make_shared>( options_->debug_options().host_port); + set_heap_snapshot_near_heap_limit(options_->heap_snapshot_near_heap_limit); + if (!(flags_ & EnvironmentFlags::kOwnsProcessState)) { set_abort_on_uncaught_exception(false); } @@ -834,7 +836,8 @@ Environment::~Environment() { // FreeEnvironment() should have set this. CHECK(is_stopping()); - if (options_->heap_snapshot_near_heap_limit > heap_limit_snapshot_taken_) { + if (heapsnapshot_near_heap_limit_callback_added()) { + set_heapsnapshot_near_heap_limit_callback_added(false); isolate_->RemoveNearHeapLimitCallback(Environment::NearHeapLimitCallback, 0); } @@ -1952,6 +1955,7 @@ size_t Environment::NearHeapLimitCallback(void* data, Debug(env, DebugCategory::DIAGNOSTICS, "Not generating snapshots because it's too risky.\n"); + env->set_heapsnapshot_near_heap_limit_callback_added(false); env->isolate()->RemoveNearHeapLimitCallback(NearHeapLimitCallback, initial_heap_limit); // The new limit must be higher than current_heap_limit or V8 might @@ -1973,16 +1977,17 @@ size_t Environment::NearHeapLimitCallback(void* data, // Remove the callback first in case it's triggered when generating // the snapshot. + env->set_heapsnapshot_near_heap_limit_callback_added(false); env->isolate()->RemoveNearHeapLimitCallback(NearHeapLimitCallback, initial_heap_limit); heap::WriteSnapshot(env, filename.c_str()); - env->heap_limit_snapshot_taken_ += 1; + env->set_heap_limit_snapshot_taken(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) { + if (env->heap_limit_snapshot_taken() < env->heap_snapshot_near_heap_limit()) { + env->set_heapsnapshot_near_heap_limit_callback_added(true); env->isolate()->AddNearHeapLimitCallback(NearHeapLimitCallback, env); } diff --git a/src/env.h b/src/env.h index 6f148dbd041b83..9bcf092fa44de4 100644 --- a/src/env.h +++ b/src/env.h @@ -1489,6 +1489,30 @@ class Environment : public MemoryRetainer { template void ForEachBaseObject(T&& iterator); + inline int64_t heap_limit_snapshot_taken() { + return heap_limit_snapshot_taken_; + } + + inline int64_t set_heap_limit_snapshot_taken(int64_t count) { + return heap_limit_snapshot_taken_ = count; + } + + inline int64_t heap_snapshot_near_heap_limit() { + return heap_snapshot_near_heap_limit_; + } + + inline void set_heap_snapshot_near_heap_limit(int64_t limit) { + heap_snapshot_near_heap_limit_ = limit; + } + + inline bool heapsnapshot_near_heap_limit_callback_added() { + return near_heap_callback_is_added_; + } + + inline void set_heapsnapshot_near_heap_limit_callback_added(bool added) { + near_heap_callback_is_added_ = added; + } + private: inline void ThrowError(v8::Local (*fun)(v8::Local), const char* errmsg); @@ -1547,6 +1571,8 @@ class Environment : public MemoryRetainer { bool is_processing_heap_limit_callback_ = false; int64_t heap_limit_snapshot_taken_ = 0; + int64_t heap_snapshot_near_heap_limit_ = 0; + bool near_heap_callback_is_added_ = false; uint32_t module_id_counter_ = 0; uint32_t script_id_counter_ = 0; diff --git a/src/node.cc b/src/node.cc index 2891c18bb9aa9a..4aceffb77f9b4b 100644 --- a/src/node.cc +++ b/src/node.cc @@ -273,7 +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) { + if (heap_snapshot_near_heap_limit() > 0) { + set_heapsnapshot_near_heap_limit_callback_added(true); isolate_->AddNearHeapLimitCallback(Environment::NearHeapLimitCallback, this); } diff --git a/src/node_v8.cc b/src/node_v8.cc index 001d3464ec0e43..e8e4125b6bf012 100644 --- a/src/node_v8.cc +++ b/src/node_v8.cc @@ -157,6 +157,30 @@ void CachedDataVersionTag(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(result); } +void SetHeapSnapshotNearHeapLimit(const FunctionCallbackInfo& args) { + CHECK(args[0]->IsNumber()); + Environment* env = Environment::GetCurrent(args); + Isolate* const isolate = args.GetIsolate(); + int64_t limit = args[0].As()->Value(); + CHECK_GE(limit, 0); + // Do not set twice which will make the process crash. + if (limit == 0) { + if (env->heapsnapshot_near_heap_limit_callback_added()) { + env->set_heapsnapshot_near_heap_limit_callback_added(false); + isolate->RemoveNearHeapLimitCallback(Environment::NearHeapLimitCallback, + 0); + env->set_heap_snapshot_near_heap_limit(limit); + } + } else { + if (!env->heapsnapshot_near_heap_limit_callback_added()) { + env->set_heapsnapshot_near_heap_limit_callback_added(true); + isolate->AddNearHeapLimitCallback(Environment::NearHeapLimitCallback, + env); + env->set_heap_snapshot_near_heap_limit(limit); + } + } +} + void UpdateHeapStatisticsBuffer(const FunctionCallbackInfo& args) { BindingData* data = Environment::GetBindingData(args); HeapStatistics s; @@ -212,6 +236,10 @@ void Initialize(Local target, SetMethodNoSideEffect( context, target, "cachedDataVersionTag", CachedDataVersionTag); + SetMethodNoSideEffect(context, + target, + "setHeapSnapshotNearHeapLimit", + SetHeapSnapshotNearHeapLimit); SetMethod(context, target, "updateHeapStatisticsBuffer", @@ -267,6 +295,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(UpdateHeapCodeStatisticsBuffer); registry->Register(UpdateHeapSpaceStatisticsBuffer); registry->Register(SetFlagsFromString); + registry->Register(SetHeapSnapshotNearHeapLimit); } } // namespace v8_utils diff --git a/test/fixtures/workload/grow-and-set-near-heap-limit.js b/test/fixtures/workload/grow-and-set-near-heap-limit.js new file mode 100644 index 00000000000000..62d2c1ffdc5bd4 --- /dev/null +++ b/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')); diff --git a/test/fixtures/workload/grow-worker-and-set-near-heap-limit.js b/test/fixtures/workload/grow-worker-and-set-near-heap-limit.js new file mode 100644 index 00000000000000..598088bcf90a78 --- /dev/null +++ b/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 + } +}); + diff --git a/test/parallel/test-heapsnapshot-near-heap-limit-by-api-in-worker.js b/test/parallel/test-heapsnapshot-near-heap-limit-by-api-in-worker.js new file mode 100644 index 00000000000000..ac55933a47122b --- /dev/null +++ b/test/parallel/test-heapsnapshot-near-heap-limit-by-api-in-worker.js @@ -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); + } +} diff --git a/test/pummel/test-heapsnapshot-near-heap-limit-by-api.js b/test/pummel/test-heapsnapshot-near-heap-limit-by-api.js new file mode 100644 index 00000000000000..9b1716200dac3c --- /dev/null +++ b/test/pummel/test-heapsnapshot-near-heap-limit-by-api.js @@ -0,0 +1,175 @@ +// 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]; +let errorCount = 0; +for (let i = 0; i < invalidValues.length; i++) { + try { + v8.setHeapSnapshotNearHeapLimit(invalidValues[i]); + } catch (e) { + console.log(e); + errorCount++; + } +} +assert.strictEqual(errorCount, invalidValues.length); + +// 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 clear limit'); + 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: 0 + }, + }); + 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')); + assert.strictEqual(list.length, 0); +} + +{ + 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`); +} diff --git a/test/pummel/test-heapsnapshot-near-heap-limit.js b/test/pummel/test-heapsnapshot-near-heap-limit.js index 1af4e61e08c028..7677fe64ad854a 100644 --- a/test/pummel/test-heapsnapshot-near-heap-limit.js +++ b/test/pummel/test-heapsnapshot-near-heap-limit.js @@ -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`); }