mod process;
use std::{
fs::{self, File},
io::{BufRead, BufReader},
time::Duration,
};
use concat_string::concat_string;
use itertools::Itertools;
use process::*;
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
use sysinfo::ProcessStatus;
use super::{Pid, ProcessHarvest, UserTable, process_status_str};
use crate::collection::{DataCollector, error::CollectionResult, processes::ProcessType};
const MAX_STAT_NAME_LEN: usize = 15;
#[derive(Debug, Clone, Default)]
pub struct PrevProcDetails {
total_read_bytes: u64,
total_write_bytes: u64,
cpu_time: u64,
}
fn fetch_cpu_usage(line: &str) -> (f64, f64) {
fn str_to_f64(val: Option<&str>) -> f64 {
val.and_then(|v| v.parse::<f64>().ok()).unwrap_or(0_f64)
}
let mut val = line.split_whitespace();
let user = str_to_f64(val.next());
let nice: f64 = str_to_f64(val.next());
let system: f64 = str_to_f64(val.next());
let idle: f64 = str_to_f64(val.next());
let iowait: f64 = str_to_f64(val.next());
let irq: f64 = str_to_f64(val.next());
let softirq: f64 = str_to_f64(val.next());
let steal: f64 = str_to_f64(val.next());
let idle = idle + iowait;
let non_idle = user + nice + system + irq + softirq + steal;
(idle, non_idle)
}
struct CpuUsage {
cpu_usage: f64,
cpu_fraction: f64,
}
fn cpu_usage_calculation(
prev_idle: &mut f64, prev_non_idle: &mut f64,
) -> CollectionResult<CpuUsage> {
let (idle, non_idle) = {
let first_line = {
let mut reader = BufReader::new(File::open("/proc/stat")?);
let mut buffer = String::new();
reader.read_line(&mut buffer)?;
buffer
};
fetch_cpu_usage(&first_line)
};
let total = idle + non_idle;
let prev_total = *prev_idle + *prev_non_idle;
let total_delta = total - prev_total;
let idle_delta = idle - *prev_idle;
*prev_idle = idle;
*prev_non_idle = non_idle;
let cpu_usage = if total_delta - idle_delta != 0.0 {
total_delta - idle_delta
} else {
1.0
};
let cpu_fraction = if total_delta != 0.0 {
cpu_usage / total_delta
} else {
0.0
};
Ok(CpuUsage {
cpu_usage,
cpu_fraction,
})
}
fn get_linux_cpu_usage(
stat: &Stat, cpu_usage: f64, cpu_fraction: f64, prev_proc_times: u64,
use_current_cpu_total: bool,
) -> (f32, u64) {
let new_proc_times = stat.utime + stat.stime;
let diff = (new_proc_times - prev_proc_times) as f64;
if cpu_usage == 0.0 {
(0.0, new_proc_times)
} else if use_current_cpu_total {
(((diff / cpu_usage) * 100.0) as f32, new_proc_times)
} else {
(
((diff / cpu_usage) * 100.0 * cpu_fraction) as f32,
new_proc_times,
)
}
}
fn read_proc(
prev_proc: &PrevProcDetails, process: Process, args: ReadProcArgs, user_table: &mut UserTable,
thread_parent: Option<Pid>,
) -> CollectionResult<(ProcessHarvest, u64)> {
let Process {
pid: _pid,
uid,
stat,
io,
cmdline,
} = process;
let ReadProcArgs {
use_current_cpu_total,
cpu_usage,
cpu_fraction,
total_memory,
time_difference_in_secs,
system_uptime,
get_process_threads: _,
} = args;
let process_state_char = stat.state;
let process_state = (
process_status_str(ProcessStatus::from(process_state_char)),
process_state_char,
);
let (cpu_usage_percent, new_process_times) = get_linux_cpu_usage(
&stat,
cpu_usage,
cpu_fraction,
prev_proc.cpu_time,
use_current_cpu_total,
);
let (parent_pid, process_type) = if let Some(thread_parent) = thread_parent {
(Some(thread_parent), ProcessType::ProcessThread)
} else if stat.is_kernel_thread {
(Some(stat.ppid), ProcessType::Kernel)
} else {
(Some(stat.ppid), ProcessType::Regular)
};
let mem_usage = stat.rss_bytes();
let mem_usage_percent = (mem_usage as f64 / total_memory as f64 * 100.0) as f32;
let virtual_mem = stat.vsize;
let (total_read, total_write, read_per_sec, write_per_sec) = if let Some(io) = io {
let total_read = io.read_bytes;
let total_write = io.write_bytes;
let prev_total_read = prev_proc.total_read_bytes;
let prev_total_write = prev_proc.total_write_bytes;
let read_per_sec = total_read
.saturating_sub(prev_total_read)
.checked_div(time_difference_in_secs)
.unwrap_or(0);
let write_per_sec = total_write
.saturating_sub(prev_total_write)
.checked_div(time_difference_in_secs)
.unwrap_or(0);
(total_read, total_write, read_per_sec, write_per_sec)
} else {
(0, 0, 0, 0)
};
let user = uid.and_then(|uid| user_table.uid_to_username(uid).ok());
let time = if let Ok(ticks_per_sec) = u32::try_from(rustix::param::clock_ticks_per_second()) {
if ticks_per_sec == 0 {
Duration::ZERO
} else {
Duration::from_secs(
system_uptime.saturating_sub(stat.start_time / ticks_per_sec as u64),
)
}
} else {
Duration::ZERO
};
let (command, name) = {
let comm = stat.comm;
if let Some(cmdline) = cmdline {
if cmdline.is_empty() {
(concat_string!("[", comm, "]"), comm)
} else {
let name = if comm.len() >= MAX_STAT_NAME_LEN {
binary_name_from_cmdline(&cmdline)
} else {
comm
};
(cmdline, name)
}
} else {
(comm.clone(), comm)
}
};
let mut command = command;
let buf_mut = unsafe { command.as_mut_vec() };
for byte in buf_mut {
if *byte == 0 {
const SPACE: u8 = ' '.to_ascii_lowercase() as u8;
*byte = SPACE;
}
}
Ok((
ProcessHarvest {
pid: process.pid,
parent_pid,
cpu_usage_percent,
mem_usage_percent,
mem_usage,
virtual_mem,
name,
command,
read_per_sec,
write_per_sec,
total_read,
total_write,
process_state,
uid,
user,
time,
#[cfg(feature = "gpu")]
gpu_mem: 0,
#[cfg(feature = "gpu")]
gpu_mem_percent: 0.0,
#[cfg(feature = "gpu")]
gpu_util: 0,
process_type,
#[cfg(unix)]
nice: stat.nice,
priority: stat.priority,
},
new_process_times,
))
}
fn binary_name_from_cmdline(cmdline: &str) -> String {
let mut start = 0;
let mut end = cmdline.len();
for (i, c) in cmdline.chars().enumerate() {
if c == '/' {
start = i + 1;
} else if c == '\0' || c == ':' {
end = i;
break;
}
}
let partial = cmdline.chars().skip(start).take(end - start).join("");
partial
.split_once(" -")
.map(|(name, _)| name.to_string())
.unwrap_or_else(|| partial.to_string())
}
pub(crate) struct PrevProc<'a> {
pub prev_idle: &'a mut f64,
pub prev_non_idle: &'a mut f64,
}
pub(crate) struct ProcHarvestOptions {
pub use_current_cpu_total: bool,
pub unnormalized_cpu: bool,
pub get_process_threads: bool,
}
fn is_str_numeric(s: &str) -> bool {
s.chars().all(|c| c.is_ascii_digit())
}
#[derive(Copy, Clone)]
pub(crate) struct ReadProcArgs {
pub use_current_cpu_total: bool,
pub cpu_usage: f64,
pub cpu_fraction: f64,
pub total_memory: u64,
pub time_difference_in_secs: u64,
pub system_uptime: u64,
pub get_process_threads: bool,
}
pub(crate) fn linux_process_data(
collector: &mut DataCollector, time_difference_in_secs: u64,
) -> CollectionResult<Vec<ProcessHarvest>> {
let total_memory = collector.total_memory();
let prev_proc = PrevProc {
prev_idle: &mut collector.prev_idle,
prev_non_idle: &mut collector.prev_non_idle,
};
let proc_harvest_options = ProcHarvestOptions {
use_current_cpu_total: collector.use_current_cpu_total,
unnormalized_cpu: collector.unnormalized_cpu,
get_process_threads: collector.get_process_threads,
};
let prev_process_details = &mut collector.prev_process_details;
let user_table = &mut collector.user_table;
let ProcHarvestOptions {
use_current_cpu_total,
unnormalized_cpu,
get_process_threads: get_threads,
} = proc_harvest_options;
let PrevProc {
prev_idle,
prev_non_idle,
} = prev_proc;
let CpuUsage {
mut cpu_usage,
cpu_fraction,
} = cpu_usage_calculation(prev_idle, prev_non_idle)?;
if unnormalized_cpu {
let num_processors = collector.sys.system.cpus().len() as f64;
cpu_usage /= num_processors;
}
let mut seen_pids: HashSet<Pid> = HashSet::default();
let pids = fs::read_dir("/proc")?.flatten().filter_map(|dir| {
if is_str_numeric(dir.file_name().to_string_lossy().trim()) {
Some(dir.path())
} else {
None
}
});
let args = ReadProcArgs {
use_current_cpu_total,
cpu_usage,
cpu_fraction,
total_memory,
time_difference_in_secs,
system_uptime: sysinfo::System::uptime(),
get_process_threads: get_threads,
};
let mut buffer = String::new();
let mut process_threads_to_check = HashMap::default();
let mut process_vector: Vec<ProcessHarvest> = pids
.filter_map(|pid_path| {
if let Ok((process, threads)) =
Process::from_path(pid_path, &mut buffer, args.get_process_threads)
{
let pid = process.pid;
let prev_proc_details = prev_process_details.entry(pid).or_default();
#[cfg_attr(not(feature = "gpu"), expect(unused_mut))]
if let Ok((mut process_harvest, new_process_times)) =
read_proc(prev_proc_details, process, args, user_table, None)
{
#[cfg(feature = "gpu")]
if let Some(gpus) = &collector.gpu_pids {
gpus.iter().for_each(|gpu| {
if let Some((mem, util)) = gpu.get(&pid) {
process_harvest.gpu_mem += mem;
process_harvest.gpu_util += util;
}
});
if let Some(gpu_total_mem) = &collector.gpus_total_mem {
process_harvest.gpu_mem_percent =
(process_harvest.gpu_mem as f64 / *gpu_total_mem as f64 * 100.0)
as f32;
}
}
prev_proc_details.cpu_time = new_process_times;
prev_proc_details.total_read_bytes = process_harvest.total_read;
prev_proc_details.total_write_bytes = process_harvest.total_write;
if !threads.is_empty() {
process_threads_to_check.insert(pid, threads);
}
seen_pids.insert(pid);
return Some(process_harvest);
}
}
None
})
.collect();
for (pid, tid_paths) in process_threads_to_check {
for tid_path in tid_paths {
if let Ok((process, _)) = Process::from_path(tid_path, &mut buffer, false) {
let tid = process.pid;
let prev_proc_details = prev_process_details.entry(tid).or_default();
if let Ok((process_harvest, new_process_times)) =
read_proc(prev_proc_details, process, args, user_table, Some(pid))
{
prev_proc_details.cpu_time = new_process_times;
prev_proc_details.total_read_bytes = process_harvest.total_read;
prev_proc_details.total_write_bytes = process_harvest.total_write;
seen_pids.insert(tid);
process_vector.push(process_harvest);
}
}
}
}
prev_process_details.retain(|pid, _| seen_pids.contains(pid));
if collector.should_run_less_routine_tasks {
prev_process_details.shrink_to_fit();
}
Ok(process_vector)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_proc_cpu_parse() {
assert_eq!(
(100_f64, 200_f64),
fetch_cpu_usage("100 0 100 100"),
"Failed to properly calculate idle/non-idle for /proc/stat CPU with 4 values"
);
assert_eq!(
(120_f64, 200_f64),
fetch_cpu_usage("100 0 100 100 20"),
"Failed to properly calculate idle/non-idle for /proc/stat CPU with 5 values"
);
assert_eq!(
(120_f64, 230_f64),
fetch_cpu_usage("100 0 100 100 20 30"),
"Failed to properly calculate idle/non-idle for /proc/stat CPU with 6 values"
);
assert_eq!(
(120_f64, 270_f64),
fetch_cpu_usage("100 0 100 100 20 30 40"),
"Failed to properly calculate idle/non-idle for /proc/stat CPU with 7 values"
);
assert_eq!(
(120_f64, 320_f64),
fetch_cpu_usage("100 0 100 100 20 30 40 50"),
"Failed to properly calculate idle/non-idle for /proc/stat CPU with 8 values"
);
assert_eq!(
(120_f64, 320_f64),
fetch_cpu_usage("100 0 100 100 20 30 40 50 100"),
"Failed to properly calculate idle/non-idle for /proc/stat CPU with 9 values"
);
assert_eq!(
(120_f64, 320_f64),
fetch_cpu_usage("100 0 100 100 20 30 40 50 100 200"),
"Failed to properly calculate idle/non-idle for /proc/stat CPU with 10 values"
);
}
#[test]
fn test_name_from_cmdline() {
assert_eq!(binary_name_from_cmdline("/usr/bin/btm"), "btm");
assert_eq!(
binary_name_from_cmdline("/usr/bin/btm\0--asdf\0--asdf/gkj"),
"btm"
);
assert_eq!(binary_name_from_cmdline("/usr/bin/btm:"), "btm");
assert_eq!(binary_name_from_cmdline("/usr/bin/b tm"), "b tm");
assert_eq!(binary_name_from_cmdline("/usr/bin/b tm:"), "b tm");
assert_eq!(binary_name_from_cmdline("/usr/bin/b tm\0--test"), "b tm");
assert_eq!(binary_name_from_cmdline("/usr/bin/b tm:\0--test"), "b tm");
assert_eq!(
binary_name_from_cmdline("/usr/bin/b t m:\0--\"test thing\""),
"b t m"
);
assert_eq!(
binary_name_from_cmdline("firefox -contentproc -isForBrowser -prefsHandle 0"),
"firefox"
);
assert_eq!(binary_name_from_cmdline("こんにちは\0"), "こんにちは");
assert_eq!(
binary_name_from_cmdline("こんにちは -こんばんは"),
"こんにちは"
);
assert_eq!(
binary_name_from_cmdline("/usr/bin/こんにちは -こんばんは\0"),
"こんにちは"
);
}
}