Skip to content

perf(pm): source resolver mainloop architecture#3028

Draft
elrrrrrrr wants to merge 21 commits into
nextfrom
perf/pm-source-resolver
Draft

perf(pm): source resolver mainloop architecture#3028
elrrrrrrr wants to merge 21 commits into
nextfrom
perf/pm-source-resolver

Conversation

@elrrrrrrr
Copy link
Copy Markdown
Contributor

@elrrrrrrr elrrrrrrr commented May 21, 2026

Summary

Draft source PR for the resolver half of #2948. This is not the final review unit; it is the source branch used to verify the full resolver stack in one place.

This source branch now matches the current resolver split-stack top (perf/pm-split-resolver-doc-cleanup), so the split PRs should compose back to this diff exactly.

Covers From #2948

  • ManifestProvider / ManifestJob boundary.
  • Resolver-owned BFS/cache/inflight/waiter state.
  • Provider jobs only perform concrete manifest work.
  • Demand-first prefetch scheduling.
  • Obsolete preload removal.
  • Speculative full-manifest version extraction.
  • Version manifest Vec hot path.
  • Ruborist global MemoryCache / PackageCache / registry OnceMap cleanup.
  • PM resolver wiring needed to preserve p1 behavior.

Actual Split Stack

Notes

#3043 is the remaining core scheduling PR. It is intentionally larger than the target review size because splitting semver and full-manifest demand paths creates a bad intermediate state: cache ownership and project-cache output would diverge by registry capability. Follow-up PRs keep tests, preload deletion, registry cleanup, and docs separate.

Validation

Validated at resolver split-stack top:

  • cargo fmt
  • cargo check -p utoo-ruborist
  • cargo test -p utoo-ruborist --lib (170 passed)
  • cargo clippy --all-targets -- -D warnings --no-deps

pack-napi warns locally because next.js is a symlink in this worktree; clippy exits successfully.

@elrrrrrrr elrrrrrrr added A-Pkg Manager Area: Package Manager benchmark Run pm-bench on PR labels May 21, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the dependency resolver from a two-phase approach to a demand-driven BFS resolution. It introduces a ManifestProvider trait and a centralized FetchQueues system to manage concurrent manifest jobs, moving in-memory caching directly into the resolver loop. Performance is further optimized by introducing multiple HTTP client pools and improving JSON parsing efficiency through speculative extraction. Feedback focuses on performance improvements within the new resolver logic, specifically addressing algorithmic complexity in job selection and prefetch tracking, as well as redundant cloning during cache warming. Additionally, it is recommended to replace recovery logic for unreachable states with panics to align with unrecoverable error guidelines.

Comment on lines +854 to +858
let prefetch_concurrency = if self
.queued
.values()
.any(|priority| *priority == FetchPriority::Demand)
{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The use of self.queued.values().any(...) here is $O(N)$ where $N$ is the number of queued jobs. Since pop_next is called in a loop during BFS traversal, this can lead to $O(N^2)$ complexity relative to the level size.

Additionally, the variable name prefetch_concurrency shadows the function parameter, which can be confusing.

Consider using !self.demand.is_empty() as a fast $O(1)$ check. While it may occasionally be a false positive due to stale entries in the VecDeque, it is a safe approximation for limiting prefetch concurrency.

        let limit = if !self.demand.is_empty() {
            prefetch_concurrency
        } else {
            usize::MAX
        };

Comment on lines +1685 to +1699
let Some(done) = fetches.next().await else {
let mut fallback = Vec::new();
for (_, waiters) in state.full_waiters.drain() {
fallback.extend(waiters);
}
for (_, waiters) in state.version_waiters.drain() {
fallback.extend(waiters);
}
for (parent, edge) in fallback {
let processed = process_dependency(graph, registry, parent, &edge, config)
.await
.map_err(|inner| chain_err(graph, parent, &edge, inner))?;
handle_processed(graph, receiver, parent, &edge, &processed, &mut next_level);
}
break;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This fallback logic implements recovery for a state that should be unreachable. According to our guidelines, we should not implement recovery logic for bugs that represent unrecoverable states. Instead of a sequential fallback, this should trigger a panic to ensure the underlying logic error is identified and fixed.

        if result.is_none() && !waiters.is_empty() {
            panic!("Async fetcher returned None while waiters still exist. This is an unrecoverable logic error.");
        }
References
  1. Do not implement recovery logic for panics. Panics should be treated as unrecoverable bugs that need to be fixed, not as transient, recoverable errors.

Comment on lines +887 to +892
fn active_prefetches(&self) -> usize {
self.active
.values()
.filter(|priority| **priority == FetchPriority::Prefetch)
.count()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The active_prefetches method performs an $O(M)$ scan of the active jobs map (where $M$ is the concurrency limit). Since this is called frequently in the main loop, it's better to maintain a counter in the FetchQueues struct.

Comment on lines +917 to +931
for (name, pkg_cache) in &warm.cache {
for (spec, version) in &pkg_cache.specs {
let Some(manifest) = pkg_cache.manifests.get(version) else {
continue;
};
let manifest = Arc::new(manifest.clone());
state
.version_cache
.insert((name.clone(), spec.clone()), Arc::clone(&manifest));
state
.version_cache
.entry((name.clone(), version.clone()))
.or_insert(manifest);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This implementation clones the manifest for every spec that points to it, which is inefficient for packages with many specs resolving to the same version.

Consider iterating over the manifests first to populate the version cache, then mapping specs to the already created Arcs.

        for (name, pkg_cache) in &warm.cache {
            let mut version_to_arc = HashMap::new();
            for (version, manifest) in &pkg_cache.manifests {
                let arc = Arc::new(manifest.clone());
                version_to_arc.insert(version, Arc::clone(&arc));
                state.version_cache.insert((name.clone(), version.clone()), arc);
            }
            for (spec, version) in &pkg_cache.specs {
                if let Some(arc) = version_to_arc.get(version) {
                    state.version_cache.insert((name.clone(), spec.clone()), Arc::clone(arc));
                }
            }
        }

@elrrrrrrr elrrrrrrr force-pushed the perf/pm-source-resolver branch from 2702d34 to e0acfeb Compare May 21, 2026 17:40
@elrrrrrrr elrrrrrrr force-pushed the perf/pm-source-resolver branch from 9b50ad8 to 9a0819b Compare May 21, 2026 22:32
@elrrrrrrr elrrrrrrr force-pushed the perf/pm-source-resolver branch from 9a0819b to bb23373 Compare May 21, 2026 23:09
@elrrrrrrr elrrrrrrr force-pushed the perf/pm-source-resolver branch from bb23373 to 711835d Compare May 21, 2026 23:39
elrrrrrrr added a commit that referenced this pull request May 26, 2026
PR 2 of 2 (stacked on #3077). Wires the staged provider + demand engine into
build_deps and retires the two-phase preload engine.

- builder.rs: build_deps* drive demand::run_main_loop_bfs; preload/bfs phases +
  gather_preload_deps deleted; RegistryClient bound -> ManifestProvider.
- registry.rs: UnifiedRegistry made stateless (drops GLOBAL_MEMORY_CACHE +
  OnceMap inflight maps); impls ManifestProvider.
- delete resolver/preload.rs + BuildEvent::Preload* + cache_version_manifest;
  cache.rs slimmed; supports_semver: bool -> ResolutionMode enum.
- demand.rs/provider.rs: remove the #3077 allow; add the driver integration
  test (CountingRegistry single-flight).

#3077 + this == the reorganised resolver tree; #3028 kept as compose-back ref.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
elrrrrrrr added a commit that referenced this pull request May 26, 2026
PR 2 of 2 (stacked on #3077). Wires the staged provider + demand engine into
build_deps and retires the two-phase preload engine.

- builder.rs: build_deps* drive demand::run_main_loop_bfs; preload/bfs phases +
  gather_preload_deps deleted; RegistryClient bound -> ManifestProvider.
- registry.rs: UnifiedRegistry made stateless (drops GLOBAL_MEMORY_CACHE +
  OnceMap inflight maps); impls ManifestProvider.
- delete resolver/preload.rs + BuildEvent::Preload* + cache_version_manifest;
  cache.rs slimmed; supports_semver: bool -> ResolutionMode enum.
- demand.rs/provider.rs: remove the #3077 allow; add the driver integration
  test (CountingRegistry single-flight).

#3077 + this == the reorganised resolver tree; #3028 kept as compose-back ref.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

📊 pm-bench-phases · f318a41 · linux (ubuntu-latest)

Workflow run — ant-design

PMs: utoo (this branch) · utoo-npm (latest published) · bun (latest)

npmjs.org

p0_full_cold

PM wall ±σ user sys RSS pgMinor
bun 11.22s 3.73s 9.83s 9.99s 632M 304.3K
utoo-next 11.61s 6.39s 9.94s 11.75s 888M 119.6K
utoo-npm 8.39s 0.21s 10.28s 12.21s 889M 123.8K
utoo 8.16s 0.82s 11.27s 12.11s 1.00G 152.8K
PM vCtx iCtx netRX netTX cache node_mod lock
bun 19.1K 18.9K 1.11G 7M 1.75G 1.64G 1M
utoo-next 116.9K 80.7K 1.08G 5M 1.62G 1.61G 2M
utoo-npm 139.9K 102.0K 1.08G 5M 1.62G 1.61G 2M
utoo 115.4K 73.2K 1.08G 6M 1.61G 1.61G 3M

p1_resolve

PM wall ±σ user sys RSS pgMinor
bun 2.32s 0.05s 3.95s 1.20s 517M 197.6K
utoo-next 3.05s 0.03s 5.07s 1.91s 617M 80.7K
utoo-npm 4.87s 1.36s 5.28s 2.36s 622M 75.4K
utoo 2.49s 0.02s 6.03s 1.77s 648M 123.5K
PM vCtx iCtx netRX netTX cache node_mod lock
bun 12.4K 3.9K 206M 3M 109M - 1M
utoo-next 56.4K 79.7K 202M 3M 7M 3M 2M
utoo-npm 84.9K 97.6K 202M 3M 7M 3M 2M
utoo 17.9K 21.5K 205M 3M 7M 3M 2M

p3_cold_install

PM wall ±σ user sys RSS pgMinor
bun 6.70s 0.21s 5.90s 9.73s 532M 190.5K
utoo-next 5.85s 0.16s 4.88s 10.31s 437M 61.3K
utoo-npm 5.78s 0.16s 4.89s 10.24s 428M 57.7K
utoo 5.61s 0.02s 4.83s 10.13s 480M 64.6K
PM vCtx iCtx netRX netTX cache node_mod lock
bun 7.7K 7.7K 935M 4M 1.67G 1.67G 1M
utoo-next 92.5K 47.0K 904M 2M 1.61G 1.61G 2M
utoo-npm 94.3K 46.6K 904M 2M 1.61G 1.61G 2M
utoo 85.8K 49.2K 904M 2M 1.61G 1.61G 2M

p4_warm_link

PM wall ±σ user sys RSS pgMinor
bun 3.28s 0.09s 0.19s 2.46s 134M 32.5K
utoo-next 2.33s 0.09s 0.52s 3.80s 81M 18.9K
utoo-npm 2.35s 0.01s 0.48s 3.85s 80M 18.9K
utoo 2.13s 0.15s 0.49s 3.81s 80M 18.2K
PM vCtx iCtx netRX netTX cache node_mod lock
bun 263 26 5M 40K 1.82G 1.64G 1M
utoo-next 41.6K 19.3K 310K 10K 1.61G 1.61G 2M
utoo-npm 41.9K 18.5K 310K 12K 1.61G 1.61G 2M
utoo 42.1K 19.0K 309K 8K 1.62G 1.61G 2M

npmmirror.com: no output captured.

elrrrrrrr added a commit that referenced this pull request May 27, 2026
The demand driver was polling all fetch futures inline on the single
driver task (`?Send` provider, `Box::pin` futures). That serialised fetch
+ parse coordination onto one thread and lost the multi-core parallelism
the original #3028 design had — benchmarks showed resolve-phase context
switches ~3x higher (vCtx 18K -> 56K) and ~30% slower wall.

Restore #3028's cfg-gated model:
- `ManifestProvider: Send + Sync`, `#[async_trait]` on native /
  `#[async_trait(?Send)]` on wasm.
- `FetchFuture = JoinHandle<FetchDone>`; `fetch_registry_manifest` uses
  `tokio::spawn` (native) / `spawn_local` (wasm), so independent fetch +
  parse jobs run in parallel across the runtime.
- drain unwraps the `JoinHandle` result.

Native parallelism restored; wasm stays single-threaded via spawn_local.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
elrrrrrrr added a commit that referenced this pull request May 28, 2026
Stack the driver work on #3083 (select + PackageVersions) and adapt it to
the new state API. This is the "#3028-on-#3083 refactored" integration
candidate — the driver, the spawn/multi-core model, the cutover, and the
perf tuning, now built on the clean leaf modules (state / queue / select).

- demand/driver.rs (+ provider impl, manifest helpers, http pool, cutover,
  pm wiring) brought from the unintegrated driver branch.
- driver adapted to PackageVersions: `state.full.cache.insert` /
  `versions_cache` / `full.failures` → `set_package(Full/List/Failed)`;
  `full.waiters`/`full.wake` → `park_on_package`/`wake_package`;
  `full.is_settled || versions_cache.contains` → `has_package_source`.

182 tests pass, clippy clean (default). Bench-gate next to confirm it
still lands ~2.4s (the whole point: match #3028 on the refactored split).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
elrrrrrrr added a commit that referenced this pull request May 28, 2026
Type-level scaffolding for the demand-driven resolver rework
(#3028 / #3084). The trait the demand driver dispatches through —
`service::ManifestProvider` — and its registry-backed adapter
(`impl ManifestProvider for UnifiedRegistry` in the new
`service::registry::provider` module) land here unreferenced. The
driver that consumes the trait is the next PR in this stack
(`resolver/demand/driver.rs`, ~700 lines). The flip of the public
entry-points (`build_deps_*` and `resolve_*` in `resolver::builder`,
plus the `api::build_lockfile` host call site) from the legacy
`RegistryClient` bound to the new `ManifestProvider` bound is the
third PR, and the two runtime tunings — the HTTP-client pool that
fans connections across Cloudflare edge IPs, and the resolver-side
`get_resolver_manifests_concurrency_limit` knob that raises the
in-flight cap for non-semver registries (npmjs) from the existing
64 (tarball-side `get_manifests_concurrency_limit`) to 256 — ride
along with the cutover. They're scoped that way because their
payoff is the demand driver's single-flight de-duplication of
concurrent fetches for the same package: landing the same tunings
on the legacy two-phase resolver overcommits its non-deduplicated
per-edge concurrency at the npmjs front door and regresses the
resolve phase. The bench-vs-`utoo-next` comparison on this PR is
expected to sit at noise — the active runtime path of
`utoo install` is byte-identical to `next` here, because nothing
in this PR is reachable from `build_deps_with_config`'s body or
from any other live entry, the HTTP client stays at its single
shared instance, and `Context.concurrency` keeps reading the
existing tarball-side knob.

File by file:

* `service/manifest_provider.rs` (new, +106 lines): the trait
  definition. One async method that takes a `ManifestJob` and
  returns a `ManifestJobDone` (the typed job-and-result shape;
  see `traits/registry.rs` below). Carries `Send + Sync` under
  a `#[cfg_attr(not(target_arch = "wasm32"), async_trait)]` and
  the `?Send` form for wasm — the native build can `tokio::spawn`
  the job futures across the multi-threaded runtime (which is
  where the demand driver's perf comes from), and the
  single-threaded wasm runtime still works via `spawn_local`.

* `service/registry/` (rename + new sibling): the existing flat
  `service/registry.rs` becomes `service/registry/mod.rs` so a
  new sibling file `service/registry/provider.rs` (+179 lines)
  can hold the `impl ManifestProvider for UnifiedRegistry`
  without bloating `mod.rs` and without widening the visibility
  of `UnifiedRegistry`'s private fields (`store`, `registry_url`,
  the supports-semver flag) — child modules already see private
  items of their parent module. The `UnifiedRegistry` struct
  body itself is unchanged.

* `traits/registry.rs` (+67 lines): the trait's job and error
  shapes — `RegistryError`, `ManifestJob` (the `Versions`,
  `Full`, `ExactVersion` job kinds the driver issues), the
  paired `ManifestJobDone` and the `ManifestFullData` payload
  the full-manifest kind returns, and the `MetadataFormat`
  enum for the response-content-type negotiation
  (`application/vnd.npm.install-v1+json` vs the full form).
  Pulled out as named items so the trait's signatures don't
  leak any of the existing `service::fetch` module's internal
  types.

* `service/manifest.rs` (+183 lines): two new helpers the
  adapter uses to keep `simd_json::serde::from_slice`'s
  in-place buffer mutation off the tokio runtime. The existing
  `parse_json_off_runtime` (a borrowing form that copies the
  buffer inside the worker) gets a buffer-consuming sibling
  `parse_json_vec_off_runtime(Vec<u8>)` whose callers can hand
  ownership of the response-body bytes straight in. The
  full-manifest parse picks up a sibling
  `parse_full_manifest_with_core_off_runtime(bytes, spec)`
  that returns both the parsed `FullManifest` and, when a spec
  was supplied that names an exact version, the
  `CoreVersionManifest` slice for that version — so the
  adapter's `ManifestJob::ExactVersion` path can hand the
  per-version result back to the driver without a second
  pass over the full document. Both helpers dispatch the
  CPU-bound parse to `rayon::spawn` on native and inline it
  on wasm via a `#[cfg(target_arch)]` switch.

* `service/cache.rs` (+43 lines): two methods on
  `ProjectCacheData` that bridge the on-disk shape (a
  per-package map of specs and resolved-version manifests, the
  format the host serializes to the lockfile sidecar) and the
  resolver-owned `(name, spec, manifest)` tuples the demand
  loop emits. `resolved_manifests(&self)` flattens the on-disk
  map into the neutral tuple form for seeding a warm resolver
  run; `from_resolved(tuples)` rebuilds the on-disk shape from
  the tuples the resolver returned. The impl block carries
  `#[allow(dead_code)]` until the cutover PR points
  `api::build_lockfile` at them — same dead-code-staging
  pattern as the earlier #3079 (state) and #3083 (select)
  splits.

* `service/mod.rs` (+4 lines): public re-exports of
  `ManifestProvider` and the supporting job types at the
  `crate::service::*` level so neither the demand module nor
  the pm binary has to reach into the sub-module path.

* `model/manifest.rs` (+5 lines) and `model/mod.rs` (+6 lines):
  the small additions on the model side that the new parse
  helpers consume — a flat-list shape for the versions-only
  abbreviated-metadata response and the corresponding
  `pub use`.

* `.github/workflows/pm-e2e-bench.yml` (+8 / -2): the
  bench-baseline build step (which overlays `origin/next`'s
  tracked files on top of the PR's tree with
  `git checkout origin/next -- .` so a `cargo build` against
  next's resolver runs against the PR's e2e harness) gets a
  cleanup of the paths the PR adds that don't exist in next:
  `git diff --no-renames --diff-filter=A --name-only
  origin/next HEAD -- crates/ | xargs -r rm -f`. Without it,
  this PR's new `service/registry/` directory ends up
  side-by-side with the overlay's flat `service/registry.rs`,
  and the build hits rustc's E0761 ("file for module
  `registry` found at both `mod.rs` and `registry.rs`"). The
  `--no-renames` flag is the load-bearing detail — under
  default rename detection git pairs the
  `registry.rs ↔ registry/mod.rs` rename as a single
  change, and the `--diff-filter=A` for the added side then
  reports zero added paths and misses the directory.

The benchmark label on the PR is on so the bench gate runs.
The two scaffolding tunings the perf model needs — the
HTTP-client pool and the resolver-side concurrency cap of 256
for non-semver registries — live in the cutover PR alongside
the entry-point bound flip, because the demand driver's
single-flight is what makes the higher cap a win rather than
a wall-clock regression vs `next`. The bench numbers on this
PR are expected to sit at the `next` baseline within noise.

Part 1/3 of the #3084 split. The remaining two are the demand
driver (Part 2) and the entry-point cutover + the runtime
tunings + the dead-code annotations coming off (Part 3, the
bench-gated one).

Refs #3028, #3084.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
elrrrrrrr added a commit that referenced this pull request May 28, 2026
Lands the demand-driven BFS resolver loop on top of the
`ManifestProvider` trait from the preceding PR in the stack. The
driver and its graph-building helpers exist as dead code in this PR
— the entry-point switch that points `api.rs` and `builder`'s
public `build_deps_*` / `resolve_*` chain at them is the third
PR. Same dead-code-staging idiom as the earlier `state.rs` (#3079)
and `select.rs` (#3083) splits.

What lands here, file by file:

* `resolver/demand/driver.rs` (new, ~700 lines): the `run_main_loop_bfs`
  entry — owns the per-run `ManifestState` (the cache + waiters +
  failures store from #3079) and the `FetchQueues` scheduler (the
  push/pop/complete state machine from #3080), pumps the
  `ManifestProvider` job stream through a `FuturesUnordered` of
  `tokio::task::JoinHandle`s (the multi-core spawn that gives the
  resolver native fan-out — `tokio::spawn` on native targets, the
  single-threaded `tokio::task::spawn_local` on wasm via the
  `#[cfg_attr]` toggle on the trait's `Send + Sync` bound). The
  `apply_fetch_result` glue feeds resolved manifests back into the
  graph through the new helpers in `builder.rs` (see below); the
  `select_edge` decision step from #3083 picks the next action
  per-edge (cache hit, version-cache hit, wait on an in-flight job,
  fail). The `handle_processed` wrapper around the
  graph-mutation step emits the existing `BuildEvent::Resolved` /
  `Failed` so progress receivers don't see a discontinuity once
  the cutover lands. A `#[cfg(test)]` module at the bottom holds
  the driver's unit-test scaffolding (`MockRegistryClient`,
  `CountingRegistry` wrapper for the single-flight property, the
  `create_*_manifest` helpers). One of those tests —
  `test_non_semver_exact_version_extract_single_flight` — is
  `#[ignore]`d in this PR with a reason string: it asserts on the
  `ManifestProvider` job count produced by a full
  `resolve(pkg, registry)` pipeline, which still routes through
  the legacy `RegistryClient::fetch_version_manifest` path in this
  PR. The cutover PR removes the `#[ignore]` once `resolve` is
  pointed at the demand driver. The other driver tests cover the
  loop's invariants in isolation (state transitions, waiter wake-up,
  schedule fairness) and pass under PR-B.

* `resolver/demand/mod.rs`, `resolver/demand/queue.rs`: the small
  re-export and visibility adjustments to expose `run_main_loop_bfs`
  and `ResolverManifestCache` at the `crate::resolver::demand`
  level so `builder.rs` can name them, and the queue's `FetchKey`
  /`FetchDone` types in the shape the driver consumes.

* `resolver/demand/state.rs`: a single attribute — `#[allow(dead_code)]`
  on the `ResolverManifestCache.entries` field. The driver writes
  the field via `ManifestState::into_resolver_cache()` at the end
  of each run; the reader is `ProjectCacheData::from_resolved` in
  the cutover PR's `api.rs` edit. Mirrors the symmetric annotation
  on the `ProjectCacheData` bridges in `service/cache.rs` from
  PR-A — both annotations come off when the entry-point switch
  wires the writer-chain to the reader-chain in PR-C.

* `resolver/builder.rs`: four new graph-building helpers extracted
  from `process_dependency`'s internal logic so the driver can
  reuse them without going back through the legacy entry-points,
  plus the new `pub(crate) async fn build_deps_with_config_output`
  that wraps the demand loop with the existing tracing + receiver
  wiring and returns the `ResolverManifestCache` the host needs to
  persist:

  - `pub(crate) fn try_reuse_dependency(...)`: hits the graph's
    existing-node index before issuing a fetch, so repeat references
    to the same `(name, resolved-version)` share one node.
  - `pub fn process_dependency_with_resolved(...)`: the
    edge-resolution tail that runs once a manifest is in hand —
    creates or reuses the dependent node, attaches the edge,
    forwards the resolution mode flags.
  - `pub(crate) fn chain_err(...)`: lifts a `RegistryError` from
    the provider's job stream into the resolver's
    `ResolveError::WithChain` so the CLI's chain-aware error
    renderer still gets the parent → child causality string when
    the demand path fails the same way the legacy path used to.
  - `pub(crate) async fn handle_resolved_registry_manifest(...)`:
    the integration point between a resolved `CoreVersionManifest`
    and the graph — caches under both the spec and the resolved
    version (so later lookups by either key hit memory), spawns the
    dependent-edge collection, fires `BuildEvent::Resolved`.

  All four are reachable only from the driver in this PR; the
  legacy `process_dependency` keeps its inline form and the
  legacy entry chain (`build_deps` / `build_deps_with_*` /
  `resolve` / `resolve_with_options`) keeps its old
  `R: RegistryClient` signatures. The new
  `build_deps_with_config_output` is the demand-side entry the
  cutover PR will route `build_deps_with_config` and `api.rs`
  through; it carries an `#[allow(dead_code)]` for this interim
  state with a one-line comment naming the next PR as its caller.

  The three import-line tweaks at the top of `builder.rs` —
  `CoreVersionManifest` joining the `crate::model::manifest`
  brace-group, the new `use` of `ResolverManifestCache` and
  `run_main_loop_bfs` from `crate::resolver::demand`, and
  `ManifestProvider` joining the `crate::service` brace-group —
  are the only edits to existing lines in this file. The orphaned
  preload-era functions (`gather_preload_deps`, `run_preload_phase`,
  `run_bfs_phase`) keep their existing signatures and live call
  paths — the cutover PR is what `#[allow(dead_code)]`-annotates
  them and the cleanup PR after the cutover deletes them.

The benchmark label is on this PR so the bench gate runs. Because
the active resolver pipeline is unchanged in this PR (`resolve`
still calls preload-then-BFS through the legacy
`RegistryClient` interface), the expected bench numbers match
PR-A on the standard npmjs workspace. The full
`p1_resolve ≈ 2.4s / vCtx ≈ 18K` win shows up in PR-C alongside
the entry-point flip.

Part 2/3 of the #3084 split.

Refs #3028 #3083 #3084 #3085

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Pkg Manager Area: Package Manager benchmark Run pm-bench on PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant