diff --git a/Cargo.lock b/Cargo.lock index cdf93babc..9a858ff11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -196,7 +196,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -296,6 +296,20 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "378f0974ae2468eaf63aa036dbe9c926b0dc7ea64c156f2ea618bc2f75b934f0" +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "dirs" version = "6.0.0" @@ -314,7 +328,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -351,7 +365,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -544,6 +558,12 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.16.1" @@ -653,6 +673,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "memchr" version = "2.8.0" @@ -804,6 +833,7 @@ dependencies = [ "cfg-if", "compact_str", "criterion2", + "dashmap", "dirs", "document-features", "fancy-regex", @@ -812,7 +842,6 @@ dependencies = [ "json-strip-comments", "nodejs-built-in-modules", "once_cell", - "papaya", "percent-encoding", "pico-args", "pnp", @@ -846,13 +875,16 @@ dependencies = [ ] [[package]] -name = "papaya" -version = "0.2.4" +name = "parking_lot_core" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "997ee03cd38c01469a7046643714f0ad28880bcb9e6679ff0666e24817ca19b7" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ - "equivalent", - "seize", + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", ] [[package]] @@ -1026,7 +1058,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1051,14 +1083,10 @@ dependencies = [ ] [[package]] -name = "seize" -version = "0.5.1" +name = "scopeguard" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "self_cell" @@ -1320,7 +1348,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2fef100fc..8682368e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,7 +88,7 @@ indexmap = { version = "2", features = ["serde"] } json-strip-comments = "3.1" nodejs-built-in-modules = "1.0.0" once_cell = "1" # Use `std::sync::OnceLock::get_or_try_init` when it is stable. -papaya = "0.2" +dashmap = { version = "6.2.1", features = ["raw-api"] } rustc-hash = { version = "2" } serde = { version = "1", features = ["derive"] } # derive for Deserialize from package.json serde_json = { version = "1", features = [ diff --git a/src/cache/borrowed_path.rs b/src/cache/borrowed_path.rs deleted file mode 100644 index 0163ebf25..000000000 --- a/src/cache/borrowed_path.rs +++ /dev/null @@ -1,30 +0,0 @@ -use super::cached_path::CachedPath; -use papaya::Equivalent; -use std::{ - hash::{Hash, Hasher}, - path::Path, -}; - -#[derive(Debug)] -pub struct BorrowedCachedPath<'a> { - pub hash: u64, - pub path: &'a Path, -} - -impl Equivalent for BorrowedCachedPath<'_> { - fn equivalent(&self, other: &CachedPath) -> bool { - self.path.as_os_str() == other.path().as_os_str() - } -} - -impl Hash for BorrowedCachedPath<'_> { - fn hash(&self, state: &mut H) { - self.hash.hash(state); - } -} - -impl PartialEq for BorrowedCachedPath<'_> { - fn eq(&self, other: &Self) -> bool { - self.path.as_os_str() == other.path.as_os_str() - } -} diff --git a/src/cache/cache_impl.rs b/src/cache/cache_impl.rs index 3ff825bc8..694b2103a 100644 --- a/src/cache/cache_impl.rs +++ b/src/cache/cache_impl.rs @@ -8,12 +8,11 @@ use std::{ }; use cfg_if::cfg_if; +use dashmap::{DashMap, mapref::entry::Entry}; #[cfg(feature = "yarn_pnp")] use once_cell::sync::OnceCell; -use papaya::{HashMap, HashSet}; use rustc_hash::FxHasher; -use super::borrowed_path::BorrowedCachedPath; use super::cached_path::{CachedPath, CachedPathImpl}; use super::hasher::IdentityHasher; use crate::{ @@ -25,20 +24,20 @@ use crate::{ #[derive(Default)] pub struct Cache { pub(crate) fs: Fs, - pub(crate) paths: HashSet>, + pub(crate) paths: DashMap>, /// Cache for raw/unbuilt tsconfigs (used when extending). - pub(crate) tsconfigs_raw: HashMap, BuildHasherDefault>, + pub(crate) tsconfigs_raw: DashMap, BuildHasherDefault>, /// Cache for built/resolved tsconfigs (used for resolution). - pub(crate) tsconfigs_built: HashMap, BuildHasherDefault>, + pub(crate) tsconfigs_built: DashMap, BuildHasherDefault>, #[cfg(feature = "yarn_pnp")] pub(crate) yarn_pnp_manifest: OnceCell, } impl Cache { pub fn clear(&self) { - self.paths.pin().clear(); - self.tsconfigs_raw.pin().clear(); - self.tsconfigs_built.pin().clear(); + self.paths.clear(); + self.tsconfigs_raw.clear(); + self.tsconfigs_built.clear(); } #[allow(clippy::cast_possible_truncation)] @@ -50,9 +49,17 @@ impl Cache { path.as_os_str().hash(&mut hasher); hasher.finish() }; - let paths = self.paths.pin(); - if let Some(entry) = paths.get(&BorrowedCachedPath { hash, path }) { - return entry.clone(); + // Look up by the memoized `hash`. `IdentityHasher` only accepts a single `write_u64`, so the + // set can't be probed by a borrowed `&Path` through dashmap's `Borrow`-based `get`; instead + // read the shard directly (raw-api) with the precomputed hash and an `OsStr` equality. This + // mirrors the `Equivalent` lookup the original `papaya` set used, and keeps it zero-alloc. + { + let shard = self.paths.shards()[self.paths.determine_shard(hash as usize)].read(); + if let Some((cached, _)) = + shard.get(hash, |(k, _)| k.path().as_os_str() == path.as_os_str()) + { + return cached.clone(); + } } let parent = path.parent().map(|p| self.value(p)); let is_node_modules = path.file_name().as_ref().is_some_and(|&name| name == "node_modules"); @@ -66,8 +73,17 @@ impl Cache { inside_node_modules, parent_weak, ))); - paths.insert(cached_path.clone()); - cached_path + // The shard guard above is dropped before the parent recursion, so a concurrent call may + // have inserted this same path in the meantime. Dedup via the entry API so every path keeps + // a single shared `Arc`, which the `canonicalized` / `node_modules` weak-pointer caches rely + // on for identity. + match self.paths.entry(cached_path.clone()) { + Entry::Occupied(occupied) => occupied.key().clone(), + Entry::Vacant(vacant) => { + vacant.insert(()); + cached_path + } + } } pub(crate) fn canonicalize(&self, path: &CachedPath) -> Result { @@ -229,20 +245,14 @@ impl Cache { callback: F, // callback for modifying tsconfig with `extends` ) -> Result, ResolveError> { // For root=true (caller tsconfig), check built cache first - if root { - let tsconfigs_built = self.tsconfigs_built.pin(); - if let Some(tsconfig) = tsconfigs_built.get(path) { - return Ok(Arc::clone(tsconfig)); - } + if root && let Some(tsconfig) = self.tsconfigs_built.get(path) { + return Ok(Arc::clone(tsconfig.value())); } // Check raw cache (callback applied, not built) - only for root=false // For root=true, we need to run the callback to ensure extends are processed - if !root { - let tsconfigs_raw = self.tsconfigs_raw.pin(); - if let Some(tsconfig) = tsconfigs_raw.get(path) { - return Ok(Arc::clone(tsconfig)); - } + if !root && let Some(tsconfig) = self.tsconfigs_raw.get(path) { + return Ok(Arc::clone(tsconfig.value())); } // Not in any cache, parse from file @@ -277,13 +287,13 @@ impl Cache { // Cache raw version (callback applied, not built) tsconfig.set_should_build(false); let raw_tsconfig = Arc::new(tsconfig.clone()); - self.tsconfigs_raw.pin().insert(path.to_path_buf(), Arc::clone(&raw_tsconfig)); + self.tsconfigs_raw.insert(path.to_path_buf(), Arc::clone(&raw_tsconfig)); if root { // Build and cache built version tsconfig.set_should_build(true); let tsconfig = Arc::new(tsconfig.build()); - self.tsconfigs_built.pin().insert(path.to_path_buf(), Arc::clone(&tsconfig)); + self.tsconfigs_built.insert(path.to_path_buf(), Arc::clone(&tsconfig)); Ok(tsconfig) } else { // Return unbuilt version @@ -322,18 +332,9 @@ impl Cache { pub fn new(fs: Fs) -> Self { Self { fs, - paths: HashSet::builder() - .hasher(BuildHasherDefault::default()) - .resize_mode(papaya::ResizeMode::Blocking) - .build(), - tsconfigs_raw: HashMap::builder() - .hasher(BuildHasherDefault::default()) - .resize_mode(papaya::ResizeMode::Blocking) - .build(), - tsconfigs_built: HashMap::builder() - .hasher(BuildHasherDefault::default()) - .resize_mode(papaya::ResizeMode::Blocking) - .build(), + paths: DashMap::with_hasher(BuildHasherDefault::default()), + tsconfigs_raw: DashMap::with_hasher(BuildHasherDefault::default()), + tsconfigs_built: DashMap::with_hasher(BuildHasherDefault::default()), #[cfg(feature = "yarn_pnp")] yarn_pnp_manifest: OnceCell::new(), } diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 24d126438..fe71d4450 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -1,4 +1,3 @@ -mod borrowed_path; mod cache_impl; mod cached_meta; mod cached_path; @@ -10,27 +9,10 @@ pub use cached_path::CachedPath; #[cfg(test)] mod tests { - use super::borrowed_path::BorrowedCachedPath; use super::cache_impl::Cache; use crate::FileSystem; use std::path::Path; - #[test] - fn test_borrowed_cached_path_eq() { - let path1 = Path::new("/foo/bar"); - let path2 = Path::new("/foo/bar"); - let path3 = Path::new("/foo/baz"); - - let borrowed1 = BorrowedCachedPath { hash: 1, path: path1 }; - let borrowed2 = BorrowedCachedPath { hash: 2, path: path2 }; - let borrowed3 = BorrowedCachedPath { hash: 1, path: path3 }; - - // Same path should be equal even with different hash - assert_eq!(borrowed1, borrowed2); - // Different path should not be equal even with same hash - assert_ne!(borrowed1, borrowed3); - } - #[test] fn test_cached_path_debug() { #[cfg(feature = "yarn_pnp")]