child_process: spawns take ~60 ms longer when the Node process was started via a shebang (macOS, v26.13.0 regression)
维护者通常 1 天内回复
还没有人认领这个 Issue。
评估
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 新手友好度
- 45/100
- Issue 类型
- 缺陷
- 描述清晰度
- 基本清楚
- 活跃度
- 活跃
- 技术栈
- javascript, node.js
调研方向
Start by running the supplied reproduction on v26.12.0 and v26.13.0, comparing shebang-launched and directly launched processes. Read the child_process and libuv process-spawning/child-exit-notification paths mentioned in the report, then investigate what changed in the v26.12.0–v26.13.0 range. Done when shebang-launched processes and their descendants no longer incur the reported per-spawn delay.
由索引模型根据 Issue 内容生成。
描述
Version
v26.13.0 (regressed; v26.12.0 and v26.5.0 are fine)
Platform
Darwin bitbox.local 25.4.0 Darwin Kernel Version 25.4.0: Thu Mar 19 19:33:25 PDT 2026; root:xnu-12377.101.15~1/RELEASE_ARM64_T6041 arm64 arm Darwin
Subsystem
child_process (libuv process spawning)
What steps will reproduce the bug?
[This writeup is by Claude so I can't 100% vouch for it but it seems right to me. At the very least the reproduction script is legit: it clearly shows the difference between v26.13.0 and v26.12.0 on my machine. I am not an expert at node internals or any macOS issues that might be causing this just for me but everything in this report seems to match what I observed as Claude helped me debug this problem.]
Node binaries are the official nodejs.org arm64 tarballs (installed with
mise, but the issue is independent of the installer; the node binaries'
code signatures are identical between the fast and slow versions, see
below).
# A script whose interpreter is node, spawning a trivial child 100 times.
cat > /tmp/sb <<'EOF'
#!/usr/bin/env node
const { execFile } = require("child_process");
const N = 100; let i = 0; const t = Date.now();
(function next() {
if (i++ === N) { console.log(((Date.now() - t) / N).toFixed(1), "ms per spawn", process.version); return; }
execFile("git", ["--version"], next);
})();
EOF
chmod +x /tmp/sb
/tmp/sb # kernel starts the process via the shebang -> node
node /tmp/sb # same script, node started directly
How often does it reproduce? Is there a required condition?
Every time. Results on the same machine, same minute (#!<abs path to node>
scripts so each version is the interpreter; git --version is the child):
| node binary | started via shebang | started as node script |
|---|---|---|
| v26.5.0 | 3.3 ms per spawn | 3.2 ms |
| v26.12.0 | 3.2 ms per spawn | — |
| v26.13.0 | 55.5 ms per spawn | 3.3 ms |
Three consecutive rounds of the v26.13.0 case: 61.8 / 61.9 / 58.4 ms via
shebang, 3.4 / 3.4 / 3.3 ms started directly. /usr/bin/env node typed at
the shell (not through the kernel's #! handling) is also fast, so the
condition is specifically that the kernel launched the process as a script
interpreter.
The required condition is only how the root of the process tree was
launched — the slowness is inherited by descendants:
- A
#!/usr/bin/env nodescript thatspawnSyncs a secondnodewhich
runs the benchmark: the second node is slow too (61.7 ms). - A
#!/bin/bashscript that doesexec node …(so the process that is
now node was created via a shebang with bash as the interpreter) is slow
as well, and so are the node children it spawns. The same bash script
run asbash scriptis fast. - Started directly, node spawning node spawning children: fast (3.5 ms).
Things that make no difference (each tested): environment variables
(replaying the full environment of the slow case into a directly-started
node is fast); PATH length (35 entries vs 2 — the failed posix_spawn PATH
probes are not the cost); which git (Apple's /usr/bin/git stub,
Homebrew's, by absolute path); nice/priority (normal in both); fsync in
the child (a child that does no I/O, git --version, shows it).
What is the expected behavior? Why is that the expected behavior?
A spawn should cost the same few milliseconds whether or not the node
process was started by the kernel as a script interpreter, as it does in
v26.12.0 and earlier.
What do you see instead?
~55–90 ms per child_process spawn, every spawn, for the life of the
process and all its node descendants.
Where the time goes: the child itself is not slow and process creation is
not slow — the parent is late noticing the exit. Instrumented in a real
workload (an Express app's git store, ~850 execFile("git", …) calls per
test run), with timestamps at execFile(), the child's spawn event, and
the completion callback, plus GIT_TRACE_PERFORMANCE=1 for git's own
clock:
| directly started tree | shebang-started tree | |
|---|---|---|
execFile → spawn event |
0.6–1.0 ms | 0.8–1.0 ms |
| git's own elapsed (GIT_TRACE_PERFORMANCE) | ~2.5 ms | ~2.5 ms |
| execFile → callback | 6.8 ms p50 / 8.0 ms p90 | 92.7 ms p50 / 160 ms p90 |
So ~85 ms elapses between the child exiting and the parent's callback.
The per-command p50s are identical for every git subcommand (config,
rev-parse, hash-object, …), consistent with a fixed per-spawn delay
rather than work.
Additional information
Real-world impact. Almost everything that launches node in a dev
workflow does it through a shebang: npm itself (bin/npm →
npm-cli.js, #!/usr/bin/env node; version managers' bash wrappers are
shebang scripts too), npx, and every node_modules/.bin/* shim. So
npm test on v26.13.0 on macOS runs any suite that spawns processes with
this tax on every spawn, while node --test … typed directly does not.
Our gate's git-heavy tests went from ~300 ms to 5–9 s per test (10–50×),
and two tests with real-world deadlines (a 1 s push timeout; "a Save
returns within 10 s") failed. It took two days to find because nothing
run "by hand" reproduces it.
Not a code-signing / Gatekeeper difference. codesign -dv --verbose=2
on the two binaries: both flags=0x10000(runtime),
TeamIdentifier=HX7739G8FX, Runtime Version=15.5.0, same entitlements.
macOS's syspolicyd does log a violates validation category policy
line for the short-lived children that posix_spawn PATH probing creates,
but that happens equally in the fast and slow cases and the PATH-length
test shows it is not the cost.
Suspicion. Something in the process-spawning or child-exit-notification
path changed between v26.12.0 and v26.13.0 (libuv upgrade?) and behaves
differently when the process's original exec was the kernel's interpreter
path (imgact_shell) — e.g. a check that looks at the process's
executable path / argv[0] / P_* flags, or a kqueue/EVFILT_PROC vs
SIGCHLD difference where the shebang-launched process has a different
state. The bisect range is exactly v26.12.0 → v26.13.0; happy to test
nightlies or a patch.
Workarounds we use. Pin macOS hosts to v26.12.0; launch scripts with
an explicit interpreter (bash script, node script, node …/npm/bin/npm-cli.js run …) so no process in the tree is shebang-started.
- 主要语言
- JavaScript
- 星标
- 123k
- 派生
- 39.3k
- 平均合并
- 4 天 11 小时
- 30 天内合并 PR
- 237
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
nodejs/node 的其他 Issue
-
util: parseArgs() treats a `-` inside a short option group as the `--` terminator可能已有人在做 @yohannesjoya 今天认领。 未关闭
难度 2/5 1-3 小时 新手友好度 75/100
维护者通常 1 天内回复
-
http2: content-encoding is incorrectly listed in kSingleValueHeaders可能已有人在做 @inoway46 于 2 天前认领。 未关闭confirmed-bug http2
难度 2/5 1-3 小时 新手友好度 74/100
维护者通常 1 天内回复
-
util.parseEnv and --env-file keep a leading UTF-8 BOM in the first key可能已有人在做 @helmipradita 于 5 天前认领。 未关闭
难度 2/5 1-3 小时 新手友好度 78/100
维护者通常 1 天内回复
-
node:internal/inspector/network_http: `TypeError: Missing dataLength` in event when response uses `setEncoding()`可能重新可做 关联的 PR 已关闭且未合并。 未关闭
难度 2/5 1-3 小时 新手友好度 78/100
维护者通常 1 天内回复
-
[Docs] `process.loadEnvFile()` does not document behaviour when variables already exist可能已有人在做 @Sepandard 于 38 天前认领。 未关闭doc
难度 1/5 1 小时以内 新手友好度 90/100
维护者通常 1 天内回复
相似的 Issue
-
[ordering-review] workshop order graph: 1 confirmed ordering problem, 3 cycles are false positives未关闭
难度 2/5 1-3 小时 新手友好度 85/100
githubnext/gh-aw-workshop#5331 ·
维护者通常 1 天内回复
-
channels:remove check:passed
难度 1/5 1 小时以内 新手友好度 65/100
维护者通常 4 天内回复
-
[vika] console keeps a stale "in progress" turn after a daemon restart mid-run可能已有人在做 关联的 PR 仍在进行中或已合并。 未关闭
难度 2/5 1-3 小时 新手友好度 86/100
volcengine/OpenViking#7011 ·
维护者通常 1 天内回复
-
__VERSION__ within dist build is not replaced, causes missing version in XDM implementation details未关闭
难度 2/5 1-3 小时 新手友好度 68/100
维护者通常 1 天内回复
-
Issue: ready for confirmation Reported on 2.4.9 Triage: Dev.Experience
难度 2/5 1-3 小时 新手友好度 70/100
magento/magento2#41773 · 1 条评论 ·
维护者通常 5 天内回复