From 3d86f805363287c98056b0f494765d8d1d9db17c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=98=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2?= Date: Sat, 12 Sep 2026 19:14:46 +0300 Subject: [PATCH] deps: improve macOS spawn PATH performance Signed-off-by: splincode --- .../child_process/child-process-spawn-path.js | 48 +++++++++++++++++++ deps/uv/src/unix/process.c | 21 ++++++-- 2 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 benchmark/child_process/child-process-spawn-path.js diff --git a/benchmark/child_process/child-process-spawn-path.js b/benchmark/child_process/child-process-spawn-path.js new file mode 100644 index 000000000000..4769474e6bf9 --- /dev/null +++ b/benchmark/child_process/child-process-spawn-path.js @@ -0,0 +1,48 @@ +'use strict'; +const common = require('../common.js'); +const { spawnSync } = require('child_process'); +const { basename, delimiter, dirname, join } = require('path'); +const { tmpdir } = require('os'); + +const bench = common.createBenchmark(main, { + n: [100], + pathEntries: [0, 8, 32], + file: ['path', 'absolute'], +}); + +function main({ n, pathEntries, file }) { + const originalPath = process.env.PATH; + const executable = process.execPath; + const executableDir = dirname(executable); + const executableName = basename(executable); + const missingPaths = []; + + for (let i = 0; i < pathEntries; ++i) { + missingPaths.push(join(tmpdir(), `node-spawn-path-missing-${process.pid}-${i}`)); + } + + process.env.PATH = [...missingPaths, executableDir].join(delimiter); + + const command = file === 'path' ? executableName : executable; + const args = ['-e', '']; + const options = { stdio: 'ignore' }; + + try { + const warmup = spawnSync(command, args, options); + if (warmup.error) + throw warmup.error; + + bench.start(); + for (let i = 0; i < n; ++i) { + const result = spawnSync(command, args, options); + if (result.error) + throw result.error; + } + bench.end(n); + } finally { + if (originalPath === undefined) + delete process.env.PATH; + else + process.env.PATH = originalPath; + } +} diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c index 539e7d9417d3..8d19dc6fad93 100644 --- a/deps/uv/src/unix/process.c +++ b/deps/uv/src/unix/process.c @@ -682,6 +682,7 @@ static int uv__spawn_resolve_and_spawn(const uv_process_options_t* options, const char *p; const char *z; const char *path; + const char *parent_path; size_t l; size_t k; int err; @@ -701,10 +702,7 @@ static int uv__spawn_resolve_and_spawn(const uv_process_options_t* options, if (options->env != NULL) env = options->env; - /* If options->file contains a slash, posix_spawn/posix_spawnp should behave - * the same, and do not involve PATH resolution at all. The libc - * `posix_spawnp` provided by Apple is buggy (since 10.15), so we now emulate it - * here, per https://github.com/libuv/libuv/pull/3583. */ + /* If options->file contains a slash, PATH resolution is not needed. */ if (strchr(options->file, '/') != NULL) { do err = posix_spawn(pid, options->file, actions, attrs, options->args, env); @@ -715,6 +713,21 @@ static int uv__spawn_resolve_and_spawn(const uv_process_options_t* options, /* Look for the definition of PATH in the provided env */ path = uv__spawn_find_path_in_env(env); + /* Apple's posix_spawnp() resolves PATH from the parent's environment rather + * than env. It is safe to use when no cwd change is requested and both PATH + * values are identical. This avoids one posix_spawn() call per PATH entry in + * the common case. */ + parent_path = getenv("PATH"); + if (options->cwd == NULL && + path != NULL && + parent_path != NULL && + strcmp(path, parent_path) == 0) { + do + err = posix_spawnp(pid, options->file, actions, attrs, options->args, env); + while (err == EINTR); + return err; + } + /* The following resolution logic (execvpe emulation) is copied from * https://git.musl-libc.org/cgit/musl/tree/src/process/execvp.c * and adapted to work for our specific usage */