From ccd3e217e4598c8f3128a937e5764084be261ad8 Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Tue, 20 Oct 2026 11:53:52 +0200 Subject: [PATCH] chore(bench): add Cargo repeat-install scenario to integrated-benchmark (#17132) * chore(bench): add Cargo repeat-install scenario to integrated-benchmark Add the cargo.repeat-install.hot-cache.hot-store scenario to pnpm-integrated-benchmark and the CI integrated-benchmark workflow to guard Cargo crate slot reuse (pnpm/pnpm#17105). * fix(bench): isolate cargo repeat-install benchmark from parent workspace * fix(bench): connect generated crates in Cargo dependency graph and verify prewarm slots --- .../scripts/compare-integrated-benchmarks.mjs | 3 +- .../compare-integrated-benchmarks.test.mjs | 4 +- .../pacquet-integrated-benchmark.yml | 32 +++- Cargo.lock | 3 + pnpm/CONTRIBUTING.md | 9 ++ pnpm/tasks/integrated-benchmark/Cargo.toml | 3 + .../integrated-benchmark/src/cli_args.rs | 25 ++- .../integrated-benchmark/src/work_env.rs | 21 ++- .../src/work_env/cargo_workspace.rs | 146 ++++++++++++++++++ .../src/work_env/fixtures.rs | 24 ++- .../src/work_env/scripts.rs | 10 +- .../src/work_env/tests.rs | 43 +++++- .../src/workspace_manifest.rs | 8 + 13 files changed, 308 insertions(+), 23 deletions(-) create mode 100644 pnpm/tasks/integrated-benchmark/src/work_env/cargo_workspace.rs diff --git a/.github/scripts/compare-integrated-benchmarks.mjs b/.github/scripts/compare-integrated-benchmarks.mjs index ae09ac469a..7c35c18f73 100644 --- a/.github/scripts/compare-integrated-benchmarks.mjs +++ b/.github/scripts/compare-integrated-benchmarks.mjs @@ -17,6 +17,7 @@ export const scenarios = [ 'ISOLATED_FRESH_RESTORE_COLD_CACHE_COLD_STORE_COLD_PNPR', 'GVS_FRESH_RESTORE_HOT_CACHE_HOT_STORE', 'STORE_ONLY_UNLOCKED_DIRECT_DEPS_COLD_CACHE_HOT_STORE', + 'CARGO_REPEAT_INSTALL_HOT_CACHE_HOT_STORE', ] export function compare (report, engine) { @@ -78,7 +79,7 @@ export async function renderComparison (directory) { let failed = false for (const scenario of scenarios) { const report = JSON.parse(await readFile(join(directory, `BENCHMARK_REPORT_${scenario}.json`), 'utf8')) - const engines = scenario.includes('PEER_HEAVY') || scenario.includes('LINKED_WORKSPACE') || scenario.includes('INJECTED_WORKSPACE') || scenario.includes('STORE_ONLY') ? ['pacquet'] : ['pacquet', 'pnpr'] + const engines = scenario.includes('PEER_HEAVY') || scenario.includes('LINKED_WORKSPACE') || scenario.includes('INJECTED_WORKSPACE') || scenario.includes('STORE_ONLY') || scenario.startsWith('CARGO_') ? ['pacquet'] : ['pacquet', 'pnpr'] for (const engine of engines) { const result = compareConfirmed(report, engine) failed ||= result.status === 'Regression' diff --git a/.github/scripts/compare-integrated-benchmarks.test.mjs b/.github/scripts/compare-integrated-benchmarks.test.mjs index 0ec5e3bca8..ad696646c8 100644 --- a/.github/scripts/compare-integrated-benchmarks.test.mjs +++ b/.github/scripts/compare-integrated-benchmarks.test.mjs @@ -73,7 +73,7 @@ test('all scenarios and both engines are checked, and missing reports fail', asy try { for (const scenario of scenarios) { const input = report(stable, stable) - if (!scenario.includes('PEER_HEAVY') && !scenario.includes('LINKED_WORKSPACE') && !scenario.includes('INJECTED_WORKSPACE') && !scenario.includes('STORE_ONLY')) { + if (!scenario.includes('PEER_HEAVY') && !scenario.includes('LINKED_WORKSPACE') && !scenario.includes('INJECTED_WORKSPACE') && !scenario.includes('STORE_ONLY') && !scenario.startsWith('CARGO_')) { input.results.push(command('pnpr@HEAD', Array(10).fill(1.4)), command('pnpr@main', stable)) } input.confirmation = { results: input.results } @@ -82,7 +82,7 @@ test('all scenarios and both engines are checked, and missing reports fail', asy const result = await renderComparison(directory) assert.equal(result.failed, true) assert.equal(result.markdown.match(/\| Regression \|/g).length, 10) - assert.equal(result.markdown.match(/\| Within tolerance \|/g).length, 14) + assert.equal(result.markdown.match(/\| Within tolerance \|/g).length, 15) const cli = new URL('./compare-integrated-benchmarks.mjs', import.meta.url) const run = spawnSync(process.execPath, [fileURLToPath(cli), directory], { encoding: 'utf8' }) assert.equal(run.status, 1) diff --git a/.github/workflows/pacquet-integrated-benchmark.yml b/.github/workflows/pacquet-integrated-benchmark.yml index 0d21c1f80b..bed3ec102e 100644 --- a/.github/workflows/pacquet-integrated-benchmark.yml +++ b/.github/workflows/pacquet-integrated-benchmark.yml @@ -276,6 +276,7 @@ jobs: - isolated-linker.fresh-restore.cold-cache.cold-store.cold-pnpr - gvs-linker.fresh-restore.hot-cache.hot-store - store-only.unlocked-direct-deps.cold-cache.hot-store + - cargo.repeat-install.hot-cache.hot-store name: ${{ matrix.scenario }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} env: @@ -346,7 +347,8 @@ jobs: if: >- matrix.scenario != 'isolated-linker.peer-heavy-resolve.hot-cache.offline' && matrix.scenario != 'isolated-linker.linked-workspace-resolve.hot-cache.offline' && - matrix.scenario != 'isolated-linker.injected-workspace-repeat-install.hot-cache.hot-store' + matrix.scenario != 'isolated-linker.injected-workspace-repeat-install.hot-cache.hot-store' && + matrix.scenario != 'cargo.repeat-install.hot-cache.hot-store' # Persists pnpm-registry's `--storage` dir across CI runs so # cold-cache benchmark scenarios don't refetch ~2.3k unscoped # packages from npmjs on every run. Replaces the older @@ -682,6 +684,15 @@ jobs: cp bench-work-env/BENCHMARK_REPORT.md bench-work-env/BENCHMARK_REPORT_STORE_ONLY_UNLOCKED_DIRECT_DEPS_COLD_CACHE_HOT_STORE.md cp bench-work-env/BENCHMARK_REPORT.json bench-work-env/BENCHMARK_REPORT_STORE_ONLY_UNLOCKED_DIRECT_DEPS_COLD_CACHE_HOT_STORE.json + - name: 'Benchmark: Cargo: repeat install, hot cache + hot store' + if: matrix.scenario == 'cargo.repeat-install.hot-cache.hot-store' + shell: bash + timeout-minutes: 20 + run: | + node .github/scripts/run-integrated-benchmark.mjs --reuse-prebuilt-binaries --scenario=cargo.repeat-install.hot-cache.hot-store --registry=npm $WORKSPACE_BENCHMARK_TARGETS + cp bench-work-env/BENCHMARK_REPORT.md bench-work-env/BENCHMARK_REPORT_CARGO_REPEAT_INSTALL_HOT_CACHE_HOT_STORE.md + cp bench-work-env/BENCHMARK_REPORT.json bench-work-env/BENCHMARK_REPORT_CARGO_REPEAT_INSTALL_HOT_CACHE_HOT_STORE.json + # When a scenario fails, hyperfine aborts and the benchmarked install's # own output is invisible: each target's script redirects stdout+stderr to # `bench-work-env//BENCHMARK_OUTPUT.ndjson` (to keep hyperfine's @@ -781,7 +792,7 @@ jobs: echo echo "**Commit:** [\`${SHA:0:12}\`](${{ github.server_url }}/${{ github.repository }}/commit/$SHA)" echo - echo 'Regular scenarios report direct and pnpr installs. The peer-heavy resolver scenario compares current Rust, main Rust, and TypeScript pnpm; the linked and injected workspace scenarios compare current Rust against main Rust. Bencher consumes pacquet@HEAD and pnpr@HEAD.' + echo 'Regular scenarios report direct and pnpr installs. The peer-heavy resolver scenario compares current Rust, main Rust, and TypeScript pnpm; the linked, injected, and Cargo workspace scenarios compare current Rust against main Rust. Bencher consumes pacquet@HEAD and pnpr@HEAD.' echo echo 'The tables below show mean ± σ. Bencher tracks absolute minimum latency; PR regression checks compare HEAD and main samples from the same runner. Historical timing alerts do not gate PRs.' echo @@ -956,6 +967,20 @@ jobs: echo '```' echo echo '' + echo + echo '### Scenario: Cargo: repeat install, hot cache + hot store' + echo + echo 'Frozen repeat install of a Cargo workspace (`cargo.enabled: true`) whose crates are already materialized in `/v11/crates/` and linked into `.pnpm/crates/crates-io`. Direct installs only.' + echo + cat bench-work-env/BENCHMARK_REPORT_CARGO_REPEAT_INSTALL_HOT_CACHE_HOT_STORE.md + echo + echo '
BENCHMARK_REPORT.json' + echo + echo '```json' + jq 'del(.results[].exit_codes, .results[].times, .confirmation.results[]?.exit_codes, .confirmation.results[]?.times)' bench-work-env/BENCHMARK_REPORT_CARGO_REPEAT_INSTALL_HOT_CACHE_HOT_STORE.json + echo '```' + echo + echo '
' ) > bench-work-env/SUMMARY.md - name: Build Bencher-shaped report @@ -1014,7 +1039,8 @@ jobs: 'ISOLATED_INJECTED_WORKSPACE_REPEAT_INSTALL_HOT_CACHE_HOT_STORE:isolated-linker.injected-workspace-repeat-install.hot-cache.hot-store' \ 'ISOLATED_FRESH_RESTORE_COLD_CACHE_COLD_STORE_COLD_PNPR:isolated-linker.fresh-restore.cold-cache.cold-store.cold-pnpr' \ 'GVS_FRESH_RESTORE_HOT_CACHE_HOT_STORE:gvs-linker.fresh-restore.hot-cache.hot-store' \ - 'STORE_ONLY_UNLOCKED_DIRECT_DEPS_COLD_CACHE_HOT_STORE:store-only.unlocked-direct-deps.cold-cache.hot-store' + 'STORE_ONLY_UNLOCKED_DIRECT_DEPS_COLD_CACHE_HOT_STORE:store-only.unlocked-direct-deps.cold-cache.hot-store' \ + 'CARGO_REPEAT_INSTALL_HOT_CACHE_HOT_STORE:cargo.repeat-install.hot-cache.hot-store' build_bencher_file bench-work-env/bencher-results-pnpr.json pnpr@HEAD \ 'ISOLATED_FRESH_RESTORE_COLD_CACHE_COLD_STORE:isolated-linker.fresh-restore.cold-cache.cold-store' \ diff --git a/Cargo.lock b/Cargo.lock index 4408789d3c..fd0806526d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5428,11 +5428,14 @@ dependencies = [ "pnpm-fs", "pnpm-lockfile", "pnpm-registry-mock", + "pnpm-store-dir", + "pnpm-tarball", "pnpr-storage", "reqwest", "serde", "serde-saphyr", "serde_json", + "ssri", "tempfile", "tokio", "which 8.0.4", diff --git a/pnpm/CONTRIBUTING.md b/pnpm/CONTRIBUTING.md index 1f04287ee8..608d82a43c 100644 --- a/pnpm/CONTRIBUTING.md +++ b/pnpm/CONTRIBUTING.md @@ -225,6 +225,15 @@ just integrated-benchmark --scenario=isolated-linker.injected-workspace-repeat-i This generated fixture uses hardlinked local files and needs no registry packages. Its untimed install creates the lockfile and populates `node_modules` before measurement. +To measure frozen repeat installs of a Cargo workspace (`cargo.enabled: true`) with crates already materialized in the store: + +```sh +just integrated-benchmark --scenario=cargo.repeat-install.hot-cache.hot-store --registry=npm pacquet@HEAD pacquet@main +``` + +This generated fixture seeds its crate files into the local CAFS store and needs no registry requests. +Its untimed install populates the store crate slots and `.pnpm/crates` before measurement. + ### CI regression checks Each PR benchmark scenario measures HEAD and main on the same runner. The check diff --git a/pnpm/tasks/integrated-benchmark/Cargo.toml b/pnpm/tasks/integrated-benchmark/Cargo.toml index bfbcddf9f8..9d19a41162 100644 --- a/pnpm/tasks/integrated-benchmark/Cargo.toml +++ b/pnpm/tasks/integrated-benchmark/Cargo.toml @@ -18,6 +18,8 @@ path = "src/main.rs" pnpm-fs = { workspace = true } pnpm-lockfile = { workspace = true } pnpm-registry-mock = { workspace = true } +pnpm-store-dir = { workspace = true } +pnpm-tarball = { workspace = true } pnpr-storage = { workspace = true } clap = { workspace = true } @@ -28,6 +30,7 @@ reqwest = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } serde-saphyr = { workspace = true } +ssri = { workspace = true } tokio = { workspace = true } which = { workspace = true } diff --git a/pnpm/tasks/integrated-benchmark/src/cli_args.rs b/pnpm/tasks/integrated-benchmark/src/cli_args.rs index f05e9c2dc7..66e717dda8 100644 --- a/pnpm/tasks/integrated-benchmark/src/cli_args.rs +++ b/pnpm/tasks/integrated-benchmark/src/cli_args.rs @@ -297,6 +297,11 @@ pub enum BenchmarkScenario { /// between machines uses. #[value(name = "store-only.unlocked-direct-deps.cold-cache.hot-store")] StoreOnlyUnlockedDepsColdCacheHotStore, + /// Frozen repeat install of a populated Cargo workspace (`cargo.enabled: true`) + /// with crates already materialized in `/v11/crates/` and linked into + /// `.pnpm/crates/crates-io`. + #[value(name = "cargo.repeat-install.hot-cache.hot-store")] + CargoRepeatInstallHotCacheHotStore, } /// Per-iteration cleanup applied by hyperfine's `--prepare`. @@ -337,7 +342,8 @@ impl BenchmarkScenario { | BenchmarkScenario::IsolatedFreshRestoreColdCacheColdStoreColdPnpr | BenchmarkScenario::IsolatedFreshRestoreHotCacheHotStore | BenchmarkScenario::GvsFreshRestoreHotCacheHotStore - | BenchmarkScenario::IsolatedInjectedWorkspaceRepeatInstallHotCacheHotStore => { + | BenchmarkScenario::IsolatedInjectedWorkspaceRepeatInstallHotCacheHotStore + | BenchmarkScenario::CargoRepeatInstallHotCacheHotStore => { &["install", "--frozen-lockfile"] } BenchmarkScenario::IsolatedFreshAddDepHotCacheHotStore => &["add", "is-odd"], @@ -359,7 +365,8 @@ impl BenchmarkScenario { /// lockfile for a frozen install. `None` uses hyperfine's normal warmup. pub fn prewarm_install_args(self) -> Option<&'static [&'static str]> { match self { - BenchmarkScenario::IsolatedInjectedWorkspaceRepeatInstallHotCacheHotStore => { + BenchmarkScenario::IsolatedInjectedWorkspaceRepeatInstallHotCacheHotStore + | BenchmarkScenario::CargoRepeatInstallHotCacheHotStore => { Some(&["install", "--offline", "--no-frozen-lockfile"]) } BenchmarkScenario::IsolatedFreshResolveHotCacheOffline => Some(&["install"]), @@ -396,7 +403,8 @@ impl BenchmarkScenario { | BenchmarkScenario::IsolatedRepeatInstallHotCacheHotStore | BenchmarkScenario::IsolatedRepeatInstallColdCacheHotStore | BenchmarkScenario::IsolatedInjectedWorkspaceRepeatInstallHotCacheHotStore - | BenchmarkScenario::StoreOnlyUnlockedDepsColdCacheHotStore => true, + | BenchmarkScenario::StoreOnlyUnlockedDepsColdCacheHotStore + | BenchmarkScenario::CargoRepeatInstallHotCacheHotStore => true, } } @@ -409,6 +417,7 @@ impl BenchmarkScenario { self.lockfile_enabled() && !self.is_offline_fresh_resolve() && !self.uses_injected_workspace_fixture() + && !self.uses_cargo_workspace_fixture() } /// The `pnpm-lock.yaml` contents to seed during init, when @@ -457,9 +466,8 @@ impl BenchmarkScenario { // heavier content re-check. The populated `node_modules` (and // workspace state) come from the pre-warm pass. Self::IsolatedRepeatInstallHotCacheHotStore - | Self::IsolatedInjectedWorkspaceRepeatInstallHotCacheHotStore => { - Cleanup::removing(&[]) - } + | Self::IsolatedInjectedWorkspaceRepeatInstallHotCacheHotStore + | Self::CargoRepeatInstallHotCacheHotStore => Cleanup::removing(&[]), Self::IsolatedRepeatInstallColdCacheHotStore => Cleanup::removing(&["cache-dir"]), // The install writes the direct dependencies it resolved back // into the lockfile; the next iteration must find them missing @@ -557,12 +565,17 @@ impl BenchmarkScenario { self.uses_peer_heavy_fixture() || self.uses_linked_workspace_fixture() || self.uses_injected_workspace_fixture() + || self.uses_cargo_workspace_fixture() } pub fn uses_injected_workspace_fixture(self) -> bool { matches!(self, BenchmarkScenario::IsolatedInjectedWorkspaceRepeatInstallHotCacheHotStore) } + pub fn uses_cargo_workspace_fixture(self) -> bool { + matches!(self, BenchmarkScenario::CargoRepeatInstallHotCacheHotStore) + } + /// Whether the install fetches into the store and links nothing /// (`enableModulesDir: false`), reading a per-file store index. pub fn store_only(self) -> bool { diff --git a/pnpm/tasks/integrated-benchmark/src/work_env.rs b/pnpm/tasks/integrated-benchmark/src/work_env.rs index 1cf4118e48..b6a6afb5f0 100644 --- a/pnpm/tasks/integrated-benchmark/src/work_env.rs +++ b/pnpm/tasks/integrated-benchmark/src/work_env.rs @@ -8,8 +8,12 @@ use scripts::{ mod fixtures; -use fixtures::{create_npmrc, create_package_json, create_pnpm_workspace, save_pristine_copies}; +use fixtures::{ + create_generated_projects, create_npmrc, create_package_json, create_pnpm_workspace, + save_pristine_copies, +}; +mod cargo_workspace; mod injected_workspace; mod linked_workspace; mod unlocked_deps; @@ -285,12 +289,7 @@ impl WorkEnv { let registry = self.registry_for(id, direct_registry, revision_mocks); fs::create_dir_all(&dir).expect("create directory for the revision"); create_package_json(&dir, self.options.selection.fixture_dir.as_deref(), scenario); - if scenario.uses_injected_workspace_fixture() { - injected_workspace::create_project(&dir); - } - if scenario.uses_linked_workspace_fixture() { - linked_workspace::create_projects(&dir); - } + create_generated_projects(&dir, scenario); self.write_registry_config(&dir, registry); create_install_script(&dir, scenario, &WorkEnv::install_command(id), id); may_create_lockfile(&dir, scenario, self.options.selection.fixture_dir.as_deref()); @@ -429,16 +428,24 @@ impl WorkEnv { /// fetches tarballs, not packuments; an up-to-date `node_modules` /// short-circuits the install outright). fn prewarm_caches(&self, cleanup_command: &str) { + let scenario = + self.options.selection.scenario.expect("scenario set when prewarm_caches() is reached"); Command::new("bash") .current_dir(self.root()) .arg("-c") .arg(cleanup_command) .pipe_mut(executor("prewarm cleanup")); for id in self.benchmarked_ids() { + if scenario.uses_cargo_workspace_fixture() { + cargo_workspace::seed_store(&self.bench_dir(id)); + } eprintln!("Pre-warming caches for {id}..."); Command::new("bash") .arg(self.prewarm_script_path(id)) .pipe_mut(executor(PREWARM_SCRIPT)); + if scenario.uses_cargo_workspace_fixture() { + cargo_workspace::verify_materialized_crates(&self.bench_dir(id)); + } } } diff --git a/pnpm/tasks/integrated-benchmark/src/work_env/cargo_workspace.rs b/pnpm/tasks/integrated-benchmark/src/work_env/cargo_workspace.rs new file mode 100644 index 0000000000..2df47036ea --- /dev/null +++ b/pnpm/tasks/integrated-benchmark/src/work_env/cargo_workspace.rs @@ -0,0 +1,146 @@ +use pnpm_store_dir::{CafsFileInfo, PackageFilesIndex, StoreDir, StoreIndex}; +use pnpm_tarball::ArchiveStoreProjection; +use ssri::{Algorithm, Integrity}; +use std::{ + collections::HashMap, + fmt::Write, + fs, + path::Path, + time::{SystemTime, UNIX_EPOCH}, +}; + +pub(super) const CRATE_COUNT: usize = 800; +const CRATE_VERSION: &str = "1.0.0"; +const MODULE_COUNT: usize = 3; + +pub(super) fn root_manifest() -> String { + serde_json::json!({ + "name": "cargo-benchmark-root", + "version": "0.1.0", + "private": true, + }) + .to_string() +} + +pub(super) fn create_project(root: &Path) { + let src_dir = root.join("src"); + fs::create_dir_all(&src_dir).expect("create cargo workspace src directory"); + fs::write( + root.join("Cargo.toml"), + format!( + "[package]\nname = \"cargo-benchmark-root\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n\ + [workspace]\n\n\ + [dependencies]\n{} = \"{CRATE_VERSION}\"\n", + crate_name(0), + ), + ) + .expect("write root Cargo.toml"); + fs::write(src_dir.join("lib.rs"), "pub fn root() {}\n").expect("write root src/lib.rs"); + fs::write(root.join("Cargo.lock"), cargo_lock_contents()).expect("write Cargo.lock"); +} + +pub(super) fn seed_store(root: &Path) { + let store_dir = StoreDir::new(root.join("store-dir")); + let entries: Vec<(String, PackageFilesIndex)> = + (0..CRATE_COUNT).map(|index| seed_crate(&store_dir, index)).collect(); + let mut store_index = + StoreIndex::open_in(&store_dir).expect("open store index for cargo benchmark"); + store_index.set_many(entries).expect("insert cargo benchmark store index entries"); +} + +pub(super) fn verify_materialized_crates(root: &Path) { + let crates_io_dir = root.join(".pnpm/crates/crates-io"); + for index in 0..CRATE_COUNT { + let name = crate_name(index); + let link = crates_io_dir.join(format!("{name}-{CRATE_VERSION}")); + assert!( + link.join(".cargo-checksum.json").is_file(), + "expected prewarm to materialize crate slot at {}", + link.display(), + ); + } +} + +fn seed_crate(store_dir: &StoreDir, index: usize) -> (String, PackageFilesIndex) { + let name = crate_name(index); + let checksum = crate_checksum(index); + let integrity = Integrity::from_hex(&checksum, Algorithm::Sha256) + .expect("build crate integrity") + .to_string(); + let key = ArchiveStoreProjection::RawArchive.store_index_key( + &integrity, + &format!("crate:{name}@{CRATE_VERSION}"), + ); + let mut files = HashMap::with_capacity(MODULE_COUNT + 2); + let mut cargo_toml = format!( + "[package]\nname = \"{name}\"\nversion = \"{CRATE_VERSION}\"\nedition = \"2021\"\n", + ); + if index + 1 < CRATE_COUNT { + writeln!(cargo_toml, "\n[dependencies]\n{} = \"{CRATE_VERSION}\"", crate_name(index + 1)) + .expect("format crate Cargo.toml dependency"); + } + files.insert("Cargo.toml".to_string(), write_cas_blob(store_dir, cargo_toml.as_bytes())); + files.insert( + "src/lib.rs".to_string(), + write_cas_blob(store_dir, format!("pub const CRATE_INDEX: usize = {index};\n").as_bytes()), + ); + for module in 0..MODULE_COUNT { + files.insert( + format!("src/module_{module}.rs"), + write_cas_blob( + store_dir, + format!("pub const MODULE_{module}: usize = {index};\n").as_bytes(), + ), + ); + } + (key, PackageFilesIndex { algo: "sha512".to_string(), files, ..PackageFilesIndex::default() }) +} + +fn write_cas_blob(store_dir: &StoreDir, contents: &[u8]) -> CafsFileInfo { + let (_, file_hash) = + store_dir.write_cas_file(contents, false).expect("write cas file for cargo benchmark"); + let checked_at = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock after unix epoch") + .as_millis() as u64; + CafsFileInfo { + digest: format!("{file_hash:x}"), + mode: 0o644, + size: contents.len() as u64, + checked_at: Some(checked_at), + } +} + +fn cargo_lock_contents() -> String { + let mut lockfile = format!( + "# This file is automatically @generated by Cargo.\n# It is not intended for manual editing.\n\ + version = 3\n\n[[package]]\nname = \"cargo-benchmark-root\"\nversion = \"0.1.0\"\n\ + dependencies = [\n \"{}\",\n]\n", + crate_name(0), + ); + for index in 0..CRATE_COUNT { + writeln!( + lockfile, + "\n[[package]]\nname = \"{}\"\nversion = \"{CRATE_VERSION}\"\n\ + source = \"registry+https://github.com/rust-lang/crates.io-index\"\n\ + checksum = \"{}\"", + crate_name(index), + crate_checksum(index), + ) + .expect("format Cargo.lock package entry"); + if index + 1 < CRATE_COUNT { + writeln!(lockfile, "dependencies = [\n \"{}\",\n]", crate_name(index + 1)).expect( + "format Cargo.lock package dependencies", + ); + } + } + lockfile +} + +fn crate_name(index: usize) -> String { + format!("benchmark-crate-{index:04}") +} + +fn crate_checksum(index: usize) -> String { + format!("{:064x}", index + 1) +} diff --git a/pnpm/tasks/integrated-benchmark/src/work_env/fixtures.rs b/pnpm/tasks/integrated-benchmark/src/work_env/fixtures.rs index 1072787b41..85bf044c4b 100644 --- a/pnpm/tasks/integrated-benchmark/src/work_env/fixtures.rs +++ b/pnpm/tasks/integrated-benchmark/src/work_env/fixtures.rs @@ -3,8 +3,9 @@ use super::{ PEER_HEAVY_WIDTH, }; use crate::{ - cli_args::BenchmarkScenario, fixtures::PACKAGE_JSON, - workspace_manifest::MinimalWorkspaceManifest, + cli_args::BenchmarkScenario, + fixtures::PACKAGE_JSON, + workspace_manifest::{CargoSettings, MinimalWorkspaceManifest}, }; use serde_json::Value; use std::{ @@ -28,6 +29,11 @@ pub(super) fn create_package_json( .expect("write injected workspace root manifest"); return; } + if scenario.uses_cargo_workspace_fixture() { + fs::write(dst, super::cargo_workspace::root_manifest()) + .expect("write cargo workspace root manifest"); + return; + } if scenario.uses_linked_workspace_fixture() { fs::write(dst, super::linked_workspace::root_manifest()) .expect("write linked-workspace package.json"); @@ -42,6 +48,17 @@ pub(super) fn create_package_json( fs::write(dst, PACKAGE_JSON).expect("write package.json for the revision"); } } +pub(super) fn create_generated_projects(dir: &Path, scenario: BenchmarkScenario) { + if scenario.uses_injected_workspace_fixture() { + super::injected_workspace::create_project(dir); + } + if scenario.uses_cargo_workspace_fixture() { + super::cargo_workspace::create_project(dir); + } + if scenario.uses_linked_workspace_fixture() { + super::linked_workspace::create_projects(dir); + } +} pub(super) fn peer_heavy_root_manifest() -> String { let mut dependencies = serde_json::Map::new(); dependencies.insert( @@ -214,6 +231,9 @@ fn apply_scenario_settings(manifest: &mut MinimalWorkspaceManifest, scenario: Be if scenario.uses_injected_workspace_fixture() { manifest.dedupe_injected_deps = Some(false); } + if scenario.uses_cargo_workspace_fixture() { + manifest.cargo = Some(CargoSettings { enabled: true }); + } if scenario.uses_linked_workspace_fixture() || scenario.uses_injected_workspace_fixture() { manifest.packages = Some(vec![".".to_string(), "packages/*".to_string()]); } diff --git a/pnpm/tasks/integrated-benchmark/src/work_env/scripts.rs b/pnpm/tasks/integrated-benchmark/src/work_env/scripts.rs index 6fbc111c82..ac2a4d4027 100644 --- a/pnpm/tasks/integrated-benchmark/src/work_env/scripts.rs +++ b/pnpm/tasks/integrated-benchmark/src/work_env/scripts.rs @@ -20,7 +20,15 @@ use std::{ /// Empty one benchmark directory's install state and metrics log. pub(super) fn wipe_bench_dir(dir: &Path) { - for name in ["node_modules", "store-dir", "cache-dir", "pnpr-storage", "cold-mock-storage"] { + for name in [ + "node_modules", + ".pnpm", + ".cargo", + "store-dir", + "cache-dir", + "pnpr-storage", + "cold-mock-storage", + ] { let path = dir.join(name); if path.exists() { remove_dir_all_with_retry(&path).expect("pre-benchmark wipe"); diff --git a/pnpm/tasks/integrated-benchmark/src/work_env/tests.rs b/pnpm/tasks/integrated-benchmark/src/work_env/tests.rs index a17f81efbb..7bbd9b235d 100644 --- a/pnpm/tasks/integrated-benchmark/src/work_env/tests.rs +++ b/pnpm/tasks/integrated-benchmark/src/work_env/tests.rs @@ -3,7 +3,7 @@ use pnpr_storage::layout::local_document_path; use super::{ BenchId, HyperfineCommand, LINKED_WORKSPACE_DEPTH, LINKED_WORKSPACE_WIDTH, PEER_HEAVY_DEPTH, PEER_HEAVY_PROVIDER, PEER_HEAVY_WIDTH, WorkEnv, collect_pnpr_direct_ratios, - create_install_script, create_package_json, create_pnpm_workspace, + create_generated_projects, create_install_script, create_package_json, create_pnpm_workspace, fixtures::peer_heavy_package_name, may_create_lockfile, measurements::PhaseEvent, @@ -692,6 +692,47 @@ fn injected_repeat_install_creates_its_lockfile_before_the_frozen_runs() { fs::remove_dir_all(dir).unwrap(); } +#[test] +fn cargo_repeat_install_seeds_workspace_and_store_before_frozen_runs() { + let scenario = BenchmarkScenario::CargoRepeatInstallHotCacheHotStore; + let temp = tempfile::TempDir::new().expect("create cargo benchmark test dir"); + let dir = temp.path(); + + create_package_json(dir, None, scenario); + create_generated_projects(dir, scenario); + create_pnpm_workspace(dir, None, "http://localhost:4873/", scenario); + create_install_script(dir, scenario, "pnpm", BenchId::PnpmRevision("HEAD")); + super::cargo_workspace::seed_store(dir); + + let manifest: serde_json::Value = + serde_json::from_slice(&fs::read(dir.join("package.json")).unwrap()).unwrap(); + assert_eq!(manifest["name"], "cargo-benchmark-root"); + let cargo_toml = fs::read_to_string(dir.join("Cargo.toml")).unwrap(); + assert!(cargo_toml.contains("\n[workspace]\n"), "cargo_toml = {cargo_toml}"); + assert!(dir.join("src/lib.rs").is_file()); + let cargo_lock = fs::read_to_string(dir.join("Cargo.lock")).unwrap(); + assert!(cargo_lock.contains("benchmark-crate-0000")); + assert!(cargo_lock.contains("dependencies = [\n \"benchmark-crate-0001\",\n]")); + assert!(cargo_lock.contains("dependencies = [\n \"benchmark-crate-0799\",\n]")); + let workspace = fs::read_to_string(dir.join("pnpm-workspace.yaml")).unwrap(); + assert!(workspace.contains("cargo:\n enabled: true"), "workspace = {workspace}"); + assert!( + fs::read_to_string(dir.join("prewarm.bash")) + .unwrap() + .ends_with("exec pnpm install --offline --no-frozen-lockfile\n"), + ); + assert!( + fs::read_to_string(dir.join("install.bash")).unwrap().contains("install --frozen-lockfile"), + ); + let store_dir = pnpm_store_dir::StoreDir::new(dir.join("store-dir")); + let store_index = pnpm_store_dir::StoreIndex::open_readonly_in(&store_dir).unwrap(); + assert_eq!(store_index.keys().unwrap().len(), super::cargo_workspace::CRATE_COUNT); + assert!(!scenario.seeds_lockfile()); + assert!(!scenario.prewarms_node_modules()); + assert!(scenario.cleanup().remove.is_empty()); + assert!(scenario.cleanup().restore.is_empty()); +} + /// Answer one request on a loopback port with `status_line` and `body`. fn serve_registry_directory_once(status_line: &'static str, body: &'static str) -> u16 { let listener = TcpListener::bind("127.0.0.1:0").expect("bind a registry directory stub"); diff --git a/pnpm/tasks/integrated-benchmark/src/workspace_manifest.rs b/pnpm/tasks/integrated-benchmark/src/workspace_manifest.rs index 47cd2bdc73..5041bf67bb 100644 --- a/pnpm/tasks/integrated-benchmark/src/workspace_manifest.rs +++ b/pnpm/tasks/integrated-benchmark/src/workspace_manifest.rs @@ -82,11 +82,19 @@ pub struct MinimalWorkspaceManifest { #[serde(skip_serializing_if = "Option::is_none")] pub trust_lockfile: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub cargo: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub supported_architectures: Option, #[serde(skip_serializing_if = "BTreeMap::is_empty")] pub allow_builds: BTreeMap, } +#[derive(Debug, Default, Serialize, Deserialize)] +#[serde(default)] +pub struct CargoSettings { + pub enabled: bool, +} + #[derive(Debug, Default, Serialize, Deserialize)] #[serde(default)] pub struct SupportedArchitectures {