Skip to content

perf(pnpr): collapse the install-accelerator cold path to one round trip#12178

Merged
zkochan merged 4 commits into
mainfrom
perf/12165
Jun 3, 2026
Merged

perf(pnpr): collapse the install-accelerator cold path to one round trip#12178
zkochan merged 4 commits into
mainfrom
perf/12165

Conversation

@zkochan

@zkochan zkochan commented Jun 3, 2026

Copy link
Copy Markdown
Member

What

Reduces the cost of a pnpr install-accelerator install against a remote server, where latency — not bandwidth — dominates (#12165). Lands two of the issue's four experiments and records the other two as evaluated-and-declined.

The old flow was three sequential round trips:

  1. GET /-/pnpr — handshake
  2. POST /v1/install — resolve, return NDJSON (D digests, I index entries, L lockfile)
  3. POST /v1/files — fetch the missing files' contents

At 100ms RTT the bulk of an install is spent waiting on round trips, not transferring data. Two of the three trips are avoidable.

How

  • Experiment 1 — single round trip (ad788234c9). A new inlineFiles request flag: when set, POST /v1/install returns a single gzipped body — a length-prefixed JSON header (lockfile, stats, store-index entries, or verification violations) followed by the missing files' contents as the same binary frames /v1/files serves. The Rust client (pacquet-pnpr-client) drops the handshake from the hot path, sends inlineFiles: true, parses the combined response, and writes the files straight into its CAFS — no follow-up fetch. A shared build_files_payload frames files identically for both endpoints; the previously-uncompressed lockfile is now compressed with the rest.
  • Experiment 2 — better compression (550177c768). The file-bearing responses (/v1/files and the inlined install body) move from gzip level 1 to level 6 (the gzip default).

The legacy NDJSON + /v1/files path is left untouched for clients that don't set the flag (e.g. the TypeScript client), so nothing breaks.

Results

A/B latency probe — same server, same latency-injecting TCP proxy at 50ms one-way, warm server store, 5-run average:

Protocol Time
main (handshake + install + files, 3 RTT) 380.7ms
this PR (single inlined install, 1 RTT) 135.1ms

≈2.8× faster, ~245ms saved — matching the ~2 round trips eliminated. The gain scales with network latency, which is the remote-server cost the issue targets.

Exp 2's gzip bump shrinks the payload ~16% on real package contents (51.1% → 43.0% of uncompressed); level 9 added under a percent for several times the CPU, so level 6 is the sweet spot.

Experiments evaluated

# Experiment Outcome
1 Streaming single round trip Landed — the dominant, latency-scaling win
2 Enhanced compression (gzip 6) Landed — ~16% smaller payload, low risk
3 Bloom-filter the integrity upload Declined — real but niche (only large warm stores on high-latency links), and a false positive would skip files the client lacks, requiring a correctness-critical re-fetch fallback that partly undoes Exp 1
4 Pipelined fetch ↔ transfer streaming Tried, reverted (bb7d652b90ee1a17bd2a) — overlap only pays off on a bandwidth-constrained link; the public npm registry is CDN-backed and latency-bound, and a pnpr accelerator sits close to its clients, so there is nothing to overlap. Under an artificial bandwidth cap the streamed response was slower (per-package gzip flushing transfers more bytes; backpressure stalled fetches)

The throughline: against the real npm registry, latency and round trips are the bottleneck, so the round-trip elimination (Exp 1) is the win that matters. Exp 3 and Exp 4 target bandwidth/upload-size costs that don't bind in practice.

Tests

  • All pnpr + pacquet-pnpr-client tests pass, including the end-to-end integration tests (resolve+download, multi-file, warm-store, lockfile-only, input-lockfile verification accept/reject, trustLockfile) exercising the inline protocol through a real server.
  • Both CLI-level pnpr_install tests pass (full install + node_modules linking).
  • cargo clippy --workspace --all-targets -- --deny warnings and the pre-push dylint/fmt/taplo checks are clean.

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

Summary by CodeRabbit

  • Documentation

    • Updated to document new single round-trip installation behavior with inline file delivery
  • Performance Improvement

    • Installation protocol refactored to complete in a single round-trip with inline file streaming, eliminating separate file fetch requests and reducing overall installation latency
    • Response format optimized with binary encoding for more efficient transmission of file content
  • Tests

    • Updated test suite to validate new inline response parsing and protocol error scenarios

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR implements a one-round-trip protocol redesign for the pnpr install accelerator, replacing a two-request exchange (install + separate file fetch) with a single combined response containing an inline JSON header and gzip-compressed binary file frames. The client now requests inlineFiles: true and receives lockfile, stats, verification violations, and file contents in one payload.

Changes

Inline Files Installation Protocol

Layer / File(s) Summary
Protocol request contract
pnpr/crates/pnpr/src/install_accelerator/protocol.rs
Adds inline_files: bool field to InstallRequest to signal client capability to receive file contents inline in install response.
Server response infrastructure and failure types
pnpr/crates/pnpr/src/install_accelerator.rs
Introduces VerifyFailure enum to classify lockfile verification outcomes (policy violations vs internal errors), centralizes stats serialization via stats_json helper, and adds inline_response/finish_inline_response to construct length-prefixed gzip payloads.
Server install handler with inline response path
pnpr/crates/pnpr/src/install_accelerator.rs
POST /v1/install now detects inline_files request, computes stats early, maps verification failures to appropriate response format (inline header or NDJSON E-line), and returns early with inline_response when inline_files is enabled; otherwise continues through NDJSON path reusing precomputed stats_json.
Server file payload framing and compression
pnpr/crates/pnpr/src/install_accelerator.rs
POST /v1/files refactored to prebuild framed binary payload (digest + size + executable flag + content per file, terminated with 64 zero bytes) using build_files_payload helper, then gzip-compress at level 6 with explicit content-type headers; supports empty payload edge case.
Client install request and inline response parsing
pacquet/crates/pnpr-client/src/lib.rs
PnprClient::install now sets inlineFiles: true, removes handshake() call, reads raw response bytes, conditionally decompresses via magic-byte detection, and parses combined inline response containing JSON header (lockfile, stats, violations, decoded index entries) plus file frame payload; documentation and outcome fields updated to reflect inline file delivery.
Client file payload writing
pacquet/crates/pnpr-client/src/lib.rs
write_files_payload signature simplified to accept only store_dir and binary payload (no digest filtering), early-exits on empty payload, and writes each frame directly via write_cas_file without verifying against explicitly requested digests.
Test updates for inline protocol
pacquet/crates/pnpr-client/src/tests.rs
Test module adds inline_payload helper to frame JSON headers into binary format, replaces NDJSON E-line mocking with inline payload parsing, updates verification and tarball-mismatch tests to call parse_inline_response, and replaces server-error test with protocol-error test (missing lockfile in header).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • pnpm/pnpm#12165: The main changes implement the one-round-trip "inline files" protocol end-to-end (client parsing of length-prefixed JSON header + framed gzip file payload, server inline_response + build_files_payload, and removal of the separate /v1/files flow), which is the exact protocol redesign proposed in the retrieved issue.

Possibly related PRs

  • pnpm/pnpm#12077: Both PRs modify the same pnpr install-accelerator protocol/endpoint wiring (POST /v1/install and the corresponding client/server parsing and /v1/files/inline file delivery behavior) between pnpr-client and the pnpr install accelerator server.
  • pnpm/pnpm#12144: Both PRs change the pnpr install protocol handling of server-rendered lockfile verification violations, moving the pnpr-client/server logic for reconstructing VerifyError from the NDJSON E-line approach to the new inline-header/binary install response parsing in the main PR.
  • pnpm/pnpm#12148: Both PRs change the /v1/install request/response handling in the pnpr install protocol—main PR switches to parsing an inlined binary "inline install payload" (including file frames), while the retrieved PR adds lockfileOnly/lockfile_only resolve-only behavior that deliberately skips streamed file/index data—so the client/server install parsing paths are directly impacted.

Poem

🐰 One request now, instead of two,
Inline files flow straight through,
JSON headers frame the way,
Gzipped bytes in one fine day,
Round trip done—what speed, what grace!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: collapsing the install-accelerator cold path from multiple round trips to one round trip, which is the core objective of this PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/12165

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 and usage tips.

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.00      9.2±0.30ms   469.7 KB/sec    1.00      9.2±0.20ms   469.0 KB/sec

@codecov-commenter

codecov-commenter commented Jun 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.39535% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.47%. Comparing base (69cfcb7) to head (ee1a17b).

Files with missing lines Patch % Lines
pnpr/crates/pnpr/src/install_accelerator.rs 80.76% 25 Missing ⚠️
pacquet/crates/pnpr-client/src/lib.rs 83.33% 7 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12178      +/-   ##
==========================================
- Coverage   87.58%   87.47%   -0.11%     
==========================================
  Files         269      269              
  Lines       30814    30848      +34     
==========================================
- Hits        26987    26983       -4     
- Misses       3827     3865      +38     

☔ View full report in Codecov by Sentry.
📢 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 3, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Each scenario has pacquet rows (direct install) and pnpr rows (the same client through the pnpr install accelerator), so pnpr@HEAD vs pacquet@HEAD is the pnpr-vs-direct ratio. Cold-store scenarios wipe the client store between runs (warm server); hot-store scenarios keep it warm. The pacquet@HEAD rows feed the pacquet Bencher testbed; the pnpr@HEAD rows feed the pnpr testbed.

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.811 ± 0.066 4.720 4.974 2.40 ± 0.06
pacquet@main 4.819 ± 0.080 4.722 4.996 2.40 ± 0.07
pnpr@HEAD 2.007 ± 0.044 1.958 2.084 1.00
pnpr@main 2.014 ± 0.058 1.916 2.118 1.00 ± 0.04
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.8108497562600006,
      "stddev": 0.0660192894710442,
      "median": 4.79593533296,
      "user": 2.52109074,
      "system": 3.7050714000000005,
      "min": 4.72008294246,
      "max": 4.97434067146,
      "times": [
        4.79843669146,
        4.72008294246,
        4.82780433746,
        4.79089227646,
        4.81959774046,
        4.79343397446,
        4.788421452460001,
        4.97434067146,
        4.764631946460001,
        4.83085552946
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.81887776776,
      "stddev": 0.08010608452704074,
      "median": 4.8114920019600005,
      "user": 2.51504884,
      "system": 3.7055178,
      "min": 4.72170864946,
      "max": 4.99609992546,
      "times": [
        4.84212845146,
        4.7930794714600005,
        4.8299045324600005,
        4.99609992546,
        4.764125866460001,
        4.83630287546,
        4.78601897646,
        4.72170864946,
        4.73545423846,
        4.88395469046
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.00710041246,
      "stddev": 0.043549508187170106,
      "median": 2.00524857446,
      "user": 2.6792136399999995,
      "system": 3.2302145,
      "min": 1.95771586146,
      "max": 2.0837555004599997,
      "times": [
        2.0474821934599996,
        1.96393236246,
        2.02328779646,
        1.97566959246,
        1.98720935246,
        1.96203317946,
        2.0310109494599997,
        2.03890733646,
        1.95771586146,
        2.0837555004599997
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.0141192374599997,
      "stddev": 0.05809513350638525,
      "median": 2.0044372279599996,
      "user": 2.66271184,
      "system": 3.2493279,
      "min": 1.91592678846,
      "max": 2.1177160364599996,
      "times": [
        2.00764991946,
        2.1177160364599996,
        1.98471888346,
        2.00016841846,
        2.0012245364599996,
        1.96797142346,
        2.09303407446,
        2.0202330744599997,
        1.91592678846,
        2.03254921946
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 675.6 ± 36.5 651.5 778.0 1.00
pacquet@main 686.7 ± 66.1 650.6 873.7 1.02 ± 0.11
pnpr@HEAD 683.5 ± 46.9 647.9 800.8 1.01 ± 0.09
pnpr@main 738.2 ± 77.3 679.4 939.6 1.09 ± 0.13
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6755881103199999,
      "stddev": 0.036486399163733606,
      "median": 0.66528869632,
      "user": 0.37408302,
      "system": 1.3232981799999999,
      "min": 0.65147287582,
      "max": 0.77798704882,
      "times": [
        0.77798704882,
        0.66062973482,
        0.66932140582,
        0.66736731482,
        0.66321007782,
        0.67193833382,
        0.66156986382,
        0.67088314882,
        0.66150129882,
        0.65147287582
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.68665188552,
      "stddev": 0.06613271726144067,
      "median": 0.66603082732,
      "user": 0.3770805199999999,
      "system": 1.33122678,
      "min": 0.65057836282,
      "max": 0.87374747082,
      "times": [
        0.67562961482,
        0.66460419582,
        0.66745745882,
        0.65057836282,
        0.66304280182,
        0.67224706582,
        0.67392602182,
        0.66281238582,
        0.66247347682,
        0.87374747082
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6835262657200001,
      "stddev": 0.04688303765875721,
      "median": 0.66618790282,
      "user": 0.36823821999999995,
      "system": 1.32630098,
      "min": 0.64791634082,
      "max": 0.80081605882,
      "times": [
        0.72244101982,
        0.66889021682,
        0.65832030982,
        0.69427660782,
        0.66348558882,
        0.67046992982,
        0.65437142482,
        0.65427515982,
        0.64791634082,
        0.80081605882
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.7382476039200001,
      "stddev": 0.07727440499550689,
      "median": 0.71597193982,
      "user": 0.39034262000000003,
      "system": 1.3109495800000002,
      "min": 0.67942669482,
      "max": 0.93955736182,
      "times": [
        0.76851654082,
        0.68541026982,
        0.71261966482,
        0.67942669482,
        0.75751603882,
        0.71932421482,
        0.69791068682,
        0.73903460082,
        0.93955736182,
        0.68315996582
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.201 ± 0.038 2.147 2.270 1.02 ± 0.02
pacquet@main 2.183 ± 0.028 2.153 2.226 1.01 ± 0.02
pnpr@HEAD 2.166 ± 0.024 2.116 2.192 1.00
pnpr@main 2.211 ± 0.023 2.186 2.262 1.02 ± 0.02
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.20051870156,
      "stddev": 0.03806395691216713,
      "median": 2.19915778006,
      "user": 3.52249176,
      "system": 2.9745311,
      "min": 2.14701857006,
      "max": 2.27018531006,
      "times": [
        2.27018531006,
        2.1487907600600002,
        2.19798224706,
        2.22195095406,
        2.2003333130600002,
        2.21890382806,
        2.14701857006,
        2.23506118106,
        2.18299958406,
        2.1819612680600002
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.1832347667600005,
      "stddev": 0.027604929881300987,
      "median": 2.1781010095599997,
      "user": 3.53364756,
      "system": 2.9653338,
      "min": 2.15275805406,
      "max": 2.2263232090600003,
      "times": [
        2.15275805406,
        2.1931049270600003,
        2.15920226906,
        2.16863724306,
        2.1597834000600002,
        2.15668577206,
        2.21461816106,
        2.2263232090600003,
        2.21366985606,
        2.18756477606
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.1660056197600004,
      "stddev": 0.024000803077224263,
      "median": 2.16327776156,
      "user": 3.5253906600000002,
      "system": 2.941669,
      "min": 2.11625871306,
      "max": 2.19191666606,
      "times": [
        2.18186724606,
        2.19191666606,
        2.16149921506,
        2.11625871306,
        2.15782689206,
        2.18919037006,
        2.16318318906,
        2.16337233406,
        2.14356793006,
        2.1913736420600003
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.21066815246,
      "stddev": 0.022931452272449936,
      "median": 2.20813315456,
      "user": 3.5498807599999993,
      "system": 2.9737479999999996,
      "min": 2.18560263806,
      "max": 2.26230642306,
      "times": [
        2.19368093806,
        2.18906622106,
        2.19575215606,
        2.2229912510600003,
        2.21720665006,
        2.22380893806,
        2.21445902606,
        2.20180728306,
        2.26230642306,
        2.18560263806
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.313 ± 0.014 1.285 1.332 1.00
pacquet@main 1.364 ± 0.043 1.320 1.461 1.04 ± 0.03
pnpr@HEAD 1.325 ± 0.018 1.303 1.356 1.01 ± 0.02
pnpr@main 1.337 ± 0.038 1.311 1.439 1.02 ± 0.03
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.3131113968800001,
      "stddev": 0.013723077729394993,
      "median": 1.31233022258,
      "user": 1.42181854,
      "system": 1.71113218,
      "min": 1.28520247858,
      "max": 1.33236761258,
      "times": [
        1.3288113685799998,
        1.32413430158,
        1.31337711658,
        1.31783167358,
        1.3069655945799998,
        1.28520247858,
        1.31128332858,
        1.30555543458,
        1.33236761258,
        1.3055850595799998
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.36440192348,
      "stddev": 0.04309798029904765,
      "median": 1.3518748340799998,
      "user": 1.4701926399999998,
      "system": 1.7531281799999998,
      "min": 1.31998220558,
      "max": 1.46106682558,
      "times": [
        1.32647274358,
        1.31998220558,
        1.39202496058,
        1.38092520058,
        1.46106682558,
        1.3471934665799998,
        1.39069172458,
        1.35655620158,
        1.34141855258,
        1.32768735358
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 1.32454432278,
      "stddev": 0.018098513176581787,
      "median": 1.32109996958,
      "user": 1.4145899400000002,
      "system": 1.71765238,
      "min": 1.3028236305799998,
      "max": 1.35588239058,
      "times": [
        1.31704780858,
        1.30648292658,
        1.32420509558,
        1.3048966175799999,
        1.33522587258,
        1.31799484358,
        1.35588239058,
        1.3028236305799998,
        1.3460597915799999,
        1.3348242505799999
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 1.33749999048,
      "stddev": 0.03772052130560947,
      "median": 1.3212697050799997,
      "user": 1.4591723399999998,
      "system": 1.7225953800000002,
      "min": 1.31077939758,
      "max": 1.43890743658,
      "times": [
        1.3220864315799998,
        1.3165202215799998,
        1.34104195358,
        1.43890743658,
        1.31669959758,
        1.3204529785799999,
        1.3426163195799998,
        1.32016251158,
        1.31077939758,
        1.3457330565799999
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12178
Testbedpacquet

🚨 1 Alert

BenchmarkMeasure
Units
ViewBenchmark Result
(Result Δ%)
Upper Boundary
(Limit %)
isolated-linker.fresh-restore.cold-cache.cold-storeLatency
seconds (s)
📈 plot
🚷 threshold
🚨 alert (🔔)
4.81 s
(+107.60%)Baseline: 2.32 s
2.78 s
(173.00%)

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
2,200.52 ms
(-4.48%)Baseline: 2,303.67 ms
2,764.41 ms
(79.60%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,313.11 ms
(-11.23%)Baseline: 1,479.28 ms
1,775.13 ms
(73.97%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
🚨 view alert (🔔)
4,810.85 ms
(+107.60%)Baseline: 2,317.41 ms
2,780.89 ms
(173.00%)

isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
675.59 ms
(+4.01%)Baseline: 649.57 ms
779.48 ms
(86.67%)
🐰 View full continuous benchmarking report in Bencher

zkochan added 2 commits June 3, 2026 22:19
Collapse the install-accelerator cold path from three sequential round
trips (handshake + /v1/install + /v1/files) to one. With a new
`inlineFiles` request flag, /v1/install returns a single gzipped body —
a length-prefixed JSON header (lockfile, stats, store-index entries, or
verification violations) followed by the missing files' contents as the
binary frames /v1/files already serves. The pacquet client sends the
flag, skips the handshake, and writes the inlined files straight to its
CAFS with no follow-up fetch.

The legacy NDJSON + /v1/files path is unchanged for clients that don't
set the flag. At 50ms one-way latency a warm-store install drops from
~381ms to ~135ms (2.8x).

See #12165.
The install accelerator gzipped its file payloads (both `/v1/files` and
the inlined install body) at level 1. Level 6 — the gzip default —
shrinks the payload ~16% on real package contents (51.1% -> 43.0% of the
uncompressed size in a fixture measurement), while level 9 buys under a
percent more for several times the CPU. Fewer bytes means fewer TCP
slow-start round trips once the server is across a latency link, which
is the cost this accelerator exists to cut.

Experiment 2 of #12165.
The inlineFiles install response previously buffered: the server fetched
every uncached tarball into its store, computed the diff, built the whole
body, then replied. Now the response is a gzip stream of length-prefixed
frames (lockfile, store-index entries, files, stats, error). A producer
resolves each client-missing package — reading the file index straight
from the download for uncached packages, or from a store snapshot for
ones the server already holds — and streams its frames the moment that
package is ready, while later tarballs are still downloading. The client
parses frames and writes files to its CAFS as they arrive.

This lets the server's upstream tarball fetch overlap the transfer to the
client instead of running strictly before it, the win the install
accelerator targets for large cold-server installs. On the small in-repo
fixtures the overlap is within noise (too little to transfer); the real
effect is measured by the CI install-accelerator benchmark. No regression
on warm or cold paths in local measurement.

Drops the per-entry base64 on store-index entries (now sent as raw bytes
in 'I' frames). Adds 'DownloadTarballToStore::run_without_mem_cache_with_index'
so the streaming path gets the file index a fresh fetch already computed
without reading it back through the open writer.

Experiment 4 of #12165.
Reverts the streaming install response from
bb7d652.

Experiment 4 overlapped the server's upstream tarball fetch with
streaming files to the client. That only pays off on a
bandwidth-constrained link, where transferring files takes enough
wall-clock time to hide under the fetch. The public npm registry is
CDN-backed and does not throttle bandwidth — it is latency-bound — and a
pnpr accelerator is typically deployed close to its clients, so neither
the fetch nor the transfer is bandwidth-bound in practice. There is
essentially nothing to overlap.

Measured against a synthetic large fixture behind an artificial 8 MiB/s
cap (the only way to even create the scenario), streaming was *slower*
than the buffered response: per-package gzip flushing transfers more
bytes, which is strictly worse on the bandwidth-limited link it is meant
to help, and the producer stalled fetches while blocked on backpressure.

The win that matters against npm is round-trip reduction (the inlined
single-round-trip response), not fetch/transfer overlap. Reverting keeps
that win and drops the streaming complexity (the tarball API addition,
the store-index Clone derives, the streaming module, and the framed
client protocol).

See #12165.
@zkochan zkochan marked this pull request as ready for review June 3, 2026 21:20
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Review Summary by Qodo

Collapse pnpr install to single round trip with inlined files

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Collapse install-accelerator cold path from 3 round trips to 1
  - Eliminates handshake and separate /v1/files fetch
  - Server inlines missing files directly in install response
• Implement inlineFiles request flag for combined response protocol
  - Single gzipped body with length-prefixed JSON header + binary frames
  - Header carries lockfile, stats, store-index entries, violations
• Increase gzip compression level from 1 to 6 for file payloads
  - Reduces payload size ~16% with minimal CPU overhead
• Refactor response building to support both NDJSON and inline protocols
  - Legacy NDJSON path unchanged for backward compatibility
  - Shared build_files_payload helper frames files identically
Diagram
flowchart LR
  Client["Client Request<br/>inlineFiles: true"]
  Server["Server<br/>Resolve + Diff"]
  Header["JSON Header<br/>lockfile, stats, index"]
  Files["Binary Frames<br/>missing files"]
  Response["Gzipped Response<br/>header + files"]
  Client -->|POST /v1/install| Server
  Server -->|build| Header
  Server -->|build| Files
  Header -->|combine| Response
  Files -->|combine| Response
  Response -->|1 RTT| Client

Loading

Grey Divider

File Changes

1. pacquet/crates/pnpr-client/src/lib.rs ✨ Enhancement +96/-156

Implement inline files protocol in Rust client

• Remove handshake call from hot path; send inlineFiles: true flag
• Parse combined response with length-prefixed JSON header + binary frames
• Replace separate /v1/files fetch with inline file materialization
• Refactor response parsing from NDJSON to structured binary format
• Update documentation to reflect single round-trip protocol

pacquet/crates/pnpr-client/src/lib.rs


2. pacquet/crates/pnpr-client/src/tests.rs 🧪 Tests +33/-16

Update tests for inline response parsing

• Update tests to use new parse_inline_response function
• Create inline_payload helper to frame JSON headers with empty files
• Adapt verification violation tests to new header-based format
• Add test for missing lockfile protocol error detection

pacquet/crates/pnpr-client/src/tests.rs


3. pnpr/crates/pnpr/src/install_accelerator.rs ✨ Enhancement +210/-60

Implement server-side inline response protocol

• Add inline_response function to build combined single-response body
• Implement finish_inline_response to frame header + files with gzip
• Extract stats_json helper for reuse across response types
• Increase gzip compression level from 1 to 6 for file payloads
• Refactor verify_input_lockfile to return violations separately
• Add violation_response to render errors in requested protocol
• Extract build_files_payload as shared helper for both endpoints
• Add empty_files_payload for lockfile-only and verification responses

pnpr/crates/pnpr/src/install_accelerator.rs


View more (1)
4. pnpr/crates/pnpr/src/install_accelerator/protocol.rs ✨ Enhancement +11/-0

Add inlineFiles request flag to protocol

• Add inline_files boolean field to InstallRequest struct
• Document new flag behavior and protocol benefits
• Explain round-trip reduction from 3 to 1

pnpr/crates/pnpr/src/install_accelerator/protocol.rs


Grey Divider

Qodo Logo

@zkochan zkochan merged commit f06ab5e into main Jun 3, 2026
28 checks passed
@zkochan zkochan deleted the perf/12165 branch June 3, 2026 21:36
zkochan added a commit that referenced this pull request Jun 4, 2026
`POST /v1/files` served any CAFS file by digest with no authentication
and no package identity, so the access gate on `/v1/install` (which is
per package) couldn't cover it — it had to be removed, not gated. It was
already superseded by the single-response inline path (#12178).

* Server: `/v1/install` always answers with the inline gzipped body
  (lockfile + stats + store-index entries + the missing files' contents);
  the NDJSON two-trip path, the `/v1/files` route, `handle_files`, and the
  `FilesRequest`/`is_valid_sha512_hex` helpers are gone.
* TS client + worker: `@pnpm/pnpr.client` now does the one inline request
  and hands the file frames to `@pnpm/worker`'s `writeCafsFiles`, which
  writes them to the CAFS; the `fetchAndWriteCafsFiles` /v1/files fetcher
  is replaced.

Closes the second half of the install-accelerator access work (#12184);
file-bearing responses are now both inline-only and access-gated.

---
Written by an agent (Claude Code, claude-opus-4-8).
zkochan added a commit that referenced this pull request Jun 4, 2026
`POST /v1/files` served any CAFS file by digest with no authentication
and no package identity, so the access gate on `/v1/install` (which is
per package) couldn't cover it — it had to be removed, not gated. It was
already superseded by the single-response inline path (#12178).

* Server: `/v1/install` always answers with the inline gzipped body
  (lockfile + stats + store-index entries + the missing files' contents);
  the NDJSON two-trip path, the `/v1/files` route, `handle_files`, and the
  `FilesRequest`/`is_valid_sha512_hex` helpers are gone.
* TS client + worker: `@pnpm/pnpr.client` now does the one inline request
  and hands the file frames to `@pnpm/worker`'s `writeCafsFiles`, which
  writes them to the CAFS; the `fetchAndWriteCafsFiles` /v1/files fetcher
  is replaced. Error bodies are decompressed before being surfaced, since
  the server also gzips its JSON error responses (e.g. an access denial).

Closes the second half of the install-accelerator access work (#12184);
file-bearing responses are now both inline-only and access-gated.

---
Written by an agent (Claude Code, claude-opus-4-8).
zkochan added a commit that referenced this pull request Jun 4, 2026
`POST /v1/files` served any CAFS file by digest with no authentication
and no package identity, so the access gate on `/v1/install` (which is
per package) couldn't cover it — it had to be removed, not gated. It was
already superseded by the single-response inline path (#12178).

* Server: `/v1/install` always answers with the inline gzipped body
  (lockfile + stats + store-index entries + the missing files' contents);
  the NDJSON two-trip path, the `/v1/files` route, `handle_files`, and the
  `FilesRequest`/`is_valid_sha512_hex` helpers are gone.
* TS client + worker: `@pnpm/pnpr.client` now does the one inline request
  and hands the file frames to `@pnpm/worker`'s `writeCafsFiles`, which
  writes them to the CAFS; the `fetchAndWriteCafsFiles` /v1/files fetcher
  is replaced. Error bodies are decompressed before being surfaced, since
  the server also gzips its JSON error responses (e.g. an access denial).

Verified end to end by `pnpm/test/install/pnpmRegistry.ts` (11 tests:
install / add / remove / workspace through a real pnpr server).

Closes the second half of the install-accelerator access work (#12184);
file-bearing responses are now both inline-only and access-gated.

---
Written by an agent (Claude Code, claude-opus-4-8).
zkochan added a commit that referenced this pull request Jun 4, 2026
…icated /v1/files (#12181)

* fix(pnpr): authorize served packages against pnpr's policy in /v1/install

A content-addressed digest in the install-accelerator store is shared
across packages and says nothing about access, so the store's possession
of a package's bytes is not a capability to receive them. `/v1/install`
served files for any package found in the store, including ones reached
only on the cache-hit / frozen-lockfile path where no access check
happened — letting a caller who knows a private package's digest pull
bytes the registry routes would 401 on.

Check every served package against pnpr's own `packages:` policy before
serving — the same decision `serve_packument` / `serve_tarball` make, in
process, with no network round trip (so a warm shared server keeps its
resolution advantage). `serve_install` resolves the caller's identity
from `Authorization`; `deny_unauthorized_packages` denies the install
(401 anonymous / 403 authenticated-but-outside-the-allowed-set) when any
served package is not readable by the caller.

This authorizes against pnpr's own surface, the authority for everything
the store can hold today (pnpr fetches anonymously, so cached content is
pnpr-hosted or publicly fetchable). When credential forwarding lands,
packages the client resolved from external registries under its own token
carry no pnpr policy and will need per-caller re-verification against the
owning registry (TTL-cached) — noted at the check and tracked in #12184.

The raw `/v1/files` endpoint is still unauthenticated; removing it (it is
superseded by the inline single-response path) is a follow-up (#12184)
that also ports the TS `@pnpm/pnpr.client` + worker off the two-trip path.

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

* fix(pnpr): remove the unauthenticated /v1/files endpoint

`POST /v1/files` served any CAFS file by digest with no authentication
and no package identity, so the access gate on `/v1/install` (which is
per package) couldn't cover it — it had to be removed, not gated. It was
already superseded by the single-response inline path (#12178).

* Server: `/v1/install` always answers with the inline gzipped body
  (lockfile + stats + store-index entries + the missing files' contents);
  the NDJSON two-trip path, the `/v1/files` route, `handle_files`, and the
  `FilesRequest`/`is_valid_sha512_hex` helpers are gone.
* TS client + worker: `@pnpm/pnpr.client` now does the one inline request
  and hands the file frames to `@pnpm/worker`'s `writeCafsFiles`, which
  writes them to the CAFS; the `fetchAndWriteCafsFiles` /v1/files fetcher
  is replaced. Error bodies are decompressed before being surfaced, since
  the server also gzips its JSON error responses (e.g. an access denial).

Verified end to end by `pnpm/test/install/pnpmRegistry.ts` (11 tests:
install / add / remove / workspace through a real pnpr server).

Closes the second half of the install-accelerator access work (#12184);
file-bearing responses are now both inline-only and access-gated.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants