Skip to content

Commit 686ac49

Browse files
Asaf-Federmanaduh95
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 Backport-PR-URL: #59631 Reviewed-By: Chengzhong Wu <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Moshe Atlow <[email protected]> Reviewed-By: theanarkh <[email protected]> Reviewed-By: Daeyeon Jeong <[email protected]>
1 parent 7067d79 commit 686ac49

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
@@ -1702,6 +1702,22 @@ changes:
17021702

17031703
Specify the maximum size, in bytes, of HTTP headers. Defaults to 16 KiB.
17041704

1705+
### `--max-old-space-size-percentage=PERCENTAGE`
1706+
1707+
Sets the max memory size of V8's old memory section as a percentage of available system memory.
1708+
This flag takes precedence over `--max-old-space-size` when both are specified.
1709+
1710+
The `PERCENTAGE` parameter must be a number greater than 0 and up to 100. representing the percentage
1711+
of available system memory to allocate to the V8 heap.
1712+
1713+
```bash
1714+
# Using 50% of available system memory
1715+
node --max-old-space-size-percentage=50 index.js
1716+
1717+
# Using 75% of available system memory
1718+
node --max-old-space-size-percentage=75 index.js
1719+
```
1720+
17051721
### `--napi-modules`
17061722

17071723
<!-- YAML
@@ -3387,6 +3403,7 @@ one is included in the list below.
33873403
* `--inspect`
33883404
* `--localstorage-file`
33893405
* `--max-http-header-size`
3406+
* `--max-old-space-size-percentage`
33903407
* `--napi-modules`
33913408
* `--network-family-autoselection-attempt-timeout`
33923409
* `--no-addons`

doc/node-config-schema.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,9 @@
278278
"max-http-header-size": {
279279
"type": "number"
280280
},
281+
"max-old-space-size-percentage": {
282+
"type": "string"
283+
},
281284
"network-family-autoselection": {
282285
"type": "boolean"
283286
},

doc/node.1

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,16 @@ The file used to store localStorage data.
346346
.It Fl -max-http-header-size Ns = Ns Ar size
347347
Specify the maximum size of HTTP headers in bytes. Defaults to 16 KiB.
348348
.
349+
.It Fl -max-old-space-size-percentage Ns = Ns Ar percentage
350+
Sets the max memory size of V8's old memory section as a percentage of available system memory.
351+
This flag takes precedence over
352+
.Fl -max-old-space-size
353+
when both are specified.
354+
The
355+
.Ar percentage
356+
parameter must be a number greater than 0 and up to 100, representing the percentage
357+
of available system memory to allocate to the V8 heap.
358+
.
349359
.It Fl -napi-modules
350360
This option is a no-op.
351361
It is kept for compatibility.

src/node.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,13 @@ static ExitCode ProcessGlobalArgsInternal(std::vector<std::string>* args,
818818
// anymore.
819819
v8_args.emplace_back("--no-harmony-import-assertions");
820820

821+
if (!per_process::cli_options->per_isolate->max_old_space_size_percentage
822+
.empty()) {
823+
v8_args.emplace_back(
824+
"--max_old_space_size=" +
825+
per_process::cli_options->per_isolate->max_old_space_size);
826+
}
827+
821828
auto env_opts = per_process::cli_options->per_isolate->per_env;
822829
if (std::find(v8_args.begin(), v8_args.end(),
823830
"--abort-on-uncaught-exception") != 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>
@@ -103,8 +105,49 @@ void PerProcessOptions::CheckOptions(std::vector<std::string>* errors,
103105
per_isolate->CheckOptions(errors, argv);
104106
}
105107

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

@@ -987,6 +1030,11 @@ PerIsolateOptionsParser::PerIsolateOptionsParser(
9871030
V8Option{},
9881031
kAllowedInEnvvar);
9891032
AddOption("--max-old-space-size", "", V8Option{}, kAllowedInEnvvar);
1033+
AddOption("--max-old-space-size-percentage",
1034+
"set V8's max old space size as a percentage of available memory "
1035+
"(e.g., '50%'). Takes precedence over --max-old-space-size.",
1036+
&PerIsolateOptions::max_old_space_size_percentage,
1037+
kAllowedInEnvvar);
9901038
AddOption("--max-semi-space-size", "", V8Option{}, kAllowedInEnvvar);
9911039
AddOption("--perf-basic-prof", "", V8Option{}, kAllowedInEnvvar);
9921040
AddOption(

src/node_options.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,13 +283,17 @@ class PerIsolateOptions : public Options {
283283
bool report_uncaught_exception = false;
284284
bool report_on_signal = false;
285285
bool experimental_shadow_realm = false;
286+
std::string max_old_space_size_percentage;
287+
std::string max_old_space_size;
286288
int64_t stack_trace_limit = 10;
287289
std::string report_signal = "SIGUSR2";
288290
bool build_snapshot = false;
289291
std::string build_snapshot_config;
290292
inline EnvironmentOptions* get_per_env_options();
291293
void CheckOptions(std::vector<std::string>* errors,
292294
std::vector<std::string>* argv) override;
295+
void HandleMaxOldSpaceSizePercentage(std::vector<std::string>* errors,
296+
std::string* max_old_space_size);
293297

294298
inline std::shared_ptr<PerIsolateOptions> Clone() const;
295299

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)