kataglyphis_rustprojecttemplate/
resource_monitor.rs1use std::fs::OpenOptions;
4use std::io::{BufWriter, Write};
5use std::path::PathBuf;
6use std::sync::{
7 Arc,
8 atomic::{AtomicBool, AtomicU64, Ordering},
9};
10use std::thread;
11use std::time::{Duration, Instant};
12
13use log::info;
14use sysinfo::{Pid, ProcessesToUpdate, System};
15
16const BYTES_PER_MIB: f64 = 1024.0 * 1024.0;
17
18#[cfg(target_os = "windows")]
19use crate::gpu_wmi::{gpu_connect, gpu_sample_wmi};
20
21#[derive(Clone, Debug, Default)]
22pub(crate) struct GpuSample {
23 pub utilization_pct: Option<f64>,
24 pub dedicated_used_bytes: Option<u64>,
25 pub shared_used_bytes: Option<u64>,
26 pub total_committed_bytes: Option<u64>,
27}
28
29static GLOBAL_COUNTERS: GlobalCounters = GlobalCounters {
34 inference_completions: AtomicU64::new(0),
35 camera_frames: AtomicU64::new(0),
36 inference_time_ns_total: AtomicU64::new(0),
37 inference_time_samples: AtomicU64::new(0),
38};
39
40struct GlobalCounters {
41 inference_completions: AtomicU64,
42 camera_frames: AtomicU64,
43 inference_time_ns_total: AtomicU64,
44 inference_time_samples: AtomicU64,
45}
46
47impl GlobalCounters {
48 #[inline]
49 fn record_inference_completion(&self) {
50 self.inference_completions.fetch_add(1, Ordering::Relaxed);
51 }
52
53 #[inline]
54 fn record_camera_frame(&self) {
55 self.camera_frames.fetch_add(1, Ordering::Relaxed);
56 }
57
58 #[inline]
59 fn record_inference_duration(&self, duration: Duration) {
60 let ns = duration.as_nanos().min(u128::from(u64::MAX)) as u64;
61 self.inference_time_ns_total
62 .fetch_add(ns, Ordering::Relaxed);
63 self.inference_time_samples.fetch_add(1, Ordering::Relaxed);
64 }
65
66 fn snapshot(&self) -> CounterValues {
67 CounterValues {
68 camera_frames: self.camera_frames.load(Ordering::Relaxed),
69 inference_completions: self.inference_completions.load(Ordering::Relaxed),
70 inference_time_ns_total: self.inference_time_ns_total.load(Ordering::Relaxed),
71 inference_time_samples: self.inference_time_samples.load(Ordering::Relaxed),
72 }
73 }
74}
75
76#[derive(Debug, Clone, Copy)]
77struct CounterValues {
78 camera_frames: u64,
79 inference_completions: u64,
80 inference_time_ns_total: u64,
81 inference_time_samples: u64,
82}
83
84#[derive(Clone, Debug)]
85pub struct ResourceMonitorConfig {
86 pub interval: Duration,
87 pub log_file: Option<PathBuf>,
88 pub include_gpu: bool,
89}
90
91#[inline]
92#[cfg_attr(not(gui_wgpu_backend), allow(dead_code))]
93pub(crate) fn record_inference_completion() {
94 GLOBAL_COUNTERS.record_inference_completion();
95}
96
97#[inline]
98#[cfg_attr(not(gui_wgpu_backend), allow(dead_code))]
99pub(crate) fn record_camera_frame() {
100 GLOBAL_COUNTERS.record_camera_frame();
101}
102
103#[inline]
104#[cfg_attr(not(gui_wgpu_backend), allow(dead_code))]
105pub(crate) fn record_inference_duration(duration: Duration) {
106 GLOBAL_COUNTERS.record_inference_duration(duration);
107}
108
109#[inline]
110pub(crate) fn bytes_to_mib(bytes: u64) -> f64 {
111 (bytes as f64) / BYTES_PER_MIB
112}
113
114pub struct ResourceMonitor {
115 stop: Arc<AtomicBool>,
116 handle: Option<thread::JoinHandle<()>>,
117}
118
119impl ResourceMonitor {
120 pub fn start(config: ResourceMonitorConfig) -> Self {
121 let stop = Arc::new(AtomicBool::new(false));
122 let stop_thread = Arc::clone(&stop);
123
124 let handle = match thread::Builder::new()
125 .name("resource-monitor".to_string())
126 .spawn(move || run_monitor_loop(config, stop_thread))
127 {
128 Ok(h) => Some(h),
129 Err(e) => {
130 log::warn!("Failed to spawn resource-monitor thread: {e}");
131 None
132 }
133 };
134
135 Self { stop, handle }
136 }
137}
138
139impl Drop for ResourceMonitor {
140 fn drop(&mut self) {
141 self.stop.store(true, Ordering::Relaxed);
142 if let Some(handle) = self.handle.take()
143 && let Err(e) = handle.join()
144 {
145 log::warn!("Resource monitor thread panicked: {e:?}");
146 }
147 }
148}
149
150#[derive(Debug, Clone, Copy, PartialEq)]
151pub(crate) struct Rates {
152 pub cam_fps: f64,
153 pub infer_fps: f64,
154 pub infer_latency_ms: f64,
155 pub infer_capacity_fps: f64,
156}
157
158pub(crate) struct CounterSnapshot {
159 at: Instant,
160 values: CounterValues,
161}
162
163impl Default for CounterSnapshot {
164 fn default() -> Self {
165 Self::new()
166 }
167}
168
169impl CounterSnapshot {
170 pub(crate) fn new() -> Self {
171 Self {
172 at: Instant::now(),
173 values: GLOBAL_COUNTERS.snapshot(),
174 }
175 }
176
177 pub(crate) fn tick(&mut self, now: Instant) -> Rates {
178 let dt_s = now.duration_since(self.at).as_secs_f64().max(0.001);
179 self.at = now;
180
181 let new_values = GLOBAL_COUNTERS.snapshot();
182
183 let cam_fps = new_values
184 .camera_frames
185 .wrapping_sub(self.values.camera_frames) as f64
186 / dt_s;
187 let infer_fps = new_values
188 .inference_completions
189 .wrapping_sub(self.values.inference_completions) as f64
190 / dt_s;
191
192 let ns_delta = new_values
193 .inference_time_ns_total
194 .wrapping_sub(self.values.inference_time_ns_total);
195 let samples_delta = new_values
196 .inference_time_samples
197 .wrapping_sub(self.values.inference_time_samples);
198
199 self.values = new_values;
200
201 let infer_latency_ms = if samples_delta > 0 {
202 (ns_delta as f64 / samples_delta as f64) / 1_000_000.0
203 } else {
204 0.0
205 };
206 let infer_capacity_fps = if infer_latency_ms > 0.0 {
207 1000.0 / infer_latency_ms
208 } else {
209 0.0
210 };
211
212 Rates {
213 cam_fps,
214 infer_fps,
215 infer_latency_ms,
216 infer_capacity_fps,
217 }
218 }
219}
220
221fn run_monitor_loop(config: ResourceMonitorConfig, stop: Arc<AtomicBool>) {
222 let pid = Pid::from_u32(std::process::id());
223 let mut sys = System::new();
224
225 let mut file_writer = config
226 .log_file
227 .as_ref()
228 .and_then(|path| OpenOptions::new().create(true).append(true).open(path).ok())
229 .map(BufWriter::new);
230
231 #[cfg(target_os = "windows")]
232 let wmi_conn = if config.include_gpu {
233 gpu_connect()
234 } else {
235 None
236 };
237
238 let mut next_tick = Instant::now();
239 let mut counters = CounterSnapshot::new();
240
241 loop {
242 if stop.load(Ordering::Relaxed) {
243 break;
244 }
245
246 let sample_instant = Instant::now();
247 sys.refresh_processes(ProcessesToUpdate::Some(&[pid]), true);
248 sys.refresh_memory();
249
250 let (proc_cpu_pct, proc_rss_bytes) = sys
251 .process(pid)
252 .map(|p| (p.cpu_usage() as f64, p.memory()))
253 .unwrap_or((0.0, 0));
254
255 let sys_total_bytes = sys.total_memory();
256 let sys_used_bytes = sys.used_memory();
257
258 let gpu = if config.include_gpu {
259 #[cfg(target_os = "windows")]
260 {
261 wmi_conn.as_ref().and_then(gpu_sample_wmi)
262 }
263 #[cfg(not(target_os = "windows"))]
264 {
265 None
266 }
267 } else {
268 None
269 };
270
271 let rates = counters.tick(sample_instant);
272
273 let mut line = format!(
274 "resource cpu={cpu:.1}% rss={rss:.1}MiB sys_used={sys_used:.1}MiB \
275 sys_total={sys_total:.1}MiB cam_fps={cam_fps:.2} infer_fps={infer_fps:.2} \
276 infer_ms={infer_ms:.2} infer_capacity_fps={infer_cap:.2}",
277 cpu = proc_cpu_pct,
278 rss = bytes_to_mib(proc_rss_bytes),
279 sys_used = bytes_to_mib(sys_used_bytes),
280 sys_total = bytes_to_mib(sys_total_bytes),
281 cam_fps = rates.cam_fps,
282 infer_fps = rates.infer_fps,
283 infer_ms = rates.infer_latency_ms,
284 infer_cap = rates.infer_capacity_fps,
285 );
286
287 if let Some(sample) = gpu {
288 append_gpu_metrics(&mut line, &sample);
289 }
290
291 info!("{line}");
292 write_line(&mut file_writer, &line);
293
294 next_tick += config.interval;
295 let sleep_for = next_tick.saturating_duration_since(Instant::now());
296 thread::sleep(sleep_for);
297 }
298
299 if let Some(mut w) = file_writer
300 && let Err(e) = w.flush()
301 {
302 log::warn!("Failed to flush resource log file: {e}");
303 }
304}
305
306fn write_line(writer: &mut Option<BufWriter<std::fs::File>>, line: &str) {
307 let Some(w) = writer.as_mut() else {
308 return;
309 };
310 if let Err(e) = writeln!(w, "{line}") {
311 log::warn!("Failed to write resource log line: {e}");
312 }
313}
314
315fn append_gpu_metrics(line: &mut String, sample: &GpuSample) {
316 use std::fmt::Write;
317 if let Some(util) = sample.utilization_pct {
318 let _ = write!(line, " gpu_util={util:.1}%");
319 }
320 if let Some(dedicated) = sample.dedicated_used_bytes {
321 let _ = write!(line, " gpu_dedicated={:.1}MiB", bytes_to_mib(dedicated));
322 }
323 if let Some(shared) = sample.shared_used_bytes {
324 let _ = write!(line, " gpu_shared={:.1}MiB", bytes_to_mib(shared));
325 }
326 if let Some(total) = sample.total_committed_bytes {
327 let _ = write!(line, " gpu_total={:.1}MiB", bytes_to_mib(total));
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334
335 #[test]
336 fn test_bytes_to_mib() {
337 assert!((bytes_to_mib(1024 * 1024) - 1.0).abs() < 0.001);
338 assert!((bytes_to_mib(512 * 1024 * 1024) - 512.0).abs() < 0.001);
339 assert!((bytes_to_mib(0) - 0.0).abs() < 0.001);
340 }
341
342 #[test]
343 fn test_counter_snapshot_initial() {
344 let snapshot = CounterSnapshot::new();
345 assert_eq!(snapshot.values.camera_frames, 0);
346 assert_eq!(snapshot.values.inference_completions, 0);
347 }
348
349 #[test]
350 fn test_rates_zero_values() {
351 let zero = Rates {
352 cam_fps: 0.0,
353 infer_fps: 0.0,
354 infer_latency_ms: 0.0,
355 infer_capacity_fps: 0.0,
356 };
357 assert!(!zero.cam_fps.is_nan());
358 assert!(!zero.infer_fps.is_nan());
359 }
360
361 #[test]
362 fn test_gpu_sample_default() {
363 let sample = GpuSample::default();
364 assert!(sample.utilization_pct.is_none());
365 assert!(sample.dedicated_used_bytes.is_none());
366 }
367}