Skip to main content

windows_erg/process/
metrics.rs

1//! Host and process metrics.
2
3use std::thread;
4use std::time::Duration;
5
6use windows::Win32::Foundation::FILETIME;
7use windows::Win32::System::ProcessStatus::{
8    GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS, PROCESS_MEMORY_COUNTERS_EX,
9};
10use windows::Win32::System::SystemInformation::{GlobalMemoryStatusEx, MEMORYSTATUSEX};
11use windows::Win32::System::Threading::{
12    ALL_PROCESSOR_GROUPS, GetActiveProcessorCount, GetProcessTimes, GetSystemTimes,
13};
14
15use super::processes::Process;
16use super::types::{
17    HostMemoryMetrics, HostMetrics, ProcessCpuTimes, ProcessMemoryMetrics, ProcessMetrics,
18};
19use crate::error::{Error, InvalidParameterError, ProcessError, ProcessOpenError, Result};
20
21impl Process {
22    /// Get cumulative process CPU time counters.
23    ///
24    /// Returned values are cumulative since process start in 100ns units.
25    pub fn cpu_times(&self) -> Result<ProcessCpuTimes> {
26        let mut creation_time = FILETIME::default();
27        let mut exit_time = FILETIME::default();
28        let mut kernel_time = FILETIME::default();
29        let mut user_time = FILETIME::default();
30
31        unsafe {
32            GetProcessTimes(
33                self.as_raw_handle(),
34                &mut creation_time,
35                &mut exit_time,
36                &mut kernel_time,
37                &mut user_time,
38            )
39        }
40        .map_err(|e| {
41            Error::Process(ProcessError::OpenFailed(ProcessOpenError::with_code(
42                self.id().as_u32(),
43                "Failed to get process CPU times",
44                e.code().0,
45            )))
46        })?;
47
48        let kernel = filetime_to_u64_100ns(kernel_time);
49        let user = filetime_to_u64_100ns(user_time);
50
51        Ok(ProcessCpuTimes {
52            user_time_100ns: user,
53            kernel_time_100ns: kernel,
54            total_time_100ns: kernel.saturating_add(user),
55        })
56    }
57
58    /// Get extended memory metrics for this process.
59    pub fn memory_metrics(&self) -> Result<ProcessMemoryMetrics> {
60        let mut counters = PROCESS_MEMORY_COUNTERS_EX {
61            cb: std::mem::size_of::<PROCESS_MEMORY_COUNTERS_EX>() as u32,
62            ..Default::default()
63        };
64
65        unsafe {
66            GetProcessMemoryInfo(
67                self.as_raw_handle(),
68                &mut counters as *mut PROCESS_MEMORY_COUNTERS_EX as *mut PROCESS_MEMORY_COUNTERS,
69                counters.cb,
70            )
71        }
72        .map_err(|e| {
73            Error::Process(ProcessError::OpenFailed(ProcessOpenError::with_code(
74                self.id().as_u32(),
75                "Failed to get process memory metrics",
76                e.code().0,
77            )))
78        })?;
79
80        Ok(ProcessMemoryMetrics {
81            working_set_bytes: counters.WorkingSetSize,
82            peak_working_set_bytes: counters.PeakWorkingSetSize,
83            page_fault_count: counters.PageFaultCount,
84            private_usage_bytes: counters.PrivateUsage,
85            commit_usage_bytes: counters.PagefileUsage,
86            peak_commit_usage_bytes: counters.PeakPagefileUsage,
87        })
88    }
89
90    /// Get point-in-time CPU and memory metrics for this process.
91    pub fn metrics(&self) -> Result<ProcessMetrics> {
92        Ok(ProcessMetrics {
93            memory: self.memory_metrics()?,
94            cpu: self.cpu_times()?,
95        })
96    }
97
98    /// Calculate process CPU usage percentage over a sampling interval.
99    ///
100    /// The returned value is normalized to whole-machine CPU usage scale [0.0, 100.0].
101    pub fn cpu_usage(&self, interval: Duration) -> Result<f64> {
102        if interval.is_zero() {
103            return Err(Error::InvalidParameter(InvalidParameterError::new(
104                "interval",
105                "interval must be greater than zero",
106            )));
107        }
108
109        let start_proc = self.cpu_times()?;
110        let (_, start_kernel, start_user) = read_system_times_100ns()?;
111
112        thread::sleep(interval);
113
114        let end_proc = self.cpu_times()?;
115        let (_, end_kernel, end_user) = read_system_times_100ns()?;
116
117        let start_total = start_kernel.saturating_add(start_user);
118        let end_total = end_kernel.saturating_add(end_user);
119
120        Ok(calculate_cpu_percentage(
121            start_proc.total_time_100ns,
122            end_proc.total_time_100ns,
123            start_total,
124            end_total,
125        ))
126    }
127}
128
129/// Get point-in-time host metrics.
130pub fn host_metrics() -> Result<HostMetrics> {
131    let mut memory_status = MEMORYSTATUSEX {
132        dwLength: std::mem::size_of::<MEMORYSTATUSEX>() as u32,
133        ..Default::default()
134    };
135
136    unsafe { GlobalMemoryStatusEx(&mut memory_status) }.map_err(|e| {
137        Error::WindowsApi(crate::error::WindowsApiError::with_context(
138            e,
139            "GlobalMemoryStatusEx",
140        ))
141    })?;
142
143    let logical_cpu_count = unsafe { GetActiveProcessorCount(ALL_PROCESSOR_GROUPS) };
144
145    Ok(HostMetrics {
146        logical_cpu_count,
147        memory: HostMemoryMetrics {
148            total_physical_bytes: memory_status.ullTotalPhys,
149            available_physical_bytes: memory_status.ullAvailPhys,
150            total_virtual_bytes: memory_status.ullTotalVirtual,
151            available_virtual_bytes: memory_status.ullAvailVirtual,
152            memory_load_percent: memory_status.dwMemoryLoad,
153        },
154    })
155}
156
157/// Calculate overall host CPU usage percentage over a sampling interval.
158///
159/// The returned value is in the range [0.0, 100.0].
160pub fn host_cpu_usage(interval: Duration) -> Result<f64> {
161    if interval.is_zero() {
162        return Err(Error::InvalidParameter(InvalidParameterError::new(
163            "interval",
164            "interval must be greater than zero",
165        )));
166    }
167
168    let (idle_start, kernel_start, user_start) = read_system_times_100ns()?;
169    thread::sleep(interval);
170    let (idle_end, kernel_end, user_end) = read_system_times_100ns()?;
171
172    let total_start = kernel_start.saturating_add(user_start);
173    let total_end = kernel_end.saturating_add(user_end);
174    let total_delta = total_end.saturating_sub(total_start);
175    if total_delta == 0 {
176        return Ok(0.0);
177    }
178
179    let idle_delta = idle_end.saturating_sub(idle_start);
180    let busy_delta = total_delta.saturating_sub(idle_delta);
181    let usage = (busy_delta as f64 / total_delta as f64) * 100.0;
182    Ok(usage.clamp(0.0, 100.0))
183}
184
185fn read_system_times_100ns() -> Result<(u64, u64, u64)> {
186    let mut idle = FILETIME::default();
187    let mut kernel = FILETIME::default();
188    let mut user = FILETIME::default();
189
190    unsafe {
191        GetSystemTimes(
192            Some(&mut idle as *mut FILETIME),
193            Some(&mut kernel as *mut FILETIME),
194            Some(&mut user as *mut FILETIME),
195        )
196    }
197    .map_err(|e| {
198        Error::WindowsApi(crate::error::WindowsApiError::with_context(
199            e,
200            "GetSystemTimes",
201        ))
202    })?;
203
204    Ok((
205        filetime_to_u64_100ns(idle),
206        filetime_to_u64_100ns(kernel),
207        filetime_to_u64_100ns(user),
208    ))
209}
210
211fn filetime_to_u64_100ns(file_time: FILETIME) -> u64 {
212    ((file_time.dwHighDateTime as u64) << 32) | (file_time.dwLowDateTime as u64)
213}
214
215fn calculate_cpu_percentage(start_proc: u64, end_proc: u64, start_sys: u64, end_sys: u64) -> f64 {
216    let proc_delta = end_proc.saturating_sub(start_proc);
217    let sys_delta = end_sys.saturating_sub(start_sys);
218
219    if sys_delta == 0 {
220        return 0.0;
221    }
222
223    ((proc_delta as f64 / sys_delta as f64) * 100.0).clamp(0.0, 100.0)
224}
225
226#[cfg(test)]
227mod tests {
228    use super::calculate_cpu_percentage;
229
230    #[test]
231    fn cpu_percentage_zero_when_no_delta() {
232        let usage = calculate_cpu_percentage(100, 100, 1000, 1000);
233        assert_eq!(usage, 0.0);
234    }
235
236    #[test]
237    fn cpu_percentage_computes_expected_ratio() {
238        let usage = calculate_cpu_percentage(100, 300, 1000, 2000);
239        assert!((usage - 20.0).abs() < 0.000_1);
240    }
241}