Skip to content

Commit e903e4e

Browse files
authored
perf: reduce memory allocation while normalizing package path (#318)
1 parent d5b88b3 commit e903e4e

File tree

2 files changed

+86
-88
lines changed

2 files changed

+86
-88
lines changed

src/cache.rs

Lines changed: 78 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ use once_cell::sync::OnceCell as OnceLock;
1414
use rustc_hash::FxHasher;
1515

1616
use crate::{
17-
context::ResolveContext as Ctx, package_json::PackageJson, path::PathUtil, FileMetadata,
18-
FileSystem, ResolveError, ResolveOptions, TsConfig,
17+
context::ResolveContext as Ctx, package_json::PackageJson, FileMetadata, FileSystem,
18+
ResolveError, ResolveOptions, TsConfig,
1919
};
2020

2121
thread_local! {
@@ -136,39 +136,17 @@ impl CacheKey for CachedPath {
136136
}
137137
}
138138

139-
pub struct CachedPathImpl {
140-
hash: u64,
141-
path: Box<Path>,
142-
parent: Option<CachedPath>,
143-
meta: OnceLock<Option<FileMetadata>>,
144-
canonicalized: OnceLock<Option<PathBuf>>,
145-
node_modules: OnceLock<Option<CachedPath>>,
146-
package_json: OnceLock<Option<Arc<PackageJson>>>,
147-
}
148-
149-
impl CachedPathImpl {
150-
const fn new(hash: u64, path: Box<Path>, parent: Option<CachedPath>) -> Self {
151-
Self {
152-
hash,
153-
path,
154-
parent,
155-
meta: OnceLock::new(),
156-
canonicalized: OnceLock::new(),
157-
node_modules: OnceLock::new(),
158-
package_json: OnceLock::new(),
159-
}
160-
}
161-
162-
pub const fn path(&self) -> &Path {
163-
&self.path
139+
impl CachedPath {
140+
pub fn path(&self) -> &Path {
141+
&self.0.path
164142
}
165143

166144
pub fn to_path_buf(&self) -> PathBuf {
167145
self.path.to_path_buf()
168146
}
169147

170-
pub const fn parent(&self) -> Option<&CachedPath> {
171-
self.parent.as_ref()
148+
pub fn parent(&self) -> Option<&Self> {
149+
self.0.parent.as_ref()
172150
}
173151

174152
fn meta<Fs: FileSystem>(&self, fs: &Fs) -> Option<FileMetadata> {
@@ -195,30 +173,31 @@ impl CachedPathImpl {
195173
)
196174
}
197175

198-
pub fn realpath<Fs: FileSystem>(&self, fs: &Fs) -> io::Result<PathBuf> {
176+
pub fn realpath<Fs: FileSystem>(&self, cache: &Cache<Fs>) -> io::Result<Self> {
199177
self.canonicalized
200178
.get_or_try_init(|| {
201-
if fs.symlink_metadata(&self.path).is_ok_and(|m| m.is_symlink) {
202-
return fs.canonicalize(&self.path).map(Some);
179+
if cache.fs.symlink_metadata(&self.path).is_ok_and(|m| m.is_symlink) {
180+
let canonicalized = cache.fs.canonicalize(&self.path)?;
181+
return Ok(Some(cache.value(&canonicalized)));
203182
}
204183
if let Some(parent) = self.parent() {
205-
let parent_path = parent.realpath(fs)?;
206-
return Ok(Some(
207-
parent_path.normalize_with(self.path.strip_prefix(&parent.path).unwrap()),
208-
));
184+
let parent_path = parent.realpath(cache)?;
185+
let normalized = parent_path
186+
.normalize_with(self.path.strip_prefix(&parent.path).unwrap(), cache);
187+
return Ok(Some(normalized));
209188
};
210189
Ok(None)
211190
})
212191
.cloned()
213-
.map(|r| r.unwrap_or_else(|| self.path.clone().to_path_buf()))
192+
.map(|r| r.unwrap_or_else(|| self.clone()))
214193
}
215194

216195
pub fn module_directory<Fs: FileSystem>(
217196
&self,
218197
module_name: &str,
219198
cache: &Cache<Fs>,
220199
ctx: &mut Ctx,
221-
) -> Option<CachedPath> {
200+
) -> Option<Self> {
222201
let cached_path = cache.value(&self.path.join(module_name));
223202
cached_path.is_dir(&cache.fs, ctx).then_some(cached_path)
224203
}
@@ -227,61 +206,31 @@ impl CachedPathImpl {
227206
&self,
228207
cache: &Cache<Fs>,
229208
ctx: &mut Ctx,
230-
) -> Option<CachedPath> {
209+
) -> Option<Self> {
231210
self.node_modules.get_or_init(|| self.module_directory("node_modules", cache, ctx)).clone()
232211
}
233212

234-
/// Find package.json of a path by traversing parent directories.
235-
///
236-
/// # Errors
237-
///
238-
/// * [ResolveError::JSON]
239-
pub fn find_package_json<Fs: FileSystem>(
240-
&self,
241-
fs: &Fs,
242-
options: &ResolveOptions,
243-
ctx: &mut Ctx,
244-
) -> Result<Option<Arc<PackageJson>>, ResolveError> {
245-
let mut cache_value = self;
246-
// Go up directories when the querying path is not a directory
247-
while !cache_value.is_dir(fs, ctx) {
248-
if let Some(cv) = &cache_value.parent {
249-
cache_value = cv.as_ref();
250-
} else {
251-
break;
252-
}
253-
}
254-
let mut cache_value = Some(cache_value);
255-
while let Some(cv) = cache_value {
256-
if let Some(package_json) = cv.package_json(fs, options, ctx)? {
257-
return Ok(Some(Arc::clone(&package_json)));
258-
}
259-
cache_value = cv.parent.as_deref();
260-
}
261-
Ok(None)
262-
}
263-
264213
/// Get package.json of the given path.
265214
///
266215
/// # Errors
267216
///
268217
/// * [ResolveError::JSON]
269218
pub fn package_json<Fs: FileSystem>(
270219
&self,
271-
fs: &Fs,
272220
options: &ResolveOptions,
221+
cache: &Cache<Fs>,
273222
ctx: &mut Ctx,
274223
) -> Result<Option<Arc<PackageJson>>, ResolveError> {
275224
// Change to `std::sync::OnceLock::get_or_try_init` when it is stable.
276225
let result = self
277226
.package_json
278227
.get_or_try_init(|| {
279228
let package_json_path = self.path.join("package.json");
280-
let Ok(package_json_string) = fs.read_to_string(&package_json_path) else {
229+
let Ok(package_json_string) = cache.fs.read_to_string(&package_json_path) else {
281230
return Ok(None);
282231
};
283232
let real_path = if options.symlinks {
284-
self.realpath(fs)?.join("package.json")
233+
self.realpath(cache)?.path().join("package.json")
285234
} else {
286235
package_json_path.clone()
287236
};
@@ -311,7 +260,37 @@ impl CachedPathImpl {
311260
result
312261
}
313262

314-
pub fn add_extension<Fs: FileSystem>(&self, ext: &str, cache: &Cache<Fs>) -> CachedPath {
263+
/// Find package.json of a path by traversing parent directories.
264+
///
265+
/// # Errors
266+
///
267+
/// * [ResolveError::JSON]
268+
pub fn find_package_json<Fs: FileSystem>(
269+
&self,
270+
options: &ResolveOptions,
271+
cache: &Cache<Fs>,
272+
ctx: &mut Ctx,
273+
) -> Result<Option<Arc<PackageJson>>, ResolveError> {
274+
let mut cache_value = self;
275+
// Go up directories when the querying path is not a directory
276+
while !cache_value.is_dir(&cache.fs, ctx) {
277+
if let Some(cv) = &cache_value.parent {
278+
cache_value = cv;
279+
} else {
280+
break;
281+
}
282+
}
283+
let mut cache_value = Some(cache_value);
284+
while let Some(cv) = cache_value {
285+
if let Some(package_json) = cv.package_json(options, cache, ctx)? {
286+
return Ok(Some(Arc::clone(&package_json)));
287+
}
288+
cache_value = cv.parent.as_ref();
289+
}
290+
Ok(None)
291+
}
292+
293+
pub fn add_extension<Fs: FileSystem>(&self, ext: &str, cache: &Cache<Fs>) -> Self {
315294
SCRATCH_PATH.with(|path| {
316295
// SAFETY: ???
317296
let path = unsafe { &mut *path.get() };
@@ -323,7 +302,7 @@ impl CachedPathImpl {
323302
})
324303
}
325304

326-
pub fn replace_extension<Fs: FileSystem>(&self, ext: &str, cache: &Cache<Fs>) -> CachedPath {
305+
pub fn replace_extension<Fs: FileSystem>(&self, ext: &str, cache: &Cache<Fs>) -> Self {
327306
SCRATCH_PATH.with(|path| {
328307
// SAFETY: ???
329308
let path = unsafe { &mut *path.get() };
@@ -342,7 +321,7 @@ impl CachedPathImpl {
342321
}
343322

344323
/// 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
324+
pub fn normalize_with<P, Fs>(&self, subpath: P, cache: &Cache<Fs>) -> Self
346325
where
347326
P: AsRef<Path>,
348327
Fs: FileSystem,
@@ -378,6 +357,30 @@ impl CachedPathImpl {
378357
}
379358
}
380359

360+
pub struct CachedPathImpl {
361+
hash: u64,
362+
path: Box<Path>,
363+
parent: Option<CachedPath>,
364+
meta: OnceLock<Option<FileMetadata>>,
365+
canonicalized: OnceLock<Option<CachedPath>>,
366+
node_modules: OnceLock<Option<CachedPath>>,
367+
package_json: OnceLock<Option<Arc<PackageJson>>>,
368+
}
369+
370+
impl CachedPathImpl {
371+
const fn new(hash: u64, path: Box<Path>, parent: Option<CachedPath>) -> Self {
372+
Self {
373+
hash,
374+
path,
375+
parent,
376+
meta: OnceLock::new(),
377+
canonicalized: OnceLock::new(),
378+
node_modules: OnceLock::new(),
379+
package_json: OnceLock::new(),
380+
}
381+
}
382+
}
383+
381384
/// Memoized cache key, code adapted from <https://stackoverflow.com/a/50478038>.
382385
trait CacheKey {
383386
fn tuple(&self) -> (u64, &Path);

src/lib.rs

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
249249
let path = self.load_realpath(&cached_path)?;
250250
// enhanced-resolve: restrictions
251251
self.check_restrictions(&path)?;
252-
let package_json = cached_path.find_package_json(&self.cache.fs, &self.options, ctx)?;
252+
let package_json = cached_path.find_package_json(&self.options, &self.cache, ctx)?;
253253
if let Some(package_json) = &package_json {
254254
// path must be inside the package.
255255
debug_assert!(path.starts_with(package_json.directory()));
@@ -499,8 +499,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
499499
) -> ResolveResult {
500500
// 1. Find the closest package scope SCOPE to DIR.
501501
// 2. If no scope was found, return.
502-
let Some(package_json) =
503-
cached_path.find_package_json(&self.cache.fs, &self.options, ctx)?
502+
let Some(package_json) = cached_path.find_package_json(&self.options, &self.cache, ctx)?
504503
else {
505504
return Ok(None);
506505
};
@@ -539,9 +538,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
539538
// 1. If X/package.json is a file,
540539
if !self.options.description_files.is_empty() {
541540
// a. Parse X/package.json, and look for "main" field.
542-
if let Some(package_json) =
543-
cached_path.package_json(&self.cache.fs, &self.options, ctx)?
544-
{
541+
if let Some(package_json) = cached_path.package_json(&self.options, &self.cache, ctx)? {
545542
// b. If "main" is a falsy value, GOTO 2.
546543
for main_field in package_json.main_fields(&self.options.main_fields) {
547544
// c. let M = X + (json main field)
@@ -605,7 +602,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
605602

606603
fn load_realpath(&self, cached_path: &CachedPath) -> Result<PathBuf, ResolveError> {
607604
if self.options.symlinks {
608-
cached_path.realpath(&self.cache.fs).map_err(ResolveError::from)
605+
cached_path.realpath(&self.cache).map(|c| c.to_path_buf()).map_err(ResolveError::from)
609606
} else {
610607
Ok(cached_path.to_path_buf())
611608
}
@@ -661,7 +658,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
661658
fn load_alias_or_file(&self, cached_path: &CachedPath, ctx: &mut Ctx) -> ResolveResult {
662659
if !self.options.alias_fields.is_empty() {
663660
if let Some(package_json) =
664-
cached_path.find_package_json(&self.cache.fs, &self.options, ctx)?
661+
cached_path.find_package_json(&self.options, &self.cache, ctx)?
665662
{
666663
if let Some(path) =
667664
self.load_browser_field(cached_path, None, &package_json, ctx)?
@@ -818,8 +815,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
818815
) -> ResolveResult {
819816
// 2. If X does not match this pattern or DIR/NAME/package.json is not a file,
820817
// return.
821-
let Some(package_json) = cached_path.package_json(&self.cache.fs, &self.options, ctx)?
822-
else {
818+
let Some(package_json) = cached_path.package_json(&self.options, &self.cache, ctx)? else {
823819
return Ok(None);
824820
};
825821
// 3. Parse DIR/NAME/package.json, and look for "exports" field.
@@ -846,8 +842,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
846842
) -> ResolveResult {
847843
// 1. Find the closest package scope SCOPE to DIR.
848844
// 2. If no scope was found, return.
849-
let Some(package_json) =
850-
cached_path.find_package_json(&self.cache.fs, &self.options, ctx)?
845+
let Some(package_json) = cached_path.find_package_json(&self.options, &self.cache, ctx)?
851846
else {
852847
return Ok(None);
853848
};
@@ -1264,7 +1259,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
12641259
if cached_path.is_dir(&self.cache.fs, ctx) {
12651260
// 4. Let pjson be the result of READ_PACKAGE_JSON(packageURL).
12661261
if let Some(package_json) =
1267-
cached_path.package_json(&self.cache.fs, &self.options, ctx)?
1262+
cached_path.package_json(&self.options, &self.cache, ctx)?
12681263
{
12691264
// 5. If pjson is not null and pjson.exports is not null or undefined, then
12701265
// 1. Return the result of PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions).

0 commit comments

Comments
 (0)