Skip to content

perf(pm): demand resolver — provider trait + registry adapter (1/3)#3085

Draft
elrrrrrrr wants to merge 4 commits into
nextfrom
perf/pm-resolver-provider
Draft

perf(pm): demand resolver — provider trait + registry adapter (1/3)#3085
elrrrrrrr wants to merge 4 commits into
nextfrom
perf/pm-resolver-provider

Conversation

@elrrrrrrr
Copy link
Copy Markdown
Contributor

@elrrrrrrr elrrrrrrr commented May 28, 2026

Part 1/3 of the #3084 split — the demand-driven resolver rework from #3028, originally landed as a single +1566/-87 PR (#3084), broken into three reviewable layers using the same dead-code-staging idiom as the earlier #3079 (state) and #3083 (select) splits.

The stack

next ──► #3083 (select + PackageVersions, ready)
          └── this PR (PR-A, provider trait + registry adapter, draft)
                └── PR-B #3086 (driver loop + four graph helpers in builder.rs, draft)
                      └── PR-C (cutover + the two runtime tunings + the dead-code allowances coming off,
                                bench-gated, will open after this PR's bench is in)

Scope of this PR

The type-level abstraction the demand resolver will dispatch through, and its registry-backed adapter, plus the supporting parse helpers, the on-disk-↔-tuples cache bridges, the registry-as-directory rename, and the bench-baseline workflow fix. Nothing calls the trait yet — service::api::build_lockfile still goes through the existing build_deps_with_config over the legacy RegistryClient trait, the same as next. The dead items get the standard #[allow(dead_code)] with a comment naming the PR that wires the caller.

File roster (10 files, +574/-34 over the perf/pm-resolver-select base, the tip of #3083):

File Purpose
service/manifest_provider.rs (new, +106) The ManifestProvider trait, one async method request_manifests(jobs) -> Vec<ManifestJobDone>. Send + Sync under #[cfg_attr(not(target_arch = "wasm32"), async_trait)] so the native build can tokio::spawn job futures across the multi-threaded runtime (where the perf comes from in the cutover) and wasm still works via spawn_local.
service/registry/ (rename) The flat service/registry.rs becomes the directory module service/registry/mod.rs. The new service/registry/provider.rs (+179) is its sibling holding impl ManifestProvider for UnifiedRegistry. Child-module shape gives the adapter access to the registry struct's private fields (store, registry_url, supports_semver) without widening their crate-visibility. UnifiedRegistry's body is unchanged.
traits/registry.rs (+67) The trait's job and error shapes — RegistryError, ManifestJob (the Versions / Full / ExactVersion request kinds), ManifestJobDone, the ManifestFullData payload, and the MetadataFormat enum for the application/vnd.npm.install-v1+json vs full content-type negotiation — pulled out as named items so the trait's signatures don't leak the existing service::fetch module's internal types.
service/manifest.rs (+183) Two new parse helpers the adapter calls to keep simd_json::serde::from_slice's in-place buffer mutation off the tokio runtime. parse_json_vec_off_runtime(Vec<u8>) is the buffer-consuming sibling of the existing borrowing-and-copying parse_json_off_runtime(Bytes). parse_full_manifest_with_core_off_runtime(bytes, Option<spec>) returns the parsed FullManifest and (when a spec was given that names an exact version) the CoreVersionManifest slice for that version, so the adapter's ManifestJob::ExactVersion path doesn't pay for a second pass over the full document. Both helpers dispatch the parse to rayon::spawn on native and inline it on wasm.
service/cache.rs (+43) Two methods on ProjectCacheData that bridge the lockfile-sidecar shape (a per-package map of specs and resolved-version CoreVersionManifests) and the resolver's neutral (name, spec, manifest) tuple stream. resolved_manifests(&self) flattens for seeding a warm run; from_resolved(tuples) rebuilds for the lockfile emission. The impl block carries #[allow(dead_code)] with a one-line comment naming the cutover PR's api::build_lockfile edit as the consumer.
service/mod.rs (+4 / -3) Re-exports the new ManifestProvider trait and the job types at crate::service::* so the demand module in PR-B and the pm binary can name them without reaching into the sub-module path.
model/manifest.rs (+5 / -1) and model/mod.rs (+6 / -1) Small model-side additions the new parse helpers consume — a flat-list shape for the abbreviated-metadata response (the "versions only" form npmjs returns with the application/vnd.npm.install-v1+json accept), plus 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 tree so the bench can build a "what next would compile here" reference binary) gets a cleanup of paths the PR adds that next lacks: 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 sits alongside next's flat service/registry.rs after the overlay and the baseline build hits rustc's E0761 ("file for module registry found at both"). The --no-renames flag is the load-bearing detail — with default rename detection on, git pairs the registry.rs ↔ registry/mod.rs change as a single rename and the --diff-filter=A for the added-side then reports zero adds and the cleanup misses the new directory. The same fix lives on perf/pm-resolver-demand (the original full-stack landing branch, kept for archaeology) and any of the downstream demand-stack branches that need to overlay next.

Runtime behavior

The active runtime path of utoo install in this PR is byte-identical to next: the resolver entry chain (api::build_lockfilebuild_deps_with_configrun_preload_phase then run_bfs_phase, all over the existing RegistryClient trait) is unmodified, the HTTP client (service::http::get_client) is the single shared LazyLock<reqwest::Client> it has always been, and Context.concurrency is still fed by the existing get_manifests_concurrency_limit (the tarball-side knob, default 64). The two runtime tunings the perf model the demand driver exploits — the round-robin pool of four reqwest clients across Cloudflare edge IPs, and a new resolver-specific get_resolver_manifests_concurrency_limit that raises the in-flight cap to 256 for the non-semver npmjs case — are bundled into the cutover PR alongside the entry-point bound flip, 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: an earlier iteration of this PR that did include them came in at p1_resolve 3.27s ± 0.05 vs utoo-next's 2.86s on the standard ant-design workspace — a 14% wall-clock regression — and vCtx ticked up from 47K to 54K. The bench comment for the previous tip c4430b2f of this branch (linked from the Workflow run 26552967227 on the timeline) records the numbers for the archaeology.

After the amendment the three runtime-tuning files (service/http.rs, pm/util/user_config.rs, pm/helper/ruborist_context.rs) sit at the perf/pm-resolver-select baseline content in this PR — they re-enter the diff in the cutover PR alongside the where-clause flips on build_deps_* / resolve_* from R: RegistryClient to R: ManifestProvider, R::Error: Send, the body change of build_deps_with_config to delegate to the new pub(crate) build_deps_with_config_output that PR-B adds (the wrapper around the demand loop that returns the run's manifest cache), the api::build_lockfile edit that consumes that cache through ProjectCacheData::from_resolved, the #[ignore] removal on PR-B's single-flight test, and the #[allow(dead_code)] annotations on the orphaned preload-era helpers (gather_preload_deps, run_preload_phase, run_bfs_phase) that get retired once the cutover routes around them. The cutover PR is where the end-state bench numbers from the #3028 / #3084 spec — the wall-clock that lands at p1_resolve ≈ 2.4s (within 26% of bun's 1.95s on the same workspace) and vCtx ≈ 18K (down from next's 47K, a ~2.6× drop) — empirically appear.

Bench

benchmark label is on. With no caller of the new trait and the runtime knobs at the next baseline, the bench-vs-utoo-next gap on this PR is expected to sit at noise — the run is a CI hygiene gate confirming the type-level scaffolding doesn't drag the existing pipeline. The load-bearing bench is on PR-C.

Refs #3028, #3083, #3084.

🤖 Generated with Claude Code

elrrrrrrr and others added 3 commits May 27, 2026 19:57
Third leaf module of the demand resolver, after `state` (#3079) and
`queue` (#3080). `select` is the pure per-edge resolution decision:

  fn select_edge(state, edge, name, spec, mode) -> EdgeStep
      // EdgeStep = Resolve | Skip | Fail | Park { wait, fetch }

It only reads `ManifestState` and returns a decision value — no `&mut`,
no async, no I/O, no graph mutation — so it's unit-testable in isolation
(7 tests covering semver/full-manifest cache hits, recorded failures,
parks, client-side version resolution, and optional-skip).

Dead-code-staged (`#![allow(dead_code)]`); the driver that consumes it
lands in the follow-up PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address review: drop the one-line `resolve_version_from_full_manifest`
wrapper (inline `resolve_version_from_versions`), and collapse the
duplicated "recorded failure? -> cached manifest?" probe — which appeared
in the semver path, the full-manifest early check, and the resolved-version
check — into a single `settled_step(state, name, lookup_key, spec)`. The
alias is now derived (`lookup_key != spec`) instead of hand-set per call.

No behaviour change: the 7 select unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rsions)

Address review: deciding an edge had to probe three separate maps
(`full.cache`, `full.failures`, `versions_cache`) to learn a package's
status. A package has at most one of those, so fold them into one
enum-keyed map:

    enum PackageVersions { Failed(String), Full(Arc<FullManifest>), List(Arc<VersionsInfo>) }
    state.packages: HashMap<String, PackageVersions>

`select_full_manifest` becomes a single `state.package(name)` lookup + a
`match`, and the "at most one source" invariant is now enforced by the
type. `full.waiters` becomes `package_waiters`.

Also fixes a latent precedence bug: a cached version manifest now resolves
the edge even if the package's full-manifest fetch later failed (the old
order let the package failure shadow a usable cached manifest). New test
pins it.

178 tests pass (8 select unit tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@elrrrrrrr elrrrrrrr added A-Pkg Manager Area: Package Manager benchmark Run pm-bench on PR labels May 28, 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 introduces a new pure, testable edge-selection logic (select_edge) for the demand-driven BFS resolver, refactors the manifest store (ManifestState) to consolidate package version sources, and adds a ManifestProvider abstraction to decouple scheduling from I/O. Additionally, it optimizes native HTTP resolution by fanning out requests across a pool of four reqwest::Clients and supports speculative version extraction during full manifest parsing. Review feedback suggests optimizing resolved_manifests to share Arc allocations across duplicate specs and simplifying the native read_body_vec implementation by using response.bytes().await directly.

Comment on lines +222 to +238
pub(crate) fn resolved_manifests(
&self,
) -> Vec<(String, String, std::sync::Arc<CoreVersionManifest>)> {
let mut out = Vec::new();
for (name, pkg) in &self.cache {
for (spec, version) in &pkg.specs {
if let Some(manifest) = pkg.manifests.get(version) {
out.push((
name.clone(),
spec.clone(),
std::sync::Arc::new(manifest.clone()),
));
}
}
}
out
}
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

In resolved_manifests, if multiple specs resolve to the same version, the manifest is cloned and wrapped in a new Arc for each spec. This defeats the purpose of Arc sharing and increases memory usage and CPU overhead. We can pre-create Arcs for all manifests of a package and share them across specs.

    pub(crate) fn resolved_manifests(
        &self,
    ) -> Vec<(String, String, std::sync::Arc<CoreVersionManifest>)> {
        let mut out = Vec::new();
        for (name, pkg) in &self.cache {
            let version_arcs: std::collections::HashMap<_, _> = pkg
                .manifests
                .iter()
                .map(|(version, manifest)| (version, std::sync::Arc::new(manifest.clone())))
                .collect();
            for (spec, version) in &pkg.specs {
                if let Some(arc) = version_arcs.get(version) {
                    out.push((
                        name.clone(),
                        spec.clone(),
                        std::sync::Arc::clone(arc),
                    ));
                }
            }
        }
        out
    }

Comment on lines +266 to +286
#[cfg(not(target_arch = "wasm32"))]
async fn read_body_vec(mut response: reqwest::Response) -> Result<Vec<u8>, FetchError> {
let capacity = response
.content_length()
.and_then(|len| usize::try_from(len).ok())
.unwrap_or(0);
let mut body = Vec::with_capacity(capacity);
while let Some(chunk) = response.chunk().await.map_err(classify_reqwest_error)? {
body.extend_from_slice(&chunk);
}
Ok(body)
}

#[cfg(target_arch = "wasm32")]
async fn read_body_vec(response: reqwest::Response) -> Result<Vec<u8>, FetchError> {
response
.bytes()
.await
.map(|bytes| bytes.to_vec())
.map_err(classify_reqwest_error)
}
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 platform-specific chunk-looping implementation for non-wasm32 can be simplified by using response.bytes().await directly, which is highly optimized and handles pre-allocation internally. This allows removing the #[cfg] blocks and unifying the implementation for both native and WASM targets.

async fn read_body_vec(response: reqwest::Response) -> Result<Vec<u8>, FetchError> {
    response
        .bytes()
        .await
        .map(|bytes| bytes.to_vec())
        .map_err(classify_reqwest_error)
}

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 elrrrrrrr force-pushed the perf/pm-resolver-provider branch from c4430b2 to c16893e Compare May 28, 2026 05:34
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>
@github-actions
Copy link
Copy Markdown

📊 pm-bench-phases · ef2c1b9 · 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 8.78s 0.21s 9.85s 9.72s 774M 318.9K
utoo-next 7.79s 0.09s 9.91s 11.63s 885M 116.8K
utoo-npm 8.80s 0.71s 10.13s 12.25s 950M 121.0K
utoo 7.73s 0.18s 9.83s 11.66s 838M 114.9K
PM vCtx iCtx netRX netTX cache node_mod lock
bun 15.3K 17.7K 1.11G 6M 1.77G 1.66G 1M
utoo-next 119.9K 86.4K 1.08G 5M 1.62G 1.61G 2M
utoo-npm 144.7K 100.6K 1.08G 5M 1.62G 1.61G 2M
utoo 115.0K 68.2K 1.08G 5M 1.62G 1.61G 2M

p1_resolve

PM wall ±σ user sys RSS pgMinor
bun 1.87s 0.03s 4.01s 1.10s 532M 163.7K
utoo-next 2.78s 0.08s 4.93s 1.75s 611M 82.8K
utoo-npm 2.94s 0.08s 5.17s 2.11s 621M 78.0K
utoo 2.79s 0.05s 4.96s 1.76s 614M 83.7K
PM vCtx iCtx netRX netTX cache node_mod lock
bun 8.0K 4.9K 205M 3M 109M - 1M
utoo-next 47.8K 67.8K 202M 2M 7M 3M 2M
utoo-npm 72.2K 89.2K 202M 2M 7M 3M 2M
utoo 48.1K 67.5K 202M 2M 7M 3M 2M

p3_cold_install

PM wall ±σ user sys RSS pgMinor
bun 6.47s 0.19s 5.90s 9.49s 587M 190.7K
utoo-next 5.52s 0.06s 4.73s 10.20s 441M 61.9K
utoo-npm 6.56s 2.01s 4.84s 10.44s 493M 64.1K
utoo 6.44s 1.58s 4.79s 10.36s 490M 60.8K
PM vCtx iCtx netRX netTX cache node_mod lock
bun 4.8K 6.2K 934M 4M 1.67G 1.67G 1M
utoo-next 92.1K 46.4K 904M 2M 1.61G 1.61G 2M
utoo-npm 108.8K 47.7K 904M 3M 1.61G 1.61G 2M
utoo 103.4K 51.5K 904M 3M 1.61G 1.61G 2M

p4_warm_link

PM wall ±σ user sys RSS pgMinor
bun 3.43s 0.09s 0.18s 2.48s 134M 33.2K
utoo-next 2.38s 0.05s 0.48s 3.81s 79M 18.6K
utoo-npm 2.39s 0.02s 0.49s 3.79s 80M 18.5K
utoo 2.21s 0.06s 0.48s 3.81s 79M 18.7K
PM vCtx iCtx netRX netTX cache node_mod lock
bun 229 21 5M 26K 1.82G 1.64G 1M
utoo-next 40.5K 17.5K 2K 4K 1.61G 1.61G 2M
utoo-npm 41.6K 18.5K 3K 6K 1.61G 1.61G 2M
utoo 42.2K 19.3K 4K 27K 1.61G 1.61G 2M

npmmirror.com: no output captured.

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