Skip to main content

coding_agent_search/daemon/
resource.rs

1//! Resource monitoring and process priority management for the daemon.
2//!
3//! This module provides utilities for:
4//! - Monitoring process memory usage
5//! - Applying nice values (CPU priority)
6//! - Applying ionice (I/O priority)
7
8#[cfg(target_os = "linux")]
9use std::fs;
10#[cfg(target_os = "linux")]
11use std::process::Command;
12
13use tracing::debug;
14#[cfg(target_os = "linux")]
15use tracing::warn;
16
17// Inline POSIX constants and FFI for sysconf / setpriority — avoids a direct `libc` dependency.
18#[cfg(target_os = "linux")]
19mod posix {
20    use std::ffi::{c_int, c_long, c_uint};
21    pub const _SC_PAGESIZE: c_int = 30;
22    pub const PRIO_PROCESS: c_int = 0;
23    unsafe extern "C" {
24        pub fn sysconf(name: c_int) -> c_long;
25        pub fn setpriority(which: c_int, who: c_uint, prio: c_int) -> c_int;
26    }
27}
28
29/// Resource monitor for tracking daemon resource usage.
30#[derive(Debug, Default)]
31pub struct ResourceMonitor {
32    /// Cached PID for /proc lookups.
33    #[cfg(target_os = "linux")]
34    pid: u32,
35}
36
37impl ResourceMonitor {
38    /// Create a new resource monitor.
39    pub fn new() -> Self {
40        Self {
41            #[cfg(target_os = "linux")]
42            pid: std::process::id(),
43        }
44    }
45
46    /// Get current process memory usage in bytes.
47    ///
48    /// Reads from /proc/self/statm on Linux. Returns 0 on error or non-Linux.
49    pub fn memory_usage(&self) -> u64 {
50        #[cfg(target_os = "linux")]
51        {
52            self.linux_memory_usage()
53        }
54        #[cfg(not(target_os = "linux"))]
55        {
56            0
57        }
58    }
59
60    /// Linux-specific memory usage from /proc/self/statm.
61    #[cfg(target_os = "linux")]
62    fn linux_memory_usage(&self) -> u64 {
63        // /proc/self/statm format: size resident share text lib data dt
64        // Fields are in pages, multiply by page size
65        let page_size = Self::page_size();
66
67        match fs::read_to_string("/proc/self/statm") {
68            Ok(content) => {
69                let parts: Vec<&str> = content.split_whitespace().collect();
70                if parts.len() >= 2 {
71                    // Use RSS (resident set size) - second field
72                    if let Ok(pages) = parts[1].parse::<u64>() {
73                        return pages * page_size;
74                    }
75                }
76                0
77            }
78            Err(e) => {
79                debug!(error = %e, "Failed to read /proc/self/statm");
80                0
81            }
82        }
83    }
84
85    /// Get system page size in bytes.
86    #[cfg(target_os = "linux")]
87    fn page_size() -> u64 {
88        // SAFETY: sysconf has no pointer arguments and is thread-safe for this key.
89        let raw = unsafe { posix::sysconf(posix::_SC_PAGESIZE) };
90        if raw > 0 { raw as u64 } else { 4096 }
91    }
92
93    /// Apply a nice value to the current process.
94    ///
95    /// Nice values range from -20 (highest priority) to 19 (lowest priority).
96    /// Returns true if successful.
97    pub fn apply_nice(&self, nice_value: i32) -> bool {
98        #[cfg(target_os = "linux")]
99        {
100            if !(-20..=19).contains(&nice_value) {
101                warn!(
102                    nice = nice_value,
103                    "Refusing out-of-range nice value (valid range: -20..=19)"
104                );
105                return false;
106            }
107
108            // SAFETY: setpriority operates on the current process id and does not
109            // retain pointers. We pass scalar values only.
110            let result = unsafe {
111                posix::setpriority(
112                    posix::PRIO_PROCESS,
113                    self.pid as std::ffi::c_uint,
114                    nice_value,
115                )
116            };
117            if result != 0 {
118                let err = std::io::Error::last_os_error();
119                warn!(nice = nice_value, error = %err, "Failed to set nice value");
120                return false;
121            }
122
123            debug!(
124                nice = nice_value,
125                pid = self.pid,
126                "Applied absolute nice value"
127            );
128            true
129        }
130
131        #[cfg(not(target_os = "linux"))]
132        {
133            debug!(nice = nice_value, "nice not supported on this platform");
134            let _ = nice_value;
135            false
136        }
137    }
138
139    /// Apply an I/O priority class to the current process using ionice.
140    ///
141    /// IO priority classes:
142    /// - 0: None (use the CFQ default)
143    /// - 1: Realtime (highest priority)
144    /// - 2: Best-effort (normal priority)
145    /// - 3: Idle (lowest priority)
146    ///
147    /// Returns true if successful.
148    pub fn apply_ionice(&self, class: u32) -> bool {
149        #[cfg(target_os = "linux")]
150        {
151            if class > 3 {
152                warn!(
153                    class = class,
154                    "Refusing unsupported ionice class (valid classes: 0..=3)"
155                );
156                return false;
157            }
158            let class_str = class.to_string();
159
160            // Use ionice command to set I/O scheduling class
161            match Command::new("ionice")
162                .args(["-c", &class_str, "-p", &self.pid.to_string()])
163                .output()
164            {
165                Ok(output) => {
166                    if output.status.success() {
167                        debug!(class = class, pid = self.pid, "Applied ionice class");
168                        true
169                    } else {
170                        let stderr = String::from_utf8_lossy(&output.stderr);
171                        warn!(
172                            class = class,
173                            error = %stderr,
174                            "ionice command failed"
175                        );
176                        false
177                    }
178                }
179                Err(e) => {
180                    warn!(error = %e, "ionice command not available");
181                    false
182                }
183            }
184        }
185
186        #[cfg(not(target_os = "linux"))]
187        {
188            debug!(class = class, "ionice not supported on this platform");
189            let _ = class;
190            false
191        }
192    }
193
194    /// Get memory usage as a human-readable string.
195    pub fn memory_usage_human(&self) -> String {
196        let bytes = self.memory_usage();
197        if bytes == 0 {
198            return "unknown".to_string();
199        }
200
201        const KB: u64 = 1024;
202        const MB: u64 = KB * 1024;
203        const GB: u64 = MB * 1024;
204
205        if bytes >= GB {
206            format!("{:.1} GB", bytes as f64 / GB as f64)
207        } else if bytes >= MB {
208            format!("{:.1} MB", bytes as f64 / MB as f64)
209        } else if bytes >= KB {
210            format!("{:.1} KB", bytes as f64 / KB as f64)
211        } else {
212            format!("{} B", bytes)
213        }
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn test_resource_monitor_creation() {
223        let monitor = ResourceMonitor::new();
224        #[cfg(target_os = "linux")]
225        assert!(monitor.pid > 0);
226    }
227
228    #[test]
229    fn test_memory_usage() {
230        let monitor = ResourceMonitor::new();
231        let mem = monitor.memory_usage();
232
233        // On Linux, we should get a non-zero value
234        #[cfg(target_os = "linux")]
235        assert!(mem > 0, "Memory usage should be non-zero on Linux");
236
237        // On non-Linux, it returns 0
238        #[cfg(not(target_os = "linux"))]
239        assert_eq!(mem, 0);
240    }
241
242    #[test]
243    fn test_memory_usage_human() {
244        let monitor = ResourceMonitor::new();
245        let human = monitor.memory_usage_human();
246
247        // Should return a valid string
248        assert!(!human.is_empty());
249
250        #[cfg(target_os = "linux")]
251        {
252            // Should contain a unit
253            assert!(
254                human.contains("KB") || human.contains("MB") || human.contains("GB"),
255                "Memory string should contain unit: {}",
256                human
257            );
258        }
259    }
260
261    #[test]
262    fn test_apply_nice_range() {
263        let monitor = ResourceMonitor::new();
264
265        // Applying nice to increase niceness (lower priority) should work
266        // Note: Decreasing niceness requires root privileges
267        #[cfg(target_os = "linux")]
268        {
269            // Nice to 19 (lowest priority) should always work
270            let result = monitor.apply_nice(19);
271            // May fail if already at max nice
272            let _ = result;
273        }
274
275        #[cfg(not(target_os = "linux"))]
276        {
277            assert!(!monitor.apply_nice(10));
278        }
279    }
280
281    #[test]
282    fn test_apply_ionice() {
283        let monitor = ResourceMonitor::new();
284
285        #[cfg(target_os = "linux")]
286        {
287            // Best-effort class (2) should work
288            let result = monitor.apply_ionice(2);
289            // May fail if ionice isn't available
290            let _ = result;
291
292            // Idle class (3) should work too
293            let result = monitor.apply_ionice(3);
294            let _ = result;
295        }
296
297        #[cfg(not(target_os = "linux"))]
298        {
299            assert!(!monitor.apply_ionice(2));
300        }
301    }
302
303    #[test]
304    fn test_page_size() {
305        #[cfg(target_os = "linux")]
306        {
307            let size = ResourceMonitor::page_size();
308            assert!(size > 0);
309            assert!(size.is_power_of_two());
310        }
311    }
312
313    #[test]
314    fn test_apply_nice_rejects_out_of_range() {
315        let monitor = ResourceMonitor::new();
316        assert!(!monitor.apply_nice(20));
317        assert!(!monitor.apply_nice(-21));
318    }
319
320    #[test]
321    fn test_apply_ionice_rejects_invalid_class() {
322        let monitor = ResourceMonitor::new();
323        assert!(!monitor.apply_ionice(4));
324    }
325}