use std::sync::Arc;
use std::time::Duration;
use sysinfo::{CpuRefreshKind, Pid, ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System};
use tokio::sync::Mutex;
use tokio::time::sleep;
use tracing::debug;
#[derive(Debug, Clone, Default)]
pub struct CPUStats {
pub physical_core_count: u32,
pub logical_core_count: u32,
pub used_percent: f64,
}
#[derive(Debug, Clone, Default)]
pub struct ProcessCPUStats {
pub used_percent: f64,
}
#[derive(Debug, Clone, Default)]
pub struct CgroupCPUStats {
pub period: u64,
pub quota: i64,
pub used_percent: f64,
}
#[derive(Debug, Clone, Default)]
pub struct CPU {
physical_core_count: u32,
logical_core_count: u32,
mutex: Arc<Mutex<()>>,
}
impl CPU {
#[allow(dead_code)]
const DEFAULT_CPU_REFRESH_INTERVAL: Duration = Duration::from_millis(500);
pub fn new() -> Self {
Self {
physical_core_count: num_cpus::get_physical() as u32,
logical_core_count: num_cpus::get() as u32,
mutex: Arc::new(Mutex::new(())),
}
}
pub async fn get_stats(&self) -> CPUStats {
let _guard = self.mutex.lock().await;
let mut sys = System::new_with_specifics(
RefreshKind::new().with_cpu(CpuRefreshKind::new().with_cpu_usage()),
);
sys.refresh_cpu_usage();
sleep(Self::DEFAULT_CPU_REFRESH_INTERVAL).await;
sys.refresh_cpu_usage();
debug!(
"physical core count: {}, logical core count: {}, global cpu usage: {}%",
self.physical_core_count,
self.logical_core_count,
sys.global_cpu_usage()
);
CPUStats {
physical_core_count: self.physical_core_count,
logical_core_count: self.logical_core_count,
used_percent: sys.global_cpu_usage() as f64,
}
}
pub async fn get_process_stats(&self, pid: u32) -> ProcessCPUStats {
let _guard = self.mutex.lock().await;
let mut sys = System::new_with_specifics(
RefreshKind::new().with_processes(ProcessRefreshKind::new().with_cpu()),
);
sys.refresh_processes(ProcessesToUpdate::Some(&[Pid::from_u32(pid)]), false);
sleep(Self::DEFAULT_CPU_REFRESH_INTERVAL).await;
sys.refresh_processes(ProcessesToUpdate::Some(&[Pid::from_u32(pid)]), false);
let cpu_usage = sys.process(Pid::from_u32(pid)).unwrap().cpu_usage();
let used_percent = cpu_usage as f64 / self.logical_core_count as f64;
debug!(
"process {} cpu usage: {}%, logical core count: {}, used percent: {}%",
pid, cpu_usage, self.logical_core_count, used_percent
);
ProcessCPUStats {
used_percent: used_percent.clamp(0.0, 100.0),
}
}
#[allow(unused_variables)]
pub async fn get_cgroup_stats(&self, pid: u32) -> Option<CgroupCPUStats> {
let _guard = self.mutex.lock().await;
#[cfg(target_os = "linux")]
{
use crate::cgroups::get_cgroup_by_pid;
use cgroups_rs::fs::cpu::CpuController;
use tracing::error;
match get_cgroup_by_pid(pid) {
Ok(cgroup) => {
let cpu_controller = cgroup.controller_of::<CpuController>()?;
let (Ok(period), Ok(quota)) =
(cpu_controller.cfs_period(), cpu_controller.cfs_quota())
else {
return None;
};
let used_percent = self
.calculate_cgroup_used_percent(&cgroup, period, quota)
.await?;
debug!(
"process {} cgroup cpu period: {} us, quota: {} us, used percent: {}%",
pid, period, quota, used_percent
);
Some(CgroupCPUStats {
period,
quota,
used_percent,
})
}
Err(err) => {
error!("failed to get cgroup for pid {}: {}", pid, err);
None
}
}
}
#[cfg(not(target_os = "linux"))]
None
}
#[cfg(target_os = "linux")]
async fn calculate_cgroup_used_percent(
&self,
cgroup: &cgroups_rs::fs::Cgroup,
period: u64,
quota: i64,
) -> Option<f64> {
let initial_usage = self.get_cgroup_usage(cgroup)?;
tokio::time::sleep(Self::DEFAULT_CPU_REFRESH_INTERVAL).await;
let final_usage = self.get_cgroup_usage(cgroup)?;
let consumed = final_usage.saturating_sub(initial_usage);
let interval_ns = Self::DEFAULT_CPU_REFRESH_INTERVAL.as_nanos() as u64;
let capacity = if quota > 0 {
let quota_ns = (quota as u64) * 1000;
let period_ns = period * 1000;
(interval_ns * quota_ns) / period_ns
} else {
interval_ns * self.logical_core_count as u64
};
if capacity > 0 {
let percent = (consumed as f64 / capacity as f64) * 100.0;
Some(percent.clamp(0.0, 100.0))
} else {
None
}
}
#[cfg(target_os = "linux")]
fn get_cgroup_usage(&self, cgroup: &cgroups_rs::fs::Cgroup) -> Option<u64> {
use cgroups_rs::fs::{cpu::CpuController, cpuacct::CpuAcctController};
if let Some(cpuacct) = cgroup.controller_of::<CpuAcctController>() {
return Some(cpuacct.cpuacct().usage);
}
if let Some(cpu) = cgroup.controller_of::<CpuController>() {
let stat = cpu.cpu().stat;
if let Some(usage_usec) = self.parse_usage_usec(&stat) {
return Some(usage_usec * 1000);
}
}
None
}
#[cfg(target_os = "linux")]
fn parse_usage_usec(&self, stat: &str) -> Option<u64> {
for line in stat.lines() {
let line = line.trim();
if line.starts_with("usage_usec") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
return parts[1].parse::<u64>().ok();
}
}
}
None
}
}