Enhance PrefetchCache with directory init-file resolution

- Added a new field `dir_init` to `PrefetchCache` to store pre-resolved init-file information for directories, allowing for efficient retrieval during prefetch operations.
- Introduced the `prefetch_dir_init` method in the `Vfs` struct to look up init-file info from the prefetch cache.
- Updated the `prefetch_project_files` function to populate the `dir_init` map based on the defined `INIT_FILE_PRIORITY`.
- Adjusted related tests to ensure proper initialization of the `dir_init` field in various scenarios.

These changes improve the performance of directory handling in the VFS by reducing the need for repeated metadata probes.
This commit is contained in:
Joe 2026-03-06 19:58:07 -06:00
parent f1992a61b1
commit 6168597acb
5 changed files with 159 additions and 66 deletions

View file

@ -44,6 +44,12 @@ pub struct PrefetchCache {
pub is_file: HashMap<PathBuf, bool>, pub is_file: HashMap<PathBuf, bool>,
/// Directory path -> sorted child paths. Consumed once per directory. /// Directory path -> sorted child paths. Consumed once per directory.
pub children: HashMap<PathBuf, Vec<PathBuf>>, pub children: HashMap<PathBuf, Vec<PathBuf>>,
/// Pre-resolved init-file info for each directory. The value is `None`
/// when the directory has no init file (plain `Folder`), or
/// `Some((init_file_name, init_path))` for the winning init entry.
/// Populated during prefetch so that `get_dir_middleware` can skip
/// per-file VFS metadata probes entirely.
pub dir_init: HashMap<PathBuf, Option<(String, PathBuf)>>,
} }
mod sealed { mod sealed {
@ -282,6 +288,10 @@ impl VfsInner {
if let Some(&is_file) = cache.is_file.get(path) { if let Some(&is_file) = cache.is_file.get(path) {
return Ok(Metadata { is_file }); return Ok(Metadata { is_file });
} }
return Err(io::Error::new(
io::ErrorKind::NotFound,
"not in prefetch cache",
));
} }
self.backend.metadata(path) self.backend.metadata(path)
@ -384,6 +394,17 @@ impl Vfs {
inner.prefetch_cache = None; inner.prefetch_cache = None;
} }
/// Look up pre-resolved init-file info for a directory from the
/// prefetch cache. Returns `Some(Some((name, path)))` when the
/// directory was found in the cache with an init file, `Some(None)`
/// when the directory was found but has no init file, and `None`
/// when there is no prefetch cache or the directory is not in it.
pub fn prefetch_dir_init(&self, dir: &Path) -> Option<Option<(String, PathBuf)>> {
let inner = self.inner.lock().unwrap();
let cache = inner.prefetch_cache.as_ref()?;
cache.dir_init.get(dir).cloned()
}
/// Manually lock the Vfs, useful for large batches of operations. /// Manually lock the Vfs, useful for large batches of operations.
pub fn lock(&self) -> VfsLock<'_> { pub fn lock(&self) -> VfsLock<'_> {
VfsLock { VfsLock {
@ -732,6 +753,7 @@ mod test {
.collect(), .collect(),
is_file: HashMap::new(), is_file: HashMap::new(),
children: HashMap::new(), children: HashMap::new(),
dir_init: HashMap::new(),
} }
} }
@ -868,6 +890,7 @@ mod test {
files: cache_files, files: cache_files,
is_file: HashMap::new(), is_file: HashMap::new(),
children: HashMap::new(), children: HashMap::new(),
dir_init: HashMap::new(),
}); });
let result = vfs.read(&file_path).unwrap(); let result = vfs.read(&file_path).unwrap();
@ -913,6 +936,7 @@ mod test {
files: cache_files, files: cache_files,
is_file: HashMap::new(), is_file: HashMap::new(),
children: HashMap::new(), children: HashMap::new(),
dir_init: HashMap::new(),
}); });
let handles: Vec<_> = (0..100) let handles: Vec<_> = (0..100)
@ -953,6 +977,7 @@ mod test {
files: cache_files, files: cache_files,
is_file: HashMap::new(), is_file: HashMap::new(),
children: HashMap::new(), children: HashMap::new(),
dir_init: HashMap::new(),
}); });
for i in 0..50 { for i in 0..50 {

View file

@ -19,7 +19,7 @@ use crate::{
apply_patch_set, compute_patch_set, AppliedPatchSet, InstanceContext, InstanceSnapshot, apply_patch_set, compute_patch_set, AppliedPatchSet, InstanceContext, InstanceSnapshot,
PatchSet, RojoTree, PatchSet, RojoTree,
}, },
snapshot_middleware::{is_script_relevant_path, snapshot_from_vfs}, snapshot_middleware::{is_script_relevant_path, snapshot_from_vfs, INIT_FILE_PRIORITY},
}; };
/// Set to `true` to validate on plugin connect (useful for testing, do not enable on production). /// Set to `true` to validate on plugin connect (useful for testing, do not enable on production).
@ -155,31 +155,35 @@ fn prefetch_project_files(project: &Project, sync_scripts_only: bool) -> io::Res
files: HashMap::new(), files: HashMap::new(),
is_file: HashMap::new(), is_file: HashMap::new(),
children: HashMap::new(), children: HashMap::new(),
dir_init: HashMap::new(),
}); });
} }
let walk_start = Instant::now(); let walk_start = Instant::now();
let mut entries: Vec<walkdir::DirEntry> = Vec::new(); let mut entries: Vec<walkdir::DirEntry> = Vec::new();
for root in &roots {
if !root.exists() { // Walk the entire project folder so that every file under it
continue; // (nested project files, etc.) is covered by the is_file map.
} // P1 returns NotFound for cache misses when prefetch is active.
for e in WalkDir::new(root).follow_links(true).into_iter().flatten() { if folder.exists() {
for e in WalkDir::new(folder)
.follow_links(true)
.into_iter()
.flatten()
{
entries.push(e); entries.push(e);
} }
} }
// Also include the project file itself (read by project middleware). // Also walk $path roots that may be OUTSIDE the project folder
if let Ok(meta) = std::fs::metadata(&project.file_location) { // (e.g. $path: "../module"). These wouldn't be covered above.
if meta.is_file() { for root in &roots {
entries.push( if !root.exists() || root.starts_with(folder) {
WalkDir::new(&project.file_location) continue;
.into_iter() }
.next() for e in WalkDir::new(root).follow_links(true).into_iter().flatten() {
.unwrap() entries.push(e);
.unwrap(),
);
} }
} }
@ -215,8 +219,9 @@ fn prefetch_project_files(project: &Project, sync_scripts_only: bool) -> io::Res
read_elapsed, read_elapsed,
); );
let mut is_file_map: HashMap<std::path::PathBuf, bool> = HashMap::new(); let mut is_file_map: HashMap<std::path::PathBuf, bool> = HashMap::with_capacity(entries.len());
let mut children_map: HashMap<std::path::PathBuf, Vec<std::path::PathBuf>> = HashMap::new(); let mut children_map: HashMap<std::path::PathBuf, Vec<std::path::PathBuf>> =
HashMap::with_capacity(dir_count);
for entry in &entries { for entry in &entries {
let path = entry.path().to_path_buf(); let path = entry.path().to_path_buf();
@ -242,10 +247,28 @@ fn prefetch_project_files(project: &Project, sync_scripts_only: bool) -> io::Res
children_map.len(), children_map.len(),
); );
let mut dir_init_map: HashMap<std::path::PathBuf, Option<(String, std::path::PathBuf)>> =
HashMap::with_capacity(children_map.len());
for (dir_path, child_paths) in &children_map {
let child_names: std::collections::HashSet<&str> = child_paths
.iter()
.filter_map(|p| p.file_name().and_then(|n| n.to_str()))
.collect();
let mut found = None;
for &(_, init_name) in INIT_FILE_PRIORITY {
if child_names.contains(init_name) {
found = Some((init_name.to_string(), dir_path.join(init_name)));
break;
}
}
dir_init_map.insert(dir_path.clone(), found);
}
Ok(PrefetchCache { Ok(PrefetchCache {
files: file_data.into_iter().collect::<HashMap<_, _>>(), files: file_data.into_iter().collect::<HashMap<_, _>>(),
is_file: is_file_map, is_file: is_file_map,
children: children_map, children: children_map,
dir_init: dir_init_map,
}) })
} }
@ -273,13 +296,16 @@ impl ServeSession {
let prefetch_start = Instant::now(); let prefetch_start = Instant::now();
match prefetch_project_files(&root_project, sync_scripts_only) { match prefetch_project_files(&root_project, sync_scripts_only) {
Ok(cache) => { Ok(cache) => {
let count = cache.files.len();
log::debug!( log::debug!(
"Prefetch total: {} files in {:.1?}", "Prefetch total: {} files in {:.1?}",
cache.files.len(), count,
prefetch_start.elapsed() prefetch_start.elapsed()
); );
if !cache.is_file.is_empty() {
vfs.set_prefetch_cache(cache); vfs.set_prefetch_cache(cache);
} }
}
Err(err) => { Err(err) => {
log::warn!("Prefetch failed, falling back to sequential reads: {err}"); log::warn!("Prefetch failed, falling back to sequential reads: {err}");
} }
@ -291,14 +317,28 @@ impl ServeSession {
let mut instance_context = InstanceContext::new(); let mut instance_context = InstanceContext::new();
instance_context.sync_scripts_only = sync_scripts_only; instance_context.sync_scripts_only = sync_scripts_only;
let watch_start = Instant::now();
let mut path_roots = Vec::new(); let mut path_roots = Vec::new();
collect_path_roots( collect_path_roots(
&root_project.tree, &root_project.tree,
root_project.folder_location(), root_project.folder_location(),
&mut path_roots, &mut path_roots,
); );
let _ = vfs.watch(root_project.folder_location());
let snap_start = Instant::now();
log::trace!("Generating snapshot of instances from VFS");
vfs.set_watch_enabled(false);
let snapshot = snapshot_from_vfs(&instance_context, vfs, start_path)?;
vfs.set_watch_enabled(true);
log::debug!("Snapshot built in {:.1?}", snap_start.elapsed());
vfs.clear_prefetch_cache();
let patch_start = Instant::now();
let project_folder = root_project.folder_location().to_path_buf();
std::thread::scope(|s| {
s.spawn(|| {
let watch_start = Instant::now();
let _ = vfs.watch(&project_folder);
for root in &path_roots { for root in &path_roots {
let _ = vfs.watch(root); let _ = vfs.watch(root);
} }
@ -307,21 +347,15 @@ impl ServeSession {
path_roots.len() + 1, path_roots.len() + 1,
watch_start.elapsed(), watch_start.elapsed(),
); );
});
let snap_start = Instant::now();
log::trace!("Generating snapshot of instances from VFS");
let snapshot = snapshot_from_vfs(&instance_context, vfs, start_path)?;
log::debug!("Snapshot built in {:.1?}", snap_start.elapsed());
vfs.clear_prefetch_cache();
let patch_start = Instant::now();
log::trace!("Computing initial patch set"); log::trace!("Computing initial patch set");
let patch_set = compute_patch_set(snapshot, &tree, root_id); let patch_set = compute_patch_set(snapshot, &tree, root_id);
log::trace!("Applying initial patch set"); log::trace!("Applying initial patch set");
apply_patch_set(&mut tree, patch_set); apply_patch_set(&mut tree, patch_set);
log::debug!("Patch computed + applied in {:.1?}", patch_start.elapsed()); });
log::debug!("Watch + patch in {:.1?}", patch_start.elapsed());
Ok((root_project, tree)) Ok((root_project, tree))
} }

View file

@ -274,9 +274,20 @@ fn compute_children_patches(
.get_instance(id) .get_instance(id)
.expect("Instance did not exist in tree"); .expect("Instance did not exist in tree");
let instance_children = instance.children().to_vec(); let instance_children = instance.children();
let snapshot_children = take(&mut snapshot.children); let snapshot_children = take(&mut snapshot.children);
if instance_children.is_empty() {
for snapshot_child in snapshot_children {
patch_set.added_instances.push(PatchAdd {
parent_id: id,
instance: snapshot_child,
});
}
return;
}
let instance_children = instance_children.to_vec();
let match_result = match_forward(snapshot_children, &instance_children, tree, session); let match_result = match_forward(snapshot_children, &instance_children, tree, session);
// Matched pairs: recursively compute patches. // Matched pairs: recursively compute patches.

View file

@ -30,7 +30,6 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
glob::Glob, glob::Glob,
project::DEFAULT_PROJECT_NAMES,
syncback::{dedup_suffix::strip_dedup_suffix, SyncbackReturn, SyncbackSnapshot}, syncback::{dedup_suffix::strip_dedup_suffix, SyncbackReturn, SyncbackSnapshot},
}; };
use crate::{ use crate::{
@ -110,6 +109,27 @@ pub fn snapshot_from_vfs(
} }
} }
/// Single source of truth for init-file resolution priority.
/// Project files are checked first, then init files in this order.
/// Used by both `get_dir_middleware` (live) and `prefetch_project_files` (cache).
pub static INIT_FILE_PRIORITY: &[(Middleware, &str)] = &[
(Middleware::Project, "default.project.json5"),
(Middleware::Project, "default.project.json"),
(Middleware::ModuleScriptDir, "init.luau"),
(Middleware::ServerScriptDir, "init.server.luau"),
(Middleware::ClientScriptDir, "init.client.luau"),
(Middleware::PluginScriptDir, "init.plugin.luau"),
(Middleware::LocalScriptDir, "init.local.luau"),
(Middleware::LegacyScriptDir, "init.legacy.luau"),
(Middleware::CsvDir, "init.csv"),
// Legacy extensions (for backwards compatibility)
// init.server.lua → Script with RunContext.Legacy (old emitLegacyScripts behavior)
// init.client.lua → LocalScript (old emitLegacyScripts behavior)
(Middleware::ModuleScriptDir, "init.lua"),
(Middleware::LegacyScriptDir, "init.server.lua"),
(Middleware::LocalScriptDir, "init.client.lua"),
];
/// Gets the appropriate middleware for a directory by checking for `init` /// Gets the appropriate middleware for a directory by checking for `init`
/// files. This uses an intrinsic priority list and for compatibility, /// files. This uses an intrinsic priority list and for compatibility,
/// that order should be left unchanged. /// that order should be left unchanged.
@ -127,37 +147,24 @@ fn get_dir_middleware<'path>(
.ok_or_else(|| anyhow::anyhow!("File name was not valid UTF-8: {}", dir_path.display()))?; .ok_or_else(|| anyhow::anyhow!("File name was not valid UTF-8: {}", dir_path.display()))?;
let dir_name = strip_dedup_suffix(dir_name); let dir_name = strip_dedup_suffix(dir_name);
static INIT_PATHS: OnceLock<Vec<(Middleware, &str)>> = OnceLock::new(); if let Some(cached) = vfs.prefetch_dir_init(dir_path) {
let order = INIT_PATHS.get_or_init(|| { return match cached {
vec![ Some((init_name, init_path)) => {
// Modern extensions (preferred) let middleware = INIT_FILE_PRIORITY
(Middleware::ModuleScriptDir, "init.luau"), .iter()
(Middleware::ServerScriptDir, "init.server.luau"), .find(|(_, name)| *name == init_name)
(Middleware::ClientScriptDir, "init.client.luau"), .map(|(m, _)| *m)
(Middleware::PluginScriptDir, "init.plugin.luau"), .unwrap_or(Middleware::Dir);
(Middleware::LocalScriptDir, "init.local.luau"), Ok((middleware, dir_name, init_path))
(Middleware::LegacyScriptDir, "init.legacy.luau"),
(Middleware::CsvDir, "init.csv"),
// Legacy extensions (for backwards compatibility)
// init.server.lua → Script with RunContext.Legacy (old emitLegacyScripts behavior)
// init.client.lua → LocalScript (old emitLegacyScripts behavior)
(Middleware::ModuleScriptDir, "init.lua"),
(Middleware::LegacyScriptDir, "init.server.lua"),
(Middleware::LocalScriptDir, "init.client.lua"),
]
});
for default_project_name in DEFAULT_PROJECT_NAMES {
let project_path = dir_path.join(default_project_name);
if vfs.metadata(&project_path).with_not_found()?.is_some() {
return Ok((Middleware::Project, dir_name, project_path));
} }
None => Ok((Middleware::Dir, dir_name, dir_path.to_path_buf())),
};
} }
for (middleware, name) in order { for &(middleware, name) in INIT_FILE_PRIORITY {
let test_path = dir_path.join(name); let test_path = dir_path.join(name);
if vfs.metadata(&test_path).with_not_found()?.is_some() { if vfs.metadata(&test_path).with_not_found()?.is_some() {
return Ok((*middleware, dir_name, test_path)); return Ok((middleware, dir_name, test_path));
} }
} }

View file

@ -241,19 +241,34 @@ fn parallel_snapshot_with_prefetch_cache() {
use std::collections::HashMap; use std::collections::HashMap;
let mut files = HashMap::new(); let mut files = HashMap::new();
let mut is_file = HashMap::new();
let mut children_map: HashMap<std::path::PathBuf, Vec<std::path::PathBuf>> = HashMap::new();
for entry in walkdir::WalkDir::new(root).follow_links(true) { for entry in walkdir::WalkDir::new(root).follow_links(true) {
let entry = entry.unwrap(); let entry = entry.unwrap();
let path = entry.path().to_path_buf(); let path = entry.path().to_path_buf();
is_file.insert(path.clone(), entry.file_type().is_file());
if entry.file_type().is_file() { if entry.file_type().is_file() {
files.insert(path.clone(), fs::read(&path).unwrap()); files.insert(path.clone(), fs::read(&path).unwrap());
} }
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()));
} }
let vfs_cached = memofs::Vfs::new_default(); let vfs_cached = memofs::Vfs::new_default();
vfs_cached.set_prefetch_cache(memofs::PrefetchCache { vfs_cached.set_prefetch_cache(memofs::PrefetchCache {
files, files,
is_file: std::collections::HashMap::new(), is_file,
children: std::collections::HashMap::new(), children: children_map,
dir_init: 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)
@ -359,6 +374,7 @@ fn overlapping_path_roots_no_duplicate_children() {
files, files,
is_file, is_file,
children: children_map, children: children_map,
dir_init: std::collections::HashMap::new(),
}); });
let snap = librojo::snapshot_from_vfs(&ctx, &vfs, &project_path) let snap = librojo::snapshot_from_vfs(&ctx, &vfs, &project_path)