Skip to content

Commit 4a907bd

Browse files
Asaf-Federmantargos
authored andcommitted
src: add percentage support to --max-old-space-size
This commit adds support for specifying --max-old-space-size as a percentage of system memory, in addition to the existing MB format. A new HandleMaxOldSpaceSizePercentage method parses percentage values, validates that they are within the 0-100% range, and provides clear error messages for invalid input. The heap size is now calculated based on available system memory when a percentage is used. Test coverage has been added for both valid and invalid cases. Documentation and the JSON schema for CLI options have been updated with examples for both formats. Refs: #57447 PR-URL: #59082 Reviewed-By: Chengzhong Wu <legendecas@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: theanarkh <theratliter@gmail.com> Reviewed-By: Daeyeon Jeong <daeyeon.dev@gmail.com>
1 parent 7ab13b7 commit 4a907bd

File tree

7 files changed

+223
-0
lines changed

7 files changed

+223
-0
lines changed

doc/api/cli.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1680,6 +1680,22 @@ changes:
16801680

16811681
Specify the maximum size, in bytes, of HTTP headers. Defaults to 16 KiB.
16821682

1683+
### `--max-old-space-size-percentage=PERCENTAGE`
1684+
1685+
Sets the max memory size of V8's old memory section as a percentage of available system memory.
1686+
This flag takes precedence over `--max-old-space-size` when both are specified.
1687+
1688+
The `PERCENTAGE` parameter must be a number greater than 0 and up to 100. representing the percentage
1689+
of available system memory to allocate to the V8 heap.
1690+
1691+
```bash
1692+
# Using 50% of available system memory
1693+
node --max-old-space-size-percentage=50 index.js
1694+
1695+
# Using 75% of available system memory
1696+
node --max-old-space-size-percentage=75 index.js
1697+
```
1698+
16831699
### `--napi-modules`
16841700

16851701
<!-- YAML
@@ -3395,6 +3411,7 @@ one is included in the list below.
33953411
* `--inspect`
33963412
* `--localstorage-file`
33973413
* `--max-http-header-size`
3414+
* `--max-old-space-size-percentage`
33983415
* `--napi-modules`
33993416
* `--network-family-autoselection-attempt-timeout`
34003417
* `--no-addons`

doc/node-config-schema.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,9 @@
266266
"max-http-header-size": {
267267
"type": "number"
268268
},
269+
"max-old-space-size-percentage": {
270+
"type": "string"
271+
},
269272
"network-family-autoselection": {
270273
"type": "boolean"
271274
},

doc/node.1

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,16 @@ The file used to store localStorage data.
332332
.It Fl -max-http-header-size Ns = Ns Ar size
333333
Specify the maximum size of HTTP headers in bytes. Defaults to 16 KiB.
334334
.
335+
.It Fl -max-old-space-size-percentage Ns = Ns Ar percentage
336+
Sets the max memory size of V8's old memory section as a percentage of available system memory.
337+
This flag takes precedence over
338+
.Fl -max-old-space-size
339+
when both are specified.
340+
The
341+
.Ar percentage
342+
parameter must be a number greater than 0 and up to 100, representing the percentage
343+
of available system memory to allocate to the V8 heap.
344+
.
335345
.It Fl -napi-modules
336346
This option is a no-op.
337347
It is kept for compatibility.

src/node.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,13 @@ static ExitCode ProcessGlobalArgsInternal(std::vector<std::string>* args,
765765
v8_args.emplace_back("--harmony-import-attributes");
766766
}
767767

768+
if (!per_process::cli_options->per_isolate->max_old_space_size_percentage
769+
.empty()) {
770+
v8_args.emplace_back(
771+
"--max_old_space_size=" +
772+
per_process::cli_options->per_isolate->max_old_space_size);
773+
}
774+
768775
auto env_opts = per_process::cli_options->per_isolate->per_env;
769776
if (std::ranges::find(v8_args, "--abort-on-uncaught-exception") !=
770777
v8_args.end() ||

src/node_options.cc

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77
#include "node_external_reference.h"
88
#include "node_internals.h"
99
#include "node_sea.h"
10+
#include "uv.h"
1011
#if HAVE_OPENSSL
1112
#include "openssl/opensslv.h"
1213
#endif
1314

1415
#include <algorithm>
1516
#include <array>
1617
#include <charconv>
18+
#include <cstdint>
1719
#include <limits>
1820
#include <sstream>
1921
#include <string_view>
@@ -107,8 +109,49 @@ void PerProcessOptions::CheckOptions(std::vector<std::string>* errors,
107109
per_isolate->CheckOptions(errors, argv);
108110
}
109111

112+
void PerIsolateOptions::HandleMaxOldSpaceSizePercentage(
113+
std::vector<std::string>* errors,
114+
std::string* max_old_space_size_percentage) {
115+
std::string original_input_for_error = *max_old_space_size_percentage;
116+
// Parse the percentage value
117+
char* end_ptr;
118+
double percentage =
119+
std::strtod(max_old_space_size_percentage->c_str(), &end_ptr);
120+
121+
// Validate the percentage value
122+
if (*end_ptr != '\0' || percentage <= 0.0 || percentage > 100.0) {
123+
errors->push_back("--max-old-space-size-percentage must be greater "
124+
"than 0 and up to 100. Got: " +
125+
original_input_for_error);
126+
return;
127+
}
128+
129+
// Get available memory in bytes
130+
uint64_t total_memory = uv_get_total_memory();
131+
uint64_t constrained_memory = uv_get_constrained_memory();
132+
133+
// Use constrained memory if available, otherwise use total memory
134+
// This logic correctly handles the documented guarantees.
135+
// Use uint64_t for the result to prevent data loss on 32-bit systems.
136+
uint64_t available_memory =
137+
(constrained_memory > 0 && constrained_memory != UINT64_MAX)
138+
? constrained_memory
139+
: total_memory;
140+
141+
// Convert to MB and calculate the percentage
142+
uint64_t memory_mb = available_memory / (1024 * 1024);
143+
uint64_t calculated_mb = static_cast<size_t>(memory_mb * percentage / 100.0);
144+
145+
// Convert back to string
146+
max_old_space_size = std::to_string(calculated_mb);
147+
}
148+
110149
void PerIsolateOptions::CheckOptions(std::vector<std::string>* errors,
111150
std::vector<std::string>* argv) {
151+
if (!max_old_space_size_percentage.empty()) {
152+
HandleMaxOldSpaceSizePercentage(errors, &max_old_space_size_percentage);
153+
}
154+
112155
per_env->CheckOptions(errors, argv);
113156
}
114157

@@ -1082,6 +1125,11 @@ PerIsolateOptionsParser::PerIsolateOptionsParser(
10821125
V8Option{},
10831126
kAllowedInEnvvar);
10841127
AddOption("--max-old-space-size", "", V8Option{}, kAllowedInEnvvar);
1128+
AddOption("--max-old-space-size-percentage",
1129+
"set V8's max old space size as a percentage of available memory "
1130+
"(e.g., '50%'). Takes precedence over --max-old-space-size.",
1131+
&PerIsolateOptions::max_old_space_size_percentage,
1132+
kAllowedInEnvvar);
10851133
AddOption("--max-semi-space-size", "", V8Option{}, kAllowedInEnvvar);
10861134
AddOption("--perf-basic-prof", "", V8Option{}, kAllowedInEnvvar);
10871135
AddOption(

src/node_options.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,13 +285,17 @@ class PerIsolateOptions : public Options {
285285
bool report_uncaught_exception = false;
286286
bool report_on_signal = false;
287287
bool experimental_shadow_realm = false;
288+
std::string max_old_space_size_percentage;
289+
std::string max_old_space_size;
288290
int64_t stack_trace_limit = 10;
289291
std::string report_signal = "SIGUSR2";
290292
bool build_snapshot = false;
291293
std::string build_snapshot_config;
292294
inline EnvironmentOptions* get_per_env_options();
293295
void CheckOptions(std::vector<std::string>* errors,
294296
std::vector<std::string>* argv) override;
297+
void HandleMaxOldSpaceSizePercentage(std::vector<std::string>* errors,
298+
std::string* max_old_space_size);
295299

296300
inline std::shared_ptr<PerIsolateOptions> Clone() const;
297301

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
'use strict';
2+
3+
// This test validates the --max-old-space-size-percentage flag functionality
4+
5+
require('../common');
6+
const assert = require('node:assert');
7+
const { spawnSync } = require('child_process');
8+
const os = require('os');
9+
10+
// Valid cases
11+
const validPercentages = [
12+
'1', '10', '25', '50', '75', '99', '100', '25.5',
13+
];
14+
15+
// Invalid cases
16+
const invalidPercentages = [
17+
['', /--max-old-space-size-percentage= requires an argument/],
18+
['0', /--max-old-space-size-percentage must be greater than 0 and up to 100\. Got: 0/],
19+
['101', /--max-old-space-size-percentage must be greater than 0 and up to 100\. Got: 101/],
20+
['-1', /--max-old-space-size-percentage must be greater than 0 and up to 100\. Got: -1/],
21+
['abc', /--max-old-space-size-percentage must be greater than 0 and up to 100\. Got: abc/],
22+
['1%', /--max-old-space-size-percentage must be greater than 0 and up to 100\. Got: 1%/],
23+
];
24+
25+
// Test valid cases
26+
validPercentages.forEach((input) => {
27+
const result = spawnSync(process.execPath, [
28+
`--max-old-space-size-percentage=${input}`,
29+
], { stdio: ['pipe', 'pipe', 'pipe'] });
30+
assert.strictEqual(result.status, 0, `Expected exit code 0 for valid input ${input}`);
31+
assert.strictEqual(result.stderr.toString(), '', `Expected empty stderr for valid input ${input}`);
32+
});
33+
34+
// Test invalid cases
35+
invalidPercentages.forEach((input) => {
36+
const result = spawnSync(process.execPath, [
37+
`--max-old-space-size-percentage=${input[0]}`,
38+
], { stdio: ['pipe', 'pipe', 'pipe'] });
39+
assert.notStrictEqual(result.status, 0, `Expected non-zero exit for invalid input ${input[0]}`);
40+
assert(input[1].test(result.stderr.toString()), `Unexpected error message for invalid input ${input[0]}`);
41+
});
42+
43+
// Test NODE_OPTIONS with valid percentages
44+
validPercentages.forEach((input) => {
45+
const result = spawnSync(process.execPath, [], {
46+
stdio: ['pipe', 'pipe', 'pipe'],
47+
env: { ...process.env, NODE_OPTIONS: `--max-old-space-size-percentage=${input}` }
48+
});
49+
assert.strictEqual(result.status, 0, `NODE_OPTIONS: Expected exit code 0 for valid input ${input}`);
50+
assert.strictEqual(result.stderr.toString(), '', `NODE_OPTIONS: Expected empty stderr for valid input ${input}`);
51+
});
52+
53+
// Test NODE_OPTIONS with invalid percentages
54+
invalidPercentages.forEach((input) => {
55+
const result = spawnSync(process.execPath, [], {
56+
stdio: ['pipe', 'pipe', 'pipe'],
57+
env: { ...process.env, NODE_OPTIONS: `--max-old-space-size-percentage=${input[0]}` }
58+
});
59+
assert.notStrictEqual(result.status, 0, `NODE_OPTIONS: Expected non-zero exit for invalid input ${input[0]}`);
60+
assert(input[1].test(result.stderr.toString()), `NODE_OPTIONS: Unexpected error message for invalid input ${input[0]}`);
61+
});
62+
63+
// Test percentage calculation validation
64+
function getHeapSizeForPercentage(percentage) {
65+
const result = spawnSync(process.execPath, [
66+
'--max-old-space-size=3000', // This value should be ignored, since percentage takes precedence
67+
`--max-old-space-size-percentage=${percentage}`,
68+
'--max-old-space-size=1000', // This value should be ignored, since percentage take precedence
69+
'-e', `
70+
const v8 = require('v8');
71+
const stats = v8.getHeapStatistics();
72+
const heapSizeLimitMB = Math.floor(stats.heap_size_limit / 1024 / 1024);
73+
console.log(heapSizeLimitMB);
74+
`,
75+
], {
76+
stdio: ['pipe', 'pipe', 'pipe'],
77+
env: {
78+
...process.env,
79+
NODE_OPTIONS: `--max-old-space-size=2000` // This value should be ignored, since percentage takes precedence
80+
}
81+
});
82+
83+
if (result.status !== 0) {
84+
throw new Error(`Failed to get heap size for ${percentage}: ${result.stderr.toString()}`);
85+
}
86+
87+
return parseInt(result.stdout.toString(), 10);
88+
}
89+
90+
const testPercentages = [25, 50, 75, 100];
91+
const heapSizes = {};
92+
93+
// Get heap sizes for all test percentages
94+
testPercentages.forEach((percentage) => {
95+
heapSizes[percentage] = getHeapSizeForPercentage(percentage);
96+
});
97+
98+
// Test relative relationships between percentages
99+
// 50% should be roughly half of 100%
100+
const ratio50to100 = heapSizes[50] / heapSizes[100];
101+
assert(
102+
ratio50to100 >= 0.4 && ratio50to100 <= 0.6,
103+
`50% heap size should be roughly half of 100% (got ${ratio50to100.toFixed(2)}, expected ~0.5)`
104+
);
105+
106+
// 25% should be roughly quarter of 100%
107+
const ratio25to100 = heapSizes[25] / heapSizes[100];
108+
assert(
109+
ratio25to100 >= 0.15 && ratio25to100 <= 0.35,
110+
`25% heap size should be roughly quarter of 100% (got ${ratio25to100.toFixed(2)}, expected ~0.25)`
111+
);
112+
113+
// 75% should be roughly three-quarters of 100%
114+
const ratio75to100 = heapSizes[75] / heapSizes[100];
115+
assert(
116+
ratio75to100 >= 0.65 && ratio75to100 <= 0.85,
117+
`75% heap size should be roughly three-quarters of 100% (got ${ratio75to100.toFixed(2)}, expected ~0.75)`
118+
);
119+
120+
// Validate heap sizes against system memory
121+
const totalMemoryMB = Math.floor(os.totalmem() / 1024 / 1024);
122+
const margin = 10; // 5% margin
123+
testPercentages.forEach((percentage) => {
124+
const upperLimit = totalMemoryMB * ((percentage + margin) / 100);
125+
assert(
126+
heapSizes[percentage] <= upperLimit,
127+
`Heap size for ${percentage}% (${heapSizes[percentage]} MB) should not exceed upper limit (${upperLimit} MB)`
128+
);
129+
const lowerLimit = totalMemoryMB * ((percentage - margin) / 100);
130+
assert(
131+
heapSizes[percentage] >= lowerLimit,
132+
`Heap size for ${percentage}% (${heapSizes[percentage]} MB) should not be less than lower limit (${lowerLimit} MB)`
133+
);
134+
});

0 commit comments

Comments
 (0)