Enhance PrefetchCache and improve directory handling in VFS

- Added a `children` field to the `PrefetchCache` structure to store sorted child paths for directories, optimizing directory read operations.
- Updated the `read_dir` method in `VfsInner` to utilize the new `children` cache, improving performance by reducing backend calls for cached directories.
- Refactored the `prefetch_project_files` function to populate the `children` map during prefetching, enhancing the efficiency of path collection.
- Adjusted tests to accommodate the new `children` field in `PrefetchCache`, ensuring comprehensive coverage of the updated functionality.

These changes aim to improve the performance and reliability of the VFS by refining the prefetching mechanism and enhancing directory handling.
This commit is contained in:
Joe 2026-02-19 02:39:23 -06:00
parent 5a943c33ea
commit 3ccaaa63db
4 changed files with 54 additions and 5 deletions

View file

@ -43,6 +43,8 @@ pub struct PrefetchCache {
/// `true` = file, `false` = directory. Paths not in the map fall through /// `true` = file, `false` = directory. Paths not in the map fall through
/// to the backend (e.g. init-file probes for paths that don't exist). /// to the backend (e.g. init-file probes for paths that don't exist).
pub is_file: HashMap<PathBuf, bool>, pub is_file: HashMap<PathBuf, bool>,
/// Directory path -> sorted child paths. Consumed once per directory.
pub children: HashMap<PathBuf, Vec<PathBuf>>,
} }
mod sealed { mod sealed {
@ -216,6 +218,19 @@ impl VfsInner {
fn read_dir<P: AsRef<Path>>(&mut self, path: P) -> io::Result<ReadDir> { fn read_dir<P: AsRef<Path>>(&mut self, path: P) -> io::Result<ReadDir> {
let path = path.as_ref(); let path = path.as_ref();
if let Some(cache) = &mut self.prefetch_cache {
if let Some(child_paths) = cache.children.remove(path) {
if self.watch_enabled {
self.backend.watch(path)?;
}
let inner = child_paths.into_iter().map(|p| Ok(DirEntry { path: p }));
return Ok(ReadDir {
inner: Box::new(inner),
});
}
}
let dir = self.backend.read_dir(path)?; let dir = self.backend.read_dir(path)?;
if self.watch_enabled { if self.watch_enabled {
@ -765,6 +780,7 @@ mod test {
.map(|(k, v)| (PathBuf::from(k), PathBuf::from(v))) .map(|(k, v)| (PathBuf::from(k), PathBuf::from(v)))
.collect(), .collect(),
is_file: HashMap::new(), is_file: HashMap::new(),
children: HashMap::new(),
} }
} }
@ -883,6 +899,7 @@ mod test {
files: cache_files, files: cache_files,
canonical: HashMap::new(), canonical: HashMap::new(),
is_file: HashMap::new(), is_file: HashMap::new(),
children: HashMap::new(),
}); });
let result = vfs.read(&file_path).unwrap(); let result = vfs.read(&file_path).unwrap();
@ -954,6 +971,7 @@ mod test {
files: cache_files, files: cache_files,
canonical: HashMap::new(), canonical: HashMap::new(),
is_file: HashMap::new(), is_file: HashMap::new(),
children: HashMap::new(),
}); });
let handles: Vec<_> = (0..100) let handles: Vec<_> = (0..100)
@ -998,6 +1016,7 @@ mod test {
files: cache_files, files: cache_files,
canonical, canonical,
is_file: HashMap::new(), is_file: HashMap::new(),
children: HashMap::new(),
}); });
for i in 0..50 { for i in 0..50 {

View file

@ -82,6 +82,11 @@ impl StdBackend {
); );
false // keep watcher running false // keep watcher running
} }
WatcherCriticalError::ChannelSendFailed(_) => {
// The event receiver was dropped (VFS shutdown). This is
// normal during process exit -- stop the thread silently.
true
}
_ => { _ => {
log::error!("{}. File watching is no longer reliable.", err); log::error!("{}. File watching is no longer reliable.", err);
std::process::exit(1); std::process::exit(1);

View file

@ -142,6 +142,7 @@ fn prefetch_project_files(project: &Project) -> io::Result<PrefetchCache> {
files: HashMap::new(), files: HashMap::new(),
canonical: HashMap::new(), canonical: HashMap::new(),
is_file: HashMap::new(), is_file: HashMap::new(),
children: HashMap::new(),
}); });
} }
@ -214,15 +215,38 @@ fn prefetch_project_files(project: &Project) -> io::Result<PrefetchCache> {
canon_elapsed, canon_elapsed,
); );
let is_file: HashMap<_, _> = entries let mut is_file_map: HashMap<std::path::PathBuf, bool> = HashMap::new();
.iter() let mut children_map: HashMap<std::path::PathBuf, Vec<std::path::PathBuf>> = HashMap::new();
.map(|e| (e.path().to_path_buf(), e.file_type().is_file()))
.collect(); for entry in &entries {
let path = entry.path().to_path_buf();
is_file_map.insert(path.clone(), entry.file_type().is_file());
if entry.depth() > 0 {
if let Some(parent) = entry.path().parent() {
children_map
.entry(parent.to_path_buf())
.or_default()
.push(path);
}
}
}
for children in children_map.values_mut() {
children.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
}
log::info!(
"Prefetch maps: {} is_file + {} children dirs built",
is_file_map.len(),
children_map.len(),
);
Ok(PrefetchCache { Ok(PrefetchCache {
files: file_data.into_iter().collect::<HashMap<_, _>>(), files: file_data.into_iter().collect::<HashMap<_, _>>(),
canonical: canonical_data.into_iter().collect::<HashMap<_, _>>(), canonical: canonical_data.into_iter().collect::<HashMap<_, _>>(),
is_file, is_file: is_file_map,
children: children_map,
}) })
} }

View file

@ -258,6 +258,7 @@ fn parallel_snapshot_with_prefetch_cache() {
files, files,
canonical, canonical,
is_file: std::collections::HashMap::new(), is_file: std::collections::HashMap::new(),
children: std::collections::HashMap::new(),
}); });
let snap_cached = librojo::snapshot_from_vfs(&ctx, &vfs_cached, &project_path) let snap_cached = librojo::snapshot_from_vfs(&ctx, &vfs_cached, &project_path)