Skip to content

perf(pnpr): restore cold-store install speed on the proxy tarball serve path#12709

Merged
zkochan merged 10 commits into
mainfrom
pnpr-perf
Jun 29, 2026
Merged

perf(pnpr): restore cold-store install speed on the proxy tarball serve path#12709
zkochan merged 10 commits into
mainfrom
pnpr-perf

Conversation

@zkochan

@zkochan zkochan commented Jun 28, 2026

Copy link
Copy Markdown
Member

Summary

Restores pnpr's cold-store install performance, which regressed from
#12570 ("verify proxied tarball integrity"). Bencher's history for the
pnpr testbed shows both cold-store install scenarios stepping up at that commit
from ~2.15s to ~3.15s (the single biggest jump, +666ms, is exactly #12570; later
auth commits added the rest). #12700 recovered the resolution-cache part
(the two hot-store scenarios and ~325ms of cold-store), leaving ~2.82s — still
well above the ~2.15s baseline.

The remaining cost is on the tarball serve path. The benchmark's
registry-mock is pnpr, and a cold-store install pulls ~1300 tarballs through
it, so anything #12570 added to each serve is paid ~1300 times. #12570 added two
things to every serve, including warm cache hits:

  1. An SRI re-hash of the cached .tgz against the version's dist.integrity.
  2. A packument load + parse to bind the request to a version and resolve that
    integrity in the first place — the larger half.

Both are redundant on a cache hit:

  • A tarball only ever enters the proxy cache through download_verified_to_cache,
    which resolves the version and verifies the bytes against dist.integrity as
    they are written
    . The cache only ever holds correct bytes for a given
    (name, filename).
  • Every install client re-verifies the tarball it receives against its own
    expected integrity regardless.

So this serves cached bytes directly: no re-hash, and the packument load is
deferred to the cache-miss download path (which still needs the integrity to
verify freshly-fetched bytes before caching them). The OSV screen on the
filename's version still runs before the cache read, exactly as the pre-#12570
path did.

Security: write-time verification — resolving the version, requiring a
supported dist.integrity, rejecting a poisoned upstream tarball, and never
caching unverified bytes — is fully preserved, so GHSA-5f9g-98vq-2jxw stays
mitigated. This is actually more protective than the pre-#12570 serve path,
which did no write-time verification at all, while matching its performance. The
namespaced /~<uplink>/ route keeps its own sidecar-keyed integrity cache and is
unchanged.

This is a pnpr-only (Rust registry server) change — no TypeScript or pacquet CLI
surface, so nothing to mirror and no changeset (pnpr is not a published npm
package).

Squash Commit Body

perf(pnpr): restore cold-store install speed on the proxy tarball serve path

pnpm/pnpm#12570 made every tarball serve — warm cache hits included — both
re-hash the cached bytes against dist.integrity and load+parse the packument to
bind the request to a version. The benchmark mock is pnpr and a cold-store
install pulls ~1300 tarballs through it, so both costs are paid per tarball:
together the cold-store regression from ~2.15s to ~3.15s on the Bencher pnpr
testbed (pnpm/pnpm#12700 recovered only the resolution-cache part).

Both are redundant on a cache hit. A tarball only enters the proxy cache via
download_verified_to_cache, which resolves the version and verifies the bytes
against dist.integrity as they are written, so the cache only ever holds correct
bytes for a (name, filename); the install client re-verifies whatever it receives
anyway. Serve cached bytes directly and defer the packument load to the
cache-miss download path, which still verifies freshly-fetched bytes before
caching them. The OSV screen on the filename version still runs first.

Write-time verification is preserved, so GHSA-5f9g-98vq-2jxw stays mitigated —
the bytes that enter the cache are still verified, which the pre-regression serve
path did not even do. Drops the now-dead per-serve integrity sidecar and helpers.

Checklist

  • The change is implemented in both the TypeScript CLI and the Rust
    pacquet/ port, or the description notes what still needs porting. (pnpr-only; no CLI surface to mirror.)
  • Added or updated tests. (tampered_upstream_tarball_is_rejected_and_not_cached now asserts write-time rejection; dead sidecar tests removed.)

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

Summary by CodeRabbit

  • New Features
    • Benchmark runs now plan and run per-revision registry mock/proxy processes to better reflect revision-targeted tarball serving and routing.
  • Bug Fixes
    • Improved benchmark cleanup resilience with retrying directory removal to handle transient failures.
    • Improved tarball proxy caching: cache hits serve immediately, while cache misses enforce OSV/integrity based on the resolved distribution and verified downloads.
  • Tests
    • Added an OSV regression test for refusing vulnerable cached tarballs under non-canonical tarball names.
    • Updated existing server/cache tests to reflect the new caching/verification behavior.

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The integrated benchmark now plans and spawns per-revision registry mocks with latency proxies, and pnpr’s tarball serving path no longer reads or writes cached-tarball integrity sidecars. Related storage helpers and tests were removed, and server tests were updated for the new cache behavior.

Changes

Per-revision registry mocks in integrated benchmark

Layer / File(s) Summary
pnpr_command_with_binary extraction
pacquet/tasks/registry-mock/src/pnpr_command.rs
Factors the command-building logic into a new exported pnpr_command_with_binary(bin, port, public_url) helper; pnpr_command now delegates to it via pnpr_binary().
Revision mock planning and registry selection
pacquet/tasks/integrated-benchmark/src/work_env.rs
Introduces RevisionMockRegistry and BenchId::revision(), plans revision mocks in Verdaccio mode, and threads revision_mocks into init, registry_for, and run.
Revision mock spawning and proxy wiring
pacquet/tasks/integrated-benchmark/src/work_env.rs, pacquet/tasks/integrated-benchmark/src/latency_proxy.rs
Adds listener-based LatencyProxy::spawn_with_listener and uses it to spawn revision-specific pnpr mock processes and front them with a latency proxy.
Benchmark cleanup and lifecycle wiring
pacquet/tasks/integrated-benchmark/src/work_env.rs
benchmark retains revision mock guards, replaces pre-run directory deletion with retrying removal, and updates hyperfine prepare cleanup to retry rm -rf per path.

Tarball cache integrity sidecar removal

Layer / File(s) Summary
serve_tarball cache-hit and cache-miss simplification
pnpr/crates/pnpr/src/server.rs
Resolves upstreams before cache handling, serves cache hits directly, derives integrity from the screened tarball dist on cache miss, and drops cached-integrity marker recording.
Storage integrity helpers removed
pnpr/crates/pnpr/src/storage.rs, pnpr/crates/pnpr/src/storage/tests.rs
Removes cached tarball integrity helper methods and deletes the associated tests and sidecar helper.
Cache behavior tests updated
pnpr/crates/pnpr/tests/server.rs
Adds a cache-hit OSV test for a non-canonical tarball filename and updates the tampered-tarball test to assert the tarball is not written to cache.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • pnpm/pnpm#12166: Refactors pacquet/tasks/integrated-benchmark/src/latency_proxy.rs in the same proxy startup area extended here.
  • pnpm/pnpm#12593: Modifies pnpr/crates/pnpr/src/server.rs tarball handling and cache-path logic in the same request flow.
  • pnpm/pnpm#12257: Updates integrated-benchmark Verdaccio proxy/registry plumbing related to the revision-specific mock routing added here.

Suggested labels

product: pacquet, product: pnpr

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pnpr-perf

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.23      8.1±0.95ms   538.0 KB/sec    1.00      6.6±0.22ms   660.2 KB/sec

@codecov-commenter

codecov-commenter commented Jun 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.48936% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.29%. Comparing base (1dd12bd) to head (b1240f9).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
pnpr/crates/pnpr/src/server.rs 91.48% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12709      +/-   ##
==========================================
+ Coverage   85.20%   85.29%   +0.08%     
==========================================
  Files         397      404       +7     
  Lines       61268    61572     +304     
==========================================
+ Hits        52206    52515     +309     
+ Misses       9062     9057       -5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Commit: 4fc41ebb5731

Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD.

Scenario: Isolated linker: fresh restore, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.161 ± 0.277 3.781 4.615 1.94 ± 0.17
pacquet@main 4.731 ± 0.176 4.531 5.041 2.20 ± 0.15
pnpr@HEAD 2.150 ± 0.125 1.983 2.405 1.00
pnpr@main 3.035 ± 0.102 2.946 3.292 1.41 ± 0.09
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.16084052054,
      "stddev": 0.27675121102565586,
      "median": 4.10752481484,
      "user": 3.7569393399999997,
      "system": 3.4924717199999997,
      "min": 3.78060459434,
      "max": 4.61496208734,
      "times": [
        4.25769946634,
        4.61496208734,
        4.2812876943400004,
        4.56231826134,
        4.03577756134,
        4.17883287934,
        3.78060459434,
        4.03621675034,
        4.01894527034,
        3.84176064034
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.73055966534,
      "stddev": 0.17573648263351296,
      "median": 4.69099406234,
      "user": 3.8524516399999995,
      "system": 3.49795622,
      "min": 4.53133841934,
      "max": 5.04091410934,
      "times": [
        4.70203467134,
        5.04091410934,
        4.53810730334,
        4.6799534533400005,
        4.81250079434,
        4.54014371934,
        4.67774439434,
        4.53133841934,
        4.93619453134,
        4.84666525734
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.15029018534,
      "stddev": 0.1251483047953714,
      "median": 2.10570863984,
      "user": 2.6575227399999997,
      "system": 3.0617255199999995,
      "min": 1.9827924723400001,
      "max": 2.40526906634,
      "times": [
        2.40526906634,
        2.05329777034,
        2.15719234634,
        2.28773272734,
        1.9827924723400001,
        2.10866142934,
        2.08957860434,
        2.23446608634,
        2.08115550034,
        2.10275585034
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 3.03517864684,
      "stddev": 0.10240409314525827,
      "median": 2.99788237984,
      "user": 2.73340234,
      "system": 3.05770852,
      "min": 2.94591992934,
      "max": 3.29161001034,
      "times": [
        2.94591992934,
        3.00075984934,
        2.94959966134,
        2.96760973434,
        3.06435934634,
        3.08007609534,
        3.29161001034,
        2.99500491034,
        2.98963713234,
        3.06720979934
      ]
    }
  ]
}

Scenario: Isolated linker: fresh restore, hot cache + hot store

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 663.0 ± 11.4 638.1 680.4 1.00
pacquet@main 671.8 ± 70.8 622.3 864.7 1.01 ± 0.11
pnpr@HEAD 751.5 ± 22.3 721.9 788.9 1.13 ± 0.04
pnpr@main 741.0 ± 59.1 701.6 903.7 1.12 ± 0.09
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.66301842792,
      "stddev": 0.011430521325628485,
      "median": 0.66239593622,
      "user": 0.39397384,
      "system": 1.33938088,
      "min": 0.63805058072,
      "max": 0.68037321372,
      "times": [
        0.66434468172,
        0.63805058072,
        0.65994595172,
        0.65957352772,
        0.66044719072,
        0.65588731672,
        0.68037321372,
        0.6706579597200001,
        0.6714324057200001,
        0.66947145072
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.6717858862200001,
      "stddev": 0.0708251451003117,
      "median": 0.64684206822,
      "user": 0.39397713999999995,
      "system": 1.33710758,
      "min": 0.62229921172,
      "max": 0.86465983572,
      "times": [
        0.6440883457200001,
        0.62229921172,
        0.66116758172,
        0.6427808297200001,
        0.6495957907200001,
        0.63335444472,
        0.64328886472,
        0.65699663472,
        0.86465983572,
        0.6996273227200001
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.7515061862200001,
      "stddev": 0.022302747830847954,
      "median": 0.74923235322,
      "user": 0.40795773999999996,
      "system": 1.4131675799999999,
      "min": 0.72194229672,
      "max": 0.78890307272,
      "times": [
        0.76722816472,
        0.72194229672,
        0.7512794607200001,
        0.73592014772,
        0.74718524572,
        0.73582040272,
        0.73134780072,
        0.7838003707200001,
        0.78890307272,
        0.7516348997200001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.7410405257200001,
      "stddev": 0.0590788995727284,
      "median": 0.7271690767200001,
      "user": 0.41621553999999994,
      "system": 1.3698100799999997,
      "min": 0.70163605072,
      "max": 0.90366145072,
      "times": [
        0.7361627527200001,
        0.71321062272,
        0.7040589997200001,
        0.74368249172,
        0.7231115367200001,
        0.70163605072,
        0.73122661672,
        0.7413165167200001,
        0.90366145072,
        0.71233821872
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.383 ± 0.033 4.337 4.461 1.97 ± 0.11
pacquet@main 4.870 ± 0.059 4.783 4.980 2.19 ± 0.13
pnpr@HEAD 2.223 ± 0.127 2.040 2.442 1.00
pnpr@main 2.849 ± 0.077 2.769 3.026 1.28 ± 0.08
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.383262611759999,
      "stddev": 0.03338612735037397,
      "median": 4.38297818646,
      "user": 3.8846257,
      "system": 3.4131755199999994,
      "min": 4.33714475646,
      "max": 4.46133999646,
      "times": [
        4.46133999646,
        4.38383386846,
        4.37179442046,
        4.35037634446,
        4.369049428459999,
        4.38212250446,
        4.38933351646,
        4.38567587946,
        4.40195540246,
        4.33714475646
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.8701052882599996,
      "stddev": 0.05905926337881084,
      "median": 4.861389707959999,
      "user": 3.9353656,
      "system": 3.42920302,
      "min": 4.783181777459999,
      "max": 4.979907894459999,
      "times": [
        4.979907894459999,
        4.80389072546,
        4.91380115746,
        4.859982716459999,
        4.86279669946,
        4.841388974459999,
        4.783181777459999,
        4.920473288459999,
        4.8991040114599995,
        4.836525637459999
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.2226014432599994,
      "stddev": 0.12667786784614857,
      "median": 2.23932319396,
      "user": 2.5250641000000003,
      "system": 2.98261312,
      "min": 2.03983294346,
      "max": 2.44221058446,
      "times": [
        2.34479785446,
        2.06877710846,
        2.44221058446,
        2.03983294346,
        2.20238895146,
        2.09335705146,
        2.29158248846,
        2.23616132446,
        2.26442106246,
        2.24248506346
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.8487906435600006,
      "stddev": 0.0769785230503732,
      "median": 2.8249017484600003,
      "user": 2.5668422,
      "system": 2.97396252,
      "min": 2.76907604746,
      "max": 3.02640782246,
      "times": [
        2.88105285746,
        3.02640782246,
        2.82353695746,
        2.79116196246,
        2.82626653946,
        2.78163735746,
        2.86375811146,
        2.76907604746,
        2.8134521924600002,
        2.9115565874600002
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, hot cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.392 ± 0.012 1.380 1.412 2.01 ± 0.11
pacquet@main 1.421 ± 0.073 1.370 1.620 2.05 ± 0.15
pnpr@HEAD 0.697 ± 0.026 0.681 0.768 1.00 ± 0.07
pnpr@main 0.694 ± 0.037 0.672 0.797 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.39181140606,
      "stddev": 0.011831796181524244,
      "median": 1.38826238456,
      "user": 1.38426744,
      "system": 1.70740646,
      "min": 1.37965123506,
      "max": 1.41228238106,
      "times": [
        1.41228238106,
        1.41180820506,
        1.37965123506,
        1.38361546806,
        1.38105441606,
        1.3853603880599998,
        1.39277408006,
        1.39116438106,
        1.3957735950599999,
        1.38462991106
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.42121798196,
      "stddev": 0.07261576702916818,
      "median": 1.39604972606,
      "user": 1.3855946399999999,
      "system": 1.7102042599999998,
      "min": 1.36992026606,
      "max": 1.62037294606,
      "times": [
        1.38738394406,
        1.62037294606,
        1.38744125806,
        1.4310286830599999,
        1.41223857906,
        1.42684821006,
        1.38484648106,
        1.40234792906,
        1.38975152306,
        1.36992026606
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6973124565600002,
      "stddev": 0.02617802823314962,
      "median": 0.6871538295600002,
      "user": 0.35370984,
      "system": 1.3174836599999997,
      "min": 0.6810344570600001,
      "max": 0.7677954090600001,
      "times": [
        0.6879564880600001,
        0.6863511710600001,
        0.6810344570600001,
        0.7677954090600001,
        0.7002381160600001,
        0.6817954300600001,
        0.6812310440600001,
        0.68394532106,
        0.7032911450600001,
        0.6994859840600001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6939318545600001,
      "stddev": 0.03730515415972974,
      "median": 0.68474413706,
      "user": 0.36808663999999996,
      "system": 1.29109236,
      "min": 0.6717218620600001,
      "max": 0.7972096570600001,
      "times": [
        0.6717218620600001,
        0.6791644010600001,
        0.67427837306,
        0.6854378340600001,
        0.6730643010600001,
        0.6857186040600001,
        0.7972096570600001,
        0.68405044006,
        0.7009741160600002,
        0.6876989570600001
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 3.192 ± 0.044 3.130 3.274 4.53 ± 0.11
pacquet@main 3.205 ± 0.059 3.137 3.340 4.54 ± 0.13
pnpr@HEAD 0.705 ± 0.015 0.682 0.732 1.00
pnpr@main 0.711 ± 0.061 0.676 0.881 1.01 ± 0.09
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 3.19188278086,
      "stddev": 0.044491575057182126,
      "median": 3.1907892154599997,
      "user": 1.94335712,
      "system": 2.0077632,
      "min": 3.12951239946,
      "max": 3.2741669934599997,
      "times": [
        3.2741669934599997,
        3.20544951746,
        3.1759129534599997,
        3.17840369246,
        3.23988138046,
        3.1721596224599997,
        3.1329136904599997,
        3.20317473846,
        3.20725282046,
        3.12951239946
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.2049756935600002,
      "stddev": 0.058603781261783124,
      "median": 3.1944073179599997,
      "user": 1.9633478199999999,
      "system": 2.0229043,
      "min": 3.1372338734599996,
      "max": 3.33989838346,
      "times": [
        3.17781552646,
        3.1856413574599998,
        3.19357728546,
        3.1372338734599996,
        3.20158183146,
        3.14601295546,
        3.19523735046,
        3.33989838346,
        3.2620678064599997,
        3.2106905654599998
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.70527640206,
      "stddev": 0.014604565716731376,
      "median": 0.70439825396,
      "user": 0.35953011999999995,
      "system": 1.3412924,
      "min": 0.68235040946,
      "max": 0.73166365546,
      "times": [
        0.7051140464600001,
        0.6951002444600001,
        0.71910235146,
        0.68235040946,
        0.68935133246,
        0.7011034744600001,
        0.70368246146,
        0.7158706744600001,
        0.7094253704600001,
        0.73166365546
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.7111088024600002,
      "stddev": 0.06068804559609077,
      "median": 0.6911490369600001,
      "user": 0.37165452,
      "system": 1.3124242999999998,
      "min": 0.67575317946,
      "max": 0.88083519446,
      "times": [
        0.69994012346,
        0.69288622046,
        0.6894118534600001,
        0.68857791946,
        0.68177612346,
        0.6856215254600001,
        0.88083519446,
        0.7161267234600001,
        0.70015916146,
        0.67575317946
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12709
Testbedpacquet
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
milliseconds (ms)
(Result Δ%)
Upper Boundary
milliseconds (ms)
(Limit %)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
🚷 view threshold
4,383.26 ms
(-7.96%)Baseline: 4,762.30 ms
5,714.75 ms
(76.70%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
3,191.88 ms
(+4.42%)Baseline: 3,056.85 ms
3,668.22 ms
(87.01%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,391.81 ms
(+2.48%)Baseline: 1,358.09 ms
1,629.71 ms
(85.40%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
4,160.84 ms
(-14.10%)Baseline: 4,843.80 ms
5,812.56 ms
(71.58%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
663.02 ms
(+2.75%)Baseline: 645.27 ms
774.33 ms
(85.62%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12709
Testbedpnpr

⚠️ WARNING: No Threshold found!

Without a Threshold, no Alerts will ever be generated.

Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the --ci-only-thresholds flag.

Click to view all benchmark results
BenchmarkLatencymilliseconds (ms)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,222.60 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
705.28 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
697.31 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,150.29 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
751.51 ms
🐰 View full continuous benchmarking report in Bencher

@zkochan zkochan changed the title perf(pnpr): keep cold-store installs fast on warm resolve paths feat(pnpr): serve mirrored public tarballs from pnpr's cache to restore cold-store install speed Jun 29, 2026
@zkochan zkochan closed this Jun 29, 2026
@zkochan

zkochan commented Jun 29, 2026

Copy link
Copy Markdown
Member Author

Findings — closing this PR (no code change needed)

After full diagnosis backed by the integrated-benchmark on CI, main already wins every install scenario, cold-store included — there is no cold-store regression on main to fix. The branch has been reset to main and this PR is being closed.

CI benchmark on main (this PR's last run)

Scenario pnpr@main pacquet@main (direct) win
fresh-install · cold-cache · cold-store 3.04 s 5.00 s ~1.6×
fresh-restore · cold-cache · cold-store 3.09 s 4.89 s ~1.6×
fresh-install · cold-cache · hot-store 0.75 s 3.26 s ~4.4×
fresh-install · hot-cache · hot-store 0.74 s 1.54 s ~2.1×

Why cold-store's win is smaller than hot-store's (and why that's not a regression)

The cold-store scenarios are dominated by per-connection fetch latency + client-side CAS materialization (extracting/writing ~1308 packages), which the hot-store scenarios skip entirely. pnpr's win is resolution offload — it can't make the client's tarball fetches or CAS writes cheaper. So cold-store lands at ~1.6× (the resolution it removes) while hot-store, where downloads/CAS are absent, shows the full ~4.4×.

What I tried, and what the benchmark proved

  1. Original change in this PR (replay sized package frames on the resolution cache hit). CI showed it regressed cold-store (fresh-install.cold-cache.cold-store 2.82 s → 4.71 s) via a redundant download path. Reverted — this is what "much slower" referred to.
  2. Route public tarballs through pnpr's warm cache (so the client fetches from pnpr's uncapped link instead of the bandwidth-capped registry). Implemented and CI-tested: no measurable change (cold-store 2.96 s vs 3.04 s, within noise). cold-store is not bandwidth-bound — the 200 Mbit/s cap never bites for the fixture's small tarballs; the cost is latency + CAS. So this lever doesn't help, and it would reverse feat(pnpr): make resolver cache authorization-aware #12700's stateless-public-tarball design for no gain. Abandoned.

Conclusion

main is already at (or better than) pre-auth performance for every scenario — #12700 fixed the auth-era resolution-cache regression, and the remaining cold-store cost is inherent (downloads + CAS), not a regression. No further legitimate speedup is available without making pnpr cache/serve tarballs and assuming a faster-than-registry client↔pnpr link — an architecture decision, not a bug fix.

(Note: a small, unrelated macOS/APFS robustness fix for the benchmark harness — retry rm -rf on transient "Directory not empty" — is worth landing on its own if you run the benchmark on macOS.)


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

The proxy-cache tarball serve path re-hashed each cached `.tgz` against the
version's `dist.integrity` on every request (#12570). On a cold-store
install the client pulls ~1300 tarballs from pnpr, so the accelerator paid a
full SRI pass per tarball — the cold-store regression visible in Bencher from
the auth-era change cluster onward (~2.1s -> ~2.9s, persistent).

The re-hash is redundant. A tarball only enters the cache through
`download_verified_to_cache`, which verifies the bytes against `dist.integrity`
as they are written, so nothing unverified is ever stored. Every install client
also re-verifies the tarball it receives against that same integrity. Serve the
cached bytes directly instead.

Write-time verification (and rejection of a poisoned upstream tarball, never
caching it) is preserved, so GHSA-5f9g-98vq-2jxw stays mitigated. The
namespaced `/~<uplink>/` route keeps its sidecar-keyed integrity cache and is
unaffected.

Drops the now-dead `record_cached_tarball_integrity`/`discard_cached_tarball`
helpers and the per-serve cached-tarball integrity sidecar.
@zkochan zkochan reopened this Jun 29, 2026
@zkochan zkochan changed the title feat(pnpr): serve mirrored public tarballs from pnpr's cache to restore cold-store install speed perf(pnpr): stop re-verifying cached proxy tarballs on every serve Jun 29, 2026
Removing only the on-read SRI re-hash recovered part of the cold-store
regression but not all of it: #12570 also made every tarball serve load
and parse the package's packument to bind the request to a version and its
`dist.integrity`. On a warm-cache cold-store install (~1300 tarballs pulled from
pnpr) that is a packument parse per tarball — the larger half of the regression.

The cache hit needs neither binding. A tarball only enters the proxy cache
through `download_verified_to_cache`, which resolves the version and verifies the
bytes against `dist.integrity` as they are written, so the cache only ever holds
correct bytes for a given (name, filename); the install client re-verifies
whatever it receives against its own expected integrity regardless. The OSV
screen on the filename's version still runs before the cache read.

So serve cached bytes directly and defer the packument load to the cache-miss
download path, which still needs the integrity to verify the freshly-fetched
bytes before caching them. This matches the pre-regression cache-hit serve cost
while keeping write-time verification (which the pre-regression path lacked), so
the bytes that enter the cache are still verified and GHSA-5f9g-98vq-2jxw stays
mitigated.
@zkochan zkochan changed the title perf(pnpr): stop re-verifying cached proxy tarballs on every serve perf(pnpr): restore cold-store install speed on the proxy tarball serve path Jun 29, 2026
@zkochan

zkochan commented Jun 29, 2026

Copy link
Copy Markdown
Member Author

Benchmark proof: cold-store restored to the pre-#12570 baseline

I bisected the regression on Bencher's pnpr testbed at the commit level. The single biggest jump is exactly #12570:

Commit (Bencher pnpr, fresh-install.cold-cache.cold-store) Latency
v396 886fe57a — last commit before #12570 2,159 ms
v397 1c04a00c#12570 2,825 ms (+666 ms)
plateau after the rest of the auth cluster ~3,150 ms
v467 — #12700 (resolution-cache fix) ~2,825 ms
current main 2,823 ms

#12700 recovered the resolution half but not the tarball-serve half. #12570 added two costs to every tarball serve (warm cache hits included): an SRI re-hash, and — the larger half — a packument load+parse to bind the request to a version. The benchmark mock is pnpr and a cold-store install pulls ~1300 tarballs through it, so both are paid ~1300×.

This PR serves cached bytes directly (no re-hash, no packument load) and defers the packument load to the cache-miss download path, which still verifies freshly-fetched bytes before caching them.

Result on this PR's CI run (Bencher pnpr testbed)

Scenario this PR main baseline direct pacquet
fresh-install · cold-cache · cold-store 2,191 ms 2,823 ms 4,293 ms
fresh-restore · cold-cache · cold-store 2,158 ms ~2,820 ms 3,894 ms

Cold-store is back to the pre-#12570 baseline (~2,159 ms) — a ~630 ms / 23% improvement over current main, restoring the win to ~2× over a direct install. (Both pnpr@HEAD and pnpr@main read through this PR's fixed mock in a single run, so they read equal in the side-by-side table; the regression is only visible against the historical main baseline above, which ran against the re-hashing mock.)

Security

Write-time verification is fully preserved: the download path still resolves the version, requires a supported dist.integrity, rejects a poisoned upstream tarball, and never caches unverified bytes — so GHSA-5f9g-98vq-2jxw stays mitigated. This is strictly more protective than the pre-#12570 serve path, which did no write-time verification at all, while matching its performance. The install client also re-verifies every tarball it receives against its own expected integrity.


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

…from its own pnpr mock

The integrated benchmark fronts all arms with a single registry-mock built
from the PR's HEAD (`just integrated-benchmark` runs `cargo build --release
--bin=pnpr`, and the mock resolves that one `target/release/pnpr`). Tarball
serving — where the pnpr proxy spends its per-request time — happens in that
shared mock, not in the per-revision `pnpr@<rev>` accelerators (which only
offload resolution; clients fetch tarballs from the client registry). So a
tarball-serve change slows or speeds every arm equally and cancels out of the
`pnpr@HEAD` vs `pnpr@main` comparison — the blind spot that let the serve-path
regression in #12570 merge looking flat and only step up on the `main`
trend afterward.

Give each compared revision its own tarball-serving mock, built from that
revision's `pnpr`. `plan_revision_mocks` assigns a client-facing latency-proxy
port to every revision that has a `pnpr@<rev>` target (so its binary is built)
before `init()` bakes the port into each target's `.npmrc`; `benchmark()` then
spawns the mock (after `build()` produced the binary, after `init()` warmed the
shared storage) and fronts it with the same latency + bandwidth link the shared
mock uses. `pacquet@<rev>` and `pnpr@<rev>` of the same revision share that
revision's mock, so the pnpr-vs-direct ratio stays fair within a revision while
the serve-path delta becomes visible across revisions.

All revisions share the one warm runtime storage: serving a warm cache is
read-only (a hit neither re-downloads nor, after #12709, writes a
sidecar), so concurrent mocks don't contend. Non-Verdaccio modes plan no mock
and keep the existing single-registry behavior.

Also retry `rm -rf` on the transient "Directory not empty" macOS/APFS raises
between a just-finished install's store writes settling and the next
iteration's cleanup, so the benchmark can run locally on macOS at all.
@zkochan zkochan marked this pull request as ready for review June 29, 2026 10:54
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

perf(pnpr): skip packument+SRI work when serving cached proxy tarballs
✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

Description

• Serve cached proxy tarballs directly, avoiding per-request packument parse and SRI re-hash.
• Preserve security by verifying tarballs at cache write-time and rejecting poisoned upstream bytes.
• Update integrated benchmark to use per-revision pnpr registry mocks so serve-path changes are
 measurable.
Diagram

graph TD
  A["pnpm client"] --> B["pnpr server"] --> C{"Cache hit?"}
  C -->|"hit"| D[("proxy tarball cache")] --> E["stream cached .tgz"]
  C -->|"miss"| F["load packument"] --> G["download + verify"] --> D
  G --> H["upstream registry"]

  subgraph Legend
    direction LR
    _svc(["Service"]) ~~~ _db[("Storage")] ~~~ _dec{"Decision"}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep a tarball integrity sidecar for cache hits
  • ➕ Retains a cheap on-serve validation gate without re-hashing bytes.
  • ➕ Could still detect unexpected cache corruption independent of write-time checks.
  • ➖ Extra I/O and metadata management on the hot path.
  • ➖ Sidecar can drift or be corrupted, adding complexity and more failure modes.
  • ➖ Still doesn’t justify packument lookup on cache hits; only partially addresses regression.
2. Introduce an in-memory packument/integrity lookup cache for serves
  • ➕ Avoids packument parsing costs on repeated tarball serves while keeping serve-time binding.
  • ➕ Can be tuned/evicted without disk metadata formats.
  • ➖ More memory pressure and cache invalidation complexity.
  • ➖ Still keeps serve-path work that is redundant given verified writes + client verification.
  • ➖ Harder to make benchmark results stable across runs and environments.

Recommendation: Proceed with the PR’s approach: trust cache hits and rely on write-time verification (plus client-side verification) to preserve security while removing per-serve packument parsing and SRI hashing. The alternatives add complexity and/or leave redundant work on the hot path; given the benchmark profile (~1300 cached tarballs per cold-store install), minimizing per-request overhead is the correct trade-off.

Files changed (6) +251 / -160

Enhancement (1) +40 / -78
server.rsServe cached proxy tarballs without packument lookup or re-hashing +40/-78

Serve cached proxy tarballs without packument lookup or re-hashing

• Moves upstream resolution and cache-read earlier and returns cached tarballs directly on hits, skipping both packument parsing and SRI re-verification. Restricts packument lookup to the cache-miss download path where dist.integrity is required to verify bytes before caching; removes now-dead per-serve integrity sidecar helpers.

pnpr/crates/pnpr/src/server.rs

Refactor (1) +0 / -23
storage.rsRemove cached-tarball integrity sidecar APIs +0/-23

Remove cached-tarball integrity sidecar APIs

• Deletes Storage methods for reading/writing cached tarball integrity markers and removing cached tarballs tied to that sidecar. This matches the new server behavior which no longer depends on per-serve integrity sidecars.

pnpr/crates/pnpr/src/storage.rs

Tests (2) +7 / -48
tests.rsDrop tests covering removed tarball integrity sidecar behavior +0/-43

Drop tests covering removed tarball integrity sidecar behavior

• Removes helper path construction and test cases that validated tarball integrity sidecar round-tripping and sidecar deletion on tarball removal. Remaining tests continue to cover packument caching behavior and other storage semantics.

pnpr/crates/pnpr/src/storage/tests.rs

server.rsAssert poisoned upstream tarballs are rejected before caching +7/-5

Assert poisoned upstream tarballs are rejected before caching

• Updates the tampered-upstream tarball test to assert that mismatched dist.integrity bytes are rejected during cache write, ensuring the poisoned tarball is never written to disk. This aligns the security invariant with the new 'trust cache hits' serve behavior.

pnpr/crates/pnpr/tests/server.rs

Other (2) +204 / -11
work_env.rsAdd per-revision pnpr registry mocks and more robust cleanup +184/-9

Add per-revision pnpr registry mocks and more robust cleanup

• Extends the integrated benchmark harness to plan and spawn a tarball-serving mock per compared pnpr revision, routed via per-revision .npmrc registry URLs to avoid a shared-mock blind spot. Also adds retry logic for directory cleanup on macOS/APFS and threads revision-mock planning through init/benchmark flows.

pacquet/tasks/integrated-benchmark/src/work_env.rs

pnpr_command.rsAllow starting registry-mock with an explicit pnpr binary path +20/-2

Allow starting registry-mock with an explicit pnpr binary path

• Refactors pnpr mock command construction so callers can provide a specific pnpr binary (used by the benchmark to run mocks built from each revision). Keeps the existing convenience wrapper that resolves the workspace pnpr binary.

pacquet/tasks/registry-mock/src/pnpr_command.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pacquet/tasks/integrated-benchmark/src/work_env.rs`:
- Around line 842-847: The planned mock port in the RevisionMockRegistry setup
is selected too early and left unreserved until start_revision_mock() runs, so
another process can steal it before LatencyProxy::spawn_on binds. Update the
work_env.rs flow around mocks.entry(...), RevisionMockRegistry, and
start_revision_mock() to keep a listener/guard alive from the planning step
through init/build, or otherwise reserve the chosen port until the mock server
actually starts.

In `@pnpr/crates/pnpr/src/server.rs`:
- Around line 1200-1204: The cached tarball fast path in
server::tarball_response logic can bypass uplink priority because
should_read_cache is driven by any(|u| u.caches()), allowing a lower-priority
cacheable uplink to shadow an earlier non-caching uplink. Update the cache-read
decision to preserve the same provenance/order contract as the upstream fallback
loop, using the highest-priority applicable uplink or an explicit origin-aware
cache check before calling state.inner.storage.open_cached_tarball.
- Around line 1188-1204: The cached tarball fast path in server.rs bypasses
authoritative-version OSV screening by returning bytes from open_cached_tarball
after only checking the filename-derived version. Update the cache-hit path
around resolve_upstreams, should_read_cache, and tarball_response so the
resolved packument version is still validated before serving cached content,
either by storing lightweight resolved-version metadata with the cache entry or
by falling back to packument lookup when that metadata is absent. Keep the
existing OSV check on the canonical resolved version consistent with the miss
path, and add tests covering cached non-canonical or stale entries being
blocked.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: dd55b384-a282-4639-bbb2-b20dfaafd951

📥 Commits

Reviewing files that changed from the base of the PR and between 4cd45a3 and 095a408.

📒 Files selected for processing (6)
  • pacquet/tasks/integrated-benchmark/src/work_env.rs
  • pacquet/tasks/registry-mock/src/pnpr_command.rs
  • pnpr/crates/pnpr/src/server.rs
  • pnpr/crates/pnpr/src/storage.rs
  • pnpr/crates/pnpr/src/storage/tests.rs
  • pnpr/crates/pnpr/tests/server.rs
💤 Files with no reviewable changes (2)
  • pnpr/crates/pnpr/src/storage.rs
  • pnpr/crates/pnpr/src/storage/tests.rs

Comment thread pacquet/tasks/integrated-benchmark/src/work_env.rs Outdated
Comment thread pnpr/crates/pnpr/src/server.rs
Comment thread pnpr/crates/pnpr/src/server.rs
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 29, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (6) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. OSV gate skipped on hit ✓ Resolved 🐞 Bug ⛨ Security
Description
In serve_tarball, a proxy-cache hit returns the cached .tgz before loading the packument, so
pnpr never resolves the tarball’s authoritative version and can skip the second
ensure_osv_allowed() check when that resolved version differs from the filename-derived version.
This can serve a tarball for a now-blocked OSV version after an OSV DB update/restart if the cached
tarball’s filename version is different/non-authoritative.
Code

pnpr/crates/pnpr/src/server.rs[R1188-1215]

+    let upstreams = resolve_upstreams(state, &name);
+    // Serve a cached tarball directly — without re-deriving its version from the
+    // packument or re-hashing the bytes. A tarball only enters the cache through
+    // `download_verified_to_cache` below, which verifies the bytes against the
+    // version's `dist.integrity` as they are written, so nothing unverified is
+    // ever stored; every install client also re-verifies whatever it receives
+    // against that same integrity. So a cache hit needs neither the packument
+    // lookup (the version/integrity binding) nor an SRI pass — both of which
+    // pnpm/pnpm#12570 added to *every* serve, making warm-cache cold-store pay a
+    // per-tarball packument parse plus hash. The OSV screen on the filename's
+    // version already ran above. Read the cache if any uplink in the chain
+    // mirrors tarballs (or there's no upstream left at all — a leftover mirror).
+    let should_read_cache = upstreams.is_empty() || upstreams.iter().any(|u| u.caches());
+    if should_read_cache {
+        match state.inner.storage.open_cached_tarball(&name, &filename).await {
+            Ok(Some((file, len))) => {
+                return tarball_response(streaming::stream_file(file), Some(len));
+            }
+            Ok(None) => {}
+            Err(err) => {
+                tracing::warn!(?err, package = %name.as_str(), %filename, "tarball cache open failed");
+            }
+        }
+    }
+
+    if upstreams.is_empty() {
+        return not_found();
+    }
Evidence
The new cache-hit early return happens before load_packument_bytes()/expected_tarball_dist() and
therefore before the “re-screen when resolved version differs” OSV check, even though the code
documents that the authoritative version is derived by matching the tarball basename in the
packument.

pnpr/crates/pnpr/src/server.rs[1149-1237]
pnpr/crates/pnpr/src/server.rs[1289-1344]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`serve_tarball()` serves cached proxy tarballs before resolving the authoritative version from the packument, which can skip the OSV screen for the resolved version (the one whose `dist.tarball` basename matched). This creates a gap where cached tarballs can bypass OSV blocking if the filename-encoded version is not the true version.
### Issue Context
- The handler explicitly acknowledges that the filename-carried version is best-effort and that the authoritative version comes from packument resolution.
- The “re-screen when resolved version differs” logic currently only runs on cache misses.
### Fix Focus Areas
- pnpr/crates/pnpr/src/server.rs[1149-1237]
- pnpr/crates/pnpr/src/server.rs[1289-1344]
### Implementation guidance
- Ensure OSV enforcement uses the authoritative resolved version even on cache hits.
- Minimal/security-first option: when OSV is enabled (`state.inner.osv_index.is_some()`), resolve `TarballDist` from the packument (or an equivalent lightweight metadata cache) before returning a cached tarball, and run `ensure_osv_allowed()` for the resolved version when it differs from `name_version`.
- Performance-preserving option: persist the resolved version (and optionally integrity) as cache metadata at write time, and read that small metadata on cache hit to run the correct OSV check without loading/parsing the full packument.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Mock port race 🐞 Bug ☼ Reliability
Description
start_revision_mock() chooses a mock server port via pick_unused_port(), which does not reserve the
port; another local process can claim it before pnpr binds, causing spawn() to fail/panic and making
benchmarks flaky.
Code

pacquet/tasks/integrated-benchmark/src/work_env.rs[R516-545]

+    fn start_revision_mock(&self, revision: &str, registry: RevisionMockRegistry) -> PnprServer {
+        let binary = self.pnpr_server_binary(revision);
+        assert!(
+            binary.is_file(),
+            "pnpr binary not found at {binary:?} — the build step did not produce it",
+        );
+        let mock_port = pick_unused_port().expect("pick a port for the revision mock");
+
+        let bench_dir = self.bench_dir(BenchId::PnprRevision(revision));
+        let stdout = File::create(bench_dir.join("revision-mock.stdout.log"))
+            .expect("create revision mock stdout log");
+        let stderr = File::create(bench_dir.join("revision-mock.stderr.log"))
+            .expect("create revision mock stderr log");
+
+        eprintln!(
+            "Serving {revision}'s tarballs from a mock built from pnpr@{revision} on 127.0.0.1:{mock_port}...",
+        );
+        // The mock advertises its tarball URLs at the client-facing proxy
+        // URL (`registry.url`), not its own loopback port, so downloads cross
+        // the emulated registry link instead of bypassing it.
+        let process = pacquet_registry_mock::pnpr_command_with_binary(
+            &binary,
+            mock_port,
+            Some(&registry.url),
+        )
+        .stdin(Stdio::null())
+        .stdout(stdout)
+        .stderr(stderr)
+        .spawn()
+        .expect("spawn revision mock");
Evidence
The benchmark selects a port with pick_unused_port() and immediately spawns the mock process on
that port, but pick_unused_port() only probes by binding to :0 and returning the port, dropping
the listener right away—so the returned port is not reserved and is subject to TOCTOU races.

pacquet/tasks/integrated-benchmark/src/work_env.rs[516-545]
pacquet/tasks/registry-mock/src/pick_port.rs[1-7]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`start_revision_mock()` selects `mock_port` using `pick_unused_port()`, which returns an unreserved port number. Between selection and the child process binding, another process can bind the port, causing `.spawn()` to fail and the benchmark to panic.
## Issue Context
The PR already reserves the *proxy* port by binding a `TcpListener` during planning time (`plan_revision_mocks()` + `LatencyProxy::spawn_with_listener()`), but the per-revision *mock server* port still uses an unreserved pick.
## Fix Focus Areas
- pacquet/tasks/integrated-benchmark/src/work_env.rs[516-545]
- pacquet/tasks/registry-mock/src/pick_port.rs[1-7]
## Suggested fix
Wrap mock startup in a small retry loop:
- pick a port
- attempt to spawn pnpr
- if spawn fails with `AddrInUse` (or OS-specific equivalent), retry with a new port up to N times
- otherwise fail fast
(Alternative if feasible: add support in pnpr/launcher to bind to `127.0.0.1:0` and report the chosen port back, avoiding TOCTOU entirely.)

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Mocks use npmjs uplink 🐞 Bug ➹ Performance
Description
Per-revision pnpr registry mocks are spawned without overriding pnpr’s upstream/uplink URL, which
defaults to https://registry.npmjs.org, so any cache miss bypasses the benchmark’s intended
local-registry/network-emulation path. This can make results network-dependent/flaky and distort the
performance delta the benchmark is trying to isolate.
Code

pacquet/tasks/integrated-benchmark/src/work_env.rs[R536-545]

+        let process = pacquet_registry_mock::pnpr_command_with_binary(
+            &binary,
+            mock_port,
+            Some(&registry.url),
+        )
+        .stdin(Stdio::null())
+        .stdout(stdout)
+        .stderr(stderr)
+        .spawn()
+        .expect("spawn revision mock");
Evidence
The benchmark spawns per-revision pnpr mocks via pnpr_command_with_binary without any upstream
override. pnpr’s default proxy config routes all packages through the npmjs uplink at
https://registry.npmjs.org, so cache misses will escape the local registry path and its emulated
link characteristics.

pacquet/tasks/integrated-benchmark/src/work_env.rs[516-563]
pacquet/tasks/registry-mock/src/pnpr_command.rs[75-116]
pnpr/crates/pnpr/src/config.rs[1042-1057]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The integrated benchmark now spawns per-revision `pnpr` proxy mocks, but the spawn path does not override the mock’s uplink/upstream registry. pnpr’s proxy defaults route `**` to the `npmjs` uplink (`https://registry.npmjs.org`), so any cache miss causes real-network fetches that bypass the benchmark’s local Verdaccio/latency/bandwidth model.
### Issue Context
The mock currently configures `--storage`, `--listen`, and `--public-url`, but not the upstream registry/uplink URL. The benchmark uses `RegistryMode::Verdaccio` specifically to keep traffic local and controllable; letting misses escape to npmjs undermines reproducibility.
### Fix Focus Areas
- pacquet/tasks/integrated-benchmark/src/work_env.rs[516-563]
- pacquet/tasks/registry-mock/src/pnpr_command.rs[75-116]
- pnpr/crates/pnpr/src/config.rs[1042-1057]
### What to change
- Extend `pacquet_registry_mock::pnpr_command_with_binary(...)` (or add a new helper) to accept an upstream registry URL (e.g. the benchmark’s `client_registry` or `registry_cache_populator`).
- Generate a small pnpr config file for the revision mock (or modify the default config) that sets the `npmjs` uplink URL to the local registry URL, then pass it via `--config` when spawning the mock.
- Ensure the per-revision mock stays within the same network-emulation topology as other benchmark targets (client→proxy link emulated; proxy→upstream should also be local/controlled).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Mixed registry backends 🐞 Bug ≡ Correctness
Description
When revision mocks are enabled, pacquet@/pnpr@ targets can be routed to per-revision mock URLs,
but pnpm@ and static targets cannot (no revision key), so a single hyperfine run may compare
installs hitting different registries/network paths. This can invalidate cross-target comparisons
within the same run output.
Code

pacquet/tasks/integrated-benchmark/src/work_env.rs[R808-820]

+    fn registry_for<'a>(
+        &'a self,
+        id: BenchId,
+        client_registry: &'a str,
+        revision_mocks: &'a HashMap<String, RevisionMockRegistry>,
+    ) -> &'a str {
+        if id.is_proxy_cache_populator() {
+            return &self.registry_cache_populator;
+        }
+        if let Some(mock) = id.revision().and_then(|rev| revision_mocks.get(rev)) {
+            return &mock.url;
+        }
+        client_registry
Evidence
registry_for() only redirects targets that have a revision key present in revision_mocks.
BenchId::revision() returns None for pnpm/static ids, so those targets always remain on
client_registry even when other targets are routed to per-revision mocks.

pacquet/tasks/integrated-benchmark/src/work_env.rs[798-821]
pacquet/tasks/integrated-benchmark/src/work_env.rs[1814-1822]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The benchmark’s new `registry_for()` routing sends only revision-keyed pacquet/pnpr targets to per-revision registry mocks. pnpm and static targets always stay on `client_registry`, which can make a single benchmark run include targets using different registry backends and link models.
### Issue Context
This is a benchmark-fidelity problem: users may interpret rows as comparable across targets in the same report, but registry/backend differences can dominate (or hide) the actual implementation delta.
### Fix Focus Areas
- pacquet/tasks/integrated-benchmark/src/work_env.rs[203-229]
- pacquet/tasks/integrated-benchmark/src/work_env.rs[808-821]
- pacquet/tasks/integrated-benchmark/src/work_env.rs[1814-1822]
### What to change
- Decide on one of:
- (Preferred) When per-revision mocks are planned, require that the run does not include `pnpm@<rev>` or `--with-pnpm` (fail fast with a clear error), OR
- Route pnpm/static targets through an equivalent mock/proxy so all targets share the same registry backend/link model.
- Add an explicit validation step in `run()` after `plan_revision_mocks()` to enforce the chosen invariant (so mismatched runs fail loudly instead of producing misleading results).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (4)
5. Cache hit skips packument 🐞 Bug ⛨ Security
Description
In serve_tarball, a proxy-cache hit returns the cached tarball without consulting the packument
when OSV is disabled, so the request is no longer re-bound to the current dist.tarball→version
mapping. This can keep serving cached tarballs even after upstream metadata would no longer map that
filename uniquely (e.g., yanks/unpublish or hostile/ambiguous packument changes) until the cache is
cleared.
Code

pnpr/crates/pnpr/src/server.rs[R1217-1224]

// Read the cache if any uplink in the chain mirrors tarballs (or there's
-    // no upstream left at all — a leftover mirror). Reading is harmless: the
-    // bytes are verified against the version's `dist.integrity` before being
-    // served, whichever uplink originally wrote them.
+    // no upstream left at all — a leftover mirror).
let should_read_cache = upstreams.is_empty() || upstreams.iter().any(|u| u.caches());
if should_read_cache {
match state.inner.storage.open_cached_tarball(&name, &filename).await {
  Ok(Some((file, len))) => {
-                let expected = cached_tarball_integrity(&integrity, len);
-                if state
-                    .inner
-                    .storage
-                    .read_cached_tarball_integrity(&name, &filename)
-                    .await
-                    .is_some_and(|cached| cached == expected)
-                {
-                    return tarball_response(streaming::stream_file(file), Some(len));
-                }
-                match streaming::verify_file(file, &integrity).await {
-                    Ok(file) => {
-                        record_cached_tarball_integrity(state, &name, &filename, expected).await;
-                        return tarball_response(streaming::stream_file(file), Some(len));
-                    }
-                    Err(err) => {
-                        let err = tarball_stream_error(err, &name, &filename);
-                        tracing::warn!(?err, package = %name.as_str(), %filename, "cached tarball failed verification");
-                        discard_cached_tarball(state, &name, &filename).await;
-                    }
-                }
+                return tarball_response(streaming::stream_file(file), Some(len));
  }
Evidence
The cache-hit branch returns the cached tarball immediately, and the packument resolution path
(resolve_tarball_dist_and_screenexpected_tarball_dist) is skipped unless OSV is enabled or
the request is a cache miss. expected_tarball_dist contains explicit fail-closed logic requiring a
tarball basename to identify exactly one declaring version, but that logic is bypassed on cache hits
when OSV is disabled.

pnpr/crates/pnpr/src/server.rs[1188-1224]
pnpr/crates/pnpr/src/server.rs[1236-1247]
pnpr/crates/pnpr/src/server.rs[1340-1369]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`serve_tarball()` serves proxy-cache hits without re-checking that the requested tarball filename still maps to exactly one declaring version in the *current* packument when OSV screening is disabled. This removes a fail-closed metadata binding on cache hits, meaning cached tarballs can remain serveable even if upstream metadata would now reject or remap that tarball.
### Issue Context
- Current behavior serves the cached file immediately on hit (fast path).
- Packument-derived binding (via `expected_tarball_dist`) is only performed on cache miss, or on cache hit when OSV is enabled.
- `expected_tarball_dist()` explicitly treats ambiguous `dist.tarball` basenames as hostile/malformed and fails closed.
### Fix Focus Areas
- pnpr/crates/pnpr/src/server.rs[1149-1247]
- pnpr/crates/pnpr/src/server.rs[1340-1370]
### Implementation direction
Re-introduce a metadata binding step for cache hits (without re-hashing the tarball bytes), so that a cache hit is only served if the current packument still uniquely maps `filename` to a version.
A performance-aware approach is to do this binding only when needed (e.g., when a cached packument entry is stale per TTL, or when no cached packument entry exists), so warm-hit performance remains close to the PR’s goal while restoring revocation/ambiguity sensitivity over time.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Overbroad delete retry ✓ Resolved 🐞 Bug ☼ Reliability
Description
remove_dir_all_with_retry() retries fs::remove_dir_all() on every error kind, so persistent
failures (e.g., permissions/IO) incur ~4.2s of sleep per directory before the error is returned.
Because it is used for each benchmark dir’s pre-wipe, a single non-transient failure can
significantly delay benchmark runs/CI while providing no benefit over failing fast for non-retryable
errors.
Code

pacquet/tasks/integrated-benchmark/src/work_env.rs[R1364-1374]

+fn remove_dir_all_with_retry(path: &Path) -> std::io::Result<()> {
+    let mut last_err = None;
+    for attempt in 0..6 {
+        match fs::remove_dir_all(path) {
+            Ok(()) => return Ok(()),
+            Err(_) if !path.exists() => return Ok(()),
+            Err(err) => {
+                last_err = Some(err);
+                thread::sleep(Duration::from_millis(200 * (attempt + 1)));
+            }
+        }
Evidence
The helper retries on the generic Err(err) arm without checking err.kind() or raw_os_error,
and is invoked during the pre-benchmark wipe for each of several directories per target; this can
introduce multi-second delays when the failure is persistent/non-transient.

pacquet/tasks/integrated-benchmark/src/work_env.rs[1362-1376]
pacquet/tasks/integrated-benchmark/src/work_env.rs[427-433]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`remove_dir_all_with_retry()` currently retries `fs::remove_dir_all()` for *any* error kind, which adds avoidable multi-second delays on persistent/non-retryable failures while still ultimately returning the error.
### Issue Context
This helper is called during the pre-benchmark wipe for multiple directories per benchmark target, so delays can accumulate when a deletion fails consistently.
### Fix Focus Areas
- pacquet/tasks/integrated-benchmark/src/work_env.rs[1362-1377]
- pacquet/tasks/integrated-benchmark/src/work_env.rs[427-433]
### What to change
- Restrict retries to known transient conditions (e.g., `io::ErrorKind::DirectoryNotEmpty`, and/or specific `raw_os_error` values like `ENOTEMPTY`/`EBUSY` where relevant).
- For non-retryable errors, return immediately (fail fast) instead of sleeping.
- Keep returning the original/last error, but consider adding attempt/error-kind context to logs/panic messages for faster diagnosis.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Mock proxy port race ✓ Resolved 🐞 Bug ☼ Reliability
Description
plan_revision_mocks() selects listen_port up front but the port is only bound later in
start_revision_mock(), so another process can claim it and make LatencyProxy::spawn_on() fail,
flaking benchmark runs. This is inherent to pick_unused_port() returning an unreserved port and is
newly introduced by deferring the bind until after init() bakes the URL into .npmrc.
Code

pacquet/tasks/integrated-benchmark/src/work_env.rs[R833-849]

+    fn plan_revision_mocks(&self) -> HashMap<String, RevisionMockRegistry> {
+        let mut mocks = HashMap::new();
+        if !matches!(self.registry_mode, RegistryMode::Verdaccio) {
+            return mocks;
+        }
+        for target in &self.targets {
+            if target.kind != TargetKind::Pnpr {
+                continue;
+            }
+            mocks.entry(target.rev.clone()).or_insert_with(|| {
+                let listen_port =
+                    pick_unused_port().expect("pick a port for the revision mock proxy");
+                RevisionMockRegistry {
+                    listen_port,
+                    url: format!("http://127.0.0.1:{listen_port}/"),
+                }
+            });
Evidence
The port is selected in plan_revision_mocks() via pick_unused_port() and only later bound when
spawning the latency proxy on registry.listen_port. Since pick_unused_port() drops the OS
reservation immediately, there is an explicit race window between selection and bind.

pacquet/tasks/integrated-benchmark/src/work_env.rs[827-852]
pacquet/tasks/integrated-benchmark/src/work_env.rs[518-574]
pacquet/tasks/registry-mock/src/pick_port.rs[3-7]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`plan_revision_mocks()` picks a TCP port number for each revision mock proxy early, but does not reserve it; the actual bind happens later. This creates a TOCTOU race where the port can be taken in between, causing benchmark startup to intermittently fail.
### Issue Context
- `pick_unused_port()` binds `127.0.0.1:0` and returns the port after the listener is dropped, so no reservation is held.
- The chosen port is written into `.npmrc` during `init()`, so the code currently needs a stable port value before the proxy exists.
### Fix Focus Areas
- pacquet/tasks/integrated-benchmark/src/work_env.rs[827-852]
- pacquet/tasks/integrated-benchmark/src/work_env.rs[518-574]
- pacquet/tasks/registry-mock/src/pick_port.rs[3-7]
### Suggested fix approaches
Pick one:
1) **Reserve the port while planning**: change `RevisionMockRegistry` to hold a bound `TcpListener` (or similar reservation handle) created during planning; later hand it to `LatencyProxy` or re-use its port without releasing it.
2) **Spawn revision mock proxies before init**: start the latency proxies early (so ports are truly bound) and write their final URLs into `.npmrc` during `init()`; spawn the actual pnpr mock processes later if needed.
3) **Avoid fixed ports**: if feasible, write `.npmrc` after the proxy is spawned (requires moving `.npmrc` generation later or regenerating it).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Cached tarball served unvalidated 🐞 Bug ☼ Reliability
Description
On a proxy-cache hit, serve_tarball streams the cached file directly without verifying it against
dist.integrity or having any eviction path for bad/legacy entries. This makes any pre-existing or
externally modified proxy cache entry a persistent source of client integrity failures/unexpected
bytes until the cache is manually wiped.
Code

pnpr/crates/pnpr/src/server.rs[R1188-1205]

+    let upstreams = resolve_upstreams(state, &name);
+    // Serve a cached tarball directly — without re-deriving its version from the
+    // packument or re-hashing the bytes. A tarball only enters the cache through
+    // `download_verified_to_cache` below, which verifies the bytes against the
+    // version's `dist.integrity` as they are written, so nothing unverified is
+    // ever stored; every install client also re-verifies whatever it receives
+    // against that same integrity. So a cache hit needs neither the packument
+    // lookup (the version/integrity binding) nor an SRI pass — both of which
+    // pnpm/pnpm#12570 added to *every* serve, making warm-cache cold-store pay a
+    // per-tarball packument parse plus hash. The OSV screen on the filename's
+    // version already ran above. Read the cache if any uplink in the chain
+    // mirrors tarballs (or there's no upstream left at all — a leftover mirror).
+    let should_read_cache = upstreams.is_empty() || upstreams.iter().any(|u| u.caches());
+    if should_read_cache {
+        match state.inner.storage.open_cached_tarball(&name, &filename).await {
+            Ok(Some((file, len))) => {
+                return tarball_response(streaming::stream_file(file), Some(len));
+            }
Evidence
The tarball handler returns cached bytes immediately on a cache hit, and the only integrity
verification in the proxy path is during the cache-miss download flow; open_cached_tarball itself
is a raw open intended for the caller to validate before serving.

pnpr/crates/pnpr/src/server.rs[1188-1267]
pnpr/crates/pnpr/src/storage.rs[415-435]
pnpr/crates/pnpr/src/streaming.rs[78-96]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Proxy tarball cache hits are now trusted implicitly and served without any server-side validation or self-healing mechanism. If the on-disk cache contains legacy/untrusted bytes (e.g., carried over from older pnpr versions, manual seeding, or external modification), pnpr will keep serving them indefinitely.
### Issue Context
- New downloads are verified during `download_verified_to_cache()`, but that does not validate already-present files.
- `Storage::open_cached_tarball()` is a raw file open; the prior pattern relied on higher-level validation before serving.
### Fix Focus Areas
- pnpr/crates/pnpr/src/server.rs[1188-1277]
- pnpr/crates/pnpr/src/storage.rs[415-435]
- pnpr/crates/pnpr/src/streaming.rs[78-96]
### Implementation guidance
- Add a backward-compatible self-heal path for proxy cache hits, without reintroducing per-request packument parsing/hashing for the common case.
- For example, write a small per-tarball “verified” metadata marker at cache-write time (after `download_verified_to_cache` succeeds).
- On cache hit, if the marker is missing (legacy entry), fall back to the slower path: load packument, verify once, then either (a) write the marker and serve, or (b) delete the bad cache entry and refetch.
- Optionally also enforce a cheap invariant on cache hits (e.g., cached file size <= `MAX_TARBALL_BYTES`) to avoid serving pathological on-disk entries.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

9. Repeated fixture seeding 🐞 Bug ➹ Performance
Description
Each per-revision mock spawn calls pnpr_command_with_binary(), which unconditionally runs
seed_runtime_storage() and walks the full fixture tree, repeating that work once per revision
mock. With many compared revisions, this adds avoidable benchmark startup overhead (not in the timed
loop, but still impacts total runtime/CI latency).
Code

pacquet/tasks/integrated-benchmark/src/work_env.rs[R538-547]

+        let process = pacquet_registry_mock::pnpr_command_with_binary(
+            &binary,
+            mock_port,
+            Some(&registry.url),
+        )
+        .stdin(Stdio::null())
+        .stdout(stdout)
+        .stderr(stderr)
+        .spawn()
+        .expect("spawn revision mock");
Evidence
start_revision_mock() invokes pnpr_command_with_binary() per revision. That function seeds
runtime storage every time, and seed_runtime_storage() traverses all fixture files using
WalkDir::new(src), making repeated spawns repeat the traversal cost.

pacquet/tasks/integrated-benchmark/src/work_env.rs[518-547]
pacquet/tasks/registry-mock/src/pnpr_command.rs[81-100]
pacquet/tasks/registry-mock/src/seed_storage.rs[16-51]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Per-revision mocks cause `seed_runtime_storage()` to run repeatedly (once per mock spawn). `seed_runtime_storage()` performs a `WalkDir` traversal of the fixture storage each time, which is redundant because the runtime storage location is shared.
### Issue Context
The seeding is idempotent, but it still walks the directory tree on every call.
### Fix Focus Areas
- pacquet/tasks/registry-mock/src/pnpr_command.rs[86-101]
- pacquet/tasks/registry-mock/src/seed_storage.rs[16-52]
- pacquet/tasks/integrated-benchmark/src/work_env.rs[518-547]
### Suggested fix
Add a process-wide guard (e.g., `static OnceLock<()>` / `std::sync::Once`) in `pnpr_command_with_binary()` (or inside `seed_runtime_storage()`) so the expensive traversal is performed at most once per orchestrator process, while preserving correctness for concurrent callers.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Cleanup command duplication ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
build_cleanup_command() now repeats the full {remove_targets} list multiple times in the
generated hyperfine --prepare string, substantially increasing the command length and making it
harder to maintain. This also increases the chance of hitting OS command-line length limits when
benchmarking many targets/paths.
Code

pacquet/tasks/integrated-benchmark/src/work_env.rs[R1396-1398]

+    let mut command = format!(
+        "rm -rf {remove_targets} || (sleep 0.5; rm -rf {remove_targets}) || (sleep 1; rm -rf {remove_targets})",
+    );
Evidence
The cleanup command constructs a single long remove_targets string by joining paths across all
bench dirs, then formats a rm -rf ... || ... rm -rf ... string that embeds remove_targets
repeatedly. Since the CLI accepts an arbitrary number of targets, the command length can grow
significantly with larger comparisons.

pacquet/tasks/integrated-benchmark/src/work_env.rs[1388-1399]
pacquet/tasks/integrated-benchmark/src/cli_args.rs[107-111]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The generated `--prepare` shell command embeds the same long `remove_targets` string multiple times for retry behavior, inflating the final command string.
### Issue Context
- `remove_targets` scales with number of targets (`CliArgs.targets`) and per-scenario cleanup paths.
- hyperfine receives this entire string as an argument, so it contributes directly to argv length.
### Fix Focus Areas
- pacquet/tasks/integrated-benchmark/src/work_env.rs[1388-1408]
- pacquet/tasks/integrated-benchmark/src/cli_args.rs[107-111]
### Suggested fix
Emit a small `cleanup.bash` file into the work env root (or each bench dir) that performs the retry loop internally (without duplicating the long path list in the hyperfine argv), and pass `--prepare "bash cleanup.bash"` to hyperfine.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread pnpr/crates/pnpr/src/server.rs
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 095a408

Serving a cached tarball directly skipped the resolved-version OSV re-screen:
the screen on the filename-carried version still ran, but a non-canonical
tarball name (one whose declaring version differs from the version in its
filename) could let a cached tarball for an OSV-blocked version slip past on an
unblocked filename version — e.g. after an OSV database update that blocks the
resolved version.

When OSV screening is enabled, resolve the tarball's authoritative version from
the packument and re-screen it before trusting cached bytes, exactly as the
cache-miss path does. The work is shared through a new
`resolve_tarball_dist_and_screen` helper and reused on a miss, so an OSV-enabled
serve resolves the packument at most once. When OSV is disabled the resolution
is pure overhead and stays skipped, so the warm-cache cold-store fast path is
unaffected.

Found by Qodo review on #12709.
@zkochan

zkochan commented Jun 29, 2026

Copy link
Copy Markdown
Member Author

Thanks — addressing both Qodo findings.

🐞 Bug 1 — OSV gate skipped on cache hit — fixed in da4d81b

Real gap, good catch. The filename-version OSV screen still ran before the cache read, so canonical npm names (the norm) were covered, but a non-canonical tarball name — one whose declaring version differs from the version in its filename — could let a cached tarball for an OSV-blocked version slip past on an unblocked filename version (e.g. after an OSV DB update).

Fix: when OSV screening is enabled, serve_tarball now resolves the authoritative version from the packument and re-screens it before trusting cached bytes, via a shared resolve_tarball_dist_and_screen helper that the cache-miss path reuses (so an OSV-enabled serve resolves the packument at most once). When OSV is disabled the resolution is pure overhead and stays skipped, so the warm-cache cold-store fast path this PR restores is unaffected. Added a regression test (osv_refuses_vulnerable_cached_tarball_under_noncanonical_name) that serves a 1.0.0 tarball under a foo-0.0.1.tgz name and asserts a cache hit is refused once 1.0.0 is OSV-blocked — it fails without the fix (200) and passes with it (403).

🐞 Bug 2 — cached tarball served without re-verification — working as intended

This is the deliberate core of the PR, not an oversight. Integrity is enforced at the two points that actually protect the client:

  1. Write time — a tarball only enters the proxy cache through download_verified_to_cache, which verifies the bytes against the version's dist.integrity as they're written. Nothing unverified is ever stored by this code path.
  2. Client read time — every install client re-verifies the tarball it receives against its own expected integrity, so a corrupt, legacy, or externally-modified cache entry surfaces as a clear client-side integrity rejection, not silently-wrong bytes.

Re-hashing on every serve (what #12570 added) is exactly the per-tarball SRI pass this PR removes to fix the cold-store regression, so re-introducing it unconditionally would undo the fix. The residual risk is narrow — an entry written by a pre-write-verification pnpr, or one modified out-of-band on disk — and it fails loudly at the client rather than corrupting an install.

A cheap self-heal (evict + refetch when a client reports an integrity failure, or a size-invariant check on hit) would be a reasonable follow-up to recover such entries automatically without paying a per-serve hash; I've left it out of this PR to keep the change focused. Happy to add it as a follow-up if you'd like.


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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pnpr/crates/pnpr/tests/server.rs`:
- Around line 1294-1316: The test currently does not prove the second request is
a cache hit because it can still return FORBIDDEN even if the warm-up never
wrote the tarball to disk. Update the regression in server.rs to explicitly
verify, after the initial warming_app request and before enabling OSV, that the
tarball exists in the proxy cache for the requested package/version, using the
same cache_dir and the cache/tarball handling path exercised by router and
config_for. Keep the final screened_app assertion, but make the test fail if
open_cached_tarball would not be hit on the second request.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4fa2c717-b6b7-4d02-be5e-b81186b7730a

📥 Commits

Reviewing files that changed from the base of the PR and between 095a408 and da4d81b.

📒 Files selected for processing (2)
  • pnpr/crates/pnpr/src/server.rs
  • pnpr/crates/pnpr/tests/server.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • pnpr/crates/pnpr/src/server.rs

Comment thread pnpr/crates/pnpr/tests/server.rs
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit da4d81b

…t plan time

The per-revision mock picked its client-facing proxy port during planning but
did not bind it until `benchmark()` spawned the proxy — after `init()` and a
multi-minute `build()`. Another process could claim the port in that window,
failing the run before it measured anything.

Bind the proxy's listening socket at plan time instead, reserving the port for
the whole init + build window, and hand the live socket to the proxy via the
new `LatencyProxy::spawn_with_listener`. Nothing connects to it before the
benchmark starts, so holding it idle through the build is harmless.

Found by CodeRabbit review on #12709.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit da4d81b

@github-actions github-actions Bot added the reviewed: coderabbit CodeRabbit submitted an approving review label Jun 29, 2026
…-hit screen

The regression test for the resolved-version OSV screen on cache hits could pass
even if the warm-up never wrote the tarball to disk: the OSV-enabled path
resolves and screens the packument before reading the cache, so a request for a
blocked resolved version is refused whether or not it is a genuine cache hit.
Assert the tarball is in the proxy cache after warming so the screened request
actually exercises the cache-hit path the fix guards.

Found by CodeRabbit review on #12709.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 2a0da90

1 similar comment
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 2a0da90

Two follow-ups to the macOS/APFS cleanup retry, from Qodo review on
#12709:

- `remove_dir_all_with_retry` retried every error kind, so a non-transient
  failure (permissions, I/O) slept ~4s before surfacing. Retry only the
  transient `DirectoryNotEmpty` that APFS raises while a store's writes settle,
  and fail fast otherwise.

- `build_cleanup_command` repeated the full removal-target list three times in
  the hyperfine `--prepare` string (once per retry), bloating the command and
  risking the OS arg-length limit with many targets. List the targets once and
  retry per-path with a shell `for` loop (`|| exit 1` still aborts the iteration
  if a path can't be removed), and assemble the command from `&&`-joined parts
  so an empty removal set can't produce a dangling `&& cp`.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Looking for bugs?

Check back in a few minutes. Qodo's review agents are on it.

@zkochan

zkochan commented Jun 29, 2026

Copy link
Copy Markdown
Member Author

Thanks for the re-review on 2a0da90a76. Status of the updated findings:

Fixed in 4fc41eb

  • Overbroad delete retry (Reliability) — remove_dir_all_with_retry now retries only the transient DirectoryNotEmpty that APFS raises while a store's writes settle, and fails fast on any other error kind instead of sleeping ~4s first.
  • Cleanup command duplication (Maintainability) — build_cleanup_command no longer repeats the removal-target list three times. It lists the targets once and retries per-path in a shell for loop (|| exit 1 still aborts the --prepare step if a path can't be removed), and assembles the command from &&-joined parts so an empty removal set can't produce a dangling && cp.

By design (no change)

  • Cache hit skips packument / Cached tarball served unvalidated (Security/Reliability) — this is the core of the PR. When OSV is enabled the resolved version is re-screened on cache hits (the resolved finding above); when OSV is disabled, a warm-cache hit serves directly by design. Integrity is enforced at write time (download_verified_to_cache) and re-verified by the install client against its own expected integrity, so a stale/yanked/tampered entry fails loudly client-side rather than serving silently-wrong bytes. Re-binding the packument on every serve is exactly the per-tarball cost fix(pnpr): verify proxied tarball integrity #12570 added that this PR removes. A self-heal (evict + refetch on a client-reported integrity failure) is a reasonable follow-up but is intentionally out of scope here.

Acknowledged, not changing

  • Repeated fixture seeding (Performance) — each per-revision mock re-runs seed_runtime_storage(), but it's idempotent (it stats and skips existing files), runs only at benchmark startup (not in the timed loop), and the shared storage is already seeded by the upstream mock, so the extra walk is a negligible, non-measured cost. Avoiding it would mean a separate no-seed spawn path for marginal benefit, so I've left it as-is.

Already resolved — OSV gate on cache hit (da4d81b01e) and the mock proxy port race (97b5dce960).


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

zkochan added 2 commits June 29, 2026 16:02
- Drop the refactor-history reference (what #12570 added and why the
  old serve path was slow) from the cache-hit comment in serve_tarball; keep
  the current invariant (write-time + client verification) that explains why a
  hit can serve directly. That history belongs in the commit log.
- Put the "per-revision mock makes a serve-path delta visible (a shared mock
  cancels it out)" rationale in one place — `plan_revision_mocks` — and have
  `registry_for`, `benchmark()`, and `pnpr_command_with_binary` reference or
  state it briefly instead of re-deriving it.
- Drop the "served unscreened before this fix" framing from the OSV cache-hit
  test comment; describe the current behavior instead.
The cache-hit block in serve_tarball spelled out the OSV non-canonical-version
scenario that `osv_refuses_vulnerable_cached_tarball_under_noncanonical_name`
already demonstrates. Tests document behavior; keep only the parts a reader
can't recover from the code — why a hit is safe to serve unverified (write-time
plus client verification) and why OSV still needs the packument — and let the
test carry the rest.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 50920c9

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 29, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

1 similar comment
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 29, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

Extract the "resolve and screen the version when OSV is enabled" gate from
serve_tarball into `screen_cached_tarball_osv`, whose name and doc carry what an
inline comment otherwise had to. The call site keeps only the irreducible why —
that a cache hit is safe to serve without re-hashing (write-time plus client
verification) — and the OSV behavior is documented once on the function.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit e4067a6

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit b1240f9

1 similar comment
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit b1240f9

@zkochan zkochan merged commit a6f04d4 into main Jun 29, 2026
30 checks passed
@zkochan zkochan deleted the pnpr-perf branch June 29, 2026 14:33
KSXGitHub pushed a commit that referenced this pull request Jun 29, 2026
Brings in the filter->recursive promotion (#12726), the `bin`
(#12703) and `repo` (#12702) commands, and a pnpr
cold-store perf fix (#12709).

Resolved one conflict in cli_args/dispatch_query.rs: the publish and bin
handlers and their imports were both added at the same spot, so both were
kept. Adapted recursive publish to #12726's new shared-function
signatures: `discover_workspace_projects` now returns `(projects,
patterns)` and `select_recursive_projects` takes an `AutoExcludeRoot`.
Publish passes `AutoExcludeRoot::Disabled` because it is not in pnpm's
run/exec/add/test root-auto-exclusion set; its private/unnamed
eligibility check drops the workspace root instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtBQzmLLDU3RcGzzCMopPB
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

product: pacquet product: pnpr reviewed: coderabbit CodeRabbit submitted an approving review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants