perf(pacquet): lazy packument hydration, sharded meta cache, and an indexed metadata mirror#12322
Conversation
Code Review by Qodo
1. Trust scan hides evidence
|
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds a lazily-hydrated PackageVersions container, an indexed pacquet-meta-v1 mirror format, and propagates shared Arc across registry, resolver, cache, trust checks, mirror persistence, and CLI; updates related tests. ChangesLazy version hydration with Arc refactor
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
PR Summary by Qodoperf(registry): lazily hydrate packument versions from raw JSON fragments WalkthroughsDescription• Store per-version manifests as raw JSON and hydrate typed manifests only on first access. • Cache hydrated manifests per version slot and tolerate undecodable fragments as absent. • Thread shared Arc through resolver, trust checks, and outdated CLI. Diagramgraph TD
R{{"NPM registry"}} --> P["Package parse"] --> V["PackageVersions (raw slots)"] --> L[("Lazy hydrate cache")] --> M["Arc<PackageVersion>"] --> C["Resolver / trust / CLI"]
V -->|"keys()/contains"| C
V --> S["Mirror serialize (verbatim)"]
subgraph Legend
direction LR
_ext{{"External"}} ~~~ _mod["Module"] ~~~ _cache[("Cache")]
end
High-Level AssessmentThe following are alternative approaches to this PR: 1. Represent versions as serde_json::Value + on-demand typed decode
2. Separate index pass: parse only keys first, fetch per-version JSON later
3. Global LRU cache keyed by (package, version) instead of per-slot OnceLock
Recommendation: Current approach (RawValue fragments + per-slot OnceLock + Arc manifests) is the best fit: it removes the dominant eager-hydration cost without changing I/O patterns, preserves registry bytes for mirror round-trips, and keeps concurrency/ownership simple. The per-slot caching also matches resolver access patterns (many key scans, few manifest hydrations). File ChangesEnhancement (2)
Refactor (7)
Tests (1)
Other (2)
|
Micro-Benchmark ResultsLinux |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
pacquet/crates/registry/src/package_versions/tests.rs (1)
31-50: ⚡ Quick winAdd selector-level regression coverage for undecodable highest versions.
This suite proves
get/iterfail closed, but it doesn’t guardPackage::pinned_version/latestbehavior when the highest semver key (ordist-tags.latest) points to an undecodable fragment. A targeted regression test would lock this contract.🤖 Prompt for 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. In `@pacquet/crates/registry/src/package_versions/tests.rs` around lines 31 - 50, Add a regression test that ensures selector-level APIs treat undecodable highest-version fragments as absent: create a Package like in the existing test but with the highest semver key (and separately a case where "dist-tags"."latest" points) to an undecodable manifest, then assert Package::pinned_version(...) (or the crate's pinned_version method) and Package::latest (or latest method) return None/absent rather than panicking or returning an invalid value; reuse the existing parse_package setup and the "9.9.9" undecodable fragment, calling the package.pinned_version(...) and package.latest() helpers and adding assertions that they are None and do not change iteration/get behavior.
🤖 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/crates/registry/src/package.rs`:
- Around line 146-160: The current pinned_version parses keys to find the
highest satisfying version string but then looks up that key and returns None if
the stored manifest is undecodable, dropping valid lower matches; change the
logic in pinned_version so you iterate over self.versions entries (use
self.versions.iter()), parse each entry's key with
key.parse::<node_semver::Version>(), filter to those that parse and satisfy the
range, and keep the associated value (Arc<PackageVersion>) at selection time;
then pick the maximum parsed Version (or sort) among those already-decoded
entries and return its value, ensuring undecodable slots are treated as absent
and lower valid manifests are returned.
- Around line 164-167: The current latest() method panics when the dist_tags
"latest" target points to a missing/undecodable entry; change latest() to avoid
expect() by returning a non-panicking result (e.g., Option<Arc<PackageVersion>>
or Result<Arc<PackageVersion>, RegistryError>), look up the "latest" tag via
self.dist_tags.get("latest") and then use
self.versions.get(version).cloned().ok_or(...) or .cloned() wrapped in
Some/None, propagate the error type up (or convert to Option) and update all
callers of latest() to handle the new return type; keep references to dist_tags,
versions, and the latest() function name to locate and fix the code.
---
Nitpick comments:
In `@pacquet/crates/registry/src/package_versions/tests.rs`:
- Around line 31-50: Add a regression test that ensures selector-level APIs
treat undecodable highest-version fragments as absent: create a Package like in
the existing test but with the highest semver key (and separately a case where
"dist-tags"."latest" points) to an undecodable manifest, then assert
Package::pinned_version(...) (or the crate's pinned_version method) and
Package::latest (or latest method) return None/absent rather than panicking or
returning an invalid value; reuse the existing parse_package setup and the
"9.9.9" undecodable fragment, calling the package.pinned_version(...) and
package.latest() helpers and adding assertions that they are None and do not
change iteration/get behavior.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 2c274600-f8f7-4ce7-b325-3b67c1056fe5
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (12)
Cargo.tomlpacquet/crates/cli/src/cli_args/outdated.rspacquet/crates/registry/Cargo.tomlpacquet/crates/registry/src/lib.rspacquet/crates/registry/src/package.rspacquet/crates/registry/src/package_versions.rspacquet/crates/registry/src/package_versions/tests.rspacquet/crates/resolving-npm-resolver/src/create_npm_resolution_verifier.rspacquet/crates/resolving-npm-resolver/src/npm_resolver.rspacquet/crates/resolving-npm-resolver/src/pick_package.rspacquet/crates/resolving-npm-resolver/src/pick_package_from_meta.rspacquet/crates/resolving-npm-resolver/src/trust_checks.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Code Coverage
- GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (2)
pacquet/**/Cargo.toml
📄 CodeRabbit inference engine (pacquet/AGENTS.md)
pacquet/**/Cargo.toml: Check whether the workspace already depends on something suitable in[workspace.dependencies]in the rootCargo.tomlbefore adding a new dependency
Keep dependencies at the right level — add a new dependency to the specific crate that needs it, not to the workspace root or shared crate unless multiple crates depend on it
Files:
pacquet/crates/registry/Cargo.toml
pacquet/**/*.rs
📄 CodeRabbit inference engine (pacquet/AGENTS.md)
pacquet/**/*.rs: Log emissions are part of matching pnpm — when porting a function that firespnpm:<channel>events throughglobalLogger,logger.debug(...), orstreamParser.write(...), mirror the call site, payload, and ordering so@pnpm/cli.default-reporterparses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing the brand into a plainStringor&strin Rust
If upstream TypeScript always validates before construction of a branded string, validate in the Rust wrapper too viaTryFrom<String>and/orFromStrand do not provide an infallible public constructor
If upstream TypeScript never validates a branded string, just brand for type-safety in Rust by exposing an infallibleFrom<String>constructor
If upstream TypeScript occasionally constructs a branded string without validation, exposefrom_str_uncheckedin Rust as an escape hatch alongside the validating constructor
Match upstream serde behavior for branded strings crossing JSON, YAML, or INI boundaries by using#[serde(try_from = "String")]for deserialization and#[serde(into = "String")]for serialization
Derive simple conversions for branded strings using#[derive(derive_more::From)]and#[derive(derive_more::Into)]instead of handwritingimplblocks; use manualimplonly when conversion needs custom logic
Model TypeScript string literal unions (like'auto' | 'always' | 'never') as Rustenums instead of newtype wrappers, since the set of valid values is closed
Treat TypeScript string template literal types (like`${string}@${string}`) the same as branded string types in Rust, using a newtype wrapper with validation
Follow the code style guide inCODE_STYLE_GUIDE.md— imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching,pipe-trait, error handling, test layout, and cloning ofArcandRc
Choose owned vs. borrowed parameters to minimize copies; widen to t...
Files:
pacquet/crates/registry/src/lib.rspacquet/crates/resolving-npm-resolver/src/npm_resolver.rspacquet/crates/registry/src/package_versions/tests.rspacquet/crates/cli/src/cli_args/outdated.rspacquet/crates/registry/src/package.rspacquet/crates/resolving-npm-resolver/src/create_npm_resolution_verifier.rspacquet/crates/registry/src/package_versions.rspacquet/crates/resolving-npm-resolver/src/pick_package.rspacquet/crates/resolving-npm-resolver/src/trust_checks.rspacquet/crates/resolving-npm-resolver/src/pick_package_from_meta.rs
🧠 Learnings (4)
📚 Learning: 2026-05-20T19:40:55.051Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11774
File: pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:0-0
Timestamp: 2026-05-20T19:40:55.051Z
Learning: In the pacquet Rust code, ensure the semver implementation uses the `node-semver` crate (not `nodejs-semver`). `node-semver`’s public API does not include a `satisfies_with_prerelease`-style method; prerelease-tolerant matching should be implemented inline by first calling `Range::satisfies`, and when it rejects a prerelease version, retry matching against a stripped `MAJOR.MINOR.PATCH` base of the prerelease version.
Applied to files:
pacquet/crates/registry/src/lib.rspacquet/crates/resolving-npm-resolver/src/npm_resolver.rspacquet/crates/registry/src/package_versions/tests.rspacquet/crates/cli/src/cli_args/outdated.rspacquet/crates/registry/src/package.rspacquet/crates/resolving-npm-resolver/src/create_npm_resolution_verifier.rspacquet/crates/registry/src/package_versions.rspacquet/crates/resolving-npm-resolver/src/pick_package.rspacquet/crates/resolving-npm-resolver/src/trust_checks.rspacquet/crates/resolving-npm-resolver/src/pick_package_from_meta.rs
📚 Learning: 2026-05-22T00:08:44.646Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11837
File: pacquet/crates/resolving-npm-resolver/src/pick_package.rs:33-51
Timestamp: 2026-05-22T00:08:44.646Z
Learning: In the pnpm/pnpm repo’s pacquet Rust crates, do not flag Unicode ellipsis characters (U+2026, `…`) in Rust doc comments (`///` / `/** */`) as a lint violation. The pacquet crate’s `dylint.toml` only enables `perfectionist::derive_ordering`, and the Dylint `unicode-ellipsis` rule is not enabled for this project—so `…` in doc comments is an intentional, repo-consistent style.
Applied to files:
pacquet/crates/registry/src/lib.rspacquet/crates/resolving-npm-resolver/src/npm_resolver.rspacquet/crates/registry/src/package_versions/tests.rspacquet/crates/cli/src/cli_args/outdated.rspacquet/crates/registry/src/package.rspacquet/crates/resolving-npm-resolver/src/create_npm_resolution_verifier.rspacquet/crates/registry/src/package_versions.rspacquet/crates/resolving-npm-resolver/src/pick_package.rspacquet/crates/resolving-npm-resolver/src/trust_checks.rspacquet/crates/resolving-npm-resolver/src/pick_package_from_meta.rs
📚 Learning: 2026-05-20T23:07:58.444Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:07:58.444Z
Learning: When reviewing code in this pacquet Rust port, follow the upstream pnpm compatibility rule: only match pnpm’s behavior exactly. Do not propose review changes that intentionally deviate from pnpm’s documented/observed behavior, even if pnpm appears buggy. If you identify a real bug in pnpm behavior, the review should prioritize fixing it upstream in pnpm first, and avoid implementing a pnpm-behavior workaround here unless the same fix has already landed upstream.
Applied to files:
pacquet/crates/registry/src/lib.rspacquet/crates/resolving-npm-resolver/src/npm_resolver.rspacquet/crates/registry/src/package_versions/tests.rspacquet/crates/cli/src/cli_args/outdated.rspacquet/crates/registry/src/package.rspacquet/crates/resolving-npm-resolver/src/create_npm_resolution_verifier.rspacquet/crates/registry/src/package_versions.rspacquet/crates/resolving-npm-resolver/src/pick_package.rspacquet/crates/resolving-npm-resolver/src/trust_checks.rspacquet/crates/resolving-npm-resolver/src/pick_package_from_meta.rs
📚 Learning: 2026-06-06T18:58:37.156Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12243
File: pacquet/crates/package-manager/src/install_package_by_snapshot.rs:319-322
Timestamp: 2026-06-06T18:58:37.156Z
Learning: When reviewing Rust code, do not assume `matches!(expr, Pattern(_))` will move out of `expr` if `Pattern(_)` contains no by-value bindings. `matches!` desugars to a `match` that auto-borrows the scrutinee for discrimination, so even if `expr` is a non-`Copy` value behind a shared reference (e.g., `&T`), the macro should not move-out of the borrowed data purely due to `matches!`. Treat `matches!(&expr, Pattern(_))` as a readability/clarity improvement, not a correctness requirement. Only flag potential move-out-of-borrow risks when the pattern includes by-value bindings that would require moving the matched value.
Applied to files:
pacquet/crates/registry/src/lib.rspacquet/crates/resolving-npm-resolver/src/npm_resolver.rspacquet/crates/registry/src/package_versions/tests.rspacquet/crates/cli/src/cli_args/outdated.rspacquet/crates/registry/src/package.rspacquet/crates/resolving-npm-resolver/src/create_npm_resolution_verifier.rspacquet/crates/registry/src/package_versions.rspacquet/crates/resolving-npm-resolver/src/pick_package.rspacquet/crates/resolving-npm-resolver/src/trust_checks.rspacquet/crates/resolving-npm-resolver/src/pick_package_from_meta.rs
🔇 Additional comments (6)
pacquet/crates/resolving-npm-resolver/src/pick_package.rs (1)
307-307: LGTM!Also applies to: 746-752, 764-783, 794-822, 827-849, 855-876, 882-897
pacquet/crates/resolving-npm-resolver/src/pick_package_from_meta.rs (1)
142-152: LGTM!Also applies to: 230-241, 307-307, 367-389, 411-416
pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs (1)
535-538: LGTM!pacquet/crates/resolving-npm-resolver/src/create_npm_resolution_verifier.rs (1)
951-955: LGTM!pacquet/crates/resolving-npm-resolver/src/trust_checks.rs (1)
178-178: LGTM!Also applies to: 224-224, 243-243
pacquet/crates/cli/src/cli_args/outdated.rs (1)
149-149: LGTM!Also applies to: 159-178
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #12322 +/- ##
==========================================
+ Coverage 87.69% 87.73% +0.03%
==========================================
Files 290 291 +1
Lines 35809 36034 +225
==========================================
+ Hits 31404 31613 +209
- Misses 4405 4421 +16 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Code review by qodo was updated up to the latest commit 4c28c66 |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
pacquet/crates/resolving-npm-resolver/src/pick_package.rs (1)
88-91:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate stale cache backend docs to match the new implementation.
Line 90 still says the default
InMemoryPackageMetaCacheuses astd Mutex, but the implementation now usesDashMap(Lines 174-194). Please align this trait doc comment so it reflects the current concurrency model.🤖 Prompt for 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. In `@pacquet/crates/resolving-npm-resolver/src/pick_package.rs` around lines 88 - 91, The doc comment stating that the default InMemoryPackageMetaCache uses a std Mutex is stale; update the trait/class doc (the comment starting "Implementations must be safe to call concurrently...") to reflect that the current default InMemoryPackageMetaCache implementation uses DashMap for concurrent access (see InMemoryPackageMetaCache implementation) and describe the resulting concurrency characteristics instead of mentioning a std Mutex or a pending tokio-aware variant.
🤖 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.
Outside diff comments:
In `@pacquet/crates/resolving-npm-resolver/src/pick_package.rs`:
- Around line 88-91: The doc comment stating that the default
InMemoryPackageMetaCache uses a std Mutex is stale; update the trait/class doc
(the comment starting "Implementations must be safe to call concurrently...") to
reflect that the current default InMemoryPackageMetaCache implementation uses
DashMap for concurrent access (see InMemoryPackageMetaCache implementation) and
describe the resulting concurrency characteristics instead of mentioning a std
Mutex or a pending tokio-aware variant.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 12060a3e-f406-4059-981a-a1c9a35f2557
📒 Files selected for processing (1)
pacquet/crates/resolving-npm-resolver/src/pick_package.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
- GitHub Check: Lint and Test (macos-latest)
- GitHub Check: Lint and Test (ubuntu-latest)
- GitHub Check: Lint and Test (windows-latest)
- GitHub Check: Dylint
- GitHub Check: Doc
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Code Coverage
- GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs
📄 CodeRabbit inference engine (pacquet/AGENTS.md)
pacquet/**/*.rs: Log emissions are part of matching pnpm — when porting a function that firespnpm:<channel>events throughglobalLogger,logger.debug(...), orstreamParser.write(...), mirror the call site, payload, and ordering so@pnpm/cli.default-reporterparses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing the brand into a plainStringor&strin Rust
If upstream TypeScript always validates before construction of a branded string, validate in the Rust wrapper too viaTryFrom<String>and/orFromStrand do not provide an infallible public constructor
If upstream TypeScript never validates a branded string, just brand for type-safety in Rust by exposing an infallibleFrom<String>constructor
If upstream TypeScript occasionally constructs a branded string without validation, exposefrom_str_uncheckedin Rust as an escape hatch alongside the validating constructor
Match upstream serde behavior for branded strings crossing JSON, YAML, or INI boundaries by using#[serde(try_from = "String")]for deserialization and#[serde(into = "String")]for serialization
Derive simple conversions for branded strings using#[derive(derive_more::From)]and#[derive(derive_more::Into)]instead of handwritingimplblocks; use manualimplonly when conversion needs custom logic
Model TypeScript string literal unions (like'auto' | 'always' | 'never') as Rustenums instead of newtype wrappers, since the set of valid values is closed
Treat TypeScript string template literal types (like`${string}@${string}`) the same as branded string types in Rust, using a newtype wrapper with validation
Follow the code style guide inCODE_STYLE_GUIDE.md— imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching,pipe-trait, error handling, test layout, and cloning ofArcandRc
Choose owned vs. borrowed parameters to minimize copies; widen to t...
Files:
pacquet/crates/resolving-npm-resolver/src/pick_package.rs
🧠 Learnings (4)
📚 Learning: 2026-05-20T19:40:55.051Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11774
File: pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:0-0
Timestamp: 2026-05-20T19:40:55.051Z
Learning: In the pacquet Rust code, ensure the semver implementation uses the `node-semver` crate (not `nodejs-semver`). `node-semver`’s public API does not include a `satisfies_with_prerelease`-style method; prerelease-tolerant matching should be implemented inline by first calling `Range::satisfies`, and when it rejects a prerelease version, retry matching against a stripped `MAJOR.MINOR.PATCH` base of the prerelease version.
Applied to files:
pacquet/crates/resolving-npm-resolver/src/pick_package.rs
📚 Learning: 2026-05-22T00:08:44.646Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11837
File: pacquet/crates/resolving-npm-resolver/src/pick_package.rs:33-51
Timestamp: 2026-05-22T00:08:44.646Z
Learning: In the pnpm/pnpm repo’s pacquet Rust crates, do not flag Unicode ellipsis characters (U+2026, `…`) in Rust doc comments (`///` / `/** */`) as a lint violation. The pacquet crate’s `dylint.toml` only enables `perfectionist::derive_ordering`, and the Dylint `unicode-ellipsis` rule is not enabled for this project—so `…` in doc comments is an intentional, repo-consistent style.
Applied to files:
pacquet/crates/resolving-npm-resolver/src/pick_package.rs
📚 Learning: 2026-05-20T23:07:58.444Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:07:58.444Z
Learning: When reviewing code in this pacquet Rust port, follow the upstream pnpm compatibility rule: only match pnpm’s behavior exactly. Do not propose review changes that intentionally deviate from pnpm’s documented/observed behavior, even if pnpm appears buggy. If you identify a real bug in pnpm behavior, the review should prioritize fixing it upstream in pnpm first, and avoid implementing a pnpm-behavior workaround here unless the same fix has already landed upstream.
Applied to files:
pacquet/crates/resolving-npm-resolver/src/pick_package.rs
📚 Learning: 2026-06-06T18:58:37.156Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12243
File: pacquet/crates/package-manager/src/install_package_by_snapshot.rs:319-322
Timestamp: 2026-06-06T18:58:37.156Z
Learning: When reviewing Rust code, do not assume `matches!(expr, Pattern(_))` will move out of `expr` if `Pattern(_)` contains no by-value bindings. `matches!` desugars to a `match` that auto-borrows the scrutinee for discrimination, so even if `expr` is a non-`Copy` value behind a shared reference (e.g., `&T`), the macro should not move-out of the borrowed data purely due to `matches!`. Treat `matches!(&expr, Pattern(_))` as a readability/clarity improvement, not a correctness requirement. Only flag potential move-out-of-borrow risks when the pattern includes by-value bindings that would require moving the matched value.
Applied to files:
pacquet/crates/resolving-npm-resolver/src/pick_package.rs
🔇 Additional comments (1)
pacquet/crates/resolving-npm-resolver/src/pick_package.rs (1)
174-185: LGTM!Also applies to: 188-194
Integrated-Benchmark Report (Linux)Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD. Scenario: Isolated linker: fresh restore, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 10.14895907022,
"stddev": 0.13535932001226222,
"median": 10.101273604420001,
"user": 3.484995799999999,
"system": 3.31729076,
"min": 10.011422428420001,
"max": 10.40017307842,
"times": [
10.123563926420001,
10.249765678420001,
10.283154000420001,
10.046638165420001,
10.078983282420001,
10.242341663420001,
10.03434345642,
10.019205022420001,
10.011422428420001,
10.40017307842
]
},
{
"command": "pacquet@main",
"mean": 10.177395697120001,
"stddev": 0.18841431008561577,
"median": 10.098530082420002,
"user": 3.603321799999999,
"system": 3.4358805599999998,
"min": 10.04369395142,
"max": 10.57592223642,
"times": [
10.07411984842,
10.13612290342,
10.070103016420001,
10.10404078642,
10.06754050842,
10.093019378420001,
10.48067891442,
10.57592223642,
10.128715427420001,
10.04369395142
]
},
{
"command": "pnpr@HEAD",
"mean": 5.3739660669200005,
"stddev": 0.11005569832868796,
"median": 5.33922932642,
"user": 2.6232628,
"system": 2.8919311600000004,
"min": 5.27414200742,
"max": 5.65716592642,
"times": [
5.38708023542,
5.3534034864199995,
5.331558054419999,
5.44500857442,
5.3469005984199995,
5.65716592642,
5.30563927142,
5.32401458142,
5.31474793342,
5.27414200742
]
},
{
"command": "pnpr@main",
"mean": 5.37791434542,
"stddev": 0.07693020410749572,
"median": 5.35248788642,
"user": 2.6032692999999996,
"system": 2.90310306,
"min": 5.28637444142,
"max": 5.54892284842,
"times": [
5.47858373342,
5.34809436542,
5.54892284842,
5.35641388842,
5.36100003342,
5.34393415242,
5.37197507242,
5.28637444142,
5.34856188442,
5.33528303442
]
}
]
}Scenario: Isolated linker: fresh restore, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.6637450609000002,
"stddev": 0.013180780607185168,
"median": 0.6651824353000001,
"user": 0.38407778000000004,
"system": 1.3056872,
"min": 0.6469065303000001,
"max": 0.6836442153000001,
"times": [
0.6830866403000001,
0.6683059963000001,
0.6836442153000001,
0.6480208333,
0.6551512273000001,
0.6529429953000001,
0.6469065303000001,
0.6684339343000001,
0.6620588743000001,
0.6688993623000001
]
},
{
"command": "pacquet@main",
"mean": 0.6986614629000002,
"stddev": 0.04283027328249936,
"median": 0.6870851588000001,
"user": 0.39904338000000006,
"system": 1.3458992999999997,
"min": 0.6487529123000001,
"max": 0.7956709373000002,
"times": [
0.6590283323000001,
0.6487529123000001,
0.7956709373000002,
0.7442229363000001,
0.6820792333000001,
0.6864469653,
0.7090320423,
0.6877233523000001,
0.6829440803000001,
0.6907138373000001
]
},
{
"command": "pnpr@HEAD",
"mean": 0.7860048464000001,
"stddev": 0.023321894011010345,
"median": 0.7768085003,
"user": 0.41237308000000006,
"system": 1.3352523999999995,
"min": 0.7614944493000001,
"max": 0.8304547153000001,
"times": [
0.7748881773000001,
0.7778336723000001,
0.7733739733000001,
0.8064063843000001,
0.7792175883000001,
0.8304547153000001,
0.7641721813000001,
0.7614944493000001,
0.8164239943000001,
0.7757833283000001
]
},
{
"command": "pnpr@main",
"mean": 0.8028894355000002,
"stddev": 0.03373847223617222,
"median": 0.7862647298000001,
"user": 0.42310598,
"system": 1.3410217999999998,
"min": 0.7688092133000001,
"max": 0.8648411883000001,
"times": [
0.8159633223000001,
0.8582764933000001,
0.7837910943000002,
0.7793350673000001,
0.7887383653000001,
0.8648411883000001,
0.8052862013000001,
0.7829272903000001,
0.7809261193000001,
0.7688092133000001
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 8.69509315152,
"stddev": 0.06565295926042186,
"median": 8.68818638192,
"user": 3.5854102999999995,
"system": 3.30741786,
"min": 8.59009770042,
"max": 8.80333055142,
"times": [
8.62154689942,
8.68934699642,
8.68702576742,
8.80333055142,
8.75290833142,
8.66625430942,
8.77260804142,
8.59009770042,
8.67638476742,
8.69142815042
]
},
{
"command": "pacquet@main",
"mean": 8.78289499182,
"stddev": 0.02902244516861471,
"median": 8.79123591242,
"user": 3.9435722000000006,
"system": 3.3759075600000004,
"min": 8.71828869342,
"max": 8.81406782942,
"times": [
8.80298746542,
8.77289720942,
8.77746487542,
8.808364430420001,
8.81406782942,
8.75361017042,
8.78990741342,
8.71828869342,
8.79879741942,
8.79256441142
]
},
{
"command": "pnpr@HEAD",
"mean": 5.12924214162,
"stddev": 0.12772183203561363,
"median": 5.07021094992,
"user": 2.4274785,
"system": 2.78094516,
"min": 5.02246941842,
"max": 5.41944841642,
"times": [
5.05467387742,
5.02246941842,
5.05326839442,
5.41944841642,
5.24245348442,
5.09250299342,
5.08574802242,
5.22934556942,
5.04393922342,
5.04857201642
]
},
{
"command": "pnpr@main",
"mean": 5.09422230592,
"stddev": 0.053246322689229936,
"median": 5.08105667192,
"user": 2.4310724,
"system": 2.81011656,
"min": 5.03921762442,
"max": 5.21929737442,
"times": [
5.13556548542,
5.07895215642,
5.0568734304200005,
5.03921762442,
5.21929737442,
5.12300109442,
5.08733708542,
5.08316118742,
5.06603993042,
5.05277769042
]
}
]
}Scenario: Isolated linker: fresh install, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 1.19244784868,
"stddev": 0.04524514694559847,
"median": 1.1849458083800002,
"user": 1.1714078399999999,
"system": 1.7402268200000002,
"min": 1.15021030438,
"max": 1.3111284113800001,
"times": [
1.2116949143800002,
1.15021030438,
1.3111284113800001,
1.1652188093800002,
1.18179784138,
1.1913870423800001,
1.16897941938,
1.16493649738,
1.19103147138,
1.18809377538
]
},
{
"command": "pacquet@main",
"mean": 1.50606618148,
"stddev": 0.03106317473053343,
"median": 1.5005556548800003,
"user": 1.6329497400000001,
"system": 1.83477672,
"min": 1.47378697738,
"max": 1.58333078838,
"times": [
1.58333078838,
1.5186828073800003,
1.49379966538,
1.4871332543800002,
1.5080467423800001,
1.5071438133800001,
1.51612326838,
1.47378697738,
1.4939674963800003,
1.4786470013800002
]
},
{
"command": "pnpr@HEAD",
"mean": 0.6938548239800002,
"stddev": 0.013490312991107714,
"median": 0.69520321388,
"user": 0.35663203999999993,
"system": 1.2828899200000001,
"min": 0.6698163313800001,
"max": 0.71584310738,
"times": [
0.6970342353800001,
0.68544542838,
0.71247267138,
0.6698163313800001,
0.69549757538,
0.68667182338,
0.6845030563800001,
0.6963551583800001,
0.71584310738,
0.69490885238
]
},
{
"command": "pnpr@main",
"mean": 0.69908728138,
"stddev": 0.0168330519148571,
"median": 0.6960881883800001,
"user": 0.35024714,
"system": 1.2924232199999997,
"min": 0.67587454638,
"max": 0.72932897738,
"times": [
0.72932897738,
0.69230397138,
0.7050045633800001,
0.68295670438,
0.72267373638,
0.7040005543800001,
0.6960596853800001,
0.67587454638,
0.69611669138,
0.68655338338
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 4.89301711724,
"stddev": 0.02722789499164887,
"median": 4.9049695705400005,
"user": 1.5899087399999998,
"system": 1.9773394399999997,
"min": 4.83939035454,
"max": 4.92379187554,
"times": [
4.857428752540001,
4.90370017554,
4.90623896554,
4.83939035454,
4.91464366854,
4.92379187554,
4.87473761754,
4.90672187754,
4.910457360540001,
4.89306052454
]
},
{
"command": "pacquet@main",
"mean": 5.069503105640001,
"stddev": 0.08954544430737345,
"median": 5.05763078954,
"user": 2.06637104,
"system": 2.1002849400000008,
"min": 4.9582244735400005,
"max": 5.19864888554,
"times": [
4.97075187554,
4.9582244735400005,
4.97939061054,
5.04973502554,
5.18140542554,
5.0437483395400005,
5.19864888554,
5.07021109354,
5.06552655354,
5.177388773540001
]
},
{
"command": "pnpr@HEAD",
"mean": 0.7292415136400001,
"stddev": 0.016778757615346866,
"median": 0.72922503204,
"user": 0.36008063999999995,
"system": 1.3028663399999998,
"min": 0.7017121255400001,
"max": 0.7659922235400001,
"times": [
0.72414327654,
0.73452336854,
0.73691882054,
0.7341149945400001,
0.7302877545400001,
0.7017121255400001,
0.72816230954,
0.7134783865400001,
0.72308187654,
0.7659922235400001
]
},
{
"command": "pnpr@main",
"mean": 0.7002026153400001,
"stddev": 0.013182275802987331,
"median": 0.6994050380400001,
"user": 0.35404124,
"system": 1.27546034,
"min": 0.6792600725400001,
"max": 0.7223846795400001,
"times": [
0.71248425354,
0.71336379254,
0.6893137865400001,
0.7223846795400001,
0.68974750354,
0.69284610254,
0.70381588654,
0.6968614145400001,
0.6792600725400001,
0.70194866154
]
}
]
} |
|
| Branch | pr/12322 |
| Testbed | pacquet |
Click to view all benchmark results
| Benchmark | Latency | Benchmark Result milliseconds (ms) (Result Δ%) | Upper Boundary milliseconds (ms) (Limit %) |
|---|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 8,695.09 ms(-5.88%)Baseline: 9,238.07 ms | 11,085.68 ms (78.44%) |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot 🚷 view threshold | 4,893.02 ms(-2.41%)Baseline: 5,014.09 ms | 6,016.91 ms (81.32%) |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 1,192.45 ms(-16.58%)Baseline: 1,429.38 ms | 1,715.26 ms (69.52%) |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 10,148.96 ms(+0.55%)Baseline: 10,093.26 ms | 12,111.91 ms (83.79%) |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 663.75 ms(+1.54%)Baseline: 653.68 ms | 784.41 ms (84.62%) |
|
| Branch | pr/12322 |
| Testbed | pnpr |
⚠️ 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-thresholdsflag.
Click to view all benchmark results
| Benchmark | Latency | milliseconds (ms) |
|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot | 5,129.24 ms |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot | 729.24 ms |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot | 693.85 ms |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot | 5,373.97 ms |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot | 786.00 ms |
|
Code review by qodo was updated up to the latest commit 126a416 |
There was a problem hiding this comment.
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 `@pacquet/crates/registry/src/package_versions.rs`:
- Around line 287-305: The loop over self.slots can silently drop a slot when
serde_json::from_str::<&RawValue>(&json) fails; update the branch in the
serialization code that handles slot.source.json() so that on Err(e) you emit a
tracing::warn (including context: the version key and the error) before
continuing, mirroring the other error paths (see the hydration path and
fragments() handling) and keeping the subsequent continue and behavior for
slot.parsed.get() unchanged; locate the logic around slot.source.json(),
serde_json::from_str::<&RawValue>, and map.serialize_entry to add the warning.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 890ddeda-fceb-4ef7-9168-4cdb499a140f
📒 Files selected for processing (5)
pacquet/crates/registry/src/package_versions.rspacquet/crates/resolving-npm-resolver/src/fetch_full_metadata_cached.rspacquet/crates/resolving-npm-resolver/src/mirror.rspacquet/crates/resolving-npm-resolver/src/mirror/tests.rspacquet/crates/resolving-npm-resolver/src/pick_package.rs
🚧 Files skipped from review as they are similar to previous changes (1)
- pacquet/crates/resolving-npm-resolver/src/pick_package.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
- GitHub Check: Lint and Test (ubuntu-latest)
- GitHub Check: Doc
- GitHub Check: Lint and Test (macos-latest)
- GitHub Check: Lint and Test (windows-latest)
- GitHub Check: Dylint
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Code Coverage
- GitHub Check: Analyze (javascript)
- GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs
📄 CodeRabbit inference engine (pacquet/AGENTS.md)
pacquet/**/*.rs: Log emissions are part of matching pnpm — when porting a function that firespnpm:<channel>events throughglobalLogger,logger.debug(...), orstreamParser.write(...), mirror the call site, payload, and ordering so@pnpm/cli.default-reporterparses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing the brand into a plainStringor&strin Rust
If upstream TypeScript always validates before construction of a branded string, validate in the Rust wrapper too viaTryFrom<String>and/orFromStrand do not provide an infallible public constructor
If upstream TypeScript never validates a branded string, just brand for type-safety in Rust by exposing an infallibleFrom<String>constructor
If upstream TypeScript occasionally constructs a branded string without validation, exposefrom_str_uncheckedin Rust as an escape hatch alongside the validating constructor
Match upstream serde behavior for branded strings crossing JSON, YAML, or INI boundaries by using#[serde(try_from = "String")]for deserialization and#[serde(into = "String")]for serialization
Derive simple conversions for branded strings using#[derive(derive_more::From)]and#[derive(derive_more::Into)]instead of handwritingimplblocks; use manualimplonly when conversion needs custom logic
Model TypeScript string literal unions (like'auto' | 'always' | 'never') as Rustenums instead of newtype wrappers, since the set of valid values is closed
Treat TypeScript string template literal types (like`${string}@${string}`) the same as branded string types in Rust, using a newtype wrapper with validation
Follow the code style guide inCODE_STYLE_GUIDE.md— imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching,pipe-trait, error handling, test layout, and cloning ofArcandRc
Choose owned vs. borrowed parameters to minimize copies; widen to t...
Files:
pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata_cached.rspacquet/crates/resolving-npm-resolver/src/mirror/tests.rspacquet/crates/resolving-npm-resolver/src/mirror.rspacquet/crates/registry/src/package_versions.rs
🧠 Learnings (4)
📚 Learning: 2026-05-20T19:40:55.051Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11774
File: pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:0-0
Timestamp: 2026-05-20T19:40:55.051Z
Learning: In the pacquet Rust code, ensure the semver implementation uses the `node-semver` crate (not `nodejs-semver`). `node-semver`’s public API does not include a `satisfies_with_prerelease`-style method; prerelease-tolerant matching should be implemented inline by first calling `Range::satisfies`, and when it rejects a prerelease version, retry matching against a stripped `MAJOR.MINOR.PATCH` base of the prerelease version.
Applied to files:
pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata_cached.rspacquet/crates/resolving-npm-resolver/src/mirror/tests.rspacquet/crates/resolving-npm-resolver/src/mirror.rspacquet/crates/registry/src/package_versions.rs
📚 Learning: 2026-05-22T00:08:44.646Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11837
File: pacquet/crates/resolving-npm-resolver/src/pick_package.rs:33-51
Timestamp: 2026-05-22T00:08:44.646Z
Learning: In the pnpm/pnpm repo’s pacquet Rust crates, do not flag Unicode ellipsis characters (U+2026, `…`) in Rust doc comments (`///` / `/** */`) as a lint violation. The pacquet crate’s `dylint.toml` only enables `perfectionist::derive_ordering`, and the Dylint `unicode-ellipsis` rule is not enabled for this project—so `…` in doc comments is an intentional, repo-consistent style.
Applied to files:
pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata_cached.rspacquet/crates/resolving-npm-resolver/src/mirror/tests.rspacquet/crates/resolving-npm-resolver/src/mirror.rspacquet/crates/registry/src/package_versions.rs
📚 Learning: 2026-05-20T23:07:58.444Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:07:58.444Z
Learning: When reviewing code in this pacquet Rust port, follow the upstream pnpm compatibility rule: only match pnpm’s behavior exactly. Do not propose review changes that intentionally deviate from pnpm’s documented/observed behavior, even if pnpm appears buggy. If you identify a real bug in pnpm behavior, the review should prioritize fixing it upstream in pnpm first, and avoid implementing a pnpm-behavior workaround here unless the same fix has already landed upstream.
Applied to files:
pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata_cached.rspacquet/crates/resolving-npm-resolver/src/mirror/tests.rspacquet/crates/resolving-npm-resolver/src/mirror.rspacquet/crates/registry/src/package_versions.rs
📚 Learning: 2026-06-06T18:58:37.156Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12243
File: pacquet/crates/package-manager/src/install_package_by_snapshot.rs:319-322
Timestamp: 2026-06-06T18:58:37.156Z
Learning: When reviewing Rust code, do not assume `matches!(expr, Pattern(_))` will move out of `expr` if `Pattern(_)` contains no by-value bindings. `matches!` desugars to a `match` that auto-borrows the scrutinee for discrimination, so even if `expr` is a non-`Copy` value behind a shared reference (e.g., `&T`), the macro should not move-out of the borrowed data purely due to `matches!`. Treat `matches!(&expr, Pattern(_))` as a readability/clarity improvement, not a correctness requirement. Only flag potential move-out-of-borrow risks when the pattern includes by-value bindings that would require moving the matched value.
Applied to files:
pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata_cached.rspacquet/crates/resolving-npm-resolver/src/mirror/tests.rspacquet/crates/resolving-npm-resolver/src/mirror.rspacquet/crates/registry/src/package_versions.rs
🔇 Additional comments (4)
pacquet/crates/registry/src/package_versions.rs (1)
18-27: LGTM!Also applies to: 42-127, 129-181, 184-244, 246-308
pacquet/crates/resolving-npm-resolver/src/mirror.rs (1)
1-464: LGTM!pacquet/crates/resolving-npm-resolver/src/mirror/tests.rs (1)
1-198: LGTM!pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata_cached.rs (1)
15-17: LGTM!Also applies to: 31-31, 192-207
…ents Profiling a warm babylon resolve (metadata mirrors hot, ~520 MB across 1,476 packuments) showed the dominant cost was not I/O but hydration: every version of every packument was deserialized into typed manifests — maps, strings, and `serde_json::Value` trees built, hashed, and dropped — even though a pick consults only the version strings plus the handful of manifests it actually considers. High-release-cadence packages make this quadratic-feeling: typescript ships 3,800 versions and a pick needs one. `Package::versions` is now a `PackageVersions` map whose entries hold the raw JSON fragment serde captured (`Arc<RawValue>`, shared rather than copied) and hydrate into `Arc<PackageVersion>` on first access, caching per slot. Key scans (range matching, preferred-version prioritization, the publish-date filter, existence probes) never hydrate; `pinned_version` matches on version strings and hydrates only the winner; the publish-date filter moves slots without touching manifests. A fragment that fails to decode behaves as if the version were absent (with a warning), matching the tolerance of the JavaScript implementation, which never validates entries it doesn't pick. Serialization emits raw fragments verbatim, so mirror round-trips keep the registry's exact bytes. Picked manifests now travel as `Arc<PackageVersion>` instead of by-value clones. Parsing the three largest packuments in the babylon graph improves 3-3.8x (e.g. typescript 36.1 ms -> 9.5 ms, 905 MB/s), and a warm babylon `--lockfile-only` resolve drops from ~9.1 s to ~7.7-8 s wall with allocator pressure largely gone from the profile; the same hydration savings apply to cold resolves. The remaining warm-resolve cost shifts to lock contention in the resolve machinery, which is a separate follow-up. Requires the `raw_value` feature of serde_json (already a workspace dependency).
Every resolve edge consults the shared meta cache before any other work, so a large graph drives tens of thousands of lookups through it from all runtime workers at once. The warm-resolve time profile showed the cache's single `Mutex<HashMap>` as the top mutex-wait site; back it with the sharded `DashMap` the crate already uses for the fetch locker and the picked-manifest cache. Wall time on the warm babylon resolve is unchanged (the walk is critical-path-bound after the lazy hydration change), but the contention disappears from the profile and the cache no longer serializes workers under load.
…ssed version fragments
Replace the mirror's two-line NDJSON shape (headers line + verbatim
packument body) with an indexed format:
pacquet-meta-v1 <headers_len> <index_len>\n
<headers JSON> etag, modified
<index JSON> name, dist-tags, time, homepage,
versions: [version, offset, len]
<fragments> concatenated raw per-version JSON
Loading a packument from the cache previously re-read and
structurally re-scanned the whole body to recover the version
fragments. The index records each fragment's byte span, so the loader
now reads the file once and hands `PackageVersions` spans into that
buffer — hydration parses a slice in place, with no scan and no
re-allocation. Conditional-GET headers still come from a fixed-size
probe of the file head. The writer streams the registry's own bytes
(fragments borrow from the lazily-parsed response body), so the
cold-install cost stays one temp-file + rename per package.
Span-addressed slots were first implemented as open-per-fragment
reads; the pick paths can probe many candidate fragments per package,
and the measured syscall cost (sys 0.4 s -> 4.5 s on a warm babylon
resolve) outweighed the bytes saved, so the loader reads the file
into one shared buffer instead.
Warm babylon `--lockfile-only` resolves drop from ~8 s to ~5.5-6.7 s
on top of the lazy-hydration change (~9.1 s before this PR). Files in
the previous NDJSON format read as cache misses and are rewritten in
the indexed format on the next 200 response; top-level packument keys
outside the index were never read back by the resolver, so nothing is
lost by not persisting them. The on-disk metadata cache is now
pacquet-specific rather than byte-shared with the pnpm CLI's mirror
(agreed maintainer decision).
…agments
Review findings on the lazy-hydration contract ("an undecodable
fragment behaves as if the version were absent"):
- `Package::pinned_version` walks satisfying candidates from highest
to lowest and hydrates until one decodes, instead of giving up when
the single highest match fails.
- `pick_package_from_meta` retries the pick against a clone that
excludes an undecodable winner (dropping dist-tags that point at
it, so the latest-tag fast path can't re-pick it); an exact-version
spec naming an undecodable manifest yields no match. Previously the
first undecodable winner ended resolution for that dependency.
- `Package::latest` returns `Option` — a dangling `dist-tags.latest`
or an undecodable manifest must not be able to panic the process
from registry-served data.
- `read_mirror_headers` bounds the declared headers length before
allocating from it, so a corrupted mirror can't trigger an
arbitrarily large allocation on the warm path.
- the serializer warns (instead of silently skipping) when a fragment
is corrupt, matching the other error paths.
Also fixes the doc build: an intra-doc link still referenced the
`FileSpan` variant that became `BufferSpan`.
126a416 to
ae28135
Compare
|
Code review by qodo was updated up to the latest commit ae28135 |
The trust-downgrade scan walked prior versions with `PackageVersions::iter()`, which silently skips fragments that fail to hydrate — an undecodable prior manifest could hide the strongest prior trust evidence and let a downgrade pass undetected. `detect_strongest_trust_evidence_before` now walks version keys, applies the cheap timestamp/prerelease filters first, and surfaces `TrustViolation::TrustCheckFailed` when an in-scope version is listed but its manifest does not decode. The early return on `StagedPublish` stays: once max-rank evidence is found, no skipped fragment can hide anything stronger.
|
Code review by qodo was updated up to the latest commit f6b75fc |
Why
Profiling a warm babylon resolve (metadata mirrors hot, ~520 MB across 1,476 packuments) showed the dominant cost was not I/O but hydration: every version of every packument was deserialized into typed manifests — maps, strings, and
serde_json::Valuetrees built, hashed, and dropped — even though a pick consults only the version strings plus the handful of manifests it actually considers. typescript ships 3,800 versions; a pick needs one. The#[serde(flatten)]catch-all onPackageVersioncompounded it by routing the whole struct through serde's buffering deserializer. The same hydration cost was paid on cold resolves inside thespawn_blockingparses from #12318.What
Three changes, applied in profile-driven order:
1. Lazy packument hydration (
df6f70eb57).Package::versionsbecomes aPackageVersionsmap whose entries hold the raw JSON fragment serde captured (Arc<RawValue>, shared not copied) and hydrate intoArc<PackageVersion>on first access, cached per slot. Key scans never hydrate;pinned_versionhydrates only the winner; the publish-date filter moves slots without touching manifests; undecodable fragments degrade to "version absent" (matching the JS implementation's tolerance); serialization re-emits raw fragments verbatim. Picked manifests travel asArc<PackageVersion>. Enables serde_json'sraw_valuefeature (already a workspace dep).2. Sharded in-memory packument cache (
4c28c6679e). Every resolve edge consults the shared meta cache, and its singleMutex<HashMap>was the top mutex-wait site in the profile; it is now the sameDashMapshape the crate's other shared maps use. Honest note: wall time was unchanged by this alone — the post-hydration profile shows the warm resolve is critical-path-bound, not lock-bound — but the contention disappears and the cache no longer serializes workers under load.3. Indexed on-disk metadata mirror (
126a416ae8, maintainer-approved cache-format break). The two-line NDJSON mirror (headers + verbatim body) becomes:The loader reads the file once and hands
PackageVersionsbyte spans into that buffer — no structural re-scan, hydration parses a slice in place. The writer streams the registry's own bytes (fragments borrow from the lazily-parsed response body), so the cold-install cost stays one temp-file + rename per package. Old-format files read as cache misses and are rewritten on the next 200. A span-per-fragmentpreadvariant was tried first and measured worse (sys 0.4 s → 4.5 s; the pick paths probe many candidate fragments per package), hence the single-buffer design.Measurements (warm babylon
--lockfile-onlyresolve, 10-core M-series)deserialize_anydominate the profiletypescript36.1→9.5 ms)Cold resolves keep the same hydration savings inside the parse tasks; cold write volume and syscall pattern are unchanged.
Evaluated and deliberately not included
Consolidating the install-phase thread pools (tokio + global rayon + the dedicated CAS-write pool + capped blocking threads show ~100k involuntary context switches on cold installs vs ~750 for the pnpm CLI). After the resolution fixes, repeated container A/Bs show no measurable wall-clock cost from the churn — it hides entirely behind network time — and history warns that speculative concurrency reshuffles here regress badly (see the #11903 prefetch revert). Deferred until a benchmark shows it on the critical path.
Tests
New
package_versionsunit tests (hydrate-on-demand + Arc-identity caching, undecodable-fragment-as-absent, verbatim raw round-trip incl. unknown keys, eager construction, hydration-free filtering) and rewrittenmirrortests (headers/index/fragment round-trip, span hydration, truncation → cache miss, old-format → cache miss, atomic overwrite). Full suites green:pacquet-registry(23),pacquet-resolving-npm-resolver(235),pacquet-package-manager+pacquet-cli(768); workspace clippy-D warnings(pedantic set), dylint, fmt.Written by an agent (Claude Code, claude-fable-5).
Summary by CodeRabbit
Chores
Bug Fixes
Performance
Tests