Skip to main content

joule_profiler_source_procfs/
lib.rs

1//! Procfs metric source for Joule Profiler.
2//!
3//! Provides memory and I/O metrics for a process and it's children and system-wide,
4//! by reading Linux's `/proc` filesystem via the `procfs` crate.
5
6use 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/// Procfs-based metric source.
51///
52/// Reads memory and I/O metrics from `/proc` for a target process and its
53/// entire child hierarchy, as well as system-wide memory metrics.
54///
55/// # Example
56///
57/// ```no_run
58/// use joule_profiler_source_procfs::{Procfs, config::ProcfsConfig};
59///
60/// let source = Procfs::new(ProcfsConfig::default()).unwrap();
61/// ```
62#[derive(Debug)]
63pub struct Procfs<B: Backend = ProcfsBackend> {
64    /// procfs source configuration.
65    config: ProcfsConfig,
66
67    /// The backend used to interract with procfs. Used for testing.
68    backend: Arc<B>,
69
70    /// Current metrics counters.
71    counters: Arc<Mutex<Counters>>,
72
73    /// The handle to the polling task and it's cancellation token.
74    polling_task_handle: Option<WorkerHandle>,
75
76    /// The handle to the polling task and it's cancellation token.
77    process_discovery_task_handle: Option<WorkerHandle>,
78
79    /// Total physical memory in bytes, read once at construction from `/proc/meminfo`.
80    mem_total: u64,
81
82    /// The current processes to be measured, updated at each poll from the process discovery task.
83    detected_processes: Arc<Mutex<HashSet<i32>>>,
84}
85
86impl Procfs {
87    /// Creates a new `Procfs` source from the given configuration.
88    ///
89    /// Reads `MemTotal` from `/proc/meminfo` at construction time.
90    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    /// Initializes the source to `pid` and starts the background poller if a `poll_interval` is configured.
256    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    /// Aborts the background poller if still running, or propagates its error if it already finished.
285    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    /// Takes a single snapshot of process and global counters and updates them.
299    ///
300    /// If the process is not found, an empty snapshot is used.
301    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    /// Returns the accumulated counters for the current phase and resets them.
308    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}