feature: add io and io/s for processes (#113)

This commit is contained in:
Clement Tsang 2020-04-10 20:18:26 -04:00 committed by GitHub
parent cf4249c988
commit f210681ae7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 298 additions and 140 deletions

View File

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Features
- [#58](https://github.com/ClementTsang/bottom/issues/58): I/O stats per process
## [0.3.0] - 2020-04-07
### Features

View File

@ -210,6 +210,8 @@ As yet _another_ process/system visualization and management application, bottom
- Display temperatures from sensors
- Display information regarding processes, like CPU, memory, and I/O usage
- Process management (process killing _is_ all you need, right?)
It also aims to be:

View File

@ -743,6 +743,8 @@ impl App {
proc_widget_state.is_grouped = !(proc_widget_state.is_grouped);
if proc_widget_state.is_grouped {
self.search_with_name();
} else {
self.proc_state.force_update = Some(self.current_widget.widget_id);
}
}
}

View File

@ -61,7 +61,7 @@ impl Data {
pub struct DataCollector {
pub data: Data,
sys: System,
prev_pid_stats: HashMap<String, (f64, Instant)>,
prev_pid_stats: HashMap<u32, processes::PrevProcDetails>,
prev_idle: f64,
prev_non_idle: f64,
mem_total_kb: u64,
@ -146,15 +146,23 @@ impl DataCollector {
// Processes. This is the longest part of the harvesting process... changing this might be
// good in the future. What was tried already:
// * Splitting the internal part into multiple scoped threads (dropped by ~.01 seconds, but upped usage)
if let Ok(process_list) = processes::get_sorted_processes_list(
&self.sys,
&mut self.prev_idle,
&mut self.prev_non_idle,
&mut self.prev_pid_stats,
self.use_current_cpu_total,
self.mem_total_kb,
current_instant,
) {
if let Ok(process_list) = if cfg!(target_os = "linux") {
processes::linux_get_processes_list(
&mut self.prev_idle,
&mut self.prev_non_idle,
&mut self.prev_pid_stats,
self.use_current_cpu_total,
current_instant
.duration_since(self.last_collection_time)
.as_secs(),
)
} else {
processes::windows_macos_get_processes_list(
&self.sys,
self.use_current_cpu_total,
self.mem_total_kb,
)
} {
self.data.list_of_processes = process_list;
}
}

View File

@ -1,7 +1,7 @@
use std::{
collections::{hash_map::RandomState, HashMap},
path::PathBuf,
process::Command,
time::Instant,
};
use sysinfo::{ProcessExt, ProcessorExt, System, SystemExt};
@ -28,6 +28,30 @@ pub struct ProcessHarvest {
pub cpu_usage_percent: f64,
pub mem_usage_percent: f64,
pub name: String,
pub read_bytes_per_sec: u64,
pub write_bytes_per_sec: u64,
pub total_read_bytes: u64,
pub total_write_bytes: u64,
}
#[derive(Debug, Default, Clone)]
pub struct PrevProcDetails {
pub total_read_bytes: u64,
pub total_write_bytes: u64,
pub cpu_time: f64,
pub proc_stat_path: PathBuf,
pub proc_io_path: PathBuf,
}
impl PrevProcDetails {
pub fn new(pid: u32) -> Self {
let pid_string = pid.to_string();
PrevProcDetails {
proc_io_path: PathBuf::from(format!("/proc/{}/io", pid_string)),
proc_stat_path: PathBuf::from(format!("/proc/{}/stat", pid_string)),
..PrevProcDetails::default()
}
}
}
fn cpu_usage_calculation(
@ -98,62 +122,54 @@ fn cpu_usage_calculation(
Ok((result, cpu_percentage))
}
fn get_process_cpu_stats(pid: u32) -> std::io::Result<f64> {
let mut path = std::path::PathBuf::new();
path.push("/proc");
path.push(&pid.to_string());
path.push("stat");
let stat_results = std::fs::read_to_string(path)?;
let val = stat_results.split_whitespace().collect::<Vec<&str>>();
let utime = val[13].parse::<f64>().unwrap_or(0_f64);
let stime = val[14].parse::<f64>().unwrap_or(0_f64);
Ok(utime + stime) // This seems to match top...
fn get_process_io(path: &PathBuf) -> std::io::Result<String> {
Ok(std::fs::read_to_string(path)?)
}
/// Note that cpu_fraction should be represented WITHOUT the \times 100 factor!
fn linux_cpu_usage<S: core::hash::BuildHasher>(
pid: u32, cpu_usage: f64, cpu_fraction: f64,
prev_pid_stats: &HashMap<String, (f64, Instant), S>,
new_pid_stats: &mut HashMap<String, (f64, Instant), S>, use_current_cpu_total: bool,
curr_time: Instant,
) -> std::io::Result<f64> {
// Based heavily on https://stackoverflow.com/a/23376195 and https://stackoverflow.com/a/1424556
let before_proc_val: f64 = if prev_pid_stats.contains_key(&pid.to_string()) {
prev_pid_stats
.get(&pid.to_string())
.unwrap_or(&(0_f64, curr_time))
.0
} else {
0_f64
};
let after_proc_val = get_process_cpu_stats(pid)?;
fn get_process_io_usage(io_stats: &[&str]) -> (u64, u64) {
// Represents read_bytes and write_bytes
(
io_stats[4].parse::<u64>().unwrap_or(0),
io_stats[5].parse::<u64>().unwrap_or(0),
)
}
new_pid_stats.insert(pid.to_string(), (after_proc_val, curr_time));
fn get_process_stats(path: &PathBuf) -> std::io::Result<String> {
Ok(std::fs::read_to_string(path)?)
}
fn get_process_cpu_stats(stats: &[&str]) -> f64 {
// utime + stime (matches top)
stats[13].parse::<f64>().unwrap_or(0_f64) + stats[14].parse::<f64>().unwrap_or(0_f64)
}
/// Note that cpu_fraction should be represented WITHOUT the x100 factor!
fn linux_cpu_usage(
proc_stats: &[&str], cpu_usage: f64, cpu_fraction: f64, before_proc_val: f64,
use_current_cpu_total: bool,
) -> std::io::Result<(f64, f64)> {
// Based heavily on https://stackoverflow.com/a/23376195 and https://stackoverflow.com/a/1424556
let after_proc_val = get_process_cpu_stats(&proc_stats);
if use_current_cpu_total {
Ok((after_proc_val - before_proc_val) / cpu_usage * 100_f64)
Ok((
(after_proc_val - before_proc_val) / cpu_usage * 100_f64,
after_proc_val,
))
} else {
Ok((after_proc_val - before_proc_val) / cpu_usage * 100_f64 * cpu_fraction)
Ok((
(after_proc_val - before_proc_val) / cpu_usage * 100_f64 * cpu_fraction,
after_proc_val,
))
}
}
fn convert_ps<S: core::hash::BuildHasher>(
process: &str, cpu_usage: f64, cpu_fraction: f64,
prev_pid_stats: &HashMap<String, (f64, Instant), S>,
new_pid_stats: &mut HashMap<String, (f64, Instant), S>, use_current_cpu_total: bool,
curr_time: Instant,
prev_pid_stats: &mut HashMap<u32, PrevProcDetails, S>,
new_pid_stats: &mut HashMap<u32, PrevProcDetails, S>, use_current_cpu_total: bool,
time_difference_in_secs: u64,
) -> std::io::Result<ProcessHarvest> {
if process.trim().to_string().is_empty() {
return Ok(ProcessHarvest {
pid: 0,
name: "".to_string(),
mem_usage_percent: 0.0,
cpu_usage_percent: 0.0,
});
}
let pid = (&process[..11])
.trim()
.to_string()
@ -166,104 +182,156 @@ fn convert_ps<S: core::hash::BuildHasher>(
.parse::<f64>()
.unwrap_or(0_f64);
let cpu_usage_percent = linux_cpu_usage(
pid,
let mut new_pid_stat = if let Some(prev_proc_stats) = prev_pid_stats.remove(&pid) {
prev_proc_stats
} else {
PrevProcDetails::new(pid)
};
let stat_results = get_process_stats(&new_pid_stat.proc_stat_path)?;
let io_results = get_process_io(&new_pid_stat.proc_io_path)?;
let proc_stats = stat_results.split_whitespace().collect::<Vec<&str>>();
let io_stats = io_results.split_whitespace().collect::<Vec<&str>>();
let (cpu_usage_percent, after_proc_val) = linux_cpu_usage(
&proc_stats,
cpu_usage,
cpu_fraction,
prev_pid_stats,
new_pid_stats,
new_pid_stat.cpu_time,
use_current_cpu_total,
curr_time,
)?;
let (total_read_bytes, total_write_bytes) = get_process_io_usage(&io_stats);
let read_bytes_per_sec = if time_difference_in_secs == 0 {
0
} else {
(total_write_bytes - new_pid_stat.total_write_bytes) / time_difference_in_secs
};
let write_bytes_per_sec = if time_difference_in_secs == 0 {
0
} else {
(total_read_bytes - new_pid_stat.total_read_bytes) / time_difference_in_secs
};
new_pid_stat.total_read_bytes = total_read_bytes;
new_pid_stat.total_write_bytes = total_write_bytes;
new_pid_stat.cpu_time = after_proc_val;
new_pid_stats.insert(pid, new_pid_stat);
Ok(ProcessHarvest {
pid,
name,
mem_usage_percent,
cpu_usage_percent,
total_read_bytes,
total_write_bytes,
read_bytes_per_sec,
write_bytes_per_sec,
})
}
pub fn get_sorted_processes_list(
sys: &System, prev_idle: &mut f64, prev_non_idle: &mut f64,
prev_pid_stats: &mut HashMap<String, (f64, Instant), RandomState>, use_current_cpu_total: bool,
mem_total_kb: u64, curr_time: Instant,
pub fn linux_get_processes_list(
prev_idle: &mut f64, prev_non_idle: &mut f64,
prev_pid_stats: &mut HashMap<u32, PrevProcDetails, RandomState>, use_current_cpu_total: bool,
time_difference_in_secs: u64,
) -> crate::utils::error::Result<Vec<ProcessHarvest>> {
let mut process_vector: Vec<ProcessHarvest> = Vec::new();
let ps_result = Command::new("ps")
.args(&["-axo", "pid:10,comm:50,%mem:5", "--noheader"])
.output()?;
let ps_stdout = String::from_utf8_lossy(&ps_result.stdout);
let split_string = ps_stdout.split('\n');
let cpu_calc = cpu_usage_calculation(prev_idle, prev_non_idle);
if let Ok((cpu_usage, cpu_fraction)) = cpu_calc {
let process_list = split_string.collect::<Vec<&str>>();
if cfg!(target_os = "linux") {
let ps_result = Command::new("ps")
.args(&["-axo", "pid:10,comm:50,%mem:5", "--noheader"])
.output()?;
let ps_stdout = String::from_utf8_lossy(&ps_result.stdout);
let split_string = ps_stdout.split('\n');
let cpu_calc = cpu_usage_calculation(prev_idle, prev_non_idle);
if let Ok((cpu_usage, cpu_fraction)) = cpu_calc {
let process_stream = split_string.collect::<Vec<&str>>();
let mut new_pid_stats = HashMap::new();
let mut new_pid_stats: HashMap<String, (f64, Instant), RandomState> = HashMap::new();
for process in process_stream {
if let Ok(process_object) = convert_ps(
let process_vector: Vec<ProcessHarvest> = process_list
.iter()
.filter_map(|process| {
if process.trim().is_empty() {
None
} else if let Ok(process_object) = convert_ps(
process,
cpu_usage,
cpu_fraction,
&prev_pid_stats,
prev_pid_stats,
&mut new_pid_stats,
use_current_cpu_total,
curr_time,
time_difference_in_secs,
) {
if !process_object.name.is_empty() {
process_vector.push(process_object);
Some(process_object)
} else {
None
}
}
}
*prev_pid_stats = new_pid_stats;
}
} else {
let process_hashmap = sys.get_processes();
let cpu_usage = sys.get_global_processor_info().get_cpu_usage() as f64 / 100.0;
let num_cpus = sys.get_processors().len() as f64;
for process_val in process_hashmap.values() {
let name = if process_val.name().is_empty() {
let process_cmd = process_val.cmd();
if process_cmd.len() > 1 {
process_cmd[0].clone()
} else {
let process_exe = process_val.exe().file_stem();
if let Some(exe) = process_exe {
let process_exe_opt = exe.to_str();
if let Some(exe_name) = process_exe_opt {
exe_name.to_string()
} else {
"".to_string()
}
None
}
})
.collect();
*prev_pid_stats = new_pid_stats;
Ok(process_vector)
} else {
Ok(Vec::new())
}
}
pub fn windows_macos_get_processes_list(
sys: &System, use_current_cpu_total: bool, mem_total_kb: u64,
) -> crate::utils::error::Result<Vec<ProcessHarvest>> {
let mut process_vector: Vec<ProcessHarvest> = Vec::new();
let process_hashmap = sys.get_processes();
let cpu_usage = sys.get_global_processor_info().get_cpu_usage() as f64 / 100.0;
let num_cpus = sys.get_processors().len() as f64;
for process_val in process_hashmap.values() {
let name = if process_val.name().is_empty() {
let process_cmd = process_val.cmd();
if process_cmd.len() > 1 {
process_cmd[0].clone()
} else {
let process_exe = process_val.exe().file_stem();
if let Some(exe) = process_exe {
let process_exe_opt = exe.to_str();
if let Some(exe_name) = process_exe_opt {
exe_name.to_string()
} else {
"".to_string()
}
} else {
"".to_string()
}
} else {
process_val.name().to_string()
};
}
} else {
process_val.name().to_string()
};
let pcu = if cfg!(target_os = "windows") {
process_val.cpu_usage() as f64
} else {
process_val.cpu_usage() as f64 / num_cpus
};
let process_cpu_usage = if use_current_cpu_total {
pcu / cpu_usage
} else {
pcu
};
let pcu = if cfg!(target_os = "windows") {
process_val.cpu_usage() as f64
} else {
process_val.cpu_usage() as f64 / num_cpus
};
let process_cpu_usage = if use_current_cpu_total {
pcu / cpu_usage
} else {
pcu
};
process_vector.push(ProcessHarvest {
pid: process_val.pid() as u32,
name,
mem_usage_percent: process_val.memory() as f64 * 100.0 / mem_total_kb as f64,
cpu_usage_percent: process_cpu_usage,
});
}
let disk_usage = process_val.disk_usage();
process_vector.push(ProcessHarvest {
pid: process_val.pid() as u32,
name,
mem_usage_percent: process_val.memory() as f64 * 100.0 / mem_total_kb as f64,
cpu_usage_percent: process_cpu_usage,
read_bytes_per_sec: disk_usage.read_bytes,
write_bytes_per_sec: disk_usage.written_bytes,
total_read_bytes: disk_usage.total_read_bytes,
total_write_bytes: disk_usage.total_written_bytes,
});
}
Ok(process_vector)

View File

@ -115,6 +115,10 @@ impl ProcessTableWidget for Painter {
process.name.clone(),
format!("{:.1}%", process.cpu_usage),
format!("{:.1}%", process.mem_usage),
process.read_per_sec.to_string(),
process.write_per_sec.to_string(),
process.total_read.to_string(),
process.total_write.to_string(),
];
Row::StyledData(
stringified_process_vec.into_iter(),
@ -147,6 +151,10 @@ impl ProcessTableWidget for Painter {
let mut name = "Name(n)".to_string();
let mut cpu = "CPU%(c)".to_string();
let mut mem = "Mem%(m)".to_string();
let rps = "R/s".to_string();
let wps = "W/s".to_string();
let total_read = "Read".to_string();
let total_write = "Write".to_string();
let direction_val = if proc_widget_state.process_sorting_reverse {
"".to_string()
@ -161,7 +169,16 @@ impl ProcessTableWidget for Painter {
ProcessSorting::NAME => name += &direction_val,
};
let process_headers = [pid_or_name, name, cpu, mem];
let process_headers = [
pid_or_name,
name,
cpu,
mem,
rps,
wps,
total_read,
total_write,
];
let process_headers_lens: Vec<usize> = process_headers
.iter()
.map(|entry| entry.len())
@ -169,7 +186,7 @@ impl ProcessTableWidget for Painter {
// Calculate widths
let width = f64::from(draw_loc.width);
let width_ratios = [0.2, 0.4, 0.2, 0.2];
let width_ratios = [0.1, 0.3, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1];
let variable_intrinsic_results = get_variable_intrinsic_widths(
width as u16,
&width_ratios,

View File

@ -31,6 +31,22 @@ pub struct ConvertedProcessData {
pub cpu_usage: f64,
pub mem_usage: f64,
pub group_pids: Vec<u32>,
pub read_per_sec: String,
pub write_per_sec: String,
pub total_read: String,
pub total_write: String,
}
#[derive(Clone, Default, Debug)]
pub struct SingleProcessData {
pub pid: u32,
pub cpu_usage: f64,
pub mem_usage: f64,
pub group_pids: Vec<u32>,
pub read_per_sec: u64,
pub write_per_sec: u64,
pub total_read: u64,
pub total_write: u64,
}
#[derive(Clone, Default, Debug)]
@ -338,21 +354,24 @@ pub fn convert_process_data(
let mut single_list: HashMap<u32, ProcessHarvest> = HashMap::new();
// cpu, mem, pids
let mut grouped_hashmap: HashMap<String, (u32, f64, f64, Vec<u32>)> =
std::collections::HashMap::new();
let mut grouped_hashmap: HashMap<String, SingleProcessData> = std::collections::HashMap::new();
// Go through every single process in the list... and build a hashmap + single list
for process in &(current_data).process_harvest {
let entry = grouped_hashmap.entry(process.name.clone()).or_insert((
process.pid,
0.0,
0.0,
Vec::new(),
));
let entry = grouped_hashmap
.entry(process.name.clone())
.or_insert(SingleProcessData {
pid: process.pid,
..SingleProcessData::default()
});
(*entry).1 += process.cpu_usage_percent;
(*entry).2 += process.mem_usage_percent;
(*entry).3.push(process.pid);
(*entry).cpu_usage += process.cpu_usage_percent;
(*entry).mem_usage += process.mem_usage_percent;
(*entry).group_pids.push(process.pid);
(*entry).read_per_sec += process.read_bytes_per_sec;
(*entry).write_per_sec += process.write_bytes_per_sec;
(*entry).total_read += process.total_read_bytes;
(*entry).total_write += process.total_write_bytes;
single_list.insert(process.pid, process.clone());
}
@ -361,12 +380,29 @@ pub fn convert_process_data(
.iter()
.map(|(name, process_details)| {
let p = process_details.clone();
let converted_rps = get_exact_byte_values(p.read_per_sec, false);
let converted_wps = get_exact_byte_values(p.write_per_sec, false);
let converted_total_read = get_exact_byte_values(p.total_read, false);
let converted_total_write = get_exact_byte_values(p.total_write, false);
let read_per_sec = format!("{:.*}{}/s", 0, converted_rps.0, converted_rps.1);
let write_per_sec = format!("{:.*}{}/s", 0, converted_wps.0, converted_wps.1);
let total_read = format!("{:.*}{}", 0, converted_total_read.0, converted_total_read.1);
let total_write = format!(
"{:.*}{}",
0, converted_total_write.0, converted_total_write.1
);
ConvertedProcessData {
pid: p.0,
pid: p.pid,
name: name.to_string(),
cpu_usage: p.1,
mem_usage: p.2,
group_pids: p.3,
cpu_usage: p.cpu_usage,
mem_usage: p.mem_usage,
group_pids: p.group_pids,
read_per_sec,
write_per_sec,
total_read,
total_write,
}
})
.collect::<Vec<_>>();

View File

@ -570,6 +570,7 @@ fn update_all_process_lists(app: &mut App) {
}
fn update_final_process_list(app: &mut App, widget_id: u64) {
use utils::gen_util::get_exact_byte_values;
let is_invalid_or_blank = match app.proc_state.widget_states.get(&widget_id) {
Some(process_state) => process_state
.process_search_state
@ -618,6 +619,20 @@ fn update_final_process_list(app: &mut App, widget_id: u64) {
}
}
let converted_rps = get_exact_byte_values(process.read_bytes_per_sec, false);
let converted_wps = get_exact_byte_values(process.write_bytes_per_sec, false);
let converted_total_read = get_exact_byte_values(process.total_read_bytes, false);
let converted_total_write = get_exact_byte_values(process.total_write_bytes, false);
let read_per_sec = format!("{:.*}{}/s", 0, converted_rps.0, converted_rps.1);
let write_per_sec = format!("{:.*}{}/s", 0, converted_wps.0, converted_wps.1);
let total_read =
format!("{:.*}{}", 0, converted_total_read.0, converted_total_read.1);
let total_write = format!(
"{:.*}{}",
0, converted_total_write.0, converted_total_write.1
);
if result {
return Some(ConvertedProcessData {
pid: process.pid,
@ -625,6 +640,10 @@ fn update_final_process_list(app: &mut App, widget_id: u64) {
cpu_usage: process.cpu_usage_percent,
mem_usage: process.mem_usage_percent,
group_pids: vec![process.pid],
read_per_sec,
write_per_sec,
total_read,
total_write,
});
}