mirror of
https://github.com/nodejs/node.git
synced 2025-08-15 13:48:44 +02:00
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: https://github.com/nodejs/node/issues/57447 PR-URL: https://github.com/nodejs/node/pull/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>
This commit is contained in:
parent
9a3e755bdb
commit
0bbe7c36c9
7 changed files with 223 additions and 0 deletions
|
@ -1719,6 +1719,22 @@ changes:
|
|||
|
||||
Specify the maximum size, in bytes, of HTTP headers. Defaults to 16 KiB.
|
||||
|
||||
### `--max-old-space-size-percentage=PERCENTAGE`
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
### `--napi-modules`
|
||||
|
||||
<!-- YAML
|
||||
|
@ -3435,6 +3451,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`
|
||||
|
|
|
@ -269,6 +269,9 @@
|
|||
"max-http-header-size": {
|
||||
"type": "number"
|
||||
},
|
||||
"max-old-space-size-percentage": {
|
||||
"type": "string"
|
||||
},
|
||||
"network-family-autoselection": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
10
doc/node.1
10
doc/node.1
|
@ -335,6 +335,16 @@ 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 max 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.
|
||||
.
|
||||
.It Fl -napi-modules
|
||||
This option is a no-op.
|
||||
It is kept for compatibility.
|
||||
|
|
|
@ -765,6 +765,13 @@ 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_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::ranges::find(v8_args, "--abort-on-uncaught-exception") !=
|
||||
v8_args.end() ||
|
||||
|
|
|
@ -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
|
||||
|
@ -14,6 +15,7 @@
|
|||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
|
@ -107,8 +109,49 @@ 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;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
@ -1086,6 +1129,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(
|
||||
|
|
|
@ -286,6 +286,8 @@ 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;
|
||||
|
@ -293,6 +295,8 @@ class PerIsolateOptions : public Options {
|
|||
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;
|
||||
|
||||
|
|
134
test/parallel/test-max-old-space-size-percentage.js
Normal file
134
test/parallel/test-max-old-space-size-percentage.js
Normal file
|
@ -0,0 +1,134 @@
|
|||
'use strict';
|
||||
|
||||
// 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 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 margin = 10; // 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)`
|
||||
);
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue