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...
This commit is contained in:
Clement Tsang 2020-04-11 21:02:27 -04:00 committed by GitHub
parent f210681ae7
commit a351f05d4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 155 additions and 49 deletions

View File

@ -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

View File

@ -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<u32, processes::PrevProcDetails>,
#[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;
}

View File

@ -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<String> {
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::<u64>().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<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)
#[cfg(target_os = "linux")]
fn get_linux_process_state(proc_stats: &[&str]) -> (char, String) {
if let Some(first_char) = proc_stats[2].chars().collect::<Vec<char>>().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::<f64>().unwrap_or(0_f64) + stats[14].parse::<f64>().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<S: core::hash::BuildHasher>(
process: &str, cpu_usage: f64, cpu_fraction: f64,
prev_pid_stats: &mut HashMap<u32, PrevProcDetails, S>,
@ -188,34 +212,54 @@ fn convert_ps<S: core::hash::BuildHasher>(
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, 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::<Vec<&str>>();
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::<Vec<&str>>();
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<S: core::hash::BuildHasher>(
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<u32, PrevProcDetails, RandomState>, 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<Vec<ProcessHarvest>> {
@ -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'
}
}

View File

@ -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<usize> = 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,

View File

@ -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::<Vec<_>>();

View File

@ -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(),
});
}