Skip to content

Commit 703c84b

Browse files
authored
perf: reduce memory allocation by using a thread_local path for path methods (#315)
1 parent 8ab444b commit 703c84b

File tree

2 files changed

+88
-29
lines changed

2 files changed

+88
-29
lines changed

src/cache.rs

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use std::{
22
borrow::{Borrow, Cow},
3+
cell::UnsafeCell,
34
convert::AsRef,
45
hash::{BuildHasherDefault, Hash, Hasher},
56
io,
67
ops::Deref,
7-
path::{Path, PathBuf},
8+
path::{Component, Path, PathBuf},
89
sync::Arc,
910
};
1011

@@ -17,6 +18,12 @@ use crate::{
1718
FileSystem, ResolveError, ResolveOptions, TsConfig,
1819
};
1920

21+
thread_local! {
22+
/// Per-thread pre-allocated path that is used to perform operations on paths more quickly.
23+
/// Learned from parcel <https://github.com/parcel-bundler/parcel/blob/a53f8f3ba1025c7ea8653e9719e0a61ef9717079/crates/parcel-resolver/src/cache.rs#L394>
24+
pub static SCRATCH_PATH: UnsafeCell<PathBuf> = UnsafeCell::new(PathBuf::with_capacity(256));
25+
}
26+
2027
#[derive(Default)]
2128
pub struct Cache<Fs> {
2229
pub(crate) fs: Fs,
@@ -303,6 +310,72 @@ impl CachedPathImpl {
303310
}
304311
result
305312
}
313+
314+
pub fn add_extension<Fs: FileSystem>(&self, ext: &str, cache: &Cache<Fs>) -> CachedPath {
315+
SCRATCH_PATH.with(|path| {
316+
// SAFETY: ???
317+
let path = unsafe { &mut *path.get() };
318+
path.clear();
319+
let s = path.as_mut_os_string();
320+
s.push(self.path.as_os_str());
321+
s.push(ext);
322+
cache.value(path)
323+
})
324+
}
325+
326+
pub fn replace_extension<Fs: FileSystem>(&self, ext: &str, cache: &Cache<Fs>) -> CachedPath {
327+
SCRATCH_PATH.with(|path| {
328+
// SAFETY: ???
329+
let path = unsafe { &mut *path.get() };
330+
path.clear();
331+
let s = path.as_mut_os_string();
332+
let self_len = self.path.as_os_str().len();
333+
let self_bytes = self.path.as_os_str().as_encoded_bytes();
334+
let slice_to_copy = self.path.extension().map_or(self_bytes, |previous_extension| {
335+
&self_bytes[..self_len - previous_extension.len() - 1]
336+
});
337+
// SAFETY: ???
338+
s.push(unsafe { std::ffi::OsStr::from_encoded_bytes_unchecked(slice_to_copy) });
339+
s.push(ext);
340+
cache.value(path)
341+
})
342+
}
343+
344+
/// Returns a new path by resolving the given subpath (including "." and ".." components) with this path.
345+
pub fn normalize_with<P, Fs>(&self, subpath: P, cache: &Cache<Fs>) -> CachedPath
346+
where
347+
P: AsRef<Path>,
348+
Fs: FileSystem,
349+
{
350+
let subpath = subpath.as_ref();
351+
let mut components = subpath.components();
352+
let Some(head) = components.next() else { return cache.value(subpath) };
353+
if matches!(head, Component::Prefix(..) | Component::RootDir) {
354+
return cache.value(subpath);
355+
}
356+
SCRATCH_PATH.with(|path| {
357+
// SAFETY: ???
358+
let path = unsafe { &mut *path.get() };
359+
path.clear();
360+
path.push(&self.path);
361+
for component in std::iter::once(head).chain(components) {
362+
match component {
363+
Component::CurDir => {}
364+
Component::ParentDir => {
365+
path.pop();
366+
}
367+
Component::Normal(c) => {
368+
path.push(c);
369+
}
370+
Component::Prefix(..) | Component::RootDir => {
371+
unreachable!("Path {:?} Subpath {:?}", self.path, subpath)
372+
}
373+
}
374+
}
375+
376+
cache.value(path)
377+
})
378+
}
306379
}
307380

308381
/// Memoized cache key, code adapted from <https://stackoverflow.com/a/50478038>.

src/lib.rs

Lines changed: 14 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -395,8 +395,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
395395
c,
396396
Component::CurDir | Component::ParentDir | Component::Normal(_)
397397
)));
398-
let path = cached_path.path().normalize_with(specifier);
399-
let cached_path = self.cache.value(&path);
398+
let cached_path = cached_path.normalize_with(specifier, &self.cache);
400399
// a. LOAD_AS_FILE(Y + X)
401400
// b. LOAD_AS_DIRECTORY(Y + X)
402401
if let Some(path) = self.load_as_file_or_directory(&cached_path, specifier, ctx)? {
@@ -546,9 +545,8 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
546545
// b. If "main" is a falsy value, GOTO 2.
547546
for main_field in package_json.main_fields(&self.options.main_fields) {
548547
// c. let M = X + (json main field)
549-
let main_field_path = cached_path.path().normalize_with(main_field);
548+
let cached_path = cached_path.normalize_with(main_field, &self.cache);
550549
// d. LOAD_AS_FILE(M)
551-
let cached_path = self.cache.value(&main_field_path);
552550
if let Some(path) = self.load_as_file(&cached_path, ctx)? {
553551
return Ok(Some(path));
554552
}
@@ -596,12 +594,8 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
596594
if ctx.fully_specified {
597595
return Ok(None);
598596
}
599-
let path = path.path().as_os_str();
600597
for extension in extensions {
601-
let mut path_with_extension = path.to_os_string();
602-
path_with_extension.reserve_exact(extension.len());
603-
path_with_extension.push(extension);
604-
let cached_path = self.cache.value(Path::new(&path_with_extension));
598+
let cached_path = path.add_extension(extension, &self.cache);
605599
if let Some(path) = self.load_alias_or_file(&cached_path, ctx)? {
606600
return Ok(Some(path));
607601
}
@@ -648,8 +642,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
648642

649643
fn load_index(&self, cached_path: &CachedPath, ctx: &mut Ctx) -> ResolveResult {
650644
for main_file in &self.options.main_files {
651-
let main_path = cached_path.path().normalize_with(main_file);
652-
let cached_path = self.cache.value(&main_path);
645+
let cached_path = cached_path.normalize_with(main_file, &self.cache);
653646
if self.options.enforce_extension.is_disabled() {
654647
if let Some(path) = self.load_alias_or_file(&cached_path, ctx)? {
655648
return Ok(Some(path));
@@ -722,8 +715,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
722715
// 1. Try to interpret X as a combination of NAME and SUBPATH where the name
723716
// may have a @scope/ prefix and the subpath begins with a slash (`/`).
724717
if !package_name.is_empty() {
725-
let package_path = cached_path.path().normalize_with(package_name);
726-
let cached_path = self.cache.value(&package_path);
718+
let cached_path = cached_path.normalize_with(package_name, &self.cache);
727719
// Try foo/node_modules/package_name
728720
if cached_path.is_dir(&self.cache.fs, ctx) {
729721
// a. LOAD_PACKAGE_EXPORTS(X, DIR)
@@ -752,8 +744,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
752744
// Try as file or directory for all other cases
753745
// b. LOAD_AS_FILE(DIR/X)
754746
// c. LOAD_AS_DIRECTORY(DIR/X)
755-
let node_module_file = cached_path.path().normalize_with(specifier);
756-
let cached_path = self.cache.value(&node_module_file);
747+
let cached_path = cached_path.normalize_with(specifier, &self.cache);
757748
if let Some(path) = self.load_as_file_or_directory(&cached_path, specifier, ctx)? {
758749
return Ok(Some(path));
759750
}
@@ -984,8 +975,8 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
984975
}
985976
}
986977
AliasValue::Ignore => {
987-
let path = cached_path.path().normalize_with(alias_key);
988-
return Err(ResolveError::Ignored(path));
978+
let cached_path = cached_path.normalize_with(alias_key, &self.cache);
979+
return Err(ResolveError::Ignored(cached_path.to_path_buf()));
989980
}
990981
}
991982
}
@@ -1025,8 +1016,8 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
10251016

10261017
// Remove the leading slash so the final path is concatenated.
10271018
let tail = tail.trim_start_matches(SLASH_START);
1028-
let normalized = alias_value.normalize_with(tail);
1029-
Cow::Owned(normalized.to_string_lossy().to_string())
1019+
let normalized = alias_value_cached_path.normalize_with(tail, &self.cache);
1020+
Cow::Owned(normalized.path().to_string_lossy().to_string())
10301021
};
10311022

10321023
*should_stop = true;
@@ -1067,13 +1058,9 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
10671058
};
10681059
let path = cached_path.path();
10691060
let Some(filename) = path.file_name() else { return Ok(None) };
1070-
let path_without_extension = path.with_extension("");
10711061
ctx.with_fully_specified(true);
10721062
for extension in extensions {
1073-
let mut path_with_extension = path_without_extension.clone().into_os_string();
1074-
path_with_extension.reserve_exact(extension.len());
1075-
path_with_extension.push(extension);
1076-
let cached_path = self.cache.value(Path::new(&path_with_extension));
1063+
let cached_path = cached_path.replace_extension(extension, &self.cache);
10771064
if let Some(path) = self.load_alias_or_file(&cached_path, ctx)? {
10781065
ctx.with_fully_specified(false);
10791066
return Ok(Some(path));
@@ -1271,8 +1258,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
12711258
continue;
12721259
};
12731260
// 2. Set parentURL to the parent folder URL of parentURL.
1274-
let package_path = cached_path.path().normalize_with(package_name);
1275-
let cached_path = self.cache.value(&package_path);
1261+
let cached_path = cached_path.normalize_with(package_name, &self.cache);
12761262
// 3. If the folder at packageURL does not exist, then
12771263
// 1. Continue the next loop iteration.
12781264
if cached_path.is_dir(&self.cache.fs, ctx) {
@@ -1297,8 +1283,8 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
12971283
// 1. If pjson.main is a string, then
12981284
for main_field in package_json.main_fields(&self.options.main_fields) {
12991285
// 1. Return the URL resolution of main in packageURL.
1300-
let path = cached_path.path().normalize_with(main_field);
1301-
let cached_path = self.cache.value(&path);
1286+
let cached_path =
1287+
cached_path.normalize_with(main_field, &self.cache);
13021288
if cached_path.is_file(&self.cache.fs, ctx) {
13031289
return Ok(Some(cached_path));
13041290
}

0 commit comments

Comments
 (0)