Skip to content

perf: content-check modified manifests and fall back to the current lockfile on the repeat-install fast path (pacquet + pnpm)#12315

Merged
zkochan merged 7 commits into
mainfrom
investigate-benchmark-results
Jun 10, 2026
Merged

perf: content-check modified manifests and fall back to the current lockfile on the repeat-install fast path (pacquet + pnpm)#12315
zkochan merged 7 commits into
mainfrom
investigate-benchmark-results

Conversation

@zkochan

@zkochan zkochan commented Jun 10, 2026

Copy link
Copy Markdown
Member

Why

On benchmarks.vlt.sh (2026-06-10 run, pacquet 0.11.2), pacquet ranked 8th–9th of 10 in every lockfile+node_modules variation — slower than pnpm, npm, yarn and vlt — e.g. astro: pacquet 936 ms vs pnpm 502 ms; babylon: pacquet 9.08 s vs pnpm 0.85 s. It also trailed vlt/npm in the node_modules and cache+node_modules variations (astro 1.5 s / 0.7 s, babylon 8.9 s / 6.4 s).

Root cause

Tracing the actual runner (a pacquet PATH shim logging per-invocation file stats) showed the harness's prepare step rewrites package.json with identical content but a fresh mtime before every timed run, while clean_all_cache wipes ~/.cache/pnpm (the packument cache and lockfile-verified.jsonl), and the node_modules variations additionally delete pnpm-lock.yaml.

  • pnpm: checkDepsStatus's modified-manifests branch re-checks the content against the lockfile (assertWantedLockfileUpToDate, assertLockfilesEqual, linkedPackagesAreUpToDate) and reports "Already up to date" with zero network — ~0.5 s is just Node startup. Verified locally: with all caches wiped and the network blocked, pnpm install still prints "Already up to date" in 228 ms.
  • pacquet: the optimistic repeat-install check bailed on any newer manifest mtime, fell into the full pipeline, and the awaited minimumReleaseAge lockfile-verification gate — its verdict cache wiped — re-fetched one packument per locked package per run: 0.94 s on astro, 9.1 s on babylon.
  • With pnpm-lock.yaml deleted, both stacks pay a similar fan-out on the synthesized-from-current lockfile (tryLockfileVerificationCache bails before the content-hash index when the lockfile file can't be stat'd), which is why even pnpm needs 2.2–11.6 s there.

What

Commit 1 — port the modified-manifests branch of checkDepsStatus (at cc4ff817aa) into optimistic_repeat_install:

  • a manifest whose mtime is newer than lastValidatedTimestamp is re-checked against the wanted lockfile instead of invalidating the fast path: lockfile-settings drift (getOutdatedLockfileSetting), per-importer satisfiesPackageManifest, and a port of linkedPackagesAreUpToDate for workspace links (isLocalFileDepUpdated for file: directory specifiers is not ported — those conservatively fall through to the full install);
  • assertLockfilesEqual runs when the wanted lockfile is newer than the reference (workspace: lastValidatedTimestamp; single-project: the current lockfile's mtime, mirroring upstream's branch shapes);
  • the workspace branch refreshes lastValidatedTimestamp after a passing content check, like upstream's updateWorkspaceState call;
  • the frozen-dispatch freshness gate is split into reusable pieces (parse_config_overrides, check_lockfile_settings_drift, check_importer_satisfies) shared with the new check, and the per-importer slice is no longer hard-wired to the root importer.

Commit 2 — treat the current lockfile as the wanted one when pnpm-lock.yaml is missing (pacquet) (requested by @zkochan): when node_modules is intact, <virtual_store_dir>/lock.yaml — the record of what the previous install materialized — stands in as the wanted lockfile for the same content checks, and pnpm-lock.yaml is regenerated from it (byte-identical to what the full install's synthesize-from-current path would write) before the fast path reports "Already up to date". Single-project installs with no lockfile on either side still refuse the fast path; lockfile: false skips the regeneration; a manifest that no longer matches (e.g. pacquet add) still takes the full resolve.

Validation

Re-ran the actual vlt.sh harness (same scripts, ubuntu-24.04-arm runner) with the patched binary swapped into the npm-installed pacquet; all hyperfine runs exited 0:

fixture, variation pacquet 0.11.2 (official run) patched pnpm (same validation run)
astro, lockfile+node_modules 935.6 ms (rank 9/10) 38–39 ms 599–621 ms
babylon, lockfile+node_modules 9 084 ms (rank 8/10) 86.6 ms ± 0.6 767.7 ms
astro, node_modules 1 501 ms (rank 4/10) 41.2 ms ± 0.8 2 226 ms
astro, cache+node_modules 704 ms (rank 5/10) 42.9 ms ± 0.9 2 017 ms
babylon, node_modules 8 962 ms (rank 6/10) 107.8 ms ± 1.0 11 566 ms

After this change only aube (~5 ms) and bun (~8 ms) stay ahead in these five variations.

cargo nextest run -p pacquet-package-manager (438 tests), -p pacquet-cli install suites, workspace clippy -D warnings, dylint, fmt, taplo and typos pacquet are clean. New tests cover the touched-but-identical manifest, a manifest that adds a dependency, a diverged wanted-vs-current lockfile, the state-timestamp refresh, linked siblings inside/outside the manifest range, lockfile regeneration (modified and unmodified manifests, workspace state bump), and lockfile: false. Two offline e2e tests additionally pin the "zero network, zero pipeline" property through Install::run's real dispatch: a real install, registry dropped, caches wiped, repeat install pointed at a dead port — both verified discriminating by temporarily disabling the content check.

Two existing tests were adjusted: fresh_install_records_lockfile_verification_for_mtime_bypassed_noop now disables the optimistic check explicitly so it keeps guarding the verification-cache wiring it was written for, and optimistic_repeat_install_does_not_short_circuit_when_lockfile_missing now passes lockfile: None (matching the CLI contract for a missing file) and documents that the guard requires both lockfiles to be absent.

Commit 1ee88c5107 — the same fallback in the pnpm CLI (@pnpm/deps.status + @pnpm/installing.commands): checkDepsStatus lets the current lockfile stand in when pnpm-lock.yaml is missing (workspace shared-lockfile branch and single-project branch), runs the same content checks against it, and returns it as wantedLockfileToRestore; installDeps writes pnpm-lock.yaml back from it before reporting "Already up to date". Guard rails: no lockfile on either side still refuses the fast path, useLockfile: false skips the restore, a failed restore falls through to the full install, and the stand-in is disabled under useGitBranchLockfile (there a missing plain pnpm-lock.yaml is the steady state and the branch lockfile may legitimately differ from the current one). Verified with the bundled CLI: install → delete pnpm-lock.yamlpnpm install --registry=http://127.0.0.1:9/ prints "Already up to date" in 29 ms and restores the lockfile byte-identically. Covered by 5 new checkDepsStatus unit tests and an installing/commands integration test that runs the repeat install against a dead registry. Changeset bumps @pnpm/deps.status, @pnpm/installing.commands, and pnpm (minor).

Not addressed here (follow-ups from the same investigation)

  • clean babylon: pacquet 34.3 s vs pnpm 30.1 s on the runner. Locally the cold install is syscall-bound (open/close churn during store-write + link; no duplicate downloads — 1 669 unique tarballs fetched exactly once; lockfile graph matches pnpm's 1 657±). Needs a Linux-side profile.

Written by an agent (Claude Code, claude-fable-5).

Summary by CodeRabbit

  • New Features

    • Automatically regenerates a missing lockfile when safe during repeat installs; exposes a restore payload so callers can recover the wanted lockfile.
    • Optional "git-branch lockfile" mode for lockfile fallback behavior.
  • Refactor

    • More nuanced repeat-install checks that consider manifest mtimes and content, and a clearer fast-path decision flow.
  • Tests

    • Expanded regression and offline tests covering lockfile restoration, repeat-install fast paths, and manifest-change scenarios.
  • Chore

    • CLI option typing updated to include optimistic repeat-install flag.

…g the repeat-install fast path

Port the modified-manifests branch of pnpm's checkDepsStatus to the
optimistic repeat-install check. A package.json whose mtime is newer
than the last validation no longer falls straight into the full install
pipeline: the check now re-reads the wanted lockfile and confirms the
content still matches — assertLockfilesEqual against the current
lockfile, getOutdatedLockfileSetting drift, satisfiesPackageManifest
per modified importer, and linkedPackagesAreUpToDate for workspace
links — before reporting "Already up to date". The workspace branch
refreshes lastValidatedTimestamp after a passing content check, like
upstream.

Ports (at cc4ff817aa):
- deps/status/src/checkDepsStatus.ts modified-projects branch and
  single-project lockfile-mtime branch
- deps/status/src/assertLockfilesEqual.ts
- lockfile/verification/src/linkedPackagesAreUpToDate.ts (without the
  isLocalFileDepUpdated branch: file: directory specifiers
  conservatively fall through to the full install)

This closes the dominant gap in the benchmarks.vlt.sh
lockfile+node_modules variation, where the harness rewrites
package.json (same content, fresh mtime) and wipes the cache dir
before every timed run. pnpm absorbs that via the content re-check
(~0.5 s); pacquet re-ran the entire install pipeline, and with
lockfile-verified.jsonl and the metadata cache wiped, the
minimumReleaseAge lockfile-verification gate re-fetched a packument
per locked package: 0.94 s on astro and 9.1 s on babylon per run
(ranking 8-9/10) against pnpm's 0.50-0.85 s. With the content check
the same scenario is a ~40 ms no-op on astro and ~250 ms on babylon.

The freshness gate is refactored into reusable pieces
(parse_config_overrides, check_lockfile_settings_drift,
check_importer_satisfies) shared between the frozen-install dispatch
and the new content check, with the per-importer slice no longer
hard-wired to the root importer.
@qodo-free-for-open-source-projects

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

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (5) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0)

Grey Divider


Action required

1. Linked check ignores overrides 🐞 Bug ≡ Correctness
Description
modified_manifests_match_lockfile() checks lockfile satisfaction using an override-applied
manifest but then runs linked_packages_are_up_to_date() against the raw on-disk manifest, so
overrides that change linkability can be missed. This can incorrectly return Decision::UpToDate
and skip a needed relink, leaving node_modules inconsistent with the effective configuration.
Code

pacquet/crates/package-manager/src/optimistic_repeat_install.rs[R411-433]

+    let linked_ctx = LinkedPackagesContext::new(config, project_manifests);
+    for project in to_check {
+        let importer_id =
+            pacquet_workspace::importer_id_from_root_dir(workspace_root, project.root_dir);
+        if let Err(error) = crate::install::check_importer_satisfies(
+            wanted,
+            project.manifest,
+            &importer_id,
+            config,
+            parsed_overrides.as_deref(),
+        ) {
+            tracing::debug!(target: "pacquet::install", %error, importer_id, "repeat-install content check: manifest no longer satisfied");
+            return Err("a modified manifest is no longer satisfied by the lockfile");
+        }
+        let Some(importer) = wanted.importers.get(&importer_id) else {
+            return Err("a modified project has no importer entry in the lockfile");
+        };
+        if !linked_packages_are_up_to_date(
+            &linked_ctx,
+            project.root_dir,
+            project.manifest,
+            importer,
+        ) {
Evidence
The code applies pnpm.overrides when checking whether the lockfile satisfies the manifest, but the
linked-package freshness check uses the unmodified manifest’s dependency specifiers, so it can
disagree with the effective (override-applied) dependency graph that the lockfile represents.

pacquet/crates/package-manager/src/optimistic_repeat_install.rs[402-433]
pacquet/crates/package-manager/src/install.rs[1303-1308]
pacquet/crates/package-manager/src/optimistic_repeat_install.rs[537-607]

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

## Issue description
During the modified-manifests content re-check, overrides are applied for the lockfile/manifest specifier comparison, but `linked_packages_are_up_to_date()` still reads dependency specifiers from `project.manifest` (the unmodified on-disk manifest). If `pnpm.overrides` changes a dependency’s effective specifier in a way that affects whether it should be linked to a workspace package, the linked-package check can make the wrong decision and allow the optimistic fast path to return `UpToDate`.
### Issue Context
- `check_importer_satisfies()` explicitly applies overrides because lockfile specifiers are written with overrides already applied.
- `linked_packages_are_up_to_date()` reads the manifest’s dependency strings directly from `manifest.value()`.
### Fix Focus Areas
- pacquet/crates/package-manager/src/optimistic_repeat_install.rs[402-436]
- pacquet/crates/package-manager/src/install.rs[1303-1322]
### Suggested fix
1. Ensure `linked_packages_are_up_to_date()` receives an override-applied manifest (and, if applicable, the same effective view used for lockfile satisfaction).
2. Options:
- Refactor `check_importer_satisfies()` to optionally return the effective manifest (or a lightweight effective dependency-spec map) so the caller can reuse it for the linked check.
- Or duplicate the override-application logic in `modified_manifests_match_lockfile()` (build `VersionsOverrider` once, apply to a cloned manifest per project) and pass the cloned manifest into `linked_packages_are_up_to_date()`.
3. Add a regression test where an override flips linkability (e.g., manifest has `"pkg-a": "^1.0.0"`, workspace has `pkg-a@2.0.0`, override forces `workspace:*`, and the fast path must *not* return `UpToDate` unless the installed state matches the override-implied linking).

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


2. Peer suffix breaks link check 🐞 Bug ≡ Correctness
Description
In linked_packages_are_up_to_date(), workspace package lookup uses
dep.version.as_regular().to_string(), which can include a peer suffix (e.g. "1.0.0(react@...)"), so
it won’t match the workspace index keyed by package.json’s plain "version". This can incorrectly
treat a registry-resolved dep as not linkable and return Decision::UpToDate when a full install is
needed to re-link to the workspace package.
Code

pacquet/crates/package-manager/src/optimistic_repeat_install.rs[R586-593]

+            let linked_dir: Option<std::borrow::Cow<'_, Path>> = match link_target {
+                Some(target) => Some(std::borrow::Cow::Owned(project_dir.join(target))),
+                None => dep
+                    .version
+                    .as_regular()
+                    .map(|ver| ver.to_string())
+                    .and_then(|version| ctx.workspace_packages.get(&dep_name)?.get(&version))
+                    .map(|dir| std::borrow::Cow::Borrowed(*dir)),
Evidence
The repeat-install linked-package check indexes workspace packages by manifest "version" strings,
but uses PkgVerPeer::to_string() (which includes the peer suffix) when looking up a
lockfile-resolved version, causing the lookup to miss peer-variant versions.
PkgVerPeer::without_peer() exists to normalize this exact mismatch.

pacquet/crates/package-manager/src/optimistic_repeat_install.rs[492-507]
pacquet/crates/package-manager/src/optimistic_repeat_install.rs[586-594]
pacquet/crates/lockfile/src/pkg_ver_peer.rs[49-59]
pacquet/crates/lockfile/src/pkg_ver_peer.rs[137-149]

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

## Issue description
`linked_packages_are_up_to_date()` tries to detect when a dependency that was previously resolved from the registry should now resolve to a workspace package (or vice versa). In the non-linked case it looks up a candidate workspace package by using the lockfile-resolved version string as a key, but that string can include a peer suffix (e.g. `1.0.0(react@17.0.2)`), while the workspace index is built from `package.json`'s plain `"version"` field (e.g. `1.0.0`). This mismatch can cause the lookup to fail and the fast path to incorrectly return `UpToDate`.
### Issue Context
`PkgVerPeer`’s `Display` includes the peer suffix, and it provides `without_peer()` specifically for normalization.
### Fix Focus Areas
- pacquet/crates/package-manager/src/optimistic_repeat_install.rs[586-593]
- pacquet/crates/lockfile/src/pkg_ver_peer.rs[49-59]
- pacquet/crates/lockfile/src/pkg_ver_peer.rs[137-149]
### Proposed fix
When building the version key for `ctx.workspace_packages` lookup, normalize away the peer suffix (and keep the base version) before indexing, e.g.:
- replace `ver.to_string()` with `ver.without_peer().to_string()` (or equivalent extraction of the base `VersionPart`), so peer-variant lockfile entries still map to the workspace manifest’s `version`.
Add/adjust a test that covers a workspace package with `version: 1.0.0` and an importer dep resolved as `1.0.0(<peer suffix>)`, ensuring the repeat-install content check falls through when it should relink.

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


3. Linked deps unchecked 🐞 Bug ≡ Correctness
Description
In workspace mode, modified_manifests_match_lockfile only runs linked_packages_are_up_to_date
for projects whose manifests are newer than lastValidatedTimestamp, so an unchanged dependent
importer can be skipped even if a linked workspace package it depends on changed (e.g., version bump
outside the declared range). This can incorrectly return Decision::UpToDate and skip a needed full
install, leaving node_modules inconsistent with workspace link semantics.
Code

pacquet/crates/package-manager/src/optimistic_repeat_install.rs[R285-358]

+    let to_check: &[&ManifestStat<'_>] = if is_workspace_install {
+        // Workspace branch: a wanted lockfile newer than the last
+        // validation must equal what the previous install materialized.
+        // Mirrors <https://github.com/pnpm/pnpm/blob/cc4ff817aa/deps/status/src/checkDepsStatus.ts#L283-L289>.
+        if wanted_mtime_ms > state.last_validated_timestamp {
+            assert_wanted_lockfile_equals_current(wanted, config)?;
+        }
+        modified
+    } else {
+        // Single-project branch keys off the lockfile mtimes instead of
+        // `lastValidatedTimestamp`. Mirrors
+        // <https://github.com/pnpm/pnpm/blob/cc4ff817aa/deps/status/src/checkDepsStatus.ts#L407-L462>.
+        let current_mtime_ms =
+            mtime_ms(&config.virtual_store_dir.join(Lockfile::CURRENT_FILE_NAME));
+        if let Some(current_mtime_ms) = current_mtime_ms
+            && wanted_mtime_ms > current_mtime_ms
+        {
+            assert_wanted_lockfile_equals_current(wanted, config)?;
+        }
+        let root = modified.first().expect("modified-manifests branch requires a modified project");
+        if root.mtime_ms > wanted_mtime_ms {
+            modified
+        } else if current_mtime_ms.is_some() {
+            // "The manifest file is not newer than the lockfile.
+            // Exiting check."
+            &[]
+        } else if wanted.packages.as_ref().is_some_and(|packages| !packages.is_empty()) {
+            // RUN_CHECK_DEPS_NO_DEPS: the lockfile requires
+            // dependencies but nothing was ever installed.
+            return Err("the lockfile requires dependencies but none were installed");
+        } else {
+            &[]
+        }
+    };
+
+    if to_check.is_empty() {
+        return Ok(());
+    }
+
+    let parsed_overrides = crate::install::parse_config_overrides(config, catalogs)
+        .map_err(|_| "pnpm.overrides cannot be parsed")?;
+    if let Err(error) =
+        crate::install::check_lockfile_settings_drift(wanted, config, parsed_overrides.as_deref())
+    {
+        tracing::debug!(target: "pacquet::install", %error, "repeat-install content check: lockfile settings drift");
+        return Err("a lockfile setting drifted from the current configuration");
+    }
+
+    let linked_ctx = LinkedPackagesContext::new(config, project_manifests);
+    for project in to_check {
+        let importer_id =
+            pacquet_workspace::importer_id_from_root_dir(workspace_root, project.root_dir);
+        if let Err(error) = crate::install::check_importer_satisfies(
+            wanted,
+            project.manifest,
+            &importer_id,
+            config,
+            parsed_overrides.as_deref(),
+        ) {
+            tracing::debug!(target: "pacquet::install", %error, importer_id, "repeat-install content check: manifest no longer satisfied");
+            return Err("a modified manifest is no longer satisfied by the lockfile");
+        }
+        let Some(importer) = wanted.importers.get(&importer_id) else {
+            return Err("a modified project has no importer entry in the lockfile");
+        };
+        if !linked_packages_are_up_to_date(
+            &linked_ctx,
+            project.root_dir,
+            project.manifest,
+            importer,
+        ) {
+            return Err("a linked package is out of date");
+        }
}
Evidence
The workspace branch sets to_check to only the manifests with mtimes newer than
lastValidatedTimestamp, and then runs linked_packages_are_up_to_date only for `project in
to_check. Since linked_packages_are_up_to_date` validates link correctness for the specific
importer snapshot it is passed, skipping an unchanged dependent importer means its workspace links
are never revalidated when only the linked package’s manifest changed.

pacquet/crates/package-manager/src/optimistic_repeat_install.rs[192-206]
pacquet/crates/package-manager/src/optimistic_repeat_install.rs[285-358]
pacquet/crates/package-manager/src/optimistic_repeat_install.rs[451-531]
pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs[48-55]

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

## Issue description
In workspace installs, the modified-manifests content re-check only validates linked-package freshness for `to_check` (which is set to `modified`), so it may fail to detect when an *unchanged* importer’s workspace link is no longer valid (e.g., a sibling package’s version changed and no longer satisfies the dependent’s semver range).
## Issue Context
- `modified` is derived purely from manifest mtimes, and in the workspace branch `to_check = modified`.
- `linked_packages_are_up_to_date(...)` validates link correctness for a specific importer by comparing that importer’s manifest spec against the linked package’s current version.
- If only a linked package’s own manifest changes (version bump) while the dependent importer’s manifest is untouched, the dependent importer won’t be in `modified`, so its links won’t be validated.
## Fix Focus Areas
- pacquet/crates/package-manager/src/optimistic_repeat_install.rs[252-360]
### Suggested fix approach
- Keep the per-importer `check_importer_satisfies(...)` gate limited to the `modified` set (to preserve the perf intent).
- But in **workspace mode**, run `linked_packages_are_up_to_date(...)` for **all importers/projects** in `project_manifests` (or all `wanted.importers` that have corresponding manifests), not just `modified`.
- Build/lookup `importer_id` for each project via `importer_id_from_root_dir(...)`.
- Fetch the corresponding `ProjectSnapshot` from `wanted.importers` and validate.
- If any importer fails the linked-package check, return the same `Decision::Skipped` reason (e.g. "a linked package is out of date").
### Add/adjust tests
- Add a workspace test where:
1) root depends on `pkg-a` via a link in the lockfile,
2) only `pkg-a/package.json` is rewritten with a version bump outside `^1.0.0`,
3) root `package.json` is *not* touched,
4) assert the decision is `Skipped` (linked package out of date).

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



Remediation recommended

4. Virtual store path hardcoded 🐞 Bug ≡ Correctness
Description
missingWantedLockfileStandIn() reads the current lockfile from /node_modules/.pnpm, ignoring
virtualStoreDir, so installs using a custom virtual store location won’t restore a deleted
pnpm-lock.yaml before the optimistic repeat-install short-circuit reports “Already up to date”.
This leaves the lockfile missing even though /lock.yaml may exist and could have been used for
restoration.
Code

deps/status/src/checkDepsStatus.ts[R637-641]

+async function missingWantedLockfileStandIn (lockfileDir: string): Promise<CheckDepsStatusResult['wantedLockfileToRestore']> {
+  if (safeStatSync(path.join(lockfileDir, WANTED_LOCKFILE)) != null) return undefined
+  const currentLockfile = await readCurrentLockfile(path.join(lockfileDir, 'node_modules/.pnpm'), { ignoreIncompatible: false })
+  if (currentLockfile == null) return undefined
+  return { lockfile: currentLockfile, lockfileDir }
Evidence
The fallback hard-codes node_modules/.pnpm as the current lockfile directory, but the lockfile FS
layer stores the current lockfile at virtualStoreDir/lock.yaml (and virtualStoreDir is a
supported config option passed through install). Therefore, with a non-default virtualStoreDir,
the restore path will look in the wrong place and fail to restore the wanted lockfile.

deps/status/src/checkDepsStatus.ts[631-642]
installing/commands/src/installDeps.ts[181-186]
config/reader/src/Config.ts[170-176]
lockfile/fs/src/read.ts[26-35]
lockfile/fs/src/write.ts[49-60]

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 new “missing wanted lockfile fallback” restores `pnpm-lock.yaml` by reading the **current** lockfile from a hard-coded directory (`<lockfileDir>/node_modules/.pnpm`). When `virtualStoreDir` is configured to a different location, the current lockfile is written to `<virtualStoreDir>/lock.yaml`, so the fallback fails to find it and won’t restore `pnpm-lock.yaml`.
### Issue Context
- `installDeps()` forwards the CLI/config options (including `virtualStoreDir`) into `checkDepsStatus()`.
- `@pnpm/lockfile.fs` reads/writes the **current lockfile** under the configured virtual store directory.
### Fix Focus Areas
- deps/status/src/checkDepsStatus.ts[631-642]
- deps/status/src/checkDepsStatus.ts[293-320]
### Implementation notes
- Thread `virtualStoreDir` into `CheckDepsStatusOptions` (it is already part of `Config` and is optional), and use `opts.virtualStoreDir` when calling `readCurrentLockfile()` / constructing the current-lockfile path.
- Keep the current behavior as a fallback when `opts.virtualStoreDir` is undefined (default to `<lockfileDir>/node_modules/.pnpm`).
- Apply the same path selection in both:
- `missingWantedLockfileStandIn()`
- the `sharedWorkspaceLockfile` branch where `pnpm-lock.yaml` is missing and the current lockfile stands in.

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


5. Wrong override base dir 🐞 Bug ≡ Correctness
Description
check_importer_satisfies() resolves relative link:/file: override targets from
manifest.path().parent() rather than the lockfile/workspace root, so workspace installs can apply
overrides to the wrong path during the modified-manifests content check. This can wrongly force a
full install (or fail offline) even when the lockfile is actually still consistent with the
effective overrides.
Code

pacquet/crates/package-manager/src/install.rs[R1311-1317]

+    let manifest_for_freshness: &PackageManifest = if let Some(parsed) = parsed_overrides {
+        let root_dir = manifest.path().parent().unwrap_or_else(|| Path::new("."));
+        let overrider = crate::VersionsOverrider::new(parsed, root_dir);
+        overrider_manifest_holder = {
+            let mut cloned: PackageManifest = manifest.clone();
+            overrider.apply(&mut cloned, Some(root_dir));
+            cloned
Evidence
The freshness check applies overrides using the manifest directory as the override root, but
VersionsOverrider’s API explicitly separates the override root (used to resolve relative local
targets) from the manifest directory (used to re-relativize), and the fresh-resolve pipeline uses
the lockfile directory as the override root.

pacquet/crates/package-manager/src/install.rs[1303-1317]
pacquet/crates/package-manager/src/overrides.rs[81-116]
pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs[795-811]

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

## Issue description
`check_importer_satisfies()` constructs `VersionsOverrider` using `root_dir = manifest.path().parent()`, and also passes that same directory as `manifest_dir` when applying the override. For workspace installs, overrides with relative `link:`/`file:` targets should be resolved relative to the lockfile/workspace root (the same base used in the fresh-resolve hook chain), while `manifest_dir` should remain the importer’s own directory.
### Issue Context
- `VersionsOverrider::new(..., root_dir)` uses `root_dir` to resolve relative local override targets.
- `VersionsOverrider::apply(..., manifest_dir)` uses `manifest_dir` to re-relativize the resolved target for the manifest being rewritten.
- The fresh-resolve path uses `lockfile_dir` as `root_dir` and `manifest.path().parent()` as `manifest_dir`, which indicates the intended semantics.
### Fix Focus Areas
- pacquet/crates/package-manager/src/install.rs[1291-1322]
- pacquet/crates/package-manager/src/optimistic_repeat_install.rs[411-421]
### Suggested fix
1. Change `check_importer_satisfies()` to accept an additional `lockfile_dir`/`workspace_root` parameter (the base directory for resolving overrides).
2. Build the `VersionsOverrider` with that base directory.
3. When applying, pass `manifest.path().parent()` (importer dir) as `manifest_dir`.
4. Update call sites:
- `check_lockfile_freshness()` should pass the manifest dir (single-project) as both base and importer dir.
- `modified_manifests_match_lockfile()` should pass `workspace_root` as the override base, and each project’s `manifest.path().parent()` as the importer dir.

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


Grey Divider

Qodo Logo

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7edfb88d-2fa6-432f-b6c7-4e795bd027f2

📥 Commits

Reviewing files that changed from the base of the PR and between e16083a and a7bbf85.

📒 Files selected for processing (6)
  • .changeset/fast-repeat-install-restores-lockfile.md
  • deps/status/src/checkDepsStatus.ts
  • deps/status/test/checkDepsStatus.test.ts
  • installing/commands/src/install.ts
  • installing/commands/src/installDeps.ts
  • installing/commands/test/install.ts
✅ Files skipped from review due to trivial changes (2)
  • installing/commands/src/install.ts
  • .changeset/fast-repeat-install-restores-lockfile.md
📜 Recent 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). (8)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Code Coverage
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use Standard Style with trailing commas, prefer functions over classes, declare functions after they are used (relying on hoisting), limit functions to no more than two or three arguments, and use a single options object for functions needing more parameters
Follow import order: standard libraries first, then external dependencies (sorted alphabetically), then relative imports
Do not write comments that restate what the code already says; rename variables, split helpers, or move checks to more obvious places instead
Do not repeat documentation at call sites that already lives on the callee; update the JSDoc once and let every call site benefit
Use JSDoc for the function's contract (preconditions, postconditions, edge cases, why the function exists), not for re-narrating the function body
Do not record past implementation shape, refactor history, or removed code in comments; use git log and git blame for that information instead
Write comments only when the reason for code is non-obvious, a hidden invariant exists, a workaround for a known bug is needed, or an exception to surrounding pattern is deliberate

Files:

  • installing/commands/test/install.ts
  • installing/commands/src/installDeps.ts
  • deps/status/test/checkDepsStatus.test.ts
  • deps/status/src/checkDepsStatus.ts
**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use instanceof Error for checking if a caught error is an Error object in Jest tests; use util.types.isNativeError() instead to work across realms

Files:

  • deps/status/test/checkDepsStatus.test.ts
🧠 Learnings (3)
📚 Learning: 2026-05-14T09:04:00.133Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11622
File: resolving/npm-resolver/test/publishedBy.test.ts:350-354
Timestamp: 2026-05-14T09:04:00.133Z
Learning: In the pnpm/pnpm repository, ESLint is the authoritative style linter. Do not raise review findings for missing trailing commas in multiline function calls (e.g., `fs.writeFileSync(...)`) when this repo’s ESLint configuration does not report them and lint passes. Prefer deferring to the ESLint results for this specific trailing-comma rule rather than enforcing it manually in code review.

Applied to files:

  • installing/commands/test/install.ts
  • installing/commands/src/installDeps.ts
  • deps/status/test/checkDepsStatus.test.ts
  • deps/status/src/checkDepsStatus.ts
📚 Learning: 2026-06-05T13:47:05.929Z
Learnt from: vsumner
Repo: pnpm/pnpm PR: 12190
File: installing/deps-installer/test/install/frozenStore.ts:2-17
Timestamp: 2026-06-05T13:47:05.929Z
Learning: In the pnpm/pnpm repository, the shared Jest preset keeps `injectGlobals` at its default (`true`), so `test` and `expect` are available as Jest globals. Therefore, reviewers should not flag (or treat as TypeScript/compilation errors) missing `import { test, expect } from 'jest/globals'` when a test file uses `test`/`expect` without importing them. Importing from `jest/globals` may still be used for consistency with sibling files, but it is not required for execution in this repo unless a Jest preset is explicitly configured with `injectGlobals: false`.

Applied to files:

  • installing/commands/test/install.ts
  • deps/status/test/checkDepsStatus.test.ts
📚 Learning: 2026-06-05T13:47:26.046Z
Learnt from: vsumner
Repo: pnpm/pnpm PR: 12190
File: installing/deps-installer/src/install/index.ts:2337-2343
Timestamp: 2026-06-05T13:47:26.046Z
Learning: In the pnpm/pnpm codebase, `PnpmError` automatically prefixes `err.code` with `ERR_PNPM_` when you pass a code that does not already start with `ERR_PNPM_` (it normalizes `this.code` via `code.startsWith('ERR_PNPM_') ? code : `ERR_PNPM_${code}``). Therefore, during code review you should NOT flag `new PnpmError(...)` call sites for passing a bare error code (e.g., `new PnpmError('FROZEN_STORE_INCOMPATIBLE_WITH_PNPR', ...)`); the resulting `err.code` will still be `ERR_PNPM_FROZEN_STORE_INCOMPATIBLE_WITH_PNPR`.

Applied to files:

  • installing/commands/test/install.ts
  • installing/commands/src/installDeps.ts
  • deps/status/test/checkDepsStatus.test.ts
  • deps/status/src/checkDepsStatus.ts
🔇 Additional comments (9)
deps/status/src/checkDepsStatus.ts (4)

71-76: LGTM!

Also applies to: 83-94, 631-642


279-282: LGTM!

Also applies to: 312-319


287-331: LGTM!


432-446: LGTM!

Also applies to: 507-516

deps/status/test/checkDepsStatus.test.ts (1)

568-792: LGTM!

installing/commands/src/installDeps.ts (3)

20-20: LGTM!

Also applies to: 175-175


182-196: LGTM!


597-619: LGTM!

installing/commands/test/install.ts (1)

179-201: LGTM!


📝 Walkthrough

Walkthrough

This PR refactors the optimistic repeat-install fast-path to accept a struct input, replaces the mtime-unchanged shortcut with manifest-stat + content rechecks against wanted/current lockfiles (with optional regeneration), and extracts override parsing and per-importer satisfiability into pub(crate) helpers; tests updated for new flows and offline scenarios.

Changes

Optimistic Repeat-Install Refactor

Layer / File(s) Summary
API Contract: OptimisticRepeatInstallCheck Struct
pacquet/crates/package-manager/src/optimistic_repeat_install.rs
Module-level docs updated; OptimisticRepeatInstallCheck<'a> added and check_optimistic_repeat_install signature changed to accept &OptimisticRepeatInstallCheck; imports expanded for catalogs, lockfile helpers, and workspace-state updates.
Freshness Gating: Override Parsing and Importer Satisfaction Helpers
pacquet/crates/package-manager/src/install.rs
Added parse_config_overrides and check_importer_satisfies; check_lockfile_freshness refactored to parse overrides, pass parsed overrides into settings-drift checks, and early-return when ignore_manifest_check is enabled; FreshnessCheckError and build_workspace_state made pub(crate).
Content Re-Check: Manifest Statting and Lockfile Validation
pacquet/crates/package-manager/src/optimistic_repeat_install.rs
Replaces manifests-unchanged shortcut with stat_manifests + modified_manifests_match_lockfile; loads wanted lockfile from disk or virtual-store current, parses overrides, verifies importer satisfiability and linked-package freshness, can regenerate wanted pnpm-lock.yaml when missing via regenerate_wanted_lockfile_if_missing.
Linked-Package Freshness Verification
pacquet/crates/package-manager/src/optimistic_repeat_install.rs
Introduces LinkedPackagesContext, ports linked_packages_are_up_to_date, and adds helpers for file-ref detection, distribution-tag detection, spec stripping, loose semver checks, and mtime conversion; updates lockfile equality assertion path.
Install.rs Caller Update and Struct Literal Construction
pacquet/crates/package-manager/src/install.rs
Optimistic repeat-install short-circuit now constructs OptimisticRepeatInstallCheck struct literal with named fields rather than calling with positional arguments.
Install Tests: Fixture Setup and Optimistic Behavior Control
pacquet/crates/package-manager/src/install/tests.rs
Regression tightened to “no lockfile anywhere” (omit wanted and current lockfiles), removed in-test in-memory lockfile, pass lockfile: None to Install; added install_then_go_offline() and touch_manifest() helpers and tests for offline short-circuit and lockfile regeneration; one test disables optimistic fast-path for second run.
Optimistic Repeat-Install Tests: Struct-Based Call Migration
pacquet/crates/package-manager/src/optimistic_repeat_install/tests.rs
Test imports updated to include OptimisticRepeatInstallCheck; local check(...) helper added for single-project defaults; broad migration of tests from positional calls to the helper or struct-literal invocation for workspace cases.
checkDepsStatus: wanted/current lockfile fallback & tests
deps/status/src/checkDepsStatus.ts, deps/status/test/checkDepsStatus.test.ts
Adds useGitBranchLockfile option and wantedLockfileToRestore payload; implements effective-wanted-lockfile selection and returns a restore payload when pnpm-lock.yaml is missing but current virtual-store lock exists; tests cover single-project and workspace fallback behavior.
installDeps CLI: restore wanted lockfile on fast-path
installing/commands/src/installDeps.ts, installing/commands/test/install.ts
Adds restoreWantedLockfileIfMissing which writes pnpm-lock.yaml from current lock when applicable; InstallCommandOptions types updated and a test verifies restored lockfile bytes under an unreachable registry scenario.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • pnpm/pnpm#11811: Related by skipping per-importer manifest checks when ignore_manifest_check is enabled.
  • pnpm/pnpm#11820: Similar refactor of parsing pnpm.overrides before freshness/drift checks.
  • pnpm/pnpm#11945: Overlaps in restructuring optimistic repeat-install inputs and call sites to use a struct-based API.

Suggested reviewers

  • anonrig

Poem

🐰 A struct arrives to tidy the run,

mtimes checked and lockfiles spun.
Overrides parsed, importers won,
Tests hop in — the job is done. 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: optimizing the repeat-install fast path through content checks and fallback to current lockfile.
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.

✏️ 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 investigate-benchmark-results

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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.

Comment thread pacquet/crates/package-manager/src/optimistic_repeat_install.rs Outdated
…-lock.yaml is missing

When `pnpm-lock.yaml` is absent but `node_modules` is intact, the
repeat-install fast path now reads `<virtual_store_dir>/lock.yaml` —
the record of what the previous install materialized — and treats it
as the wanted lockfile: the manifests are content-checked against it
and `pnpm-lock.yaml` is regenerated from it (byte-identical to what
the full install's synthesize-from-current path would rewrite) before
the check reports "Already up to date". Single-project installs with
no lockfile on either side still refuse the fast path, and
`lockfile: false` skips the regeneration.

Previously a deleted `pnpm-lock.yaml` always fell into the full
pipeline: the wanted lockfile got synthesized from the current one,
but the awaited lockfile-verification gate then missed its verdict
cache (`try_lockfile_verification_cache` bails before the content-hash
index when the lockfile file can't be stat'd) and re-fetched a
packument per locked package. In the benchmarks.vlt.sh
`cache+node_modules` / `node_modules` variations this cost
0.7-1.5 s on astro and 6.4-9.0 s on babylon per run; with the
fallback both are a ~40 ms (astro) / ~190 ms (babylon) no-op that
restores the lockfile.
@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 10.250 ± 0.083 10.175 10.397 1.90 ± 0.04
pacquet@main 10.299 ± 0.154 10.132 10.573 1.91 ± 0.05
pnpr@HEAD 5.394 ± 0.099 5.305 5.640 1.00 ± 0.03
pnpr@main 5.383 ± 0.111 5.318 5.687 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 10.250249586119999,
      "stddev": 0.08321113889249053,
      "median": 10.21988205142,
      "user": 3.17478852,
      "system": 3.2984231399999997,
      "min": 10.17485057092,
      "max": 10.39748326792,
      "times": [
        10.297570589920001,
        10.39748326792,
        10.182667221920001,
        10.21937879592,
        10.38346001592,
        10.17485057092,
        10.18370820392,
        10.22038530692,
        10.18630914292,
        10.25668274492
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 10.29870085852,
      "stddev": 0.1543646567154255,
      "median": 10.24238042792,
      "user": 3.16739262,
      "system": 3.2886327399999997,
      "min": 10.13212770892,
      "max": 10.57336370992,
      "times": [
        10.227825666920001,
        10.13450377192,
        10.57336370992,
        10.34928879892,
        10.20485324592,
        10.13212770892,
        10.368695305920001,
        10.52969467492,
        10.20972051292,
        10.25693518892
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 5.393506841219999,
      "stddev": 0.09925639986558536,
      "median": 5.35382475892,
      "user": 2.4514180199999998,
      "system": 2.9488596399999993,
      "min": 5.30461939792,
      "max": 5.639826300919999,
      "times": [
        5.30461939792,
        5.44465974292,
        5.34164661292,
        5.34087191292,
        5.33318717292,
        5.363681134919999,
        5.34841244192,
        5.639826300919999,
        5.45892661892,
        5.359237075919999
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 5.383372208420001,
      "stddev": 0.11149464275257254,
      "median": 5.34245931092,
      "user": 2.4302030199999995,
      "system": 2.9678795399999998,
      "min": 5.31795368192,
      "max": 5.68710968892,
      "times": [
        5.68710968892,
        5.323644252919999,
        5.31795368192,
        5.3410234579199996,
        5.32994962092,
        5.41310078492,
        5.4008259899199995,
        5.35003436292,
        5.326185079919999,
        5.34389516392
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 648.2 ± 7.6 637.8 664.4 1.00
pacquet@main 680.9 ± 87.6 630.6 926.6 1.05 ± 0.14
pnpr@HEAD 777.2 ± 36.4 742.9 844.6 1.20 ± 0.06
pnpr@main 771.3 ± 63.1 735.4 946.2 1.19 ± 0.10
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6482023312,
      "stddev": 0.0075624372534389825,
      "median": 0.6468382539,
      "user": 0.3865014600000001,
      "system": 1.31522818,
      "min": 0.6378281009,
      "max": 0.6643879159,
      "times": [
        0.6484191839,
        0.6378281009,
        0.6552784449,
        0.6410641699,
        0.6433187269,
        0.6643879159,
        0.6469698149,
        0.6517714859,
        0.6462787759,
        0.6467066929
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.6808613608,
      "stddev": 0.08764509432998406,
      "median": 0.6573317099,
      "user": 0.37195596,
      "system": 1.32112688,
      "min": 0.6305525709,
      "max": 0.9265953939,
      "times": [
        0.6533578529,
        0.6644046659,
        0.6456491899,
        0.6613055669,
        0.9265953939,
        0.6665474389,
        0.6799830639000001,
        0.6446915769,
        0.6355262879,
        0.6305525709
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.7771527297,
      "stddev": 0.03636939823259288,
      "median": 0.7691421514000001,
      "user": 0.40929306,
      "system": 1.32843358,
      "min": 0.7429166229,
      "max": 0.8446161109,
      "times": [
        0.7429166229,
        0.7655972109,
        0.7726870919000001,
        0.7474180659,
        0.8393295129,
        0.7799929129000001,
        0.7505627059000001,
        0.7535464839,
        0.8446161109,
        0.7748605789
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.7713410014000001,
      "stddev": 0.06312405646409608,
      "median": 0.7514588014000001,
      "user": 0.39421086,
      "system": 1.3144451799999999,
      "min": 0.7353625369,
      "max": 0.9462116429,
      "times": [
        0.7533312769,
        0.7586677089,
        0.7406147389000001,
        0.7588764679000001,
        0.9462116429,
        0.7859342629,
        0.7377620139000001,
        0.7353625369,
        0.7470630389,
        0.7495863259000001
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 9.236 ± 0.039 9.166 9.311 1.81 ± 0.04
pacquet@main 9.211 ± 0.047 9.163 9.318 1.80 ± 0.04
pnpr@HEAD 5.108 ± 0.104 5.045 5.398 1.00
pnpr@main 5.130 ± 0.126 5.046 5.450 1.00 ± 0.03
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 9.2361865565,
      "stddev": 0.03943421382690157,
      "median": 9.234001786299999,
      "user": 3.5936060399999996,
      "system": 3.2745829200000003,
      "min": 9.1655423223,
      "max": 9.3108161133,
      "times": [
        9.254976987300001,
        9.2682791343,
        9.3108161133,
        9.1655423223,
        9.2428634013,
        9.2231818003,
        9.2086108563,
        9.2525228123,
        9.2251401713,
        9.209931966300001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 9.2108817493,
      "stddev": 0.04670374424342645,
      "median": 9.1934899128,
      "user": 3.5345855399999997,
      "system": 3.28818082,
      "min": 9.1631616143,
      "max": 9.3179181363,
      "times": [
        9.3179181363,
        9.1760942233,
        9.2599904733,
        9.1631616143,
        9.2057232843,
        9.1848938793,
        9.1853879213,
        9.2286681353,
        9.194513992300001,
        9.1924658333
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 5.1084145446,
      "stddev": 0.10436044844784274,
      "median": 5.0765626333,
      "user": 2.2767361399999997,
      "system": 2.85607462,
      "min": 5.0452625723,
      "max": 5.3976340363,
      "times": [
        5.0755161173,
        5.105489485300001,
        5.1127604523,
        5.0452625723,
        5.0544525143,
        5.0776091493,
        5.1019007403000005,
        5.0609102393,
        5.0526101393000005,
        5.3976340363
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 5.1302138009,
      "stddev": 0.12646576265401605,
      "median": 5.090505605300001,
      "user": 2.2755872399999997,
      "system": 2.84102492,
      "min": 5.0458204313,
      "max": 5.4500411533,
      "times": [
        5.0532381723,
        5.0480072233,
        5.0730240663,
        5.0458204313,
        5.1115122513,
        5.1225208283,
        5.2378288853,
        5.4500411533,
        5.1079871443,
        5.0521578533
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.409 ± 0.022 1.365 1.448 2.09 ± 0.06
pacquet@main 1.374 ± 0.008 1.363 1.385 2.04 ± 0.05
pnpr@HEAD 0.677 ± 0.009 0.667 0.700 1.00 ± 0.03
pnpr@main 0.675 ± 0.017 0.658 0.715 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.40856376824,
      "stddev": 0.022025648761858682,
      "median": 1.41142602684,
      "user": 1.6050851800000003,
      "system": 1.7553874999999999,
      "min": 1.36523586784,
      "max": 1.44820557784,
      "times": [
        1.42411936784,
        1.41471889584,
        1.41873968184,
        1.41048348584,
        1.4026996438400001,
        1.44820557784,
        1.40115573184,
        1.41236856784,
        1.36523586784,
        1.38791086184
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.3738036131400002,
      "stddev": 0.007806097106627103,
      "median": 1.37530083934,
      "user": 1.5338213799999998,
      "system": 1.7620035,
      "min": 1.36269107984,
      "max": 1.38511227484,
      "times": [
        1.38511227484,
        1.37526021084,
        1.36338126784,
        1.36269107984,
        1.3770274468400001,
        1.38027648384,
        1.37202102784,
        1.38146957684,
        1.36545529484,
        1.37534146784
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.67711073514,
      "stddev": 0.009095088254549764,
      "median": 0.6746231713399999,
      "user": 0.34612268,
      "system": 1.2905897,
      "min": 0.66678328884,
      "max": 0.7002554708400001,
      "times": [
        0.7002554708400001,
        0.6774577968400001,
        0.67783144384,
        0.6735115418400001,
        0.6818622588400001,
        0.67366886484,
        0.67557747784,
        0.6728404518400001,
        0.67131875584,
        0.66678328884
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.67478137194,
      "stddev": 0.017466607436591314,
      "median": 0.67003145334,
      "user": 0.33475007999999995,
      "system": 1.2696439,
      "min": 0.65775206084,
      "max": 0.7153379588400001,
      "times": [
        0.68701316384,
        0.68105901684,
        0.67004886884,
        0.7153379588400001,
        0.6588741678400001,
        0.65775206084,
        0.65827547284,
        0.66965657784,
        0.67978239384,
        0.67001403784
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 5.002 ± 0.019 4.978 5.027 7.08 ± 0.53
pacquet@main 4.985 ± 0.021 4.944 5.011 7.05 ± 0.53
pnpr@HEAD 0.717 ± 0.079 0.664 0.934 1.02 ± 0.14
pnpr@main 0.707 ± 0.053 0.670 0.845 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 5.0020508989,
      "stddev": 0.01914691217865675,
      "median": 5.003364528700001,
      "user": 1.7861380400000002,
      "system": 1.9010909599999999,
      "min": 4.9783028407,
      "max": 5.0270478047,
      "times": [
        5.0183527867,
        4.9997946847,
        4.9824467407,
        4.9783028407,
        5.0235762057,
        4.9857811657,
        5.0069343727,
        4.9806081717,
        5.0176642157,
        5.0270478047
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.9852695889000005,
      "stddev": 0.020694617136304695,
      "median": 4.987522306200001,
      "user": 1.7703771400000001,
      "system": 1.8883508599999996,
      "min": 4.9437941517,
      "max": 5.0111509647,
      "times": [
        4.9961476337,
        4.9437941517,
        4.9776931717,
        5.0111509647,
        4.9788545957,
        4.9818541107000005,
        4.9628946527,
        5.0022387667,
        5.0048773397,
        4.9931905017
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.7174659067,
      "stddev": 0.07934262420780244,
      "median": 0.6901247437,
      "user": 0.35191363999999997,
      "system": 1.30041496,
      "min": 0.6641326697,
      "max": 0.9337536837,
      "times": [
        0.7486656707,
        0.6873900907,
        0.9337536837,
        0.7009255727,
        0.6858668947000001,
        0.6814154267,
        0.7048114197,
        0.6641326697,
        0.6748382417000001,
        0.6928593967
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.7066373518000001,
      "stddev": 0.0525167253624754,
      "median": 0.6829330612,
      "user": 0.33574724,
      "system": 1.27428916,
      "min": 0.6699019347,
      "max": 0.8450006827000001,
      "times": [
        0.7126766987,
        0.8450006827000001,
        0.7373419847,
        0.6944976127,
        0.6811593527,
        0.6841495977000001,
        0.6787847277,
        0.6817165247,
        0.6699019347,
        0.6811444017
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12315
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
9,236.19 ms
(+7.41%)Baseline: 8,599.00 ms
10,318.80 ms
(89.51%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
5,002.05 ms
(-0.40%)Baseline: 5,022.30 ms
6,026.76 ms
(83.00%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,408.56 ms
(-0.97%)Baseline: 1,422.37 ms
1,706.85 ms
(82.52%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
10,250.25 ms
(+2.04%)Baseline: 10,045.52 ms
12,054.63 ms
(85.03%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
648.20 ms
(-0.82%)Baseline: 653.59 ms
784.31 ms
(82.65%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12315
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
5,108.41 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
717.47 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
677.11 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
5,393.51 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
777.15 ms
🐰 View full continuous benchmarking report in Bencher

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

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 21d09e0

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.00      7.6±0.09ms   569.8 KB/sec    1.00      7.6±0.23ms   570.5 KB/sec

@codecov-commenter

codecov-commenter commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.32044% with 64 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.65%. Comparing base (bf1b731) to head (a7bbf85).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
...s/package-manager/src/optimistic_repeat_install.rs 81.33% 56 Missing ⚠️
pacquet/crates/package-manager/src/install.rs 87.09% 8 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12315      +/-   ##
==========================================
- Coverage   87.70%   87.65%   -0.06%     
==========================================
  Files         288      288              
  Lines       35177    35475     +298     
==========================================
+ Hits        30852    31094     +242     
- Misses       4325     4381      +56     

☔ 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.

…o-end

Add two regression tests that install for real against the mock
registry, then drop the registry, wipe the packument/verdict cache,
and point the repeat install at a dead port — the exact state the
vlt.sh benchmark prepare step produces. Any future change that lets a
touched-but-unchanged manifest or a deleted pnpm-lock.yaml fall off
the fast path (into resolution or the lockfile-verification fan-out)
now fails the install instead of silently regressing the benchmark:

- a manifest rewritten with identical content (newer mtime) must
  short-circuit with "Already up to date" and zero pipeline events;
- a deleted pnpm-lock.yaml with intact node_modules must
  short-circuit via the current-as-wanted fallback and restore
  pnpm-lock.yaml byte-identically.

Both verified discriminating: disabling the content check makes both
fail on the dead registry.
@qodo-free-for-open-source-projects

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

Copy link
Copy Markdown

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

zkochan added 3 commits June 10, 2026 20:58
…sing

Port of the same optimization that landed on the pacquet side in this
branch. When `pnpm-lock.yaml` is absent but `node_modules` is intact,
`checkDepsStatus` lets the current lockfile
(`node_modules/.pnpm/lock.yaml`) — the record of what the previous
install materialized — stand in as the wanted lockfile for the
up-to-date checks (settings drift, `satisfiesPackageManifest`,
`linkedPackagesAreUpToDate`), and returns it so `installDeps` can
restore `pnpm-lock.yaml` from it before reporting "Already up to
date". Previously this scenario threw
`RUN_CHECK_DEPS_LOCKFILE_NOT_FOUND` (single project) or failed the
wanted-lockfile stat (workspace), forcing a full resolution plus a
re-verification of every locked package against the registry — in the
benchmarks.vlt.sh `node_modules` variations that cost pnpm 2.2 s
(astro) to 11.6 s (babylon) per run.

Single projects with no lockfile on either side still refuse the fast
path, `useLockfile: false` skips the restore, a failed restore falls
through to the full install, and the stand-in is disabled under
`useGitBranchLockfile` (there a missing `pnpm-lock.yaml` is the steady
state and the branch lockfile may legitimately differ from the current
one).
@qodo-free-for-open-source-projects

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

Copy link
Copy Markdown

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

@zkochan zkochan changed the title perf(pacquet): content-check modified manifests before leaving the repeat-install fast path perf: content-check modified manifests and fall back to the current lockfile on the repeat-install fast path (pacquet + pnpm) Jun 10, 2026
@zkochan zkochan merged commit d976edf into main Jun 10, 2026
28 checks passed
@zkochan zkochan deleted the investigate-benchmark-results branch June 10, 2026 19:24
KSXGitHub pushed a commit that referenced this pull request Jun 10, 2026
Integrate the 9 commits main gained (#12271, #12294, #12301, #12303,
#12305, #12312, #12315, #12316, and the release/version bumps).

Conflict resolution: all four conflicts (record_lockfile_verified,
build_modules, hoisted_dep_graph, install) were between this branch's
lint edits and main's feature changes — took main's authoritative
versions; lint compliance is re-derived by re-running clippy in the
follow-up commit.
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