e6a5c010 fix(core): make nx planning and hashing faster (#36994)
Stacked on #36992. The diff here contains the follow-up native planner
and hasher changes.
## Current Behavior
Native planning and hashing repeat work across shared dependencies. The
planner concatenates and sorts every repeated dependency ID before
deduplicating them. The hasher accumulates each instruction into shared
result maps, sorts long keys per task, and converts those maps to
JavaScript by recreating every property name. Noninteractive runs still
format TUI trace events for individual hash entries.
## Expected Behavior
Task hashes and the hash format stay the same. The changes reduce the
work needed to produce them.
- Union dependency IDs in a temporary bitset before allocating the final
lists.
- Gather hash results locally per task, order them using shared integer
key ranks, and convert contiguous details to JavaScript while reusing
string handles.
- Skip TUI trace collection when stderr cannot support a TUI, unless the
existing capability override is enabled.
### Measured impact
These synthetic results were recorded before the five latest follow-up
commits. They are retained as historical evidence, not measurements of
the final head.
Measured on a synthetic fixture built for this change: 600 projects
across 8 layers sharing one closure of 100 packages, giving 60,362
project-to-external edges against 1,553 internal ones. That fan-in is
the shape the planner's deduplication collapses.
All three arms share one TypeScript build from master and differ only in
the release Rust binding, which is possible because these commits change
no shipped TypeScript and no N-API signatures. Medians of seven
interleaved rounds against master
[`29200561dd`](https://github.com/nrwl/nx/commit/29200561dd17a4fed8fdc1ae0cb359be5bf9ddef),
arm order reversed on alternate rounds, one warmup per arm discarded.
| Measure | master | #36992 | this PR | vs master | vs #36992 |
| --------------------- | -------: | -------: | -------: | --------: |
--------: |
| `hashMultipleTasks` | 746 ms | 646 ms | 387 ms | -48.2% | -40.2% |
| `run-many` wall | 2,173 ms | 2,117 ms | 1,808 ms | -16.8% | -14.6% |
| tasks peak RSS | 380 MB | 351 MB | 328 MB | -13.5% | -6.6% |
| tasks RSS footprint | 624 MB | 597 MB | 572 MB | -8.3% | -4.2% |
| cold graph (control) | 3,310 ms | 3,291 ms | 3,279 ms | -0.9% | -0.4%
|
| warm graph (control) | 270 ms | 271 ms | 269 ms | -0.4% | -0.7% |
Graph construction is the control and does not move, which is what a
change confined to the planner and hasher should show.
### Hash compatibility
All 600 task hashes are identical across the three arms. The sorted
task-id to hash listing digests to the same SHA-256 in each, so the
speedup does not come from computing something different.
### Scope and tradeoffs
The fixture is synthetic and the runs are macOS ARM64 on a shared
machine. They cover native planning, hashing, and N-API conversion, and
exclude Cloud and DTE. Linux and Windows validation remain outstanding.
Some small or sparse workloads are slightly slower. The bitset's
temporary size depends on the highest instruction ID, so a check that
primed 20,000 unrelated projects before planning a 16-project chain went
from 0.146 to 0.195 ms with essentially unchanged RSS. A small-call
check measured one cold task at 31.8 to 33.0 ms.
<details>
<summary>Implementation details</summary>
### Union dependency IDs before building the final lists
`packages/nx/src/native/tasks/hash_planner.rs` adds a temporary
`InstructionIdSet` backed by a bitset. Dependency IDs are inserted into
that set as child closures are merged, then emitted into an exactly
sized, sorted vector. The bitset is discarded, and memoized results keep
compact vectors.
This avoids concatenating and sorting all repeated IDs, and avoids
retaining vector capacity sized for the duplicates. For R incoming IDs
and a highest pool ID U, insertion is O(R), followed by scanning O(U/64)
words and emitting the unique IDs. Cycle detection, the legacy fallback,
and final plan ordering remain in place.
### Accumulate hash work locally and publish one result per task
`packages/nx/src/native/tasks/task_hasher.rs` replaces the flattened
instruction iterator and shared per-instruction accumulation with
parallel task processing. Each worker gathers its detail entries and
collected inputs locally, then publishes the completed task result once.
The separate shared input-accumulator map is removed.
Instruction types are classified once per pool entry. Shared-value cache
hits can be copied directly without taking the instruction pool's shard
lock, and the task environment is resolved once per task. Only pending
work enters the inner parallel iterator. Keeping that inner iterator
matters, because a single task with several expensive runtime or file
inputs can still process them concurrently.
Environment and runtime instructions remain outside the shared-value
slots because their values depend on the task environment. When input
collection is enabled, instructions still run the collection path for
each task.
### Rank hash keys once and keep details in contiguous storage
The hasher sorts the pool's detail keys once using the existing UTF-8
ordering, assigns integer ranks, and sorts each task's entries by rank.
Hash assembly feeds the same ordered value bytes into xxh3, with the
existing concatenation and output format.
Different instructions can have the same display key. Those keys share a
rank, and a stable sort with duplicate resolution preserves the existing
last-value-wins rule. The final entries stay in a vector instead of
being inserted into a native hash table and then enumerated and sorted
again.
Assembly now happens in the task worker, which removes the separate
assembly pass. Debug timing includes assembly within hashing, so the old
separate assembly-duration field and the final per-task hash-value trace
are removed.
### Reuse JavaScript handles for detail keys as well as values
`packages/nx/src/native/types/shared_str.rs` adds `SharedStrMap`,
exported through `types.rs` and used by `HashDetails`. It converts the
contiguous entries directly into a JavaScript object and routes both
property names and values through the existing `SharedStr` handle cache.
Previously the generic map conversion recreated property-name strings
for each task.
The returned results remain ordinary objects with the same fields and
writable, enumerable, configurable properties. Detail properties are now
emitted in canonical key order, where the previous native map supplied
unspecified iteration order. The measured JavaScript heap results are
mixed, so this does not claim a consistent retained-heap reduction.
### Avoid formatting TUI traces when the TUI cannot run
`packages/nx/src/native/logger/mod.rs` installs the TUI tracing layer
only when stderr is a terminal or `NX_TUI_SKIP_CAPABILITY_CHECK=true`.
This follows the existing task runner's terminal capability requirement,
and console and file logging retain their own filters.
The previous layer formatted and buffered trace events even in a
noninteractive process. Hashing emits a trace for every detail entry, so
that meant millions of formatting operations and temporary allocations.
The buffers are bounded, so the problem is the repeated work rather than
an unbounded log history.
</details>
<details>
<summary>Regression coverage</summary>
`packages/nx/src/native/tests/task-hasher.spec.ts` covers batch and
single-task equivalence, UTF-8 versus UTF-16 ordering, ordinary object
descriptors, repeated and frozen results, serialization, per-task
environments, live JSON changes, input collection, empty selections, and
recovery after missing environments.
The Rust additions check bitset word boundaries, ranked assembly against
an independent map implementation, duplicate-key resolution, Unicode
ordering, empty assembly, and TUI logging filters. Separate V8
scavenge-stress checks with a 1 MiB semi-space matched the prior
baseline, and console and file logging checks passed.
</details>
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
No linked issue. This came from investigating memory use during
concurrent `nx affected` commands in CI.
---------
Co-authored-by: Kyle Cannon <867978+kylecannon@users.noreply.github.com>
Co-authored-by: Craigory Coppola <craigorycoppola@gmail.com> e6a5c010 fix(core): make nx planning and hashing faster (#36994)
Stacked on #36992. The diff here contains the follow-up native planner
and hasher changes.
## Current Behavior
Native planning and hashing repeat work across shared dependencies. The
planner concatenates and sorts every repeated dependency ID before
deduplicating them. The hasher accumulates each instruction into shared
result maps, sorts long keys per task, and converts those maps to
JavaScript by recreating every property name. Noninteractive runs still
format TUI trace events for individual hash entries.
## Expected Behavior
Task hashes and the hash format stay the same. The changes reduce the
work needed to produce them.
- Union dependency IDs in a temporary bitset before allocating the final
lists.
- Gather hash results locally per task, order them using shared integer
key ranks, and convert contiguous details to JavaScript while reusing
string handles.
- Skip TUI trace collection when stderr cannot support a TUI, unless the
existing capability override is enabled.
### Measured impact
These synthetic results were recorded before the five latest follow-up
commits. They are retained as historical evidence, not measurements of
the final head.
Measured on a synthetic fixture built for this change: 600 projects
across 8 layers sharing one closure of 100 packages, giving 60,362
project-to-external edges against 1,553 internal ones. That fan-in is
the shape the planner's deduplication collapses.
All three arms share one TypeScript build from master and differ only in
the release Rust binding, which is possible because these commits change
no shipped TypeScript and no N-API signatures. Medians of seven
interleaved rounds against master
[`29200561dd`](https://github.com/nrwl/nx/commit/29200561dd17a4fed8fdc1ae0cb359be5bf9ddef),
arm order reversed on alternate rounds, one warmup per arm discarded.
| Measure | master | #36992 | this PR | vs master | vs #36992 |
| --------------------- | -------: | -------: | -------: | --------: |
--------: |
| `hashMultipleTasks` | 746 ms | 646 ms | 387 ms | -48.2% | -40.2% |
| `run-many` wall | 2,173 ms | 2,117 ms | 1,808 ms | -16.8% | -14.6% |
| tasks peak RSS | 380 MB | 351 MB | 328 MB | -13.5% | -6.6% |
| tasks RSS footprint | 624 MB | 597 MB | 572 MB | -8.3% | -4.2% |
| cold graph (control) | 3,310 ms | 3,291 ms | 3,279 ms | -0.9% | -0.4%
|
| warm graph (control) | 270 ms | 271 ms | 269 ms | -0.4% | -0.7% |
Graph construction is the control and does not move, which is what a
change confined to the planner and hasher should show.
### Hash compatibility
All 600 task hashes are identical across the three arms. The sorted
task-id to hash listing digests to the same SHA-256 in each, so the
speedup does not come from computing something different.
### Scope and tradeoffs
The fixture is synthetic and the runs are macOS ARM64 on a shared
machine. They cover native planning, hashing, and N-API conversion, and
exclude Cloud and DTE. Linux and Windows validation remain outstanding.
Some small or sparse workloads are slightly slower. The bitset's
temporary size depends on the highest instruction ID, so a check that
primed 20,000 unrelated projects before planning a 16-project chain went
from 0.146 to 0.195 ms with essentially unchanged RSS. A small-call
check measured one cold task at 31.8 to 33.0 ms.
<details>
<summary>Implementation details</summary>
### Union dependency IDs before building the final lists
`packages/nx/src/native/tasks/hash_planner.rs` adds a temporary
`InstructionIdSet` backed by a bitset. Dependency IDs are inserted into
that set as child closures are merged, then emitted into an exactly
sized, sorted vector. The bitset is discarded, and memoized results keep
compact vectors.
This avoids concatenating and sorting all repeated IDs, and avoids
retaining vector capacity sized for the duplicates. For R incoming IDs
and a highest pool ID U, insertion is O(R), followed by scanning O(U/64)
words and emitting the unique IDs. Cycle detection, the legacy fallback,
and final plan ordering remain in place.
### Accumulate hash work locally and publish one result per task
`packages/nx/src/native/tasks/task_hasher.rs` replaces the flattened
instruction iterator and shared per-instruction accumulation with
parallel task processing. Each worker gathers its detail entries and
collected inputs locally, then publishes the completed task result once.
The separate shared input-accumulator map is removed.
Instruction types are classified once per pool entry. Shared-value cache
hits can be copied directly without taking the instruction pool's shard
lock, and the task environment is resolved once per task. Only pending
work enters the inner parallel iterator. Keeping that inner iterator
matters, because a single task with several expensive runtime or file
inputs can still process them concurrently.
Environment and runtime instructions remain outside the shared-value
slots because their values depend on the task environment. When input
collection is enabled, instructions still run the collection path for
each task.
### Rank hash keys once and keep details in contiguous storage
The hasher sorts the pool's detail keys once using the existing UTF-8
ordering, assigns integer ranks, and sorts each task's entries by rank.
Hash assembly feeds the same ordered value bytes into xxh3, with the
existing concatenation and output format.
Different instructions can have the same display key. Those keys share a
rank, and a stable sort with duplicate resolution preserves the existing
last-value-wins rule. The final entries stay in a vector instead of
being inserted into a native hash table and then enumerated and sorted
again.
Assembly now happens in the task worker, which removes the separate
assembly pass. Debug timing includes assembly within hashing, so the old
separate assembly-duration field and the final per-task hash-value trace
are removed.
### Reuse JavaScript handles for detail keys as well as values
`packages/nx/src/native/types/shared_str.rs` adds `SharedStrMap`,
exported through `types.rs` and used by `HashDetails`. It converts the
contiguous entries directly into a JavaScript object and routes both
property names and values through the existing `SharedStr` handle cache.
Previously the generic map conversion recreated property-name strings
for each task.
The returned results remain ordinary objects with the same fields and
writable, enumerable, configurable properties. Detail properties are now
emitted in canonical key order, where the previous native map supplied
unspecified iteration order. The measured JavaScript heap results are
mixed, so this does not claim a consistent retained-heap reduction.
### Avoid formatting TUI traces when the TUI cannot run
`packages/nx/src/native/logger/mod.rs` installs the TUI tracing layer
only when stderr is a terminal or `NX_TUI_SKIP_CAPABILITY_CHECK=true`.
This follows the existing task runner's terminal capability requirement,
and console and file logging retain their own filters.
The previous layer formatted and buffered trace events even in a
noninteractive process. Hashing emits a trace for every detail entry, so
that meant millions of formatting operations and temporary allocations.
The buffers are bounded, so the problem is the repeated work rather than
an unbounded log history.
</details>
<details>
<summary>Regression coverage</summary>
`packages/nx/src/native/tests/task-hasher.spec.ts` covers batch and
single-task equivalence, UTF-8 versus UTF-16 ordering, ordinary object
descriptors, repeated and frozen results, serialization, per-task
environments, live JSON changes, input collection, empty selections, and
recovery after missing environments.
The Rust additions check bitset word boundaries, ranked assembly against
an independent map implementation, duplicate-key resolution, Unicode
ordering, empty assembly, and TUI logging filters. Separate V8
scavenge-stress checks with a 1 MiB semi-space matched the prior
baseline, and console and file logging checks passed.
</details>
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
No linked issue. This came from investigating memory use during
concurrent `nx affected` commands in CI.
---------
Co-authored-by: Kyle Cannon <867978+kylecannon@users.noreply.github.com>
Co-authored-by: Craigory Coppola <craigorycoppola@gmail.com> e6a5c010 fix(core): make nx planning and hashing faster (#36994)
Stacked on #36992. The diff here contains the follow-up native planner
and hasher changes.
## Current Behavior
Native planning and hashing repeat work across shared dependencies. The
planner concatenates and sorts every repeated dependency ID before
deduplicating them. The hasher accumulates each instruction into shared
result maps, sorts long keys per task, and converts those maps to
JavaScript by recreating every property name. Noninteractive runs still
format TUI trace events for individual hash entries.
## Expected Behavior
Task hashes and the hash format stay the same. The changes reduce the
work needed to produce them.
- Union dependency IDs in a temporary bitset before allocating the final
lists.
- Gather hash results locally per task, order them using shared integer
key ranks, and convert contiguous details to JavaScript while reusing
string handles.
- Skip TUI trace collection when stderr cannot support a TUI, unless the
existing capability override is enabled.
### Measured impact
These synthetic results were recorded before the five latest follow-up
commits. They are retained as historical evidence, not measurements of
the final head.
Measured on a synthetic fixture built for this change: 600 projects
across 8 layers sharing one closure of 100 packages, giving 60,362
project-to-external edges against 1,553 internal ones. That fan-in is
the shape the planner's deduplication collapses.
All three arms share one TypeScript build from master and differ only in
the release Rust binding, which is possible because these commits change
no shipped TypeScript and no N-API signatures. Medians of seven
interleaved rounds against master
[`29200561dd`](https://github.com/nrwl/nx/commit/29200561dd17a4fed8fdc1ae0cb359be5bf9ddef),
arm order reversed on alternate rounds, one warmup per arm discarded.
| Measure | master | #36992 | this PR | vs master | vs #36992 |
| --------------------- | -------: | -------: | -------: | --------: |
--------: |
| `hashMultipleTasks` | 746 ms | 646 ms | 387 ms | -48.2% | -40.2% |
| `run-many` wall | 2,173 ms | 2,117 ms | 1,808 ms | -16.8% | -14.6% |
| tasks peak RSS | 380 MB | 351 MB | 328 MB | -13.5% | -6.6% |
| tasks RSS footprint | 624 MB | 597 MB | 572 MB | -8.3% | -4.2% |
| cold graph (control) | 3,310 ms | 3,291 ms | 3,279 ms | -0.9% | -0.4%
|
| warm graph (control) | 270 ms | 271 ms | 269 ms | -0.4% | -0.7% |
Graph construction is the control and does not move, which is what a
change confined to the planner and hasher should show.
### Hash compatibility
All 600 task hashes are identical across the three arms. The sorted
task-id to hash listing digests to the same SHA-256 in each, so the
speedup does not come from computing something different.
### Scope and tradeoffs
The fixture is synthetic and the runs are macOS ARM64 on a shared
machine. They cover native planning, hashing, and N-API conversion, and
exclude Cloud and DTE. Linux and Windows validation remain outstanding.
Some small or sparse workloads are slightly slower. The bitset's
temporary size depends on the highest instruction ID, so a check that
primed 20,000 unrelated projects before planning a 16-project chain went
from 0.146 to 0.195 ms with essentially unchanged RSS. A small-call
check measured one cold task at 31.8 to 33.0 ms.
<details>
<summary>Implementation details</summary>
### Union dependency IDs before building the final lists
`packages/nx/src/native/tasks/hash_planner.rs` adds a temporary
`InstructionIdSet` backed by a bitset. Dependency IDs are inserted into
that set as child closures are merged, then emitted into an exactly
sized, sorted vector. The bitset is discarded, and memoized results keep
compact vectors.
This avoids concatenating and sorting all repeated IDs, and avoids
retaining vector capacity sized for the duplicates. For R incoming IDs
and a highest pool ID U, insertion is O(R), followed by scanning O(U/64)
words and emitting the unique IDs. Cycle detection, the legacy fallback,
and final plan ordering remain in place.
### Accumulate hash work locally and publish one result per task
`packages/nx/src/native/tasks/task_hasher.rs` replaces the flattened
instruction iterator and shared per-instruction accumulation with
parallel task processing. Each worker gathers its detail entries and
collected inputs locally, then publishes the completed task result once.
The separate shared input-accumulator map is removed.
Instruction types are classified once per pool entry. Shared-value cache
hits can be copied directly without taking the instruction pool's shard
lock, and the task environment is resolved once per task. Only pending
work enters the inner parallel iterator. Keeping that inner iterator
matters, because a single task with several expensive runtime or file
inputs can still process them concurrently.
Environment and runtime instructions remain outside the shared-value
slots because their values depend on the task environment. When input
collection is enabled, instructions still run the collection path for
each task.
### Rank hash keys once and keep details in contiguous storage
The hasher sorts the pool's detail keys once using the existing UTF-8
ordering, assigns integer ranks, and sorts each task's entries by rank.
Hash assembly feeds the same ordered value bytes into xxh3, with the
existing concatenation and output format.
Different instructions can have the same display key. Those keys share a
rank, and a stable sort with duplicate resolution preserves the existing
last-value-wins rule. The final entries stay in a vector instead of
being inserted into a native hash table and then enumerated and sorted
again.
Assembly now happens in the task worker, which removes the separate
assembly pass. Debug timing includes assembly within hashing, so the old
separate assembly-duration field and the final per-task hash-value trace
are removed.
### Reuse JavaScript handles for detail keys as well as values
`packages/nx/src/native/types/shared_str.rs` adds `SharedStrMap`,
exported through `types.rs` and used by `HashDetails`. It converts the
contiguous entries directly into a JavaScript object and routes both
property names and values through the existing `SharedStr` handle cache.
Previously the generic map conversion recreated property-name strings
for each task.
The returned results remain ordinary objects with the same fields and
writable, enumerable, configurable properties. Detail properties are now
emitted in canonical key order, where the previous native map supplied
unspecified iteration order. The measured JavaScript heap results are
mixed, so this does not claim a consistent retained-heap reduction.
### Avoid formatting TUI traces when the TUI cannot run
`packages/nx/src/native/logger/mod.rs` installs the TUI tracing layer
only when stderr is a terminal or `NX_TUI_SKIP_CAPABILITY_CHECK=true`.
This follows the existing task runner's terminal capability requirement,
and console and file logging retain their own filters.
The previous layer formatted and buffered trace events even in a
noninteractive process. Hashing emits a trace for every detail entry, so
that meant millions of formatting operations and temporary allocations.
The buffers are bounded, so the problem is the repeated work rather than
an unbounded log history.
</details>
<details>
<summary>Regression coverage</summary>
`packages/nx/src/native/tests/task-hasher.spec.ts` covers batch and
single-task equivalence, UTF-8 versus UTF-16 ordering, ordinary object
descriptors, repeated and frozen results, serialization, per-task
environments, live JSON changes, input collection, empty selections, and
recovery after missing environments.
The Rust additions check bitset word boundaries, ranked assembly against
an independent map implementation, duplicate-key resolution, Unicode
ordering, empty assembly, and TUI logging filters. Separate V8
scavenge-stress checks with a 1 MiB semi-space matched the prior
baseline, and console and file logging checks passed.
</details>
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
No linked issue. This came from investigating memory use during
concurrent `nx affected` commands in CI.
---------
Co-authored-by: Kyle Cannon <867978+kylecannon@users.noreply.github.com>
Co-authored-by: Craigory Coppola <craigorycoppola@gmail.com> e6a5c010 fix(core): make nx planning and hashing faster (#36994)
Stacked on #36992. The diff here contains the follow-up native planner
and hasher changes.
## Current Behavior
Native planning and hashing repeat work across shared dependencies. The
planner concatenates and sorts every repeated dependency ID before
deduplicating them. The hasher accumulates each instruction into shared
result maps, sorts long keys per task, and converts those maps to
JavaScript by recreating every property name. Noninteractive runs still
format TUI trace events for individual hash entries.
## Expected Behavior
Task hashes and the hash format stay the same. The changes reduce the
work needed to produce them.
- Union dependency IDs in a temporary bitset before allocating the final
lists.
- Gather hash results locally per task, order them using shared integer
key ranks, and convert contiguous details to JavaScript while reusing
string handles.
- Skip TUI trace collection when stderr cannot support a TUI, unless the
existing capability override is enabled.
### Measured impact
These synthetic results were recorded before the five latest follow-up
commits. They are retained as historical evidence, not measurements of
the final head.
Measured on a synthetic fixture built for this change: 600 projects
across 8 layers sharing one closure of 100 packages, giving 60,362
project-to-external edges against 1,553 internal ones. That fan-in is
the shape the planner's deduplication collapses.
All three arms share one TypeScript build from master and differ only in
the release Rust binding, which is possible because these commits change
no shipped TypeScript and no N-API signatures. Medians of seven
interleaved rounds against master
[`29200561dd`](https://github.com/nrwl/nx/commit/29200561dd17a4fed8fdc1ae0cb359be5bf9ddef),
arm order reversed on alternate rounds, one warmup per arm discarded.
| Measure | master | #36992 | this PR | vs master | vs #36992 |
| --------------------- | -------: | -------: | -------: | --------: |
--------: |
| `hashMultipleTasks` | 746 ms | 646 ms | 387 ms | -48.2% | -40.2% |
| `run-many` wall | 2,173 ms | 2,117 ms | 1,808 ms | -16.8% | -14.6% |
| tasks peak RSS | 380 MB | 351 MB | 328 MB | -13.5% | -6.6% |
| tasks RSS footprint | 624 MB | 597 MB | 572 MB | -8.3% | -4.2% |
| cold graph (control) | 3,310 ms | 3,291 ms | 3,279 ms | -0.9% | -0.4%
|
| warm graph (control) | 270 ms | 271 ms | 269 ms | -0.4% | -0.7% |
Graph construction is the control and does not move, which is what a
change confined to the planner and hasher should show.
### Hash compatibility
All 600 task hashes are identical across the three arms. The sorted
task-id to hash listing digests to the same SHA-256 in each, so the
speedup does not come from computing something different.
### Scope and tradeoffs
The fixture is synthetic and the runs are macOS ARM64 on a shared
machine. They cover native planning, hashing, and N-API conversion, and
exclude Cloud and DTE. Linux and Windows validation remain outstanding.
Some small or sparse workloads are slightly slower. The bitset's
temporary size depends on the highest instruction ID, so a check that
primed 20,000 unrelated projects before planning a 16-project chain went
from 0.146 to 0.195 ms with essentially unchanged RSS. A small-call
check measured one cold task at 31.8 to 33.0 ms.
<details>
<summary>Implementation details</summary>
### Union dependency IDs before building the final lists
`packages/nx/src/native/tasks/hash_planner.rs` adds a temporary
`InstructionIdSet` backed by a bitset. Dependency IDs are inserted into
that set as child closures are merged, then emitted into an exactly
sized, sorted vector. The bitset is discarded, and memoized results keep
compact vectors.
This avoids concatenating and sorting all repeated IDs, and avoids
retaining vector capacity sized for the duplicates. For R incoming IDs
and a highest pool ID U, insertion is O(R), followed by scanning O(U/64)
words and emitting the unique IDs. Cycle detection, the legacy fallback,
and final plan ordering remain in place.
### Accumulate hash work locally and publish one result per task
`packages/nx/src/native/tasks/task_hasher.rs` replaces the flattened
instruction iterator and shared per-instruction accumulation with
parallel task processing. Each worker gathers its detail entries and
collected inputs locally, then publishes the completed task result once.
The separate shared input-accumulator map is removed.
Instruction types are classified once per pool entry. Shared-value cache
hits can be copied directly without taking the instruction pool's shard
lock, and the task environment is resolved once per task. Only pending
work enters the inner parallel iterator. Keeping that inner iterator
matters, because a single task with several expensive runtime or file
inputs can still process them concurrently.
Environment and runtime instructions remain outside the shared-value
slots because their values depend on the task environment. When input
collection is enabled, instructions still run the collection path for
each task.
### Rank hash keys once and keep details in contiguous storage
The hasher sorts the pool's detail keys once using the existing UTF-8
ordering, assigns integer ranks, and sorts each task's entries by rank.
Hash assembly feeds the same ordered value bytes into xxh3, with the
existing concatenation and output format.
Different instructions can have the same display key. Those keys share a
rank, and a stable sort with duplicate resolution preserves the existing
last-value-wins rule. The final entries stay in a vector instead of
being inserted into a native hash table and then enumerated and sorted
again.
Assembly now happens in the task worker, which removes the separate
assembly pass. Debug timing includes assembly within hashing, so the old
separate assembly-duration field and the final per-task hash-value trace
are removed.
### Reuse JavaScript handles for detail keys as well as values
`packages/nx/src/native/types/shared_str.rs` adds `SharedStrMap`,
exported through `types.rs` and used by `HashDetails`. It converts the
contiguous entries directly into a JavaScript object and routes both
property names and values through the existing `SharedStr` handle cache.
Previously the generic map conversion recreated property-name strings
for each task.
The returned results remain ordinary objects with the same fields and
writable, enumerable, configurable properties. Detail properties are now
emitted in canonical key order, where the previous native map supplied
unspecified iteration order. The measured JavaScript heap results are
mixed, so this does not claim a consistent retained-heap reduction.
### Avoid formatting TUI traces when the TUI cannot run
`packages/nx/src/native/logger/mod.rs` installs the TUI tracing layer
only when stderr is a terminal or `NX_TUI_SKIP_CAPABILITY_CHECK=true`.
This follows the existing task runner's terminal capability requirement,
and console and file logging retain their own filters.
The previous layer formatted and buffered trace events even in a
noninteractive process. Hashing emits a trace for every detail entry, so
that meant millions of formatting operations and temporary allocations.
The buffers are bounded, so the problem is the repeated work rather than
an unbounded log history.
</details>
<details>
<summary>Regression coverage</summary>
`packages/nx/src/native/tests/task-hasher.spec.ts` covers batch and
single-task equivalence, UTF-8 versus UTF-16 ordering, ordinary object
descriptors, repeated and frozen results, serialization, per-task
environments, live JSON changes, input collection, empty selections, and
recovery after missing environments.
The Rust additions check bitset word boundaries, ranked assembly against
an independent map implementation, duplicate-key resolution, Unicode
ordering, empty assembly, and TUI logging filters. Separate V8
scavenge-stress checks with a 1 MiB semi-space matched the prior
baseline, and console and file logging checks passed.
</details>
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
No linked issue. This came from investigating memory use during
concurrent `nx affected` commands in CI.
---------
Co-authored-by: Kyle Cannon <867978+kylecannon@users.noreply.github.com>
Co-authored-by: Craigory Coppola <craigorycoppola@gmail.com> e6a5c010 fix(core): make nx planning and hashing faster (#36994)
Stacked on #36992. The diff here contains the follow-up native planner
and hasher changes.
## Current Behavior
Native planning and hashing repeat work across shared dependencies. The
planner concatenates and sorts every repeated dependency ID before
deduplicating them. The hasher accumulates each instruction into shared
result maps, sorts long keys per task, and converts those maps to
JavaScript by recreating every property name. Noninteractive runs still
format TUI trace events for individual hash entries.
## Expected Behavior
Task hashes and the hash format stay the same. The changes reduce the
work needed to produce them.
- Union dependency IDs in a temporary bitset before allocating the final
lists.
- Gather hash results locally per task, order them using shared integer
key ranks, and convert contiguous details to JavaScript while reusing
string handles.
- Skip TUI trace collection when stderr cannot support a TUI, unless the
existing capability override is enabled.
### Measured impact
These synthetic results were recorded before the five latest follow-up
commits. They are retained as historical evidence, not measurements of
the final head.
Measured on a synthetic fixture built for this change: 600 projects
across 8 layers sharing one closure of 100 packages, giving 60,362
project-to-external edges against 1,553 internal ones. That fan-in is
the shape the planner's deduplication collapses.
All three arms share one TypeScript build from master and differ only in
the release Rust binding, which is possible because these commits change
no shipped TypeScript and no N-API signatures. Medians of seven
interleaved rounds against master
[`29200561dd`](https://github.com/nrwl/nx/commit/29200561dd17a4fed8fdc1ae0cb359be5bf9ddef),
arm order reversed on alternate rounds, one warmup per arm discarded.
| Measure | master | #36992 | this PR | vs master | vs #36992 |
| --------------------- | -------: | -------: | -------: | --------: |
--------: |
| `hashMultipleTasks` | 746 ms | 646 ms | 387 ms | -48.2% | -40.2% |
| `run-many` wall | 2,173 ms | 2,117 ms | 1,808 ms | -16.8% | -14.6% |
| tasks peak RSS | 380 MB | 351 MB | 328 MB | -13.5% | -6.6% |
| tasks RSS footprint | 624 MB | 597 MB | 572 MB | -8.3% | -4.2% |
| cold graph (control) | 3,310 ms | 3,291 ms | 3,279 ms | -0.9% | -0.4%
|
| warm graph (control) | 270 ms | 271 ms | 269 ms | -0.4% | -0.7% |
Graph construction is the control and does not move, which is what a
change confined to the planner and hasher should show.
### Hash compatibility
All 600 task hashes are identical across the three arms. The sorted
task-id to hash listing digests to the same SHA-256 in each, so the
speedup does not come from computing something different.
### Scope and tradeoffs
The fixture is synthetic and the runs are macOS ARM64 on a shared
machine. They cover native planning, hashing, and N-API conversion, and
exclude Cloud and DTE. Linux and Windows validation remain outstanding.
Some small or sparse workloads are slightly slower. The bitset's
temporary size depends on the highest instruction ID, so a check that
primed 20,000 unrelated projects before planning a 16-project chain went
from 0.146 to 0.195 ms with essentially unchanged RSS. A small-call
check measured one cold task at 31.8 to 33.0 ms.
<details>
<summary>Implementation details</summary>
### Union dependency IDs before building the final lists
`packages/nx/src/native/tasks/hash_planner.rs` adds a temporary
`InstructionIdSet` backed by a bitset. Dependency IDs are inserted into
that set as child closures are merged, then emitted into an exactly
sized, sorted vector. The bitset is discarded, and memoized results keep
compact vectors.
This avoids concatenating and sorting all repeated IDs, and avoids
retaining vector capacity sized for the duplicates. For R incoming IDs
and a highest pool ID U, insertion is O(R), followed by scanning O(U/64)
words and emitting the unique IDs. Cycle detection, the legacy fallback,
and final plan ordering remain in place.
### Accumulate hash work locally and publish one result per task
`packages/nx/src/native/tasks/task_hasher.rs` replaces the flattened
instruction iterator and shared per-instruction accumulation with
parallel task processing. Each worker gathers its detail entries and
collected inputs locally, then publishes the completed task result once.
The separate shared input-accumulator map is removed.
Instruction types are classified once per pool entry. Shared-value cache
hits can be copied directly without taking the instruction pool's shard
lock, and the task environment is resolved once per task. Only pending
work enters the inner parallel iterator. Keeping that inner iterator
matters, because a single task with several expensive runtime or file
inputs can still process them concurrently.
Environment and runtime instructions remain outside the shared-value
slots because their values depend on the task environment. When input
collection is enabled, instructions still run the collection path for
each task.
### Rank hash keys once and keep details in contiguous storage
The hasher sorts the pool's detail keys once using the existing UTF-8
ordering, assigns integer ranks, and sorts each task's entries by rank.
Hash assembly feeds the same ordered value bytes into xxh3, with the
existing concatenation and output format.
Different instructions can have the same display key. Those keys share a
rank, and a stable sort with duplicate resolution preserves the existing
last-value-wins rule. The final entries stay in a vector instead of
being inserted into a native hash table and then enumerated and sorted
again.
Assembly now happens in the task worker, which removes the separate
assembly pass. Debug timing includes assembly within hashing, so the old
separate assembly-duration field and the final per-task hash-value trace
are removed.
### Reuse JavaScript handles for detail keys as well as values
`packages/nx/src/native/types/shared_str.rs` adds `SharedStrMap`,
exported through `types.rs` and used by `HashDetails`. It converts the
contiguous entries directly into a JavaScript object and routes both
property names and values through the existing `SharedStr` handle cache.
Previously the generic map conversion recreated property-name strings
for each task.
The returned results remain ordinary objects with the same fields and
writable, enumerable, configurable properties. Detail properties are now
emitted in canonical key order, where the previous native map supplied
unspecified iteration order. The measured JavaScript heap results are
mixed, so this does not claim a consistent retained-heap reduction.
### Avoid formatting TUI traces when the TUI cannot run
`packages/nx/src/native/logger/mod.rs` installs the TUI tracing layer
only when stderr is a terminal or `NX_TUI_SKIP_CAPABILITY_CHECK=true`.
This follows the existing task runner's terminal capability requirement,
and console and file logging retain their own filters.
The previous layer formatted and buffered trace events even in a
noninteractive process. Hashing emits a trace for every detail entry, so
that meant millions of formatting operations and temporary allocations.
The buffers are bounded, so the problem is the repeated work rather than
an unbounded log history.
</details>
<details>
<summary>Regression coverage</summary>
`packages/nx/src/native/tests/task-hasher.spec.ts` covers batch and
single-task equivalence, UTF-8 versus UTF-16 ordering, ordinary object
descriptors, repeated and frozen results, serialization, per-task
environments, live JSON changes, input collection, empty selections, and
recovery after missing environments.
The Rust additions check bitset word boundaries, ranked assembly against
an independent map implementation, duplicate-key resolution, Unicode
ordering, empty assembly, and TUI logging filters. Separate V8
scavenge-stress checks with a 1 MiB semi-space matched the prior
baseline, and console and file logging checks passed.
</details>
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
No linked issue. This came from investigating memory use during
concurrent `nx affected` commands in CI.
---------
Co-authored-by: Kyle Cannon <867978+kylecannon@users.noreply.github.com>
Co-authored-by: Craigory Coppola <craigorycoppola@gmail.com> e6a5c010 fix(core): make nx planning and hashing faster (#36994)
Stacked on #36992. The diff here contains the follow-up native planner
and hasher changes.
## Current Behavior
Native planning and hashing repeat work across shared dependencies. The
planner concatenates and sorts every repeated dependency ID before
deduplicating them. The hasher accumulates each instruction into shared
result maps, sorts long keys per task, and converts those maps to
JavaScript by recreating every property name. Noninteractive runs still
format TUI trace events for individual hash entries.
## Expected Behavior
Task hashes and the hash format stay the same. The changes reduce the
work needed to produce them.
- Union dependency IDs in a temporary bitset before allocating the final
lists.
- Gather hash results locally per task, order them using shared integer
key ranks, and convert contiguous details to JavaScript while reusing
string handles.
- Skip TUI trace collection when stderr cannot support a TUI, unless the
existing capability override is enabled.
### Measured impact
These synthetic results were recorded before the five latest follow-up
commits. They are retained as historical evidence, not measurements of
the final head.
Measured on a synthetic fixture built for this change: 600 projects
across 8 layers sharing one closure of 100 packages, giving 60,362
project-to-external edges against 1,553 internal ones. That fan-in is
the shape the planner's deduplication collapses.
All three arms share one TypeScript build from master and differ only in
the release Rust binding, which is possible because these commits change
no shipped TypeScript and no N-API signatures. Medians of seven
interleaved rounds against master
[`29200561dd`](https://github.com/nrwl/nx/commit/29200561dd17a4fed8fdc1ae0cb359be5bf9ddef),
arm order reversed on alternate rounds, one warmup per arm discarded.
| Measure | master | #36992 | this PR | vs master | vs #36992 |
| --------------------- | -------: | -------: | -------: | --------: |
--------: |
| `hashMultipleTasks` | 746 ms | 646 ms | 387 ms | -48.2% | -40.2% |
| `run-many` wall | 2,173 ms | 2,117 ms | 1,808 ms | -16.8% | -14.6% |
| tasks peak RSS | 380 MB | 351 MB | 328 MB | -13.5% | -6.6% |
| tasks RSS footprint | 624 MB | 597 MB | 572 MB | -8.3% | -4.2% |
| cold graph (control) | 3,310 ms | 3,291 ms | 3,279 ms | -0.9% | -0.4%
|
| warm graph (control) | 270 ms | 271 ms | 269 ms | -0.4% | -0.7% |
Graph construction is the control and does not move, which is what a
change confined to the planner and hasher should show.
### Hash compatibility
All 600 task hashes are identical across the three arms. The sorted
task-id to hash listing digests to the same SHA-256 in each, so the
speedup does not come from computing something different.
### Scope and tradeoffs
The fixture is synthetic and the runs are macOS ARM64 on a shared
machine. They cover native planning, hashing, and N-API conversion, and
exclude Cloud and DTE. Linux and Windows validation remain outstanding.
Some small or sparse workloads are slightly slower. The bitset's
temporary size depends on the highest instruction ID, so a check that
primed 20,000 unrelated projects before planning a 16-project chain went
from 0.146 to 0.195 ms with essentially unchanged RSS. A small-call
check measured one cold task at 31.8 to 33.0 ms.
<details>
<summary>Implementation details</summary>
### Union dependency IDs before building the final lists
`packages/nx/src/native/tasks/hash_planner.rs` adds a temporary
`InstructionIdSet` backed by a bitset. Dependency IDs are inserted into
that set as child closures are merged, then emitted into an exactly
sized, sorted vector. The bitset is discarded, and memoized results keep
compact vectors.
This avoids concatenating and sorting all repeated IDs, and avoids
retaining vector capacity sized for the duplicates. For R incoming IDs
and a highest pool ID U, insertion is O(R), followed by scanning O(U/64)
words and emitting the unique IDs. Cycle detection, the legacy fallback,
and final plan ordering remain in place.
### Accumulate hash work locally and publish one result per task
`packages/nx/src/native/tasks/task_hasher.rs` replaces the flattened
instruction iterator and shared per-instruction accumulation with
parallel task processing. Each worker gathers its detail entries and
collected inputs locally, then publishes the completed task result once.
The separate shared input-accumulator map is removed.
Instruction types are classified once per pool entry. Shared-value cache
hits can be copied directly without taking the instruction pool's shard
lock, and the task environment is resolved once per task. Only pending
work enters the inner parallel iterator. Keeping that inner iterator
matters, because a single task with several expensive runtime or file
inputs can still process them concurrently.
Environment and runtime instructions remain outside the shared-value
slots because their values depend on the task environment. When input
collection is enabled, instructions still run the collection path for
each task.
### Rank hash keys once and keep details in contiguous storage
The hasher sorts the pool's detail keys once using the existing UTF-8
ordering, assigns integer ranks, and sorts each task's entries by rank.
Hash assembly feeds the same ordered value bytes into xxh3, with the
existing concatenation and output format.
Different instructions can have the same display key. Those keys share a
rank, and a stable sort with duplicate resolution preserves the existing
last-value-wins rule. The final entries stay in a vector instead of
being inserted into a native hash table and then enumerated and sorted
again.
Assembly now happens in the task worker, which removes the separate
assembly pass. Debug timing includes assembly within hashing, so the old
separate assembly-duration field and the final per-task hash-value trace
are removed.
### Reuse JavaScript handles for detail keys as well as values
`packages/nx/src/native/types/shared_str.rs` adds `SharedStrMap`,
exported through `types.rs` and used by `HashDetails`. It converts the
contiguous entries directly into a JavaScript object and routes both
property names and values through the existing `SharedStr` handle cache.
Previously the generic map conversion recreated property-name strings
for each task.
The returned results remain ordinary objects with the same fields and
writable, enumerable, configurable properties. Detail properties are now
emitted in canonical key order, where the previous native map supplied
unspecified iteration order. The measured JavaScript heap results are
mixed, so this does not claim a consistent retained-heap reduction.
### Avoid formatting TUI traces when the TUI cannot run
`packages/nx/src/native/logger/mod.rs` installs the TUI tracing layer
only when stderr is a terminal or `NX_TUI_SKIP_CAPABILITY_CHECK=true`.
This follows the existing task runner's terminal capability requirement,
and console and file logging retain their own filters.
The previous layer formatted and buffered trace events even in a
noninteractive process. Hashing emits a trace for every detail entry, so
that meant millions of formatting operations and temporary allocations.
The buffers are bounded, so the problem is the repeated work rather than
an unbounded log history.
</details>
<details>
<summary>Regression coverage</summary>
`packages/nx/src/native/tests/task-hasher.spec.ts` covers batch and
single-task equivalence, UTF-8 versus UTF-16 ordering, ordinary object
descriptors, repeated and frozen results, serialization, per-task
environments, live JSON changes, input collection, empty selections, and
recovery after missing environments.
The Rust additions check bitset word boundaries, ranked assembly against
an independent map implementation, duplicate-key resolution, Unicode
ordering, empty assembly, and TUI logging filters. Separate V8
scavenge-stress checks with a 1 MiB semi-space matched the prior
baseline, and console and file logging checks passed.
</details>
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
No linked issue. This came from investigating memory use during
concurrent `nx affected` commands in CI.
---------
Co-authored-by: Kyle Cannon <867978+kylecannon@users.noreply.github.com>
Co-authored-by: Craigory Coppola <craigorycoppola@gmail.com> e6a5c010 fix(core): make nx planning and hashing faster (#36994)
Stacked on #36992. The diff here contains the follow-up native planner
and hasher changes.
## Current Behavior
Native planning and hashing repeat work across shared dependencies. The
planner concatenates and sorts every repeated dependency ID before
deduplicating them. The hasher accumulates each instruction into shared
result maps, sorts long keys per task, and converts those maps to
JavaScript by recreating every property name. Noninteractive runs still
format TUI trace events for individual hash entries.
## Expected Behavior
Task hashes and the hash format stay the same. The changes reduce the
work needed to produce them.
- Union dependency IDs in a temporary bitset before allocating the final
lists.
- Gather hash results locally per task, order them using shared integer
key ranks, and convert contiguous details to JavaScript while reusing
string handles.
- Skip TUI trace collection when stderr cannot support a TUI, unless the
existing capability override is enabled.
### Measured impact
These synthetic results were recorded before the five latest follow-up
commits. They are retained as historical evidence, not measurements of
the final head.
Measured on a synthetic fixture built for this change: 600 projects
across 8 layers sharing one closure of 100 packages, giving 60,362
project-to-external edges against 1,553 internal ones. That fan-in is
the shape the planner's deduplication collapses.
All three arms share one TypeScript build from master and differ only in
the release Rust binding, which is possible because these commits change
no shipped TypeScript and no N-API signatures. Medians of seven
interleaved rounds against master
[`29200561dd`](https://github.com/nrwl/nx/commit/29200561dd17a4fed8fdc1ae0cb359be5bf9ddef),
arm order reversed on alternate rounds, one warmup per arm discarded.
| Measure | master | #36992 | this PR | vs master | vs #36992 |
| --------------------- | -------: | -------: | -------: | --------: |
--------: |
| `hashMultipleTasks` | 746 ms | 646 ms | 387 ms | -48.2% | -40.2% |
| `run-many` wall | 2,173 ms | 2,117 ms | 1,808 ms | -16.8% | -14.6% |
| tasks peak RSS | 380 MB | 351 MB | 328 MB | -13.5% | -6.6% |
| tasks RSS footprint | 624 MB | 597 MB | 572 MB | -8.3% | -4.2% |
| cold graph (control) | 3,310 ms | 3,291 ms | 3,279 ms | -0.9% | -0.4%
|
| warm graph (control) | 270 ms | 271 ms | 269 ms | -0.4% | -0.7% |
Graph construction is the control and does not move, which is what a
change confined to the planner and hasher should show.
### Hash compatibility
All 600 task hashes are identical across the three arms. The sorted
task-id to hash listing digests to the same SHA-256 in each, so the
speedup does not come from computing something different.
### Scope and tradeoffs
The fixture is synthetic and the runs are macOS ARM64 on a shared
machine. They cover native planning, hashing, and N-API conversion, and
exclude Cloud and DTE. Linux and Windows validation remain outstanding.
Some small or sparse workloads are slightly slower. The bitset's
temporary size depends on the highest instruction ID, so a check that
primed 20,000 unrelated projects before planning a 16-project chain went
from 0.146 to 0.195 ms with essentially unchanged RSS. A small-call
check measured one cold task at 31.8 to 33.0 ms.
<details>
<summary>Implementation details</summary>
### Union dependency IDs before building the final lists
`packages/nx/src/native/tasks/hash_planner.rs` adds a temporary
`InstructionIdSet` backed by a bitset. Dependency IDs are inserted into
that set as child closures are merged, then emitted into an exactly
sized, sorted vector. The bitset is discarded, and memoized results keep
compact vectors.
This avoids concatenating and sorting all repeated IDs, and avoids
retaining vector capacity sized for the duplicates. For R incoming IDs
and a highest pool ID U, insertion is O(R), followed by scanning O(U/64)
words and emitting the unique IDs. Cycle detection, the legacy fallback,
and final plan ordering remain in place.
### Accumulate hash work locally and publish one result per task
`packages/nx/src/native/tasks/task_hasher.rs` replaces the flattened
instruction iterator and shared per-instruction accumulation with
parallel task processing. Each worker gathers its detail entries and
collected inputs locally, then publishes the completed task result once.
The separate shared input-accumulator map is removed.
Instruction types are classified once per pool entry. Shared-value cache
hits can be copied directly without taking the instruction pool's shard
lock, and the task environment is resolved once per task. Only pending
work enters the inner parallel iterator. Keeping that inner iterator
matters, because a single task with several expensive runtime or file
inputs can still process them concurrently.
Environment and runtime instructions remain outside the shared-value
slots because their values depend on the task environment. When input
collection is enabled, instructions still run the collection path for
each task.
### Rank hash keys once and keep details in contiguous storage
The hasher sorts the pool's detail keys once using the existing UTF-8
ordering, assigns integer ranks, and sorts each task's entries by rank.
Hash assembly feeds the same ordered value bytes into xxh3, with the
existing concatenation and output format.
Different instructions can have the same display key. Those keys share a
rank, and a stable sort with duplicate resolution preserves the existing
last-value-wins rule. The final entries stay in a vector instead of
being inserted into a native hash table and then enumerated and sorted
again.
Assembly now happens in the task worker, which removes the separate
assembly pass. Debug timing includes assembly within hashing, so the old
separate assembly-duration field and the final per-task hash-value trace
are removed.
### Reuse JavaScript handles for detail keys as well as values
`packages/nx/src/native/types/shared_str.rs` adds `SharedStrMap`,
exported through `types.rs` and used by `HashDetails`. It converts the
contiguous entries directly into a JavaScript object and routes both
property names and values through the existing `SharedStr` handle cache.
Previously the generic map conversion recreated property-name strings
for each task.
The returned results remain ordinary objects with the same fields and
writable, enumerable, configurable properties. Detail properties are now
emitted in canonical key order, where the previous native map supplied
unspecified iteration order. The measured JavaScript heap results are
mixed, so this does not claim a consistent retained-heap reduction.
### Avoid formatting TUI traces when the TUI cannot run
`packages/nx/src/native/logger/mod.rs` installs the TUI tracing layer
only when stderr is a terminal or `NX_TUI_SKIP_CAPABILITY_CHECK=true`.
This follows the existing task runner's terminal capability requirement,
and console and file logging retain their own filters.
The previous layer formatted and buffered trace events even in a
noninteractive process. Hashing emits a trace for every detail entry, so
that meant millions of formatting operations and temporary allocations.
The buffers are bounded, so the problem is the repeated work rather than
an unbounded log history.
</details>
<details>
<summary>Regression coverage</summary>
`packages/nx/src/native/tests/task-hasher.spec.ts` covers batch and
single-task equivalence, UTF-8 versus UTF-16 ordering, ordinary object
descriptors, repeated and frozen results, serialization, per-task
environments, live JSON changes, input collection, empty selections, and
recovery after missing environments.
The Rust additions check bitset word boundaries, ranked assembly against
an independent map implementation, duplicate-key resolution, Unicode
ordering, empty assembly, and TUI logging filters. Separate V8
scavenge-stress checks with a 1 MiB semi-space matched the prior
baseline, and console and file logging checks passed.
</details>
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
No linked issue. This came from investigating memory use during
concurrent `nx affected` commands in CI.
---------
Co-authored-by: Kyle Cannon <867978+kylecannon@users.noreply.github.com>
Co-authored-by: Craigory Coppola <craigorycoppola@gmail.com>