Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
20 changes: 20 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1702,6 +1702,25 @@ changes:

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

### `--max-old-space-size-percentage=percentage`

Sets the maximum 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.

**Note:** This flag utilizes `--max-old-space-size`, which may be unreliable on 32-bit platforms due to
integer overflow issues.

```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
```

### `--napi-modules`

<!-- YAML
Expand Down Expand Up @@ -3387,6 +3406,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
3 changes: 3 additions & 0 deletions doc/node-config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,9 @@
"max-http-header-size": {
"type": "number"
},
"max-old-space-size-percentage": {
"type": "string"
},
"network-family-autoselection": {
"type": "boolean"
},
Expand Down
14 changes: 14 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,20 @@ The file used to store localStorage data.
.It Fl -max-http-header-size Ns = Ns Ar size
Specify the maximum size of HTTP headers in bytes. Defaults to 16 KiB.
.
.It Fl -max-old-space-size-percentage Ns = Ns Ar percentage
Sets the maximum memory size of V8's old memory section as a percentage of available system memory.
This flag takes precedence over
.Fl -max-old-space-size
when both are specified.
The
.Ar 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.
.Pp
Note: This flag utilizes
.Fl -max-old-space-size ,
which may be unreliable on 32-bit platforms due to integer overflow issues.
.
.It Fl -napi-modules
This option is a no-op.
It is kept for compatibility.
Expand Down
7 changes: 7 additions & 0 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,13 @@ static ExitCode ProcessGlobalArgsInternal(std::vector<std::string>* args,
// anymore.
v8_args.emplace_back("--no-harmony-import-assertions");

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

auto env_opts = per_process::cli_options->per_isolate->per_env;
if (std::find(v8_args.begin(), v8_args.end(),
"--abort-on-uncaught-exception") != v8_args.end() ||
Expand Down
53 changes: 53 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
#include "node_external_reference.h"
#include "node_internals.h"
#include "node_sea.h"
#include "uv.h"
#if HAVE_OPENSSL
#include "openssl/opensslv.h"
#endif

#include <algorithm>
#include <array>
#include <charconv>
#include <cstdint>
#include <limits>
#include <sstream>
#include <string_view>
Expand Down Expand Up @@ -103,8 +105,54 @@ 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_percentage) {
std::string original_input_for_error = *max_old_space_size_percentage;
// Parse the percentage value
char* end_ptr;
double percentage =
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 "
"than 0 and up to 100. Got: " +
original_input_for_error);
return;
}

// Get available memory in bytes
uint64_t total_memory = uv_get_total_memory();
uint64_t constrained_memory = uv_get_constrained_memory();

// Use constrained memory if available, otherwise use total memory
// This logic correctly handles the documented guarantees.
// Use uint64_t for the result to prevent data loss on 32-bit systems.
uint64_t available_memory =
(constrained_memory > 0 && constrained_memory != UINT64_MAX)
? constrained_memory
: total_memory;

if (available_memory == 0) {
errors->push_back("the available memory can not be calculated");
return;
}

// Convert to MB and calculate the percentage
uint64_t memory_mb = available_memory / (1024 * 1024);
uint64_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_percentage.empty()) {
HandleMaxOldSpaceSizePercentage(errors, &max_old_space_size_percentage);
}

per_env->CheckOptions(errors, argv);
}

Expand Down Expand Up @@ -987,6 +1035,11 @@ PerIsolateOptionsParser::PerIsolateOptionsParser(
V8Option{},
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
4 changes: 4 additions & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -283,13 +283,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_percentage;
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
143 changes: 143 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,143 @@
'use strict';

// This test validates the --max-old-space-size-percentage flag functionality

const common = require('../common');
// This flag utilizes --max-old-space-size, which is unreliable on
// 32-bit platforms due to integer overflow issues.
common.skipIf32Bits();

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

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

// Invalid cases
const invalidPercentages = [
['', /--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%/],
];

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

// Test invalid cases
invalidPercentages.forEach((input) => {
const result = spawnSync(process.execPath, [
`--max-old-space-size-percentage=${input[0]}`,
], { stdio: ['pipe', 'pipe', 'pipe'] });
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, [], {
stdio: ['pipe', 'pipe', 'pipe'],
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}`);
});

// Test NODE_OPTIONS with invalid percentages
invalidPercentages.forEach((input) => {
const result = spawnSync(process.execPath, [], {
stdio: ['pipe', 'pipe', 'pipe'],
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[0]}`);
assert(input[1].test(result.stderr.toString()), `NODE_OPTIONS: Unexpected error message for invalid input ${input[0]}`);
});

// Test percentage calculation validation
function getHeapSizeForPercentage(percentage) {
const result = spawnSync(process.execPath, [
'--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();
const heapSizeLimitMB = Math.floor(stats.heap_size_limit / 1024 / 1024);
console.log(heapSizeLimitMB);
`,
], {
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()}`);
}

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

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.15 && ratio25to100 <= 0.35,
`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.65 && ratio75to100 <= 0.85,
`75% heap size should be roughly three-quarters of 100% (got ${ratio75to100.toFixed(2)}, expected ~0.75)`
);

// Validate heap sizes against system memory
const totalMemoryMB = Math.floor(os.totalmem() / 1024 / 1024);
const uint64Max = 2 ** 64 - 1;
const constrainedMemory = process.constrainedMemory();
const constrainedMemoryMB = Math.floor(constrainedMemory / 1024 / 1024);
const effectiveMemoryMB =
constrainedMemory > 0 && constrainedMemory !== uint64Max ? constrainedMemoryMB : totalMemoryMB;
const margin = 10; // 10% margin
testPercentages.forEach((percentage) => {
const upperLimit = effectiveMemoryMB * ((percentage + margin) / 100);
assert(
heapSizes[percentage] <= upperLimit,
`Heap size for ${percentage}% (${heapSizes[percentage]} MB) should not exceed upper limit (${upperLimit} MB)`
);
const lowerLimit = effectiveMemoryMB * ((percentage - margin) / 100);
assert(
heapSizes[percentage] >= lowerLimit,
`Heap size for ${percentage}% (${heapSizes[percentage]} MB) should not be less than lower limit (${lowerLimit} MB)`
);
});
Loading