Skip to main content

oxirs_arq/
system_load_monitor.rs

1//! System Load Monitoring for Adaptive Query Execution
2//!
3//! This module provides real-time system resource monitoring to enable
4//! adaptive concurrency adjustment for optimal performance under varying load conditions.
5
6use anyhow::Result;
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::Arc;
9use std::time::{Duration, Instant};
10
11/// System load monitor for adaptive resource management
12#[derive(Debug, Clone)]
13pub struct SystemLoadMonitor {
14    /// CPU usage percentage (0-100)
15    cpu_usage: Arc<AtomicU64>,
16    /// Memory usage percentage (0-100)
17    memory_usage: Arc<AtomicU64>,
18    /// Last update timestamp
19    last_update: Arc<std::sync::Mutex<Instant>>,
20    /// Update interval
21    update_interval: Duration,
22}
23
24impl SystemLoadMonitor {
25    /// Create new system load monitor
26    pub fn new() -> Self {
27        Self::with_update_interval(Duration::from_secs(1))
28    }
29
30    /// Create monitor with custom update interval
31    pub fn with_update_interval(interval: Duration) -> Self {
32        Self {
33            cpu_usage: Arc::new(AtomicU64::new(0)),
34            memory_usage: Arc::new(AtomicU64::new(0)),
35            last_update: Arc::new(std::sync::Mutex::new(Instant::now())),
36            update_interval: interval,
37        }
38    }
39
40    /// Get current CPU usage percentage (0-100)
41    pub fn cpu_usage(&self) -> f64 {
42        self.maybe_update();
43        f64::from_bits(self.cpu_usage.load(Ordering::Relaxed)) / 100.0
44    }
45
46    /// Get current memory usage percentage (0-100)
47    pub fn memory_usage(&self) -> f64 {
48        self.maybe_update();
49        f64::from_bits(self.memory_usage.load(Ordering::Relaxed)) / 100.0
50    }
51
52    /// Get overall system load (0.0 - 1.0)
53    pub fn overall_load(&self) -> f64 {
54        let cpu = self.cpu_usage();
55        let mem = self.memory_usage();
56
57        // Weighted average: CPU 60%, Memory 40%
58        (cpu * 0.6 + mem * 0.4).min(1.0)
59    }
60
61    /// Check if system is under high load
62    pub fn is_high_load(&self, threshold: f64) -> bool {
63        self.overall_load() > threshold
64    }
65
66    /// Check if system is under low load
67    pub fn is_low_load(&self, threshold: f64) -> bool {
68        self.overall_load() < threshold
69    }
70
71    /// Get recommended concurrency level based on current load
72    pub fn recommended_concurrency(&self, max_concurrency: usize) -> usize {
73        let load = self.overall_load();
74
75        // Scale concurrency inversely with load
76        // At 0% load: use max concurrency
77        // At 50% load: use 75% of max
78        // At 80% load: use 40% of max
79        // At 90%+ load: use 25% of max
80
81        let scale_factor = if load < 0.5 {
82            1.0
83        } else if load < 0.7 {
84            0.75
85        } else if load < 0.8 {
86            0.5
87        } else if load < 0.9 {
88            0.4
89        } else {
90            0.25
91        };
92
93        ((max_concurrency as f64 * scale_factor).max(1.0) as usize).min(max_concurrency)
94    }
95
96    /// Update system metrics if needed
97    fn maybe_update(&self) {
98        let mut last_update = self.last_update.lock().expect("Lock poisoned");
99
100        if last_update.elapsed() < self.update_interval {
101            return; // Too soon to update
102        }
103
104        // Update timestamp
105        *last_update = Instant::now();
106        drop(last_update); // Release lock before potentially slow system calls
107
108        // Update CPU and memory metrics
109        if let Ok((cpu, memory)) = self.read_system_metrics() {
110            self.cpu_usage.store(cpu.to_bits(), Ordering::Relaxed);
111            self.memory_usage.store(memory.to_bits(), Ordering::Relaxed);
112        }
113    }
114
115    /// Read actual system metrics from OS
116    fn read_system_metrics(&self) -> Result<(f64, f64)> {
117        // Cross-platform system metrics using sysinfo would go here
118        // For now, use a lightweight fallback approach
119
120        #[cfg(target_os = "linux")]
121        {
122            self.read_linux_metrics()
123        }
124
125        #[cfg(target_os = "macos")]
126        {
127            self.read_macos_metrics()
128        }
129
130        #[cfg(target_os = "windows")]
131        {
132            self.read_windows_metrics()
133        }
134
135        #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
136        {
137            // Fallback: estimate based on available parallelism
138            Ok((50.0, 50.0)) // Conservative estimates
139        }
140    }
141
142    #[cfg(target_os = "linux")]
143    fn read_linux_metrics(&self) -> Result<(f64, f64)> {
144        // Read from /proc/stat for CPU
145        // Read from /proc/meminfo for memory
146        // Simplified implementation - production would use sysinfo crate
147
148        let cpu = self.estimate_cpu_from_loadavg()?;
149        let memory = self.estimate_memory_from_available()?;
150
151        Ok((cpu, memory))
152    }
153
154    #[cfg(target_os = "macos")]
155    fn read_macos_metrics(&self) -> Result<(f64, f64)> {
156        // Use host_statistics for CPU
157        // Use vm_stat for memory
158        // Simplified implementation
159
160        let cpu = 30.0; // Conservative estimate for macOS
161        let memory = 40.0;
162
163        Ok((cpu, memory))
164    }
165
166    #[cfg(target_os = "windows")]
167    fn read_windows_metrics(&self) -> Result<(f64, f64)> {
168        // Use Windows API for system metrics
169        // Simplified implementation
170
171        let cpu = 40.0;
172        let memory = 45.0;
173
174        Ok((cpu, memory))
175    }
176
177    #[cfg(target_os = "linux")]
178    fn estimate_cpu_from_loadavg(&self) -> Result<f64> {
179        // Read /proc/loadavg and normalize by number of cores
180        use std::fs;
181
182        if let Ok(loadavg) = fs::read_to_string("/proc/loadavg") {
183            if let Some(load_str) = loadavg.split_whitespace().next() {
184                if let Ok(load) = load_str.parse::<f64>() {
185                    let num_cpus = std::thread::available_parallelism()
186                        .map(|n| n.get())
187                        .unwrap_or(1) as f64;
188                    // Convert load average to percentage
189                    return Ok((load / num_cpus * 100.0).min(100.0));
190                }
191            }
192        }
193
194        Ok(30.0) // Fallback estimate
195    }
196
197    #[cfg(target_os = "linux")]
198    fn estimate_memory_from_available(&self) -> Result<f64> {
199        // Read /proc/meminfo
200        use std::fs;
201
202        if let Ok(meminfo) = fs::read_to_string("/proc/meminfo") {
203            let mut total = 0u64;
204            let mut available = 0u64;
205
206            for line in meminfo.lines() {
207                if line.starts_with("MemTotal:") {
208                    if let Some(val) = line.split_whitespace().nth(1) {
209                        total = val.parse().unwrap_or(0);
210                    }
211                } else if line.starts_with("MemAvailable:") {
212                    if let Some(val) = line.split_whitespace().nth(1) {
213                        available = val.parse().unwrap_or(0);
214                    }
215                }
216            }
217
218            if total > 0 {
219                let used = total.saturating_sub(available);
220                return Ok((used as f64 / total as f64 * 100.0).min(100.0));
221            }
222        }
223
224        Ok(50.0) // Fallback estimate
225    }
226}
227
228impl Default for SystemLoadMonitor {
229    fn default() -> Self {
230        Self::new()
231    }
232}
233
234/// Adaptive concurrency controller
235#[derive(Debug)]
236pub struct AdaptiveConcurrencyController {
237    /// System load monitor
238    monitor: SystemLoadMonitor,
239    /// Base maximum concurrency
240    max_concurrency: usize,
241    /// Current concurrency level
242    current_concurrency: Arc<AtomicU64>,
243    /// High load threshold (0.0 - 1.0)
244    high_load_threshold: f64,
245    /// Low load threshold (0.0 - 1.0)
246    low_load_threshold: f64,
247    /// Adjustment interval
248    adjustment_interval: Duration,
249    /// Last adjustment time
250    last_adjustment: Arc<std::sync::Mutex<Instant>>,
251}
252
253impl AdaptiveConcurrencyController {
254    /// Create new adaptive concurrency controller
255    pub fn new(max_concurrency: usize) -> Self {
256        Self {
257            monitor: SystemLoadMonitor::new(),
258            max_concurrency,
259            current_concurrency: Arc::new(AtomicU64::new(max_concurrency as u64)),
260            high_load_threshold: 0.75, // 75% system load
261            low_load_threshold: 0.40,  // 40% system load
262            adjustment_interval: Duration::from_secs(5),
263            last_adjustment: Arc::new(std::sync::Mutex::new(Instant::now())),
264        }
265    }
266
267    /// Get current recommended concurrency level
268    pub fn current_concurrency(&self) -> usize {
269        self.current_concurrency.load(Ordering::Relaxed) as usize
270    }
271
272    /// Update concurrency based on system load (call periodically)
273    pub fn update_concurrency(&self) {
274        let mut last_adj = self.last_adjustment.lock().expect("Lock poisoned");
275
276        if last_adj.elapsed() < self.adjustment_interval {
277            return; // Too soon to adjust
278        }
279
280        *last_adj = Instant::now();
281        drop(last_adj);
282
283        let load = self.monitor.overall_load();
284        let current = self.current_concurrency.load(Ordering::Relaxed) as usize;
285
286        let new_concurrency = if load > self.high_load_threshold {
287            // High load - reduce concurrency by 25%
288            ((current as f64 * 0.75).max(1.0) as usize).min(self.max_concurrency)
289        } else if load < self.low_load_threshold {
290            // Low load - increase concurrency by 25%
291            ((current as f64 * 1.25) as usize).min(self.max_concurrency)
292        } else {
293            // Moderate load - use recommended level
294            self.monitor.recommended_concurrency(self.max_concurrency)
295        };
296
297        self.current_concurrency
298            .store(new_concurrency as u64, Ordering::Relaxed);
299    }
300
301    /// Get system load monitor for detailed metrics
302    pub fn monitor(&self) -> &SystemLoadMonitor {
303        &self.monitor
304    }
305
306    /// Configure thresholds
307    pub fn with_thresholds(mut self, high: f64, low: f64) -> Self {
308        self.high_load_threshold = high.clamp(0.0, 1.0);
309        self.low_load_threshold = low.clamp(0.0, 1.0);
310        self
311    }
312
313    /// Configure adjustment interval
314    pub fn with_adjustment_interval(mut self, interval: Duration) -> Self {
315        self.adjustment_interval = interval;
316        self
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    #[test]
325    fn test_system_load_monitor_creation() {
326        let monitor = SystemLoadMonitor::new();
327
328        // CPU and memory should be valid percentages
329        let cpu = monitor.cpu_usage();
330        let mem = monitor.memory_usage();
331
332        assert!((0.0..=100.0).contains(&cpu));
333        assert!((0.0..=100.0).contains(&mem));
334    }
335
336    #[test]
337    fn test_overall_load_calculation() {
338        let monitor = SystemLoadMonitor::new();
339        let load = monitor.overall_load();
340
341        // Load should be between 0 and 1
342        assert!((0.0..=1.0).contains(&load));
343    }
344
345    #[test]
346    fn test_recommended_concurrency() {
347        let monitor = SystemLoadMonitor::new();
348        let rec = monitor.recommended_concurrency(16);
349
350        // Should recommend between 1 and max
351        assert!(rec >= 1);
352        assert!(rec <= 16);
353    }
354
355    #[test]
356    fn test_high_low_load_detection() {
357        let monitor = SystemLoadMonitor::new();
358
359        // These should be mutually exclusive
360        let is_high = monitor.is_high_load(0.75);
361        let is_low = monitor.is_low_load(0.40);
362
363        // Can't be both high and low simultaneously
364        if is_high {
365            assert!(!is_low);
366        }
367    }
368
369    #[test]
370    fn test_adaptive_concurrency_controller() {
371        let controller = AdaptiveConcurrencyController::new(16);
372
373        let initial = controller.current_concurrency();
374        assert_eq!(initial, 16);
375
376        // Update concurrency (should be safe to call)
377        controller.update_concurrency();
378
379        let after_update = controller.current_concurrency();
380        assert!(after_update >= 1);
381        assert!(after_update <= 16);
382    }
383
384    #[test]
385    fn test_concurrency_adjustment_interval() {
386        let controller = AdaptiveConcurrencyController::new(16);
387
388        // First update should work
389        controller.update_concurrency();
390        let first = controller.current_concurrency();
391
392        // Immediate second update should be ignored (too soon)
393        controller.update_concurrency();
394        let second = controller.current_concurrency();
395
396        assert_eq!(first, second, "Concurrency should not change immediately");
397    }
398
399    #[test]
400    fn test_threshold_configuration() {
401        let controller = AdaptiveConcurrencyController::new(16).with_thresholds(0.80, 0.30);
402
403        assert_eq!(controller.high_load_threshold, 0.80);
404        assert_eq!(controller.low_load_threshold, 0.30);
405    }
406
407    #[test]
408    fn test_adjustment_interval_configuration() {
409        let interval = Duration::from_secs(10);
410        let controller = AdaptiveConcurrencyController::new(16).with_adjustment_interval(interval);
411
412        assert_eq!(controller.adjustment_interval, interval);
413    }
414
415    #[test]
416    fn test_monitor_access() {
417        let controller = AdaptiveConcurrencyController::new(16);
418        let monitor = controller.monitor();
419
420        // Should be able to get detailed metrics
421        let cpu = monitor.cpu_usage();
422        let mem = monitor.memory_usage();
423
424        assert!((0.0..=100.0).contains(&cpu));
425        assert!((0.0..=100.0).contains(&mem));
426    }
427
428    #[test]
429    fn test_concurrency_bounds() {
430        let controller = AdaptiveConcurrencyController::new(8);
431
432        // Even with load adjustments, should stay within bounds
433        for _ in 0..10 {
434            controller.update_concurrency();
435            let current = controller.current_concurrency();
436            assert!(current >= 1, "Concurrency should never be zero");
437            assert!(current <= 8, "Concurrency should not exceed max");
438        }
439    }
440}