From 939cd99322f33a66a26ce7a3365226b4ca2d85ca Mon Sep 17 00:00:00 2001 From: pshu Date: Fri, 22 May 2026 02:50:01 +0800 Subject: [PATCH 1/7] chore: ignore optimization-artifacts/ used by micro-opt profiling --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 779b0627..be23db63 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules fuzz/Cargo.lock artifacts bindings +optimization-artifacts/ From f76bab1e880a187fed749bf39b60972a61e4f06a Mon Sep 17 00:00:00 2001 From: pshu Date: Fri, 22 May 2026 03:10:33 +0800 Subject: [PATCH 2/7] perf(resolver): cut allocs and std::path overhead on cold-cache single-thread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Combines three small wins on the single-thread bench: 1. Byte-level specifier dispatch in require_without_parse — avoids the std Path::Components walk just to pick the require_* branch on every resolve. Behavior is preserved for unix; windows keeps the std parser only for drive-prefix detection. 2. Raw byte Path eq for the cache DashSet key on unix — mirrors the existing raw-byte hash (#226) and skips std Components iteration on every cache lookup. 3. Skip format!(".{subpath}") at four package_exports/resolve sites when subpath is empty (the common bare-specifier case like '@scope/pkg'). Removes one String alloc per resolve in the common path. Bench (callgrind / CodSpeed CPU simulation formula, macOS+linux/arm64): resolver/single-thread: accesses: 143,727,872 -> 142,544,544 (-0.82%) estimated_cycles: 176,488,172 -> 174,865,264 (-0.92%) resolver/[single-threaded]resolve with many extensions: accesses: 325,700,719 -> 323,358,526 (-0.72%) estimated_cycles: 380,377,449 -> 377,302,346 (-0.81%) --- src/cache.rs | 14 ++++++- src/lib.rs | 101 ++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 97 insertions(+), 18 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index 66d92643..4edab83f 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -427,7 +427,19 @@ impl Hash for dyn CacheKey + '_ { impl PartialEq for dyn CacheKey + '_ { fn eq(&self, other: &Self) -> bool { - self.tuple().1 == other.tuple().1 + let a = self.tuple().1; + let b = other.tuple().1; + // On Unix, raw byte compare matches `Path::eq` semantics and skips + // the std `Components` iteration cost. Mirrors the hash side already + // bytes-based in `Cache::value`. + #[cfg(unix)] + { + a.as_os_str().as_bytes() == b.as_os_str().as_bytes() + } + #[cfg(not(unix))] + { + a == b + } } } diff --git a/src/lib.rs b/src/lib.rs index bc0bd309..f913760e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -97,6 +97,50 @@ use crate::{ type ResolveResult = Result, ResolveError>; +/// Classifies a specifier into the dispatch bucket used by +/// [`ResolverGeneric::require_without_parse`]. +/// +/// Avoids the std `Path::Components` walk for the dispatch decision, which +/// fires on every resolve. The bench `single-thread` case triggers this +/// ~880 times per iteration with all-cold caches. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum SpecifierKind { + Absolute, + Relative, + Hash, + Other, +} + +#[inline] +fn specifier_kind(specifier: &str) -> SpecifierKind { + let bytes = specifier.as_bytes(); + match bytes { + [] => SpecifierKind::Other, + [b'/', ..] => SpecifierKind::Absolute, + #[cfg(windows)] + [b'\\', ..] => SpecifierKind::Absolute, + [b'.', b'.'] | [b'.', b'.', b'/', ..] => SpecifierKind::Relative, + #[cfg(windows)] + [b'.', b'.', b'\\', ..] => SpecifierKind::Relative, + [b'.'] | [b'.', b'/', ..] => SpecifierKind::Relative, + #[cfg(windows)] + [b'.', b'\\', ..] => SpecifierKind::Relative, + [b'#', ..] => SpecifierKind::Hash, + _ => { + #[cfg(windows)] + { + if matches!( + Path::new(specifier).components().next(), + Some(Component::Prefix(_)) + ) { + return SpecifierKind::Absolute; + } + } + SpecifierKind::Other + } + } +} + /// Context returned from the [Resolver::resolve_with_context] API #[derive(Debug, Default, Clone)] pub struct ResolveContext { @@ -336,20 +380,14 @@ impl ResolverGeneric { return Ok(path); } - let result = match Path::new(specifier).components().next() { + let result = match specifier_kind(specifier) { // 2. If X begins with '/' - Some(Component::RootDir | Component::Prefix(_)) => { - self.require_absolute(cached_path, specifier, ctx).await - } - // 3. If X begins with './' or '/' or '../' - Some(Component::CurDir | Component::ParentDir) => { - self.require_relative(cached_path, specifier, ctx).await - } + SpecifierKind::Absolute => self.require_absolute(cached_path, specifier, ctx).await, + // 3. If X begins with './' or '../' (or is "." / "..") + SpecifierKind::Relative => self.require_relative(cached_path, specifier, ctx).await, // 4. If X begins with '#' - Some(Component::Normal(_)) if specifier.as_bytes()[0] == b'#' => { - self.require_hash(cached_path, specifier, ctx).await - } - _ => { + SpecifierKind::Hash => self.require_hash(cached_path, specifier, ctx).await, + SpecifierKind::Other => { // 1. If X is a core module, // a. return the core module // b. STOP @@ -1096,9 +1134,18 @@ impl ResolverGeneric { // 5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH, // `package.json` "exports", ["node", "require"]) defined in the ESM resolver. // Note: The subpath is not prepended with a dot on purpose + // Skip the `format!` alloc when subpath is empty (the common bare-specifier + // case like `@scope/pkg`). When non-empty, prepend once before the loop. + let prefixed_owned; + let prefixed: &str = if subpath.is_empty() { + "." + } else { + prefixed_owned = format!(".{subpath}"); + &prefixed_owned + }; for exports in package_json.exports_fields(&self.options.exports_fields) { if let Some(path) = self - .package_exports_resolve(cached_path.path(), &format!(".{subpath}"), exports, ctx) + .package_exports_resolve(cached_path.path(), prefixed, exports, ctx) .await? { // 6. RESOLVE_ESM_MATCH(MATCH) @@ -1136,9 +1183,16 @@ impl ResolverGeneric { let package_url = package_json.directory(); // Note: The subpath is not prepended with a dot on purpose // because `package_exports_resolve` matches subpath without the leading dot. + let prefixed_owned; + let prefixed: &str = if subpath.is_empty() { + "." + } else { + prefixed_owned = format!(".{subpath}"); + &prefixed_owned + }; for exports in package_json.exports_fields(&self.options.exports_fields) { if let Some(cached_path) = self - .package_exports_resolve(package_url, &format!(".{subpath}"), exports, ctx) + .package_exports_resolve(package_url, prefixed, exports, ctx) .await? { // 6. RESOLVE_ESM_MATCH(MATCH) @@ -1707,9 +1761,16 @@ impl ResolverGeneric { .package_json(&self.cache.fs, &self.options, ctx) .await? { + let prefixed_owned; + let prefixed: &str = if subpath.is_empty() { + "." + } else { + prefixed_owned = format!(".{subpath}"); + &prefixed_owned + }; for exports in package_json.exports_fields(&self.options.exports_fields) { if let Some(path) = self - .package_exports_resolve(cached_path.path(), &format!(".{subpath}"), exports, ctx) + .package_exports_resolve(cached_path.path(), prefixed, exports, ctx) .await? { return Ok(Some(path)); @@ -1727,9 +1788,15 @@ impl ResolverGeneric { } } } - let subpath = format!(".{subpath}"); + let subpath_owned; + let subpath: &str = if subpath.is_empty() { + "." + } else { + subpath_owned = format!(".{subpath}"); + &subpath_owned + }; ctx.with_fully_specified(false); - self.require(&cached_path, &subpath, ctx).await.map(Some) + self.require(&cached_path, subpath, ctx).await.map(Some) } /// PACKAGE_EXPORTS_RESOLVE(packageURL, subpath, exports, conditions) From 6e9b24a79bfb54d7b2b134e2d864e5ad646fe943 Mon Sep 17 00:00:00 2001 From: pshu Date: Fri, 22 May 2026 03:17:19 +0800 Subject: [PATCH 3/7] perf(path): preallocate normalize_with output to avoid Vec regrowth normalize_with walks the subpath components and pushes each one onto a PathBuf seeded from self.to_path_buf(). The seeded PathBuf has capacity == self.len() so every pushed component (separator + bytes) forced at least one Vec regrow + memcpy of the existing path. Switch to PathBuf::with_capacity(self.len() + subpath.len() + 1) and push self once up front. The worst-case capacity covers self, the separator, and the full subpath, so the loop body's pushes never grow. Bench (callgrind / CodSpeed CPU simulation formula, macOS+linux/arm64): resolver/single-thread: accesses: 142,544,544 -> 140,995,923 (-1.09% step; -1.90% vs baseline) estimated_cycles: 174,865,264 -> 173,031,518 (-1.05% step; -1.96% vs baseline) resolver/[single-threaded]resolve with many extensions: accesses: 323,358,526 -> 323,689,695 (+0.10% step; -0.62% vs baseline) --- src/path.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/path.rs b/src/path.rs index 5034533d..255b4af3 100644 --- a/src/path.rs +++ b/src/path.rs @@ -77,7 +77,10 @@ impl PathUtil for Path { return subpath.to_path_buf(); } - let mut ret = self.to_path_buf(); + // Pre-size to the worst-case length so the loop's pushes can never grow + // the inner Vec. `+1` covers the separator inserted by `PathBuf::push`. + let mut ret = PathBuf::with_capacity(self.as_os_str().len() + subpath.as_os_str().len() + 1); + ret.push(self); for component in std::iter::once(head).chain(components) { match component { Component::CurDir => {} From 601d27a8999a828398ccc4f4756b0d5591335a54 Mon Sep 17 00:00:00 2001 From: pshu Date: Fri, 22 May 2026 03:20:46 +0800 Subject: [PATCH 4/7] perf(cache): pre-size PathBuf when joining hot module/package paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cache.value(self.path.join("node_modules")) and the package.json lookup both rely on std::Path::join, which does self.to_path_buf() (exact-size alloc) followed by .push(sub) — guaranteed to trigger a Vec regrow + memcpy of the just-allocated bytes on every call. Introduce path_join_preallocated that PathBuf::with_capacity(base.len + sub.len + 1) before pushing, so the loop never grows. Use it at the two hottest join sites (cached_node_modules' walk and package_json's get_or_try_init). Bench (callgrind / CodSpeed CPU simulation formula, macOS+linux/arm64): resolver/single-thread: accesses: 140,995,923 -> 140,702,086 (-0.21% step; -2.11% vs baseline) estimated_cycles: 173,031,518 -> 172,695,646 (-0.19% step; -2.15% vs baseline) resolver/[single-threaded]resolve with many extensions: accesses: 323,689,695 -> 322,333,719 (-0.42% step; -1.03% vs baseline) --- src/cache.rs | 6 +++--- src/path.rs | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index 4edab83f..5f6ff534 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -19,7 +19,7 @@ use tokio::sync::OnceCell as OnceLock; use crate::{ context::ResolveContext as Ctx, package_json::{off_to_location, PackageJson}, - path::PathUtil, + path::{path_join_preallocated, PathUtil}, FileMetadata, FileSystem, JSONError, ResolveError, ResolveOptions, TsConfig, }; @@ -268,7 +268,7 @@ impl CachedPathImpl { cache: &Cache, ctx: &mut Ctx, ) -> Option { - let cached_path = cache.value(&self.path.join(module_name)); + let cached_path = cache.value(&path_join_preallocated(&self.path, module_name)); cached_path .is_dir(&cache.fs, ctx) .await @@ -349,7 +349,7 @@ impl CachedPathImpl { let result = self .package_json .get_or_try_init(|| async { - let package_json_path = self.path.join("package.json"); + let package_json_path = path_join_preallocated(&self.path, "package.json"); let Ok(package_json_string) = fs.read(&package_json_path).await else { return Ok(None); }; diff --git a/src/path.rs b/src/path.rs index 255b4af3..2603a821 100644 --- a/src/path.rs +++ b/src/path.rs @@ -11,6 +11,18 @@ pub fn path_to_str(path: &Path) -> &str { path.to_str().expect("path should be UTF-8") } +/// `Path::join` equivalent that allocates the worst-case capacity up front so +/// the inner `Vec` never has to regrow when the separator + segment is pushed. +/// Hot on parent walks that repeatedly join names like `node_modules` and +/// `package.json` to a cached directory path. +#[inline] +pub fn path_join_preallocated(base: &Path, sub: &str) -> PathBuf { + let mut buf = PathBuf::with_capacity(base.as_os_str().len() + sub.len() + 1); + buf.push(base); + buf.push(sub); + buf +} + /// Extension trait to add path normalization to std's [`Path`]. pub trait PathUtil { /// Normalize this path without performing I/O. From e8284030f5fd516187ffeaf776c4b075f2cf4094 Mon Sep 17 00:00:00 2001 From: pshu Date: Fri, 22 May 2026 03:26:11 +0800 Subject: [PATCH 5/7] perf(cache): byte-level path parent in Cache::value on unix Cache::value's recursion calls Path::parent for every cache miss to chain up to the root, and std::Path::parent builds a Components iterator just to walk one step back. The bench shows parse_next_component_back weighs ~2M Ir on resolver/single-thread alone. Add path_parent_unix that scans the raw bytes once for the last non-separator and the previous separator, matching std's exact semantics (verified with a new test against std::Path::parent across absolute, relative, trailing-slash, repeated-slash, and root cases). Cache::value uses it on cfg(unix), keeping the std path for windows. Bench (callgrind / CodSpeed CPU simulation formula, macOS+linux/arm64): resolver/single-thread: accesses: 140,702,086 -> 139,041,327 (-1.18% step; -3.26% vs baseline) estimated_cycles: 172,695,646 -> 171,064,577 (-0.94% step; -3.07% vs baseline) resolver/[single-threaded]resolve with many extensions: accesses: 322,333,719 -> 316,116,887 (-1.93% step; -2.94% vs baseline) --- src/cache.rs | 5 ++++ src/path.rs | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/src/cache.rs b/src/cache.rs index 5f6ff534..b6922143 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -16,6 +16,8 @@ use futures::future::BoxFuture; use rustc_hash::FxHasher; use tokio::sync::OnceCell as OnceLock; +#[cfg(unix)] +use crate::path::path_parent_unix; use crate::{ context::ResolveContext as Ctx, package_json::{off_to_location, PackageJson}, @@ -59,6 +61,9 @@ impl Cache { if let Some(cache_entry) = self.paths.get((hash, path).borrow() as &dyn CacheKey) { return cache_entry.clone(); } + #[cfg(unix)] + let parent = path_parent_unix(path).map(|p| self.value(p)); + #[cfg(not(unix))] let parent = path.parent().map(|p| self.value(p)); let data = CachedPath(Arc::new(CachedPathImpl::new( hash, diff --git a/src/path.rs b/src/path.rs index 2603a821..3e35958b 100644 --- a/src/path.rs +++ b/src/path.rs @@ -23,6 +23,38 @@ pub fn path_join_preallocated(base: &Path, sub: &str) -> PathBuf { buf } +/// Byte-level [`Path::parent`] for unix targets. +/// +/// `std::path::Path::parent` builds a `Components` iterator and walks one step +/// back, which the bench shows costs ~2M Ir per `resolver/single-thread` +/// iteration (one call per cache miss in [`crate::cache::Cache::value`]). +/// Operating on raw bytes — like the existing `Cache::value` hash and the +/// `CachedPath` eq path — sidesteps that. +/// +/// Behavior mirrors std's impl: drops trailing separators, splits at the last +/// remaining separator, collapses repeated separators before the split point, +/// and returns `None` for an empty path or a path whose only content is one +/// or more separators (i.e. root). +#[cfg(unix)] +#[inline] +pub fn path_parent_unix(path: &Path) -> Option<&Path> { + use std::{ffi::OsStr, os::unix::ffi::OsStrExt}; + + let bytes = path.as_os_str().as_bytes(); + let last_non_slash = bytes.iter().rposition(|&b| b != b'/')?; + let trimmed = &bytes[..=last_non_slash]; + let parent_end = trimmed + .iter() + .rposition(|&b| b == b'/') + .map_or(0, |slash_pos| { + bytes[..slash_pos] + .iter() + .rposition(|&b| b != b'/') + .map_or_else(|| usize::from(bytes.first() == Some(&b'/')), |p| p + 1) + }); + Some(Path::new(OsStr::from_bytes(&bytes[..parent_end]))) +} + /// Extension trait to add path normalization to std's [`Path`]. pub trait PathUtil { /// Normalize this path without performing I/O. @@ -144,6 +176,38 @@ async fn is_invalid_exports_target() { assert!(!Path::new("/").is_invalid_exports_target()); } +#[cfg(unix)] +#[tokio::test] +async fn path_parent_unix_matches_std() { + let cases = [ + "/foo/bar", + "/foo", + "/", + "", + "foo", + "foo/bar", + ".", + "..", + "./foo", + "../foo", + "/foo/", + "/foo/bar/", + "//foo/bar", + "foo//bar", + "/a/b/c/d/e", + "/", + "//", + ]; + for case in cases { + let p = Path::new(case); + assert_eq!( + path_parent_unix(p), + p.parent(), + "case={case:?} diverged from std::Path::parent" + ); + } +} + #[tokio::test] async fn normalize() { assert_eq!(Path::new("/foo/.././foo/").normalize(), Path::new("/foo")); From 8e6285b0c76825129c5aad61695e2b4ef2442611 Mon Sep 17 00:00:00 2001 From: pshu Date: Fri, 22 May 2026 03:34:40 +0800 Subject: [PATCH 6/7] perf(cache): skip normalize_with allocation when realpath chain has no symlinks In CachedPathImpl::realpath, when the parent's canonical path matches the parent's stored path byte-for-byte, no symlinks were found anywhere up the chain. Cache None in that case instead of building Some(normalize_with(...)). The outer wrapper already falls back to self.path on None, so behavior is identical for the common (no-symlinks) input shape while skipping one PathBuf allocation per cached path on first realpath. Bench (callgrind / CodSpeed CPU simulation formula, macOS+linux/arm64): resolver/single-thread: accesses: 139,041,327 -> 138,958,385 (-0.06% step; -3.32% vs baseline) estimated_cycles: 171,064,577 -> 170,887,380 (-0.10% step; -3.17% vs baseline) --- src/cache.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/cache.rs b/src/cache.rs index b6922143..0bedf101 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -254,6 +254,22 @@ impl CachedPathImpl { } if let Some(parent) = self.parent() { let parent_path = parent.realpath(fs).await?; + // Fast path: when no symlinks were found anywhere up the chain + // (the common case in resolver inputs), `parent_path` is the + // same bytes as `parent.path`. Cache `None` instead of an + // owned copy of `self.path` so the get_or_try_init body + // sidesteps the `normalize_with` allocation and the outer + // `unwrap_or_else` falls through to `self.path` directly. + #[cfg(unix)] + let parent_unchanged = { + use std::os::unix::ffi::OsStrExt; + parent_path.as_os_str().as_bytes() == parent.path.as_os_str().as_bytes() + }; + #[cfg(not(unix))] + let parent_unchanged = parent_path.as_path() == &*parent.path; + if parent_unchanged { + return Ok(None); + } return Ok(Some( parent_path.normalize_with(self.path.strip_prefix(&parent.path).unwrap()), )); From 0c51d29aa8e92c536c0d004c77069d06fc11ce9e Mon Sep 17 00:00:00 2001 From: pshu Date: Fri, 22 May 2026 07:49:18 +0800 Subject: [PATCH 7/7] perf(fs): use sync std::fs in FileSystemOs for metadata/read calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FileSystemOs's async trait methods previously called tokio::fs::*, which internally spawn_blocking + acquire a semaphore + park/unpark per syscall. The bench shows this scheduling layer costs ~20M Ir per single-thread iteration on tokio runtime internals alone — dwarfing the actual stat/read work. Switch to sync std::fs::{metadata, symlink_metadata, read, read_to_string} inside the async fn body. The trait signature is unchanged, callers still await normally, and canonicalize was already sync (dunce::canonicalize). Tradeoff: each fs call now blocks the runtime thread for the duration of the syscall (microseconds). Multi-thread tokio users will lose some concurrency overlap relative to the spawn_blocking model, but the wins on per-call overhead are large enough that swc/oxc/ripgrep all make the same tradeoff. wasm target keeps its existing std::fs path. Bench (callgrind / CodSpeed CPU simulation formula, macOS+linux/arm64): resolver/single-thread: accesses: 138,958,385 -> 100,380,124 (-27.76% step; -30.16% vs baseline) estimated_cycles: 170,887,380 -> 124,181,874 (-27.34% step; -29.64% vs baseline) resolver/[single-threaded]resolve with many extensions: accesses: ~316M -> 216,209,862 (~-31% step; -33.63% vs baseline) --- src/file_system.rs | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/file_system.rs b/src/file_system.rs index feaeaa92..2169d46f 100644 --- a/src/file_system.rs +++ b/src/file_system.rs @@ -147,19 +147,26 @@ impl FileSystemOs { #[cfg(not(target_arch = "wasm32"))] #[async_trait::async_trait] impl FileSystem for FileSystemOs { + // The bulk of resolver work is filesystem stat/read operations whose syscall + // cost is microseconds. Calling them through `tokio::fs` forces a + // `spawn_blocking` + semaphore acquire + park/unpark per call, which the + // single-thread bench shows costs ~20M Ir per iteration in tokio runtime + // overhead alone. Use sync std::fs here; the trait still exposes an `async fn` + // so callers can await as before. Other Rust resolvers (swc, oxc) make the + // same tradeoff for the same reason. async fn read(&self, path: &Path) -> io::Result> { cfg_if! { if #[cfg(feature = "yarn_pnp")] { if self.options.enable_pnp { return match VPath::from(path)? { VPath::Zip(info) => self.pnp_lru.read(info.physical_base_path(), info.zip_path), - VPath::Virtual(info) => tokio::fs::read(info.physical_base_path()).await, - VPath::Native(path) => tokio::fs::read(&path).await, + VPath::Virtual(info) => fs::read(info.physical_base_path()), + VPath::Native(path) => fs::read(&path), } } }} - tokio::fs::read(path).await + fs::read(path) } async fn read_to_string(&self, path: &Path) -> io::Result { @@ -168,13 +175,13 @@ impl FileSystem for FileSystemOs { if self.options.enable_pnp { return match VPath::from(path)? { VPath::Zip(info) => self.pnp_lru.read_to_string(info.physical_base_path(), info.zip_path), - VPath::Virtual(info) => tokio::fs::read_to_string(info.physical_base_path()).await, - VPath::Native(path) => tokio::fs::read_to_string(&path).await, + VPath::Virtual(info) => fs::read_to_string(info.physical_base_path()), + VPath::Native(path) => fs::read_to_string(&path), } } } } - tokio::fs::read_to_string(path).await + fs::read_to_string(path) } async fn metadata(&self, path: &Path) -> io::Result { @@ -186,24 +193,18 @@ impl FileSystem for FileSystemOs { .pnp_lru .file_type(info.physical_base_path(), info.zip_path) .map(FileMetadata::from), - VPath::Virtual(info) => { - tokio::fs::metadata(info.physical_base_path()) - .await - .map(FileMetadata::from) - } - VPath::Native(path) => tokio::fs::metadata(path).await.map(FileMetadata::from), + VPath::Virtual(info) => fs::metadata(info.physical_base_path()).map(FileMetadata::from), + VPath::Native(path) => fs::metadata(path).map(FileMetadata::from), } } } } - tokio::fs::metadata(path).await.map(FileMetadata::from) + fs::metadata(path).map(FileMetadata::from) } async fn symlink_metadata(&self, path: &Path) -> io::Result { - tokio::fs::symlink_metadata(path) - .await - .map(FileMetadata::from) + fs::symlink_metadata(path).map(FileMetadata::from) } async fn canonicalize(&self, path: &Path) -> io::Result {