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

@ -19,7 +19,7 @@ use crate::{
apply_patch_set, compute_patch_set, AppliedPatchSet, InstanceContext, InstanceSnapshot,
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).
@ -155,31 +155,35 @@ fn prefetch_project_files(project: &Project, sync_scripts_only: bool) -> io::Res
files: HashMap::new(),
is_file: HashMap::new(),
children: HashMap::new(),
dir_init: HashMap::new(),
});
}
let walk_start = Instant::now();
let mut entries: Vec<walkdir::DirEntry> = Vec::new();
for root in &roots {
if !root.exists() {
continue;
}
for e in WalkDir::new(root).follow_links(true).into_iter().flatten() {
// Walk the entire project folder so that every file under it
// (nested project files, etc.) is covered by the is_file map.
// P1 returns NotFound for cache misses when prefetch is active.
if folder.exists() {
for e in WalkDir::new(folder)
.follow_links(true)
.into_iter()
.flatten()
{
entries.push(e);
}
}
// Also include the project file itself (read by project middleware).
if let Ok(meta) = std::fs::metadata(&project.file_location) {
if meta.is_file() {
entries.push(
WalkDir::new(&project.file_location)
.into_iter()
.next()
.unwrap()
.unwrap(),
);
// Also walk $path roots that may be OUTSIDE the project folder
// (e.g. $path: "../module"). These wouldn't be covered above.
for root in &roots {
if !root.exists() || root.starts_with(folder) {
continue;
}
for e in WalkDir::new(root).follow_links(true).into_iter().flatten() {
entries.push(e);
}
}
@ -215,8 +219,9 @@ fn prefetch_project_files(project: &Project, sync_scripts_only: bool) -> io::Res
read_elapsed,
);
let mut is_file_map: HashMap<std::path::PathBuf, bool> = HashMap::new();
let mut children_map: HashMap<std::path::PathBuf, Vec<std::path::PathBuf>> = 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::with_capacity(dir_count);
for entry in &entries {
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(),
);
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 {
files: file_data.into_iter().collect::<HashMap<_, _>>(),
is_file: is_file_map,
children: children_map,
dir_init: dir_init_map,
})
}
@ -273,12 +296,15 @@ impl ServeSession {
let prefetch_start = Instant::now();
match prefetch_project_files(&root_project, sync_scripts_only) {
Ok(cache) => {
let count = cache.files.len();
log::debug!(
"Prefetch total: {} files in {:.1?}",
cache.files.len(),
count,
prefetch_start.elapsed()
);
vfs.set_prefetch_cache(cache);
if !cache.is_file.is_empty() {
vfs.set_prefetch_cache(cache);
}
}
Err(err) => {
log::warn!("Prefetch failed, falling back to sequential reads: {err}");
@ -291,37 +317,45 @@ impl ServeSession {
let mut instance_context = InstanceContext::new();
instance_context.sync_scripts_only = sync_scripts_only;
let watch_start = Instant::now();
let mut path_roots = Vec::new();
collect_path_roots(
&root_project.tree,
root_project.folder_location(),
&mut path_roots,
);
let _ = vfs.watch(root_project.folder_location());
for root in &path_roots {
let _ = vfs.watch(root);
}
log::debug!(
"Set up {} recursive watches in {:.1?}",
path_roots.len() + 1,
watch_start.elapsed(),
);
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();
log::trace!("Computing initial patch set");
let patch_set = compute_patch_set(snapshot, &tree, root_id);
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 {
let _ = vfs.watch(root);
}
log::debug!(
"Set up {} recursive watches in {:.1?}",
path_roots.len() + 1,
watch_start.elapsed(),
);
});
log::trace!("Applying initial patch set");
apply_patch_set(&mut tree, patch_set);
log::debug!("Patch computed + applied in {:.1?}", patch_start.elapsed());
log::trace!("Computing initial patch set");
let patch_set = compute_patch_set(snapshot, &tree, root_id);
log::trace!("Applying initial patch set");
apply_patch_set(&mut tree, patch_set);
});
log::debug!("Watch + patch in {:.1?}", patch_start.elapsed());
Ok((root_project, tree))
}

View file

@ -274,9 +274,20 @@ fn compute_children_patches(
.get_instance(id)
.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);
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);
// Matched pairs: recursively compute patches.

View file

@ -30,7 +30,6 @@ use serde::{Deserialize, Serialize};
use crate::{
glob::Glob,
project::DEFAULT_PROJECT_NAMES,
syncback::{dedup_suffix::strip_dedup_suffix, SyncbackReturn, SyncbackSnapshot},
};
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`
/// files. This uses an intrinsic priority list and for compatibility,
/// 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()))?;
let dir_name = strip_dedup_suffix(dir_name);
static INIT_PATHS: OnceLock<Vec<(Middleware, &str)>> = OnceLock::new();
let order = INIT_PATHS.get_or_init(|| {
vec![
// Modern extensions (preferred)
(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"),
]
});
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));
}
if let Some(cached) = vfs.prefetch_dir_init(dir_path) {
return match cached {
Some((init_name, init_path)) => {
let middleware = INIT_FILE_PRIORITY
.iter()
.find(|(_, name)| *name == init_name)
.map(|(m, _)| *m)
.unwrap_or(Middleware::Dir);
Ok((middleware, dir_name, init_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);
if vfs.metadata(&test_path).with_not_found()?.is_some() {
return Ok((*middleware, dir_name, test_path));
return Ok((middleware, dir_name, test_path));
}
}