Skip to content

Commit 18e15d4

Browse files
BrooooooklynCodex
andauthored
perf: bypass file system read cache if memory cache is available (#707)
This is for reading pacakge.json and tsconfig.json. ``` resolver/single-thread time: [70.355 µs 70.908 µs 71.507 µs] change: [−10.363% −7.0636% −3.8633%] (p = 0.00 < 0.05) Performance has improved. Found 5 outliers among 100 measurements (5.00%) 2 (2.00%) high mild 3 (3.00%) high severe resolver/multi-thread time: [59.697 µs 61.750 µs 64.767 µs] change: [−15.418% −7.3458% +1.2883%] (p = 0.11 > 0.05) No change in performance detected. Found 3 outliers among 100 measurements (3.00%) 3 (3.00%) high severe resolver/resolve from symlinks time: [25.561 ms 25.734 ms 25.910 ms] change: [−19.181% −17.957% −16.794%] (p = 0.00 < 0.05) Performance has improved. Found 1 outliers among 100 measurements (1.00%) 1 (1.00%) high mild ``` --------- Co-authored-by: Codex <[email protected]>
1 parent 34959e2 commit 18e15d4

File tree

4 files changed

+80
-5
lines changed

4 files changed

+80
-5
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ pnp = { version = "0.12.2", optional = true }
9494

9595
document-features = { version = "0.2.11", optional = true }
9696

97+
[target.'cfg(any(target_os = "macos", target_os = "linux"))'.dependencies]
98+
libc = "0.2"
99+
97100
[target.'cfg(target_os = "windows")'.dependencies]
98101
windows = { version = "0.62.0", features = ["Win32_Storage_FileSystem"] }
99102

src/cache.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,12 @@ impl<Fs: FileSystem> Cache<Fs> {
119119
.package_json
120120
.get_or_try_init(|| {
121121
let package_json_path = path.path.join("package.json");
122-
let Ok(package_json_string) = self.fs.read_to_string(&package_json_path) else {
122+
let Ok(package_json_string) =
123+
self.fs.read_to_string_bypass_system_cache(&package_json_path)
124+
else {
123125
return Ok(None);
124126
};
127+
125128
let real_path = if options.symlinks {
126129
self.canonicalize(path)?.join("package.json")
127130
} else {
@@ -174,7 +177,7 @@ impl<Fs: FileSystem> Cache<Fs> {
174177
};
175178
let mut tsconfig_string = self
176179
.fs
177-
.read_to_string(&tsconfig_path)
180+
.read_to_string_bypass_system_cache(&tsconfig_path)
178181
.map_err(|_| ResolveError::TsconfigNotFound(path.to_path_buf()))?;
179182
let mut tsconfig =
180183
TsConfig::parse(root, &tsconfig_path, &mut tsconfig_string).map_err(|error| {

src/file_system.rs

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@ pub trait FileSystem: Send + Sync {
2929
/// napi env.
3030
fn read_to_string(&self, path: &Path) -> io::Result<String>;
3131

32+
/// Reads a file while bypassing the system cache.
33+
///
34+
/// This is useful in scenarios where the file content is already cached in memory
35+
/// and you want to avoid the overhead of using the system cache.
36+
///
37+
/// # Errors
38+
///
39+
/// * See [std::fs::read_to_string]
40+
fn read_to_string_bypass_system_cache(&self, path: &Path) -> io::Result<String> {
41+
self.read_to_string(path)
42+
}
43+
3244
/// See [std::fs::metadata]
3345
///
3446
/// # Errors
@@ -126,10 +138,10 @@ pub struct FileSystemOs {
126138
impl FileSystemOs {
127139
/// # Errors
128140
///
129-
/// See [std::fs::read_to_string]
130-
pub fn read_to_string(path: &Path) -> io::Result<String> {
141+
/// See [std::io::ErrorKind::InvalidData]
142+
#[inline]
143+
pub fn validate_string(bytes: Vec<u8>) -> io::Result<String> {
131144
// `simdutf8` is faster than `std::str::from_utf8` which `fs::read_to_string` uses internally
132-
let bytes = std::fs::read(path)?;
133145
if simdutf8::basic::from_utf8(&bytes).is_err() {
134146
// Same error as `fs::read_to_string` produces (`io::Error::INVALID_UTF8`)
135147
return Err(io::Error::new(
@@ -141,6 +153,14 @@ impl FileSystemOs {
141153
Ok(unsafe { String::from_utf8_unchecked(bytes) })
142154
}
143155

156+
/// # Errors
157+
///
158+
/// See [std::fs::read_to_string]
159+
pub fn read_to_string(path: &Path) -> io::Result<String> {
160+
let bytes = std::fs::read(path)?;
161+
Self::validate_string(bytes)
162+
}
163+
144164
/// # Errors
145165
///
146166
/// See [std::fs::metadata]
@@ -219,6 +239,54 @@ impl FileSystem for FileSystemOs {
219239
Self::read_to_string(path)
220240
}
221241

242+
fn read_to_string_bypass_system_cache(&self, path: &Path) -> io::Result<String> {
243+
cfg_if! {
244+
if #[cfg(feature = "yarn_pnp")] {
245+
if self.yarn_pnp {
246+
return match VPath::from(path)? {
247+
VPath::Zip(info) => {
248+
self.pnp_lru.read_to_string(info.physical_base_path(), info.zip_path)
249+
}
250+
VPath::Virtual(info) => Self::read_to_string(&info.physical_base_path()),
251+
VPath::Native(path) => Self::read_to_string(&path),
252+
}
253+
}
254+
}
255+
}
256+
#[cfg(target_os = "macos")]
257+
{
258+
use libc::F_NOCACHE;
259+
use std::{io::Read, os::unix::fs::OpenOptionsExt};
260+
let mut fd = fs::OpenOptions::new().read(true).custom_flags(F_NOCACHE).open(path)?;
261+
let meta = fd.metadata()?;
262+
#[allow(clippy::cast_possible_truncation)]
263+
let mut buffer = Vec::with_capacity(meta.len() as usize);
264+
fd.read_to_end(&mut buffer)?;
265+
Self::validate_string(buffer)
266+
}
267+
#[cfg(target_os = "linux")]
268+
{
269+
use std::{io::Read, os::fd::AsRawFd};
270+
// Avoid `O_DIRECT` on Linux: it requires page-aligned buffers and aligned offsets,
271+
// which is incompatible with a regular Vec-based read and many CI filesystems.
272+
let mut fd = fs::OpenOptions::new().read(true).open(path)?;
273+
// Best-effort hint to avoid polluting the page cache.
274+
// SAFETY: `fd` is valid and `posix_fadvise` is safe.
275+
let _ = unsafe { libc::posix_fadvise(fd.as_raw_fd(), 0, 0, libc::POSIX_FADV_DONTNEED) };
276+
let meta = fd.metadata();
277+
let mut buffer = meta.ok().map_or_else(Vec::new, |meta| {
278+
#[allow(clippy::cast_possible_truncation)]
279+
Vec::with_capacity(meta.len() as usize)
280+
});
281+
fd.read_to_end(&mut buffer)?;
282+
Self::validate_string(buffer)
283+
}
284+
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
285+
{
286+
Self::read_to_string(path)
287+
}
288+
}
289+
222290
fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
223291
cfg_if! {
224292
if #[cfg(feature = "yarn_pnp")] {

0 commit comments

Comments
 (0)