From a351f05d4a111130a751dbad85eadb0d718d3f97 Mon Sep 17 00:00:00 2001 From: Clement Tsang <34804052+ClementTsang@users.noreply.github.com> Date: Sat, 11 Apr 2020 21:02:27 -0400 Subject: [PATCH] feature: Show process state (#114) This is not 100% finished and will be refined in the future, as I plan to do a bit of an overhaul on how the process widget is going to look and functionality. In particular, tabbed is currently kinda just slapped together (I just combine all the states together as one big string). However, it is enough to work and show state normally... --- CHANGELOG.md | 2 + src/app/data_harvester.rs | 53 ++++++++--- src/app/data_harvester/processes.rs | 139 +++++++++++++++++++++------- src/canvas/widgets/process_table.rs | 5 +- src/data_conversion.rs | 4 + src/main.rs | 1 + 6 files changed, 155 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8614794..380627cf 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 55331813..9bdfb1d5 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 f8097c4b..2f825a02 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, 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) = linux_cpu_usage( - &proc_stats, - cpu_usage, - cpu_fraction, - new_pid_stat.cpu_time, - use_current_cpu_total, - )?; + 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; - 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 - }; + (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 de794287..bde4fe9b 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 2991b92f..103a3917 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 1f18e8f1..3a2db3b8 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(), }); }