Skip to content

Commit e62f6ce

Browse files
authoredSep 8, 2022
v8: add setHeapSnapshotNearHeapLimit
PR-URL: #44420 Refs: #33010 Reviewed-By: Joyee Cheung <[email protected]> Reviewed-By: Chengzhong Wu <[email protected]>
1 parent 0b5b5ed commit e62f6ce

12 files changed

+294
-17
lines changed
 

‎doc/api/v8.md

+15
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,20 @@ if (isMainThread) {
356356
}
357357
```
358358

359+
## `v8.setHeapSnapshotNearHeapLimit(limit)`
360+
361+
<!-- YAML
362+
added: REPLACEME
363+
-->
364+
365+
> Stability: 1 - Experimental
366+
367+
* `limit` {integer}
368+
369+
The API is a no-op if `--heapsnapshot-near-heap-limit` is already set from the
370+
command line or the API is called more than once. `limit` must be a positive
371+
integer. See [`--heapsnapshot-near-heap-limit`][] for more information.
372+
359373
## Serialization API
360374

361375
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.
10201034
[HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
10211035
[Hook Callbacks]: #hook-callbacks
10221036
[V8]: https://developers.google.com/v8/
1037+
[`--heapsnapshot-near-heap-limit`]: cli.md#--heapsnapshot-near-heap-limitmax_count
10231038
[`AsyncLocalStorage`]: async_context.md#class-asynclocalstorage
10241039
[`Buffer`]: buffer.md
10251040
[`DefaultDeserializer`]: #class-v8defaultdeserializer

‎lib/v8.js

+17-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const {
3333
} = primordials;
3434

3535
const { Buffer } = require('buffer');
36-
const { validateString } = require('internal/validators');
36+
const { validateString, validateUint32 } = require('internal/validators');
3737
const {
3838
Serializer,
3939
Deserializer
@@ -59,6 +59,7 @@ const {
5959
} = internalBinding('heap_utils');
6060
const { HeapSnapshotStream } = require('internal/heap_utils');
6161
const promiseHooks = require('internal/promise_hooks');
62+
const { getOptionValue } = require('internal/options');
6263

6364
/**
6465
* Generates a snapshot of the current V8 heap
@@ -95,6 +96,7 @@ const {
9596
updateHeapStatisticsBuffer,
9697
updateHeapSpaceStatisticsBuffer,
9798
updateHeapCodeStatisticsBuffer,
99+
setHeapSnapshotNearHeapLimit: _setHeapSnapshotNearHeapLimit,
98100

99101
// Properties for heap statistics buffer extraction.
100102
kTotalHeapSizeIndex,
@@ -226,6 +228,18 @@ function getHeapCodeStatistics() {
226228
};
227229
}
228230

231+
let heapSnapshotNearHeapLimitCallbackAdded = false;
232+
function setHeapSnapshotNearHeapLimit(limit) {
233+
validateUint32(limit, 'limit', 1);
234+
if (heapSnapshotNearHeapLimitCallbackAdded ||
235+
getOptionValue('--heapsnapshot-near-heap-limit') > 0
236+
) {
237+
return;
238+
}
239+
heapSnapshotNearHeapLimitCallbackAdded = true;
240+
_setHeapSnapshotNearHeapLimit(limit);
241+
}
242+
229243
/* V8 serialization API */
230244

231245
/* JS methods for the base objects */
@@ -387,5 +401,6 @@ module.exports = {
387401
serialize,
388402
writeHeapSnapshot,
389403
promiseHooks,
390-
startupSnapshot
404+
startupSnapshot,
405+
setHeapSnapshotNearHeapLimit,
391406
};

‎src/env-inl.h

+18
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,24 @@ Realm* Environment::principal_realm() const {
892892
return principal_realm_.get();
893893
}
894894

895+
inline void Environment::set_heap_snapshot_near_heap_limit(uint32_t limit) {
896+
heap_snapshot_near_heap_limit_ = limit;
897+
}
898+
899+
inline void Environment::AddHeapSnapshotNearHeapLimitCallback() {
900+
DCHECK(!heapsnapshot_near_heap_limit_callback_added_);
901+
heapsnapshot_near_heap_limit_callback_added_ = true;
902+
isolate_->AddNearHeapLimitCallback(Environment::NearHeapLimitCallback, this);
903+
}
904+
905+
inline void Environment::RemoveHeapSnapshotNearHeapLimitCallback(
906+
size_t heap_limit) {
907+
DCHECK(heapsnapshot_near_heap_limit_callback_added_);
908+
heapsnapshot_near_heap_limit_callback_added_ = false;
909+
isolate_->RemoveNearHeapLimitCallback(Environment::NearHeapLimitCallback,
910+
heap_limit);
911+
}
912+
895913
} // namespace node
896914

897915
// These two files depend on each other. Including base_object-inl.h after this

‎src/env.cc

+9-10
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,9 @@ Environment::Environment(IsolateData* isolate_data,
682682
inspector_host_port_ = std::make_shared<ExclusiveAccess<HostPort>>(
683683
options_->debug_options().host_port);
684684

685+
heap_snapshot_near_heap_limit_ =
686+
static_cast<uint32_t>(options_->heap_snapshot_near_heap_limit);
687+
685688
if (!(flags_ & EnvironmentFlags::kOwnsProcessState)) {
686689
set_abort_on_uncaught_exception(false);
687690
}
@@ -797,9 +800,8 @@ Environment::~Environment() {
797800
// FreeEnvironment() should have set this.
798801
CHECK(is_stopping());
799802

800-
if (options_->heap_snapshot_near_heap_limit > heap_limit_snapshot_taken_) {
801-
isolate_->RemoveNearHeapLimitCallback(Environment::NearHeapLimitCallback,
802-
0);
803+
if (heapsnapshot_near_heap_limit_callback_added_) {
804+
RemoveHeapSnapshotNearHeapLimitCallback(0);
803805
}
804806

805807
isolate()->GetHeapProfiler()->RemoveBuildEmbedderGraphCallback(
@@ -1788,8 +1790,7 @@ size_t Environment::NearHeapLimitCallback(void* data,
17881790
Debug(env,
17891791
DebugCategory::DIAGNOSTICS,
17901792
"Not generating snapshots because it's too risky.\n");
1791-
env->isolate()->RemoveNearHeapLimitCallback(NearHeapLimitCallback,
1792-
initial_heap_limit);
1793+
env->RemoveHeapSnapshotNearHeapLimitCallback(initial_heap_limit);
17931794
// The new limit must be higher than current_heap_limit or V8 might
17941795
// crash.
17951796
return current_heap_limit + 1;
@@ -1809,17 +1810,15 @@ size_t Environment::NearHeapLimitCallback(void* data,
18091810

18101811
// Remove the callback first in case it's triggered when generating
18111812
// the snapshot.
1812-
env->isolate()->RemoveNearHeapLimitCallback(NearHeapLimitCallback,
1813-
initial_heap_limit);
1813+
env->RemoveHeapSnapshotNearHeapLimitCallback(initial_heap_limit);
18141814

18151815
heap::WriteSnapshot(env, filename.c_str());
18161816
env->heap_limit_snapshot_taken_ += 1;
18171817

18181818
// Don't take more snapshots than the number specified by
18191819
// --heapsnapshot-near-heap-limit.
1820-
if (env->heap_limit_snapshot_taken_ <
1821-
env->options_->heap_snapshot_near_heap_limit) {
1822-
env->isolate()->AddNearHeapLimitCallback(NearHeapLimitCallback, env);
1820+
if (env->heap_limit_snapshot_taken_ < env->heap_snapshot_near_heap_limit_) {
1821+
env->AddHeapSnapshotNearHeapLimitCallback();
18231822
}
18241823

18251824
FPrintF(stderr, "Wrote snapshot to %s\n", filename.c_str());

‎src/env.h

+9-1
Original file line numberDiff line numberDiff line change
@@ -1040,6 +1040,12 @@ class Environment : public MemoryRetainer {
10401040
template <typename T>
10411041
void ForEachBaseObject(T&& iterator);
10421042

1043+
inline void set_heap_snapshot_near_heap_limit(uint32_t limit);
1044+
1045+
inline void AddHeapSnapshotNearHeapLimitCallback();
1046+
1047+
inline void RemoveHeapSnapshotNearHeapLimitCallback(size_t heap_limit);
1048+
10431049
private:
10441050
inline void ThrowError(v8::Local<v8::Value> (*fun)(v8::Local<v8::String>),
10451051
const char* errmsg);
@@ -1097,7 +1103,9 @@ class Environment : public MemoryRetainer {
10971103
std::string exec_path_;
10981104

10991105
bool is_processing_heap_limit_callback_ = false;
1100-
int64_t heap_limit_snapshot_taken_ = 0;
1106+
uint32_t heap_limit_snapshot_taken_ = 0;
1107+
uint32_t heap_snapshot_near_heap_limit_ = 0;
1108+
bool heapsnapshot_near_heap_limit_callback_added_ = false;
11011109

11021110
uint32_t module_id_counter_ = 0;
11031111
uint32_t script_id_counter_ = 0;

‎src/node.cc

+2-3
Original file line numberDiff line numberDiff line change
@@ -241,9 +241,8 @@ static void AtomicsWaitCallback(Isolate::AtomicsWaitEvent event,
241241
void Environment::InitializeDiagnostics() {
242242
isolate_->GetHeapProfiler()->AddBuildEmbedderGraphCallback(
243243
Environment::BuildEmbedderGraph, this);
244-
if (options_->heap_snapshot_near_heap_limit > 0) {
245-
isolate_->AddNearHeapLimitCallback(Environment::NearHeapLimitCallback,
246-
this);
244+
if (heap_snapshot_near_heap_limit_ > 0) {
245+
AddHeapSnapshotNearHeapLimitCallback();
247246
}
248247
if (options_->trace_uncaught)
249248
isolate_->SetCaptureStackTraceForUncaughtExceptions(true);

‎src/node_v8.cc

+14
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,15 @@ void CachedDataVersionTag(const FunctionCallbackInfo<Value>& args) {
157157
args.GetReturnValue().Set(result);
158158
}
159159

160+
void SetHeapSnapshotNearHeapLimit(const FunctionCallbackInfo<Value>& args) {
161+
CHECK(args[0]->IsUint32());
162+
Environment* env = Environment::GetCurrent(args);
163+
uint32_t limit = args[0].As<v8::Uint32>()->Value();
164+
CHECK_GT(limit, 0);
165+
env->AddHeapSnapshotNearHeapLimitCallback();
166+
env->set_heap_snapshot_near_heap_limit(limit);
167+
}
168+
160169
void UpdateHeapStatisticsBuffer(const FunctionCallbackInfo<Value>& args) {
161170
BindingData* data = Environment::GetBindingData<BindingData>(args);
162171
HeapStatistics s;
@@ -212,6 +221,10 @@ void Initialize(Local<Object> target,
212221

213222
SetMethodNoSideEffect(
214223
context, target, "cachedDataVersionTag", CachedDataVersionTag);
224+
SetMethodNoSideEffect(context,
225+
target,
226+
"setHeapSnapshotNearHeapLimit",
227+
SetHeapSnapshotNearHeapLimit);
215228
SetMethod(context,
216229
target,
217230
"updateHeapStatisticsBuffer",
@@ -267,6 +280,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
267280
registry->Register(UpdateHeapCodeStatisticsBuffer);
268281
registry->Register(UpdateHeapSpaceStatisticsBuffer);
269282
registry->Register(SetFlagsFromString);
283+
registry->Register(SetHeapSnapshotNearHeapLimit);
270284
}
271285

272286
} // namespace v8_utils
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use strict';
2+
const path = require('path');
3+
const v8 = require('v8');
4+
5+
v8.setHeapSnapshotNearHeapLimit(+process.env.limit);
6+
if (process.env.limit2) {
7+
v8.setHeapSnapshotNearHeapLimit(+process.env.limit2);
8+
}
9+
require(path.resolve(__dirname, 'grow.js'));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict';
2+
const path = require('path');
3+
const { Worker } = require('worker_threads');
4+
const max_snapshots = parseInt(process.env.TEST_SNAPSHOTS) || 1;
5+
new Worker(path.join(__dirname, 'grow-and-set-near-heap-limit.js'), {
6+
env: {
7+
...process.env,
8+
limit: max_snapshots,
9+
},
10+
resourceLimits: {
11+
maxOldGenerationSizeMb:
12+
parseInt(process.env.TEST_OLD_SPACE_SIZE) || 20
13+
}
14+
});
15+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copy from test-heapsnapshot-near-heap-limit-worker.js
2+
'use strict';
3+
4+
require('../common');
5+
const tmpdir = require('../common/tmpdir');
6+
const assert = require('assert');
7+
const { spawnSync } = require('child_process');
8+
const fixtures = require('../common/fixtures');
9+
const fs = require('fs');
10+
11+
const env = {
12+
...process.env,
13+
NODE_DEBUG_NATIVE: 'diagnostics'
14+
};
15+
16+
{
17+
tmpdir.refresh();
18+
const child = spawnSync(process.execPath, [
19+
fixtures.path('workload', 'grow-worker-and-set-near-heap-limit.js'),
20+
], {
21+
cwd: tmpdir.path,
22+
env: {
23+
TEST_SNAPSHOTS: 1,
24+
TEST_OLD_SPACE_SIZE: 50,
25+
...env
26+
}
27+
});
28+
console.log(child.stdout.toString());
29+
const stderr = child.stderr.toString();
30+
console.log(stderr);
31+
const risky = /Not generating snapshots because it's too risky/.test(stderr);
32+
if (!risky) {
33+
// There should be one snapshot taken and then after the
34+
// snapshot heap limit callback is popped, the OOM callback
35+
// becomes effective.
36+
assert(stderr.includes('ERR_WORKER_OUT_OF_MEMORY'));
37+
const list = fs.readdirSync(tmpdir.path)
38+
.filter((file) => file.endsWith('.heapsnapshot'));
39+
assert.strictEqual(list.length, 1);
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// Copy from test-heapsnapshot-near-heap-limit.js
2+
'use strict';
3+
4+
const common = require('../common');
5+
6+
if (common.isPi) {
7+
common.skip('Too slow for Raspberry Pi devices');
8+
}
9+
10+
const tmpdir = require('../common/tmpdir');
11+
const assert = require('assert');
12+
const { spawnSync } = require('child_process');
13+
const fixtures = require('../common/fixtures');
14+
const fs = require('fs');
15+
const v8 = require('v8');
16+
17+
const invalidValues = [-1, '', {}, NaN, undefined];
18+
for (let i = 0; i < invalidValues.length; i++) {
19+
assert.throws(() => v8.setHeapSnapshotNearHeapLimit(invalidValues[i]),
20+
/ERR_INVALID_ARG_TYPE|ERR_OUT_OF_RANGE/);
21+
}
22+
23+
// Set twice
24+
v8.setHeapSnapshotNearHeapLimit(1);
25+
v8.setHeapSnapshotNearHeapLimit(2);
26+
27+
const env = {
28+
...process.env,
29+
NODE_DEBUG_NATIVE: 'diagnostics',
30+
};
31+
32+
{
33+
console.log('\nTesting set by cmd option and api');
34+
tmpdir.refresh();
35+
const child = spawnSync(process.execPath, [
36+
'--trace-gc',
37+
'--heapsnapshot-near-heap-limit=1',
38+
'--max-old-space-size=50',
39+
fixtures.path('workload', 'grow-and-set-near-heap-limit.js'),
40+
], {
41+
cwd: tmpdir.path,
42+
env: {
43+
...env,
44+
limit: 2,
45+
},
46+
});
47+
console.log(child.stdout.toString());
48+
const stderr = child.stderr.toString();
49+
console.log(stderr);
50+
assert(common.nodeProcessAborted(child.status, child.signal),
51+
'process should have aborted, but did not');
52+
const list = fs.readdirSync(tmpdir.path)
53+
.filter((file) => file.endsWith('.heapsnapshot'));
54+
const risky = [...stderr.matchAll(
55+
/Not generating snapshots because it's too risky/g)].length;
56+
assert(list.length + risky > 0 && list.length <= 1,
57+
`Generated ${list.length} snapshots ` +
58+
`and ${risky} was too risky`);
59+
}
60+
61+
{
62+
console.log('\nTesting limit = 1');
63+
tmpdir.refresh();
64+
const child = spawnSync(process.execPath, [
65+
'--trace-gc',
66+
'--max-old-space-size=50',
67+
fixtures.path('workload', 'grow-and-set-near-heap-limit.js'),
68+
], {
69+
cwd: tmpdir.path,
70+
env: {
71+
...env,
72+
limit: 1,
73+
},
74+
});
75+
console.log(child.stdout.toString());
76+
const stderr = child.stderr.toString();
77+
console.log(stderr);
78+
assert(common.nodeProcessAborted(child.status, child.signal),
79+
'process should have aborted, but did not');
80+
const list = fs.readdirSync(tmpdir.path)
81+
.filter((file) => file.endsWith('.heapsnapshot'));
82+
const risky = [...stderr.matchAll(
83+
/Not generating snapshots because it's too risky/g)].length;
84+
assert(list.length + risky > 0 && list.length <= 1,
85+
`Generated ${list.length} snapshots ` +
86+
`and ${risky} was too risky`);
87+
}
88+
89+
{
90+
console.log('\nTesting set limit twice');
91+
tmpdir.refresh();
92+
const child = spawnSync(process.execPath, [
93+
'--trace-gc',
94+
'--max-old-space-size=50',
95+
fixtures.path('workload', 'grow-and-set-near-heap-limit.js'),
96+
], {
97+
cwd: tmpdir.path,
98+
env: {
99+
...env,
100+
limit: 1,
101+
limit2: 2
102+
},
103+
});
104+
console.log(child.stdout.toString());
105+
const stderr = child.stderr.toString();
106+
console.log(stderr);
107+
assert(common.nodeProcessAborted(child.status, child.signal),
108+
'process should have aborted, but did not');
109+
const list = fs.readdirSync(tmpdir.path)
110+
.filter((file) => file.endsWith('.heapsnapshot'));
111+
const risky = [...stderr.matchAll(
112+
/Not generating snapshots because it's too risky/g)].length;
113+
assert(list.length + risky > 0 && list.length <= 1,
114+
`Generated ${list.length} snapshots ` +
115+
`and ${risky} was too risky`);
116+
}
117+
118+
{
119+
console.log('\nTesting limit = 3');
120+
tmpdir.refresh();
121+
const child = spawnSync(process.execPath, [
122+
'--trace-gc',
123+
'--max-old-space-size=50',
124+
fixtures.path('workload', 'grow-and-set-near-heap-limit.js'),
125+
], {
126+
cwd: tmpdir.path,
127+
env: {
128+
...env,
129+
limit: 3,
130+
},
131+
});
132+
console.log(child.stdout.toString());
133+
const stderr = child.stderr.toString();
134+
console.log(stderr);
135+
assert(common.nodeProcessAborted(child.status, child.signal),
136+
'process should have aborted, but did not');
137+
const list = fs.readdirSync(tmpdir.path)
138+
.filter((file) => file.endsWith('.heapsnapshot'));
139+
const risky = [...stderr.matchAll(
140+
/Not generating snapshots because it's too risky/g)].length;
141+
assert(list.length + risky > 0 && list.length <= 3,
142+
`Generated ${list.length} snapshots ` +
143+
`and ${risky} was too risky`);
144+
}

‎test/pummel/test-heapsnapshot-near-heap-limit.js

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

0 commit comments

Comments
 (0)
Please sign in to comment.