diff --git a/CHANGELOG.md b/CHANGELOG.md index a86147943..380627cf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#58](https://github.com/ClementTsang/bottom/issues/58): I/O stats per process +- [#114](https://github.com/ClementTsang/bottom/pull/114): Process state per process + ## [0.3.0] - 2020-04-07 ### Features diff --git a/src/app/data_harvester.rs b/src/app/data_harvester.rs index 553318130..9bdfb1d50 100644 --- a/src/app/data_harvester.rs +++ b/src/app/data_harvester.rs @@ -1,6 +1,9 @@ //! This is the main file to house data collection functions. -use std::{collections::HashMap, time::Instant}; +use std::time::Instant; + +#[cfg(target_os = "linux")] +use std::collections::HashMap; use sysinfo::{System, SystemExt}; @@ -61,8 +64,11 @@ impl Data { pub struct DataCollector { pub data: Data, sys: System, + #[cfg(target_os = "linux")] prev_pid_stats: HashMap, + #[cfg(target_os = "linux")] prev_idle: f64, + #[cfg(target_os = "linux")] prev_non_idle: f64, mem_total_kb: u64, temperature_type: temperature::TemperatureType, @@ -79,8 +85,11 @@ impl Default for DataCollector { DataCollector { data: Data::default(), sys: System::new_all(), + #[cfg(target_os = "linux")] prev_pid_stats: HashMap::new(), + #[cfg(target_os = "linux")] prev_idle: 0_f64, + #[cfg(target_os = "linux")] prev_non_idle: 0_f64, mem_total_kb: 0, temperature_type: temperature::TemperatureType::Celsius, @@ -147,21 +156,35 @@ impl DataCollector { // 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) = 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(), - ) + #[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(), + ) + } + #[cfg(not(target_os = "linux"))] + { + Ok(Vec::new()) + } } else { - processes::windows_macos_get_processes_list( - &self.sys, - self.use_current_cpu_total, - self.mem_total_kb, - ) + #[cfg(not(target_os = "linux"))] + { + processes::windows_macos_get_processes_list( + &self.sys, + self.use_current_cpu_total, + self.mem_total_kb, + ) + } + #[cfg(target_os = "linux")] + { + Ok(Vec::new()) + } } { self.data.list_of_processes = process_list; } diff --git a/src/app/data_harvester/processes.rs b/src/app/data_harvester/processes.rs index f8097c4b7..2f825a02b 100644 --- a/src/app/data_harvester/processes.rs +++ b/src/app/data_harvester/processes.rs @@ -1,13 +1,17 @@ +use std::path::PathBuf; +use sysinfo::ProcessStatus; + +#[cfg(target_os = "linux")] +use crate::utils::error; +#[cfg(target_os = "linux")] use std::{ collections::{hash_map::RandomState, HashMap}, - path::PathBuf, process::Command, }; +#[cfg(not(target_os = "linux"))] use sysinfo::{ProcessExt, ProcessorExt, System, SystemExt}; -use crate::utils::error; - #[derive(Clone)] pub enum ProcessSorting { CPU, @@ -32,6 +36,8 @@ pub struct ProcessHarvest { pub write_bytes_per_sec: u64, pub total_read_bytes: u64, pub total_write_bytes: u64, + pub process_state: String, + pub process_state_char: char, } #[derive(Debug, Default, Clone)] @@ -54,6 +60,7 @@ impl PrevProcDetails { } } +#[cfg(target_os = "linux")] fn cpu_usage_calculation( prev_idle: &mut f64, prev_non_idle: &mut f64, ) -> error::Result<(f64, f64)> { @@ -122,11 +129,13 @@ fn cpu_usage_calculation( Ok((result, cpu_percentage)) } +#[cfg(target_os = "linux")] fn get_process_io(path: &PathBuf) -> std::io::Result { Ok(std::fs::read_to_string(path)?) } -fn get_process_io_usage(io_stats: &[&str]) -> (u64, u64) { +#[cfg(target_os = "linux")] +fn get_linux_process_io_usage(io_stats: &[&str]) -> (u64, u64) { // Represents read_bytes and write_bytes ( io_stats[4].parse::().unwrap_or(0), @@ -134,20 +143,34 @@ fn get_process_io_usage(io_stats: &[&str]) -> (u64, u64) { ) } +#[cfg(target_os = "linux")] 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) +#[cfg(target_os = "linux")] +fn get_linux_process_state(proc_stats: &[&str]) -> (char, String) { + if let Some(first_char) = proc_stats[2].chars().collect::>().first() { + ( + *first_char, + ProcessStatus::from(*first_char).to_string().to_string(), + ) + } else { + ('?', String::default()) + } } /// Note that cpu_fraction should be represented WITHOUT the x100 factor! -fn linux_cpu_usage( +#[cfg(target_os = "linux")] +fn get_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)> { + 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) + } + // Based heavily on https://stackoverflow.com/a/23376195 and https://stackoverflow.com/a/1424556 let after_proc_val = get_process_cpu_stats(&proc_stats); @@ -164,6 +187,7 @@ fn linux_cpu_usage( } } +#[cfg(target_os = "linux")] fn convert_ps( process: &str, cpu_usage: f64, cpu_fraction: f64, prev_pid_stats: &mut HashMap, @@ -188,34 +212,54 @@ fn convert_ps( 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, - new_pid_stat.cpu_time, - use_current_cpu_total, - )?; - - 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 - }; + let (cpu_usage_percent, process_state_char, process_state) = + if let Ok(stat_results) = get_process_stats(&new_pid_stat.proc_stat_path) { + let proc_stats = stat_results.split_whitespace().collect::>(); + let (process_state_char, process_state) = get_linux_process_state(&proc_stats); + + let (cpu_usage_percent, after_proc_val) = get_linux_cpu_usage( + &proc_stats, + cpu_usage, + cpu_fraction, + new_pid_stat.cpu_time, + use_current_cpu_total, + )?; + new_pid_stat.cpu_time = after_proc_val; + + (cpu_usage_percent, process_state_char, process_state) + } else { + (0.0, '?', String::new()) + }; - 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; + // This can fail if permission is denied! + let (total_read_bytes, total_write_bytes, read_bytes_per_sec, write_bytes_per_sec) = + if let Ok(io_results) = get_process_io(&new_pid_stat.proc_io_path) { + let io_stats = io_results.split_whitespace().collect::>(); + + let (total_read_bytes, total_write_bytes) = get_linux_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; + + ( + total_read_bytes, + total_write_bytes, + read_bytes_per_sec, + write_bytes_per_sec, + ) + } else { + (0, 0, 0, 0) + }; new_pid_stats.insert(pid, new_pid_stat); @@ -228,9 +272,12 @@ fn convert_ps( total_write_bytes, read_bytes_per_sec, write_bytes_per_sec, + process_state, + process_state_char, }) } +#[cfg(target_os = "linux")] 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, @@ -279,6 +326,7 @@ pub fn linux_get_processes_list( } } +#[cfg(not(target_os = "linux"))] pub fn windows_macos_get_processes_list( sys: &System, use_current_cpu_total: bool, mem_total_kb: u64, ) -> crate::utils::error::Result> { @@ -331,8 +379,33 @@ pub fn windows_macos_get_processes_list( write_bytes_per_sec: disk_usage.written_bytes, total_read_bytes: disk_usage.total_read_bytes, total_write_bytes: disk_usage.total_written_bytes, + process_state: process_val.status().to_string().to_string(), + process_state_char: convert_process_status_to_char(process_val.status()), }); } Ok(process_vector) } + +#[allow(unused_variables)] +#[cfg(not(target_os = "linux"))] +fn convert_process_status_to_char(status: ProcessStatus) -> char { + if cfg!(target_os = "macos") { + #[cfg(target_os = "macos")] + { + match status { + ProcessStatus::Run => 'R', + ProcessStatus::Sleep => 'S', + ProcessStatus::Idle => 'D', + ProcessStatus::Zombie => 'Z', + _ => '?', + } + } + #[cfg(not(target_os = "macos"))] + { + '?' + } + } else { + 'R' + } +} diff --git a/src/canvas/widgets/process_table.rs b/src/canvas/widgets/process_table.rs index de7942877..bde4fe9b5 100644 --- a/src/canvas/widgets/process_table.rs +++ b/src/canvas/widgets/process_table.rs @@ -119,6 +119,7 @@ impl ProcessTableWidget for Painter { process.write_per_sec.to_string(), process.total_read.to_string(), process.total_write.to_string(), + process.process_states.to_string(), ]; Row::StyledData( stringified_process_vec.into_iter(), @@ -155,6 +156,7 @@ impl ProcessTableWidget for Painter { let wps = "W/s".to_string(); let total_read = "Read".to_string(); let total_write = "Write".to_string(); + let process_state = "State".to_string(); let direction_val = if proc_widget_state.process_sorting_reverse { "▼".to_string() @@ -178,6 +180,7 @@ impl ProcessTableWidget for Painter { wps, total_read, total_write, + process_state, ]; let process_headers_lens: Vec = process_headers .iter() @@ -186,7 +189,7 @@ impl ProcessTableWidget for Painter { // Calculate widths let width = f64::from(draw_loc.width); - let width_ratios = [0.1, 0.3, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]; + let width_ratios = [0.1, 0.2, 0.1, 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 2991b92f5..103a3917d 100644 --- a/src/data_conversion.rs +++ b/src/data_conversion.rs @@ -35,6 +35,7 @@ pub struct ConvertedProcessData { pub write_per_sec: String, pub total_read: String, pub total_write: String, + pub process_states: String, } #[derive(Clone, Default, Debug)] @@ -47,6 +48,7 @@ pub struct SingleProcessData { pub write_per_sec: u64, pub total_read: u64, pub total_write: u64, + pub process_state: String, } #[derive(Clone, Default, Debug)] @@ -372,6 +374,7 @@ pub fn convert_process_data( (*entry).write_per_sec += process.write_bytes_per_sec; (*entry).total_read += process.total_read_bytes; (*entry).total_write += process.total_write_bytes; + (*entry).process_state.push(process.process_state_char); single_list.insert(process.pid, process.clone()); } @@ -403,6 +406,7 @@ pub fn convert_process_data( write_per_sec, total_read, total_write, + process_states: p.process_state, } }) .collect::>(); diff --git a/src/main.rs b/src/main.rs index 1f18e8f1b..3a2db3b8b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -644,6 +644,7 @@ fn update_final_process_list(app: &mut App, widget_id: u64) { write_per_sec, total_read, total_write, + process_states: process.process_state.clone(), }); }