Skip to main content

xet_runtime/
fd_diagnostics.rs

1#[cfg(feature = "fd-track")]
2use std::sync::atomic::{AtomicUsize, Ordering};
3
4#[cfg(feature = "fd-track")]
5use tracing::debug;
6
7#[cfg(feature = "fd-track")]
8static BASELINE_FD_COUNT: AtomicUsize = AtomicUsize::new(0);
9#[cfg(feature = "fd-track")]
10static PEAK_FD_COUNT: AtomicUsize = AtomicUsize::new(0);
11
12#[cfg(any(target_os = "macos", target_os = "linux"))]
13const PROC_FD_PATH: &str = if cfg!(target_os = "macos") {
14    "/dev/fd"
15} else {
16    "/proc/self/fd"
17};
18
19/// Returns the number of currently open file descriptors for this process.
20#[cfg(any(target_os = "macos", target_os = "linux"))]
21pub fn count_open_fds() -> usize {
22    std::fs::read_dir(PROC_FD_PATH).map(|d| d.count()).unwrap_or(0)
23}
24
25/// Returns the number of currently open file descriptors for this process.
26#[cfg(not(any(target_os = "macos", target_os = "linux")))]
27pub fn count_open_fds() -> usize {
28    0
29}
30
31#[cfg(feature = "fd-track")]
32fn baseline_or_init() -> usize {
33    let baseline = BASELINE_FD_COUNT.load(Ordering::Relaxed);
34    if baseline == 0 {
35        set_fd_baseline();
36        BASELINE_FD_COUNT.load(Ordering::Relaxed)
37    } else {
38        baseline
39    }
40}
41
42#[cfg(feature = "fd-track")]
43fn update_peak(current: usize) {
44    let mut observed = PEAK_FD_COUNT.load(Ordering::Relaxed);
45    while current > observed
46        && PEAK_FD_COUNT
47            .compare_exchange_weak(observed, current, Ordering::Relaxed, Ordering::Relaxed)
48            .is_err()
49    {
50        observed = PEAK_FD_COUNT.load(Ordering::Relaxed);
51    }
52}
53
54/// Sets the baseline FD count to the current value. Call this once at the start
55/// of a test or process to establish a reference point.
56pub fn set_fd_baseline() {
57    #[cfg(feature = "fd-track")]
58    {
59        let count = count_open_fds();
60        BASELINE_FD_COUNT.store(count, Ordering::Relaxed);
61        PEAK_FD_COUNT.store(count, Ordering::Relaxed);
62        debug!(
63            target: "xet_runtime::fd_track",
64            fd_count = count,
65            "FD baseline initialized"
66        );
67    }
68}
69
70/// Returns the number of file descriptors opened since the last call to
71/// [`set_fd_baseline`]. Negative values (FDs closed) are reported as 0.
72pub fn fds_since_baseline() -> usize {
73    #[cfg(feature = "fd-track")]
74    {
75        let baseline = baseline_or_init();
76        count_open_fds().saturating_sub(baseline)
77    }
78    #[cfg(not(feature = "fd-track"))]
79    {
80        0
81    }
82}
83
84/// Logs the current FD count snapshot with a label when `fd-track` is enabled.
85pub fn report_fd_count(label: &str) {
86    #[cfg(feature = "fd-track")]
87    {
88        let count = count_open_fds();
89        let baseline = baseline_or_init();
90        update_peak(count);
91        debug!(
92            target: "xet_runtime::fd_track",
93            label,
94            fd_count = count,
95            baseline,
96            delta = count as isize - baseline as isize,
97            opened_since_baseline = fds_since_baseline(),
98            peak = PEAK_FD_COUNT.load(Ordering::Relaxed),
99            "FD snapshot"
100        );
101    }
102    #[cfg(not(feature = "fd-track"))]
103    {
104        let _ = label;
105    }
106}
107
108/// RAII scope guard that records FD growth/cleanup over a code path.
109/// Logs on creation/drop only when `fd-track` is enabled.
110#[derive(Debug, Default)]
111pub struct FdTrackGuard {
112    #[cfg(feature = "fd-track")]
113    label: String,
114    #[cfg(feature = "fd-track")]
115    start_count: usize,
116    #[cfg(all(feature = "fd-track", unix))]
117    start_snapshot: Vec<(i32, String)>,
118}
119
120pub fn track_fd_scope(label: impl Into<String>) -> FdTrackGuard {
121    #[cfg(feature = "fd-track")]
122    {
123        let label = label.into();
124        let start_count = count_open_fds();
125        let baseline = baseline_or_init();
126        update_peak(start_count);
127        debug!(
128            target: "xet_runtime::fd_track",
129            label,
130            start_fd = start_count,
131            baseline,
132            delta = start_count as isize - baseline as isize,
133            "FD scope start"
134        );
135        FdTrackGuard {
136            label,
137            start_count,
138            #[cfg(unix)]
139            start_snapshot: list_open_fds(),
140        }
141    }
142    #[cfg(not(feature = "fd-track"))]
143    {
144        let _ = label;
145        FdTrackGuard::default()
146    }
147}
148
149impl Drop for FdTrackGuard {
150    fn drop(&mut self) {
151        #[cfg(feature = "fd-track")]
152        {
153            let end_count = count_open_fds();
154            let baseline = baseline_or_init();
155            update_peak(end_count);
156            let change = end_count as isize - self.start_count as isize;
157            debug!(
158                target: "xet_runtime::fd_track",
159                label = self.label,
160                start_fd = self.start_count,
161                end_fd = end_count,
162                change,
163                baseline,
164                baseline_delta = end_count as isize - baseline as isize,
165                opened_since_baseline = fds_since_baseline(),
166                peak = PEAK_FD_COUNT.load(Ordering::Relaxed),
167                "FD scope end"
168            );
169
170            #[cfg(unix)]
171            if change > 0 {
172                let end_snapshot = list_open_fds();
173                print_new_fds(&self.label, &self.start_snapshot, &end_snapshot);
174            }
175        }
176    }
177}
178
179/// Returns a snapshot of all currently open file descriptors and what they
180/// point to (via readlink on macOS/Linux). Useful for diffing before/after
181/// to identify leaked FDs.
182#[cfg(target_os = "macos")]
183pub fn list_open_fds() -> Vec<(i32, String)> {
184    #[cfg(feature = "fd-track")]
185    {
186        let Ok(entries) = std::fs::read_dir("/dev/fd") else {
187            return Vec::new();
188        };
189        let mut fds: Vec<(i32, String)> = entries
190            .filter_map(|e| {
191                let e = e.ok()?;
192                let fd: i32 = e.file_name().to_str()?.parse().ok()?;
193                let target = std::fs::read_link(e.path())
194                    .map(|p| p.display().to_string())
195                    .unwrap_or_else(|_| "<unknown>".into());
196                Some((fd, target))
197            })
198            .collect();
199        fds.sort_by_key(|(fd, _)| *fd);
200        fds
201    }
202    #[cfg(not(feature = "fd-track"))]
203    {
204        Vec::new()
205    }
206}
207
208/// Returns a snapshot of all currently open file descriptors and what they
209/// point to (via readlink on Linux).
210#[cfg(target_os = "linux")]
211pub fn list_open_fds() -> Vec<(i32, String)> {
212    #[cfg(feature = "fd-track")]
213    {
214        let Ok(entries) = std::fs::read_dir("/proc/self/fd") else {
215            return Vec::new();
216        };
217        let mut fds: Vec<(i32, String)> = entries
218            .filter_map(|e| {
219                let e = e.ok()?;
220                let fd: i32 = e.file_name().to_str()?.parse().ok()?;
221                let target = std::fs::read_link(e.path())
222                    .map(|p| p.display().to_string())
223                    .unwrap_or_else(|_| "<unknown>".into());
224                Some((fd, target))
225            })
226            .collect();
227        fds.sort_by_key(|(fd, _)| *fd);
228        fds
229    }
230    #[cfg(not(feature = "fd-track"))]
231    {
232        Vec::new()
233    }
234}
235
236#[cfg(not(any(target_os = "macos", target_os = "linux")))]
237pub fn list_open_fds() -> Vec<(i32, String)> {
238    Vec::new()
239}
240
241/// Prints FDs present in `after` but not in `before`.
242#[cfg(unix)]
243pub fn print_new_fds(label: &str, before: &[(i32, String)], after: &[(i32, String)]) {
244    #[cfg(feature = "fd-track")]
245    {
246        let before_set: std::collections::HashSet<i32> = before.iter().map(|(fd, _)| *fd).collect();
247        let new_fds: Vec<_> = after.iter().filter(|(fd, _)| !before_set.contains(fd)).collect();
248
249        if new_fds.is_empty() {
250            debug!(target: "xet_runtime::fd_track", label, "FD diff: no new descriptors");
251            return;
252        }
253
254        debug!(
255            target: "xet_runtime::fd_track",
256            label,
257            new_fd_count = new_fds.len(),
258            "FD diff: detected new descriptors"
259        );
260        for (fd, target) in &new_fds {
261            debug!(target: "xet_runtime::fd_track", label, fd = *fd, target = %target, "FD diff entry");
262        }
263
264        let pid = std::process::id();
265        let fd_list: String = new_fds.iter().map(|(fd, _)| fd.to_string()).collect::<Vec<_>>().join(",");
266        if let Ok(output) = std::process::Command::new("lsof")
267            .args(["-p", &pid.to_string(), "-a", "-d", &fd_list])
268            .output()
269            && output.status.success()
270        {
271            let lsof = String::from_utf8_lossy(&output.stdout);
272            debug!(target: "xet_runtime::fd_track", label, lsof = %lsof, "FD diff lsof details");
273        }
274    }
275    #[cfg(not(feature = "fd-track"))]
276    {
277        let _ = (label, before, after);
278    }
279}
280
281#[cfg(not(unix))]
282pub fn print_new_fds(label: &str, before: &[(i32, String)], after: &[(i32, String)]) {
283    let _ = (label, before, after);
284}