zccache 1.12.12

Local-first compiler cache for C/C++/Rust/Emscripten
Documentation
//! Directory watch registration helpers.
//!
//! Compile/link requests call into these to ensure that the directories
//! holding their source/header files are being watched. Canonicalization is
//! deferred behind a raw-path pre-filter because `canonicalize()` is 1-5ms on
//! Windows for paths already known to be watched.

use super::*;

/// Watch a directory for file changes, if not already watched.
pub(super) async fn watch_directory(state: &SharedState, dir: &Path) {
    watch_directories(state, &[dir.into()]).await;
}

async fn canonicalize_watch_registration_batch(dirs: Vec<NormalizedPath>) -> Vec<NormalizedPath> {
    tokio::task::spawn_blocking(move || {
        dirs.into_iter()
            .filter_map(|dir| match dir.canonicalize() {
                Ok(p) => {
                    #[cfg(windows)]
                    {
                        let s = p.to_string_lossy();
                        if let Some(stripped) = s.strip_prefix(r"\\?\") {
                            Some(stripped.into())
                        } else {
                            Some(p.into())
                        }
                    }
                    #[cfg(not(windows))]
                    {
                        Some(p.into())
                    }
                }
                Err(e) => {
                    tracing::debug!("cannot canonicalize {}: {e}", dir.display());
                    None
                }
            })
            .collect()
    })
    .await
    .unwrap_or_else(|err| {
        tracing::warn!("watch canonicalization worker failed: {err}");
        Vec::new()
    })
}

/// Watch multiple directories in a single batch.
///
/// Canonicalizes all paths up front, reserves unwatched paths under
/// `watched_dirs`, then registers each new watch independently.
pub(super) async fn watch_directories(state: &SharedState, dirs: &[NormalizedPath]) {
    if dirs.is_empty() {
        return;
    }

    // Pre-filter: skip dirs we've already processed (by raw path).
    // This avoids expensive canonicalize() syscalls (~1-5ms each on Windows)
    // for directories that are already being watched.
    let new_raw: Vec<NormalizedPath> = dirs
        .iter()
        .filter(|d| !state.watched_raw_dirs.contains_key(*d))
        .cloned()
        .collect();
    if new_raw.is_empty() {
        return;
    }

    let canonical = canonicalize_watch_registration_batch(new_raw.clone()).await;

    // Mark raw paths as processed (even if canonicalize failed) so we don't
    // retry them on every subsequent call.
    for d in &new_raw {
        state.watched_raw_dirs.insert(d.clone(), ());
    }

    if canonical.is_empty() {
        return;
    }

    // Reserve already-watched paths without holding the set lock across
    // notify's blocking watch registration.
    // Each directory here is the exact parent of a source/header file from
    // depfile scanning — no need to walk children or parents.
    let new_dirs: Vec<NormalizedPath> = {
        let mut watched = state.watched_dirs.lock().await;
        canonical
            .into_iter()
            .filter(|p| watched.insert(p.clone()))
            .collect()
    };

    if new_dirs.is_empty() {
        return;
    }

    // Batch the per-dir watcher-mutex acquisition: acquire the watcher
    // lock once and register every directory under a single guard. Each
    // `w.watch(dir)` is a synchronous syscall (~50us inotify_add_watch on
    // Linux); holding the tokio Mutex across N back-to-back syscalls
    // costs far less futex traffic than N separate acquire/release pairs,
    // and serialises 50 concurrent cold-miss handlers through one
    // futex-wait instead of 50 per request.
    //
    // Also batch the rollback for the "watcher unavailable" / "watch
    // failed" cases under a single `watched_dirs` lock acquisition,
    // preserving the prior behaviour of removing the entry so a future
    // call may retry.
    let mut to_unmark: Vec<NormalizedPath> = Vec::new();
    {
        let mut watcher_guard = state.watcher.lock().await;
        match *watcher_guard {
            Some(ref mut w) => {
                for dir in &new_dirs {
                    match w.watch(dir) {
                        Ok(()) => {
                            tracing::info!("watching directory: {}", dir.display());
                        }
                        Err(e) => {
                            tracing::warn!("failed to watch {}: {e}", dir.display());
                            to_unmark.push(dir.clone());
                        }
                    }
                }
            }
            None => {
                // No watcher available — roll back every reservation.
                to_unmark.extend(new_dirs.iter().cloned());
            }
        }
    }

    if !to_unmark.is_empty() {
        let mut watched = state.watched_dirs.lock().await;
        for dir in &to_unmark {
            watched.remove(dir);
        }
    }
}