1use chrono::{DateTime, Utc};
8use parking_lot::RwLock;
9use serde::{Deserialize, Serialize};
10use std::collections::VecDeque;
11use std::sync::Arc;
12use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
13use sysinfo::System;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct ResourceSnapshot {
18 pub timestamp: DateTime<Utc>,
20 pub cpu_percent: f32,
22 pub memory_used_mb: u64,
24 pub memory_total_mb: u64,
26 pub active_agents: usize,
28 pub pending_tasks: usize,
30 pub total_token_usage: u64,
32 pub disk_used_gb: f64,
34 pub load_avg_1m: f32,
36}
37
38#[derive(Debug, Clone, Copy)]
40pub struct OverloadThreshold {
41 pub cpu_percent: f32,
43 pub memory_percent: f32,
45 pub load_avg: f32,
47}
48
49impl Default for OverloadThreshold {
50 fn default() -> Self {
51 Self {
52 cpu_percent: 90.0,
53 memory_percent: 90.0,
54 load_avg: 8.0,
55 }
56 }
57}
58
59pub struct ResourceMonitor {
64 interval_secs: u64,
66 history_max: usize,
68 history: RwLock<VecDeque<ResourceSnapshot>>,
69 total_token_usage: AtomicU64,
70 active_agents: AtomicUsize,
71 pending_tasks: AtomicUsize,
72 overload_threshold: RwLock<OverloadThreshold>,
73 sys: parking_lot::Mutex<System>,
74 disk_cache: parking_lot::Mutex<Option<(std::time::Instant, f64)>>,
78}
79
80impl Default for ResourceMonitor {
81 fn default() -> Self {
82 Self::new(60, 60)
83 }
84}
85
86impl ResourceMonitor {
87 pub fn new(interval_secs: u64, history_max: usize) -> Self {
89 Self {
90 interval_secs,
91 history_max,
92 history: RwLock::new(VecDeque::with_capacity(history_max)),
93 total_token_usage: AtomicU64::new(0),
94 active_agents: AtomicUsize::new(0),
95 pending_tasks: AtomicUsize::new(0),
96 overload_threshold: RwLock::new(OverloadThreshold::default()),
97 sys: parking_lot::Mutex::new(System::new_all()),
98 disk_cache: parking_lot::Mutex::new(None),
99 }
100 }
101
102 pub fn snapshot(&self) -> ResourceSnapshot {
107 let mut sys = self.sys.lock();
108 sys.refresh_all();
109
110 let cpu_percent =
112 sys.cpus().iter().map(|c| c.cpu_usage()).sum::<f32>() / sys.cpus().len().max(1) as f32;
113
114 let total_memory = sys.total_memory();
115 let used_memory = sys.used_memory();
116 let memory_total_mb = total_memory / (1024 * 1024);
117 let memory_used_mb = used_memory / (1024 * 1024);
118
119 let load_avg_1m = System::load_average().one as f32;
120
121 let disk_used_gb = self.cached_disk_usage();
122
123 ResourceSnapshot {
124 timestamp: Utc::now(),
125 cpu_percent,
126 memory_used_mb,
127 memory_total_mb,
128 active_agents: self.active_agents.load(Ordering::Relaxed),
129 pending_tasks: self.pending_tasks.load(Ordering::Relaxed),
130 total_token_usage: self.total_token_usage.load(Ordering::Relaxed),
131 disk_used_gb,
132 load_avg_1m,
133 }
134 }
135
136 fn cached_disk_usage(&self) -> f64 {
141 const DISK_CACHE_TTL_SECS: u64 = 300;
142 let now = std::time::Instant::now();
143 {
144 let cache = self.disk_cache.lock();
145 if let Some((ts, val)) = *cache
146 && now.duration_since(ts).as_secs() < DISK_CACHE_TTL_SECS
147 {
148 return val;
149 }
150 }
151 let cwd = std::env::current_dir().unwrap_or_default();
152 let gb = walk_dir_size(&cwd) as f64 / (1024.0 * 1024.0 * 1024.0);
153 *self.disk_cache.lock() = Some((now, gb));
154 gb
155 }
156
157 pub fn record_snapshot(&self) {
162 let snap = self.snapshot();
163 let mut history = self.history.write();
164 if history.len() >= self.history_max {
165 history.pop_front();
166 }
167 history.push_back(snap);
168 }
169
170 pub fn start_sampling(self: &Arc<Self>) -> tokio::task::JoinHandle<()> {
175 let monitor = Arc::clone(self);
176 let interval = self.interval_secs;
177 tokio::spawn(async move {
178 let mut ticker = tokio::time::interval(std::time::Duration::from_secs(interval));
179 loop {
180 ticker.tick().await;
181 let m = Arc::clone(&monitor);
184 let _ = tokio::task::spawn_blocking(move || m.record_snapshot()).await;
185 }
186 })
187 }
188
189 pub fn history(&self, last_n: usize) -> Vec<ResourceSnapshot> {
191 let guard = self.history.read();
192 let n = last_n.min(guard.len());
193 guard.iter().rev().take(n).cloned().collect()
194 }
195
196 pub fn is_overloaded(&self) -> bool {
198 let snap = self.snapshot();
199 let memory_percent = if snap.memory_total_mb > 0 {
200 (snap.memory_used_mb as f32 / snap.memory_total_mb as f32) * 100.0
201 } else {
202 0.0
203 };
204
205 let t = self.overload_threshold.read();
206 snap.cpu_percent >= t.cpu_percent
207 || memory_percent >= t.memory_percent
208 || snap.load_avg_1m >= t.load_avg
209 }
210
211 pub fn set_active_agents(&self, count: usize) {
213 self.active_agents.store(count, Ordering::Relaxed);
214 }
215
216 pub fn set_pending_tasks(&self, count: usize) {
218 self.pending_tasks.store(count, Ordering::Relaxed);
219 }
220
221 pub fn add_token_usage(&self, tokens: u64) {
223 self.total_token_usage.fetch_add(tokens, Ordering::Relaxed);
224 }
225
226 pub fn overload_threshold(&self) -> OverloadThreshold {
228 *self.overload_threshold.read()
229 }
230
231 pub fn set_overload_threshold(&self, threshold: OverloadThreshold) {
233 *self.overload_threshold.write() = threshold;
234 tracing::info!("ResourceMonitor thresholds hot-reloaded");
235 }
236}
237
238const DISK_WALK_MAX_DEPTH: u8 = 10;
241const DISK_WALK_MAX_ENTRIES: usize = 200_000;
244
245fn walk_dir_size(path: &std::path::Path) -> u64 {
254 fn walk(path: &std::path::Path, depth: u8) -> u64 {
255 if depth >= DISK_WALK_MAX_DEPTH {
256 return 0;
257 }
258 let mut total = 0u64;
259 let Ok(entries) = std::fs::read_dir(path) else {
260 return 0;
261 };
262 for (visited, entry) in entries.flatten().enumerate() {
263 if visited >= DISK_WALK_MAX_ENTRIES {
264 break;
265 }
266 let Ok(m) = std::fs::symlink_metadata(entry.path()) else {
268 continue;
269 };
270 if m.is_file() {
271 total += m.len();
272 } else if m.is_dir() {
273 const SKIP_DIRS: &[&str] = &["target", "node_modules", ".git", ".next", "dist"];
276 if SKIP_DIRS.iter().any(|s| entry.file_name() == *s) {
277 continue;
278 }
279 total += walk(&entry.path(), depth + 1);
280 }
281 }
282 total
283 }
284 walk(path, 0)
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn test_snapshot_structure() {
293 let monitor = ResourceMonitor::default();
294 let snap = monitor.snapshot();
295
296 assert!(snap.timestamp <= Utc::now());
297 assert!(snap.cpu_percent >= 0.0);
299 assert!(snap.disk_used_gb >= 0.0);
300 assert!(snap.load_avg_1m >= 0.0);
301 }
302
303 #[test]
304 fn test_is_overloaded_default_threshold() {
305 let monitor = ResourceMonitor::default();
306 let _ = monitor.is_overloaded();
310 }
311
312 #[test]
313 fn test_is_overloaded_high_thresholds_not_overloaded() {
314 let monitor = ResourceMonitor::default();
317 let result = monitor.is_overloaded();
320 let _ = result;
323 }
324
325 #[test]
326 fn test_history_management() {
327 let monitor = ResourceMonitor::new(1, 5);
328
329 assert!(monitor.history(10).is_empty());
331
332 for _ in 0..3 {
334 monitor.record_snapshot();
335 }
336
337 let history = monitor.history(10);
339 assert_eq!(history.len(), 3);
340 }
341
342 #[test]
343 fn test_history_eviction() {
344 let monitor = ResourceMonitor::new(1, 3);
345
346 for _ in 0..5 {
348 monitor.record_snapshot();
349 }
350
351 let history = monitor.history(10);
353 assert_eq!(history.len(), 3);
354 }
355
356 #[test]
357 fn test_set_active_agents() {
358 let monitor = ResourceMonitor::default();
359 monitor.set_active_agents(5);
360 let snap = monitor.snapshot();
361 assert_eq!(snap.active_agents, 5);
362 }
363
364 #[test]
365 fn test_set_pending_tasks() {
366 let monitor = ResourceMonitor::default();
367 monitor.set_pending_tasks(3);
368 let snap = monitor.snapshot();
369 assert_eq!(snap.pending_tasks, 3);
370 }
371
372 #[test]
373 fn test_add_token_usage() {
374 let monitor = ResourceMonitor::default();
375 monitor.add_token_usage(100);
376 monitor.add_token_usage(200);
377 let snap = monitor.snapshot();
378 assert_eq!(snap.total_token_usage, 300);
379 }
380
381 #[test]
382 fn test_overload_threshold_default() {
383 let threshold = OverloadThreshold::default();
384 assert_eq!(threshold.cpu_percent, 90.0);
385 assert_eq!(threshold.memory_percent, 90.0);
386 assert_eq!(threshold.load_avg, 8.0);
387 }
388
389 #[test]
390 fn test_overload_threshold_custom() {
391 let threshold = OverloadThreshold {
392 cpu_percent: 75.0,
393 memory_percent: 80.0,
394 load_avg: 4.0,
395 };
396 assert_eq!(threshold.cpu_percent, 75.0);
397 assert_eq!(threshold.memory_percent, 80.0);
398 assert_eq!(threshold.load_avg, 4.0);
399 }
400
401 #[test]
402 fn test_history_last_n() {
403 let monitor = ResourceMonitor::new(1, 10);
404 let empty = monitor.history(5);
405 assert!(empty.is_empty());
406
407 let many = monitor.history(100);
408 assert!(many.is_empty());
409 }
410
411 #[test]
412 fn test_load_average_struct() {
413 let la = System::load_average();
414 assert!(la.one >= 0.0);
415 assert!(la.five >= 0.0);
416 assert!(la.fifteen >= 0.0);
417 }
418}