Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Show process state #114

Merged
merged 9 commits into from
Apr 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
53 changes: 38 additions & 15 deletions src/app/data_harvester.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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;
}
Expand Down
143 changes: 108 additions & 35 deletions src/app/data_harvester/processes.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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)]
Expand All @@ -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)> {
Expand Down Expand Up @@ -122,32 +129,48 @@ 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),
io_stats[5].parse::<u64>().unwrap_or(0),
)
}

#[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);

Expand All @@ -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>,
Expand All @@ -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, 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::<Vec<&str>>();
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::<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);

Expand All @@ -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,
Expand Down Expand Up @@ -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>> {
Expand Down Expand Up @@ -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'
}
}
5 changes: 4 additions & 1 deletion src/canvas/widgets/process_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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()
Expand All @@ -178,6 +180,7 @@ impl ProcessTableWidget for Painter {
wps,
total_read,
total_write,
process_state,
];
let process_headers_lens: Vec<usize> = process_headers
.iter()
Expand All @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions src/data_conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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)]
Expand Down Expand Up @@ -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());
}
Expand Down Expand Up @@ -403,6 +406,7 @@ pub fn convert_process_data(
write_per_sec,
total_read,
total_write,
process_states: p.process_state,
}
})
.collect::<Vec<_>>();
Expand Down
Loading