From 2af57d87bccf23c5ceb88f28c2642f7655f1fc97 Mon Sep 17 00:00:00 2001 From: Oz Date: Wed, 3 Jun 2026 01:15:54 +0000 Subject: [PATCH] fix: cap recursive inotify watches to prevent multi-GB memory growth On Linux, the notify crate's inotify backend creates one watch descriptor per directory when using RecursiveMode::Recursive. For users with very large directory trees (deep node_modules, monorepos), this causes the internal HashMap and event buffers to grow to 10+ GB. Add a bounded directory count check before registering recursive watches. When a directory tree exceeds 100,000 subdirectories, the watcher falls back to RecursiveMode::NonRecursive to prevent excessive memory usage. Sentry issue: https://sentry.io/organizations/warpdotdev/issues/7259255054/ Root cause: notify::inotify::EventLoop::add_watch HashMap + Vec growth (98.4% of 11.22 GB sampled heap in heap-profile.pb) Co-Authored-By: Oz --- crates/watcher/Cargo.toml | 1 + crates/watcher/src/lib.rs | 61 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/crates/watcher/Cargo.toml b/crates/watcher/Cargo.toml index 401cb6e927..a3641ec6f7 100644 --- a/crates/watcher/Cargo.toml +++ b/crates/watcher/Cargo.toml @@ -13,3 +13,4 @@ log.workspace = true anyhow.workspace = true notify-debouncer-full.workspace = true futures.workspace = true +walkdir.workspace = true diff --git a/crates/watcher/src/lib.rs b/crates/watcher/src/lib.rs index febbf1c1b8..000e7b500a 100644 --- a/crates/watcher/src/lib.rs +++ b/crates/watcher/src/lib.rs @@ -19,6 +19,17 @@ use notify_debouncer_full::{ }; use warpui_core::{Entity, ModelContext}; +/// Maximum number of directories allowed in a single recursive watch registration. +/// On Linux, the `notify` crate's inotify backend creates one watch descriptor per +/// directory. Each watch consumes memory for internal bookkeeping (path storage, +/// event buffers, HashMap entries). When a directory tree contains hundreds of +/// thousands of subdirectories (e.g., deep `node_modules` trees or monorepos), +/// the cumulative memory can reach 10+ GB. +/// +/// When a directory tree exceeds this threshold, the watch falls back to +/// `RecursiveMode::NonRecursive` to prevent excessive memory usage. +const MAX_RECURSIVE_WATCH_DIRECTORIES: usize = 100_000; + #[derive(Debug)] enum BackgroundFileWatcherCommand { AddPath { @@ -68,9 +79,34 @@ impl BackgroundFileWatcher { response, recursive_mode, } => { + // On Linux, recursive watches create one inotify descriptor + // per directory, which can consume 10+ GB of memory for very + // large trees. Guard against this by checking the directory + // count before registering. + let effective_mode = if recursive_mode == RecursiveMode::Recursive { + match estimate_directory_count( + &path, + MAX_RECURSIVE_WATCH_DIRECTORIES, + ) { + count if count > MAX_RECURSIVE_WATCH_DIRECTORIES => { + log::warn!( + "Directory tree at {} contains >{} directories \ + (sampled {count}). Falling back to non-recursive \ + watch to prevent excessive memory usage.", + path.display(), + MAX_RECURSIVE_WATCH_DIRECTORIES, + ); + RecursiveMode::NonRecursive + } + _ => RecursiveMode::Recursive, + } + } else { + recursive_mode + }; + let _ = response.send( self.notifier - .watch_filtered(path, recursive_mode, filter) + .watch_filtered(path, effective_mode, filter) .inspect_err(|err| { log::warn!("Failed to watch path: {err:?}"); }) @@ -383,3 +419,26 @@ fn deduplicate_and_merge_raw_notifier_events( Ok(update) } + +/// Estimates the number of directories under `root` by walking the tree up to +/// `limit + 1` entries. Returns as soon as the count exceeds `limit`, avoiding +/// a full traversal of very large trees. +/// +/// Symlinks are *not* followed to avoid infinite loops. Permission errors and +/// other I/O failures on individual entries are silently skipped. +fn estimate_directory_count(root: &Path, limit: usize) -> usize { + let mut count: usize = 0; + for entry in walkdir::WalkDir::new(root) + .follow_links(false) + .into_iter() + .filter_map(|e| e.ok()) + { + if entry.file_type().is_dir() { + count += 1; + if count > limit { + return count; + } + } + } + count +}