Skip to content

Commit a08fc49

Browse files
committed
worker: add flag to control old space size
This adds a new flag `--thread-max-old-space-size` (name completely provisional). This has two advantages over the existing `--max-old-space-size` flag: 1. It allows setting the old space size for the main thread and using `resourceLimits` for worker threads. Currently `resourceLimits` will be ignored when `--max-old-space-size` is set (see the attached issues). 2. It is implemented using V8's public API, rather than relying on V8's internal flags whose stability and functionality are not guaranteed. The downside is that there are now two flags which (in most cases) do the same thing, so it may cause some confusion. I also think that we should deprecate `--max-old-space-size`, since the semantics feel pretty error-prone, but that's a story for another day. Refs: #41066 Refs: #43991 Refs: #43992
1 parent 7a18ee8 commit a08fc49

13 files changed

+154
-34
lines changed

doc/api/cli.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1125,6 +1125,20 @@ added: v18.0.0
11251125
Configures the test runner to only execute top level tests that have the `only`
11261126
option set.
11271127

1128+
### `--thread-max-old-space-size`
1129+
1130+
<!-- YAML
1131+
added: REPLACEME
1132+
-->
1133+
1134+
Sets the max memory size of V8's old memory section for the main thread (in
1135+
megabytes). As memory consumption approaches the limit, V8 will spend more time
1136+
on garbage collection in an effort to free unused memory.
1137+
1138+
Unlike [`--max-old-space-size`][], this option doesn't affect any additional
1139+
[worker threads][]. To configure the old space size for worker threads, pass in
1140+
an appropriate [`resourceLimits`][] to their constructor.
1141+
11281142
### `--throw-deprecation`
11291143

11301144
<!-- YAML
@@ -1719,6 +1733,7 @@ Node.js options that are allowed are:
17191733
* `--secure-heap-min`
17201734
* `--secure-heap`
17211735
* `--test-only`
1736+
* `--thread-max-old-space-size`
17221737
* `--throw-deprecation`
17231738
* `--title`
17241739
* `--tls-cipher-list`
@@ -2051,6 +2066,9 @@ Sets the max memory size of V8's old memory section. As memory
20512066
consumption approaches the limit, V8 will spend more time on
20522067
garbage collection in an effort to free unused memory.
20532068

2069+
Unlike [`--thread-max-old-space-size`][], this sets the max old space size of
2070+
all [worker threads][].
2071+
20542072
On a machine with 2 GiB of memory, consider setting this to
20552073
1536 (1.5 GiB) to leave some memory for other uses and avoid swapping.
20562074

@@ -2103,8 +2121,10 @@ done
21032121
[`--diagnostic-dir`]: #--diagnostic-dirdirectory
21042122
[`--experimental-wasm-modules`]: #--experimental-wasm-modules
21052123
[`--heap-prof-dir`]: #--heap-prof-dir
2124+
[`--max-old-space-size`]: #--max-old-space-sizesize-in-megabytes
21062125
[`--openssl-config`]: #--openssl-configfile
21072126
[`--redirect-warnings`]: #--redirect-warningsfile
2127+
[`--thread-max-old-space-size`]: #--thread-max-old-space-size
21082128
[`Atomics.wait()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/wait
21092129
[`Buffer`]: buffer.md#class-buffer
21102130
[`CRYPTO_secure_malloc_init`]: https://www.openssl.org/docs/man1.1.0/man3/CRYPTO_secure_malloc_init.html
@@ -2117,6 +2137,7 @@ done
21172137
[`dnsPromises.lookup()`]: dns.md#dnspromiseslookuphostname-options
21182138
[`import` specifier]: esm.md#import-specifiers
21192139
[`process.setUncaughtExceptionCaptureCallback()`]: process.md#processsetuncaughtexceptioncapturecallbackfn
2140+
[`resourceLimits`]: worker_threads.md#new-workerfilename-options
21202141
[`tls.DEFAULT_MAX_VERSION`]: tls.md#tlsdefault_max_version
21212142
[`tls.DEFAULT_MIN_VERSION`]: tls.md#tlsdefault_min_version
21222143
[`unhandledRejection`]: process.md#event-unhandledrejection
@@ -2136,3 +2157,4 @@ done
21362157
[semi-space]: https://www.memorymanagement.org/glossary/s.html#semi.space
21372158
[timezone IDs]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
21382159
[ways that `TZ` is handled in other environments]: https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
2160+
[worker threads]: worker_threads.md

doc/node.1

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,11 @@ Starts the Node.js command line test runner.
394394
Configures the test runner to only execute top level tests that have the `only`
395395
option set.
396396
.
397+
.It Fl -thread-max-old-space-size
398+
Sets the max memory size of V8's old memory section for the main thread (in
399+
megabytes). As memory consumption approaches the limit, V8 will spend more time
400+
on garbage collection in an effort to free unused memory.
401+
.
397402
.It Fl -throw-deprecation
398403
Throw errors for deprecations.
399404
.

src/api/environment.cc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,10 @@ IsolateData* CreateIsolateData(Isolate* isolate,
330330
uv_loop_t* loop,
331331
MultiIsolatePlatform* platform,
332332
ArrayBufferAllocator* allocator) {
333-
return new IsolateData(isolate, loop, platform, allocator);
333+
auto options = std::make_shared<PerIsolateOptions>(
334+
*(per_process::cli_options->per_isolate));
335+
return new IsolateData(
336+
isolate, loop, std::move(options), platform, allocator);
334337
}
335338

336339
void FreeIsolateData(IsolateData* isolate_data) {

src/env.cc

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -444,17 +444,16 @@ void IsolateData::CreateProperties() {
444444

445445
IsolateData::IsolateData(Isolate* isolate,
446446
uv_loop_t* event_loop,
447+
std::shared_ptr<PerIsolateOptions> options,
447448
MultiIsolatePlatform* platform,
448449
ArrayBufferAllocator* node_allocator,
449450
const IsolateDataSerializeInfo* isolate_data_info)
450451
: isolate_(isolate),
451452
event_loop_(event_loop),
453+
options_(options),
452454
node_allocator_(node_allocator == nullptr ? nullptr
453455
: node_allocator->GetImpl()),
454456
platform_(platform) {
455-
options_.reset(
456-
new PerIsolateOptions(*(per_process::cli_options->per_isolate)));
457-
458457
if (isolate_data_info == nullptr) {
459458
CreateProperties();
460459
} else {

src/env.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,7 @@ class NODE_EXTERN_PRIVATE IsolateData : public MemoryRetainer {
594594
public:
595595
IsolateData(v8::Isolate* isolate,
596596
uv_loop_t* event_loop,
597+
std::shared_ptr<PerIsolateOptions> options,
597598
MultiIsolatePlatform* platform = nullptr,
598599
ArrayBufferAllocator* node_allocator = nullptr,
599600
const IsolateDataSerializeInfo* isolate_data_info = nullptr);
@@ -668,9 +669,9 @@ class NODE_EXTERN_PRIVATE IsolateData : public MemoryRetainer {
668669

669670
v8::Isolate* const isolate_;
670671
uv_loop_t* const event_loop_;
672+
std::shared_ptr<PerIsolateOptions> options_;
671673
NodeArrayBufferAllocator* const node_allocator_;
672674
MultiIsolatePlatform* platform_;
673-
std::shared_ptr<PerIsolateOptions> options_;
674675
worker::Worker* worker_context_ = nullptr;
675676
};
676677

src/node_main_instance.cc

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ NodeMainInstance::NodeMainInstance(Isolate* isolate,
4040
platform_(platform),
4141
isolate_data_(nullptr),
4242
snapshot_data_(nullptr) {
43-
isolate_data_ =
44-
std::make_unique<IsolateData>(isolate_, event_loop, platform, nullptr);
43+
auto options = std::make_shared<PerIsolateOptions>(
44+
*(per_process::cli_options->per_isolate));
45+
isolate_data_ = std::make_unique<IsolateData>(
46+
isolate_, event_loop, std::move(options), platform, nullptr);
4547

4648
SetIsolateMiscHandlers(isolate_, {});
4749
}
@@ -77,16 +79,28 @@ NodeMainInstance::NodeMainInstance(const SnapshotData* snapshot_data,
7779

7880
isolate_ = Isolate::Allocate();
7981
CHECK_NOT_NULL(isolate_);
82+
83+
auto options = std::make_shared<PerIsolateOptions>(
84+
*(per_process::cli_options->per_isolate));
85+
8086
// Register the isolate on the platform before the isolate gets initialized,
8187
// so that the isolate can access the platform during initialization.
8288
platform->RegisterIsolate(isolate_, event_loop);
8389
SetIsolateCreateParamsForNode(isolate_params_.get());
90+
91+
size_t thread_max_old_space_size = options->thread_max_old_space_size;
92+
if (thread_max_old_space_size != 0) {
93+
isolate_params_->constraints.set_max_old_generation_size_in_bytes(
94+
thread_max_old_space_size * 1024 * 1024);
95+
}
96+
8497
Isolate::Initialize(isolate_, *isolate_params_);
8598

8699
// If the indexes are not nullptr, we are not deserializing
87100
isolate_data_ = std::make_unique<IsolateData>(
88101
isolate_,
89102
event_loop,
103+
std::move(options),
90104
platform,
91105
array_buffer_allocator_.get(),
92106
snapshot_data == nullptr ? nullptr : &(snapshot_data->isolate_data_info));

src/node_options.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,12 @@ PerIsolateOptionsParser::PerIsolateOptionsParser(
661661
&PerIsolateOptions::track_heap_objects,
662662
kAllowedInEnvironment);
663663

664+
AddOption(
665+
"--thread-max-old-space-size",
666+
"set the maximum old space heap size (in megabytes) for this isolate",
667+
&PerIsolateOptions::thread_max_old_space_size,
668+
kAllowedInEnvironment);
669+
664670
// Explicitly add some V8 flags to mark them as allowed in NODE_OPTIONS.
665671
AddOption("--abort-on-uncaught-exception",
666672
"aborting instead of exiting causes a core file to be generated "

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ class PerIsolateOptions : public Options {
208208
bool report_uncaught_exception = false;
209209
bool report_on_signal = false;
210210
bool experimental_shadow_realm = false;
211+
size_t thread_max_old_space_size = 0;
211212
std::string report_signal = "SIGUSR2";
212213
inline EnvironmentOptions* get_per_env_options();
213214
void CheckOptions(std::vector<std::string>* errors) override;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use strict';
2+
const assert = require('assert');
3+
const v8 = require('v8');
4+
5+
function allocateUntilCrash(resourceLimits) {
6+
const array = [];
7+
while (true) {
8+
const usedMB = v8.getHeapStatistics().used_heap_size / 1024 / 1024;
9+
const maxReservedSize = resourceLimits.maxOldGenerationSizeMb +
10+
resourceLimits.maxYoungGenerationSizeMb;
11+
assert(usedMB < maxReservedSize);
12+
13+
let seenSpaces = 0;
14+
for (const { space_name, space_size } of v8.getHeapSpaceStatistics()) {
15+
if (space_name === 'new_space') {
16+
seenSpaces++;
17+
assert(
18+
space_size / 1024 / 1024 < resourceLimits.maxYoungGenerationSizeMb * 2);
19+
} else if (space_name === 'old_space') {
20+
seenSpaces++;
21+
assert(space_size / 1024 / 1024 < resourceLimits.maxOldGenerationSizeMb);
22+
} else if (space_name === 'code_space') {
23+
seenSpaces++;
24+
assert(space_size / 1024 / 1024 < resourceLimits.codeRangeSizeMb);
25+
}
26+
}
27+
assert.strictEqual(seenSpaces, 3);
28+
29+
for (let i = 0; i < 100; i++)
30+
array.push([array]);
31+
}
32+
}
33+
34+
module.exports = { allocateUntilCrash };
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const { allocateUntilCrash } = require('../common/allocate-and-check-limits');
2+
const resourceLimits = JSON.parse(process.argv[2]);
3+
allocateUntilCrash(resourceLimits);
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use strict';
2+
3+
require('../common');
4+
const assert = require('assert');
5+
const fixtures = require('../common/fixtures');
6+
const fixture = fixtures.path('thread-max-old-space-size.js');
7+
const { spawnSync } = require('child_process');
8+
const resourceLimits = {
9+
maxOldGenerationSizeMb: 16,
10+
maxYoungGenerationSizeMb: 4,
11+
// Set codeRangeSizeMb really high to effectively ignore it.
12+
codeRangeSizeMb: 999999,
13+
stackSizeMb: 4,
14+
};
15+
const res = spawnSync(process.execPath, [
16+
`--stack-size=${1024 * resourceLimits.stackSizeMb}`,
17+
`--thread-max-old-space-size=${resourceLimits.maxOldGenerationSizeMb}`,
18+
`--max-semi-space-size=${resourceLimits.maxYoungGenerationSizeMb / 2}`,
19+
fixture,
20+
JSON.stringify(resourceLimits)
21+
]);
22+
assert(res.stderr.toString('utf8').includes('Allocation failed - JavaScript heap out of memory'));
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Flags: --thread-max-old-space-size=1024
2+
'use strict';
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const { Worker, resourceLimits } = require('worker_threads');
6+
const { allocateUntilCrash } = require('../common/allocate-and-check-limits');
7+
8+
const testResourceLimits = {
9+
maxOldGenerationSizeMb: 16,
10+
maxYoungGenerationSizeMb: 4,
11+
codeRangeSizeMb: 16,
12+
stackSizeMb: 1,
13+
};
14+
15+
// Do not use isMainThread so that this test itself can be run inside a Worker.
16+
if (!process.env.HAS_STARTED_WORKER) {
17+
process.env.HAS_STARTED_WORKER = 1;
18+
const w = new Worker(__filename, { resourceLimits: testResourceLimits });
19+
assert.deepStrictEqual(w.resourceLimits, testResourceLimits);
20+
w.on('exit', common.mustCall((code) => {
21+
assert.strictEqual(code, 1);
22+
assert.deepStrictEqual(w.resourceLimits, {});
23+
}));
24+
w.on('error', common.expectsError({
25+
code: 'ERR_WORKER_OUT_OF_MEMORY',
26+
message: 'Worker terminated due to reaching memory limit: ' +
27+
'JS heap out of memory'
28+
}));
29+
return;
30+
}
31+
32+
assert.deepStrictEqual(resourceLimits, testResourceLimits);
33+
// resourceLimits should be used; --thread-max-old-space-size should only
34+
// affect the main thread.
35+
allocateUntilCrash(resourceLimits);
Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
'use strict';
22
const common = require('../common');
33
const assert = require('assert');
4-
const v8 = require('v8');
54
const { Worker, resourceLimits, isMainThread } = require('worker_threads');
5+
const { allocateUntilCrash } = require('../common/allocate-and-check-limits');
66

77
if (isMainThread) {
88
assert.deepStrictEqual(resourceLimits, {});
@@ -33,29 +33,4 @@ if (!process.env.HAS_STARTED_WORKER) {
3333
}
3434

3535
assert.deepStrictEqual(resourceLimits, testResourceLimits);
36-
const array = [];
37-
while (true) {
38-
const usedMB = v8.getHeapStatistics().used_heap_size / 1024 / 1024;
39-
const maxReservedSize = resourceLimits.maxOldGenerationSizeMb +
40-
resourceLimits.maxYoungGenerationSizeMb;
41-
assert(usedMB < maxReservedSize);
42-
43-
let seenSpaces = 0;
44-
for (const { space_name, space_size } of v8.getHeapSpaceStatistics()) {
45-
if (space_name === 'new_space') {
46-
seenSpaces++;
47-
assert(
48-
space_size / 1024 / 1024 < resourceLimits.maxYoungGenerationSizeMb * 2);
49-
} else if (space_name === 'old_space') {
50-
seenSpaces++;
51-
assert(space_size / 1024 / 1024 < resourceLimits.maxOldGenerationSizeMb);
52-
} else if (space_name === 'code_space') {
53-
seenSpaces++;
54-
assert(space_size / 1024 / 1024 < resourceLimits.codeRangeSizeMb);
55-
}
56-
}
57-
assert.strictEqual(seenSpaces, 3);
58-
59-
for (let i = 0; i < 100; i++)
60-
array.push([array]);
61-
}
36+
allocateUntilCrash(resourceLimits);

0 commit comments

Comments
 (0)