Optimize fs_watcher to use less RAM by doing less work (#39602)

mac_watcher already does this so it would make more sense to also do
this on Windows and it saves ~500-600mb of ram on the chromium project.

This does not improve memory usage on linux because inotify cannot do
recursive directory monitoring

Release Notes:

- N/A
This commit is contained in:
localcc
2025-10-06 18:24:28 +02:00
committed by GitHub
parent 354cc65daa
commit 6da5945cd2
+67 -43
View File
@@ -1,7 +1,8 @@
use notify::EventKind;
use parking_lot::Mutex;
use std::{
collections::HashMap,
collections::{BTreeMap, HashMap},
ops::DerefMut,
sync::{Arc, OnceLock},
};
use util::{ResultExt, paths::SanitizedPath};
@@ -11,7 +12,7 @@ use crate::{PathEvent, PathEventKind, Watcher};
pub struct FsWatcher {
tx: smol::channel::Sender<()>,
pending_path_events: Arc<Mutex<Vec<PathEvent>>>,
registrations: Mutex<HashMap<Arc<std::path::Path>, WatcherRegistrationId>>,
registrations: Mutex<BTreeMap<Arc<std::path::Path>, WatcherRegistrationId>>,
}
impl FsWatcher {
@@ -29,8 +30,11 @@ impl FsWatcher {
impl Drop for FsWatcher {
fn drop(&mut self) {
let mut registrations = self.registrations.lock();
let registrations = registrations.drain();
let mut registrations = BTreeMap::new();
{
let old = &mut self.registrations.lock();
std::mem::swap(old.deref_mut(), &mut registrations);
}
let _ = global(|g| {
for (_, registration) in registrations {
@@ -42,57 +46,77 @@ impl Drop for FsWatcher {
impl Watcher for FsWatcher {
fn add(&self, path: &std::path::Path) -> anyhow::Result<()> {
let root_path = SanitizedPath::new_arc(path);
let tx = self.tx.clone();
let pending_paths = self.pending_path_events.clone();
#[cfg(target_os = "windows")]
{
// Return early if an ancestor of this path was already being watched.
// saves a huge amount of memory
if let Some((watched_path, _)) = self
.registrations
.lock()
.range::<std::path::Path, _>((
std::ops::Bound::Unbounded,
std::ops::Bound::Included(path),
))
.next_back()
&& path.starts_with(watched_path.as_ref())
{
return Ok(());
}
}
#[cfg(target_os = "linux")]
{
if self.registrations.lock().contains_key(path) {
return Ok(());
}
}
let root_path = SanitizedPath::new_arc(path);
let path: Arc<std::path::Path> = path.into();
if self.registrations.lock().contains_key(&path) {
return Ok(());
}
#[cfg(target_os = "windows")]
let mode = notify::RecursiveMode::Recursive;
#[cfg(target_os = "linux")]
let mode = notify::RecursiveMode::NonRecursive;
let registration_id = global({
let path = path.clone();
|g| {
g.add(
path,
notify::RecursiveMode::NonRecursive,
move |event: &notify::Event| {
let kind = match event.kind {
EventKind::Create(_) => Some(PathEventKind::Created),
EventKind::Modify(_) => Some(PathEventKind::Changed),
EventKind::Remove(_) => Some(PathEventKind::Removed),
_ => None,
};
let mut path_events = event
.paths
.iter()
.filter_map(|event_path| {
let event_path = SanitizedPath::new(event_path);
event_path.starts_with(&root_path).then(|| PathEvent {
path: event_path.as_path().to_path_buf(),
kind,
})
g.add(path, mode, move |event: &notify::Event| {
let kind = match event.kind {
EventKind::Create(_) => Some(PathEventKind::Created),
EventKind::Modify(_) => Some(PathEventKind::Changed),
EventKind::Remove(_) => Some(PathEventKind::Removed),
_ => None,
};
let mut path_events = event
.paths
.iter()
.filter_map(|event_path| {
let event_path = SanitizedPath::new(event_path);
event_path.starts_with(&root_path).then(|| PathEvent {
path: event_path.as_path().to_path_buf(),
kind,
})
.collect::<Vec<_>>();
})
.collect::<Vec<_>>();
if !path_events.is_empty() {
path_events.sort();
let mut pending_paths = pending_paths.lock();
if pending_paths.is_empty() {
tx.try_send(()).ok();
}
util::extend_sorted(
&mut *pending_paths,
path_events,
usize::MAX,
|a, b| a.path.cmp(&b.path),
);
if !path_events.is_empty() {
path_events.sort();
let mut pending_paths = pending_paths.lock();
if pending_paths.is_empty() {
tx.try_send(()).ok();
}
},
)
util::extend_sorted(
&mut *pending_paths,
path_events,
usize::MAX,
|a, b| a.path.cmp(&b.path),
);
}
})
}
})??;