mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
Enhance VFS watch functionality and introduce recording mode
- Updated the `watch` method in `VfsBackend` to accept a `recursive` parameter, allowing for more flexible directory watching. - Implemented a recording mode in the `Vfs` struct to track accessed paths without immediately watching them, improving performance during read operations. - Added tests to verify the behavior of the new recording mode and ensure correct path tracking. - Enhanced the `StdBackend` and `NoopBackend` implementations to support the updated watch functionality. These changes aim to improve the VFS's ability to manage file system events and enhance user control over watching behavior.
This commit is contained in:
parent
bfb5815f5e
commit
2ea1d49316
8 changed files with 486 additions and 56 deletions
|
|
@ -241,7 +241,7 @@ impl VfsBackend for InMemoryFs {
|
|||
inner.event_receiver.clone()
|
||||
}
|
||||
|
||||
fn watch(&mut self, _path: &Path) -> io::Result<()> {
|
||||
fn watch(&mut self, _path: &Path, _recursive: bool) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ mod noop_backend;
|
|||
mod snapshot;
|
||||
mod std_backend;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
use std::{io, str};
|
||||
|
|
@ -94,7 +94,7 @@ pub trait VfsBackend: sealed::Sealed + Send + 'static {
|
|||
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()>;
|
||||
|
||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent>;
|
||||
fn watch(&mut self, path: &Path) -> io::Result<()>;
|
||||
fn watch(&mut self, path: &Path, recursive: bool) -> io::Result<()>;
|
||||
fn unwatch(&mut self, path: &Path) -> io::Result<()>;
|
||||
}
|
||||
|
||||
|
|
@ -159,17 +159,28 @@ pub enum VfsEvent {
|
|||
struct VfsInner {
|
||||
backend: Box<dyn VfsBackend>,
|
||||
watch_enabled: bool,
|
||||
watch_recursive: bool,
|
||||
prefetch_cache: Option<PrefetchCache>,
|
||||
recorded_watch_paths: Option<HashSet<PathBuf>>,
|
||||
}
|
||||
|
||||
impl VfsInner {
|
||||
fn watch_or_record(&mut self, path: &Path) -> io::Result<()> {
|
||||
if let Some(ref mut recorded) = self.recorded_watch_paths {
|
||||
recorded.insert(path.to_path_buf());
|
||||
Ok(())
|
||||
} else {
|
||||
self.backend.watch(path, self.watch_recursive)
|
||||
}
|
||||
}
|
||||
|
||||
/// Read raw bytes from the prefetch cache or the backend.
|
||||
/// Removes the entry from the cache on hit to free memory.
|
||||
fn read_raw(&mut self, path: &Path) -> io::Result<Vec<u8>> {
|
||||
if let Some(cache) = &mut self.prefetch_cache {
|
||||
if let Some(contents) = cache.files.remove(path) {
|
||||
if self.watch_enabled {
|
||||
self.backend.watch(path)?;
|
||||
self.watch_or_record(path)?;
|
||||
}
|
||||
return Ok(contents);
|
||||
}
|
||||
|
|
@ -178,7 +189,7 @@ impl VfsInner {
|
|||
let contents = self.backend.read(path)?;
|
||||
|
||||
if self.watch_enabled {
|
||||
self.backend.watch(path)?;
|
||||
self.watch_or_record(path)?;
|
||||
}
|
||||
|
||||
Ok(contents)
|
||||
|
|
@ -220,7 +231,7 @@ impl VfsInner {
|
|||
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)?;
|
||||
self.watch_or_record(path)?;
|
||||
}
|
||||
let inner = child_paths.into_iter().map(|p| Ok(DirEntry { path: p }));
|
||||
return Ok(ReadDir {
|
||||
|
|
@ -232,7 +243,7 @@ impl VfsInner {
|
|||
let dir = self.backend.read_dir(path)?;
|
||||
|
||||
if self.watch_enabled {
|
||||
self.backend.watch(path)?;
|
||||
self.watch_or_record(path)?;
|
||||
}
|
||||
|
||||
Ok(dir)
|
||||
|
|
@ -345,7 +356,9 @@ impl Vfs {
|
|||
let lock = VfsInner {
|
||||
backend: Box::new(backend),
|
||||
watch_enabled: true,
|
||||
watch_recursive: true,
|
||||
prefetch_cache: None,
|
||||
recorded_watch_paths: None,
|
||||
};
|
||||
|
||||
Self {
|
||||
|
|
@ -388,6 +401,36 @@ impl Vfs {
|
|||
inner.watch_enabled = enabled;
|
||||
}
|
||||
|
||||
/// Sets whether new watches use recursive or non-recursive mode.
|
||||
///
|
||||
/// When false, each `backend.watch()` call only watches the specific
|
||||
/// directory -- not its children. This allows selective watching where
|
||||
/// only directories actually traversed during snapshot get OS handles.
|
||||
pub fn set_watch_recursive(&self, recursive: bool) {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.watch_recursive = recursive;
|
||||
}
|
||||
|
||||
/// Begin recording mode: `read`/`read_dir` calls record touched paths
|
||||
/// into an internal set instead of calling `backend.watch()`.
|
||||
pub fn start_watch_recording(&self) {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.recorded_watch_paths = Some(HashSet::new());
|
||||
}
|
||||
|
||||
/// End recording mode and return the set of paths that were touched.
|
||||
pub fn take_recorded_paths(&self) -> Option<HashSet<PathBuf>> {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.recorded_watch_paths.take()
|
||||
}
|
||||
|
||||
/// Explicitly watch a path using the current `watch_recursive` setting.
|
||||
pub fn watch<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
let recursive = inner.watch_recursive;
|
||||
inner.backend.watch(path.as_ref(), recursive)
|
||||
}
|
||||
|
||||
/// Read a file from the VFS, or the underlying backend if it isn't
|
||||
/// resident.
|
||||
///
|
||||
|
|
@ -767,6 +810,50 @@ mod test {
|
|||
assert_eq!(vfs.read("test").unwrap().as_slice(), b"data");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recording_mode_collects_read_dir_paths() {
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
"/root",
|
||||
VfsSnapshot::dir(HashMap::from([
|
||||
("child.txt", VfsSnapshot::file("hello")),
|
||||
(
|
||||
"subdir",
|
||||
VfsSnapshot::dir(HashMap::from([("nested.txt", VfsSnapshot::file("world"))])),
|
||||
),
|
||||
])),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let vfs = Vfs::new(imfs);
|
||||
vfs.start_watch_recording();
|
||||
|
||||
let _ = vfs.read_dir("/root").unwrap();
|
||||
let _ = vfs.read_dir("/root/subdir").unwrap();
|
||||
let _ = vfs.read("/root/child.txt").unwrap();
|
||||
|
||||
let recorded = vfs.take_recorded_paths().unwrap();
|
||||
assert!(recorded.contains(&PathBuf::from("/root")));
|
||||
assert!(recorded.contains(&PathBuf::from("/root/subdir")));
|
||||
assert!(recorded.contains(&PathBuf::from("/root/child.txt")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recording_mode_returns_none_when_not_started() {
|
||||
let imfs = InMemoryFs::new();
|
||||
let vfs = Vfs::new(imfs);
|
||||
assert!(vfs.take_recorded_paths().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recording_mode_cleared_after_take() {
|
||||
let imfs = InMemoryFs::new();
|
||||
let vfs = Vfs::new(imfs);
|
||||
vfs.start_watch_recording();
|
||||
let _ = vfs.take_recorded_paths();
|
||||
assert!(vfs.take_recorded_paths().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefetch_cache_watch_registered_on_cache_hit() {
|
||||
let contents = "hello world".to_string();
|
||||
|
|
@ -779,7 +866,6 @@ mod test {
|
|||
cache_files.insert(file_path.clone(), contents.as_bytes().to_vec());
|
||||
vfs.set_prefetch_cache(PrefetchCache {
|
||||
files: cache_files,
|
||||
canonical: HashMap::new(),
|
||||
is_file: HashMap::new(),
|
||||
children: HashMap::new(),
|
||||
});
|
||||
|
|
@ -825,7 +911,6 @@ mod test {
|
|||
let vfs = Arc::new(Vfs::new(StdBackend::new_for_testing()));
|
||||
vfs.set_prefetch_cache(PrefetchCache {
|
||||
files: cache_files,
|
||||
canonical: HashMap::new(),
|
||||
is_file: HashMap::new(),
|
||||
children: HashMap::new(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ impl VfsBackend for NoopBackend {
|
|||
crossbeam_channel::never()
|
||||
}
|
||||
|
||||
fn watch(&mut self, _path: &Path) -> io::Result<()> {
|
||||
fn watch(&mut self, _path: &Path, _recursive: bool) -> io::Result<()> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ pub struct StdBackend {
|
|||
debouncer: Debouncer<notify::RecommendedWatcher, RecommendedCache>,
|
||||
watcher_receiver: Receiver<VfsEvent>,
|
||||
watches: HashSet<PathBuf>,
|
||||
recursive_watches: HashSet<PathBuf>,
|
||||
/// Receiver for critical errors from the watcher thread.
|
||||
/// Callers should poll this to detect when watching becomes unreliable.
|
||||
critical_error_receiver: Receiver<WatcherCriticalError>,
|
||||
|
|
@ -113,6 +114,7 @@ impl StdBackend {
|
|||
debouncer,
|
||||
watcher_receiver: event_rx,
|
||||
watches: HashSet::new(),
|
||||
recursive_watches: HashSet::new(),
|
||||
critical_error_receiver: error_rx,
|
||||
}
|
||||
}
|
||||
|
|
@ -328,26 +330,43 @@ impl VfsBackend for StdBackend {
|
|||
self.watcher_receiver.clone()
|
||||
}
|
||||
|
||||
fn watch(&mut self, path: &Path) -> io::Result<()> {
|
||||
if self.watches.contains(path)
|
||||
|| path
|
||||
.ancestors()
|
||||
.any(|ancestor| self.watches.contains(ancestor))
|
||||
fn watch(&mut self, path: &Path, recursive: bool) -> io::Result<()> {
|
||||
if self.watches.contains(path) {
|
||||
return Ok(());
|
||||
}
|
||||
if path
|
||||
.ancestors()
|
||||
.skip(1)
|
||||
.any(|ancestor| self.recursive_watches.contains(ancestor))
|
||||
{
|
||||
Ok(())
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mode = if recursive {
|
||||
RecursiveMode::Recursive
|
||||
} else {
|
||||
// Only add to watches AFTER the watch succeeds
|
||||
// This prevents a failed watch from permanently marking the path as "watched"
|
||||
match self.debouncer.watch(path, RecursiveMode::Recursive) {
|
||||
Ok(()) => {
|
||||
log::debug!("Watching path: {}", path.display());
|
||||
self.watches.insert(path.to_path_buf());
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("Failed to watch path {}: {:?}", path.display(), err);
|
||||
Err(io::Error::other(format!("{:?}", err)))
|
||||
RecursiveMode::NonRecursive
|
||||
};
|
||||
match self.debouncer.watch(path, mode) {
|
||||
Ok(()) => {
|
||||
log::debug!(
|
||||
"Watching path ({}): {}",
|
||||
if recursive {
|
||||
"recursive"
|
||||
} else {
|
||||
"non-recursive"
|
||||
},
|
||||
path.display()
|
||||
);
|
||||
self.watches.insert(path.to_path_buf());
|
||||
if recursive {
|
||||
self.recursive_watches.insert(path.to_path_buf());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("Failed to watch path {}: {:?}", path.display(), err);
|
||||
Err(io::Error::other(format!("{:?}", err)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -366,6 +385,7 @@ impl VfsBackend for StdBackend {
|
|||
);
|
||||
}
|
||||
self.watches.remove(path);
|
||||
self.recursive_watches.remove(path);
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
|
|
@ -382,6 +402,7 @@ impl VfsBackend for StdBackend {
|
|||
path.display()
|
||||
);
|
||||
self.watches.remove(path);
|
||||
self.recursive_watches.remove(path);
|
||||
Ok(())
|
||||
} else {
|
||||
log::warn!("Failed to unwatch path {}: {:?}", path.display(), err);
|
||||
|
|
@ -414,17 +435,17 @@ mod tests {
|
|||
let mut backend = StdBackend::new_for_testing();
|
||||
|
||||
// Watch should succeed and add to internal set
|
||||
assert!(backend.watch(&file_path).is_ok());
|
||||
assert!(backend.watch(&file_path, true).is_ok());
|
||||
|
||||
// Watching again should be a no-op (already watched)
|
||||
assert!(backend.watch(&file_path).is_ok());
|
||||
assert!(backend.watch(&file_path, true).is_ok());
|
||||
|
||||
// Watch a non-existent path - behavior varies by platform
|
||||
// On some systems, watching a non-existent file might succeed
|
||||
// because notify watches the parent directory. We just verify
|
||||
// that calling watch doesn't panic or corrupt state.
|
||||
let nonexistent = dir.path().join("nonexistent.txt");
|
||||
let _ = backend.watch(&nonexistent); // Result varies by platform
|
||||
let _ = backend.watch(&nonexistent, true); // Result varies by platform
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -451,13 +472,13 @@ mod tests {
|
|||
let mut backend = StdBackend::new_for_testing();
|
||||
|
||||
// Watch the file
|
||||
assert!(backend.watch(&file_path).is_ok());
|
||||
assert!(backend.watch(&file_path, true).is_ok());
|
||||
|
||||
// Unwatch should succeed
|
||||
assert!(backend.unwatch(&file_path).is_ok());
|
||||
|
||||
// Should be able to watch again after unwatch
|
||||
assert!(backend.watch(&file_path).is_ok());
|
||||
assert!(backend.watch(&file_path, true).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -471,10 +492,10 @@ mod tests {
|
|||
let mut backend = StdBackend::new_for_testing();
|
||||
|
||||
// Watch the parent directory
|
||||
assert!(backend.watch(&subdir).is_ok());
|
||||
assert!(backend.watch(&subdir, true).is_ok());
|
||||
|
||||
// Watching a file inside should be a no-op (covered by parent)
|
||||
assert!(backend.watch(&file_path).is_ok());
|
||||
assert!(backend.watch(&file_path, true).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -504,12 +525,12 @@ mod tests {
|
|||
|
||||
// Rapidly cycle watch/unwatch to stress test state consistency
|
||||
for _ in 0..10 {
|
||||
assert!(backend.watch(&file_path).is_ok());
|
||||
assert!(backend.watch(&file_path, true).is_ok());
|
||||
assert!(backend.unwatch(&file_path).is_ok());
|
||||
}
|
||||
|
||||
// Final watch should still work
|
||||
assert!(backend.watch(&file_path).is_ok());
|
||||
assert!(backend.watch(&file_path, true).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -522,7 +543,7 @@ mod tests {
|
|||
let event_rx = backend.event_receiver();
|
||||
|
||||
// Watch the directory (more reliable than watching the file directly)
|
||||
assert!(backend.watch(dir.path()).is_ok());
|
||||
assert!(backend.watch(dir.path(), true).is_ok());
|
||||
|
||||
// Give the watcher time to start
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
|
|
@ -597,7 +618,7 @@ mod tests {
|
|||
|
||||
let mut backend = StdBackend::new_for_testing();
|
||||
let event_rx = backend.event_receiver();
|
||||
assert!(backend.watch(dir.path()).is_ok());
|
||||
assert!(backend.watch(dir.path(), true).is_ok());
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
|
||||
// Rapid modifications - 20 writes in quick succession
|
||||
|
|
@ -635,7 +656,7 @@ mod tests {
|
|||
|
||||
let mut backend = StdBackend::new_for_testing();
|
||||
let event_rx = backend.event_receiver();
|
||||
assert!(backend.watch(dir.path()).is_ok());
|
||||
assert!(backend.watch(dir.path(), true).is_ok());
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
|
||||
// 10 cycles of create/delete
|
||||
|
|
@ -670,7 +691,7 @@ mod tests {
|
|||
let dir = tempdir().unwrap();
|
||||
let mut backend = StdBackend::new_for_testing();
|
||||
let event_rx = backend.event_receiver();
|
||||
assert!(backend.watch(dir.path()).is_ok());
|
||||
assert!(backend.watch(dir.path(), true).is_ok());
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
|
||||
// Create 20 files rapidly
|
||||
|
|
@ -722,7 +743,7 @@ mod tests {
|
|||
|
||||
let mut backend = StdBackend::new_for_testing();
|
||||
let event_rx = backend.event_receiver();
|
||||
assert!(backend.watch(dir.path()).is_ok());
|
||||
assert!(backend.watch(dir.path(), true).is_ok());
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
|
||||
// Rename the file
|
||||
|
|
@ -758,7 +779,7 @@ mod tests {
|
|||
|
||||
let mut backend = StdBackend::new_for_testing();
|
||||
let event_rx = backend.event_receiver();
|
||||
assert!(backend.watch(dir.path()).is_ok());
|
||||
assert!(backend.watch(dir.path(), true).is_ok());
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
|
||||
// Chain of renames
|
||||
|
|
@ -781,7 +802,7 @@ mod tests {
|
|||
let dir = tempdir().unwrap();
|
||||
let mut backend = StdBackend::new_for_testing();
|
||||
let event_rx = backend.event_receiver();
|
||||
assert!(backend.watch(dir.path()).is_ok());
|
||||
assert!(backend.watch(dir.path(), true).is_ok());
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
|
||||
// Create nested structure
|
||||
|
|
@ -826,7 +847,7 @@ mod tests {
|
|||
|
||||
let mut backend = StdBackend::new_for_testing();
|
||||
let event_rx = backend.event_receiver();
|
||||
assert!(backend.watch(dir.path()).is_ok());
|
||||
assert!(backend.watch(dir.path(), true).is_ok());
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
|
||||
// 100 writes with no delay
|
||||
|
|
@ -863,7 +884,7 @@ mod tests {
|
|||
let dir = tempdir().unwrap();
|
||||
let mut backend = StdBackend::new_for_testing();
|
||||
let event_rx = backend.event_receiver();
|
||||
assert!(backend.watch(dir.path()).is_ok());
|
||||
assert!(backend.watch(dir.path(), true).is_ok());
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
|
||||
let files: Vec<_> = (0..5)
|
||||
|
|
@ -915,7 +936,7 @@ mod tests {
|
|||
|
||||
let mut backend = StdBackend::new_for_testing();
|
||||
let event_rx = backend.event_receiver();
|
||||
assert!(backend.watch(dir.path()).is_ok());
|
||||
assert!(backend.watch(dir.path(), true).is_ok());
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
|
||||
// Simulate multiple undo/redo cycles
|
||||
|
|
@ -969,7 +990,7 @@ mod tests {
|
|||
let dir = tempdir().unwrap();
|
||||
let mut backend = StdBackend::new_for_testing();
|
||||
let event_rx = backend.event_receiver();
|
||||
assert!(backend.watch(dir.path()).is_ok());
|
||||
assert!(backend.watch(dir.path(), true).is_ok());
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
|
||||
// Rapidly create subdirectories with files
|
||||
|
|
@ -1005,7 +1026,7 @@ mod tests {
|
|||
|
||||
let mut backend = StdBackend::new_for_testing();
|
||||
let event_rx = backend.event_receiver();
|
||||
assert!(backend.watch(dir.path()).is_ok());
|
||||
assert!(backend.watch(dir.path(), true).is_ok());
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
|
||||
// Create empty file
|
||||
|
|
@ -1041,7 +1062,7 @@ mod tests {
|
|||
|
||||
let mut backend = StdBackend::new_for_testing();
|
||||
let event_rx = backend.event_receiver();
|
||||
assert!(backend.watch(dir.path()).is_ok());
|
||||
assert!(backend.watch(dir.path(), true).is_ok());
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
|
||||
fs_err::write(&file_path, "-- long name test").unwrap();
|
||||
|
|
@ -1067,7 +1088,7 @@ mod tests {
|
|||
|
||||
let mut backend = StdBackend::new_for_testing();
|
||||
let event_rx = backend.event_receiver();
|
||||
assert!(backend.watch(dir.path()).is_ok());
|
||||
assert!(backend.watch(dir.path(), true).is_ok());
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
|
||||
for name in &special_files {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue