Skip to content

Commit b85ba6d

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: nodejs#41066 Refs: nodejs#43991 Refs: nodejs#43992
1 parent 0484022 commit b85ba6d

13 files changed

+148
-34
lines changed

doc/api/cli.md

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

1120+
### `--thread-max-old-space-size`
1121+
1122+
<!-- YAML
1123+
added: REPLACEME
1124+
-->
1125+
1126+
Sets the max memory size of V8's old memory section for the main thread (in
1127+
megabytes). As memory consumption approaches the limit, V8 will spend more time
1128+
on garbage collection in an effort to free unused memory.
1129+
1130+
Unlike [`--max-old-space-size`][], this option doesn't affect any additional
1131+
[worker threads][]. To configure the old space size for worker threads, pass in
1132+
an appropriate [`resourceLimits`][] to their constructor.
1133+
11201134
### `--throw-deprecation`
11211135

11221136
<!-- YAML
@@ -1710,6 +1724,7 @@ Node.js options that are allowed are:
17101724
* `--secure-heap-min`
17111725
* `--secure-heap`
17121726
* `--test-only`
1727+
* `--thread-max-old-space-size`
17131728
* `--throw-deprecation`
17141729
* `--title`
17151730
* `--tls-cipher-list`
@@ -2042,6 +2057,9 @@ Sets the max memory size of V8's old memory section. As memory
20422057
consumption approaches the limit, V8 will spend more time on
20432058
garbage collection in an effort to free unused memory.
20442059

2060+
Unlike [`--thread-max-old-space-size`][], this sets the max old space size of
2061+
all [worker threads][].
2062+
20452063
On a machine with 2 GiB of memory, consider setting this to
20462064
1536 (1.5 GiB) to leave some memory for other uses and avoid swapping.
20472065

@@ -2093,8 +2111,10 @@ done
20932111
[`--diagnostic-dir`]: #--diagnostic-dirdirectory
20942112
[`--experimental-wasm-modules`]: #--experimental-wasm-modules
20952113
[`--heap-prof-dir`]: #--heap-prof-dir
2114+
[`--max-old-space-size`]: #--max-old-space-sizesize-in-megabytes
20962115
[`--openssl-config`]: #--openssl-configfile
20972116
[`--redirect-warnings`]: #--redirect-warningsfile
2117+
[`--thread-max-old-space-size`]: #--thread-max-old-space-size
20982118
[`Atomics.wait()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/wait
20992119
[`Buffer`]: buffer.md#class-buffer
21002120
[`CRYPTO_secure_malloc_init`]: https://www.openssl.org/docs/man1.1.0/man3/CRYPTO_secure_malloc_init.html
@@ -2107,6 +2127,7 @@ done
21072127
[`dnsPromises.lookup()`]: dns.md#dnspromiseslookuphostname-options
21082128
[`import` specifier]: esm.md#import-specifiers
21092129
[`process.setUncaughtExceptionCaptureCallback()`]: process.md#processsetuncaughtexceptioncapturecallbackfn
2130+
[`resourceLimits`]: worker_threads.md#new-workerfilename-options
21102131
[`tls.DEFAULT_MAX_VERSION`]: tls.md#tlsdefault_max_version
21112132
[`tls.DEFAULT_MIN_VERSION`]: tls.md#tlsdefault_min_version
21122133
[`unhandledRejection`]: process.md#event-unhandledrejection
@@ -2126,3 +2147,4 @@ done
21262147
[semi-space]: https://www.memorymanagement.org/glossary/s.html#semi.space
21272148
[timezone IDs]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
21282149
[ways that `TZ` is handled in other environments]: https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
2150+
[worker threads]: worker_threads.md

doc/node.1

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

src/api/environment.cc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,9 @@ 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(isolate, loop, std::move(options), platform, allocator);
334336
}
335337

336338
void FreeIsolateData(IsolateData* isolate_data) {

src/env.cc

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

368368
IsolateData::IsolateData(Isolate* isolate,
369369
uv_loop_t* event_loop,
370+
std::shared_ptr<PerIsolateOptions> options,
370371
MultiIsolatePlatform* platform,
371372
ArrayBufferAllocator* node_allocator,
372373
const std::vector<size_t>* indexes)
373374
: isolate_(isolate),
374375
event_loop_(event_loop),
376+
options_(options),
375377
node_allocator_(node_allocator == nullptr ? nullptr
376378
: node_allocator->GetImpl()),
377379
platform_(platform) {
378-
options_.reset(
379-
new PerIsolateOptions(*(per_process::cli_options->per_isolate)));
380-
381380
if (indexes == nullptr) {
382381
CreateProperties();
383382
} else {

src/env.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,7 @@ class NODE_EXTERN_PRIVATE IsolateData : public MemoryRetainer {
579579
public:
580580
IsolateData(v8::Isolate* isolate,
581581
uv_loop_t* event_loop,
582+
std::shared_ptr<PerIsolateOptions> options,
582583
MultiIsolatePlatform* platform = nullptr,
583584
ArrayBufferAllocator* node_allocator = nullptr,
584585
const std::vector<size_t>* indexes = nullptr);
@@ -642,9 +643,9 @@ class NODE_EXTERN_PRIVATE IsolateData : public MemoryRetainer {
642643

643644
v8::Isolate* const isolate_;
644645
uv_loop_t* const event_loop_;
646+
std::shared_ptr<PerIsolateOptions> options_;
645647
NodeArrayBufferAllocator* const node_allocator_;
646648
MultiIsolatePlatform* platform_;
647-
std::shared_ptr<PerIsolateOptions> options_;
648649
worker::Worker* worker_context_ = nullptr;
649650
};
650651

src/node_main_instance.cc

Lines changed: 17 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,20 +79,33 @@ 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
93107
: &(snapshot_data->isolate_data_indices));
108+
94109
IsolateSettings s;
95110
SetIsolateMiscHandlers(isolate_, s);
96111
if (snapshot_data == nullptr) {

src/node_options.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,12 @@ PerIsolateOptionsParser::PerIsolateOptionsParser(
657657
&PerIsolateOptions::track_heap_objects,
658658
kAllowedInEnvironment);
659659

660+
AddOption(
661+
"--thread-max-old-space-size",
662+
"set the maximum old space heap size (in megabytes) for this isolate",
663+
&PerIsolateOptions::thread_max_old_space_size,
664+
kAllowedInEnvironment);
665+
660666
// Explicitly add some V8 flags to mark them as allowed in NODE_OPTIONS.
661667
AddOption("--abort-on-uncaught-exception",
662668
"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
@@ -207,6 +207,7 @@ class PerIsolateOptions : public Options {
207207
bool report_uncaught_exception = false;
208208
bool report_on_signal = false;
209209
bool experimental_shadow_realm = false;
210+
size_t thread_max_old_space_size = 0;
210211
std::string report_signal = "SIGUSR2";
211212
inline EnvironmentOptions* get_per_env_options();
212213
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);

0 commit comments

Comments
 (0)