1#[allow(unused_imports)]
68use crate::error::{Error, Result};
69#[cfg(feature = "metrics")]
70use crate::metrics::MetricsCollector;
71use arc_swap::ArcSwap;
72use std::collections::VecDeque;
73use std::sync::{Arc, RwLock};
74use std::time::{Duration, Instant};
75
76#[cfg(all(feature = "async-std", not(feature = "tokio")))]
78#[allow(unused_imports)]
79use async_std::task::JoinHandle as AsyncJoinHandle;
80#[cfg(not(any(feature = "tokio", feature = "async-std")))]
81#[cfg(feature = "tokio")]
82#[allow(unused_imports)]
83use tokio::task::JoinHandle;
84#[cfg(feature = "tokio")]
85#[allow(unused_imports)]
86use tokio::time;
87
88#[cfg(target_os = "linux")]
90use std::fs::File;
91#[cfg(target_os = "linux")]
92use std::io::{BufRead, BufReader};
93#[cfg(target_os = "linux")]
94use std::num::NonZeroI64;
95
96#[cfg(target_os = "macos")]
97use std::process::Command;
98
99#[cfg(all(target_os = "windows", feature = "windows-monitoring"))]
100use windows::Win32::System::Diagnostics::ToolHelp::{
101 CreateToolhelp32Snapshot, Thread32First, Thread32Next, TH32CS_SNAPTHREAD, THREADENTRY32,
102};
103#[cfg(all(target_os = "windows", feature = "windows-monitoring"))]
104use windows::Win32::System::ProcessStatus::{GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS};
105#[cfg(all(target_os = "windows", feature = "windows-monitoring"))]
106use windows::Win32::System::Threading::{GetProcessTimes, OpenProcess, PROCESS_QUERY_INFORMATION};
107
108#[cfg(all(target_os = "windows", feature = "windows-monitoring"))]
109use windows::Win32::Foundation::{CloseHandle, FILETIME};
110
111#[derive(Debug, Clone)]
113pub struct ResourceUsage {
114 timestamp: Instant,
116
117 memory_bytes: u64,
119
120 cpu_percent: f64,
122
123 thread_count: u32,
125}
126
127#[derive(Debug, Clone)]
129pub enum Alert {
130 MemorySoftLimit {
132 limit_bytes: u64,
134 current_bytes: u64,
136 },
137}
138
139impl ResourceUsage {
140 #[must_use]
142 pub fn new(memory_bytes: u64, cpu_percent: f64, thread_count: u32) -> Self {
143 Self {
144 timestamp: Instant::now(),
145 memory_bytes,
146 cpu_percent,
147 thread_count,
148 }
149 }
150
151 #[must_use]
153 pub const fn memory_bytes(&self) -> u64 {
154 self.memory_bytes
155 }
156
157 #[must_use]
159 #[allow(clippy::cast_precision_loss)]
160 pub fn memory_mb(&self) -> f64 {
161 self.memory_bytes as f64 / 1_048_576.0
163 }
164
165 #[must_use]
167 pub const fn cpu_percent(&self) -> f64 {
168 self.cpu_percent
169 }
170
171 #[must_use]
173 pub const fn thread_count(&self) -> u32 {
174 self.thread_count
175 }
176
177 #[must_use]
179 pub fn age(&self) -> Duration {
180 self.timestamp.elapsed()
181 }
182}
183
184#[allow(dead_code)]
186pub struct ResourceTracker {
187 sample_interval: Duration,
189
190 current_usage: Arc<ArcSwap<ResourceUsage>>,
192
193 history: Arc<RwLock<VecDeque<ResourceUsage>>>,
195
196 max_history: usize,
198
199 #[cfg(feature = "tokio")]
201 task_handle: Option<tokio::task::JoinHandle<()>>,
202 #[cfg(all(feature = "async-std", not(feature = "tokio")))]
203 task_handle: Option<async_std::task::JoinHandle<()>>,
204 #[cfg(not(any(feature = "tokio", feature = "async-std")))]
205 task_handle: Option<std::thread::JoinHandle<()>>,
206
207 pid: u32,
209
210 memory_soft_limit_bytes: Option<u64>,
212
213 #[allow(clippy::type_complexity)]
215 on_alert: Option<Arc<dyn Fn(Alert) + Send + Sync + 'static>>,
216
217 #[cfg(feature = "metrics")]
219 metrics: Option<MetricsCollector>,
220}
221
222impl ResourceTracker {
223 #[must_use]
231 pub fn new(sample_interval: Duration) -> Self {
232 let initial_usage = ResourceUsage::new(0, 0.0, 0);
234 let current_pid = std::process::id();
235
236 Self {
237 sample_interval,
238 current_usage: Arc::new(ArcSwap::from_pointee(initial_usage)),
239 history: Arc::new(RwLock::new(VecDeque::new())),
240 max_history: 60, task_handle: None,
242 pid: current_pid,
243 memory_soft_limit_bytes: None,
244 on_alert: None,
245 #[cfg(feature = "metrics")]
246 metrics: None,
247 }
248 }
249
250 #[must_use]
257 pub const fn with_pid(mut self, pid: u32) -> Self {
258 self.pid = pid;
259 self
260 }
261
262 #[must_use]
264 pub const fn with_max_history(mut self, max_entries: usize) -> Self {
265 self.max_history = max_entries;
266 self
267 }
268
269 #[must_use]
271 pub const fn with_memory_soft_limit_bytes(mut self, bytes: u64) -> Self {
272 self.memory_soft_limit_bytes = Some(bytes);
273 self
274 }
275
276 #[must_use]
278 pub fn with_alert_handler<F>(mut self, f: F) -> Self
279 where
280 F: Fn(Alert) + Send + Sync + 'static,
281 {
282 self.on_alert = Some(Arc::new(f));
283 self
284 }
285
286 #[cfg(feature = "metrics")]
288 #[must_use]
289 pub fn with_metrics(mut self, metrics: MetricsCollector) -> Self {
290 self.metrics = Some(metrics);
291 self
292 }
293
294 #[must_use]
298 pub fn with_alert_to_tracing(mut self) -> Self {
299 self.on_alert = Some(Arc::new(|alert| match alert {
300 Alert::MemorySoftLimit {
301 limit_bytes,
302 current_bytes,
303 } => {
304 tracing::warn!(
305 target: "proc_daemon::resources",
306 limit_bytes,
307 current_bytes,
308 "Resource alert: soft memory limit exceeded"
309 );
310 }
311 }));
312 self
313 }
314
315 #[cfg(all(feature = "tokio", not(feature = "async-std")))]
330 pub fn start(&mut self) -> Result<()> {
331 if self.task_handle.is_some() {
332 return Ok(()); }
334
335 let sample_interval = self.sample_interval;
336 let usage_history = Arc::clone(&self.history);
337 let current_usage = Arc::clone(&self.current_usage);
338 let pid = self.pid;
339 let max_history = self.max_history;
340 let memory_soft_limit_bytes = self.memory_soft_limit_bytes;
341 let on_alert = self.on_alert.clone();
342 #[cfg(feature = "metrics")]
343 let metrics = self.metrics.clone();
344
345 let handle = tokio::spawn(async move {
346 let mut interval_timer = time::interval(sample_interval);
347 let mut last_cpu_time = 0.0;
348 let mut last_timestamp = Instant::now();
349 #[cfg(feature = "metrics")]
350 let mut last_tick = Instant::now();
351
352 loop {
353 interval_timer.tick().await;
354 #[cfg(feature = "metrics")]
355 let tick_now = Instant::now();
356
357 if let Ok(usage) =
359 Self::sample_resource_usage(pid, &mut last_cpu_time, &mut last_timestamp)
360 {
361 current_usage.store(Arc::new(usage.clone()));
363
364 {
366 let mut hist = usage_history.write().unwrap();
367 hist.push_back(usage.clone());
368 while hist.len() > max_history {
370 hist.pop_front();
371 }
372 drop(hist); }
374
375 if let Some(limit) = memory_soft_limit_bytes {
377 if usage.memory_bytes() > limit {
378 if let Some(cb) = on_alert.as_ref() {
379 cb(Alert::MemorySoftLimit {
380 limit_bytes: limit,
381 current_bytes: usage.memory_bytes(),
382 });
383 }
384 }
385 }
386
387 #[cfg(feature = "metrics")]
389 if let Some(m) = metrics.as_ref() {
390 m.set_gauge("proc.memory_bytes", usage.memory_bytes());
391 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
392 let cpu_milli = (usage.cpu_percent() * 1000.0).max(0.0).round() as u64;
393 m.set_gauge("proc.cpu_milli_percent", cpu_milli);
394 m.set_gauge("proc.thread_count", u64::from(usage.thread_count()));
395 m.increment_counter("proc.samples_total", 1);
396 m.record_histogram(
397 "proc.sample_interval",
398 tick_now.saturating_duration_since(last_tick),
399 );
400 last_tick = tick_now;
401 }
402 }
403 }
404 });
405
406 self.task_handle = Some(handle);
407 Ok(())
408 }
409
410 #[cfg(all(feature = "async-std", not(feature = "tokio")))]
412 #[allow(clippy::missing_errors_doc)]
413 pub fn start(&mut self) -> Result<()> {
414 if self.task_handle.is_some() {
415 return Ok(()); }
417
418 let sample_interval = self.sample_interval;
419 let usage_history = Arc::clone(&self.history);
420 let current_usage = Arc::clone(&self.current_usage);
421 let pid = self.pid;
422 let max_history = self.max_history; let memory_soft_limit_bytes = self.memory_soft_limit_bytes;
424 let on_alert = self.on_alert.clone();
425 #[cfg(feature = "metrics")]
426 let metrics = self.metrics.clone();
427
428 let handle = async_std::task::spawn(async move {
429 let mut last_cpu_time = 0.0;
430 let mut last_timestamp = Instant::now();
431 #[cfg(feature = "metrics")]
432 let mut last_tick = Instant::now();
433
434 loop {
435 async_std::task::sleep(sample_interval).await;
436 #[cfg(feature = "metrics")]
437 let tick_now = Instant::now();
438
439 if let Ok(usage) =
441 Self::sample_resource_usage(pid, &mut last_cpu_time, &mut last_timestamp)
442 {
443 let new_arc = Arc::new(usage.clone());
446 current_usage.store(new_arc);
447
448 {
450 let mut hist = usage_history.write().unwrap();
451 hist.push_back(usage.clone());
452 while hist.len() > max_history {
454 hist.pop_front();
455 }
456 } if let Some(limit) = memory_soft_limit_bytes {
460 if usage.memory_bytes() > limit {
461 if let Some(cb) = on_alert.as_ref() {
462 cb(Alert::MemorySoftLimit {
463 limit_bytes: limit,
464 current_bytes: usage.memory_bytes(),
465 });
466 }
467 }
468 }
469
470 #[cfg(feature = "metrics")]
472 if let Some(m) = metrics.as_ref() {
473 m.set_gauge("proc.memory_bytes", usage.memory_bytes());
474 let cpu_milli = (usage.cpu_percent() * 1000.0).max(0.0).round() as u64;
475 m.set_gauge("proc.cpu_milli_percent", cpu_milli);
476 m.set_gauge("proc.thread_count", u64::from(usage.thread_count()));
477 m.increment_counter("proc.samples_total", 1);
478 m.record_histogram(
479 "proc.sample_interval",
480 tick_now.saturating_duration_since(last_tick),
481 );
482 }
483 #[cfg(feature = "metrics")]
484 {
485 last_tick = tick_now;
486 }
487 }
488 }
489 });
490
491 self.task_handle = Some(handle);
492 Ok(())
493 }
494
495 #[cfg(all(feature = "tokio", not(feature = "async-std")))]
499 pub async fn stop(&mut self) {
500 if let Some(handle) = self.task_handle.take() {
501 handle.abort();
502 let _ = handle.await;
503 }
504 }
505
506 #[cfg(all(feature = "async-std", not(feature = "tokio")))]
510 pub fn stop(&mut self) {
511 self.task_handle.take();
513 }
514
515 #[must_use]
517 pub fn current_usage(&self) -> ResourceUsage {
518 self.current_usage.load_full().as_ref().clone()
519 }
520
521 #[must_use]
523 pub fn history(&self) -> Vec<ResourceUsage> {
524 self.history
525 .read()
526 .map_or_else(|_| Vec::new(), |history| history.iter().cloned().collect())
527 }
528
529 #[allow(unused_variables, dead_code)]
531 #[allow(clippy::needless_pass_by_ref_mut)]
532 fn sample_resource_usage(
533 pid: u32,
534 last_cpu_time: &mut f64,
535 last_timestamp: &mut Instant,
536 ) -> Result<ResourceUsage> {
537 #[cfg(target_os = "linux")]
538 {
539 let memory = Self::get_memory_linux(pid)?;
541 let (cpu, threads) = Self::get_cpu_linux(pid, last_cpu_time, last_timestamp)?;
542 Ok(ResourceUsage::new(memory, cpu, threads))
543 }
544
545 #[cfg(target_os = "macos")]
546 {
547 let memory = Self::get_memory_macos(pid)?;
549 let (cpu, threads) = Self::get_cpu_macos(pid)?;
550 Ok(ResourceUsage::new(memory, cpu, threads))
551 }
552
553 #[cfg(target_os = "windows")]
554 {
555 let memory = Self::get_memory_windows(pid)?;
557 let (cpu, threads) = Self::get_cpu_windows(pid, last_cpu_time, last_timestamp)?;
558 Ok(ResourceUsage::new(memory, cpu, threads))
559 }
560
561 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
562 {
563 Ok(ResourceUsage::new(0, 0.0, 0))
565 }
566 }
567
568 #[cfg(target_os = "linux")]
569 fn get_memory_linux(pid: u32) -> Result<u64> {
570 let path = format!("/proc/{pid}/status");
572 let file = File::open(&path).map_err(|e| {
573 Error::io_with_source(format!("Failed to open {path} for memory stats"), e)
574 })?;
575
576 let reader = BufReader::new(file);
577 let mut memory_bytes = 0;
578
579 for line in reader.lines() {
580 let line = line.map_err(|e| {
581 Error::io_with_source("Failed to read process memory stats".to_string(), e)
582 })?;
583
584 if line.starts_with("VmRSS:") {
586 let parts: Vec<&str> = line.split_whitespace().collect();
587 if !parts.is_empty() {
588 if let Ok(kb) = parts[1].parse::<u64>() {
589 memory_bytes = kb * 1024;
590 break;
591 }
592 }
593 }
594 }
595
596 Ok(memory_bytes)
597 }
598
599 #[cfg(target_os = "linux")]
600 #[allow(
601 clippy::cast_precision_loss,
602 clippy::cast_possible_truncation,
603 clippy::similar_names
604 )]
605 fn get_cpu_linux(
606 pid: u32,
607 last_cpu_time: &mut f64,
608 last_timestamp: &mut Instant,
609 ) -> Result<(f64, u32)> {
610 let path = format!("/proc/{pid}/stat");
612 let file = File::open(&path).map_err(|e| {
613 Error::io_with_source(format!("Failed to open {path} for CPU stats"), e)
614 })?;
615
616 let reader = BufReader::new(file);
617 let mut cpu_percent = 0.0;
618 let mut thread_count: u32 = 0;
619
620 if let Ok(line) = reader.lines().next().ok_or_else(|| {
621 Error::runtime("Failed to read CPU stats from proc filesystem".to_string())
622 }) {
623 let line = line.map_err(|e| {
624 Error::io_with_source("Failed to read process CPU stats".to_string(), e)
625 })?;
626
627 if let Some((cpu_time, threads)) = Self::parse_proc_stat(&line) {
628 thread_count = threads;
629
630 let now = Instant::now();
631 if *last_timestamp != now {
632 let time_diff = now.duration_since(*last_timestamp).as_secs_f64();
633 if time_diff > 0.0 {
634 let num_cores = num_cpus::get() as f64;
635 let cpu_time_diff = cpu_time - *last_cpu_time;
636 let ticks = Self::linux_clk_tck();
637
638 cpu_percent = (cpu_time_diff / ticks) / time_diff * 100.0 / num_cores;
639 }
640 }
641
642 *last_cpu_time = cpu_time;
643 *last_timestamp = now;
644 }
645 }
646
647 Ok((cpu_percent, thread_count))
648 }
649
650 #[cfg(target_os = "linux")]
651 fn parse_proc_stat(line: &str) -> Option<(f64, u32)> {
652 let open = line.find('(')?;
653 let close = line.rfind(')')?;
654 if close <= open {
655 return None;
656 }
657
658 let rest = line.get((close + 1)..)?;
660 let parts: Vec<&str> = rest.split_whitespace().collect();
661 if parts.len() <= 17 {
663 return None;
664 }
665
666 let utime = parts.get(11)?.parse::<f64>().unwrap_or(0.0);
667 let stime = parts.get(12)?.parse::<f64>().unwrap_or(0.0);
668 let child_user_time = parts.get(13)?.parse::<f64>().unwrap_or(0.0);
669 let child_system_time = parts.get(14)?.parse::<f64>().unwrap_or(0.0);
670 let thread_count = parts.get(17)?.parse::<u32>().unwrap_or(0);
671
672 let current_cpu_time = utime + stime + child_user_time + child_system_time;
673 Some((current_cpu_time, thread_count))
674 }
675
676 #[cfg(target_os = "linux")]
677 #[allow(clippy::cast_precision_loss)]
678 #[allow(unsafe_code)]
679 fn linux_clk_tck() -> f64 {
680 #[cfg_attr(not(target_os = "linux"), allow(unused_unsafe))]
684 let ticks = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
685 NonZeroI64::new(ticks).map_or(100.0, |v| v.get() as f64)
686 }
687
688 #[cfg(target_os = "macos")]
689 #[allow(dead_code)]
690 fn get_memory_macos(pid: u32) -> Result<u64> {
691 let output = Command::new("/bin/ps")
693 .args(["-xo", "rss=", "-p", &pid.to_string()])
694 .output()
695 .map_err(|e| {
696 Error::io_with_source(
697 "Failed to execute ps command for memory stats".to_string(),
698 e,
699 )
700 })?;
701
702 let memory_kb = String::from_utf8_lossy(&output.stdout)
703 .trim()
704 .parse::<u64>()
705 .unwrap_or(0);
706
707 Ok(memory_kb * 1024)
708 }
709
710 #[cfg(target_os = "macos")]
711 #[allow(dead_code)]
712 fn get_cpu_macos(pid: u32) -> Result<(f64, u32)> {
713 let output = Command::new("/bin/ps")
715 .args(["-xo", "%cpu,thcount=", "-p", &pid.to_string()])
716 .output()
717 .map_err(|e| {
718 Error::io_with_source("Failed to execute ps command for CPU stats".to_string(), e)
719 })?;
720
721 let stats = String::from_utf8_lossy(&output.stdout);
722 let parts: Vec<&str> = stats.split_whitespace().collect();
723
724 let cpu_percent = if parts.is_empty() {
725 0.0
726 } else {
727 parts[0].parse::<f64>().unwrap_or(0.0)
728 };
729
730 let thread_count = if parts.len() > 1 {
731 parts[1].parse::<u32>().unwrap_or(0)
732 } else {
733 0
734 };
735
736 Ok((cpu_percent, thread_count))
737 }
738
739 #[cfg(all(target_os = "windows", feature = "windows-monitoring"))]
740 #[allow(unsafe_code)]
741 fn get_memory_windows(pid: u32) -> Result<u64> {
742 let mut pmc = PROCESS_MEMORY_COUNTERS::default();
743 let handle =
744 unsafe { OpenProcess(PROCESS_QUERY_INFORMATION, false, pid) }.map_err(|e| {
745 Error::runtime_with_source(
746 format!("Failed to open process {} for memory stats", pid),
747 e,
748 )
749 })?;
750
751 let result = unsafe {
752 GetProcessMemoryInfo(
753 handle,
754 &mut pmc,
755 std::mem::size_of::<PROCESS_MEMORY_COUNTERS>() as u32,
756 )
757 }
758 .map_err(|e| {
759 Error::runtime_with_source("Failed to get process memory info".to_string(), e)
760 });
761
762 unsafe { CloseHandle(handle) };
763 result?;
764
765 Ok(u64::try_from(pmc.WorkingSetSize).unwrap_or(pmc.WorkingSetSize as u64))
766 }
767
768 #[cfg(all(target_os = "windows", not(feature = "windows-monitoring")))]
769 fn get_memory_windows(_pid: u32) -> Result<u64> {
770 Err(Error::runtime(
771 "Windows monitoring not enabled. Enable the 'windows-monitoring' feature".to_string(),
772 ))
773 }
774
775 #[cfg(all(target_os = "windows", feature = "windows-monitoring"))]
776 #[allow(unsafe_code, clippy::cast_precision_loss)]
777 fn get_cpu_windows(
778 pid: u32,
779 last_cpu_time: &mut f64,
780 last_timestamp: &mut Instant,
781 ) -> Result<(f64, u32)> {
782 let mut cpu_percent = 0.0;
783 let mut thread_count = 0;
784
785 let handle =
786 unsafe { OpenProcess(PROCESS_QUERY_INFORMATION, false, pid) }.map_err(|e| {
787 Error::runtime_with_source(
788 format!("Failed to open process {} for CPU stats", pid),
789 e,
790 )
791 })?;
792
793 unsafe {
795 let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0).map_err(|e| {
796 Error::runtime_with_source(
797 "Failed to create ToolHelp snapshot for threads".to_string(),
798 e,
799 )
800 })?;
801
802 let mut entry: THREADENTRY32 = std::mem::zeroed();
803 entry.dwSize = std::mem::size_of::<THREADENTRY32>() as u32;
804
805 if Thread32First(snapshot, &mut entry).is_ok() {
806 loop {
807 if entry.th32OwnerProcessID == pid {
808 thread_count += 1;
809 }
810 if Thread32Next(snapshot, &mut entry).is_err() {
811 break;
812 }
813 }
814 }
815
816 let _ = CloseHandle(snapshot);
817 }
818
819 let mut creation_time = FILETIME::default();
821 let mut exit_time = FILETIME::default();
822 let mut kernel_time = FILETIME::default();
823 let mut user_time = FILETIME::default();
824
825 let result = unsafe {
826 GetProcessTimes(
827 handle,
828 &mut creation_time,
829 &mut exit_time,
830 &mut kernel_time,
831 &mut user_time,
832 )
833 };
834
835 if result.is_ok() {
836 let kernel_ns = Self::filetime_to_ns(&kernel_time);
837 let user_ns = Self::filetime_to_ns(&user_time);
838 let total_time = (kernel_ns + user_ns) as f64 / 1_000_000_000.0; let now = Instant::now();
841 if *last_timestamp != now {
842 let time_diff = now.duration_since(*last_timestamp).as_secs_f64();
843 if time_diff > 0.0 {
844 let time_diff_cpu = total_time - *last_cpu_time;
845 let num_cores = num_cpus::get() as f64;
846
847 cpu_percent = (time_diff_cpu / time_diff) * 100.0 / num_cores;
849 }
850 }
851
852 *last_cpu_time = total_time;
854 *last_timestamp = now;
855 }
856
857 unsafe { CloseHandle(handle) };
858
859 Ok((cpu_percent, thread_count))
860 }
861
862 #[cfg(all(target_os = "windows", not(feature = "windows-monitoring")))]
863 fn get_cpu_windows(
864 _pid: u32,
865 _last_cpu_time: &mut f64,
866 _last_timestamp: &mut Instant,
867 ) -> Result<(f64, u32)> {
868 Err(Error::runtime(
869 "Windows monitoring not enabled. Enable the 'windows-monitoring' feature".to_string(),
870 ))
871 }
872
873 #[cfg(all(target_os = "windows", feature = "windows-monitoring"))]
874 fn filetime_to_ns(ft: &windows::Win32::Foundation::FILETIME) -> u64 {
875 let high = (ft.dwHighDateTime as u64) << 32;
877 let low = ft.dwLowDateTime as u64;
878 (high + low) * 100
880 }
881}
882
883impl Drop for ResourceTracker {
884 fn drop(&mut self) {
885 if let Some(handle) = self.task_handle.take() {
886 #[cfg(feature = "tokio")]
887 handle.abort();
888 #[cfg(all(feature = "async-std", not(feature = "tokio")))]
891 drop(handle); }
893 }
894}
895
896#[cfg(test)]
897mod tests {
898 use super::*;
899 use std::time::Duration;
900
901 #[cfg(feature = "tokio")]
902 #[cfg_attr(miri, ignore)]
903 #[tokio::test]
904 async fn test_resource_tracker_creation() {
905 let tracker = ResourceTracker::new(Duration::from_secs(1));
906 assert_eq!(tracker.max_history, 60);
907 assert_eq!(tracker.sample_interval, Duration::from_secs(1));
908 }
909
910 #[cfg(all(feature = "async-std", not(feature = "tokio")))]
911 #[async_std::test]
912 async fn test_resource_tracker_creation() {
913 let tracker = ResourceTracker::new(Duration::from_secs(1));
914 assert_eq!(tracker.max_history, 60);
915 assert_eq!(tracker.sample_interval, Duration::from_secs(1));
916 }
917
918 #[cfg(feature = "tokio")]
919 #[cfg_attr(miri, ignore)]
920 #[tokio::test]
921 async fn test_resource_usage_methods() {
922 let usage = ResourceUsage::new(1_048_576, 5.5, 4);
923 assert_eq!(usage.memory_bytes(), 1_048_576);
924 let epsilon: f64 = 1e-6;
926 assert!((usage.memory_mb() - 1.0).abs() < epsilon);
927 assert!((usage.cpu_percent() - 5.5).abs() < epsilon);
928 assert_eq!(usage.thread_count(), 4);
929 assert!(usage.age() >= Duration::from_nanos(0));
930 }
931
932 #[cfg(all(feature = "async-std", not(feature = "tokio")))]
933 #[async_std::test]
934 async fn test_resource_usage_methods() {
935 let usage = ResourceUsage::new(1_048_576, 5.5, 4);
936 assert_eq!(usage.memory_bytes(), 1_048_576);
937 let epsilon: f64 = 1e-6;
939 assert!((usage.memory_mb() - 1.0).abs() < epsilon);
940 assert!((usage.cpu_percent() - 5.5).abs() < epsilon);
941 assert_eq!(usage.thread_count(), 4);
942 assert!(usage.age() >= Duration::from_nanos(0));
943 }
944
945 #[cfg(feature = "tokio")]
946 #[cfg_attr(miri, ignore)]
947 #[tokio::test]
948 async fn test_tracker_with_max_history() {
949 let tracker = ResourceTracker::new(Duration::from_secs(1)).with_max_history(100);
950 assert_eq!(tracker.max_history, 100);
951 }
952
953 #[cfg(all(target_os = "windows", feature = "windows-monitoring"))]
954 #[cfg_attr(miri, ignore)]
955 #[test]
956 fn test_windows_toolhelp_thread_count_path() {
957 let pid = std::process::id();
958 let mut last_cpu_time = 0.0;
959 let mut last_timestamp = Instant::now();
960
961 let usage = ResourceTracker::sample_resource_usage(
963 pid,
964 &mut last_cpu_time,
965 &mut last_timestamp,
966 )
967 .expect(
968 "Windows sample_resource_usage should succeed with windows-monitoring feature enabled",
969 );
970
971 assert!(
973 usage.thread_count() >= 1,
974 "expected at least 1 thread, got {}",
975 usage.thread_count()
976 );
977 }
978
979 #[cfg(all(feature = "async-std", not(feature = "tokio")))]
980 #[async_std::test]
981 async fn test_tracker_with_max_history() {
982 let tracker = ResourceTracker::new(Duration::from_secs(1)).with_max_history(100);
983 assert_eq!(tracker.max_history, 100);
984 }
985}