From a68394a79131ad94e10f63c93052056d3f6e00dd Mon Sep 17 00:00:00 2001 From: RafaelGSS Date: Mon, 26 Jan 2026 13:52:50 -0300 Subject: [PATCH] src: set UV_THREADPOOL_SIZE based on available parallelism When UV_THREADPOOL_SIZE is not set, Node.js will auto-size it based on uv_available_parallelism(), with a minimum of 4 and a maximum of 1024. --- doc/api/cli.md | 9 ++++ src/node.cc | 19 +++++++++ src/node_dotenv.cc | 4 ++ src/node_dotenv.h | 1 + .../test_uv_threadpool_size/node-options.js | 6 ++- test/parallel/test-uv-threadpool-size-auto.js | 42 +++++++++++++++++++ 6 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-uv-threadpool-size-auto.js diff --git a/doc/api/cli.md b/doc/api/cli.md index 9ef967373c63dc..d81722c758284d 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -4066,6 +4066,15 @@ Wed May 12 2021 20:30:48 GMT+0100 (Irish Standard Time) ### `UV_THREADPOOL_SIZE=size` + + Set the number of threads used in libuv's threadpool to `size` threads. Asynchronous system APIs are used by Node.js whenever possible, but where they diff --git a/src/node.cc b/src/node.cc index de21740386cffa..01c5e4fe27aa52 100644 --- a/src/node.cc +++ b/src/node.cc @@ -1220,6 +1220,25 @@ InitializeOncePerProcessInternal(const std::vector& args, #endif // HAVE_OPENSSL } + // Set UV_THREADPOOL_SIZE based on available parallelism if not already set + // by the user. The libuv threadpool defaults to 4 threads, which can be + // suboptimal on machines with many CPU cores. Use uv_available_parallelism() + // as a heuristic, with a minimum of 4 (the previous default) and a maximum + // of 1024 (libuv's upper bound). + { + char buf[64]; + size_t buf_size = sizeof(buf); + int rc = uv_os_getenv("UV_THREADPOOL_SIZE", buf, &buf_size); + if (rc == UV_ENOENT && + !per_process::dotenv_file.HasKey("UV_THREADPOOL_SIZE")) { + unsigned int parallelism = uv_available_parallelism(); + unsigned int threadpool_size = std::min(std::max(4u, parallelism), 1024u); + char size_str[16]; + snprintf(size_str, sizeof(size_str), "%u", threadpool_size); + uv_os_setenv("UV_THREADPOOL_SIZE", size_str); + } + } + if (!(flags & ProcessInitializationFlags::kNoInitializeNodeV8Platform)) { uv_thread_setname("node-MainThread"); per_process::v8_platform.Initialize( diff --git a/src/node_dotenv.cc b/src/node_dotenv.cc index e1940904d1c039..b8a4126d92394f 100644 --- a/src/node_dotenv.cc +++ b/src/node_dotenv.cc @@ -352,6 +352,10 @@ Dotenv::ParseResult Dotenv::ParsePath(const std::string_view path) { return ParseResult::Valid; } +bool Dotenv::HasKey(const std::string_view key) const { + return store_.contains(std::string(key)); +} + void Dotenv::AssignNodeOptionsIfAvailable(std::string* node_options) const { auto match = store_.find("NODE_OPTIONS"); diff --git a/src/node_dotenv.h b/src/node_dotenv.h index 689c763907c26a..eb003b28282ed9 100644 --- a/src/node_dotenv.h +++ b/src/node_dotenv.h @@ -28,6 +28,7 @@ class Dotenv { void ParseContent(const std::string_view content); ParseResult ParsePath(const std::string_view path); void AssignNodeOptionsIfAvailable(std::string* node_options) const; + bool HasKey(const std::string_view key) const; v8::Maybe SetEnvironment(Environment* env); v8::MaybeLocal ToObject(Environment* env) const; diff --git a/test/node-api/test_uv_threadpool_size/node-options.js b/test/node-api/test_uv_threadpool_size/node-options.js index 68351c6cbee8dd..0337993e79b9ae 100644 --- a/test/node-api/test_uv_threadpool_size/node-options.js +++ b/test/node-api/test_uv_threadpool_size/node-options.js @@ -19,10 +19,14 @@ const code = ` assert.strictEqual(size, 4); test(size); `.trim(); +// Strip UV_THREADPOOL_SIZE from the inherited environment so that the +// --env-file value is not shadowed by the dynamic default set by the parent. +const env = { ...process.env }; +delete env.UV_THREADPOOL_SIZE; const child = spawnSync( process.execPath, [ `--env-file=${uvThreadPoolPath}`, '--eval', code ], - { cwd: __dirname, encoding: 'utf-8' }, + { cwd: __dirname, encoding: 'utf-8', env }, ); assert.strictEqual(child.stderr, ''); assert.strictEqual(child.status, 0); diff --git a/test/parallel/test-uv-threadpool-size-auto.js b/test/parallel/test-uv-threadpool-size-auto.js new file mode 100644 index 00000000000000..84a3483255f7ea --- /dev/null +++ b/test/parallel/test-uv-threadpool-size-auto.js @@ -0,0 +1,42 @@ +'use strict'; +require('../common'); + +const { spawnSyncAndAssert } = require('../common/child_process'); +const assert = require('assert'); +const os = require('os'); + +const expectedSize = Math.min(Math.max(4, os.availableParallelism()), 1024); + +// When UV_THREADPOOL_SIZE is not set, Node.js should auto-size it based on +// uv_available_parallelism(), with a minimum of 4 and a maximum of 1024. +{ + const env = { ...process.env }; + delete env.UV_THREADPOOL_SIZE; + + spawnSyncAndAssert( + process.execPath, + ['-e', 'console.log(process.env.UV_THREADPOOL_SIZE)'], + { env }, + { + stdout(output) { + assert.strictEqual(output.trim(), String(expectedSize)); + }, + }, + ); +} + +// When UV_THREADPOOL_SIZE is explicitly set, Node.js should not override it. +{ + const env = { ...process.env, UV_THREADPOOL_SIZE: '8' }; + + spawnSyncAndAssert( + process.execPath, + ['-e', 'console.log(process.env.UV_THREADPOOL_SIZE)'], + { env }, + { + stdout(output) { + assert.strictEqual(output.trim(), '8'); + }, + }, + ); +}