diff --git a/doc/api/cli.md b/doc/api/cli.md index edaebed0cefaba..680fb41ab57036 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -4051,6 +4051,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 @@ -4064,15 +4073,22 @@ on synchronous system APIs. Node.js APIs that use the threadpool are: * `dns.lookup()` * all `zlib` APIs, other than those that are explicitly synchronous +If this environment variable is not set, Node.js automatically sizes the +threadpool based on the available CPU parallelism, using a minimum of `4` +threads and a maximum of `1024`. This ensures better default performance on +machines with many CPU cores, where the previous fixed default of `4` threads +could become a bottleneck. + Because libuv's threadpool has a fixed size, it means that if for whatever reason any of these APIs takes a long time, other (seemingly unrelated) APIs that run in libuv's threadpool will experience degraded performance. In order to mitigate this issue, one potential solution is to increase the size of libuv's threadpool by setting the `'UV_THREADPOOL_SIZE'` environment variable to a value -greater than `4` (its current default value). However, setting this from inside -the process using `process.env.UV_THREADPOOL_SIZE=size` is not guranteed to work -as the threadpool would have been created as part of the runtime initialisation -much before user code is run. For more information, see the [libuv threadpool documentation][]. +greater than the automatically computed default. However, setting this from +inside the process using `process.env.UV_THREADPOOL_SIZE=size` is not guaranteed +to work as the threadpool would have been created as part of the runtime +initialisation much before user code is run. For more information, see the +[libuv threadpool documentation][]. ## Useful V8 options diff --git a/src/node.cc b/src/node.cc index 5f25a5229675e9..9289f1fa035614 100644 --- a/src/node.cc +++ b/src/node.cc @@ -1233,6 +1233,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/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'); + }, + }, + ); +}