Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,18 @@ jobs:
with:
cache-key: benchmark
save-cache: ${{ github.ref_name == 'main' }}
tools: cargo-codspeed
tools: cargo-codspeed,just

- uses: ./.github/actions/pnpm

- run: cargo codspeed build --features codspeed
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.9

- name: Install bench-pm fixtures
run: just install-bench-fixtures

- run: cargo codspeed build --features codspeed,yarn_pnp
env:
RUSTFLAGS: "-C debuginfo=1 -C strip=none"

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ target/
/fixtures/pnpm/node_modules
/fixtures/yarn/node_modules
/fixtures/tsconfck/node_modules
/fixtures/bench-pm/installs/
fuzz/Cargo.lock
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ description.workspace = true
name = "resolver"
harness = false

[[bench]]
name = "package_managers"
harness = false

[lints.clippy]
all = { level = "warn", priority = -1 }
cargo = { level = "warn", priority = -1 }
Expand Down
313 changes: 313 additions & 0 deletions benches/package_managers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
use criterion::{Criterion, criterion_group, criterion_main};
use oxc_resolver::Resolver;
use rayon::prelude::*;

use workload::Combo;

fn run_combo(c: &mut Criterion, combo: Combo) {
let Some(root) = combo.fixture_root_if_installed() else {
eprintln!(
"skip pm/{}: fixture not installed (run `just install-bench-fixtures`)",
combo.slug()
);
return;
};
let reqs = workload::requests(&root);
let opts = combo.resolve_options(&root);

// Correctness gate: every request must resolve and land inside the expected package.
let resolver = Resolver::new(opts.clone());
for req in &reqs {
let resolution = resolver.resolve(&req.importer, req.specifier).unwrap_or_else(|err| {
panic!(
"pm/{}: resolve({}, {:?}) failed: {err}",
combo.slug(),
req.importer.display(),
req.specifier
)
});
let path = resolution.path();
let normalized = path.to_string_lossy().replace('\\', "/");
assert!(
workload::matches(&normalized, req),
"pm/{}: resolve({}, {:?}) -> {} (expected package {:?} ending with {:?})",
combo.slug(),
req.importer.display(),
req.specifier,
path.display(),
req.pkg_dir,
req.internal_path,
);
}

// One bench per combo, modelling a bundler-style build: one Resolver, the
// workload's 16 requests fanned out across worker threads. Resolver creation
// is inside the timed body so the .pnp.cjs parse, store discovery, and
// initial cache warmup all count.
c.bench_function(&format!("pm/{}", combo.slug()), |b| {
b.iter(|| {
let resolver = Resolver::new(opts.clone());
reqs.par_iter().for_each(|req| {
_ = resolver.resolve(&req.importer, req.specifier);
});
});
});
}

fn bench_package_managers(c: &mut Criterion) {
for &combo in Combo::ALL {
#[cfg(not(feature = "yarn_pnp"))]
if matches!(combo, Combo::YarnPnp) {
eprintln!("skip pm/yarn-pnp: rebuild with `--features yarn_pnp` to include it");
continue;
}
run_combo(c, combo);
}
}

criterion_group!(package_managers, bench_package_managers);
criterion_main!(package_managers);

mod workload {
//! Shared workload definitions for the package-manager benchmarks.
//!
//! Every combo installs the same template monorepo (with per-PM config
//! overlays) into `fixtures/bench-pm/installs/<slug>/`. The same list of
//! [`Request`]s is replayed against each combo so resolution costs are
//! directly comparable.

use std::{env, path::PathBuf};

use oxc_resolver::ResolveOptions;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Combo {
NpmFlat,
PnpmIsolated,
PnpmHoisted,
YarnFlat,
YarnIsolated,
YarnPnp,
BunFlat,
BunIsolated,
}

impl Combo {
pub const ALL: &'static [Self] = &[
Self::NpmFlat,
Self::PnpmIsolated,
Self::PnpmHoisted,
Self::YarnFlat,
Self::YarnIsolated,
Self::YarnPnp,
Self::BunFlat,
Self::BunIsolated,
];

pub fn slug(self) -> &'static str {
match self {
Self::NpmFlat => "npm-flat",
Self::PnpmIsolated => "pnpm-isolated",
Self::PnpmHoisted => "pnpm-hoisted",
Self::YarnFlat => "yarn-flat",
Self::YarnIsolated => "yarn-isolated",
Self::YarnPnp => "yarn-pnp",
Self::BunFlat => "bun-flat",
Self::BunIsolated => "bun-isolated",
}
}

pub fn fixture_root(self) -> PathBuf {
env::current_dir().unwrap().join("fixtures/bench-pm/installs").join(self.slug())
}

pub fn fixture_root_if_installed(self) -> Option<PathBuf> {
let root = self.fixture_root();
let installed = match self {
Self::YarnPnp => root.join(".pnp.cjs").is_file(),
_ => root.join("node_modules").is_dir(),
};
installed.then_some(root)
}

pub fn resolve_options(self, fixture_root: &std::path::Path) -> ResolveOptions {
let base = ResolveOptions {
cwd: Some(fixture_root.to_path_buf()),
condition_names: vec!["node".into(), "require".into()],
extensions: vec![".js".into(), ".cjs".into(), ".mjs".into(), ".json".into()],
..ResolveOptions::default()
};
match self {
#[cfg(feature = "yarn_pnp")]
Self::YarnPnp => ResolveOptions { yarn_pnp: true, ..base },
_ => base,
}
}
}

pub struct Request {
pub importer: PathBuf,
pub specifier: &'static str,
/// The package's directory name as it appears in `node_modules/` (e.g., `"react"`,
/// `"@babel/runtime"`, `"packages/utils"` for workspace deps). Used to verify the
/// resolver picked the right package — the resolved path must contain a segment
/// matching this name in one of several PM-specific forms.
pub pkg_dir: &'static str,
/// Path inside the package that the resolver should land on (e.g., `"/index.js"`,
/// `"/helpers/extends.js"`). The resolved absolute path must end with this string.
pub internal_path: &'static str,
}

/// Returns whether `resolved_path` correctly resolves `req`. The path is checked for two things:
/// (a) it contains a segment matching `req.pkg_dir` in one of the layout-specific forms, and
/// (b) it ends with `req.internal_path`.
///
/// Layout-specific segment forms (the slash before each variant matters — it anchors the match):
/// - `/<pkg>/` — flat layouts (npm-flat, pnpm-hoisted, yarn-flat, bun-flat) plus the inner
/// `node_modules/<pkg>/` directory of every isolated layout (pnpm-isolated, yarn-pnp cache,
/// bun-isolated)
/// - `/<pkg>@` — pnpm/bun virtual store dir names like `react@18.3.1`
/// - `/<pkg>-<kind>-` where kind ∈ {npm, patch, virtual, workspace} — yarn berry descriptor forms,
/// covering `.store/<pkg>-npm-<ver>-<hash>/`, `.store/<pkg>-patch-<hash>/`, and `.yarn/cache/<pkg>-npm-...zip/`
/// - the same `+`/`-` variants with `/` replaced for scoped packages (pnpm uses `@scope+name@`,
/// yarn-isolated uses `@scope-name-npm-`)
pub fn matches(resolved_path: &str, req: &Request) -> bool {
const KINDS: &[&str] = &["npm-", "patch-", "virtual-", "workspace-"];

if !resolved_path.ends_with(req.internal_path) {
return false;
}
let scoped_dashed = req.pkg_dir.replace('/', "-");
let scoped_plus = req.pkg_dir.replace('/', "+");
let mut candidates: Vec<String> = vec![
format!("/{}/", req.pkg_dir),
format!("/{}@", req.pkg_dir),
format!("/{scoped_plus}@"),
];
for kind in KINDS {
candidates.push(format!("/{}-{kind}", req.pkg_dir));
candidates.push(format!("/{scoped_dashed}-{kind}"));
}
candidates.iter().any(|c| resolved_path.contains(c.as_str()))
}

pub fn requests(root: &std::path::Path) -> Vec<Request> {
// The resolver takes the containing directory of the importer, not the file path itself.
let deep = root.join("apps/web/nested/deep/src");
let shallow = root.join("apps/web/src");
vec![
// Bare unscoped, exports field with conditions
Request {
importer: deep.clone(),
specifier: "react",
pkg_dir: "react",
internal_path: "/index.js",
},
// Subpath through exports
Request {
importer: deep.clone(),
specifier: "react/jsx-runtime",
pkg_dir: "react",
internal_path: "/jsx-runtime.js",
},
// Scoped, deep subpath with conditional exports
Request {
importer: deep.clone(),
specifier: "@babel/runtime/helpers/extends",
pkg_dir: "@babel/runtime",
internal_path: "/helpers/extends.js",
},
// Scoped subpath that resolves to a directory's index.js
Request {
importer: deep.clone(),
specifier: "@babel/runtime/regenerator",
pkg_dir: "@babel/runtime",
internal_path: "/regenerator/index.js",
},
// Conditional exports default branch (require condition)
Request {
importer: deep.clone(),
specifier: "axios",
pkg_dir: "axios",
internal_path: "/dist/node/axios.cjs",
},
// Exports field exact-path mapping
Request {
importer: deep.clone(),
specifier: "axios/unsafe/utils.js",
pkg_dir: "axios",
internal_path: "/lib/utils.js",
},
// No exports, main field, ESM-only large package
Request {
importer: deep.clone(),
specifier: "lodash-es",
pkg_dir: "lodash-es",
internal_path: "/lodash.js",
},
// Direct file inside a large dist directory
Request {
importer: deep.clone(),
specifier: "lodash-es/debounce.js",
pkg_dir: "lodash-es",
internal_path: "/debounce.js",
},
// Exports field as plain string
Request {
importer: deep.clone(),
specifier: "chalk",
pkg_dir: "chalk",
internal_path: "/source/index.js",
},
// Large single package, main field only
Request {
importer: deep.clone(),
specifier: "typescript",
pkg_dir: "typescript",
internal_path: "/lib/typescript.js",
},
// Local workspace package (symlinked across PMs; PnP resolves directly to the workspace dir)
Request {
importer: deep.clone(),
specifier: "@bench/utils",
pkg_dir: "packages/utils",
internal_path: "/src/index.js",
},
// Workspace package subpath through exports
Request {
importer: deep.clone(),
specifier: "@bench/utils/hash",
pkg_dir: "packages/utils",
internal_path: "/src/hash.js",
},
// Another workspace package
Request {
importer: deep,
specifier: "@bench/ui",
pkg_dir: "packages/ui",
internal_path: "/src/index.js",
},
// Shallow importer: react, exercises a shorter ancestor walk
Request {
importer: shallow.clone(),
specifier: "react",
pkg_dir: "react",
internal_path: "/index.js",
},
// Shallow importer: workspace dep
Request {
importer: shallow.clone(),
specifier: "@bench/utils",
pkg_dir: "packages/utils",
internal_path: "/src/index.js",
},
// Relative path traversal across the monorepo (from apps/web/src/)
Request {
importer: shallow,
specifier: "../../../packages/utils",
pkg_dir: "packages/utils",
internal_path: "/src/index.js",
},
]
}
}
Loading
Loading