use std::fs;
use std::sync::Mutex;
pub trait CPUUtilization {
fn get_cpu_utilization(&self) -> f64;
}
pub trait ProcessCPUUtilization {
fn get_process_cpu_utilization(&mut self, pid: u32) -> f64;
fn get_process_cpu_utilization_with_total(&mut self, pid: u32, cpu_total: Option<u64>) -> f64 {
let _ = cpu_total;
self.get_process_cpu_utilization(pid)
}
}
#[allow(dead_code)]
pub struct CpuState {
pub total: u64,
pub idle: u64,
}
#[allow(dead_code)]
impl CpuState {
pub fn new(total: u64, idle: u64) -> Self {
Self { total, idle }
}
pub fn cpu_usage(&self, prev: &CpuState) -> f64 {
let diff_total = self.total.saturating_sub(prev.total);
let diff_idle = self.idle.saturating_sub(prev.idle);
if diff_total == 0 {
0.0
} else {
(diff_total - diff_idle) as f64 / diff_total as f64
}
}
}
#[allow(dead_code)]
pub struct ProcStatCpuUsage {
stats: Mutex<CpuState>,
}
impl Default for ProcStatCpuUsage {
fn default() -> Self {
Self::new()
}
}
#[allow(dead_code)]
impl ProcStatCpuUsage {
pub fn new() -> Self {
let (total, idle) = read_proc_stat().unwrap_or((0, 0));
Self {
stats: Mutex::new(CpuState::new(total, idle)),
}
}
}
#[allow(dead_code)]
impl CPUUtilization for ProcStatCpuUsage {
fn get_cpu_utilization(&self) -> f64 {
let (curr_total, curr_idle) = read_proc_stat().unwrap_or((0, 0));
let current_state = CpuState::new(curr_total, curr_idle);
let mut last = self.stats.lock().unwrap_or_else(|e| e.into_inner());
let usage = current_state.cpu_usage(&last);
*last = current_state;
usage
}
}
#[allow(dead_code)]
pub(crate) fn read_proc_stat() -> Option<(u64, u64)> {
let content = fs::read_to_string("/proc/stat").ok()?;
let line = content.lines().next()?;
if !line.starts_with("cpu ") {
return None;
}
let parts: Vec<u64> = line
.split_whitespace()
.skip(1)
.filter_map(|s| s.parse().ok())
.collect();
if parts.len() < 8 {
return None;
}
let user = parts[0];
let nice = parts[1];
let system = parts[2];
let idle = parts[3];
let iowait = parts[4];
let irq = parts[5];
let softirq = parts[6];
let steal = parts[7];
let total_ticks = user + nice + system + idle + iowait + irq + softirq + steal;
let idle_ticks = idle + iowait;
Some((total_ticks, idle_ticks))
}
#[allow(dead_code)]
pub struct ProcStatProcessUtil {
before_cpu_total: u64,
before_pid_time: u64,
}
impl Default for ProcStatProcessUtil {
fn default() -> Self {
Self::new()
}
}
#[allow(dead_code)]
impl ProcStatProcessUtil {
pub fn new() -> Self {
Self {
before_cpu_total: 0,
before_pid_time: 0,
}
}
fn read_pid_time(pid: u32) -> Option<u64> {
let stat_path = format!("/proc/{}/stat", pid);
let content = fs::read_to_string(&stat_path).ok()?;
let last_paren = content.rfind(')')?;
let fields: Vec<&str> = content[last_paren + 1..].split_whitespace().collect();
if fields.len() < 13 {
return None;
}
let utime: u64 = fields[11].parse().ok()?;
let stime: u64 = fields[12].parse().ok()?;
Some(utime + stime)
}
}
#[allow(dead_code)]
impl ProcessCPUUtilization for ProcStatProcessUtil {
fn get_process_cpu_utilization(&mut self, pid: u32) -> f64 {
let (cpu_total, _) = match read_proc_stat() {
Some(vals) => vals,
None => return 0.0,
};
let pid_time = match Self::read_pid_time(pid) {
Some(time) => time,
None => return 0.0,
};
if self.before_cpu_total == 0 {
self.before_cpu_total = cpu_total;
self.before_pid_time = pid_time;
return 0.0;
}
let cpu_delta = cpu_total.saturating_sub(self.before_cpu_total);
let pid_delta = pid_time.saturating_sub(self.before_pid_time);
self.before_cpu_total = cpu_total;
self.before_pid_time = pid_time;
if cpu_delta == 0 {
0.0
} else {
pid_delta as f64 / cpu_delta as f64
}
}
fn get_process_cpu_utilization_with_total(&mut self, pid: u32, cpu_total: Option<u64>) -> f64 {
let cpu_total = match cpu_total {
Some(total) => total,
None => match read_proc_stat() {
Some((total, _)) => total,
None => return 0.0,
},
};
let pid_time = match Self::read_pid_time(pid) {
Some(time) => time,
None => return 0.0,
};
if self.before_cpu_total == 0 {
self.before_cpu_total = cpu_total;
self.before_pid_time = pid_time;
return 0.0;
}
let cpu_delta = cpu_total.saturating_sub(self.before_cpu_total);
let pid_delta = pid_time.saturating_sub(self.before_pid_time);
self.before_cpu_total = cpu_total;
self.before_pid_time = pid_time;
if cpu_delta == 0 {
0.0
} else {
pid_delta as f64 / cpu_delta as f64
}
}
}
use std::collections::{HashMap, HashSet};
use sysinfo::{ProcessesToUpdate, System};
pub trait AppCPUUtilization {
fn get_app_cpu_utilization(&mut self, app_name: &str) -> f64;
fn get_app_pids(&mut self, app_name: &str) -> Vec<u32>;
fn get_app_snapshot(&mut self, app_name: &str) -> (f64, Vec<u32>) {
let util = self.get_app_cpu_utilization(app_name);
let pids = self.get_app_pids(app_name);
(util, pids)
}
fn get_app_snapshot_with_total(
&mut self,
app_name: &str,
cpu_total: Option<u64>,
) -> (f64, Vec<u32>) {
let _ = cpu_total;
self.get_app_snapshot(app_name)
}
fn set_refresh_interval(&mut self, _interval: std::time::Duration) {}
}
pub struct AppMonitor<T: ProcessCPUUtilization> {
process_trackers: HashMap<u32, T>,
tracker_factory: fn() -> T,
system: System,
last_pid_sweep: Option<std::time::Instant>,
cached_pids: Vec<u32>,
sweep_interval: std::time::Duration,
}
#[allow(dead_code)]
impl<T: ProcessCPUUtilization> AppMonitor<T> {
pub fn new(tracker_factory: fn() -> T, sweep_interval: std::time::Duration) -> Self {
Self {
process_trackers: HashMap::new(),
tracker_factory,
system: System::new(),
last_pid_sweep: None,
cached_pids: Vec::new(),
sweep_interval,
}
}
fn get_pids_from_sysinfo(&mut self, app_name: &str) -> Vec<u32> {
if self.sweep_interval.as_secs() > 0
&& let Some(last) = self.last_pid_sweep
&& last.elapsed() < self.sweep_interval
{
return self.cached_pids.clone();
}
self.system.refresh_processes(ProcessesToUpdate::All, true);
let mut pids = Vec::new();
for (pid, process) in self.system.processes() {
if process.thread_kind().is_some() {
continue;
}
let name = process.name().to_string_lossy();
if name == app_name || name.contains(app_name) {
pids.push(pid.as_u32());
}
}
if self.sweep_interval.as_secs() > 0 {
self.cached_pids = pids.clone();
self.last_pid_sweep = Some(std::time::Instant::now());
}
pids
}
fn update_trackers(&mut self, current_pids: &[u32]) {
let pid_set: HashSet<u32> = current_pids.iter().copied().collect();
self.process_trackers.retain(|pid, _| pid_set.contains(pid));
for &pid in current_pids {
if !self.process_trackers.contains_key(&pid) {
self.process_trackers.insert(pid, (self.tracker_factory)());
}
}
}
fn calculate_total_utilization(
&mut self,
app_name: &str,
cpu_total: Option<u64>,
) -> (f64, Vec<u32>) {
let current_pids = self.get_pids_from_sysinfo(app_name);
if current_pids.is_empty() {
self.process_trackers.clear();
return (0.0, current_pids);
}
self.update_trackers(¤t_pids);
let cpu_total = match cpu_total.or_else(|| read_proc_stat().map(|(total, _)| total)) {
Some(total) => Some(total),
None => {
tracing::debug!(
"Failed to read /proc/stat; application CPU utilization will be 0 for this sample"
);
None
}
};
let mut total_utilization = 0.0;
for &pid in ¤t_pids {
if let Some(tracker) = self.process_trackers.get_mut(&pid) {
total_utilization += tracker.get_process_cpu_utilization_with_total(pid, cpu_total);
}
}
(total_utilization, current_pids)
}
}
impl<T: ProcessCPUUtilization> AppCPUUtilization for AppMonitor<T> {
fn set_refresh_interval(&mut self, interval: std::time::Duration) {
self.sweep_interval = interval;
}
fn get_app_cpu_utilization(&mut self, app_name: &str) -> f64 {
self.calculate_total_utilization(app_name, None).0
}
fn get_app_pids(&mut self, app_name: &str) -> Vec<u32> {
self.get_pids_from_sysinfo(app_name)
}
fn get_app_snapshot(&mut self, app_name: &str) -> (f64, Vec<u32>) {
self.calculate_total_utilization(app_name, None)
}
fn get_app_snapshot_with_total(
&mut self,
app_name: &str,
cpu_total: Option<u64>,
) -> (f64, Vec<u32>) {
self.calculate_total_utilization(app_name, cpu_total)
}
}