use std::collections::HashSet;
use std::time::Duration;
use sysinfo::{Pid, Process, ProcessesToUpdate, System};
pub(crate) struct ResourceSampler {
handle: tokio::task::JoinHandle<()>,
}
fn pigeon_process_tree(system: &System, root: Pid) -> Vec<&Process> {
let mut ids: HashSet<Pid> = HashSet::from([root]);
loop {
let before = ids.len();
for (pid, process) in system.processes() {
if !ids.contains(pid) && process.parent().is_some_and(|parent| ids.contains(&parent)) {
ids.insert(*pid);
}
}
if ids.len() == before {
break;
}
}
ids.into_iter()
.filter_map(|pid| system.process(pid))
.collect()
}
impl ResourceSampler {
pub(crate) fn spawn(interval: Duration) -> Self {
let handle = tokio::spawn(async move {
let Ok(pid) = sysinfo::get_current_pid() else {
return;
};
let mut system = System::new();
loop {
tokio::time::sleep(interval).await;
system.refresh_processes(ProcessesToUpdate::All, true);
let processes = pigeon_process_tree(&system, pid);
if processes.is_empty() {
continue;
}
let cpu_percent: f64 = processes.iter().map(|p| p.cpu_usage() as f64).sum();
let mem_bytes: f64 = processes.iter().map(|p| p.memory() as f64).sum();
let (read_bytes, written_bytes) =
processes
.iter()
.fold((0u64, 0u64), |(read, written), process| {
let disk = process.disk_usage();
(read + disk.read_bytes, written + disk.written_bytes)
});
let instance = crate::observability::instance();
metrics::gauge!("pigeon_resource_cpu_percent", "instance" => instance)
.set(cpu_percent);
metrics::gauge!("pigeon_resource_mem_bytes", "instance" => instance).set(mem_bytes);
metrics::counter!("pigeon_resource_disk_read_bytes_total", "instance" => instance)
.increment(read_bytes);
metrics::counter!("pigeon_resource_disk_written_bytes_total", "instance" => instance)
.increment(written_bytes);
}
});
Self { handle }
}
}
impl Drop for ResourceSampler {
fn drop(&mut self) {
self.handle.abort();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn dropping_the_sampler_stops_its_sampling_task() {
let sampler = ResourceSampler::spawn(Duration::from_millis(5));
tokio::time::sleep(Duration::from_millis(20)).await;
let abort_handle = sampler.handle.abort_handle();
drop(sampler);
tokio::time::sleep(Duration::from_millis(50)).await;
assert!(abort_handle.is_finished());
}
#[tokio::test]
async fn pigeon_process_tree_includes_a_spawned_child_process() {
let mut child = tokio::process::Command::new("sleep")
.arg("2")
.spawn()
.expect("failed to spawn test child process");
let child_pid = Pid::from_u32(child.id().expect("spawned child has a pid"));
let root_pid = sysinfo::get_current_pid().expect("failed to get current pid");
tokio::time::sleep(Duration::from_millis(100)).await;
let mut system = System::new();
system.refresh_processes(ProcessesToUpdate::All, true);
let tree = pigeon_process_tree(&system, root_pid);
let tree_pids: HashSet<Pid> = tree.iter().map(|process| process.pid()).collect();
let _ = child.kill().await;
let _ = child.wait().await;
assert!(tree_pids.contains(&root_pid));
assert!(tree_pids.contains(&child_pid));
}
}