fix(core): backport the planning, hashing and worker task graph changes to 22.7.x - #37083
AgentEnder wants to merge 4 commits into
Conversation
…36992) <!-- Please make sure you have read the submission guidelines before posting an PR --> <!-- https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr --> <!-- Please make sure that your commit message follows our format --> <!-- Example: `fix(nx): must begin with lowercase` --> The hash planner turns each external dependency name into an owned `External` instruction and interns it as it walks. Projects that share an npm closure hit the same names repeatedly, so the planner allocates and interns duplicates, and the id vector retains capacity for them until the later sort and dedup. Borrowed names are collected in a set first, so each unique external is interned once. Plans and hashes are unchanged. Measured on a synthetic fixture of 600 projects sharing one closure of 100 packages, medians of seven interleaved rounds against current master: | Measure | master | this PR | | ------------------- | -----: | ------: | | `hashMultipleTasks` | 746 ms | 646 ms | | tasks peak RSS | 380 MB | 351 MB | Graph construction does not move, and all 600 task hashes are identical to master. <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes # Co-authored-by: Kyle Cannon <867978+kylecannon@users.noreply.github.com>
|
View your CI Pipeline Execution ↗ for commit ee8176e
☁️ Nx Cloud last updated this comment at |
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`](2920056), 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>
Every fork received the whole task graph, re-serialized by the runner each time, with per-task hash details that only Nx Cloud reads on the runner side and the terminal output the Nx Cloud life cycle writes back onto finished tasks. Both are most of every task's size and grow the payload as the run goes on. Each fork now gets a copy without any run result, made once per graph object by pruneTaskGraph, over the same JSON channel as before. Structure never changes mid-run, so the copy never goes stale. On a flat 600-task fixture at parallel 8 the tasks phase drops from 45.9 s to 29.0 s, runner peak RSS from 568 MiB to 276 MiB, and per-worker peak RSS from 182 MiB to 112 MiB; on a dependency-chain fixture the same run drops from 142.7 s to 126.6 s with the same memory savings. Hashing is unchanged. Backport note: on master the daemon client strips the same fields before a hashing request and shares pruneTaskGraph; that stripping arrived with #37017, which is not on 22.7.x, so here pruneTaskGraph is wired into the worker send sites only.
3a08a2c to
2a531db
Compare
There was a problem hiding this comment.
Important
At least one additional CI pipeline execution has run since the conclusion below was written and it may no longer be applicable.
Nx Cloud is proposing a fix for your failed CI:
We extended the webpack version cap (>=5.101.3 <5.111.0) to @nx/module-federation, matching the fix already applied to @nx/webpack in this PR. Without this cap, pnpm resolved @nx/module-federation's uncapped ^5.101.3 webpack dependency to 5.111.0 in e2e workspaces, causing @module-federation/enhanced to peer against webpack@5.111.0 while the build executor ran under webpack@5.110.3 — producing the [contenthash:20] not implemented in this context crash.
Warning
❌ We could not verify this fix.
diff --git a/packages/module-federation/package.json b/packages/module-federation/package.json
index a244c324..d33dde1a 100644
--- a/packages/module-federation/package.json
+++ b/packages/module-federation/package.json
@@ -35,7 +35,7 @@
"http-proxy-middleware": "^3.0.5",
"picocolors": "catalog:",
"tslib": "catalog:typescript",
- "webpack": "^5.101.3"
+ "webpack": ">=5.101.3 <5.111.0"
},
"devDependencies": {
"nx": "workspace:*"
🔔 Heads up, your workspace has pending recommendations ↗ to auto-apply fixes for similar failures.
Or Apply changes locally with:
npx nx-cloud apply-locally HaR9-1jTv
Apply fix locally with your editor ↗ View interactive diff ↗
🎓 Learn more about Self-Healing CI on nx.dev
…ion builds webpack 5.111.0 fails federation builds with "Path variable [contenthash:20] not implemented in this context" (webpack/webpack#22034). Master pins the version the webpack init generator writes into a workspace (#37034); this branch's generator did not add webpack at all, so a new workspace took whatever @nx/webpack, @nx/module-federation or @nx/rspack resolved. The generator now adds `webpack: ~5.110.0`, and the three packages that depend on webpack cap it below 5.111.0 for workspaces that never ran init. The lockfile already resolved a version inside the cap, so only its specifier lines change.
9fa5050 to
ee8176e
Compare
Current Behavior
22.7.x hashes tasks and sends the task graph to workers the way 23.2 did before #36992, #36994 and #37015.
Expected Behavior
Backport of the three merged planning, hashing and worker-transport changes, cherry-picked in order:
What differs from master:
hash_planner.rson this branch keeps the reference-keyed external dependency map from the earlier memory backport, so the twoinsertcalls index it by reference. The tests brought over needed theTask::newandwith_outputshelpers, added totypes.rs.packages/nx; the three new snapshot values are byte-identical to master's.pruneTaskGraphis wired only into the five worker send sites.Object, so the four hasher tests that assert on a result's prototype resolve the host realm'sObject.prototypefirst.@nx/webpack's webpack dependency below 5.111.0, which breaks module federation builds. Master has the same cap in the init generator from fix(webpack): cap the generated webpack version below 5.111.0 #37034; that generator does not install webpack on this branch, so the cap lives on the package dependency instead. Any new module federation workspace on 22.7.x hits this today, and the e2e suites that cover it only ran here because a change underpackages/nxmarks everything affected.Verified locally:
cargo check --all-targetsandcargo test -p nx(399 passing) on Rust 1.94.0, and the planner, hasher, daemon client, pruning and worker-transport specs under Jest.Related Issue(s)
Backport of #36992, #36994 and #37015 to 22.7.x, with the webpack cap from #37034 carried as a package dependency.