From c837d144d2cd7bba032e3eddf31aa595dc85b797 Mon Sep 17 00:00:00 2001 From: Asaf Federman Date: Wed, 23 Jul 2025 21:19:00 +0300 Subject: [PATCH 1/8] 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 --- doc/api/cli.md | 22 ++- doc/node-config-schema.json | 3 + src/node.cc | 6 + src/node_options.cc | 50 ++++++- src/node_options.h | 4 + .../test-max-old-space-size-percentage.js | 138 ++++++++++++++++++ 6 files changed, 217 insertions(+), 6 deletions(-) create mode 100644 test/parallel/test-max-old-space-size-percentage.js diff --git a/doc/api/cli.md b/doc/api/cli.md index 5e1fb9c6399604..153a27f60b14a0 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -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." On Windows, the following settings are respected (unlike Chromium's policy, distrust and intermediate CA are not currently supported): @@ -3863,17 +3863,29 @@ documented here: -### `--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 ``` diff --git a/doc/node-config-schema.json b/doc/node-config-schema.json index e280834028c940..396565a2b2d0c6 100644 --- a/doc/node-config-schema.json +++ b/doc/node-config-schema.json @@ -269,6 +269,9 @@ "max-http-header-size": { "type": "number" }, + "max-old-space-size": { + "type": "string" + }, "network-family-autoselection": { "type": "boolean" }, diff --git a/src/node.cc b/src/node.cc index 18a1f388881aa6..41d2dfb37cd1ed 100644 --- a/src/node.cc +++ b/src/node.cc @@ -765,6 +765,12 @@ static ExitCode ProcessGlobalArgsInternal(std::vector* 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() || diff --git a/src/node_options.cc b/src/node_options.cc index 6aaa67c38dfba2..366af1435a9b1e 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -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 @@ -107,8 +108,52 @@ void PerProcessOptions::CheckOptions(std::vector* errors, per_isolate->CheckOptions(errors, argv); } +void PerIsolateOptions::HandleMaxOldSpaceSizePercentage( + std::vector* 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(memory_mb * percentage / 100.0); + + // Convert back to string + *max_old_space_size = std::to_string(calculated_mb); +} + void PerIsolateOptions::CheckOptions(std::vector* errors, std::vector* argv) { + if (!max_old_space_size.empty() && max_old_space_size.back() == '%') { + HandleMaxOldSpaceSizePercentage(errors, &max_old_space_size); + } + per_env->CheckOptions(errors, argv); } @@ -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( diff --git a/src/node_options.h b/src/node_options.h index 8bd537f7d9e149..e6fcc4c5634654 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -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; int64_t stack_trace_limit = 10; std::string report_signal = "SIGUSR2"; bool build_snapshot = false; @@ -292,6 +293,9 @@ class PerIsolateOptions : public Options { inline EnvironmentOptions* get_per_env_options(); void CheckOptions(std::vector* errors, std::vector* argv) override; + void HandleMaxOldSpaceSizePercentage( + std::vector* errors, + std::string* max_old_space_size); inline std::shared_ptr Clone() const; diff --git a/test/parallel/test-max-old-space-size-percentage.js b/test/parallel/test-max-old-space-size-percentage.js new file mode 100644 index 00000000000000..3a0931f10083ff --- /dev/null +++ b/test/parallel/test-max-old-space-size-percentage.js @@ -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(); + 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%'); From 90f8288513f65b0a043354831934dfa5a74a5fbc Mon Sep 17 00:00:00 2001 From: Asaf Federman Date: Wed, 23 Jul 2025 21:19:00 +0300 Subject: [PATCH 2/8] doc: remove unrelated changes --- doc/api/cli.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/api/cli.md b/doc/api/cli.md index 153a27f60b14a0..751a678ad5a558 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -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.” On Windows, the following settings are respected (unlike Chromium's policy, distrust and intermediate CA are not currently supported): From 84d34338e24e9731dc339d4b040c166a845f831d Mon Sep 17 00:00:00 2001 From: Asaf Federman Date: Wed, 23 Jul 2025 21:19:01 +0300 Subject: [PATCH 3/8] src: introduce max-old-space-size-percentage as a new cli flag --- doc/api/cli.md | 19 ++- doc/node-config-schema.json | 2 +- src/node.cc | 4 +- src/node_options.cc | 36 +++--- src/node_options.h | 1 + .../test-max-old-space-size-percentage.js | 108 +++++++++--------- 6 files changed, 93 insertions(+), 77 deletions(-) diff --git a/doc/api/cli.md b/doc/api/cli.md index 751a678ad5a558..e082194fe87307 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -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` @@ -3859,11 +3860,27 @@ documented here: ### `--jitless` +### `--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 +``` + -### `--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 diff --git a/doc/node-config-schema.json b/doc/node-config-schema.json index 396565a2b2d0c6..df4118172638de 100644 --- a/doc/node-config-schema.json +++ b/doc/node-config-schema.json @@ -269,7 +269,7 @@ "max-http-header-size": { "type": "number" }, - "max-old-space-size": { + "max-old-space-size-percentage": { "type": "string" }, "network-family-autoselection": { diff --git a/src/node.cc b/src/node.cc index 41d2dfb37cd1ed..8dd8add65ded69 100644 --- a/src/node.cc +++ b/src/node.cc @@ -765,7 +765,9 @@ static ExitCode ProcessGlobalArgsInternal(std::vector* 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); diff --git a/src/node_options.cc b/src/node_options.cc index 366af1435a9b1e..e1b55333557972 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -109,25 +109,23 @@ void PerProcessOptions::CheckOptions(std::vector* errors, } void PerIsolateOptions::HandleMaxOldSpaceSizePercentage( - std::vector* 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* 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 = + 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; } @@ -145,13 +143,13 @@ void PerIsolateOptions::HandleMaxOldSpaceSizePercentage( size_t calculated_mb = static_cast(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* errors, std::vector* 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); @@ -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( diff --git a/src/node_options.h b/src/node_options.h index e6fcc4c5634654..8005039f88409c 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -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"; diff --git a/test/parallel/test-max-old-space-size-percentage.js b/test/parallel/test-max-old-space-size-percentage.js index 3a0931f10083ff..d52971b4680c28 100644 --- a/test/parallel/test-max-old-space-size-percentage.js +++ b/test/parallel/test-max-old-space-size-percentage.js @@ -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(); 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 = {}; @@ -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)` + ); +}); From ebaa850bb2e2af5dec9282df7979b7cd84b220fc Mon Sep 17 00:00:00 2001 From: Asaf Federman Date: Wed, 23 Jul 2025 21:19:01 +0300 Subject: [PATCH 4/8] doc: remove doc related to modified implementation --- doc/api/cli.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/doc/api/cli.md b/doc/api/cli.md index e082194fe87307..ea2cb727cf78cc 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -3886,23 +3886,11 @@ 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 ``` From c37775630da098a294e698c461cb18fb6af2d5f8 Mon Sep 17 00:00:00 2001 From: Asaf Federman Date: Wed, 23 Jul 2025 21:19:01 +0300 Subject: [PATCH 5/8] doc: change max-old-space-size-percentage type to be number --- doc/node-config-schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/node-config-schema.json b/doc/node-config-schema.json index df4118172638de..0532732ed4a04c 100644 --- a/doc/node-config-schema.json +++ b/doc/node-config-schema.json @@ -270,7 +270,7 @@ "type": "number" }, "max-old-space-size-percentage": { - "type": "string" + "type": "number" }, "network-family-autoselection": { "type": "boolean" From a23ece3b0f12b85c25b04e5c66ceaa2eec039f81 Mon Sep 17 00:00:00 2001 From: Asaf Federman Date: Wed, 23 Jul 2025 21:19:01 +0300 Subject: [PATCH 6/8] doc: revert max-old-space-size-percentage type to be a string --- doc/node-config-schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/node-config-schema.json b/doc/node-config-schema.json index 0532732ed4a04c..df4118172638de 100644 --- a/doc/node-config-schema.json +++ b/doc/node-config-schema.json @@ -270,7 +270,7 @@ "type": "number" }, "max-old-space-size-percentage": { - "type": "number" + "type": "string" }, "network-family-autoselection": { "type": "boolean" From b919043d35a82456694ca426afb2c3614d318ae0 Mon Sep 17 00:00:00 2001 From: Asaf Federman Date: Wed, 23 Jul 2025 21:19:01 +0300 Subject: [PATCH 7/8] doc: fix linting and doc issues --- doc/api/cli.md | 32 ++++++++++++++++---------------- doc/node.1 | 10 ++++++++++ src/node.cc | 11 +++++------ src/node_options.cc | 7 ++++--- src/node_options.h | 5 ++--- 5 files changed, 37 insertions(+), 28 deletions(-) diff --git a/doc/api/cli.md b/doc/api/cli.md index ea2cb727cf78cc..64c31ef2576865 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1716,6 +1716,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` diff --git a/doc/node.1 b/doc/node.1 index 14ce85c9e6c319..0e350041a65b3d 100644 --- a/doc/node.1 +++ b/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. diff --git a/src/node.cc b/src/node.cc index 8dd8add65ded69..d3bbaa40250557 100644 --- a/src/node.cc +++ b/src/node.cc @@ -765,12 +765,11 @@ static ExitCode ProcessGlobalArgsInternal(std::vector* 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); + 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; diff --git a/src/node_options.cc b/src/node_options.cc index e1b55333557972..d2b1d5f8c648b9 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -126,7 +126,8 @@ void PerIsolateOptions::HandleMaxOldSpaceSizePercentage( // 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); + "than 0 and up to 100. Got: " + + original_input_for_error); return; } @@ -135,8 +136,8 @@ void PerIsolateOptions::HandleMaxOldSpaceSizePercentage( 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; + 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); diff --git a/src/node_options.h b/src/node_options.h index 8005039f88409c..31a048ef334085 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -294,9 +294,8 @@ class PerIsolateOptions : public Options { inline EnvironmentOptions* get_per_env_options(); void CheckOptions(std::vector* errors, std::vector* argv) override; - void HandleMaxOldSpaceSizePercentage( - std::vector* errors, - std::string* max_old_space_size); + void HandleMaxOldSpaceSizePercentage(std::vector* errors, + std::string* max_old_space_size); inline std::shared_ptr Clone() const; From 8e5f5ea35119a0d66790eb5ac3cd61af924d4120 Mon Sep 17 00:00:00 2001 From: Asaf Federman Date: Wed, 23 Jul 2025 21:19:02 +0300 Subject: [PATCH 8/8] src: handle UINT64_MAX (according to libuv doc) and remove redundant validation --- src/node_options.cc | 25 +++++++++---------- .../test-max-old-space-size-percentage.js | 8 +++--- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/node_options.cc b/src/node_options.cc index d2b1d5f8c648b9..6f7097f222b470 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -112,12 +113,6 @@ void PerIsolateOptions::HandleMaxOldSpaceSizePercentage( std::vector* 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 = @@ -131,17 +126,21 @@ void PerIsolateOptions::HandleMaxOldSpaceSizePercentage( return; } - // Get available memory in MB - size_t total_memory = uv_get_total_memory(); - size_t constrained_memory = uv_get_constrained_memory(); + // 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 - size_t available_memory = - (constrained_memory > 0) ? constrained_memory : 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 - size_t memory_mb = available_memory / (1024 * 1024); - size_t calculated_mb = static_cast(memory_mb * percentage / 100.0); + uint64_t memory_mb = available_memory / (1024 * 1024); + uint64_t calculated_mb = static_cast(memory_mb * percentage / 100.0); // Convert back to string max_old_space_size = std::to_string(calculated_mb); diff --git a/test/parallel/test-max-old-space-size-percentage.js b/test/parallel/test-max-old-space-size-percentage.js index d52971b4680c28..511a10109f9d2b 100644 --- a/test/parallel/test-max-old-space-size-percentage.js +++ b/test/parallel/test-max-old-space-size-percentage.js @@ -99,27 +99,27 @@ testPercentages.forEach((percentage) => { // 50% should be roughly half of 100% const ratio50to100 = heapSizes[50] / heapSizes[100]; assert( - ratio50to100 >= 0.45 && ratio50to100 <= 0.55, + 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.3, + 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.7 && ratio75to100 <= 0.8, + 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 = 5; // 5% margin +const margin = 10; // 5% margin testPercentages.forEach((percentage) => { const upperLimit = totalMemoryMB * ((percentage + margin) / 100); assert(