-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
only use recursive watchers on macOS and windows #4100
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,7 +29,7 @@ use std::{ | |
path::{Path, PathBuf, MAIN_SEPARATOR}, | ||
sync::{ | ||
mpsc::{channel, RecvError, TryRecvError}, | ||
Arc, Mutex, | ||
Arc, Mutex, MutexGuard, | ||
}, | ||
time::Duration, | ||
}; | ||
|
@@ -80,6 +80,64 @@ pub trait FileSystem: ValueToString { | |
fn metadata(&self, fs_path: FileSystemPathVc) -> FileMetaVc; | ||
} | ||
|
||
#[derive(Default)] | ||
struct DiskWatcher { | ||
watcher: Mutex<Option<RecommendedWatcher>>, | ||
/// Keeps track of which directories are currently watched. This is only | ||
/// used on a OS that doesn't support recursive watching. | ||
#[cfg(not(any(target_os = "macos", target_os = "windows")))] | ||
watching: dashmap::DashSet<PathBuf>, | ||
} | ||
|
||
impl DiskWatcher { | ||
#[cfg(not(any(target_os = "macos", target_os = "windows")))] | ||
fn restore_if_watching(&self, dir_path: &Path, root_path: &Path) -> Result<()> { | ||
if self.watching.contains(dir_path) { | ||
let mut watcher = self.watcher.lock().unwrap(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have a lock that's only used inside a loop. If we passed the |
||
self.start_watching(&mut watcher, dir_path, &root_path)?; | ||
} | ||
Ok(()) | ||
} | ||
|
||
#[cfg(not(any(target_os = "macos", target_os = "windows")))] | ||
fn ensure_watching(&self, dir_path: &Path, root_path: &Path) -> Result<()> { | ||
if self.watching.contains(dir_path) { | ||
return Ok(()); | ||
} | ||
let mut watcher = self.watcher.lock().unwrap(); | ||
if self.watching.insert(dir_path.to_path_buf()) { | ||
self.start_watching(&mut watcher, dir_path, root_path)?; | ||
} | ||
Ok(()) | ||
} | ||
|
||
#[cfg(not(any(target_os = "macos", target_os = "windows")))] | ||
fn start_watching( | ||
&self, | ||
watcher: &mut MutexGuard<Option<RecommendedWatcher>>, | ||
dir_path: &Path, | ||
root_path: &Path, | ||
) -> Result<()> { | ||
if let Some(watcher) = watcher.as_mut() { | ||
let mut path = dir_path; | ||
while let Err(err) = watcher.watch(path, RecursiveMode::NonRecursive) { | ||
if path == root_path { | ||
return Err(err).context(format!( | ||
"Unable to watch {} (tried up to {})", | ||
dir_path.display(), | ||
path.display() | ||
)); | ||
} | ||
let Some(parent_path) = path.parent() else { | ||
return Err(err).context(format!("Unable to watch {} (tried up to {})", dir_path.display(), path.display())); | ||
}; | ||
path = parent_path; | ||
} | ||
} | ||
Ok(()) | ||
} | ||
} | ||
|
||
#[turbo_tasks::value(cell = "new", eq = "manual")] | ||
pub struct DiskFileSystem { | ||
pub name: String, | ||
|
@@ -93,20 +151,36 @@ pub struct DiskFileSystem { | |
dir_invalidator_map: Arc<InvalidatorMap>, | ||
#[turbo_tasks(debug_ignore, trace_ignore)] | ||
#[serde(skip)] | ||
watcher: Mutex<Option<RecommendedWatcher>>, | ||
watcher: Arc<DiskWatcher>, | ||
} | ||
|
||
impl DiskFileSystem { | ||
/// Returns the root as Path | ||
fn root_path(&self) -> &Path { | ||
simplified(Path::new(&self.root)) | ||
} | ||
|
||
/// registers the path as an invalidator for the current task, | ||
/// has to be called within a turbo-tasks function | ||
fn register_invalidator(&self, path: impl AsRef<Path>, file: bool) { | ||
fn register_invalidator(&self, path: &Path) -> Result<()> { | ||
let invalidator = turbo_tasks::get_invalidator(); | ||
if file { | ||
self.invalidator_map.insert(path_to_key(path), invalidator); | ||
} else { | ||
self.dir_invalidator_map | ||
.insert(path_to_key(path), invalidator); | ||
self.invalidator_map.insert(path_to_key(path), invalidator); | ||
#[cfg(not(any(target_os = "macos", target_os = "windows")))] | ||
if let Some(dir) = path.parent() { | ||
self.watcher.ensure_watching(dir, self.root_path())?; | ||
} | ||
Ok(()) | ||
} | ||
|
||
/// registers the path as an invalidator for the current task, | ||
/// has to be called within a turbo-tasks function | ||
fn register_dir_invalidator(&self, path: &Path) -> Result<()> { | ||
let invalidator = turbo_tasks::get_invalidator(); | ||
self.dir_invalidator_map | ||
.insert(path_to_key(path), invalidator); | ||
#[cfg(not(any(target_os = "macos", target_os = "windows")))] | ||
self.watcher.ensure_watching(path, self.root_path())?; | ||
Ok(()) | ||
} | ||
|
||
pub fn invalidate(&self) { | ||
|
@@ -119,7 +193,7 @@ impl DiskFileSystem { | |
} | ||
|
||
pub fn start_watching(&self) -> Result<()> { | ||
let mut watcher_guard = self.watcher.lock().unwrap(); | ||
let mut watcher_guard = self.watcher.watcher.lock().unwrap(); | ||
if watcher_guard.is_some() { | ||
return Ok(()); | ||
} | ||
|
@@ -133,7 +207,12 @@ impl DiskFileSystem { | |
let mut watcher = watcher(tx, Duration::from_millis(1))?; | ||
// Add a path to be watched. All files and directories at that path and | ||
// below will be monitored for changes. | ||
#[cfg(any(target_os = "macos", target_os = "windows"))] | ||
watcher.watch(&root, RecursiveMode::Recursive)?; | ||
#[cfg(not(any(target_os = "macos", target_os = "windows")))] | ||
for dir_path in self.watcher.watching.iter() { | ||
watcher.watch(&*dir_path, RecursiveMode::NonRecursive)?; | ||
} | ||
|
||
// We need to invalidate all reads that happened before watching | ||
// Best is to start_watching before starting to read | ||
|
@@ -145,12 +224,18 @@ impl DiskFileSystem { | |
} | ||
|
||
watcher_guard.replace(watcher); | ||
drop(watcher_guard); | ||
|
||
let disk_watcher = self.watcher.clone(); | ||
let root_path = self.root_path().to_path_buf(); | ||
Comment on lines
+229
to
+230
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ditto unused. Could we make a consistent interface between Mac and Linux so we can just make calls to empty methods? |
||
|
||
spawn_thread(move || { | ||
let mut batched_invalidate_path = HashSet::new(); | ||
let mut batched_invalidate_path_dir = HashSet::new(); | ||
let mut batched_invalidate_path_and_children = HashSet::new(); | ||
let mut batched_invalidate_path_and_children_dir = HashSet::new(); | ||
#[cfg(not(any(target_os = "macos", target_os = "windows")))] | ||
let mut batched_new_paths = HashSet::new(); | ||
|
||
'outer: loop { | ||
let mut event = rx.recv().map_err(|e| match e { | ||
|
@@ -161,7 +246,16 @@ impl DiskFileSystem { | |
Ok(DebouncedEvent::Write(path)) => { | ||
batched_invalidate_path.insert(path); | ||
} | ||
Ok(DebouncedEvent::Create(path)) | Ok(DebouncedEvent::Remove(path)) => { | ||
Ok(DebouncedEvent::Create(path)) => { | ||
batched_invalidate_path_and_children.insert(path.clone()); | ||
batched_invalidate_path_and_children_dir.insert(path.clone()); | ||
if let Some(parent) = path.parent() { | ||
batched_invalidate_path_dir.insert(PathBuf::from(parent)); | ||
} | ||
#[cfg(not(any(target_os = "macos", target_os = "windows")))] | ||
batched_new_paths.insert(path.clone()); | ||
} | ||
Ok(DebouncedEvent::Remove(path)) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to remove a from the |
||
batched_invalidate_path_and_children.insert(path.clone()); | ||
batched_invalidate_path_and_children_dir.insert(path.clone()); | ||
if let Some(parent) = path.parent() { | ||
|
@@ -177,6 +271,8 @@ impl DiskFileSystem { | |
if let Some(parent) = destination.parent() { | ||
batched_invalidate_path_dir.insert(PathBuf::from(parent)); | ||
} | ||
#[cfg(not(any(target_os = "macos", target_os = "windows")))] | ||
batched_new_paths.insert(destination.clone()); | ||
} | ||
Ok(DebouncedEvent::Rescan) => { | ||
batched_invalidate_path_and_children.insert(PathBuf::from(&root)); | ||
|
@@ -257,21 +353,27 @@ impl DiskFileSystem { | |
&mut batched_invalidate_path_and_children_dir, | ||
); | ||
} | ||
#[cfg(not(any(target_os = "macos", target_os = "windows")))] | ||
{ | ||
for path in batched_new_paths.drain() { | ||
let _ = disk_watcher.restore_if_watching(&path, &root_path); | ||
} | ||
} | ||
} | ||
}); | ||
Ok(()) | ||
} | ||
|
||
pub fn stop_watching(&self) { | ||
if let Some(watcher) = self.watcher.lock().unwrap().take() { | ||
if let Some(watcher) = self.watcher.watcher.lock().unwrap().take() { | ||
drop(watcher); | ||
// thread will detect the stop because the channel is disconnected | ||
} | ||
} | ||
|
||
pub async fn to_sys_path(&self, fs_path: FileSystemPathVc) -> Result<PathBuf> { | ||
// just in case there's a windows unc path prefix we remove it with `dunce` | ||
let path = simplified(Path::new(&self.root)); | ||
let path = self.root_path(); | ||
let fs_path = fs_path.await?; | ||
Ok(if fs_path.path.is_empty() { | ||
path.to_path_buf() | ||
|
@@ -299,7 +401,7 @@ impl DiskFileSystemVc { | |
mutex_map: Default::default(), | ||
invalidator_map: Arc::new(InvalidatorMap::new()), | ||
dir_invalidator_map: Arc::new(InvalidatorMap::new()), | ||
watcher: Mutex::new(None), | ||
watcher: Default::default(), | ||
}; | ||
|
||
Ok(Self::cell(instance)) | ||
|
@@ -329,7 +431,7 @@ impl FileSystem for DiskFileSystem { | |
#[turbo_tasks::function] | ||
async fn read(&self, fs_path: FileSystemPathVc) -> Result<FileContentVc> { | ||
let full_path = self.to_sys_path(fs_path).await?; | ||
self.register_invalidator(&full_path, true); | ||
self.register_invalidator(&full_path)?; | ||
|
||
let content = read_file(full_path, &self.mutex_map).await?; | ||
Ok(content.cell()) | ||
|
@@ -338,7 +440,7 @@ impl FileSystem for DiskFileSystem { | |
#[turbo_tasks::function] | ||
async fn read_dir(&self, fs_path: FileSystemPathVc) -> Result<DirectoryContentVc> { | ||
let full_path = self.to_sys_path(fs_path).await?; | ||
self.register_invalidator(&full_path, false); | ||
self.register_dir_invalidator(&full_path)?; | ||
let fs_path = fs_path.await?; | ||
|
||
// we use the sync std function here as it's a lot faster (600%) in | ||
|
@@ -392,7 +494,7 @@ impl FileSystem for DiskFileSystem { | |
#[turbo_tasks::function] | ||
async fn read_link(&self, fs_path: FileSystemPathVc) -> Result<LinkContentVc> { | ||
let full_path = self.to_sys_path(fs_path).await?; | ||
self.register_invalidator(&full_path, true); | ||
self.register_invalidator(&full_path)?; | ||
|
||
let _lock = self.mutex_map.lock(full_path.clone()).await; | ||
let link_path = match retry_future(|| fs::read_link(&full_path)).await { | ||
|
@@ -474,7 +576,7 @@ impl FileSystem for DiskFileSystem { | |
#[turbo_tasks::function] | ||
async fn track(&self, fs_path: FileSystemPathVc) -> Result<CompletionVc> { | ||
let full_path = self.to_sys_path(fs_path).await?; | ||
self.register_invalidator(full_path, true); | ||
self.register_invalidator(&full_path)?; | ||
Ok(CompletionVc::new()) | ||
} | ||
|
||
|
@@ -629,7 +731,7 @@ impl FileSystem for DiskFileSystem { | |
#[turbo_tasks::function] | ||
async fn metadata(&self, fs_path: FileSystemPathVc) -> Result<FileMetaVc> { | ||
let full_path = self.to_sys_path(fs_path).await?; | ||
self.register_invalidator(&full_path, true); | ||
self.register_invalidator(&full_path)?; | ||
|
||
let _lock = self.mutex_map.lock(full_path.clone()).await; | ||
let meta = retry_future(|| fs::metadata(full_path.clone())) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MutexGuard
is unused on Mac/Windows, giving a clippy warning.