diff --git a/CHANGELOG.md b/CHANGELOG.md index 4aeb114e..a8614794 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index dc761ea6..2ad6ce3b 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/src/app.rs b/src/app.rs index 986f8178..005f3f0a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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); } } } diff --git a/src/app/data_harvester.rs b/src/app/data_harvester.rs index a0c7c955..55331813 100644 --- a/src/app/data_harvester.rs +++ b/src/app/data_harvester.rs @@ -61,7 +61,7 @@ impl Data { pub struct DataCollector { pub data: Data, sys: System, - prev_pid_stats: HashMap, + prev_pid_stats: HashMap, 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; } } diff --git a/src/app/data_harvester/processes.rs b/src/app/data_harvester/processes.rs index 2eb65e05..f8097c4b 100644 --- a/src/app/data_harvester/processes.rs +++ b/src/app/data_harvester/processes.rs @@ -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 { - 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::>(); - let utime = val[13].parse::().unwrap_or(0_f64); - let stime = val[14].parse::().unwrap_or(0_f64); - - Ok(utime + stime) // This seems to match top... +fn get_process_io(path: &PathBuf) -> std::io::Result { + Ok(std::fs::read_to_string(path)?) } -/// Note that cpu_fraction should be represented WITHOUT the \times 100 factor! -fn linux_cpu_usage( - pid: u32, cpu_usage: f64, cpu_fraction: f64, - prev_pid_stats: &HashMap, - new_pid_stats: &mut HashMap, use_current_cpu_total: bool, - curr_time: Instant, -) -> std::io::Result { - // 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::().unwrap_or(0), + io_stats[5].parse::().unwrap_or(0), + ) +} - new_pid_stats.insert(pid.to_string(), (after_proc_val, curr_time)); +fn get_process_stats(path: &PathBuf) -> std::io::Result { + Ok(std::fs::read_to_string(path)?) +} + +fn get_process_cpu_stats(stats: &[&str]) -> f64 { + // utime + stime (matches top) + stats[13].parse::().unwrap_or(0_f64) + stats[14].parse::().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( process: &str, cpu_usage: f64, cpu_fraction: f64, - prev_pid_stats: &HashMap, - new_pid_stats: &mut HashMap, use_current_cpu_total: bool, - curr_time: Instant, + prev_pid_stats: &mut HashMap, + new_pid_stats: &mut HashMap, use_current_cpu_total: bool, + time_difference_in_secs: u64, ) -> std::io::Result { - 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( .parse::() .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::>(); + let io_stats = io_results.split_whitespace().collect::>(); + + 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, 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, use_current_cpu_total: bool, + time_difference_in_secs: u64, ) -> crate::utils::error::Result> { - let mut process_vector: Vec = 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::>(); - 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::>(); + let mut new_pid_stats = HashMap::new(); - let mut new_pid_stats: HashMap = HashMap::new(); - - for process in process_stream { - if let Ok(process_object) = convert_ps( + let process_vector: Vec = 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> { + let mut process_vector: Vec = 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) diff --git a/src/canvas/widgets/process_table.rs b/src/canvas/widgets/process_table.rs index e164a413..de794287 100644 --- a/src/canvas/widgets/process_table.rs +++ b/src/canvas/widgets/process_table.rs @@ -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 = 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, diff --git a/src/data_conversion.rs b/src/data_conversion.rs index 4a2d9775..2991b92f 100644 --- a/src/data_conversion.rs +++ b/src/data_conversion.rs @@ -31,6 +31,22 @@ pub struct ConvertedProcessData { pub cpu_usage: f64, pub mem_usage: f64, pub group_pids: Vec, + 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, + 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 = HashMap::new(); // cpu, mem, pids - let mut grouped_hashmap: HashMap)> = - std::collections::HashMap::new(); + let mut grouped_hashmap: HashMap = 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::>(); diff --git a/src/main.rs b/src/main.rs index 7b4d4660..1f18e8f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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, }); }