1use std::io;
15use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
16use std::time::{Duration, Instant};
17
18#[cfg(not(target_os = "linux"))]
19use sysinfo::{get_current_pid, CpuExt, ProcessExt, System, SystemExt};
20
21#[repr(align(64))]
26pub struct SystemHealth {
27 system_cpu: AtomicU32,
29 process_cpu: AtomicU32,
31 system_memory_mb: AtomicU64,
33 process_memory_mb: AtomicU64,
35 load_average: AtomicU32,
37 thread_count: AtomicU32,
39 fd_count: AtomicU32,
41 health_score: AtomicU32,
43 last_update: AtomicU64,
45 update_interval_ms: u64,
47 created_at: Instant,
49 #[cfg(not(target_os = "linux"))]
50 sys: std::sync::Mutex<System>,
51 #[cfg(not(target_os = "linux"))]
52 pid: Option<sysinfo::Pid>,
53}
54
55#[derive(Debug, Clone)]
57pub struct SystemSnapshot {
58 pub system_cpu_percent: f64,
60 pub process_cpu_percent: f64,
62 pub system_memory_mb: u64,
64 pub process_memory_mb: u64,
66 pub load_average: f64,
68 pub thread_count: u32,
70 pub fd_count: u32,
72 pub health_score: f64,
74 pub last_update: Duration,
76}
77
78#[derive(Debug, Clone)]
80pub struct ProcessStats {
81 pub cpu_percent: f64,
83 pub memory_mb: f64,
85 pub threads: u32,
87 pub file_handles: u32,
89 pub uptime: Duration,
91}
92
93impl SystemHealth {
94 #[inline]
96 pub fn new() -> Self {
97 let instance = Self {
98 system_cpu: AtomicU32::new(0),
99 process_cpu: AtomicU32::new(0),
100 system_memory_mb: AtomicU64::new(0),
101 process_memory_mb: AtomicU64::new(0),
102 load_average: AtomicU32::new(0),
103 thread_count: AtomicU32::new(0),
104 fd_count: AtomicU32::new(0),
105 health_score: AtomicU32::new(10000), last_update: AtomicU64::new(0),
107 update_interval_ms: 1000, created_at: Instant::now(),
109 #[cfg(not(target_os = "linux"))]
110 sys: std::sync::Mutex::new(System::new()),
111 #[cfg(not(target_os = "linux"))]
112 pid: get_current_pid().ok(),
113 };
114
115 instance.update_metrics();
117 instance
118 }
119
120 #[inline]
122 pub fn with_interval(interval: Duration) -> Self {
123 let mut instance = Self::new();
124 instance.update_interval_ms = interval.as_millis() as u64;
125 instance
126 }
127
128 #[inline]
130 pub fn cpu_used(&self) -> f64 {
131 self.maybe_update();
132 self.system_cpu.load(Ordering::Relaxed) as f64 / 100.0
133 }
134
135 #[inline]
137 pub fn cpu_free(&self) -> f64 {
138 100.0 - self.cpu_used()
139 }
140
141 #[inline]
143 pub fn mem_used_mb(&self) -> f64 {
144 self.maybe_update();
145 self.system_memory_mb.load(Ordering::Relaxed) as f64
146 }
147
148 #[inline]
150 pub fn mem_used_gb(&self) -> f64 {
151 self.mem_used_mb() / 1024.0
152 }
153
154 #[inline]
156 pub fn process_cpu_used(&self) -> f64 {
157 self.maybe_update();
158 self.process_cpu.load(Ordering::Relaxed) as f64 / 100.0
159 }
160
161 #[inline]
163 pub fn process_mem_used_mb(&self) -> f64 {
164 self.maybe_update();
165 self.process_memory_mb.load(Ordering::Relaxed) as f64
166 }
167
168 #[inline]
170 pub fn load_avg(&self) -> f64 {
171 self.maybe_update();
172 self.load_average.load(Ordering::Relaxed) as f64 / 100.0
173 }
174
175 #[inline]
177 pub fn thread_count(&self) -> u32 {
178 self.maybe_update();
179 self.thread_count.load(Ordering::Relaxed)
180 }
181
182 #[inline]
184 pub fn fd_count(&self) -> u32 {
185 self.maybe_update();
186 self.fd_count.load(Ordering::Relaxed)
187 }
188
189 #[inline]
191 pub fn health_score(&self) -> f64 {
192 self.maybe_update();
193 self.health_score.load(Ordering::Relaxed) as f64 / 100.0
194 }
195
196 #[inline(always)]
198 pub fn quick_check(&self) -> HealthStatus {
199 let score = self.health_score();
200
201 if score >= 80.0 {
202 HealthStatus::Healthy
203 } else if score >= 60.0 {
204 HealthStatus::Warning
205 } else if score >= 40.0 {
206 HealthStatus::Degraded
207 } else {
208 HealthStatus::Critical
209 }
210 }
211
212 #[inline]
214 pub fn update(&self) {
215 self.update_metrics();
216 }
217
218 pub fn snapshot(&self) -> SystemSnapshot {
220 self.maybe_update();
221
222 let last_update_ns = self.last_update.load(Ordering::Relaxed);
223 let last_update = if last_update_ns > 0 {
224 Duration::from_nanos(last_update_ns)
225 } else {
226 Duration::ZERO
227 };
228
229 SystemSnapshot {
230 system_cpu_percent: self.system_cpu.load(Ordering::Relaxed) as f64 / 100.0,
231 process_cpu_percent: self.process_cpu.load(Ordering::Relaxed) as f64 / 100.0,
232 system_memory_mb: self.system_memory_mb.load(Ordering::Relaxed),
233 process_memory_mb: self.process_memory_mb.load(Ordering::Relaxed),
234 load_average: self.load_average.load(Ordering::Relaxed) as f64 / 100.0,
235 thread_count: self.thread_count.load(Ordering::Relaxed),
236 fd_count: self.fd_count.load(Ordering::Relaxed),
237 health_score: self.health_score.load(Ordering::Relaxed) as f64 / 100.0,
238 last_update,
239 }
240 }
241
242 pub fn process(&self) -> ProcessStats {
244 self.maybe_update();
245
246 ProcessStats {
247 cpu_percent: self.process_cpu.load(Ordering::Relaxed) as f64 / 100.0,
248 memory_mb: self.process_memory_mb.load(Ordering::Relaxed) as f64,
249 threads: self.thread_count.load(Ordering::Relaxed),
250 file_handles: self.fd_count.load(Ordering::Relaxed),
251 uptime: self.created_at.elapsed(),
252 }
253 }
254
255 #[inline]
258 fn maybe_update(&self) {
259 let now = self.created_at.elapsed().as_millis() as u64;
260 let last_update = self.last_update.load(Ordering::Relaxed);
261
262 if now >= last_update && (now - last_update) > self.update_interval_ms {
263 self.update_metrics();
264 }
265 }
266
267 fn update_metrics(&self) {
268 let now_ns = self.created_at.elapsed().as_nanos() as u64;
269
270 if let Ok(cpu) = self.get_system_cpu() {
272 self.system_cpu
273 .store((cpu * 100.0) as u32, Ordering::Relaxed);
274 }
275
276 if let Ok(memory_mb) = self.get_system_memory_mb() {
277 self.system_memory_mb.store(memory_mb, Ordering::Relaxed);
278 }
279
280 if let Ok(load) = self.get_load_average() {
281 self.load_average
282 .store((load * 100.0) as u32, Ordering::Relaxed);
283 }
284
285 if let Ok(cpu) = self.get_process_cpu() {
287 self.process_cpu
288 .store((cpu * 100.0) as u32, Ordering::Relaxed);
289 }
290
291 if let Ok(memory_mb) = self.get_process_memory_mb() {
292 self.process_memory_mb.store(memory_mb, Ordering::Relaxed);
293 }
294
295 if let Ok(threads) = self.get_thread_count() {
296 self.thread_count.store(threads, Ordering::Relaxed);
297 }
298
299 if let Ok(fds) = self.get_fd_count() {
300 self.fd_count.store(fds, Ordering::Relaxed);
301 }
302
303 let health = self.calculate_health_score();
305 self.health_score
306 .store((health * 100.0) as u32, Ordering::Relaxed);
307
308 self.last_update.store(now_ns, Ordering::Relaxed);
309 }
310
311 fn calculate_health_score(&self) -> f64 {
312 let mut score: f64 = 100.0;
313
314 let system_cpu = self.system_cpu.load(Ordering::Relaxed) as f64 / 100.0;
316 if system_cpu > 80.0 {
317 score -= 30.0; } else if system_cpu > 60.0 {
319 score -= 15.0;
320 } else if system_cpu > 40.0 {
321 score -= 5.0;
322 }
323
324 let load = self.load_average.load(Ordering::Relaxed) as f64 / 100.0;
326 let cpu_count = num_cpus::get() as f64;
327 if load > cpu_count * 2.0 {
328 score -= 25.0;
329 } else if load > cpu_count * 1.5 {
330 score -= 10.0;
331 } else if load > cpu_count {
332 score -= 5.0;
333 }
334
335 let process_cpu = self.process_cpu.load(Ordering::Relaxed) as f64 / 100.0;
337 if process_cpu > 50.0 {
338 score -= 15.0;
339 } else if process_cpu > 25.0 {
340 score -= 8.0;
341 }
342
343 let memory_gb = self.system_memory_mb.load(Ordering::Relaxed) as f64 / 1024.0;
345 if memory_gb > 16.0 {
346 score -= 10.0;
348 } else if memory_gb > 8.0 {
349 score -= 5.0;
350 }
351
352 let threads = self.thread_count.load(Ordering::Relaxed);
354 if threads > 1000 {
355 score -= 20.0;
356 } else if threads > 500 {
357 score -= 10.0;
358 } else if threads > 200 {
359 score -= 5.0;
360 }
361
362 let fds = self.fd_count.load(Ordering::Relaxed);
364 if fds > 10000 {
365 score -= 15.0;
366 } else if fds > 5000 {
367 score -= 8.0;
368 } else if fds > 1000 {
369 score -= 3.0;
370 }
371
372 score.max(0.0)
373 }
374
375 #[cfg(target_os = "linux")]
378 fn get_system_cpu(&self) -> io::Result<f64> {
379 let contents = std::fs::read_to_string("/proc/stat")?;
380 if let Some(line) = contents.lines().next() {
381 let parts: Vec<&str> = line.split_whitespace().collect();
382 if parts.len() >= 5 && parts[0] == "cpu" {
383 let user: u64 = parts[1].parse().unwrap_or(0);
384 let nice: u64 = parts[2].parse().unwrap_or(0);
385 let system: u64 = parts[3].parse().unwrap_or(0);
386 let idle: u64 = parts[4].parse().unwrap_or(0);
387
388 let total = user + nice + system + idle;
389 let used = user + nice + system;
390
391 if total > 0 {
392 return Ok(used as f64 / total as f64 * 100.0);
393 }
394 }
395 }
396 Ok(0.0)
397 }
398
399 #[cfg(not(target_os = "linux"))]
400 fn get_system_cpu(&self) -> io::Result<f64> {
401 let mut guard = self.sys.lock().unwrap();
403 guard.refresh_cpu();
404 Ok(guard.global_cpu_info().cpu_usage() as f64)
405 }
406
407 #[cfg(target_os = "linux")]
408 fn get_system_memory_mb(&self) -> io::Result<u64> {
409 let contents = std::fs::read_to_string("/proc/meminfo")?;
410 let mut total_kb = 0u64;
411 let mut free_kb = 0u64;
412 let mut available_kb = 0u64;
413
414 for line in contents.lines() {
415 if line.starts_with("MemTotal:") {
416 total_kb = line
417 .split_whitespace()
418 .nth(1)
419 .and_then(|s| s.parse().ok())
420 .unwrap_or(0);
421 } else if line.starts_with("MemFree:") {
422 free_kb = line
423 .split_whitespace()
424 .nth(1)
425 .and_then(|s| s.parse().ok())
426 .unwrap_or(0);
427 } else if line.starts_with("MemAvailable:") {
428 available_kb = line
429 .split_whitespace()
430 .nth(1)
431 .and_then(|s| s.parse().ok())
432 .unwrap_or(0);
433 }
434 }
435
436 let used_kb = if available_kb > 0 {
438 total_kb - available_kb
439 } else {
440 total_kb - free_kb
441 };
442
443 Ok(used_kb / 1024) }
445
446 #[cfg(not(target_os = "linux"))]
447 fn get_system_memory_mb(&self) -> io::Result<u64> {
448 let mut guard = self.sys.lock().unwrap();
449 guard.refresh_memory();
450 let used_kib = guard.used_memory();
452 Ok(used_kib / 1024)
453 }
454
455 #[cfg(target_os = "linux")]
456 fn get_load_average(&self) -> io::Result<f64> {
457 let contents = std::fs::read_to_string("/proc/loadavg")?;
458 if let Some(first) = contents.split_whitespace().next() {
459 return first
460 .parse()
461 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid load average"));
462 }
463 Ok(0.0)
464 }
465
466 #[cfg(not(target_os = "linux"))]
467 fn get_load_average(&self) -> io::Result<f64> {
468 let guard = self.sys.lock().unwrap();
469 let la = guard.load_average();
470 Ok(la.one)
471 }
472
473 #[cfg(target_os = "linux")]
474 fn get_process_cpu(&self) -> io::Result<f64> {
475 let contents = std::fs::read_to_string("/proc/self/stat")?;
476 let parts: Vec<&str> = contents.split_whitespace().collect();
477
478 if parts.len() >= 15 {
479 let utime: u64 = parts[13].parse().unwrap_or(0);
480 let stime: u64 = parts[14].parse().unwrap_or(0);
481 let total = utime + stime;
482
483 Ok(total as f64 * 0.01) } else {
487 Ok(0.0)
488 }
489 }
490
491 #[cfg(not(target_os = "linux"))]
492 fn get_process_cpu(&self) -> io::Result<f64> {
493 let mut guard = self.sys.lock().unwrap();
494 if let Some(pid) = self.pid {
495 guard.refresh_process(pid);
496 if let Some(proc_) = guard.process(pid) {
497 let raw = proc_.cpu_usage() as f64;
500 let cores = num_cpus::get() as f64;
501 let norm = if cores > 0.0 { raw / cores } else { raw };
502 return Ok(norm.clamp(0.0, 100.0));
503 }
504 }
505 Ok(0.0)
506 }
507
508 #[cfg(target_os = "linux")]
509 fn get_process_memory_mb(&self) -> io::Result<u64> {
510 let contents = std::fs::read_to_string("/proc/self/status")?;
511 for line in contents.lines() {
512 if line.starts_with("VmRSS:") {
513 if let Some(kb_str) = line.split_whitespace().nth(1) {
514 if let Ok(kb) = kb_str.parse::<u64>() {
515 return Ok(kb / 1024); }
517 }
518 }
519 }
520 Ok(0)
521 }
522
523 #[cfg(not(target_os = "linux"))]
524 fn get_process_memory_mb(&self) -> io::Result<u64> {
525 let mut guard = self.sys.lock().unwrap();
526 if let Some(pid) = self.pid {
527 guard.refresh_process(pid);
528 if let Some(proc_) = guard.process(pid) {
529 return Ok(proc_.memory() / 1024);
531 }
532 }
533 Ok(0)
534 }
535
536 #[cfg(target_os = "linux")]
537 fn get_thread_count(&self) -> io::Result<u32> {
538 let contents = std::fs::read_to_string("/proc/self/status")?;
539 for line in contents.lines() {
540 if line.starts_with("Threads:") {
541 if let Some(count_str) = line.split_whitespace().nth(1) {
542 if let Ok(count) = count_str.parse() {
543 return Ok(count);
544 }
545 }
546 }
547 }
548 Ok(1) }
550
551 #[cfg(not(target_os = "linux"))]
552 fn get_thread_count(&self) -> io::Result<u32> {
553 Ok(1)
556 }
557
558 #[cfg(target_os = "linux")]
559 fn get_fd_count(&self) -> io::Result<u32> {
560 match std::fs::read_dir("/proc/self/fd") {
561 Ok(entries) => Ok(entries.count() as u32),
562 Err(_) => Ok(0),
563 }
564 }
565
566 #[cfg(not(target_os = "linux"))]
567 fn get_fd_count(&self) -> io::Result<u32> {
568 Ok(0)
570 }
571}
572
573#[derive(Debug, Clone, Copy, PartialEq, Eq)]
575pub enum HealthStatus {
576 Healthy,
578 Warning,
580 Degraded,
582 Critical,
584}
585
586impl HealthStatus {
587 #[inline]
589 pub fn is_degraded(&self) -> bool {
590 matches!(self, Self::Degraded | Self::Critical)
591 }
592
593 #[inline]
595 pub fn is_healthy(&self) -> bool {
596 matches!(self, Self::Healthy)
597 }
598
599 #[inline]
601 pub fn has_issues(&self) -> bool {
602 !matches!(self, Self::Healthy)
603 }
604}
605
606impl Default for SystemHealth {
607 fn default() -> Self {
608 Self::new()
609 }
610}
611
612impl std::fmt::Display for SystemHealth {
613 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
614 let snapshot = self.snapshot();
615 write!(
616 f,
617 "SystemHealth(CPU: {:.1}%, Mem: {} MB, Health: {:.1}%)",
618 snapshot.system_cpu_percent, snapshot.system_memory_mb, snapshot.health_score
619 )
620 }
621}
622
623impl std::fmt::Debug for SystemHealth {
624 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
625 let snapshot = self.snapshot();
626 f.debug_struct("SystemHealth")
627 .field("system_cpu", &snapshot.system_cpu_percent)
628 .field("process_cpu", &snapshot.process_cpu_percent)
629 .field("system_memory_mb", &snapshot.system_memory_mb)
630 .field("process_memory_mb", &snapshot.process_memory_mb)
631 .field("load_average", &snapshot.load_average)
632 .field("threads", &snapshot.thread_count)
633 .field("fds", &snapshot.fd_count)
634 .field("health_score", &snapshot.health_score)
635 .finish()
636 }
637}
638
639impl std::fmt::Display for HealthStatus {
640 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
641 match self {
642 Self::Healthy => write!(f, "Healthy"),
643 Self::Warning => write!(f, "Warning"),
644 Self::Degraded => write!(f, "Degraded"),
645 Self::Critical => write!(f, "Critical"),
646 }
647 }
648}
649
650unsafe impl Send for SystemHealth {}
652unsafe impl Sync for SystemHealth {}
653
654#[cfg(test)]
655mod tests {
656 use super::*;
657 use std::thread;
658
659 #[test]
660 fn test_basic_functionality() {
661 let health = SystemHealth::new();
662
663 let _cpu = health.cpu_used();
665 let _mem = health.mem_used_mb();
666 let _process_cpu = health.process_cpu_used();
667 let _process_mem = health.process_mem_used_mb();
668 let _load = health.load_avg();
669 let _threads = health.thread_count();
670 let _fds = health.fd_count();
671 let _score = health.health_score();
672
673 let status = health.quick_check();
675 assert!(matches!(
676 status,
677 HealthStatus::Healthy
678 | HealthStatus::Warning
679 | HealthStatus::Degraded
680 | HealthStatus::Critical
681 ));
682 }
683
684 #[test]
685 fn test_cpu_free() {
686 let health = SystemHealth::new();
687
688 let used = health.cpu_used();
689 let free = health.cpu_free();
690
691 assert!((used + free - 100.0).abs() < 0.1);
693 }
694
695 #[test]
696 fn test_memory_units() {
697 let health = SystemHealth::new();
698
699 let mb = health.mem_used_mb();
700 let gb = health.mem_used_gb();
701
702 if mb > 0.0 {
704 assert!((gb * 1024.0 - mb).abs() < 1.0);
705 }
706 }
707
708 #[test]
709 fn test_snapshot() {
710 let health = SystemHealth::new();
711
712 let snapshot = health.snapshot();
713
714 assert!(snapshot.system_cpu_percent >= 0.0);
716 assert!(snapshot.system_cpu_percent <= 100.0);
717 assert!(snapshot.health_score >= 0.0);
718 assert!(snapshot.health_score <= 100.0);
719 assert!(snapshot.thread_count > 0); }
721
722 #[test]
723 fn test_process_stats() {
724 let health = SystemHealth::new();
725
726 let stats = health.process();
727
728 assert!(stats.threads > 0); assert!(stats.uptime > Duration::ZERO);
730 assert!(stats.cpu_percent >= 0.0);
731 assert!(stats.memory_mb >= 0.0);
732 }
733
734 #[test]
735 fn test_health_status() {
736 let healthy = HealthStatus::Healthy;
737 let warning = HealthStatus::Warning;
738 let degraded = HealthStatus::Degraded;
739 let critical = HealthStatus::Critical;
740
741 assert!(healthy.is_healthy());
742 assert!(!healthy.is_degraded());
743 assert!(!healthy.has_issues());
744
745 assert!(!warning.is_healthy());
746 assert!(!warning.is_degraded());
747 assert!(warning.has_issues());
748
749 assert!(!degraded.is_healthy());
750 assert!(degraded.is_degraded());
751 assert!(degraded.has_issues());
752
753 assert!(!critical.is_healthy());
754 assert!(critical.is_degraded());
755 assert!(critical.has_issues());
756 }
757
758 #[test]
759 fn test_custom_interval() {
760 let health = SystemHealth::with_interval(Duration::from_millis(500));
761
762 let _cpu = health.cpu_used();
764 let _score = health.health_score();
765 }
766
767 #[test]
768 fn test_force_update() {
769 let health = SystemHealth::new();
770
771 let score_before = health.health_score();
772
773 health.update();
775
776 let score_after = health.health_score();
777
778 assert!(score_before >= 0.0);
780 assert!(score_after >= 0.0);
781 }
782
783 #[test]
784 fn test_concurrent_access() {
785 let health = std::sync::Arc::new(SystemHealth::new());
786 let mut handles = vec![];
787
788 for _ in 0..10 {
790 let health_clone = health.clone();
791 let handle = thread::spawn(move || {
792 for _ in 0..100 {
793 let _cpu = health_clone.cpu_used();
794 let _mem = health_clone.mem_used_mb();
795 let _status = health_clone.quick_check();
796 }
797 });
798 handles.push(handle);
799 }
800
801 for handle in handles {
803 handle.join().unwrap();
804 }
805
806 let final_score = health.health_score();
808 assert!((0.0..=100.0).contains(&final_score));
809 }
810
811 #[test]
812 fn test_display_formatting() {
813 let health = SystemHealth::new();
814
815 let display_str = format!("{health}");
816 assert!(display_str.contains("SystemHealth"));
817 assert!(display_str.contains("CPU"));
818 assert!(display_str.contains("Mem"));
819
820 let debug_str = format!("{health:?}");
821 assert!(debug_str.contains("SystemHealth"));
822
823 let status = health.quick_check();
824 let status_str = format!("{status}");
825 assert!(!status_str.is_empty());
826 }
827}
828
829#[cfg(all(test, feature = "bench-tests", not(tarpaulin)))]
830#[allow(unused_imports)]
831mod benchmarks {
832 use super::*;
833 use std::time::Instant;
834
835 #[cfg_attr(not(feature = "bench-tests"), ignore)]
836 #[test]
837 fn bench_quick_check() {
838 let health = SystemHealth::new();
839 let iterations = 1_000_000;
840
841 let start = Instant::now();
842 for _ in 0..iterations {
843 let _ = health.quick_check();
844 }
845 let elapsed = start.elapsed();
846
847 println!(
848 "SystemHealth quick_check: {:.2} ns/op",
849 elapsed.as_nanos() as f64 / iterations as f64
850 );
851
852 assert!(elapsed.as_nanos() / iterations < 200);
854 }
855
856 #[cfg_attr(not(feature = "bench-tests"), ignore)]
857 #[test]
858 fn bench_cached_metrics() {
859 let health = SystemHealth::new();
860 let iterations = 1_000_000;
861
862 let start = Instant::now();
863 for _ in 0..iterations {
864 let _ = health.cpu_used();
865 let _ = health.mem_used_mb();
866 let _ = health.health_score();
867 }
868 let elapsed = start.elapsed();
869
870 println!(
871 "SystemHealth cached metrics: {:.2} ns/op",
872 elapsed.as_nanos() as f64 / iterations as f64 / 3.0
873 );
874
875 assert!(elapsed.as_nanos() / iterations < 1000);
877 }
878
879 #[cfg_attr(not(feature = "bench-tests"), ignore)]
880 #[test]
881 fn bench_force_update() {
882 let health = SystemHealth::new();
883 let iterations = 1000; let start = Instant::now();
886 for _ in 0..iterations {
887 health.update();
888 }
889 let elapsed = start.elapsed();
890
891 println!(
892 "SystemHealth force update: {:.2} μs/op",
893 elapsed.as_micros() as f64 / iterations as f64
894 );
895
896 assert!(elapsed.as_millis() < 2000);
898 }
899}