Skip to content

perf(pacquet): batch/prefetch packument fetches during fresh-resolve tree walk #11900

Description

@zkochan

Summary

In the fresh-resolve install path (no --frozen-lockfile, no usable lockfile), pacquet's dep-tree walker fetches package metadata serially as it recurses. pnpm pipelines/batches the same fetches, so by the time the walker reaches a node its packument is already in memory. The vlt.sh clean / cache / node_modules / lockfile variations exercise this path and pacquet is 4-12× slower than pnpm on deep-tree fixtures.

Benchmark evidence

From benchmarks.vlt.sh/latest/chart-data.json — selected rows where the lockfile is wiped or stale (pacquet 0.2.8 vs pnpm 11.2.2):

Variation Fixture pnpm pacquet ratio
clean astro 2.87s 18.33s 6.4× slower
cache astro 1.48s 11.22s 7.6× slower
cache+node_modules astro 2.42s 9.31s 3.8× slower
node_modules astro 3.41s 16.56s 4.9× slower
lockfile astro 2.79s 13.17s 4.7× slower
cache+lockfile astro 0.91s 4.03s 4.4× slower

astro is a single astro@^5 dep that pulls hundreds of transitive packages — the worst case for a serial walker.

Current pacquet behavior

pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs walks the dep tree with #[async_recursion]. Each resolve() for a (name, range) consults the per-(registry, name) packument cache initialized at install_with_fresh_lockfile.rs#L298-L334, but if there's no cached packument the fetch happens inline and blocks the recursion at that node.

There is no prefetcher / lookahead — descendants don't get their packuments requested until their parent's resolve() returns. With a tree depth of 10+ and fan-out of 5+, this serializes hundreds of HTTP requests that pnpm makes concurrent.

Proposed approach

Two complementary changes, both modeled on pnpm's resolver:

  1. Prefetch the direct deps' packuments before starting the tree walk. Spawn n tokio tasks for the manifest's direct deps so the first recursion level has its packuments ready in cache.

  2. Lookahead/batched fetch inside the recursion. When resolve() returns a manifest that lists dependencies / optionalDependencies, immediately fire packument fetches for each child name in parallel (de-duplicated via the existing per-(registry, name) mutex map). The recursion then awaits a cache hit instead of a blocking fetch.

pnpm's equivalent is in resolving/npm-resolver/src/index.ts and the prefetcher around installing/deps-resolver/src/resolveDependencyTree.ts.

This is implicitly part of #11633 Stage 2 Tier 1 ("Resolution graph & recursion engine") but worth tracking as a focused perf sub-task with the vlt.sh benchmark numbers attached so progress is measurable.

Expected impact

Closes the astro outlier across all lockfile-wiped variations. Smaller but meaningful gain on large and any other deep-tree resolve.

Related


Written by an agent (Claude Code, claude-opus-4-7).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions