Repository navigation
child_process: build the default env block in one native pass - #65325
Merged
nodejs-github-bot merged 1 commit intoSep 30, 2026
Merged
nodejs-github-bot merged 1 commit into
nodejs-github-bot merged 1 commit into
Conversation
codebytere
force-pushed
the
perf/child_process-default-env-pairs
branch
from
August 16, 2026 15:51
40fcf9b to
a53bee9
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #65325 +/- ##
=======================================
Coverage 90.27% 90.28%
=======================================
Files 789 789
Lines 272907 272975 +68
Branches 52117 52119 +2
=======================================
+ Hits 246373 246443 +70
+ Misses 16988 16977 -11
- Partials 9546 9555 +9
🚀 New features to boost your workflow:
|
codebytere
requested review from
aduh95 and
anonrig
and removed request for
RafaelGSS
August 17, 2026 10:32
Contributor
|
Benchmark GHA (child_process): https://github.com/nodejs/node/actions/runs/32034574172 |
codebytere
requested review from
mcollina and
trivikr
and removed request for
aduh95 and
anonrig
August 19, 2026 08:54
Member
Author
|
@aduh95 PTAL, feedback addressed! |
aduh95
approved these changes
Aug 20, 2026
Collaborator
Contributor
|
codebytere
force-pushed
the
perf/child_process-default-env-pairs
branch
from
August 20, 2026 12:42
a53bee9 to
11be597
Compare
Collaborator
Collaborator
codebytere
force-pushed
the
perf/child_process-default-env-pairs
branch
from
September 23, 2026 10:22
11be597 to
803dbfd
Compare
jasnell
reviewed
Sep 23, 2026
jasnell
reviewed
Sep 23, 2026
jasnell
reviewed
Sep 23, 2026
codebytere
force-pushed
the
perf/child_process-default-env-pairs
branch
from
September 23, 2026 14:51
803dbfd to
c529b81
Compare
jasnell
approved these changes
Sep 23, 2026
Collaborator
Contributor
|
Benchmark GHA (child_process / params): https://github.com/nodejs/node/actions/runs/35887137260 |
When spawn()/spawnSync() are called without options.env, normalizeSpawnArguments() copied process.env with a spread and then walked the copy to build the KEY=value array uv_spawn() takes. Spreading the process.env proxy costs one enumerator callback plus a query and a getter interceptor per variable, each doing a linear getenv() scan and allocating; with a couple of hundred variables that was the single largest JS-side cost of spawning a process. Add KVStore::Pairs() (Enumerate() + Get() by default, one uv_os_environ() pass for the real environment, skipping hidden variables on Windows exactly like Enumerate() does), expose it as process_wrap.getEnvPairs(), and use it for the default-environment case. A user supplied options.env and the permission model case keep the existing code. On Windows the same sort/first-wins-case-insensitive filter is applied to the pairs. The variables copyProcessEnvToEnv() propagates are part of the real environment by definition, and its entries cannot contain null bytes, so those steps only remain on the options.env path. Signed-off-by: Shelley Vohr <[email protected]>
codebytere
force-pushed
the
perf/child_process-default-env-pairs
branch
from
September 24, 2026 06:09
c529b81 to
61cc514
Compare
jasnell
approved these changes
Sep 28, 2026
jasnell
approved these changes
Sep 28, 2026
Member
|
So nice I approved it twice, I guess lol... Very obvious I'm a bit mentally distracted today, sigh. |
Collaborator
Collaborator
|
Landed in bd83a5c |
aduh95
pushed a commit
that referenced
this pull request
Oct 3, 2026
When spawn()/spawnSync() are called without options.env, normalizeSpawnArguments() copied process.env with a spread and then walked the copy to build the KEY=value array uv_spawn() takes. Spreading the process.env proxy costs one enumerator callback plus a query and a getter interceptor per variable, each doing a linear getenv() scan and allocating; with a couple of hundred variables that was the single largest JS-side cost of spawning a process. Add KVStore::Pairs() (Enumerate() + Get() by default, one uv_os_environ() pass for the real environment, skipping hidden variables on Windows exactly like Enumerate() does), expose it as process_wrap.getEnvPairs(), and use it for the default-environment case. A user supplied options.env and the permission model case keep the existing code. On Windows the same sort/first-wins-case-insensitive filter is applied to the pairs. The variables copyProcessEnvToEnv() propagates are part of the real environment by definition, and its entries cannot contain null bytes, so those steps only remain on the options.env path. Signed-off-by: Shelley Vohr <[email protected]> PR-URL: #65325 Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: James M Snell <[email protected]>
aduh95
pushed a commit
that referenced
this pull request
Oct 3, 2026
When spawn()/spawnSync() are called without options.env, normalizeSpawnArguments() copied process.env with a spread and then walked the copy to build the KEY=value array uv_spawn() takes. Spreading the process.env proxy costs one enumerator callback plus a query and a getter interceptor per variable, each doing a linear getenv() scan and allocating; with a couple of hundred variables that was the single largest JS-side cost of spawning a process. Add KVStore::Pairs() (Enumerate() + Get() by default, one uv_os_environ() pass for the real environment, skipping hidden variables on Windows exactly like Enumerate() does), expose it as process_wrap.getEnvPairs(), and use it for the default-environment case. A user supplied options.env and the permission model case keep the existing code. On Windows the same sort/first-wins-case-insensitive filter is applied to the pairs. The variables copyProcessEnvToEnv() propagates are part of the real environment by definition, and its entries cannot contain null bytes, so those steps only remain on the options.env path. Signed-off-by: Shelley Vohr <[email protected]> PR-URL: #65325 Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: James M Snell <[email protected]>
bmmmm
added a commit
to bmmmm/bumpii
that referenced
this pull request
Oct 13, 2026
Measured on the daily `bumpii digest --brew-upgrade` with nothing pending: 7.2 s, of which the forge fetch phase took 4.7 s because gitea.com sends 30 release bodies uncompressed (343 KB in 3.6 s; 10 in 1.9 s) and the plain and greedy `brew outdated` listings ran one after the other (0.65 s each). - The digest asks for FIRST_PAGE (10) releases first and fetches the 30 only when the installed release itself is not on the short page (`pageReaches`). Not "a release below it": forges list by date, and review measured a multi-line project undercounting 4 for 9 and an installed 3.0.0 reading "ahead of 2.9.20" with exit 0 under the weaker test. What the short page still cannot see is stated on the docblock as an accepted narrowing. - Both `brew outdated` listings start together in every digest; the greedy one is read only after the plain one answered, and drained on failure. - Three runs afterwards: 5.9 / 3.9 / 4.8 s, fetch phase 1.8–2.9 s. The JSON document and every count are unchanged for the live tools.json. Also: since Node 26.11 (nodejs/node#65325) a child spawned without `env` gets the OS environment, so tests that reassigned `process.env` never ran their fake brew — eleven overview scenarios went red after this machine's brew upgrade pulled 26.11.1. They mutate the object in place now; trap in AGENTS.md.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
spawn(),spawnSync()and everything built on them get ~20–24 % faster when nooptions.envis passed (the common case), by not going through theprocess.envproxy once per variable to build the child's environment block.(Linux x64, 183-variable environment - the effect scales with environment size; 30 runs.)
normalizeSpawnArguments()did{ ...process.env }and walked the copy to build theKEY=valuearray foruv_spawn(). Spreading the proxy hits a query and a getter interceptor per variable, each a linear scan of the environment under the env mutex plus an allocation - O(n²), and the largest JS-side cost of a spawn (~190 µs of 1.7 ms here).This adds
KVStore::Pairs()(RealEnvStore: oneuv_os_environ()pass, skipping hidden=X:entries on Windows likeEnumerate()does; default:Enumerate()+Get()), exposes it asinternalBinding('process_wrap').getEnvPairs(), and uses it when nooptions.envis given and the permission model is off. Everything else keeps the existing path.Notes:
process.envchanges). Windows keeps its case-insensitive de-duplication.123) were hoisted to the front by object key ordering. That is the only observable difference for a normal environment.Object.prototypeno longer leak into the default child environment.options.envobjects keep their documented prototype-inclusive behavior.Tests:
test-child-process-default-env.js(new): the child sees exactly the parent's current environment viaspawnSync,execFileSyncandspawn, including variables added/deleted/emptied at runtime, values with=and non-ASCII; a userenvis passed through unmerged; a variable set after one spawn is seen by the next.Disclosure: the code, test, measurements and this description were written by Claude Code, directed and reviewed by @codebytere.