use crate::metrics::{CpuMetrics, MetricsLog};
use chrono::Utc;
use std::{
ops::Deref,
sync::{Arc, Mutex},
};
use sysinfo::{Pid, System};
use tokio::time::Duration;
pub async fn keep_logging(pids: Vec<u32>, metrics_log: Arc<Mutex<MetricsLog>>) {
let mut system = System::new_all();
loop {
tokio::time::sleep(Duration::from_millis(1000)).await;
for pid in pids.iter() {
let metrics = get_metrics(&mut system, *pid).await;
update_metrics_log(metrics, &metrics_log);
}
}
}
fn update_metrics_log(metrics: anyhow::Result<CpuMetrics>, metrics_log: &Arc<Mutex<MetricsLog>>) {
match metrics {
Ok(metrics) => metrics_log
.lock()
.expect("Should be able to acquire lock on metrics log")
.push_metrics(metrics),
Err(error) => metrics_log
.lock()
.expect("Should be able to acquire lock on metrics err")
.push_error(error),
}
}
async fn get_metrics(system: &mut System, pid: u32) -> anyhow::Result<CpuMetrics> {
system.refresh_all();
if let Some(process) = system.process(Pid::from_u32(pid)) {
let core_count = num_cpus::get() as i32;
let cpu_usage = if core_count > 0 {
(process.cpu_usage() as f64) / (core_count as f64)
} else {
process.cpu_usage() as f64
};
let timestamp = Utc::now().timestamp_millis();
let process_name: String = process
.exe()
.map(|path| path.to_string_lossy().into_owned())
.unwrap_or_else(|| {
let process_name = process.name().to_os_string();
let name_str = process_name.to_string_lossy();
name_str.deref().to_string()
});
let metrics = CpuMetrics {
process_id: format!("{pid}"),
process_name,
cpu_usage,
core_count,
timestamp,
};
Ok(metrics)
} else {
Err(anyhow::anyhow!(format!("process with id {pid} not found")))
}
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::Context;
use subprocess::Exec;
use tokio::time::{sleep, Duration};
#[tokio::test]
#[cfg(target_family = "windows")]
async fn metrics_can_be_gatered_using_process_id() -> anyhow::Result<()> {
let mut proc = Exec::cmd("powershell")
.arg("-Command")
.arg(r#"while($true) {get-random | out-null}"#)
.detached()
.popen()
.context("Failed to spawn detached process")?;
let pid = proc.pid().context("Process should have a pid")?;
let mut system = System::new_all();
let mut metrics_log = vec![];
let iterations = 50;
for _ in 0..iterations {
let metrics = get_metrics(&mut system, pid).await?;
metrics_log.push(metrics);
sleep(Duration::from_millis(200)).await;
}
proc.kill().context("Failed to kill process")?;
assert_eq!(metrics_log.len(), iterations);
let cpu_usage = metrics_log.iter().fold(0_f64, |acc, metrics| {
acc + metrics.cpu_usage / metrics.core_count as f64
}) / iterations as f64;
println!("{cpu_usage}");
assert!(cpu_usage > 0_f64);
Ok(())
}
#[tokio::test]
#[cfg(target_family = "windows")]
async fn should_return_err_if_wrong_pid() {
let mut system = System::new_all();
system.refresh_all();
let mut rand_pid = 1337;
loop {
if !system.processes().contains_key(&Pid::from_u32(rand_pid)) {
break;
} else {
rand_pid += 1;
}
}
let res = get_metrics(&mut system, rand_pid).await;
assert!(res.is_err());
}
#[tokio::test]
#[cfg(target_family = "unix")]
async fn metrics_can_be_gatered_using_process_id() -> anyhow::Result<()> {
use subprocess::NullFile;
let mut proc = Exec::cmd("bash")
.arg("-c")
.arg("while true; do shuf -i 0-1337 -n 1; done")
.detached()
.stdout(NullFile)
.popen()
.context("Failed to spawn detached process")?;
let pid = proc.pid().context("Process should have a pid")?;
let mut system = System::new_all();
let mut metrics_log = vec![];
let iterations = 50;
for _ in 0..iterations {
let metrics = get_metrics(&mut system, pid).await?;
metrics_log.push(metrics);
sleep(Duration::from_millis(200)).await;
}
proc.kill().context("Failed to kill process")?;
assert_eq!(metrics_log.len(), iterations);
let cpu_usage = metrics_log.iter().fold(0_f64, |acc, metrics| {
acc + metrics.cpu_usage / metrics.core_count as f64
}) / iterations as f64;
println!("{cpu_usage}");
assert!(cpu_usage > 0_f64);
Ok(())
}
}