diff --git a/src/cache.rs b/src/cache.rs index 6e5d1951..e0cf1cc0 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,10 +1,11 @@ use std::{ borrow::{Borrow, Cow}, + cell::UnsafeCell, convert::AsRef, hash::{BuildHasherDefault, Hash, Hasher}, io, ops::Deref, - path::{Path, PathBuf}, + path::{Component, Path, PathBuf}, sync::Arc, }; @@ -17,6 +18,12 @@ use crate::{ FileSystem, ResolveError, ResolveOptions, TsConfig, }; +thread_local! { + /// Per-thread pre-allocated path that is used to perform operations on paths more quickly. + /// Learned from parcel + pub static SCRATCH_PATH: UnsafeCell = UnsafeCell::new(PathBuf::with_capacity(256)); +} + #[derive(Default)] pub struct Cache { pub(crate) fs: Fs, @@ -303,6 +310,72 @@ impl CachedPathImpl { } result } + + pub fn add_extension(&self, ext: &str, cache: &Cache) -> CachedPath { + SCRATCH_PATH.with(|path| { + // SAFETY: ??? + let path = unsafe { &mut *path.get() }; + path.clear(); + let s = path.as_mut_os_string(); + s.push(self.path.as_os_str()); + s.push(ext); + cache.value(path) + }) + } + + pub fn replace_extension(&self, ext: &str, cache: &Cache) -> CachedPath { + SCRATCH_PATH.with(|path| { + // SAFETY: ??? + let path = unsafe { &mut *path.get() }; + path.clear(); + let s = path.as_mut_os_string(); + let self_len = self.path.as_os_str().len(); + let self_bytes = self.path.as_os_str().as_encoded_bytes(); + let slice_to_copy = self.path.extension().map_or(self_bytes, |previous_extension| { + &self_bytes[..self_len - previous_extension.len() - 1] + }); + // SAFETY: ??? + s.push(unsafe { std::ffi::OsStr::from_encoded_bytes_unchecked(slice_to_copy) }); + s.push(ext); + cache.value(path) + }) + } + + /// Returns a new path by resolving the given subpath (including "." and ".." components) with this path. + pub fn normalize_with(&self, subpath: P, cache: &Cache) -> CachedPath + where + P: AsRef, + Fs: FileSystem, + { + let subpath = subpath.as_ref(); + let mut components = subpath.components(); + let Some(head) = components.next() else { return cache.value(subpath) }; + if matches!(head, Component::Prefix(..) | Component::RootDir) { + return cache.value(subpath); + } + SCRATCH_PATH.with(|path| { + // SAFETY: ??? + let path = unsafe { &mut *path.get() }; + path.clear(); + path.push(&self.path); + for component in std::iter::once(head).chain(components) { + match component { + Component::CurDir => {} + Component::ParentDir => { + path.pop(); + } + Component::Normal(c) => { + path.push(c); + } + Component::Prefix(..) | Component::RootDir => { + unreachable!("Path {:?} Subpath {:?}", self.path, subpath) + } + } + } + + cache.value(path) + }) + } } /// Memoized cache key, code adapted from . diff --git a/src/lib.rs b/src/lib.rs index 0a6730ff..ada46bdd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -395,8 +395,7 @@ impl ResolverGeneric { c, Component::CurDir | Component::ParentDir | Component::Normal(_) ))); - let path = cached_path.path().normalize_with(specifier); - let cached_path = self.cache.value(&path); + let cached_path = cached_path.normalize_with(specifier, &self.cache); // a. LOAD_AS_FILE(Y + X) // b. LOAD_AS_DIRECTORY(Y + X) if let Some(path) = self.load_as_file_or_directory(&cached_path, specifier, ctx)? { @@ -546,9 +545,8 @@ impl ResolverGeneric { // b. If "main" is a falsy value, GOTO 2. for main_field in package_json.main_fields(&self.options.main_fields) { // c. let M = X + (json main field) - let main_field_path = cached_path.path().normalize_with(main_field); + let cached_path = cached_path.normalize_with(main_field, &self.cache); // d. LOAD_AS_FILE(M) - let cached_path = self.cache.value(&main_field_path); if let Some(path) = self.load_as_file(&cached_path, ctx)? { return Ok(Some(path)); } @@ -596,12 +594,8 @@ impl ResolverGeneric { if ctx.fully_specified { return Ok(None); } - let path = path.path().as_os_str(); for extension in extensions { - let mut path_with_extension = path.to_os_string(); - path_with_extension.reserve_exact(extension.len()); - path_with_extension.push(extension); - let cached_path = self.cache.value(Path::new(&path_with_extension)); + let cached_path = path.add_extension(extension, &self.cache); if let Some(path) = self.load_alias_or_file(&cached_path, ctx)? { return Ok(Some(path)); } @@ -648,8 +642,7 @@ impl ResolverGeneric { fn load_index(&self, cached_path: &CachedPath, ctx: &mut Ctx) -> ResolveResult { for main_file in &self.options.main_files { - let main_path = cached_path.path().normalize_with(main_file); - let cached_path = self.cache.value(&main_path); + let cached_path = cached_path.normalize_with(main_file, &self.cache); if self.options.enforce_extension.is_disabled() { if let Some(path) = self.load_alias_or_file(&cached_path, ctx)? { return Ok(Some(path)); @@ -722,8 +715,7 @@ impl ResolverGeneric { // 1. Try to interpret X as a combination of NAME and SUBPATH where the name // may have a @scope/ prefix and the subpath begins with a slash (`/`). if !package_name.is_empty() { - let package_path = cached_path.path().normalize_with(package_name); - let cached_path = self.cache.value(&package_path); + let cached_path = cached_path.normalize_with(package_name, &self.cache); // Try foo/node_modules/package_name if cached_path.is_dir(&self.cache.fs, ctx) { // a. LOAD_PACKAGE_EXPORTS(X, DIR) @@ -752,8 +744,7 @@ impl ResolverGeneric { // Try as file or directory for all other cases // b. LOAD_AS_FILE(DIR/X) // c. LOAD_AS_DIRECTORY(DIR/X) - let node_module_file = cached_path.path().normalize_with(specifier); - let cached_path = self.cache.value(&node_module_file); + let cached_path = cached_path.normalize_with(specifier, &self.cache); if let Some(path) = self.load_as_file_or_directory(&cached_path, specifier, ctx)? { return Ok(Some(path)); } @@ -984,8 +975,8 @@ impl ResolverGeneric { } } AliasValue::Ignore => { - let path = cached_path.path().normalize_with(alias_key); - return Err(ResolveError::Ignored(path)); + let cached_path = cached_path.normalize_with(alias_key, &self.cache); + return Err(ResolveError::Ignored(cached_path.to_path_buf())); } } } @@ -1025,8 +1016,8 @@ impl ResolverGeneric { // Remove the leading slash so the final path is concatenated. let tail = tail.trim_start_matches(SLASH_START); - let normalized = alias_value.normalize_with(tail); - Cow::Owned(normalized.to_string_lossy().to_string()) + let normalized = alias_value_cached_path.normalize_with(tail, &self.cache); + Cow::Owned(normalized.path().to_string_lossy().to_string()) }; *should_stop = true; @@ -1067,13 +1058,9 @@ impl ResolverGeneric { }; let path = cached_path.path(); let Some(filename) = path.file_name() else { return Ok(None) }; - let path_without_extension = path.with_extension(""); ctx.with_fully_specified(true); for extension in extensions { - let mut path_with_extension = path_without_extension.clone().into_os_string(); - path_with_extension.reserve_exact(extension.len()); - path_with_extension.push(extension); - let cached_path = self.cache.value(Path::new(&path_with_extension)); + let cached_path = cached_path.replace_extension(extension, &self.cache); if let Some(path) = self.load_alias_or_file(&cached_path, ctx)? { ctx.with_fully_specified(false); return Ok(Some(path)); @@ -1271,8 +1258,7 @@ impl ResolverGeneric { continue; }; // 2. Set parentURL to the parent folder URL of parentURL. - let package_path = cached_path.path().normalize_with(package_name); - let cached_path = self.cache.value(&package_path); + let cached_path = cached_path.normalize_with(package_name, &self.cache); // 3. If the folder at packageURL does not exist, then // 1. Continue the next loop iteration. if cached_path.is_dir(&self.cache.fs, ctx) { @@ -1297,8 +1283,8 @@ impl ResolverGeneric { // 1. If pjson.main is a string, then for main_field in package_json.main_fields(&self.options.main_fields) { // 1. Return the URL resolution of main in packageURL. - let path = cached_path.path().normalize_with(main_field); - let cached_path = self.cache.value(&path); + let cached_path = + cached_path.normalize_with(main_field, &self.cache); if cached_path.is_file(&self.cache.fs, ctx) { return Ok(Some(cached_path)); }