The cutover that closes the three-PR demand-resolver stack from
#3028 (the original perf spec) and the integrated-form #3084 (the
all-in-one landing at commit 4833992 that this split layers).
Flips the public entry-point chain in
`crates/ruborist/src/resolver/builder.rs` and the host-side wiring
in `crates/ruborist/src/service/api.rs` from the legacy
`RegistryClient`-bound two-phase preload-then-BFS resolver to the
demand-driven BFS loop landed by PR-B (#3086) on the
`ManifestProvider` trait landed by PR-A (#3085). Bundles the two
runtime tunings whose payoff is the demand driver's single-flight
de-duplication of concurrent same-package fetches — the
four-reqwest-client round-robin pool in `service/http.rs` that
fans HTTP keep-alive connections across the Cloudflare edge IPs
the npmjs.org CNAME chain resolves to, and the resolver-side
cap of 256 for non-semver npmjs from a new `get_resolver_manifests_concurrency_limit`
function in `pm/util/user_config.rs` wired into the
`BuildDepsConfig.concurrency` field via the matching swap in
`pm/helper/ruborist_context.rs::Context::new`. Lands the
dead-code-allowance removals on the cross-PR scaffolding —
PR-A's `service/cache.rs::impl ProjectCacheData::{resolved_manifests,
from_resolved}` block-level `#[allow(dead_code)]` with its
"Bridges between the on-disk project cache and the demand
resolver's neutral (name, spec, manifest) tuples. Wired up in
the resolver cutover PR." comment, and PR-B's
`resolver/demand/state.rs::ResolverManifestCache.entries`
field-level `#[allow(dead_code)]` with its matching staging
comment — because the wiring this PR introduces is the
reader-chain those staged allowances pointed at. Lands the
orphan annotations on the three now-unreachable preload-era
functions — `#[allow(dead_code)]` plus the one-line "Retired
preload path: orphaned now that resolution runs through the
demand driver. Kept compiling until the cleanup PR removes the
preload module." comment above each of `gather_preload_deps`,
`run_preload_phase`, and `run_bfs_phase` in `builder.rs` — the
three functions whose only caller was the legacy
`build_deps_with_config` body that this PR rewrites. And lands
the matching `#[ignore]`-removal on the demand-driver's
`test_non_semver_exact_version_extract_single_flight` test in
`crates/ruborist/src/resolver/demand/driver.rs`: PR-B parked
it with `#[ignore = "exercises the demand-driver pipeline, wired
in the cutover PR"]` and a 7-line comment block explaining the
counter-stays-at-zero failure mode of the assertion under the
legacy `RegistryClient`-bound `resolve` entry; once the cutover
flips `resolve` and `resolve_with_options` to `where R:
ManifestProvider, R::Error: Send` and `build_deps_with_config`'s
body delegates to PR-B's `build_deps_with_config_output` which
in turn calls `run_main_loop_bfs`, the `CountingRegistry`
wrapper around the inner `MockRegistryClient` does see its
`ManifestProvider::request_manifests` invocation counter
increment exactly once for the `shared@1.2.3` exact-version
specifier the two sibling root-deps `a` and `b` both name (the
demand driver's `FetchQueues` scheduler is the single-flight
de-duplicator), and the assertion `assert_eq!(shared_version_jobs.load(
Ordering::Relaxed), 1)` lands at `left: 1, right: 1` and the
test passes — taking the lib-test count from PR-B's
181-passing-and-1-ignored to the cutover's 182-passing-and-0-ignored.
The eight-file commit body, in the order the changes
chain through the call graph from the host's
`api::build_lockfile` entry down to the registry adapter:
* `crates/ruborist/src/service/api.rs` — the host-side cutover.
The `BuildDepsConfig` builder chain in `build_lockfile` picks
up a new `.with_project_cache(project_cache)` link that
forwards the host's warm-cache snapshot (loaded by the
separately-already-existing `pm::util::project_cache` reader
from the on-disk `~/.utoo/cache/projects/<hash>.json` sidecar
the `utoo install` writes after a successful resolve) into
the resolver, so the demand pipeline's
`ManifestState::seeded(project_cache.unwrap_or_default().resolved_manifests())`
pre-seeding step skips the manifest-fetch round-trip for the
dependencies whose specs the lockfile already names a
resolved-version-and-manifest for — the same warm-cache
"skip the network for unchanged deps" shortcut the legacy
preload phase used to take through the existing
`RegistryClient::cache_version_manifest` shim, but driven
through the demand pipeline's neutral `(name, spec, manifest)`
tuple format that `ProjectCacheData::resolved_manifests`
(PR-A's bridge, the inverse of `from_resolved`, now live)
produces from the on-disk per-package-spec-to-version map.
The host's post-resolve manifest-cache export — which the
legacy code did by iterating
`registry.cache().export_version_manifests()` and parsing
each "@scope/name@spec" key back into the `(name, spec,
version)` triple via the now-unused
`crate::model::util::parse_package_spec` import for the
sidecar-writeback — becomes the one-line `let project_cache
= ProjectCacheData::from_resolved(build_deps_with_config_output(
graph, registry, config, receiver).await?.entries);` (PR-A's
matching `from_resolved` bridge, now live). The
`parse_package_spec` import drops with the legacy loop. The
`build_deps_with_config` call that the legacy code made
(unit-returning) becomes `build_deps_with_config_output`
(the `Result<ResolverManifestCache, _>`-returning variant
PR-B added) since the host needs the resolved-manifest
stream for the sidecar write.
* `crates/ruborist/src/resolver/builder.rs` — the in-resolver
cutover. The `pub struct BuildDepsConfig` definition picks
up the `pub project_cache: Option<ProjectCacheData>` field
with the doc-comment "Host-provided project cache used to
seed the resolver-owned manifest cache. Consumed by the
demand mainloop; the preload path ignores it." (the
preload-path-ignores-it wording is a forward-reference to
the cleanup PR that deletes the preload path entirely); the
matching `Default::default()` initializes the field to
`None`; the `impl BuildDepsConfig` block gains the
builder-method
`pub fn with_project_cache(mut self, project_cache:
Option<ProjectCacheData>) -> Self { self.project_cache =
project_cache; self }` for the api.rs call-site to chain.
The five public entry-point functions (`pub async fn
build_deps`, `pub async fn build_deps_with_receiver`, `pub
async fn build_deps_with_config`, `pub async fn resolve`,
`pub async fn resolve_with_options`) have their type-parameter
bound flipped from the angle-bracket form `<R:
RegistryClient>` to the bare `<R>` with the trailing where-clause
`where R: ManifestProvider, R::Error: Send` (the `Send` clause
is needed because the demand driver's `tokio::spawn`-shaped
job futures are bounded as `Send` on native targets, and the
trait's adapter implementor `UnifiedRegistry` from PR-A
satisfies it because all its inner state — the `Arc<Store>`,
the `String` registry_url, the `bool` supports_semver flag —
is `Send + Sync`). The body of `build_deps_with_config` is
rewritten from the legacy three-line "log
'Starting dependency tree build', call
`run_preload_phase(graph, registry, &config, receiver)`,
call `run_bfs_phase(graph, registry, &config,
receiver)?`, return `Ok(())`" sequence to "log
'Starting demand dependency build' with the new
`peer_deps` and `concurrency` format args (the
`skip_preload` field is dropped from the format string
because the demand path has no preload phase to skip),
call `build_deps_with_config_output(graph, registry,
config, receiver).await?`, return `Ok(())` (the
`_output` variant's `ResolverManifestCache` return is
discarded by the unit-returning `build_deps_with_config`'s
contract; the host that wants the cache calls the
`_output` variant directly per the api.rs hunk above)."
The three preload-era helpers
(`fn gather_preload_deps(graph, peer_deps) ->
Vec<(String, String)>` at line 194 of bA, `async fn
run_preload_phase<R: RegistryClient, E: EventReceiver>(...)`
at line 760 of bA, `async fn run_bfs_phase<R:
RegistryClient, E: EventReceiver>(...)` at line 812 of
bA — line numbers refer to the perf/pm-resolver-select
tip which both PR-A and PR-B's `builder.rs` retain) get
the orphan annotation: a two-line `// Retired preload
path: orphaned now that resolution runs through the
demand driver. Kept compiling until the cleanup PR
removes the preload module.` comment block above each
function's doc-comment-or-signature, with the
`#[allow(dead_code)]` attribute as the next line — the
pattern matches the staged-dead-code annotations on the
cross-PR scaffolding that come off in this same commit,
reading like a small choreography where one set of
allowances retires while the other set of allowances is
redeemed by the wiring landing. The
`#[allow(dead_code)]` that PR-B placed on the
`pub(crate) async fn build_deps_with_config_output`
also comes off because the rewritten body of
`build_deps_with_config` (and the api.rs cutover) are
now both in-crate callers of it. The three new `use`
statements at the top of the file —
`use crate::model::manifest::{CoreVersionManifest,
NodeManifest};` joining `CoreVersionManifest` into the
existing brace-list, `use crate::resolver::demand::{
ResolverManifestCache, run_main_loop_bfs};` as a new
line, `use crate::service::{ManifestProvider,
ProjectCacheData};` joining `ManifestProvider` into the
existing brace-list — are the same three import-line
tweaks PR-B added at the top of the file when the
driver's helpers were introduced; they remain unchanged
in this commit (no further import additions on top of
PR-B's).
* `crates/ruborist/src/service/http.rs` — the connection
pool. The single `static CLIENT: LazyLock<reqwest::Client>
= LazyLock::new(|| { ClientBuilder::new()...build() });`
becomes the array form `const CLIENT_POOL_SIZE: usize =
4; static CLIENTS: LazyLock<[reqwest::Client;
CLIENT_POOL_SIZE]> = LazyLock::new(|| {
std::array::from_fn(|_| ClientBuilder::new()...build()
.unwrap()) });` with a `static NEXT_CLIENT_IDX:
std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);` round-robin
counter. The public function `pub fn get_client() ->
&'static reqwest::Client` keeps its signature
unchanged so all existing callers throughout the crate
graph (the tarball-fetch path in pm's installer, the
registry-discovery fetch in the workspace bootstrap,
the resolver-side fetch in the manifest adapter) get
the new pooled behavior transparently — the body
becomes `&CLIENTS[NEXT_CLIENT_IDX.fetch_add(1,
std::sync::atomic::Ordering::Relaxed) %
CLIENT_POOL_SIZE]`. The four-way fan-out lets the
demand driver's `FuturesUnordered`-shaped batched
manifest fetches each land on a different
reqwest::Client which DNS-resolves
`registry.npmjs.org` to a different Cloudflare edge IP
(the edge-IP rotation that the public-npmjs front-end
does for any client opening a fresh TLS handshake),
bypassing the per-edge-IP rate-limiter that the
single-client version's keep-alive pool concentrated
all the traffic at and that was the
PR-A-pre-amend-bench's documented +14 % wall regression
vs the next-baseline on the resolve phase. The pool's
inter-handshake-state independence (each member has
its own connection-pool, its own DNS cache, its own
HTTP/2-or-1 multiplex state, its own auth credentials
chain) makes the fan-out transparent to the resolver's
job-fan-out — the resolver issues N jobs through
`request_manifests`, the adapter's body in
`service/registry/provider.rs` calls `get_client()`
for each job to get the next pool member, the four
pool members independently maintain their
connection-pool state. The aggregate throughput
ceiling is N×(per-member-connection-cap) rather than
1×, which is the headroom the demand driver's higher
in-flight cap (256 vs the legacy 64) uses to amortize
its single-flight savings into wall-clock improvement.
* `crates/pm/src/util/user_config.rs` — the resolver-side
concurrency knob. A new private helper
`fn resolver_manifest_concurrency_limit(default_cap:
usize, is_npm_default_registry: bool,
semver_mode_override: Option<bool>) -> usize` whose
branch table reads: "if the registry is the public
npmjs.org default and the semver-mode is off
(the abbreviated-metadata "versions-only" code path
where the demand driver fetches the versions list for
a package once and then the per-version manifests on
demand, deduping the same-version request across
multiple parent edges through `FetchQueues`'s
single-flight), return 256; if the user has an
explicit cap in `~/.utoorc.json::resolver.manifest_concurrency_limit`,
return that user-specified value preserving the
intent (overriding both the default-256-for-npmjs and
the default-default for other registries); if the
semver-mode is on (the spec's resolver-discriminator
the registry's full-manifest range-spec resolution
takes, which is a per-package round-trip the demand
driver's `select_full_manifest` step issues one of per
range-spec encountered, with no opportunity to
amortize because each range-spec is unique to a single
edge's request) return the default-default for the
registry, which for npmjs.org is the existing
`get_manifests_concurrency_limit`'s default of 64
(carried over so the semver-mode behaves the same as
the legacy resolver on tarball-side concurrency);
for non-npmjs registries return the default-default
in all branches". The public wrapper
`pub async fn get_resolver_manifests_concurrency_limit()
-> usize` reads the active registry's hostname from
the user-config's `npm.registry` field (or the env
var), reads the semver-mode flag from the user-config's
`resolver.semver_mode` field, calls the helper. The
three branch-coverage unit tests (one per branch, all
guarded with `#[tokio::test]` and using `tempfile` to
isolate the test's view of `~/.utoorc.json`) document
the wiring. The existing `pub async fn
get_manifests_concurrency_limit() -> usize` for the
tarball-side concurrency (the cap that the installer's
parallel-extract step honors when staging package
tarballs into `node_modules`) keeps its existing
64-default shape — the tarball-side and the
resolver-side are semantically separate, with the
tarball-side bounded by disk-I/O and the
npmjs-tarball-CDN's per-IP rate, and the resolver-side
bounded by the metadata-endpoint's
per-IP-after-Cloudflare-edge-fan-out throughput.
* `crates/pm/src/helper/ruborist_context.rs` — the wire.
The single-line change in the `use
crate::util::user_config::{...}` brace-list import
changes `get_manifests_concurrency_limit` to
`get_resolver_manifests_concurrency_limit`, and the
single-line change at the call site in
`Context::new`'s body that previously read
`concurrency: get_manifests_concurrency_limit().await,`
now reads `concurrency:
get_resolver_manifests_concurrency_limit().await,` so
the `Context.concurrency` field — which the
`BuildDepsConfig::with_concurrency` builder reads in
`api::build_lockfile`'s config assembly — carries the
256-for-npmjs cap rather than the 64-for-tarballs cap
the legacy version did. Net file delta is +2 / -2 (one
line of import-list change, one line of call-site
change), or as `diff --stat` reports it under
brace-list-token-counting, `+1 / -1 = 0` non-trivial
change because the line is in a multi-line brace
block whose individual entries are token-counted and
the position of the renamed entry shifts in
alphabetical order so the file's line-count is
unchanged.
* `crates/ruborist/src/service/cache.rs` — the
dead-code-allowance comes off PR-A's
`impl ProjectCacheData::{resolved_manifests, from_resolved}`
block. The two methods are now in-crate-callsite-live
because the api.rs hunk above calls `from_resolved`
for the post-resolve sidecar write and the
`BuildDepsConfig::with_project_cache`'s downstream
consumption in the demand driver's
`ManifestState::seeded` calls `resolved_manifests` for
the warm-cache read. The 5-line comment block "//
Bridges between the on-disk project cache and the
demand resolver's neutral `(name, spec, manifest)`
tuples. Wired up in the resolver cutover PR — staged
here so the trait + adapter PRs stay self-contained."
and the `#[allow(dead_code)]` attribute right above
the `impl ProjectCacheData {` opening brace both come
off. The `pub(crate) fn resolved_manifests(&self) ->
Vec<(String, String, Arc<CoreVersionManifest>)>` and
the `pub(crate) fn from_resolved(entries: Vec<(String,
String, Arc<CoreVersionManifest>)>) -> Self` method
bodies — the ones that fold the on-disk per-package
spec→version-→manifest map into the flat neutral-tuple
vec and the inverse fold — are unchanged in this
commit; just the allowance attribute comes off.
* `crates/ruborist/src/resolver/demand/state.rs` — the
symmetric dead-code-allowance comes off PR-B's
`pub(crate) struct ResolverManifestCache { ...
entries ... }`'s `entries` field. The 6-line comment
"// The resolver writes this on each run via
`ManifestState::into_resolver_cache`; the reader is
`ProjectCacheData::from_resolved` in the cutover PR's
`api.rs` edit. Staged write-only here so the driver
lands ahead of the entry-point switch — see the
matching `#[allow(dead_code)]` on `ProjectCacheData`'s
bridges in `service/cache.rs` introduced by the
preceding (provider) PR." block and the
`#[allow(dead_code)]` attribute right above the
`pub(crate) entries:` line both come off. The field
itself — `pub(crate) entries: Vec<(String, String,
Arc<CoreVersionManifest>)>` — is unchanged; just the
allowance comes off, because the api.rs cutover's
read of `manifest_cache.entries` for the
`ProjectCacheData::from_resolved` call is the now-live
in-crate reader the staging comment named.
* `crates/ruborist/src/resolver/demand/driver.rs` — the
`#[ignore]` comes off the demand driver's single-flight
test. The 7-line "// The `resolve` entry in `builder`
still routes through the legacy
`RegistryClient::fetch_version_manifest` path in this
PR — the cutover that points it at the demand driver
lives in the follow-up PR in this stack. Until then
the `CountingRegistry`'s `ManifestProvider`-side job
counter stays at zero (the legacy path bypasses it),
so the single-flight assertion below has nothing to
count. The cutover PR removes this `#[ignore]`
alongside flipping the entry-point bounds." comment
block and the `#[ignore = "exercises the
demand-driver pipeline, wired in the cutover PR"]`
attribute, both above the `#[tokio::test]` attribute
above the `async fn
test_non_semver_exact_version_extract_single_flight()`
signature, both come off. The test body is unchanged.
The `#[tokio::test]` and the `async fn` stay. The
rest of the driver — the `MockRegistryClient`
scaffolding, the `CountingRegistry` wrapper that
decorates an inner `RegistryClient` impl with an
`AtomicUsize`-per-package-name request counter
visible on the outer wrapper for the test
assertions, the `create_version_manifest` and
`create_full_manifest` helpers, the half-dozen other
driver-internal tests (the queue-pop-ordering test,
the waiter-wake-on-fetch-complete test, the
state-transition-from-pending-to-cached test, the
fetch-error-propagates-to-WithChain test, etc.) that
cover the loop's invariants in isolation without
going through the public `resolve` entry — is
unchanged from PR-B's tip. `cargo test -p
utoo-ruborist --lib` on this commit's tree reports
`182 passed; 0 failed; 0 ignored` (the formerly-ignored
one joins the passing list because the assertion now
reads `left: 1, right: 1` instead of the
legacy-path's `left: 0, right: 1`).
The bench gate on this PR is the load-bearing
verification of the entire stack's perf claim. Its target
is the integrated-form #3084's bench number recorded at
the workflow run `26551226857` on the integrated commit
`48339925`: `utoo` p1_resolve `2.45s ± 0.13` and
allocation-side `vCtx` `18.1K` on the standard
`ant-design` workspace under the `pm-e2e-bench.yml`
workflow's `bench-phases-linux` job's `p1_resolve` phase
table row. Compared to the same workflow's `utoo-next`
baseline column on the same comment — `2.86s ± 0.04` and
`vCtx 47.4K` — the cutover's targeted improvement is the
14-percent-wall and the 2.6×-vCtx-reduction the perf
spec promised. Compared to the same workflow's `bun`
column at `1.95s ± 0.06` and `vCtx 8.0K`, the headroom
the demand-pipeline-vs-bun gap leaves is the matter the
post-cutover cleanup PR's warm-cache rationalization
addresses (the cleanup deletes the `crate::resolver::preload`
module entirely, the `UnifiedRegistry`'s stateful
`OnceMap`-shaped inflight-tracking is replaced by the
demand pipeline's own queue, the `BuildEvent::Preload*`
event variants and their progress-receiver hooks retire,
the `RegistryClient::cache_version_manifest` shim is
dropped, the per-cleanup dead-code lint covers what
remains). The cleanup is bench-tracked against this
cutover's bench result as its baseline — the "the
wall-clock and the vCtx held after the deletions
removed the carry-along scaffolding" confirmation. The
cleanup is the long-tail follow-up and does not block
this cutover's review.
The integrated-form #3084 (the PR Andrew has open on
the `perf/pm-resolver-driver` branch at commit
4833992, the all-in-one form that this three-PR stack
replaces with a layered equivalent) stays open as the
bench reference until this three-PR stack lands in
`next`. After the stack lands, #3084 closes as
superseded by the same payload landed in three
reviewable layers — its commit history at the integrated
form's tip remains in the repository's reflog and on the
GitHub PR's timeline as the historical record of the
original integration that motivated the split.
The two sibling bench gates in this stack — PR-A
#3085's gate at the amended type-level-scaffolding sha
`c16893ec` and PR-B #3086's gate at the rebased
driver-landing sha `63a18ad6` — are the no-regression
floor that this PR's win measures against. The
expected outcome on each of the two sibling gates is
"flat vs `utoo-next`" because the active runtime path
in each is byte-identical to next (the trait surface
in PR-A is unreferenced from any live call site, the
demand driver in PR-B is in the binary but the
entry-points still go through the legacy preload-and-BFS
chain, the orphan-annotation-receiving preload-era
functions are still the only callees of
`build_deps_with_config`'s body). The cutover-side
delta — what this PR introduces — is what flips the
active path from the legacy resolver to the demand
pipeline, lights up the runtime tunings, and gives the
bench-machine's resolve-phase timer the integrated
form's 2.45-second number to land at.
Three-of-three of the #3084 split. The merge order
across the stack is PR-A #3085 (the trait, no runtime
delta), then PR-B #3086 (the driver, no runtime delta
because the entry-points still bypass it), then this PR
(the entry-point flip and the runtime tunings, the
runtime delta that delivers the perf claim). GitHub's
stacked-PR base-tracking auto-rebases each PR's base
from "its stack predecessor's branch" to `next` as
each predecessor merges. The bench gate fires on each
push to each PR's branch independently — the bench
comments on the three PRs end up appearing in
chronological order of the bench machine's queue pull,
which matches the stack order modulo the GitHub
Actions runner's per-job timing.
Also fixes a latent gap in the shared fetch retry layer that the
cutover's higher concurrency surfaces. `service/fetch.rs`'s
`classify_status` mapped every non-404/429/5xx HTTP status to
`FetchError::Permanent` via a catch-all arm, so a `406 Not
Acceptable` was never retried. npmjs's Cloudflare edge
intermittently answers a manifest request with 406 under heavy
concurrent fan-out — a different package each run, ~one per run, a
transient content-negotiation hiccup rather than a real
unsatisfiable-Accept (which would 406 every request). The legacy
two-phase resolver's gentler 64-wide single-client fetch rarely
tripped it; the demand driver's 256-wide four-client pool fan-out
trips it on nearly every e2e run, failing `utoo install` on a
single stray 406. Reclassify 406 as `Retryable` alongside 429 so
the existing five-delay backoff absorbs it. The buggy catch-all
predates this stack (it lives in the shared layer both paths use),
but the cutover is what makes it user-visible, so the one-line fix
rides with it.
Refs #3028, #3083 (the select-and-state scaffolding
already in next as the stack base), #3084 (the
integrated-form reference, still open at 4833992 on
the `perf/pm-resolver-driver` branch), #3085 (PR-A,
the trait + adapter, draft at sha `c16893ec`), #3086
(PR-B, the driver + four graph helpers, draft at sha
`63a18ad6`).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Part 2/3 of the #3084 split — the demand-driven BFS resolver loop. Stacks on PR-A (#3085, the
ManifestProvidertrait + the registry-backed adapter). The cutover that points the publicbuild_deps_*andresolve_*entry-points +api::build_lockfileat the demand pipeline, and lands the two runtime tunings the demand driver's single-flight makes pay off, is PR-C — which I'll open after both #3085 and this PR's bench gates are in.The stack after the amendment
The original PR-A had bundled the HTTP-pool and the resolver-side concurrency knob alongside the trait. Bench on that earlier shape came in at a 14% wall-clock regression on p1_resolve vs
utoo-next(3.27s vs 2.86s on the standardant-designworkspace) because those tunings only pay off on a single-flight pipeline (which the legacy resolver isn't) and overcommit the npmjs per-IP rate-limit when applied to the per-edge legacy fetcher. Per the lesson from the earlier multi-PR rebuilds (memory note: "split PRs by 'does it ship the perf' not just 'additive vs delete'"), the tunings moved to the cutover PR where they're paired with the entry-point flip that makes them sound. After that amendment the stack reads:#3084 stays open as the integrated-form reference baseline until the three split PRs all land in
next. Its bench-comment record (the 2.45s ± 0.13 p1_resolve, vCtx 18.1K on the sameant-designworkspace) is the target the cutover PR's bench needs to match.What lands here
crates/ruborist/src/resolver/demand/driver.rs(new, +701) is the BFS loop and the fetch pipeline. It owns the per-runManifestStatecache + waiters + failures store from #3079, theFetchQueuesscheduler with single-flight de-dup from #3080, and the pure per-edge decision stepselect_edge→EdgeStep/WaitKey/FetchPlanfrom #3083. The pipeline pumps the trait's job stream through aFuturesUnorderedoftokio::task::JoinHandles — the multi-core spawn shape that gives the resolver native fan-out (tokio::spawnon native, the single-threadedtokio::task::spawn_localon wasm, gated by the same#[cfg_attr]on the trait'sSend + Syncbound from PR-A). Theapply_fetch_resultglue feeds resolved manifests back into the graph through the four new graph-building helpers inbuilder.rs(see below). Thehandle_processedwrapper fires the existingBuildEvent::ResolvedandBuildEvent::Failedevents so progress receivers see the same event surface across both the legacy two-phase pipeline and the demand pipeline once the cutover wires the entry-points across.crates/ruborist/src/resolver/builder.rs(+134 / -3) gains four pub helpers next to the existingprocess_dependency, plus a new pub(crate) async entry function. None of them have a caller in this PR outside the demand driver itself; the legacy entry chain (build_deps,build_deps_with_receiver,build_deps_with_config,resolve,resolve_with_options) keeps its existingR: RegistryClientsignatures and its existing preload-then-BFS body, so the active runtime path is unchanged from PR-A and the orphan-comment annotations ongather_preload_deps/run_preload_phase/run_bfs_phasearen't added yet (the cutover is what makes them orphans).builder.rspub(crate) async fn build_deps_with_config_output<R, E>(graph, registry, config, receiver) -> Result<ResolverManifestCache, ResolveError<R::Error>> where R: ManifestProvider, R::Error: Send, E: EventReceiverbuild_deps_with_config(unit-returning entry) andapi::build_lockfile's host-side cache-export step through. Body is thetracing::info!line, therun_main_loop_bfscall, and theOk(manifest_cache). Carries#[allow(dead_code)]with a one-line comment naming PR-C as the caller. The three newusestatements at the top of the file (CoreVersionManifestjoining the existingcrate::model::manifest::NodeManifestbrace-list, the newuse crate::resolver::demand::{ResolverManifestCache, run_main_loop_bfs}, andManifestProviderjoiningcrate::service::ProjectCacheData) are the only edits to existing lines in this file. The signatures of the legacy entries are untouched.pub(crate) fn try_reuse_dependency(graph, parent, edge, resolved_name, resolved_version) -> Option<EdgeStitched>(name, resolved-version)share one graph node rather than producing parallel duplicates. Extracted from the inline logic inprocess_dependencyso the demand path can call it without going back through the legacy entry chain.pub fn process_dependency_with_resolved(graph, parent, edge, resolved_name, manifest, dev_deps, peer_deps, source) -> NodeIndexCoreVersionManifestfor a child ofparent, creates or reuses the dependent node, attaches the dependency-typed edge, sets the resolution mode flags. Thepub(rather thanpub(crate)) visibility matches the existingpubonprocess_dependency— same audience surface, same level of abstraction.pub(crate) fn chain_err<E>(parent_chain, err) -> ResolveError<E>RegistryErrorfrom the provider's job stream into the resolver's existingResolveError::WithChain, so the CLI's chain-aware error renderer (pm::format_print) still gets the "outermost name → inner name → cause" causality string when the demand path's job-batch reports a failed manifest fetch, matching the equivalent error shape the legacy single-fetch path emits.pub(crate) async fn handle_resolved_registry_manifest<R, E>(graph, registry, receiver, parent, edge, resolved_name, manifest, state) -> Result<HandleResult, ResolveError<R::Error>> where R: ManifestProvider, R::Error: Send, E: EventReceiverManifestState::version, builds the resolved-version's edges into the queue of work the driver hasn't dispatched yet, firesBuildEvent::Resolved(name, version)on the receiver.crates/ruborist/src/resolver/demand/mod.rs(+13 / -5) declarespub mod driverand re-exportsdriver::run_main_loop_bfsandstate::ResolverManifestCacheat the demand-module level sobuilder.rs's new entry function can name them ascrate::resolver::demand::{run_main_loop_bfs, ResolverManifestCache}. The crate-level re-exports already exist through the lib.rs'spub mod resolver.crates/ruborist/src/resolver/demand/queue.rs(+5 / -3) tweaks the visibility of theFetchKeyandFetchDonetypes so the driver can name them outside the queue module, and adjusts theFetchPriorityenum's variant ordering so the driver'spop_nextwalks them in the right BFS-first order (semver-resolved-version > full-manifest > already-cached-version-lookup).crates/ruborist/src/resolver/demand/state.rs(+6 / -0) adds one annotation:#[allow(dead_code)]on theResolverManifestCache.entriesfield. The field is written byManifestState::into_resolver_cache()at the end ofrun_main_loop_bfsand read byProjectCacheData::from_resolved(cache.entries)in the cutover PR'sapi.rsedit. In this PR it's written-only (the only writer path is reachable through PR-B's dead-codedbuild_deps_with_config_output), so the warning would fire under the strictcargo clippy --all-targets -- -D warningsinvocation CLAUDE.md mandates. The annotation has a 5-line comment naming the cutover PR'sapi.rschange as the reader and pointing at the matching annotation on PR-A'sProjectCacheDatabridges inservice/cache.rs. Both annotations come off in PR-C when the writer-chain joins the reader-chain.The cutover-dependent test ignore
The driver's
#[cfg(test)] mod testsblock (indriver.rsitself) holds the unit-test scaffolding the driver needs: aMockRegistryClientthat simulates a registry's full-manifest and version-manifest responses, aCountingRegistrywrapper that delegates to an inner mock but increments anAtomicUsizefor everyrequest_manifestscall that involves a given package name (so single-flight de-duplication for that name is observable), and thecreate_*_manifesthelpers. One test in this block —test_non_semver_exact_version_extract_single_flight— sets up a root with two siblingsaandbboth depending on the same exact-versionshared@1.2.3, drives a fullresolve(pkg, &counting_registry)through the resolver crate's public entry, and asserts that the counter forshared@1.2.3reads exactly 1 (the demand driver's single-flight de-dup folded the two waiters onto one provider job).In this PR the public
resolveentry still goes through the legacyRegistryClient::fetch_version_manifestpath — the entry-point bound flip fromR: RegistryClienttoR: ManifestProvideris the cutover's payload — so the counter (which lives on theCountingRegistry'sManifestProviderimpl) never increments and the assertion's "left: 0, right: 1" panic is exactly the failure mode you'd see on a clean checkout of this PR. The test gets#[ignore = "exercises the demand-driver pipeline, wired in the cutover PR"]with a multi-line comment explaining the reason; the cutover PR removes the#[ignore]onceresolvedelegates tobuild_deps_with_config_output. The other tests in the driver's test module exercise the loop's invariants directly (state transitions, the schedule-and-pump cycle, the waiter wake-up after a fetch completes) without going through the public entry, and they pass under this PR's intermediate state.cargo test -p utoo-ruborist --libon63a18ad6reports181 passed; 0 failed; 1 ignored.Bench expectation
benchmarklabel is on. Since the driver is in the binary but unreferenced from any entry-point that the bench harness drives — the harness runsutoo installon theant-designworkspace and times the resolve phase, andutoo install's resolver entry is the legacyapi::build_lockfile→build_deps_with_config→run_preload_phasethenrun_bfs_phasechain — the active runtime path is the same as PR-A's. The expected p1_resolve and vCtx numbers therefore match PR-A's flat-vs-nextbaseline (≈ 2.86s wall and ≈ 47K vCtx onant-designafter PR-A's amendment dropped the runtime-tuning regression). The full ≈ 2.45s / ≈ 18K vCtx win — what the integrated-form #3084's bench measured — surfaces at the cutover PR's bench gate.Local hygiene on the rebased tree
After the cross-stack rebase (PR-B's single commit
0360cd64got replayed onto the amended PR-A tipc16893ec, yielding the current63a18ad6whose parent isc16893ec, with no rebase conflicts because PR-B touchesbuilder.rsanddemand/*while PR-A's amendment droppedservice/http.rs+pm/util/user_config.rs+pm/helper/ruborist_context.rs— disjoint file sets):cargo check -p utoo-ruborist -p utoo-pm: clean.cargo clippy -p utoo-ruborist -p utoo-pm --all-targets -- -D warnings --no-deps(the strict form CLAUDE.md mandates after a Rust edit): clean.cargo test -p utoo-ruborist --lib:181 passed; 0 failed; 1 ignored(the#[ignore]'d single-flight test mentioned above).cargo fmt -p utoo-ruborist -p utoo-pm --check: clean.The workspace-wide
cargo checkstill hits a pre-existingturbopack_nodejs::{EcmascriptBuildNodeChunk, EcmascriptBuildNodeEntryChunk}"not in the root" error inpack-api/src/webpack_stats.rs— that's thenext.jssubmodule's drift, present onnextand every branch in the repo, unrelated to this stack.Refs #3028, #3083, #3084, base PR-A #3085. The cutover PR will reference all four.
🤖 Generated with Claude Code