1#[allow(unused_imports)]
68use crate::error::{Error, Result};
69#[cfg(feature = "metrics")]
70use crate::metrics::MetricsCollector;
71use arc_swap::ArcSwap;
72use std::sync::{Arc, RwLock};
73use std::time::{Duration, Instant};
74
75#[cfg(all(feature = "async-std", not(feature = "tokio")))]
77#[allow(unused_imports)]
78use async_std::task::JoinHandle as AsyncJoinHandle;
79#[cfg(not(any(feature = "tokio", feature = "async-std")))]
80use std::thread::JoinHandle;
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
94#[cfg(target_os = "macos")]
95use std::process::Command;
96
97#[cfg(all(target_os = "windows", feature = "windows-monitoring"))]
98use windows::Win32::System::Diagnostics::ToolHelp::{
99 CreateToolhelp32Snapshot, Thread32First, Thread32Next, TH32CS_SNAPTHREAD, THREADENTRY32,
100};
101#[cfg(all(target_os = "windows", feature = "windows-monitoring"))]
102use windows::Win32::System::ProcessStatus::{GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS};
103#[cfg(all(target_os = "windows", feature = "windows-monitoring"))]
104use windows::Win32::System::Threading::{GetProcessTimes, OpenProcess, PROCESS_QUERY_INFORMATION};
105
106#[cfg(all(target_os = "windows", feature = "windows-monitoring"))]
107use windows::Win32::Foundation::{CloseHandle, FILETIME};
108
109#[derive(Debug, Clone)]
111pub struct ResourceUsage {
112 timestamp: Instant,
114
115 memory_bytes: u64,
117
118 cpu_percent: f64,
120
121 thread_count: u32,
123}
124
125#[derive(Debug, Clone)]
127pub enum Alert {
128 MemorySoftLimit {
130 limit_bytes: u64,
132 current_bytes: u64,
134 },
135}
136
137impl ResourceUsage {
138 #[must_use]
140 pub fn new(memory_bytes: u64, cpu_percent: f64, thread_count: u32) -> Self {
141 Self {
142 timestamp: Instant::now(),
143 memory_bytes,
144 cpu_percent,
145 thread_count,
146 }
147 }
148
149 #[must_use]
151 pub const fn memory_bytes(&self) -> u64 {
152 self.memory_bytes
153 }
154
155 #[must_use]
157 #[allow(clippy::cast_precision_loss)]
158 pub fn memory_mb(&self) -> f64 {
159 self.memory_bytes as f64 / 1_048_576.0
161 }
162
163 #[must_use]
165 pub const fn cpu_percent(&self) -> f64 {
166 self.cpu_percent
167 }
168
169 #[must_use]
171 pub const fn thread_count(&self) -> u32 {
172 self.thread_count
173 }
174
175 #[must_use]
177 pub fn age(&self) -> Duration {
178 self.timestamp.elapsed()
179 }
180}
181
182#[allow(dead_code)]
184pub struct ResourceTracker {
185 sample_interval: Duration,
187
188 current_usage: Arc<ArcSwap<ResourceUsage>>,
190
191 history: Arc<RwLock<Vec<ResourceUsage>>>,
193
194 max_history: usize,
196
197 #[cfg(feature = "tokio")]
199 task_handle: Option<tokio::task::JoinHandle<()>>,
200 #[cfg(all(feature = "async-std", not(feature = "tokio")))]
201 task_handle: Option<async_std::task::JoinHandle<()>>,
202 #[cfg(not(any(feature = "tokio", feature = "async-std")))]
203 task_handle: Option<std::thread::JoinHandle<()>>,
204
205 pid: u32,
207
208 memory_soft_limit_bytes: Option<u64>,
210
211 #[allow(clippy::type_complexity)]
213 on_alert: Option<Arc<dyn Fn(Alert) + Send + Sync + 'static>>,
214
215 #[cfg(feature = "metrics")]
217 metrics: Option<MetricsCollector>,
218}
219
220impl ResourceTracker {
221 #[must_use]
223 pub fn new(sample_interval: Duration) -> Self {
224 let initial_usage = ResourceUsage::new(0, 0.0, 0);
226
227 Self {
228 sample_interval,
229 current_usage: Arc::new(ArcSwap::from_pointee(initial_usage)),
230 history: Arc::new(RwLock::new(Vec::new())),
231 max_history: 60, task_handle: None,
233 pid: std::process::id(),
234 memory_soft_limit_bytes: None,
235 on_alert: None,
236 #[cfg(feature = "metrics")]
237 metrics: None,
238 }
239 }
240
241 #[must_use]
243 pub const fn with_max_history(mut self, max_entries: usize) -> Self {
244 self.max_history = max_entries;
245 self
246 }
247
248 #[must_use]
250 pub const fn with_memory_soft_limit_bytes(mut self, bytes: u64) -> Self {
251 self.memory_soft_limit_bytes = Some(bytes);
252 self
253 }
254
255 #[must_use]
257 pub fn with_alert_handler<F>(mut self, f: F) -> Self
258 where
259 F: Fn(Alert) + Send + Sync + 'static,
260 {
261 self.on_alert = Some(Arc::new(f));
262 self
263 }
264
265 #[cfg(feature = "metrics")]
267 #[must_use]
268 pub fn with_metrics(mut self, metrics: MetricsCollector) -> Self {
269 self.metrics = Some(metrics);
270 self
271 }
272
273 #[must_use]
277 pub fn with_alert_to_tracing(mut self) -> Self {
278 self.on_alert = Some(Arc::new(|alert| match alert {
279 Alert::MemorySoftLimit {
280 limit_bytes,
281 current_bytes,
282 } => {
283 tracing::warn!(
284 target: "proc_daemon::resources",
285 limit_bytes,
286 current_bytes,
287 "Resource alert: soft memory limit exceeded"
288 );
289 }
290 }));
291 self
292 }
293
294 #[cfg(all(feature = "tokio", not(feature = "async-std")))]
302 #[allow(clippy::missing_errors_doc)]
303 pub fn start(&mut self) -> Result<()> {
304 if self.task_handle.is_some() {
305 return Ok(()); }
307
308 let sample_interval = self.sample_interval;
309 let usage_history = Arc::clone(&self.history);
310 let current_usage = Arc::clone(&self.current_usage);
311 let pid = self.pid;
312 let max_history = self.max_history;
313 let memory_soft_limit_bytes = self.memory_soft_limit_bytes;
314 let on_alert = self.on_alert.clone();
315 #[cfg(feature = "metrics")]
316 let metrics = self.metrics.clone();
317
318 let handle = tokio::spawn(async move {
319 let mut interval_timer = time::interval(sample_interval);
320 let mut last_cpu_time = 0.0;
321 let mut last_timestamp = Instant::now();
322 #[cfg(feature = "metrics")]
323 let mut last_tick = Instant::now();
324
325 loop {
326 interval_timer.tick().await;
327 #[cfg(feature = "metrics")]
328 let tick_now = Instant::now();
329
330 if let Ok(usage) =
332 Self::sample_resource_usage(pid, &mut last_cpu_time, &mut last_timestamp)
333 {
334 current_usage.store(Arc::new(usage.clone()));
336
337 if let Ok(mut hist) = usage_history.write() {
339 hist.push(usage.clone());
340
341 if hist.len() > max_history {
343 hist.remove(0);
344 }
345 }
346
347 if let Some(limit) = memory_soft_limit_bytes {
349 if usage.memory_bytes() > limit {
350 if let Some(cb) = on_alert.as_ref() {
351 cb(Alert::MemorySoftLimit {
352 limit_bytes: limit,
353 current_bytes: usage.memory_bytes(),
354 });
355 }
356 }
357 }
358
359 #[cfg(feature = "metrics")]
361 if let Some(m) = metrics.as_ref() {
362 m.set_gauge("proc.memory_bytes", usage.memory_bytes());
363 let cpu_milli = (usage.cpu_percent() * 1000.0).max(0.0).round() as u64;
364 m.set_gauge("proc.cpu_milli_percent", cpu_milli);
365 m.set_gauge("proc.thread_count", u64::from(usage.thread_count()));
366 m.increment_counter("proc.samples_total", 1);
367 m.record_histogram(
368 "proc.sample_interval",
369 tick_now.saturating_duration_since(last_tick),
370 );
371 last_tick = tick_now;
372 }
373 }
374 }
375 });
376
377 self.task_handle = Some(handle);
378 Ok(())
379 }
380
381 #[cfg(all(feature = "async-std", not(feature = "tokio")))]
383 #[allow(clippy::missing_errors_doc)]
384 pub fn start(&mut self) -> Result<()> {
385 if self.task_handle.is_some() {
386 return Ok(()); }
388
389 let sample_interval = self.sample_interval;
390 let usage_history = Arc::clone(&self.history);
391 let current_usage = Arc::clone(&self.current_usage);
392 let pid = self.pid;
393 let max_history = self.max_history; let memory_soft_limit_bytes = self.memory_soft_limit_bytes;
395 let on_alert = self.on_alert.clone();
396 #[cfg(feature = "metrics")]
397 let metrics = self.metrics.clone();
398
399 let handle = async_std::task::spawn(async move {
400 let mut last_cpu_time = 0.0;
401 let mut last_timestamp = Instant::now();
402 #[cfg(feature = "metrics")]
403 let mut last_tick = Instant::now();
404
405 loop {
406 async_std::task::sleep(sample_interval).await;
407 #[cfg(feature = "metrics")]
408 let tick_now = Instant::now();
409
410 if let Ok(usage) =
412 Self::sample_resource_usage(pid, &mut last_cpu_time, &mut last_timestamp)
413 {
414 current_usage.store(Arc::new(usage.clone()));
416
417 if let Ok(mut hist) = usage_history.write() {
419 hist.push(usage.clone());
420
421 if hist.len() > max_history {
423 hist.remove(0);
424 }
425 }
426
427 if let Some(limit) = memory_soft_limit_bytes {
429 if usage.memory_bytes() > limit {
430 if let Some(cb) = on_alert.as_ref() {
431 cb(Alert::MemorySoftLimit {
432 limit_bytes: limit,
433 current_bytes: usage.memory_bytes(),
434 });
435 }
436 }
437 }
438
439 #[cfg(feature = "metrics")]
441 if let Some(m) = metrics.as_ref() {
442 m.set_gauge("proc.memory_bytes", usage.memory_bytes());
443 let cpu_milli = (usage.cpu_percent() * 1000.0).max(0.0).round() as u64;
444 m.set_gauge("proc.cpu_milli_percent", cpu_milli);
445 m.set_gauge("proc.thread_count", u64::from(usage.thread_count()));
446 m.increment_counter("proc.samples_total", 1);
447 m.record_histogram(
448 "proc.sample_interval",
449 tick_now.saturating_duration_since(last_tick),
450 );
451 }
452 #[cfg(feature = "metrics")]
453 {
454 last_tick = tick_now;
455 }
456 }
457 }
458 });
459
460 self.task_handle = Some(handle);
461 Ok(())
462 }
463
464 #[cfg(all(feature = "tokio", not(feature = "async-std")))]
468 pub async fn stop(&mut self) {
469 if let Some(handle) = self.task_handle.take() {
470 handle.abort();
471 let _ = handle.await;
472 }
473 }
474
475 #[cfg(all(feature = "async-std", not(feature = "tokio")))]
479 pub fn stop(&mut self) {
480 self.task_handle.take();
482 }
483
484 #[must_use]
486 pub fn current_usage(&self) -> ResourceUsage {
487 self.current_usage.load_full().as_ref().clone()
488 }
489
490 #[must_use]
492 pub fn history(&self) -> Vec<ResourceUsage> {
493 self.history
494 .read()
495 .map_or_else(|_| Vec::new(), |history| history.clone())
496 }
497
498 #[allow(unused_variables, dead_code)]
500 #[allow(clippy::needless_pass_by_ref_mut)]
501 fn sample_resource_usage(
502 pid: u32,
503 last_cpu_time: &mut f64,
504 last_timestamp: &mut Instant,
505 ) -> Result<ResourceUsage> {
506 #[cfg(target_os = "linux")]
507 {
508 let memory = Self::get_memory_linux(pid)?;
510 let (cpu, threads) = Self::get_cpu_linux(pid, last_cpu_time, last_timestamp)?;
511 Ok(ResourceUsage::new(memory, cpu, threads))
512 }
513
514 #[cfg(target_os = "macos")]
515 {
516 let memory = Self::get_memory_macos(pid)?;
518 let (cpu, threads) = Self::get_cpu_macos(pid)?;
519 Ok(ResourceUsage::new(memory, cpu, threads))
520 }
521
522 #[cfg(target_os = "windows")]
523 {
524 let memory = Self::get_memory_windows(pid)?;
526 let (cpu, threads) = Self::get_cpu_windows(pid, last_cpu_time, last_timestamp)?;
527 Ok(ResourceUsage::new(memory, cpu, threads))
528 }
529
530 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
531 {
532 Ok(ResourceUsage::new(0, 0.0, 0))
534 }
535 }
536
537 #[cfg(target_os = "linux")]
538 fn get_memory_linux(pid: u32) -> Result<u64> {
539 let path = format!("/proc/{pid}/status");
541 let file = File::open(&path).map_err(|e| {
542 Error::io_with_source(format!("Failed to open {path} for memory stats"), e)
543 })?;
544
545 let reader = BufReader::new(file);
546 let mut memory_bytes = 0;
547
548 for line in reader.lines() {
549 let line = line.map_err(|e| {
550 Error::io_with_source("Failed to read process memory stats".to_string(), e)
551 })?;
552
553 if line.starts_with("VmRSS:") {
555 let parts: Vec<&str> = line.split_whitespace().collect();
556 if !parts.is_empty() {
557 if let Ok(kb) = parts[1].parse::<u64>() {
558 memory_bytes = kb * 1024;
559 break;
560 }
561 }
562 }
563 }
564
565 Ok(memory_bytes)
566 }
567
568 #[cfg(target_os = "linux")]
569 #[allow(
570 clippy::cast_precision_loss,
571 clippy::cast_possible_truncation,
572 clippy::similar_names
573 )]
574 fn get_cpu_linux(
575 pid: u32,
576 last_cpu_time: &mut f64,
577 last_timestamp: &mut Instant,
578 ) -> Result<(f64, u32)> {
579 let path = format!("/proc/{pid}/stat");
581 let file = File::open(&path).map_err(|e| {
582 Error::io_with_source(format!("Failed to open {path} for CPU stats"), e)
583 })?;
584
585 let reader = BufReader::new(file);
586 let mut cpu_percent = 0.0;
587 let mut thread_count: u32 = 0;
588
589 if let Ok(line) = reader.lines().next().ok_or_else(|| {
590 Error::runtime("Failed to read CPU stats from proc filesystem".to_string())
591 }) {
592 let line = line.map_err(|e| {
593 Error::io_with_source("Failed to read process CPU stats".to_string(), e)
594 })?;
595
596 let parts: Vec<&str> = line.split_whitespace().collect();
597 if parts.len() >= 24 {
598 thread_count = parts[19].parse::<u32>().unwrap_or(0);
600
601 let utime = parts[13].parse::<f64>().unwrap_or(0.0);
603 let stime = parts[14].parse::<f64>().unwrap_or(0.0);
604 let child_user_time = parts[15].parse::<f64>().unwrap_or(0.0);
605 let child_system_time = parts[16].parse::<f64>().unwrap_or(0.0);
606
607 let current_cpu_time = utime + stime + child_user_time + child_system_time;
608 let now = Instant::now();
609
610 if *last_timestamp != now {
612 let time_diff = now.duration_since(*last_timestamp).as_secs_f64();
613 if time_diff > 0.0 {
614 let num_cores = num_cpus::get() as f64;
616 let cpu_time_diff = current_cpu_time - *last_cpu_time;
617
618 cpu_percent = (cpu_time_diff / 100.0) / time_diff * 100.0 / num_cores;
621 }
622 }
623
624 *last_cpu_time = current_cpu_time;
626 *last_timestamp = now;
627 }
628 }
629
630 Ok((cpu_percent, thread_count))
631 }
632
633 #[cfg(target_os = "macos")]
634 #[allow(dead_code)]
635 fn get_memory_macos(pid: u32) -> Result<u64> {
636 let output = Command::new("ps")
638 .args(["-xo", "rss=", "-p", &pid.to_string()])
639 .output()
640 .map_err(|e| {
641 Error::io_with_source(
642 "Failed to execute ps command for memory stats".to_string(),
643 e,
644 )
645 })?;
646
647 let memory_kb = String::from_utf8_lossy(&output.stdout)
648 .trim()
649 .parse::<u64>()
650 .unwrap_or(0);
651
652 Ok(memory_kb * 1024)
653 }
654
655 #[cfg(target_os = "macos")]
656 #[allow(dead_code)]
657 fn get_cpu_macos(pid: u32) -> Result<(f64, u32)> {
658 let output = Command::new("ps")
660 .args(["-xo", "%cpu,thcount=", "-p", &pid.to_string()])
661 .output()
662 .map_err(|e| {
663 Error::io_with_source("Failed to execute ps command for CPU stats".to_string(), e)
664 })?;
665
666 let stats = String::from_utf8_lossy(&output.stdout);
667 let parts: Vec<&str> = stats.split_whitespace().collect();
668
669 let cpu_percent = if parts.is_empty() {
670 0.0
671 } else {
672 parts[0].parse::<f64>().unwrap_or(0.0)
673 };
674
675 let thread_count = if parts.len() > 1 {
676 parts[1].parse::<u32>().unwrap_or(0)
677 } else {
678 0
679 };
680
681 Ok((cpu_percent, thread_count))
682 }
683
684 #[cfg(all(target_os = "windows", feature = "windows-monitoring"))]
685 #[allow(unsafe_code)]
686 fn get_memory_windows(pid: u32) -> Result<u64> {
687 let mut pmc = PROCESS_MEMORY_COUNTERS::default();
688 let handle =
689 unsafe { OpenProcess(PROCESS_QUERY_INFORMATION, false, pid) }.map_err(|e| {
690 Error::runtime_with_source(
691 format!("Failed to open process {} for memory stats", pid),
692 e,
693 )
694 })?;
695
696 let result = unsafe {
697 GetProcessMemoryInfo(
698 handle,
699 &mut pmc,
700 std::mem::size_of::<PROCESS_MEMORY_COUNTERS>() as u32,
701 )
702 }
703 .map_err(|e| {
704 Error::runtime_with_source("Failed to get process memory info".to_string(), e)
705 });
706
707 unsafe { CloseHandle(handle) };
708 result?;
709
710 Ok(u64::try_from(pmc.WorkingSetSize).unwrap_or(pmc.WorkingSetSize as u64))
711 }
712
713 #[cfg(all(target_os = "windows", not(feature = "windows-monitoring")))]
714 fn get_memory_windows(_pid: u32) -> Result<u64> {
715 Err(Error::runtime(
716 "Windows monitoring not enabled. Enable the 'windows-monitoring' feature".to_string(),
717 ))
718 }
719
720 #[cfg(all(target_os = "windows", feature = "windows-monitoring"))]
721 #[allow(unsafe_code, clippy::cast_precision_loss)]
722 fn get_cpu_windows(
723 pid: u32,
724 last_cpu_time: &mut f64,
725 last_timestamp: &mut Instant,
726 ) -> Result<(f64, u32)> {
727 let mut cpu_percent = 0.0;
728 let mut thread_count = 0;
729
730 let handle =
731 unsafe { OpenProcess(PROCESS_QUERY_INFORMATION, false, pid) }.map_err(|e| {
732 Error::runtime_with_source(
733 format!("Failed to open process {} for CPU stats", pid),
734 e,
735 )
736 })?;
737
738 unsafe {
740 let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0).map_err(|e| {
741 Error::runtime_with_source(
742 "Failed to create ToolHelp snapshot for threads".to_string(),
743 e,
744 )
745 })?;
746
747 let mut entry: THREADENTRY32 = std::mem::zeroed();
748 entry.dwSize = std::mem::size_of::<THREADENTRY32>() as u32;
749
750 if Thread32First(snapshot, &mut entry).is_ok() {
751 loop {
752 if entry.th32OwnerProcessID == pid {
753 thread_count += 1;
754 }
755 if Thread32Next(snapshot, &mut entry).is_err() {
756 break;
757 }
758 }
759 }
760
761 let _ = CloseHandle(snapshot);
762 }
763
764 let mut creation_time = FILETIME::default();
766 let mut exit_time = FILETIME::default();
767 let mut kernel_time = FILETIME::default();
768 let mut user_time = FILETIME::default();
769
770 let result = unsafe {
771 GetProcessTimes(
772 handle,
773 &mut creation_time,
774 &mut exit_time,
775 &mut kernel_time,
776 &mut user_time,
777 )
778 };
779
780 if result.is_ok() {
781 let kernel_ns = Self::filetime_to_ns(&kernel_time);
782 let user_ns = Self::filetime_to_ns(&user_time);
783 let total_time = (kernel_ns + user_ns) as f64 / 1_000_000_000.0; let now = Instant::now();
786 if *last_timestamp != now {
787 let time_diff = now.duration_since(*last_timestamp).as_secs_f64();
788 if time_diff > 0.0 {
789 let time_diff_cpu = total_time - *last_cpu_time;
790 let num_cores = num_cpus::get() as f64;
791
792 cpu_percent = (time_diff_cpu / time_diff) * 100.0 / num_cores;
794 }
795 }
796
797 *last_cpu_time = total_time;
799 *last_timestamp = now;
800 }
801
802 unsafe { CloseHandle(handle) };
803
804 Ok((cpu_percent, thread_count))
805 }
806
807 #[cfg(all(target_os = "windows", not(feature = "windows-monitoring")))]
808 fn get_cpu_windows(
809 _pid: u32,
810 _last_cpu_time: &mut f64,
811 _last_timestamp: &mut Instant,
812 ) -> Result<(f64, u32)> {
813 Err(Error::runtime(
814 "Windows monitoring not enabled. Enable the 'windows-monitoring' feature".to_string(),
815 ))
816 }
817
818 #[cfg(all(target_os = "windows", feature = "windows-monitoring"))]
819 fn filetime_to_ns(ft: &windows::Win32::Foundation::FILETIME) -> u64 {
820 let high = (ft.dwHighDateTime as u64) << 32;
822 let low = ft.dwLowDateTime as u64;
823 (high + low) * 100
825 }
826}
827
828impl Drop for ResourceTracker {
829 fn drop(&mut self) {
830 if let Some(handle) = self.task_handle.take() {
831 #[cfg(feature = "tokio")]
832 handle.abort();
833 #[cfg(all(feature = "async-std", not(feature = "tokio")))]
836 drop(handle); }
838 }
839}
840
841#[cfg(test)]
842mod tests {
843 use super::*;
844 use std::time::Duration;
845
846 #[cfg(feature = "tokio")]
847 #[cfg_attr(miri, ignore)]
848 #[tokio::test]
849 async fn test_resource_tracker_creation() {
850 let tracker = ResourceTracker::new(Duration::from_secs(1));
851 assert_eq!(tracker.max_history, 60);
852 assert_eq!(tracker.sample_interval, Duration::from_secs(1));
853 }
854
855 #[cfg(all(feature = "async-std", not(feature = "tokio")))]
856 #[async_std::test]
857 async fn test_resource_tracker_creation() {
858 let tracker = ResourceTracker::new(Duration::from_secs(1));
859 assert_eq!(tracker.max_history, 60);
860 assert_eq!(tracker.sample_interval, Duration::from_secs(1));
861 }
862
863 #[cfg(feature = "tokio")]
864 #[cfg_attr(miri, ignore)]
865 #[tokio::test]
866 async fn test_resource_usage_methods() {
867 let usage = ResourceUsage::new(1_048_576, 5.5, 4);
868 assert_eq!(usage.memory_bytes(), 1_048_576);
869 let epsilon: f64 = 1e-6;
871 assert!((usage.memory_mb() - 1.0).abs() < epsilon);
872 assert!((usage.cpu_percent() - 5.5).abs() < epsilon);
873 assert_eq!(usage.thread_count(), 4);
874 assert!(usage.age() >= Duration::from_nanos(0));
875 }
876
877 #[cfg(all(feature = "async-std", not(feature = "tokio")))]
878 #[async_std::test]
879 async fn test_resource_usage_methods() {
880 let usage = ResourceUsage::new(1_048_576, 5.5, 4);
881 assert_eq!(usage.memory_bytes(), 1_048_576);
882 let epsilon: f64 = 1e-6;
884 assert!((usage.memory_mb() - 1.0).abs() < epsilon);
885 assert!((usage.cpu_percent() - 5.5).abs() < epsilon);
886 assert_eq!(usage.thread_count(), 4);
887 assert!(usage.age() >= Duration::from_nanos(0));
888 }
889
890 #[cfg(feature = "tokio")]
891 #[cfg_attr(miri, ignore)]
892 #[tokio::test]
893 async fn test_tracker_with_max_history() {
894 let tracker = ResourceTracker::new(Duration::from_secs(1)).with_max_history(100);
895 assert_eq!(tracker.max_history, 100);
896 }
897
898 #[cfg(all(target_os = "windows", feature = "windows-monitoring"))]
899 #[cfg_attr(miri, ignore)]
900 #[test]
901 fn test_windows_toolhelp_thread_count_path() {
902 let pid = std::process::id();
903 let mut last_cpu_time = 0.0;
904 let mut last_timestamp = Instant::now();
905
906 let usage = ResourceTracker::sample_resource_usage(
908 pid,
909 &mut last_cpu_time,
910 &mut last_timestamp,
911 )
912 .expect(
913 "Windows sample_resource_usage should succeed with windows-monitoring feature enabled",
914 );
915
916 assert!(
918 usage.thread_count() >= 1,
919 "expected at least 1 thread, got {}",
920 usage.thread_count()
921 );
922 }
923
924 #[cfg(all(feature = "async-std", not(feature = "tokio")))]
925 #[async_std::test]
926 async fn test_tracker_with_max_history() {
927 let tracker = ResourceTracker::new(Duration::from_secs(1)).with_max_history(100);
928 assert_eq!(tracker.max_history, 100);
929 }
930}