Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
src: introduce max-old-space-size-percentage as a new cli flag
  • Loading branch information
Asaf-Federman committed Jul 23, 2025
commit 84d34338e24e9731dc339d4b040c166a845f831d
19 changes: 18 additions & 1 deletion doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -3417,6 +3417,7 @@ one is included in the list below.
* `--inspect`
* `--localstorage-file`
* `--max-http-header-size`
* `--max-old-space-size-percentage`
* `--napi-modules`
* `--network-family-autoselection-attempt-timeout`
* `--no-addons`
Expand Down Expand Up @@ -3859,11 +3860,27 @@ documented here:

### `--jitless`

### `--max-old-space-size-percentage=PERCENTAGE`
Comment thread
Asaf-Federman marked this conversation as resolved.
Outdated

Sets the max memory size of V8's old memory section as a percentage of available system memory.
This flag takes precedence over `--max-old-space-size` when both are specified.

The `PERCENTAGE` parameter must be a number greater than 0 and up to 100. representing the percentage
of available system memory to allocate to the V8 heap.

```bash
# Using 50% of available system memory
node --max-old-space-size-percentage=50 index.js

# Using 75% of available system memory
node --max-old-space-size-percentage=75 index.js
```

<!-- Anchor to make sure old links find a target -->

<a id="--max-old-space-sizesize-in-megabytes"></a>

### `--max-old-space-size=SIZE` (in MiB or percentage)
### `--max-old-space-size=SIZE` (in MiB)

Sets the max memory size of V8's old memory section. As memory
consumption approaches the limit, V8 will spend more time on
Expand Down
2 changes: 1 addition & 1 deletion doc/node-config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@
"max-http-header-size": {
"type": "number"
},
"max-old-space-size": {
"max-old-space-size-percentage": {
"type": "string"
},
"network-family-autoselection": {
Expand Down
4 changes: 3 additions & 1 deletion src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -765,7 +765,9 @@ static ExitCode ProcessGlobalArgsInternal(std::vector<std::string>* args,
v8_args.emplace_back("--harmony-import-attributes");
}

if (!per_process::cli_options->per_isolate->max_old_space_size.empty()) {
if (!per_process::cli_options->
per_isolate->
max_old_space_size_percentage.empty()) {
v8_args.emplace_back("--max_old_space_size=" +
per_process::cli_options->per_isolate->
max_old_space_size);
Expand Down
36 changes: 18 additions & 18 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -109,25 +109,23 @@ void PerProcessOptions::CheckOptions(std::vector<std::string>* errors,
}

void PerIsolateOptions::HandleMaxOldSpaceSizePercentage(
std::vector<std::string>* errors, std::string* max_old_space_size) {
std::string original_input_for_error = *max_old_space_size;

// Remove the '%' suffix
max_old_space_size->pop_back();

// Check if the percentage value is empty after removing '%'
if (max_old_space_size->empty()) {
errors->push_back("--max-old-space-size percentage must not be empty");
std::vector<std::string>* errors,
std::string* max_old_space_size_percentage) {
std::string original_input_for_error = *max_old_space_size_percentage;
// Check if the percentage value is empty
if (max_old_space_size_percentage->empty()) {
errors->push_back("--max-old-space-size-percentage must not be empty");
return;
}

// Parse the percentage value
char* end_ptr;
double percentage = std::strtod(max_old_space_size->c_str(), &end_ptr);
double percentage =
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why double? It's likely best to just allow integer values here between 0 and 100. Not critical, of course, I'd just find it a bit cleaner

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why double? It's likely best to just allow integer values here between 0 and 100. Not critical, of course, I'd just find it a bit cleaner

Should this be an integer, or would a float provide better flexibility for nodes with a large amount of memory?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really depends on how granular we want it to be. Personally I'd be fine with integer and just not worry about fractional percentages but I'd be interested in what others think

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it was a breaking change I would agree, but this is a new feature, why not support more usecases even if they are rare?

std::strtod(max_old_space_size_percentage->c_str(), &end_ptr);

// Validate the percentage value
if (*end_ptr != '\0' || percentage <= 0.0 || percentage > 100.0) {
errors->push_back("--max-old-space-size percentage must be greater "
errors->push_back("--max-old-space-size-percentage must be greater "
"than 0 and up to 100. Got: " + original_input_for_error);
return;
}
Expand All @@ -145,13 +143,13 @@ void PerIsolateOptions::HandleMaxOldSpaceSizePercentage(
size_t calculated_mb = static_cast<size_t>(memory_mb * percentage / 100.0);

// Convert back to string
*max_old_space_size = std::to_string(calculated_mb);
max_old_space_size = std::to_string(calculated_mb);
}

void PerIsolateOptions::CheckOptions(std::vector<std::string>* errors,
std::vector<std::string>* argv) {
if (!max_old_space_size.empty() && max_old_space_size.back() == '%') {
HandleMaxOldSpaceSizePercentage(errors, &max_old_space_size);
if (!max_old_space_size_percentage.empty()) {
HandleMaxOldSpaceSizePercentage(errors, &max_old_space_size_percentage);
}

per_env->CheckOptions(errors, argv);
Expand Down Expand Up @@ -1124,10 +1122,12 @@ PerIsolateOptionsParser::PerIsolateOptionsParser(
"help system profilers to translate JavaScript interpreted frames",
V8Option{},
kAllowedInEnvvar);
AddOption("--max-old-space-size",
"set V8's max old space size. SIZE is in Megabytes (e.g., '2048') "
"or as a percentage of available memory (e.g., '50%').",
&PerIsolateOptions::max_old_space_size, kAllowedInEnvvar);
AddOption("--max-old-space-size", "", V8Option{}, kAllowedInEnvvar);
AddOption("--max-old-space-size-percentage",
"set V8's max old space size as a percentage of available memory "
"(e.g., '50%'). Takes precedence over --max-old-space-size.",
&PerIsolateOptions::max_old_space_size_percentage,
kAllowedInEnvvar);
AddOption("--max-semi-space-size", "", V8Option{}, kAllowedInEnvvar);
AddOption("--perf-basic-prof", "", V8Option{}, kAllowedInEnvvar);
AddOption(
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ class PerIsolateOptions : public Options {
bool report_uncaught_exception = false;
bool report_on_signal = false;
bool experimental_shadow_realm = false;
std::string max_old_space_size_percentage;
std::string max_old_space_size;
int64_t stack_trace_limit = 10;
std::string report_signal = "SIGUSR2";
Expand Down
108 changes: 52 additions & 56 deletions test/parallel/test-max-old-space-size-percentage.js
Original file line number Diff line number Diff line change
@@ -1,107 +1,92 @@
'use strict';

// This test validates the --max-old-space-size=XX% CLI flag functionality.
// It tests valid and invalid percentage values, NODE_OPTIONS integration,
// backward compatibility with MB values, and percentage calculation accuracy.
// This test validates the --max-old-space-size-percentage flag functionality

require('../common');
const assert = require('node:assert');
const { spawnSync } = require('child_process');
const os = require('os');

// Valid percentage cases
// Valid cases
const validPercentages = [
'1%', '10%', '25%', '50%', '75%', '99%', '100%', '25.5%',
'1', '10', '25', '50', '75', '99', '100', '25.5',
];

// Invalid percentage cases
// Invalid cases
const invalidPercentages = [
'%', '0%', '101%', '-1%', 'abc%', '100.1%', '0.0%',
['', /--max-old-space-size-percentage= requires an argument/],
['0', /--max-old-space-size-percentage must be greater than 0 and up to 100\. Got: 0/],
['101', /--max-old-space-size-percentage must be greater than 0 and up to 100\. Got: 101/],
['-1', /--max-old-space-size-percentage must be greater than 0 and up to 100\. Got: -1/],
['abc', /--max-old-space-size-percentage must be greater than 0 and up to 100\. Got: abc/],
['1%', /--max-old-space-size-percentage must be greater than 0 and up to 100\. Got: 1%/],
];

// Helper for error message matching
function assertErrorMessage(stderr, context) {
assert(
/illegal value for flag --max-old-space-size=|--max-old-space-size percentage must be|--max-old-space-size percentage must not be empty/.test(stderr),
`Expected error message for ${context}, got: ${stderr}`
);
}

// Test valid percentage cases
// Test valid cases
validPercentages.forEach((input) => {
const result = spawnSync(process.execPath, [
`--max-old-space-size=${input}`,
'-e', 'console.log("OK")',
`--max-old-space-size-percentage=${input}`,
], { stdio: ['pipe', 'pipe', 'pipe'] });
assert.strictEqual(result.status, 0, `Expected exit code 0 for valid input ${input}`);
assert.match(result.stdout.toString(), /OK/, `Expected stdout to contain OK for valid input ${input}`);
assert.strictEqual(result.stderr.toString(), '', `Expected empty stderr for valid input ${input}`);
});

// Test invalid percentage cases
// Test invalid cases
invalidPercentages.forEach((input) => {
const result = spawnSync(process.execPath, [
`--max-old-space-size=${input}`,
'-e', 'console.log("FAIL")',
`--max-old-space-size-percentage=${input[0]}`,
], { stdio: ['pipe', 'pipe', 'pipe'] });
assert.notStrictEqual(result.status, 0, `Expected non-zero exit for invalid input ${input}`);
assertErrorMessage(result.stderr.toString(), input);
assert.notStrictEqual(result.status, 0, `Expected non-zero exit for invalid input ${input[0]}`);
assert(input[1].test(result.stderr.toString()), `Unexpected error message for invalid input ${input[0]}`);
});

// Test NODE_OPTIONS with valid percentages
validPercentages.forEach((input) => {
const result = spawnSync(process.execPath, [
'-e', 'console.log("NODE_OPTIONS OK")',
], {
const result = spawnSync(process.execPath, [], {
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env, NODE_OPTIONS: `--max-old-space-size=${input}` }
env: { ...process.env, NODE_OPTIONS: `--max-old-space-size-percentage=${input}` }
});
assert.strictEqual(result.status, 0, `NODE_OPTIONS: Expected exit code 0 for valid input ${input}`);
assert.strictEqual(result.stderr.toString(), '', `NODE_OPTIONS: Expected empty stderr for valid input ${input}`);
assert.match(result.stdout.toString(), /NODE_OPTIONS OK/, `NODE_OPTIONS: Expected stdout for valid input ${input}`);
});

// Test NODE_OPTIONS with invalid percentages
invalidPercentages.forEach((input) => {
const result = spawnSync(process.execPath, [
'-e', 'console.log("NODE_OPTIONS FAIL")',
], {
const result = spawnSync(process.execPath, [], {
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env, NODE_OPTIONS: `--max-old-space-size=${input}` }
env: { ...process.env, NODE_OPTIONS: `--max-old-space-size-percentage=${input[0]}` }
});
assert.notStrictEqual(result.status, 0, `NODE_OPTIONS: Expected non-zero exit for invalid input ${input}`);
assertErrorMessage(result.stderr.toString(), `NODE_OPTIONS ${input}`);
assert.notStrictEqual(result.status, 0, `NODE_OPTIONS: Expected non-zero exit for invalid input ${input[0]}`);
assert(input[1].test(result.stderr.toString()), `NODE_OPTIONS: Unexpected error message for invalid input ${input[0]}`);
});

// Test backward compatibility: MB values
const maxOldSpaceSizeMB = 600; // Example MB value
const mbResult = spawnSync(process.execPath, [
`--max-old-space-size=${maxOldSpaceSizeMB}`,
'-e', 'console.log("Regular MB test")',
], { stdio: ['pipe', 'pipe', 'pipe'] });
assert.strictEqual(mbResult.status, 0, `Expected exit code 0 for MB value ${maxOldSpaceSizeMB}`);
assert.match(mbResult.stdout.toString(), /Regular MB test/, `Expected stdout for MB value but received ${mbResult.stdout.toString()} for value ${maxOldSpaceSizeMB}`);
assert.strictEqual(mbResult.stderr.toString(), '', `Expected empty stderr for MB value ${maxOldSpaceSizeMB}`);

// Test percentage calculation validation
function getHeapSizeForPercentage(percentage) {
const result = spawnSync(process.execPath, [
`--max-old-space-size=${percentage}%`,
'--max-old-space-size=3000', // This value should be ignored, since percentage takes precedence
`--max-old-space-size-percentage=${percentage}`,
'--max-old-space-size=1000', // This value should be ignored, since percentage take precedence
'-e', `
const v8 = require('v8');
const stats = v8.getHeapStatistics();
Comment thread
legendecas marked this conversation as resolved.
const heapSizeLimitMB = Math.floor(stats.heap_size_limit / 1024 / 1024);
console.log(heapSizeLimitMB);
`,
], { stdio: ['pipe', 'pipe', 'pipe'] });
], {
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...process.env,
NODE_OPTIONS: `--max-old-space-size=2000` // This value should be ignored, since percentage takes precedence
}
});

if (result.status !== 0) {
throw new Error(`Failed to get heap size for ${percentage}%: ${result.stderr.toString()}`);
throw new Error(`Failed to get heap size for ${percentage}: ${result.stderr.toString()}`);
}

return parseInt(result.stdout.toString(), 10);
}

// Test that percentages produce reasonable heap sizes
const testPercentages = [25, 50, 75, 100];
const heapSizes = {};

Expand All @@ -114,25 +99,36 @@ testPercentages.forEach((percentage) => {
// 50% should be roughly half of 100%
const ratio50to100 = heapSizes[50] / heapSizes[100];
assert(
ratio50to100 >= 0.4 && ratio50to100 <= 0.6,
ratio50to100 >= 0.45 && ratio50to100 <= 0.55,
`50% heap size should be roughly half of 100% (got ${ratio50to100.toFixed(2)}, expected ~0.5)`
);

// 25% should be roughly quarter of 100%
const ratio25to100 = heapSizes[25] / heapSizes[100];
assert(
ratio25to100 >= 0.2 && ratio25to100 <= 0.4,
ratio25to100 >= 0.2 && ratio25to100 <= 0.3,
`25% heap size should be roughly quarter of 100% (got ${ratio25to100.toFixed(2)}, expected ~0.25)`
);

// 75% should be roughly three-quarters of 100%
const ratio75to100 = heapSizes[75] / heapSizes[100];
assert(
ratio75to100 >= 0.6 && ratio75to100 <= 0.9,
ratio75to100 >= 0.7 && ratio75to100 <= 0.8,
`75% heap size should be roughly three-quarters of 100% (got ${ratio75to100.toFixed(2)}, expected ~0.75)`
);

// Test that larger percentages produce larger heap sizes
assert(heapSizes[25] <= heapSizes[50], '25% should produce smaller heap than 50%');
assert(heapSizes[50] <= heapSizes[75], '50% should produce smaller heap than 75%');
assert(heapSizes[75] <= heapSizes[100], '75% should produce smaller heap than 100%');
// Validate heap sizes against system memory
const totalMemoryMB = Math.floor(os.totalmem() / 1024 / 1024);
const margin = 5; // 5% margin
testPercentages.forEach((percentage) => {
const upperLimit = totalMemoryMB * ((percentage + margin) / 100);
assert(
heapSizes[percentage] <= upperLimit,
`Heap size for ${percentage}% (${heapSizes[percentage]} MB) should not exceed upper limit (${upperLimit} MB)`
);
const lowerLimit = totalMemoryMB * ((percentage - margin) / 100);
assert(
heapSizes[percentage] >= lowerLimit,
`Heap size for ${percentage}% (${heapSizes[percentage]} MB) should not be less than lower limit (${lowerLimit} MB)`
);
});