1use futures::{StreamExt, future::try_join_all, try_join};
7use joule_profiler_core::{
8 sensor::{Sensor, Sensors},
9 source::MetricReader,
10 types::{Metric, Metrics},
11 unit::{MetricUnit, Unit, UnitPrefix},
12};
13use log::{debug, trace};
14use std::{
15 collections::HashSet,
16 sync::{Arc, Mutex},
17 time::Duration,
18};
19use tokio::task::JoinHandle;
20use tokio::task::spawn_blocking;
21use tokio_timerfd::Interval;
22use tokio_util::sync::CancellationToken;
23
24use crate::{
25 backend::{Backend, ProcfsBackend},
26 config::ProcfsConfig,
27 counters::{Counters, compute_mem_used},
28 error::ProcfsError,
29 snapshot::ProcSnapshot,
30 utils::make_conversion,
31};
32
33mod backend;
34pub mod config;
35pub mod counters;
36pub mod error;
37mod snapshot;
38mod utils;
39
40const IO_COUNTERS_METRIC_UNIT: MetricUnit = MetricUnit {
41 prefix: UnitPrefix::None,
42 unit: Unit::Byte,
43};
44
45const PROCFS_SOURCE_NAME: &str = "procfs";
46
47type Result<T> = std::result::Result<T, ProcfsError>;
48type WorkerHandle = (CancellationToken, JoinHandle<Result<()>>);
49
50#[derive(Debug)]
63pub struct Procfs<B: Backend = ProcfsBackend> {
64 config: ProcfsConfig,
66
67 backend: Arc<B>,
69
70 counters: Arc<Mutex<Counters>>,
72
73 polling_task_handle: Option<WorkerHandle>,
75
76 process_discovery_task_handle: Option<WorkerHandle>,
78
79 mem_total: u64,
81
82 detected_processes: Arc<Mutex<HashSet<i32>>>,
84}
85
86impl Procfs {
87 pub fn new(config: ProcfsConfig) -> Result<Self> {
91 Ok(Self {
92 config,
93 mem_total: 0,
94 backend: Arc::new(ProcfsBackend),
95 counters: Arc::default(),
96 detected_processes: Arc::default(),
97 polling_task_handle: None,
98 process_discovery_task_handle: None,
99 })
100 }
101}
102
103impl<B: Backend> Procfs<B> {
104 fn spawn_polling_worker(
105 backend: Arc<B>,
106 counters: Arc<Mutex<Counters>>,
107 detected_processes: Arc<Mutex<HashSet<i32>>>,
108 poll_interval: Duration,
109 ) -> Result<WorkerHandle> {
110 let mut ticker = Interval::new_interval(poll_interval)?;
111 let cancellation_token = CancellationToken::new();
112 let cancellation_token_clone = cancellation_token.clone();
113
114 let handle = tokio::spawn(async move {
115 debug!("Starting procfs polling.");
116
117 loop {
118 tokio::select! {
119 _ = ticker.next() => {
120 trace!("Polled procfs source.");
121 Self::measure_and_update_pids(&backend, &counters, &detected_processes).await?;
122 }
123 () = cancellation_token.cancelled() => {
124 debug!("procfs worker stopped.");
125 break;
126 }
127 }
128 }
129 Ok(())
130 });
131 Ok((cancellation_token_clone, handle))
132 }
133
134 fn spawn_process_discovery_worker(
135 backend: Arc<B>,
136 pid: i32,
137 detected_processes: Arc<Mutex<HashSet<i32>>>,
138 poll_interval: Duration,
139 ) -> Result<WorkerHandle> {
140 let mut ticker = Interval::new_interval(poll_interval)?;
141 let cancellation_token = CancellationToken::new();
142 let cancellation_token_clone = cancellation_token.clone();
143
144 let handle = tokio::spawn(async move {
145 debug!("Starting procfs process discovery worker.");
146
147 loop {
148 tokio::select! {
149 _ = ticker.next() => {
150 trace!("Polled procfs discovery task.");
151 let backend_clone = backend.clone();
152 let children = spawn_blocking(move || backend_clone.collect_children(pid)).await?;
153 let mut lock = detected_processes.lock().map_err(|_| ProcfsError::MutexPoisoned)?;
154 *lock = children;
155 }
156 () = cancellation_token.cancelled() => {
157 debug!("procfs process discovery worker stopped.");
158 break;
159 }
160 }
161 }
162 Ok(())
163 });
164 Ok((cancellation_token_clone, handle))
165 }
166
167 async fn measure_and_update_pids(
168 backend: &Arc<B>,
169 counters: &Arc<Mutex<Counters>>,
170 detected_processes: &Arc<Mutex<HashSet<i32>>>,
171 ) -> Result<()> {
172 let pids = detected_processes
173 .lock()
174 .map_err(|_| ProcfsError::MutexPoisoned)?
175 .clone();
176
177 let proc_reads: Vec<_> = pids
178 .into_iter()
179 .map(|pid| {
180 let backend = Arc::clone(backend);
181 spawn_blocking(move || {
182 let result = backend.read_proc(pid);
183 (pid, result)
184 })
185 })
186 .collect();
187
188 let backend_global = Arc::clone(backend);
189 let global_read = spawn_blocking(move || backend_global.measure_global());
190
191 let (proc_results, global_result) = try_join!(try_join_all(proc_reads), global_read)?;
192 let global = global_result?;
193
194 let mut present_pids = HashSet::new();
195 let mut snapshot = ProcSnapshot::default();
196 let mut pids_updated = false;
197
198 for (pid, result) in proc_results {
199 match result {
200 Ok(proc_snapshot) => {
201 present_pids.insert(pid);
202 snapshot += proc_snapshot;
203 }
204 Err(ProcfsError::Procfs(procfs::ProcError::NotFound(_))) => {
205 trace!("PID {pid} not present, removing it from processes list.");
206 pids_updated = true;
207 }
208 Err(ProcfsError::Procfs(procfs::ProcError::Incomplete(_))) => {
209 trace!(
210 "PID {pid} data incomplete (process exited during read), removing it from processes list."
211 );
212 pids_updated = true;
213 }
214 Err(err) => return Err(err),
215 }
216 }
217
218 let mut counters = counters.lock().map_err(|_| ProcfsError::MutexPoisoned)?;
219 counters.update(&snapshot, &global);
220
221 if pids_updated {
222 trace!("Updating processes list: {present_pids:?}");
223 let mut lock = detected_processes
224 .lock()
225 .map_err(|_| ProcfsError::MutexPoisoned)?;
226 *lock = present_pids;
227 }
228
229 Ok(())
230 }
231}
232
233impl<B: Backend> MetricReader for Procfs<B> {
234 type Type = Counters;
235 type Error = ProcfsError;
236 type Config = ProcfsConfig;
237
238 fn from_config(config: ProcfsConfig) -> Result<Self> {
239 Ok(Self {
240 config,
241 mem_total: 0,
242 backend: Arc::new(B::default()),
243 counters: Arc::default(),
244 detected_processes: Arc::default(),
245 polling_task_handle: None,
246 process_discovery_task_handle: None,
247 })
248 }
249
250 async fn pre_init(&mut self) -> Result<()> {
251 self.mem_total = self.backend.mem_total()?;
252 Ok(())
253 }
254
255 async fn init(&mut self, pid: i32) -> Result<()> {
257 debug!("Initializing procfs source.");
258
259 let mut lock = self
260 .detected_processes
261 .lock()
262 .map_err(|_| ProcfsError::MutexPoisoned)?;
263 *lock = self.backend.collect_children(pid);
264
265 self.process_discovery_task_handle = Some(Self::spawn_process_discovery_worker(
266 self.backend.clone(),
267 pid,
268 self.detected_processes.clone(),
269 self.config.process_detection_poll_interval,
270 )?);
271
272 let counters = self.counters.clone();
273
274 self.polling_task_handle = Some(Self::spawn_polling_worker(
275 self.backend.clone(),
276 counters,
277 self.detected_processes.clone(),
278 self.config.poll_interval,
279 )?);
280
281 Ok(())
282 }
283
284 async fn join(&mut self) -> Result<()> {
286 debug!("Joining procfs source.");
287 if let Some((cancellation_token, handle)) = self.polling_task_handle.take() {
288 cancellation_token.cancel();
289 handle.await??;
290 }
291 if let Some((cancellation_token, handle)) = self.process_discovery_task_handle.take() {
292 cancellation_token.cancel();
293 handle.await??;
294 }
295 Ok(())
296 }
297
298 async fn measure(&mut self) -> Result<()> {
302 Self::measure_and_update_pids(&self.backend, &self.counters, &self.detected_processes)
303 .await?;
304 Ok(())
305 }
306
307 async fn retrieve(&mut self) -> Result<Self::Type> {
309 let mut lock = self
310 .counters
311 .lock()
312 .map_err(|_| ProcfsError::MutexPoisoned)?;
313 let counters = *lock;
314 lock.reset();
315 Ok(counters)
316 }
317
318 fn get_sensors(&self) -> Result<Sensors> {
319 let proc_memory_unit: MetricUnit = self.config.proc_memory_unit.into();
320 let global_memory_unit: MetricUnit = self.config.global_memory_unit.into();
321
322 let proc_sensors = [
323 "proc_vm_size_min",
324 "proc_vm_size_max",
325 "proc_rss_min",
326 "proc_rss_max",
327 "proc_pss_min",
328 "proc_pss_max",
329 "proc_shared_min",
330 "proc_shared_max",
331 "proc_anon_min",
332 "proc_anon_max",
333 ]
334 .into_iter()
335 .map(|name| Sensor::new(name, proc_memory_unit, Self::get_name()));
336
337 let io_sensors = vec![
338 Sensor::new(
339 "proc_io_read_bytes",
340 IO_COUNTERS_METRIC_UNIT,
341 Self::get_name(),
342 ),
343 Sensor::new(
344 "proc_io_write_bytes",
345 IO_COUNTERS_METRIC_UNIT,
346 Self::get_name(),
347 ),
348 ];
349
350 let global_sensors = [
351 "global_mem_used_min",
352 "global_mem_used_max",
353 "global_cached_min",
354 "global_cached_max",
355 "global_anon_min",
356 "global_anon_max",
357 "global_swap_free_min",
358 "global_swap_free_max",
359 ]
360 .into_iter()
361 .map(|name| Sensor::new(name, global_memory_unit, Self::get_name()));
362
363 Ok(proc_sensors
364 .chain(global_sensors)
365 .chain(io_sensors)
366 .collect())
367 }
368
369 fn to_metrics(&self, counters: Self::Type) -> Result<Metrics> {
370 let proc_unit: MetricUnit = self.config.proc_memory_unit.into();
371 let global_unit: MetricUnit = self.config.global_memory_unit.into();
372
373 let proc = counters.proc;
374
375 let proc_memory_metrics: Metrics = [
376 ("proc_vm_size_min", proc.vm_size.min().unwrap_or_default()),
377 ("proc_vm_size_max", proc.vm_size.max().unwrap_or_default()),
378 ("proc_rss_min", proc.rss.min().unwrap_or_default()),
379 ("proc_rss_max", proc.rss.max().unwrap_or_default()),
380 ("proc_pss_min", proc.pss.min().unwrap_or_default()),
381 ("proc_pss_max", proc.pss.max().unwrap_or_default()),
382 ("proc_shared_min", proc.shared.min().unwrap_or_default()),
383 ("proc_shared_max", proc.shared.max().unwrap_or_default()),
384 ("proc_anon_min", proc.anon.min().unwrap_or_default()),
385 ("proc_anon_max", proc.anon.max().unwrap_or_default()),
386 ]
387 .into_iter()
388 .map(|(name, value)| {
389 let value = make_conversion(self.config.proc_memory_unit, value);
390 Metric::new(name, value, proc_unit, Self::get_name())
391 })
392 .collect();
393
394 let io_metrics: Metrics = [
395 (
396 "proc_io_read_bytes",
397 proc.end_read_bytes.saturating_sub(proc.begin_read_bytes),
398 ),
399 (
400 "proc_io_write_bytes",
401 proc.end_write_bytes.saturating_sub(proc.begin_write_bytes),
402 ),
403 ]
404 .into_iter()
405 .map(|(name, value)| Metric::new(name, value, IO_COUNTERS_METRIC_UNIT, Self::get_name()))
406 .collect();
407
408 let global = counters.global;
409 let global_memory_unit = self.config.global_memory_unit;
410 let mem_used = compute_mem_used(
411 self.mem_total,
412 global.mem_available,
413 global.mem_free,
414 global.cached,
415 );
416
417 let mut global_memory_metrics: Metrics = [
418 (
419 "global_mem_used_min",
420 make_conversion(global_memory_unit, mem_used.min().unwrap_or_default()),
421 ),
422 (
423 "global_mem_used_max",
424 make_conversion(global_memory_unit, mem_used.max().unwrap_or_default()),
425 ),
426 (
427 "global_cached_min",
428 make_conversion(global_memory_unit, global.cached.min().unwrap_or_default()),
429 ),
430 (
431 "global_cached_max",
432 make_conversion(global_memory_unit, global.cached.max().unwrap_or_default()),
433 ),
434 (
435 "global_swap_free_min",
436 make_conversion(
437 global_memory_unit,
438 global.swap_free.min().unwrap_or_default(),
439 ),
440 ),
441 (
442 "global_swap_free_max",
443 make_conversion(
444 global_memory_unit,
445 global.swap_free.max().unwrap_or_default(),
446 ),
447 ),
448 ]
449 .into_iter()
450 .map(|(name, value)| Metric::new(name, value, global_unit, Self::get_name()))
451 .collect();
452
453 if let Some(anon) = global.anon {
454 let anon: Vec<_> = [
455 ("global_anon_min", anon.min().unwrap_or_default()),
456 ("global_anon_max", anon.max().unwrap_or_default()),
457 ]
458 .into_iter()
459 .map(|(name, value)| {
460 Metric::new(
461 name,
462 make_conversion(global_memory_unit, value),
463 global_unit,
464 Self::get_name(),
465 )
466 })
467 .collect();
468 global_memory_metrics.extend(anon);
469 }
470
471 Ok(proc_memory_metrics
472 .into_iter()
473 .chain(global_memory_metrics)
474 .chain(io_metrics)
475 .collect())
476 }
477
478 fn get_name() -> &'static str {
479 PROCFS_SOURCE_NAME
480 }
481
482 fn get_id() -> &'static str {
483 PROCFS_SOURCE_NAME
484 }
485}
486
487#[cfg(test)]
488mod tests {
489 use std::{collections::HashSet, path::PathBuf, sync::Arc, time::Duration};
490
491 use joule_profiler_core::source::MetricReader;
492 use tokio::time::sleep;
493
494 use crate::{
495 Procfs,
496 backend::MockBackend,
497 config::ProcfsConfig,
498 error::ProcfsError,
499 snapshot::{GlobalSnapshot, ProcSnapshot},
500 };
501
502 fn create_source(backend: MockBackend) -> Procfs<MockBackend> {
503 Procfs {
504 backend: Arc::new(backend),
505 config: ProcfsConfig::default(),
506 mem_total: 0,
507 counters: Arc::default(),
508 detected_processes: Arc::default(),
509 polling_task_handle: None,
510 process_discovery_task_handle: None,
511 }
512 }
513
514 #[tokio::test]
515 async fn test_init_function_initializes_field_correctly() {
516 let mem_total = 4096;
517 let pid = 1;
518 let children: HashSet<_> = vec![pid, 2].into_iter().collect();
519
520 let mut backend = MockBackend::new();
521
522 backend
523 .expect_collect_children()
524 .returning(move |_| vec![pid, 2].into_iter().collect());
525
526 backend
527 .expect_mem_total()
528 .once()
529 .returning(move || Ok(mem_total));
530
531 let mut source = create_source(backend);
532
533 source.pre_init().await.unwrap();
534 source.init(pid).await.unwrap();
535
536 assert_eq!(source.mem_total, mem_total);
537 assert!(source.detected_processes.lock().unwrap().eq(&children));
538 }
539
540 #[tokio::test]
541 async fn test_process_detection_polling_updates_processes_list() {
542 let children: HashSet<_> = vec![1, 2, 3, 4].into_iter().collect();
543
544 let mut backend = MockBackend::new();
545 backend
546 .expect_collect_children()
547 .once()
548 .returning(|_| HashSet::new());
549
550 backend
551 .expect_collect_children()
552 .returning(|_| vec![1, 2, 3, 4].into_iter().collect());
553
554 backend.expect_mem_total().once().returning(|| Ok(0));
555 let mut source = create_source(backend);
556 source.config.process_detection_poll_interval = Duration::from_millis(1);
557 assert!(
558 source
559 .detected_processes
560 .lock()
561 .unwrap()
562 .eq(&HashSet::new())
563 );
564
565 source.pre_init().await.unwrap();
566 source.init(1).await.unwrap();
567
568 sleep(Duration::from_millis(5)).await;
569
570 assert!(source.detected_processes.lock().unwrap().eq(&children));
571 }
572
573 #[tokio::test]
574 async fn test_polling_task_updates_counters() {
575 let mut backend = MockBackend::new();
576 backend
577 .expect_collect_children()
578 .returning(|_| vec![1].into_iter().collect());
579
580 backend.expect_measure_global().once().returning(|| {
581 Ok(GlobalSnapshot {
582 anon: Some(100),
583 cached: 200,
584 mem_available: Some(300),
585 mem_free: 400,
586 swap_free: 500,
587 })
588 });
589
590 backend
591 .expect_read_proc()
592 .returning(|_| Ok(ProcSnapshot::default()));
593
594 backend.expect_measure_global().once().returning(|| {
595 Ok(GlobalSnapshot {
596 anon: Some(200),
597 cached: 400,
598 mem_available: Some(600),
599 mem_free: 800,
600 swap_free: 1000,
601 })
602 });
603
604 backend.expect_mem_total().once().returning(|| Ok(0));
605
606 let mut source = create_source(backend);
607 let global_counters = source.counters.lock().unwrap().global;
608 assert!(global_counters.anon.is_none());
609 assert!(global_counters.cached.min().is_none());
610 assert!(global_counters.cached.max().is_none());
611 assert!(global_counters.mem_available.is_none());
612 assert!(global_counters.mem_free.min().is_none());
613 assert!(global_counters.mem_free.max().is_none());
614 assert!(global_counters.swap_free.min().is_none());
615 assert!(global_counters.swap_free.max().is_none());
616
617 source.config.poll_interval = Duration::from_millis(10);
618 source.pre_init().await.unwrap();
619 source.init(1).await.unwrap();
620
621 sleep(Duration::from_millis(20)).await;
622
623 let global_counters = source.counters.lock().unwrap().global;
624
625 assert_eq!(global_counters.anon.unwrap().min(), Some(100));
626 assert_eq!(global_counters.anon.unwrap().max(), Some(200));
627 assert_eq!(global_counters.cached.min(), Some(200));
628 assert_eq!(global_counters.cached.max(), Some(400));
629 assert_eq!(global_counters.mem_available.unwrap().min(), Some(300));
630 assert_eq!(global_counters.mem_available.unwrap().max(), Some(600));
631 assert_eq!(global_counters.mem_free.min(), Some(400));
632 assert_eq!(global_counters.mem_free.max(), Some(800));
633 assert_eq!(global_counters.swap_free.min(), Some(500));
634 assert_eq!(global_counters.swap_free.max(), Some(1000));
635 }
636
637 #[tokio::test]
638 async fn test_measure_updates_counters_once() {
639 let mut backend = MockBackend::new();
640
641 backend
642 .expect_collect_children()
643 .returning(|_| vec![1].into_iter().collect());
644
645 backend
646 .expect_read_proc()
647 .returning(|_| Ok(ProcSnapshot::default()));
648
649 backend.expect_measure_global().returning(|| {
650 Ok(GlobalSnapshot {
651 anon: Some(100),
652 cached: 200,
653 mem_available: Some(300),
654 mem_free: 400,
655 swap_free: 500,
656 })
657 });
658
659 backend.expect_mem_total().once().returning(|| Ok(1000));
660
661 let mut source = create_source(backend);
662
663 source.pre_init().await.unwrap();
664 source.init(1).await.unwrap();
665 source.measure().await.unwrap();
666
667 let counters = source.counters.lock().unwrap();
668
669 assert_eq!(counters.global.cached.max(), Some(200));
670 assert_eq!(counters.global.cached.max(), Some(200));
671 assert_eq!(counters.global.mem_available.unwrap().max(), Some(300));
672 assert_eq!(counters.global.mem_free.max(), Some(400));
673 assert_eq!(counters.global.swap_free.max(), Some(500));
674 }
675
676 #[tokio::test]
677 async fn test_retrieve_resets_counters() {
678 let mut backend = MockBackend::new();
679
680 backend
681 .expect_collect_children()
682 .returning(|_| vec![1].into_iter().collect());
683
684 backend
685 .expect_read_proc()
686 .returning(|_| Ok(ProcSnapshot::default()));
687
688 backend.expect_measure_global().returning(|| {
689 Ok(GlobalSnapshot {
690 anon: Some(10),
691 cached: 20,
692 mem_available: Some(30),
693 mem_free: 40,
694 swap_free: 50,
695 })
696 });
697
698 backend.expect_mem_total().once().returning(|| Ok(1000));
699
700 let mut source = create_source(backend);
701 source.pre_init().await.unwrap();
702 source.init(1).await.unwrap();
703
704 source.measure().await.unwrap();
705
706 let first = source.retrieve().await.unwrap();
707 let second = source.retrieve().await.unwrap();
708
709 assert!(first.global.cached.max().is_some());
710 assert!(second.global.cached.max().is_none());
711 }
712
713 #[tokio::test]
714 async fn test_dead_pid_removed_from_detected_processes() {
715 use procfs::ProcError;
716
717 let mut backend = MockBackend::new();
718
719 backend
720 .expect_collect_children()
721 .returning(|_| vec![1, 2].into_iter().collect());
722
723 backend.expect_read_proc().returning(|pid| {
724 if pid == 2 {
725 Err(ProcfsError::Procfs(ProcError::NotFound(Some(
726 PathBuf::new(),
727 ))))
728 } else {
729 Ok(ProcSnapshot::default())
730 }
731 });
732
733 backend
734 .expect_measure_global()
735 .returning(|| Ok(GlobalSnapshot::default()));
736
737 backend.expect_mem_total().once().returning(|| Ok(1000));
738
739 let mut source = create_source(backend);
740 source.pre_init().await.unwrap();
741 source.init(1).await.unwrap();
742
743 source.measure().await.unwrap();
744
745 let processes = source.detected_processes.lock().unwrap();
746
747 assert!(processes.contains(&1));
748 assert!(!processes.contains(&2));
749 }
750
751 #[tokio::test]
752 async fn test_join_stops_polling_task() {
753 let mut backend = MockBackend::new();
754
755 backend
756 .expect_collect_children()
757 .returning(|_| vec![1].into_iter().collect());
758
759 backend.expect_mem_total().once().returning(|| Ok(0));
760
761 backend
762 .expect_read_proc()
763 .returning(|_| Ok(ProcSnapshot::default()));
764
765 backend
766 .expect_measure_global()
767 .returning(|| Ok(GlobalSnapshot::default()));
768
769 let mut source = create_source(backend);
770 source.config.poll_interval = Duration::from_millis(5);
771 source.pre_init().await.unwrap();
772 source.init(1).await.unwrap();
773 sleep(Duration::from_millis(15)).await;
774
775 source.join().await.unwrap();
776 }
777}