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
Next Next commit
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
  • Loading branch information
Asaf-Federman committed Jul 23, 2025
commit c837d144d2cd7bba032e3eddf31aa595dc85b797
22 changes: 17 additions & 5 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -3039,11 +3039,11 @@ On macOS, the following settings are respected:

* Default and System Keychains
* Trust:
* Any certificate where the When using this certificate flag is set to Always Trust or
* Any certificate where the Secure Sockets Layer (SSL) flag is set to Always Trust.
* Any certificate where the "When using this certificate" flag is set to "Always Trust" or
* Any certificate where the "Secure Sockets Layer (SSL)" flag is set to "Always Trust."
* Distrust:
* Any certificate where the When using this certificate flag is set to Never Trust or
* Any certificate where the Secure Sockets Layer (SSL) flag is set to Never Trust.
* Any certificate where the "When using this certificate" flag is set to "Never Trust" or
* Any certificate where the "Secure Sockets Layer (SSL)" flag is set to "Never Trust."
Comment thread
Asaf-Federman marked this conversation as resolved.
Outdated

On Windows, the following settings are respected (unlike Chromium's policy, distrust
and intermediate CA are not currently supported):
Expand Down Expand Up @@ -3863,17 +3863,29 @@ documented here:

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

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

Sets the max memory size of V8's old memory section. As memory
consumption approaches the limit, V8 will spend more time on
garbage collection in an effort to free unused memory.

The `SIZE` parameter can be specified in two formats:

* **Megabytes**: A number representing the heap size in MiB (e.g., `1536`)
* **Percentage**: A number followed by `%` representing a percentage of available system memory (e.g., `50%`)

When using percentage format, Node.js calculates the heap size based on the available system memory.
The percentage must be greater than 0 and up to 100.

On a machine with 2 GiB of memory, consider setting this to
1536 (1.5 GiB) to leave some memory for other uses and avoid swapping.

```bash
# Using megabytes
node --max-old-space-size=1536 index.js

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

<!-- Anchor to make sure old links find a target -->
Expand Down
3 changes: 3 additions & 0 deletions doc/node-config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,9 @@
"max-http-header-size": {
"type": "number"
},
"max-old-space-size": {
"type": "string"
},
"network-family-autoselection": {
"type": "boolean"
},
Expand Down
6 changes: 6 additions & 0 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,12 @@ 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()) {
v8_args.emplace_back("--max_old_space_size=" +
per_process::cli_options->per_isolate->
max_old_space_size);
}

auto env_opts = per_process::cli_options->per_isolate->per_env;
if (std::ranges::find(v8_args, "--abort-on-uncaught-exception") !=
v8_args.end() ||
Expand Down
50 changes: 49 additions & 1 deletion src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "node_external_reference.h"
#include "node_internals.h"
#include "node_sea.h"
#include "uv.h"
#if HAVE_OPENSSL
#include "openssl/opensslv.h"
#endif
Expand Down Expand Up @@ -107,8 +108,52 @@ void PerProcessOptions::CheckOptions(std::vector<std::string>* errors,
per_isolate->CheckOptions(errors, argv);
}

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");
return;
}

// Parse the percentage value
char* end_ptr;
double percentage = std::strtod(max_old_space_size->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 "
"than 0 and up to 100. Got: " + original_input_for_error);
return;
}

// Get available memory in MB
size_t total_memory = uv_get_total_memory();
size_t constrained_memory = uv_get_constrained_memory();

// Use constrained memory if available, otherwise use total memory
size_t available_memory = (constrained_memory > 0) ? constrained_memory
: total_memory;

// Convert to MB and calculate the percentage
size_t memory_mb = available_memory / (1024 * 1024);
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);
}

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);
}

per_env->CheckOptions(errors, argv);
}

Expand Down Expand Up @@ -1079,7 +1124,10 @@ PerIsolateOptionsParser::PerIsolateOptionsParser(
"help system profilers to translate JavaScript interpreted frames",
V8Option{},
kAllowedInEnvvar);
AddOption("--max-old-space-size", "", 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-semi-space-size", "", V8Option{}, kAllowedInEnvvar);
AddOption("--perf-basic-prof", "", V8Option{}, kAllowedInEnvvar);
AddOption(
Expand Down
4 changes: 4 additions & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -285,13 +285,17 @@ 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;
int64_t stack_trace_limit = 10;
std::string report_signal = "SIGUSR2";
bool build_snapshot = false;
std::string build_snapshot_config;
inline EnvironmentOptions* get_per_env_options();
void CheckOptions(std::vector<std::string>* errors,
std::vector<std::string>* argv) override;
void HandleMaxOldSpaceSizePercentage(
std::vector<std::string>* errors,
std::string* max_old_space_size);

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

Expand Down
138 changes: 138 additions & 0 deletions test/parallel/test-max-old-space-size-percentage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
'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.

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

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

// Invalid percentage cases
const invalidPercentages = [
'%', '0%', '101%', '-1%', 'abc%', '100.1%', '0.0%',
];

// 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
validPercentages.forEach((input) => {
const result = spawnSync(process.execPath, [
`--max-old-space-size=${input}`,
'-e', 'console.log("OK")',
], { 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
invalidPercentages.forEach((input) => {
const result = spawnSync(process.execPath, [
`--max-old-space-size=${input}`,
'-e', 'console.log("FAIL")',
], { stdio: ['pipe', 'pipe', 'pipe'] });
assert.notStrictEqual(result.status, 0, `Expected non-zero exit for invalid input ${input}`);
assertErrorMessage(result.stderr.toString(), input);
});

// Test NODE_OPTIONS with valid percentages
validPercentages.forEach((input) => {
const result = spawnSync(process.execPath, [
'-e', 'console.log("NODE_OPTIONS OK")',
], {
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env, NODE_OPTIONS: `--max-old-space-size=${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")',
], {
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env, NODE_OPTIONS: `--max-old-space-size=${input}` }
});
assert.notStrictEqual(result.status, 0, `NODE_OPTIONS: Expected non-zero exit for invalid input ${input}`);
assertErrorMessage(result.stderr.toString(), `NODE_OPTIONS ${input}`);
});

// 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}%`,
'-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'] });

if (result.status !== 0) {
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 = {};

// Get heap sizes for all test percentages
testPercentages.forEach((percentage) => {
heapSizes[percentage] = getHeapSizeForPercentage(percentage);
});

// Test relative relationships between percentages
// 50% should be roughly half of 100%
const ratio50to100 = heapSizes[50] / heapSizes[100];
assert(
ratio50to100 >= 0.4 && ratio50to100 <= 0.6,
`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,
`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,
`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%');