1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
//! 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);
}
}
}