use std::{
env, fs,
io::{self, Write},
path::{Path, PathBuf},
};
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
use memory_fs::BenchMemoryFS;
use oxc_resolver::{FileSystem as FileSystemTrait, FileSystemOs, PackageJson};
use rayon::prelude::*;
fn data() -> Vec<(PathBuf, &'static str)> {
let cwd = env::current_dir().unwrap();
let f1 = cwd.join("fixtures/enhanced-resolve");
let f2 = f1.join("test/fixtures");
vec![
(cwd.clone(), "@napi-rs/cli"),
(cwd.clone(), "@napi-rs/wasm-runtime"),
(cwd.clone(), "vite-plus"),
(cwd.clone(), "emnapi"),
(cwd, "typescript"),
(f1.clone(), "./"),
(f1.clone(), "./lib/index"),
(f1.clone(), "/absolute/path"),
(f2.clone(), "./main1.js#fragment?query"),
(f2.clone(), "m1/a.js?query#fragment"),
(f2.join("browser-module"), "./lib/replaced"),
(f2.join("browser-module/lib"), "./replaced"),
(f2.join("exports-field"), "exports-field"),
(f2.join("exports-field"), "exports-field/dist/main.js"),
(f2.join("exports-field"), "exports-field/dist/main.js?foo"),
(f2.join("exports-field"), "exports-field/dist/main.js#foo"),
(f2.join("exports-field"), "@exports-field/core"),
(f2.join("imports-exports-wildcard"), "m/features/f.js"),
(f2.join("extension-alias"), "./index.js"),
(f2.join("extension-alias"), "./dir2/index.mjs"),
(f2.join("extensions"), "./foo"),
(f2.join("extensions"), "."),
(f2.join("extensions"), "./dir"),
(f2.join("extensions"), "module/"),
(f2.join("imports-field"), "#imports-field"),
(f2.join("imports-exports-wildcard/node_modules/m/"), "#internal/i.js"),
(f2.join("scoped"), "@scope/pack1"),
(f2.join("scoped"), "@scope/pack2/lib"),
(f2.clone(), "dash"),
(f2.clone(), "dash-name"),
(f2.join("node_modules/dash"), "dash"),
(f2.join("node_modules/dash"), "dash-name"),
(f2.join("node_modules/dash-name"), "dash"),
(f2.join("node_modules/dash-name"), "dash-name"),
(f1.clone(), "aaa"),
(f1.clone(), "ggg"),
(f1.clone(), "rrr"),
(f1.clone(), "@"),
(f1, "@@@"),
]
}
fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> io::Result<()> {
#[cfg(target_family = "unix")]
{
std::os::unix::fs::symlink(original, link)
}
#[cfg(target_family = "windows")]
{
std::os::windows::fs::symlink_file(original, link)
}
}
fn create_symlinks() -> io::Result<PathBuf> {
let root = env::current_dir()?.join("fixtures/enhanced-resolve");
let dirname = root.join("test");
let temp_path = dirname.join("temp_symlinks");
let create_symlink_fixtures = || -> io::Result<()> {
fs::create_dir(&temp_path)?;
let mut index = fs::File::create(temp_path.join("index.js"))?;
index.write_all(b"console.log('Hello, World!')")?;
for i in 0..10000 {
symlink(temp_path.join("index.js"), temp_path.join(format!("file{i}.js")))?;
}
Ok(())
};
if !temp_path.exists()
&& let Err(err) = create_symlink_fixtures()
{
let _ = fs::remove_dir_all(&temp_path);
return Err(err);
}
Ok(temp_path)
}
fn resolve_options() -> oxc_resolver::ResolveOptions {
use oxc_resolver::{AliasValue, ResolveOptions};
let alias_value = AliasValue::from("./");
ResolveOptions {
extensions: vec![".ts".into(), ".js".into()],
condition_names: vec!["webpack".into(), "require".into()],
alias_fields: vec![vec!["browser".into()]],
extension_alias: vec![
(".js".into(), vec![".ts".into(), ".js".into()]),
(".mjs".into(), vec![".mts".into()]),
],
alias: vec![
("/absolute/path".into(), vec![alias_value.clone()]),
("aaa".into(), vec![alias_value.clone()]),
("bbb".into(), vec![alias_value.clone()]),
("ccc".into(), vec![alias_value.clone()]),
("ddd".into(), vec![alias_value.clone()]),
("eee".into(), vec![alias_value.clone()]),
("fff".into(), vec![alias_value.clone()]),
("ggg".into(), vec![alias_value.clone()]),
("hhh".into(), vec![alias_value.clone()]),
("iii".into(), vec![alias_value.clone()]),
("jjj".into(), vec![alias_value.clone()]),
("kkk".into(), vec![alias_value.clone()]),
("lll".into(), vec![alias_value.clone()]),
("mmm".into(), vec![alias_value.clone()]),
("nnn".into(), vec![alias_value.clone()]),
("ooo".into(), vec![alias_value.clone()]),
("ppp".into(), vec![alias_value.clone()]),
("qqq".into(), vec![alias_value.clone()]),
("rrr".into(), vec![alias_value.clone()]),
("sss".into(), vec![alias_value.clone()]),
("@".into(), vec![alias_value.clone()]),
("@@".into(), vec![alias_value.clone()]),
("@@@".into(), vec![alias_value]),
],
..ResolveOptions::default()
}
}
fn oxc_resolver_memory() -> oxc_resolver::ResolverGeneric<BenchMemoryFS> {
use oxc_resolver::ResolverGeneric;
let fs = BenchMemoryFS::new();
ResolverGeneric::new_with_file_system(fs, resolve_options())
}
fn oxc_resolver_real() -> oxc_resolver::Resolver {
use oxc_resolver::Resolver;
Resolver::new(resolve_options())
}
fn pin_rayon_threads() {
let _ = rayon::ThreadPoolBuilder::new().num_threads(4).build_global();
}
fn bench_resolver_memory(c: &mut Criterion) {
pin_rayon_threads();
let data = data();
let cwd = env::current_dir().unwrap();
let symlink_test_dir = cwd.join("fixtures/enhanced-resolve/test/temp_symlinks");
for (path, request) in &data {
assert!(
oxc_resolver_memory().resolve(path, request).is_ok(),
"{} {request}",
path.display()
);
}
let symlink_specifiers = (0u32..10000).map(|i| format!("./file{i}")).collect::<Vec<_>>();
{
let oxc_resolver = oxc_resolver_memory();
for specifier in &symlink_specifiers {
assert!(oxc_resolver.resolve(&symlink_test_dir, specifier).is_ok(), "{specifier}");
}
}
let mut group = c.benchmark_group("resolver_memory");
group.bench_with_input(BenchmarkId::from_parameter("single-thread"), &data, |b, data| {
let oxc_resolver = oxc_resolver_memory();
b.iter(|| {
for (path, request) in data {
_ = oxc_resolver.resolve(path, request);
}
});
});
group.bench_with_input(BenchmarkId::from_parameter("drop"), &data, |b, data| {
b.iter(|| {
let oxc_resolver = oxc_resolver_memory(); for (path, request) in data {
_ = oxc_resolver.resolve(path, request);
}
});
});
group.bench_with_input(BenchmarkId::from_parameter("multi-thread"), &data, |b, data| {
let oxc_resolver = oxc_resolver_memory();
b.iter(|| {
data.par_iter().for_each(|(path, request)| {
_ = oxc_resolver.resolve(path, request);
});
});
});
group.bench_with_input(
BenchmarkId::from_parameter("resolve from symlinks"),
&symlink_specifiers,
|b, specifiers| {
let oxc_resolver = oxc_resolver_memory();
b.iter(|| {
for specifier in specifiers {
_ = oxc_resolver.resolve(&symlink_test_dir, specifier);
}
});
},
);
group.bench_with_input(BenchmarkId::from_parameter("find tsconfig"), &data, |b, data| {
let oxc_resolver = oxc_resolver_memory();
let paths = data
.iter()
.map(|(path, request)| oxc_resolver.resolve(path, request).unwrap().into_path_buf())
.collect::<Vec<_>>();
b.iter(|| {
for path in &paths {
let _ = oxc_resolver.find_tsconfig(path);
}
});
});
}
fn bench_resolver_real(c: &mut Criterion) {
pin_rayon_threads();
let data = data();
let symlink_test_dir = create_symlinks().expect("Create symlink fixtures failed");
for (path, request) in &data {
assert!(oxc_resolver_real().resolve(path, request).is_ok(), "{} {request}", path.display());
}
let symlink_specifiers = (0u32..10000).map(|i| format!("./file{i}")).collect::<Vec<_>>();
{
let oxc_resolver = oxc_resolver_real();
for specifier in &symlink_specifiers {
assert!(oxc_resolver.resolve(&symlink_test_dir, specifier).is_ok(), "{specifier}");
}
}
let mut group = c.benchmark_group("resolver_real");
group.bench_with_input(BenchmarkId::from_parameter("single-thread"), &data, |b, data| {
let oxc_resolver = oxc_resolver_real();
b.iter(|| {
for (path, request) in data {
_ = oxc_resolver.resolve(path, request);
}
});
});
group.bench_with_input(BenchmarkId::from_parameter("multi-thread"), &data, |b, data| {
let oxc_resolver = oxc_resolver_real();
b.iter(|| {
data.par_iter().for_each(|(path, request)| {
_ = oxc_resolver.resolve(path, request);
});
});
});
group.bench_with_input(
BenchmarkId::from_parameter("resolve from symlinks"),
&symlink_specifiers,
|b, specifiers| {
let oxc_resolver = oxc_resolver_real();
b.iter(|| {
for specifier in specifiers {
_ = oxc_resolver.resolve(&symlink_test_dir, specifier);
}
});
},
);
}
fn bench_package_json_deserialization(c: &mut Criterion) {
let mut group = c.benchmark_group("package_json_deserialization");
let small_json = r#"{
"name": "test-package",
"version": "1.0.0"
}"#;
let medium_json = r##"{
"name": "test-package",
"version": "1.0.0",
"main": "./lib/index.js",
"type": "module",
"exports": {
".": "./lib/index.js",
"./feature": "./lib/feature.js"
},
"imports": {
"#internal": "./src/internal.js"
},
"browser": {
"./lib/node.js": "./lib/browser.js"
},
"sideEffects": false
}"##;
let large_json = r##"{
"name": "test-package",
"version": "1.0.0",
"main": "./lib/index.js",
"type": "module",
"exports": {
".": {
"import": "./lib/index.mjs",
"require": "./lib/index.cjs",
"browser": "./lib/browser.js"
},
"./feature": {
"import": "./lib/feature.mjs",
"require": "./lib/feature.cjs"
},
"./utils": "./lib/utils.js",
"./internal/*": "./lib/internal/*.js"
},
"imports": {
"#internal": "./src/internal.js",
"#utils/*": "./src/utils/*.js"
},
"browser": {
"./lib/node.js": "./lib/browser.js",
"module-a": "./browser/module-a.js",
"module-b": "module-c",
"./lib/replaced.js": "./lib/browser"
},
"sideEffects": ["*.css", "*.scss"],
"dependencies": {
"lodash": "^4.17.21",
"react": "^18.0.0",
"express": "^4.18.0"
},
"devDependencies": {
"typescript": "^5.0.0",
"eslint": "^8.0.0",
"jest": "^29.0.0"
},
"scripts": {
"test": "jest",
"build": "tsc",
"lint": "eslint src"
}
}"##;
let complex_json_path = env::current_dir()
.unwrap()
.join("fixtures/enhanced-resolve/test/fixtures/browser-module/package.json");
let complex_json =
fs::read_to_string(&complex_json_path).expect("Failed to read complex package.json");
let test_path = PathBuf::from("/test/package.json");
let test_realpath = test_path.clone();
#[cfg(feature = "yarn_pnp")]
let fs = FileSystemOs::new(false);
#[cfg(not(feature = "yarn_pnp"))]
let fs = FileSystemOs::new();
let data = [
("small", small_json.to_string()),
("medium", medium_json.to_string()),
("large", large_json.to_string()),
("complex_real", complex_json),
];
for (name, json) in data {
group.bench_function(name, |b| {
b.iter_with_setup_wrapper(|runner| {
let json = json.clone().into_bytes();
runner.run(|| {
PackageJson::parse(&fs, test_path.clone(), test_realpath.clone(), json)
.expect("Failed to parse JSON");
});
});
});
}
group.finish();
}
fn bench_tsconfig_paths_aliases(c: &mut Criterion) {
use oxc_resolver::{
ResolveOptions, ResolverGeneric, TsconfigDiscovery, TsconfigOptions, TsconfigReferences,
};
let mut group = c.benchmark_group("tsconfig_paths_aliases_memory");
let alias_count = 200usize;
let (fs, importer, tsconfig_path, requests) =
BenchMemoryFS::with_large_tsconfig_paths_fixture(alias_count);
let resolver = ResolverGeneric::new_with_file_system(
fs,
ResolveOptions {
tsconfig: Some(TsconfigDiscovery::Manual(TsconfigOptions {
config_file: tsconfig_path,
references: TsconfigReferences::Disabled,
})),
extensions: vec![".ts".into(), ".js".into()],
..ResolveOptions::default()
},
);
for request in &requests {
assert!(
resolver.resolve_file(&importer, request).is_ok(),
"alias_count={alias_count} request={request}"
);
}
group.bench_with_input(
BenchmarkId::from_parameter("query each alias"),
&requests,
|b, requests| {
b.iter(|| {
for request in requests {
_ = resolver.resolve_file(&importer, request);
}
});
},
);
}
criterion_group!(
resolver,
bench_resolver_memory,
bench_resolver_real,
bench_package_json_deserialization,
bench_tsconfig_paths_aliases
);
criterion_main!(resolver);
mod memory_fs {
use std::{
fs, io,
path::{Path, PathBuf},
sync::LazyLock,
};
use oxc_resolver::{FileMetadata, FileSystem, ResolveError};
use rustc_hash::{FxHashMap, FxHashSet};
use walkdir::WalkDir;
#[derive(Clone)]
pub struct BenchMemoryFS {
files: FxHashMap<PathBuf, Vec<u8>>,
directories: FxHashSet<PathBuf>,
symlinks: FxHashMap<PathBuf, PathBuf>,
}
static BENCH_FS: LazyLock<BenchMemoryFS> = LazyLock::new(|| {
let mut fs = BenchMemoryFS {
files: FxHashMap::default(),
directories: FxHashSet::default(),
symlinks: FxHashMap::default(),
};
fs.load_fixtures();
fs
});
impl BenchMemoryFS {
pub fn new() -> Self {
BENCH_FS.clone()
}
pub fn with_large_tsconfig_paths_fixture(
alias_count: usize,
) -> (Self, PathBuf, PathBuf, Vec<String>) {
let fs = Self::new();
let cwd = std::env::current_dir().unwrap();
let root = cwd.join(format!("fixtures/tsconfig/bench-tsconfig-paths/{alias_count}"));
let importer = root.join("app/main.ts");
let tsconfig_path = root.join("tsconfig.json");
assert!(
fs.files.contains_key(&importer),
"missing benchmark fixture: {}",
importer.display()
);
assert!(
fs.files.contains_key(&tsconfig_path),
"missing benchmark fixture: {}",
tsconfig_path.display()
);
let requests = (0..alias_count).map(|i| format!("@pkg{i:04}/file")).collect();
(fs, importer, tsconfig_path, requests)
}
fn add_parent_directories(&mut self, path: &Path) {
for ancestor in path.ancestors().skip(1) {
self.directories.insert(ancestor.to_path_buf());
}
}
fn load_fixtures(&mut self) {
let cwd = std::env::current_dir().unwrap();
self.add_parent_directories(&cwd);
self.load_directory_tree(&cwd.join("fixtures/enhanced-resolve"));
self.load_directory_tree(&cwd.join("fixtures/tsconfig/bench-tsconfig-paths"));
self.load_node_modules_packages(&cwd);
self.create_symlink_fixtures(&cwd);
}
fn load_directory_tree(&mut self, root: &Path) {
if !root.exists() {
return;
}
for entry in WalkDir::new(root).follow_links(false).into_iter().filter_map(Result::ok) {
let path = entry.path();
let Ok(metadata) = fs::symlink_metadata(path) else { continue };
let abs_path = path.to_path_buf();
if metadata.is_symlink() {
if let Ok(target) = fs::read_link(path) {
self.symlinks.insert(abs_path.clone(), target);
self.add_parent_directories(&abs_path);
}
} else if metadata.is_dir() {
self.directories.insert(abs_path.clone());
self.add_parent_directories(&abs_path);
} else if metadata.is_file()
&& let Ok(content) = fs::read(path)
{
self.files.insert(abs_path.clone(), content);
self.add_parent_directories(&abs_path);
}
}
}
fn load_node_modules_packages(&mut self, cwd: &Path) {
let node_modules = cwd.join("node_modules");
if !node_modules.exists() {
return;
}
let packages =
["@napi-rs/cli", "@napi-rs/wasm-runtime", "vite-plus", "emnapi", "typescript"];
for package_name in packages {
let package_path = node_modules.join(package_name);
if !package_path.exists() {
continue;
}
if package_name.starts_with('@')
&& let Some(parent) = package_path.parent()
&& parent != node_modules
{
self.directories.insert(parent.to_path_buf());
self.add_parent_directories(parent);
}
if let Ok(metadata) = fs::symlink_metadata(&package_path) {
if metadata.is_symlink() {
if let Ok(target) = fs::read_link(&package_path) {
self.symlinks.insert(package_path.clone(), target.clone());
self.add_parent_directories(&package_path);
let resolved_target = if target.is_relative() {
package_path.parent().unwrap().join(&target)
} else {
target
};
if resolved_target.exists() {
self.load_package_files(&resolved_target);
}
self.load_package_files(&package_path);
}
} else {
self.load_package_files(&package_path);
}
}
}
}
fn load_package_files(&mut self, package_root: &Path) {
for entry in WalkDir::new(package_root)
.follow_links(true) .max_depth(5) .into_iter()
.filter_map(Result::ok)
{
let path = entry.path();
let Ok(metadata) = fs::metadata(path) else { continue };
let abs_path = path.to_path_buf();
if metadata.is_dir() {
self.directories.insert(abs_path.clone());
self.add_parent_directories(&abs_path);
} else if metadata.is_file() {
if let Some(ext) = path.extension() {
let ext_str = ext.to_str();
if matches!(
ext_str,
Some("json" | "js" | "mjs" | "cjs" | "ts" | "mts" | "cts" | "d.ts")
) && let Ok(content) = fs::read(path)
{
self.files.insert(abs_path.clone(), content);
self.add_parent_directories(&abs_path);
}
} else if path.file_name() == Some(std::ffi::OsStr::new("package.json")) {
if let Ok(content) = fs::read(path) {
self.files.insert(abs_path.clone(), content);
self.add_parent_directories(&abs_path);
}
}
}
}
}
fn create_symlink_fixtures(&mut self, cwd: &Path) {
let temp_path = cwd.join("fixtures/enhanced-resolve/test/temp_symlinks");
self.directories.insert(temp_path.clone());
self.add_parent_directories(&temp_path);
let index_path = temp_path.join("index.js");
self.files.insert(index_path, b"console.log('Hello, World!')".to_vec());
for i in 0..10000 {
let symlink_path = temp_path.join(format!("file{i}.js"));
self.symlinks.insert(symlink_path, PathBuf::from("index.js"));
}
}
}
impl Default for BenchMemoryFS {
fn default() -> Self {
Self::new()
}
}
impl FileSystem for BenchMemoryFS {
#[cfg(not(feature = "yarn_pnp"))]
fn new() -> Self {
Self::default()
}
#[cfg(feature = "yarn_pnp")]
fn new(_yarn_pnp: bool) -> Self {
Self::default()
}
fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
if let Some(bytes) = self.files.get(path) {
return Ok(bytes.clone());
}
let mut current = path.to_path_buf();
let mut visited = FxHashSet::default();
while let Some(target) = self.symlinks.get(¤t) {
if !visited.insert(current.clone()) {
return Err(io::Error::other("Circular symlink"));
}
current = if target.is_relative() {
current.parent().unwrap().join(target)
} else {
target.clone()
};
if let Some(bytes) = self.files.get(¤t) {
return Ok(bytes.clone());
}
}
Err(io::Error::new(
io::ErrorKind::NotFound,
format!("File not found: {}", path.display()),
))
}
fn read_to_string(&self, path: &Path) -> io::Result<String> {
let bytes = self.read(path)?;
String::from_utf8(bytes).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
if self.files.contains_key(path) {
return Ok(FileMetadata::new(true, false, false));
}
if self.directories.contains(path) {
return Ok(FileMetadata::new(false, true, false));
}
let mut current = path.to_path_buf();
let mut visited = FxHashSet::default();
while let Some(target) = self.symlinks.get(¤t) {
if !visited.insert(current.clone()) {
return Err(io::Error::other("Circular symlink"));
}
current = if target.is_relative() {
current.parent().unwrap().join(target)
} else {
target.clone()
};
if self.files.contains_key(¤t) {
return Ok(FileMetadata::new(true, false, false));
} else if self.directories.contains(¤t) {
return Ok(FileMetadata::new(false, true, false));
}
}
Err(io::Error::new(
io::ErrorKind::NotFound,
format!("Path not found: {}", path.display()),
))
}
fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata> {
if self.symlinks.contains_key(path) {
return Ok(FileMetadata::new(false, false, true));
}
self.metadata(path)
}
fn read_link(&self, path: &Path) -> Result<PathBuf, ResolveError> {
self.symlinks.get(path).cloned().ok_or_else(|| {
ResolveError::from(io::Error::new(
io::ErrorKind::NotFound,
format!("Not a symlink: {}", path.display()),
))
})
}
fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
let mut current = path.to_path_buf();
let mut visited = FxHashSet::default();
while let Some(target) = self.symlinks.get(¤t) {
if !visited.insert(current.clone()) {
return Err(io::Error::other("Circular symlink"));
}
current = if target.is_relative() {
current.parent().unwrap().join(target)
} else {
target.clone()
};
}
if self.files.contains_key(¤t) || self.directories.contains(¤t) {
Ok(current)
} else {
Err(io::Error::new(
io::ErrorKind::NotFound,
format!("Path not found: {}", path.display()),
))
}
}
}
}