mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-07-31 01:24:31 +02:00
feature: support virtual memory column for processes (#1767)
* quick refactor of bytes/name * oop * Add virt mem field * add value * add virtual memory columns + tests * fix * Changelog
This commit is contained in:
parent
d06f239b5f
commit
4d935bdd70
@ -29,6 +29,7 @@ That said, these are more guidelines rather than hardset rules, though the proje
|
||||
- [#1642](https://github.com/ClementTsang/bottom/pull/1642): Support changing the widget borders.
|
||||
- [#1717](https://github.com/ClementTsang/bottom/pull/1717): Support delete key (fn + delete on macOS) to kill processes.
|
||||
- [#1306](https://github.com/ClementTsang/bottom/pull/1306): Support using left/right key to collapse/expand process trees respectively.
|
||||
- [#1767](https://github.com/ClementTsang/bottom/pull/1767): Add a virtual memory column for processes.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
|
@ -46,6 +46,8 @@ cfg_if! {
|
||||
}
|
||||
}
|
||||
|
||||
pub type Bytes = u64;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ProcessHarvest {
|
||||
/// The pid of the process.
|
||||
@ -61,7 +63,10 @@ pub struct ProcessHarvest {
|
||||
pub mem_usage_percent: f32,
|
||||
|
||||
/// Memory usage as bytes.
|
||||
pub mem_usage_bytes: u64,
|
||||
pub mem_usage: Bytes,
|
||||
|
||||
/// Virtual memory.
|
||||
pub virtual_mem: Bytes,
|
||||
|
||||
/// The name of the process.
|
||||
pub name: String,
|
||||
@ -70,16 +75,16 @@ pub struct ProcessHarvest {
|
||||
pub command: String,
|
||||
|
||||
/// Bytes read per second.
|
||||
pub read_bytes_per_sec: u64,
|
||||
pub read_per_sec: Bytes,
|
||||
|
||||
/// Bytes written per second.
|
||||
pub write_bytes_per_sec: u64,
|
||||
pub write_per_sec: Bytes,
|
||||
|
||||
/// The total number of bytes read by the process.
|
||||
pub total_read_bytes: u64,
|
||||
pub total_read: Bytes,
|
||||
|
||||
/// The total number of bytes written by the process.
|
||||
pub total_write_bytes: u64,
|
||||
pub total_write: Bytes,
|
||||
|
||||
/// The current state of the process (e.g. zombie, asleep).
|
||||
pub process_state: (&'static str, char),
|
||||
@ -90,7 +95,6 @@ pub struct ProcessHarvest {
|
||||
/// This is the *effective* user ID of the process. This is only used on
|
||||
/// Unix platforms.
|
||||
#[cfg(target_family = "unix")]
|
||||
#[allow(dead_code)]
|
||||
pub uid: Option<libc::uid_t>,
|
||||
|
||||
/// This is the process' user.
|
||||
|
@ -163,36 +163,31 @@ fn read_proc(
|
||||
use_current_cpu_total,
|
||||
);
|
||||
let parent_pid = Some(stat.ppid);
|
||||
let mem_usage_bytes = stat.rss_bytes();
|
||||
let mem_usage_percent = (mem_usage_bytes as f64 / total_memory as f64 * 100.0) as f32;
|
||||
let mem_usage = stat.rss_bytes();
|
||||
let mem_usage_percent = (mem_usage as f64 / total_memory as f64 * 100.0) as f32;
|
||||
let virtual_mem = stat.vsize;
|
||||
|
||||
// This can fail if permission is denied!
|
||||
let (total_read_bytes, total_write_bytes, read_bytes_per_sec, write_bytes_per_sec) =
|
||||
if let Some(io) = io {
|
||||
let total_read_bytes = io.read_bytes;
|
||||
let total_write_bytes = io.write_bytes;
|
||||
let prev_total_read_bytes = prev_proc.total_read_bytes;
|
||||
let prev_total_write_bytes = prev_proc.total_write_bytes;
|
||||
// XXX: This can fail if permission is denied.
|
||||
let (total_read, total_write, read_per_sec, write_per_sec) = if let Some(io) = io {
|
||||
let total_read = io.read_bytes;
|
||||
let total_write = io.write_bytes;
|
||||
let prev_total_read = prev_proc.total_read_bytes;
|
||||
let prev_total_write = prev_proc.total_write_bytes;
|
||||
|
||||
let read_bytes_per_sec = total_read_bytes
|
||||
.saturating_sub(prev_total_read_bytes)
|
||||
.checked_div(time_difference_in_secs)
|
||||
.unwrap_or(0);
|
||||
let read_per_sec = total_read
|
||||
.saturating_sub(prev_total_read)
|
||||
.checked_div(time_difference_in_secs)
|
||||
.unwrap_or(0);
|
||||
|
||||
let write_bytes_per_sec = total_write_bytes
|
||||
.saturating_sub(prev_total_write_bytes)
|
||||
.checked_div(time_difference_in_secs)
|
||||
.unwrap_or(0);
|
||||
let write_per_sec = total_write
|
||||
.saturating_sub(prev_total_write)
|
||||
.checked_div(time_difference_in_secs)
|
||||
.unwrap_or(0);
|
||||
|
||||
(
|
||||
total_read_bytes,
|
||||
total_write_bytes,
|
||||
read_bytes_per_sec,
|
||||
write_bytes_per_sec,
|
||||
)
|
||||
} else {
|
||||
(0, 0, 0, 0)
|
||||
};
|
||||
(total_read, total_write, read_per_sec, write_per_sec)
|
||||
} else {
|
||||
(0, 0, 0, 0)
|
||||
};
|
||||
|
||||
let user = uid
|
||||
.and_then(|uid| {
|
||||
@ -250,13 +245,14 @@ fn read_proc(
|
||||
parent_pid,
|
||||
cpu_usage_percent,
|
||||
mem_usage_percent,
|
||||
mem_usage_bytes,
|
||||
mem_usage,
|
||||
virtual_mem,
|
||||
name,
|
||||
command,
|
||||
read_bytes_per_sec,
|
||||
write_bytes_per_sec,
|
||||
total_read_bytes,
|
||||
total_write_bytes,
|
||||
read_per_sec,
|
||||
write_per_sec,
|
||||
total_read,
|
||||
total_write,
|
||||
process_state,
|
||||
uid,
|
||||
user,
|
||||
@ -385,8 +381,8 @@ pub(crate) fn linux_process_data(
|
||||
}
|
||||
|
||||
prev_proc_details.cpu_time = new_process_times;
|
||||
prev_proc_details.total_read_bytes = process_harvest.total_read_bytes;
|
||||
prev_proc_details.total_write_bytes = process_harvest.total_write_bytes;
|
||||
prev_proc_details.total_read_bytes = process_harvest.total_read;
|
||||
prev_proc_details.total_write_bytes = process_harvest.total_write;
|
||||
|
||||
pids_to_clear.remove(&pid);
|
||||
return Some(process_harvest);
|
||||
|
@ -51,13 +51,18 @@ pub(crate) struct Stat {
|
||||
|
||||
/// The resident set size, or the number of pages the process has in real
|
||||
/// memory.
|
||||
pub rss: u64,
|
||||
rss: u64,
|
||||
|
||||
/// The virtual memory size in bytes.
|
||||
pub vsize: u64,
|
||||
|
||||
/// The start time of the process, represented in clock ticks.
|
||||
pub start_time: u64,
|
||||
}
|
||||
|
||||
impl Stat {
|
||||
/// Get process stats from a file; this assumes the file is located at
|
||||
/// `/proc/<PID>/stat`.
|
||||
fn from_file(mut f: File, buffer: &mut String) -> anyhow::Result<Stat> {
|
||||
// Since this is just one line, we can read it all at once. However, since it
|
||||
// (technically) might have non-utf8 characters, we can't just use read_to_string.
|
||||
@ -96,8 +101,7 @@ impl Stat {
|
||||
let mut rest = rest.skip(6);
|
||||
let start_time: u64 = next_part(&mut rest)?.parse()?;
|
||||
|
||||
// Skip one field until rss (vsize)
|
||||
let mut rest = rest.skip(1);
|
||||
let vsize: u64 = next_part(&mut rest)?.parse()?;
|
||||
let rss: u64 = next_part(&mut rest)?.parse()?;
|
||||
|
||||
Ok(Stat {
|
||||
@ -107,6 +111,7 @@ impl Stat {
|
||||
utime,
|
||||
stime,
|
||||
rss,
|
||||
vsize,
|
||||
start_time,
|
||||
})
|
||||
}
|
||||
|
@ -79,12 +79,13 @@ pub(crate) trait UnixProcessExt {
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
mem_usage_bytes: process_val.memory(),
|
||||
mem_usage: process_val.memory(),
|
||||
virtual_mem: process_val.virtual_memory(),
|
||||
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,
|
||||
read_per_sec: disk_usage.read_bytes,
|
||||
write_per_sec: disk_usage.written_bytes,
|
||||
total_read: disk_usage.total_read_bytes,
|
||||
total_write: disk_usage.total_written_bytes,
|
||||
process_state,
|
||||
uid,
|
||||
user: uid
|
||||
|
@ -98,12 +98,13 @@ pub fn sysinfo_process_data(
|
||||
} else {
|
||||
0.0
|
||||
} as f32,
|
||||
mem_usage_bytes: process_val.memory(),
|
||||
mem_usage: process_val.memory(),
|
||||
virtual_mem: process_val.virtual_memory(),
|
||||
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,
|
||||
read_per_sec: disk_usage.read_bytes,
|
||||
write_per_sec: disk_usage.written_bytes,
|
||||
total_read: disk_usage.total_read_bytes,
|
||||
total_write: disk_usage.total_written_bytes,
|
||||
process_state,
|
||||
user: process_val
|
||||
.user_id()
|
||||
|
@ -119,7 +119,8 @@ pub struct GeneralArgs {
|
||||
long_help = "Sets the location of the config file. Expects a config file in the TOML format. \
|
||||
If it doesn't exist, a default config file is created at the path. If no path is provided, \
|
||||
the default config location will be used.",
|
||||
alias = "config-location"
|
||||
alias = "config-location",
|
||||
alias = "config",
|
||||
)]
|
||||
pub config_location: Option<PathBuf>,
|
||||
|
||||
|
@ -34,7 +34,7 @@ mod test {
|
||||
#[test]
|
||||
fn valid_process_column_config() {
|
||||
let config = r#"
|
||||
columns = ["CPU%", "PiD", "user", "MEM", "Tread", "T.Write", "Rps", "W/s", "tiMe", "USER", "state"]
|
||||
columns = ["CPU%", "PiD", "user", "MEM", "virt", "Tread", "T.Write", "Rps", "W/s", "tiMe", "USER", "state"]
|
||||
"#;
|
||||
|
||||
let generated: ProcessesConfig = toml_edit::de::from_str(config).unwrap();
|
||||
@ -45,6 +45,7 @@ mod test {
|
||||
ProcWidgetColumn::PidOrCount,
|
||||
ProcWidgetColumn::User,
|
||||
ProcWidgetColumn::Mem,
|
||||
ProcWidgetColumn::VirtualMem,
|
||||
ProcWidgetColumn::TotalRead,
|
||||
ProcWidgetColumn::TotalWrite,
|
||||
ProcWidgetColumn::ReadPerSecond,
|
||||
|
@ -78,6 +78,7 @@ fn make_column(column: ProcColumn) -> SortColumn<ProcColumn> {
|
||||
CpuPercent => SortColumn::new(CpuPercent).default_descending(),
|
||||
MemValue => SortColumn::new(MemValue).default_descending(),
|
||||
MemPercent => SortColumn::new(MemPercent).default_descending(),
|
||||
VirtualMem => SortColumn::new(VirtualMem).default_descending(),
|
||||
Pid => SortColumn::new(Pid),
|
||||
Count => SortColumn::new(Count),
|
||||
Name => SortColumn::soft(Name, Some(0.3)),
|
||||
@ -114,6 +115,7 @@ pub enum ProcWidgetColumn {
|
||||
ProcNameOrCommand,
|
||||
Cpu,
|
||||
Mem,
|
||||
VirtualMem,
|
||||
ReadPerSecond,
|
||||
WritePerSecond,
|
||||
TotalRead,
|
||||
@ -253,6 +255,7 @@ impl ProcWidgetState {
|
||||
MemPercent
|
||||
}
|
||||
}
|
||||
ProcWidgetColumn::VirtualMem => VirtualMem,
|
||||
ProcWidgetColumn::ReadPerSecond => ReadPerSecond,
|
||||
ProcWidgetColumn::WritePerSecond => WritePerSecond,
|
||||
ProcWidgetColumn::TotalRead => TotalRead,
|
||||
@ -303,6 +306,7 @@ impl ProcWidgetState {
|
||||
match col.inner() {
|
||||
CpuPercent => ProcWidgetColumn::Cpu,
|
||||
MemValue | MemPercent => ProcWidgetColumn::Mem,
|
||||
VirtualMem => ProcWidgetColumn::VirtualMem,
|
||||
Pid | Count => ProcWidgetColumn::PidOrCount,
|
||||
Name | Command => ProcWidgetColumn::ProcNameOrCommand,
|
||||
ReadPerSecond => ProcWidgetColumn::ReadPerSecond,
|
||||
@ -700,14 +704,14 @@ impl ProcWidgetState {
|
||||
*usage += process.mem_usage_percent;
|
||||
}
|
||||
MemUsage::Bytes(usage) => {
|
||||
*usage += process.mem_usage_bytes;
|
||||
*usage += process.mem_usage;
|
||||
}
|
||||
}
|
||||
|
||||
pwd.rps += process.read_bytes_per_sec;
|
||||
pwd.wps += process.write_bytes_per_sec;
|
||||
pwd.total_read += process.total_read_bytes;
|
||||
pwd.total_write += process.total_write_bytes;
|
||||
pwd.rps += process.read_per_sec;
|
||||
pwd.wps += process.write_per_sec;
|
||||
pwd.total_read += process.total_read;
|
||||
pwd.total_write += process.total_write;
|
||||
pwd.time = pwd.time.max(process.time);
|
||||
#[cfg(feature = "gpu")]
|
||||
{
|
||||
@ -1075,6 +1079,7 @@ mod test {
|
||||
id: "A".into(),
|
||||
cpu_usage_percent: 0.0,
|
||||
mem_usage: MemUsage::Percent(1.1),
|
||||
virtual_mem: 100,
|
||||
rps: 0,
|
||||
wps: 0,
|
||||
total_read: 0,
|
||||
|
@ -18,6 +18,7 @@ pub enum ProcColumn {
|
||||
CpuPercent,
|
||||
MemValue,
|
||||
MemPercent,
|
||||
VirtualMem,
|
||||
Pid,
|
||||
Count,
|
||||
Name,
|
||||
@ -48,11 +49,12 @@ impl ProcColumn {
|
||||
ProcColumn::Command => &["Command"],
|
||||
ProcColumn::CpuPercent => &["CPU%"],
|
||||
// TODO: Change this
|
||||
ProcColumn::MemValue | ProcColumn::MemPercent => &["Mem", "Mem%"],
|
||||
ProcColumn::MemValue | ProcColumn::MemPercent => &["Mem", "Mem%", "Memory", "Memory%"],
|
||||
ProcColumn::VirtualMem => &["Virt", "Virtual", "VirtMem", "Virtual Memory"],
|
||||
ProcColumn::ReadPerSecond => &["R/s", "Read", "Rps"],
|
||||
ProcColumn::WritePerSecond => &["W/s", "Write", "Wps"],
|
||||
ProcColumn::TotalRead => &["T.Read", "TWrite"],
|
||||
ProcColumn::TotalWrite => &["T.Write", "TRead"],
|
||||
ProcColumn::TotalRead => &["T.Read", "TRead", "Total Read"],
|
||||
ProcColumn::TotalWrite => &["T.Write", "TWrite", "Total Write"],
|
||||
ProcColumn::State => &["State"],
|
||||
ProcColumn::User => &["User"],
|
||||
ProcColumn::Time => &["Time"],
|
||||
@ -71,6 +73,7 @@ impl ColumnHeader for ProcColumn {
|
||||
ProcColumn::CpuPercent => "CPU%",
|
||||
ProcColumn::MemValue => "Mem",
|
||||
ProcColumn::MemPercent => "Mem%",
|
||||
ProcColumn::VirtualMem => "Virt",
|
||||
ProcColumn::Pid => "PID",
|
||||
ProcColumn::Count => "Count",
|
||||
ProcColumn::Name => "Name",
|
||||
@ -118,6 +121,9 @@ impl SortsRow for ProcColumn {
|
||||
ProcColumn::MemValue | ProcColumn::MemPercent => {
|
||||
data.sort_by(|a, b| sort_partial_fn(descending)(&a.mem_usage, &b.mem_usage));
|
||||
}
|
||||
ProcColumn::VirtualMem => {
|
||||
data.sort_by(|a, b| sort_partial_fn(descending)(&a.virtual_mem, &b.virtual_mem));
|
||||
}
|
||||
ProcColumn::Pid => {
|
||||
data.sort_by(|a, b| sort_partial_fn(descending)(a.pid, b.pid));
|
||||
}
|
||||
@ -184,6 +190,7 @@ impl<'de> Deserialize<'de> for ProcColumn {
|
||||
"cpu%" => Ok(ProcColumn::CpuPercent),
|
||||
// TODO: Maybe change this in the future.
|
||||
"mem" | "mem%" => Ok(ProcColumn::MemPercent),
|
||||
"virt" | "virtual" | "virtmem" | "virtual memory" => Ok(ProcColumn::VirtualMem),
|
||||
"pid" => Ok(ProcColumn::Pid),
|
||||
"count" => Ok(ProcColumn::Count),
|
||||
"name" => Ok(ProcColumn::Name),
|
||||
@ -214,6 +221,7 @@ impl From<&ProcColumn> for ProcWidgetColumn {
|
||||
ProcColumn::Name | ProcColumn::Command => ProcWidgetColumn::ProcNameOrCommand,
|
||||
ProcColumn::CpuPercent => ProcWidgetColumn::Cpu,
|
||||
ProcColumn::MemPercent | ProcColumn::MemValue => ProcWidgetColumn::Mem,
|
||||
ProcColumn::VirtualMem => ProcWidgetColumn::VirtualMem,
|
||||
ProcColumn::ReadPerSecond => ProcWidgetColumn::ReadPerSecond,
|
||||
ProcColumn::WritePerSecond => ProcWidgetColumn::WritePerSecond,
|
||||
ProcColumn::TotalRead => ProcWidgetColumn::TotalRead,
|
||||
|
@ -199,6 +199,7 @@ pub struct ProcWidgetData {
|
||||
pub id: Id,
|
||||
pub cpu_usage_percent: f32,
|
||||
pub mem_usage: MemUsage,
|
||||
pub virtual_mem: u64,
|
||||
pub rps: u64,
|
||||
pub wps: u64,
|
||||
pub total_read: u64,
|
||||
@ -229,7 +230,7 @@ impl ProcWidgetData {
|
||||
let mem_usage = if is_mem_percent {
|
||||
MemUsage::Percent(process.mem_usage_percent)
|
||||
} else {
|
||||
MemUsage::Bytes(process.mem_usage_bytes)
|
||||
MemUsage::Bytes(process.mem_usage)
|
||||
};
|
||||
|
||||
Self {
|
||||
@ -238,10 +239,11 @@ impl ProcWidgetData {
|
||||
id,
|
||||
cpu_usage_percent: process.cpu_usage_percent,
|
||||
mem_usage,
|
||||
rps: process.read_bytes_per_sec,
|
||||
wps: process.write_bytes_per_sec,
|
||||
total_read: process.total_read_bytes,
|
||||
total_write: process.total_write_bytes,
|
||||
virtual_mem: process.virtual_mem,
|
||||
rps: process.read_per_sec,
|
||||
wps: process.write_per_sec,
|
||||
total_read: process.total_read,
|
||||
total_write: process.total_write,
|
||||
process_state: process.process_state.0,
|
||||
process_char: process.process_state.1,
|
||||
user: process.user.to_string(),
|
||||
@ -302,6 +304,7 @@ impl ProcWidgetData {
|
||||
match column {
|
||||
ProcColumn::CpuPercent => format!("{:.1}%", self.cpu_usage_percent),
|
||||
ProcColumn::MemValue | ProcColumn::MemPercent => self.mem_usage.to_string(),
|
||||
ProcColumn::VirtualMem => binary_byte_string(self.virtual_mem),
|
||||
ProcColumn::Pid => self.pid.to_string(),
|
||||
ProcColumn::Count => self.num_similar.to_string(),
|
||||
ProcColumn::Name | ProcColumn::Command => self.id.to_prefixed_string(),
|
||||
@ -332,6 +335,7 @@ impl DataToCell<ProcColumn> for ProcWidgetData {
|
||||
Some(match column {
|
||||
ProcColumn::CpuPercent => format!("{:.1}%", self.cpu_usage_percent).into(),
|
||||
ProcColumn::MemValue | ProcColumn::MemPercent => self.mem_usage.to_string().into(),
|
||||
ProcColumn::VirtualMem => binary_byte_string(self.virtual_mem).into(),
|
||||
ProcColumn::Pid => self.pid.to_string().into(),
|
||||
ProcColumn::Count => self.num_similar.to_string().into(),
|
||||
ProcColumn::Name | ProcColumn::Command => self.id.to_prefixed_string().into(),
|
||||
|
@ -836,27 +836,27 @@ impl Prefix {
|
||||
),
|
||||
PrefixType::MemBytes => matches_condition(
|
||||
&numerical_query.condition,
|
||||
process.mem_usage_bytes as f64,
|
||||
process.mem_usage as f64,
|
||||
numerical_query.value,
|
||||
),
|
||||
PrefixType::Rps => matches_condition(
|
||||
&numerical_query.condition,
|
||||
process.read_bytes_per_sec as f64,
|
||||
process.read_per_sec as f64,
|
||||
numerical_query.value,
|
||||
),
|
||||
PrefixType::Wps => matches_condition(
|
||||
&numerical_query.condition,
|
||||
process.write_bytes_per_sec as f64,
|
||||
process.write_per_sec as f64,
|
||||
numerical_query.value,
|
||||
),
|
||||
PrefixType::TRead => matches_condition(
|
||||
&numerical_query.condition,
|
||||
process.total_read_bytes as f64,
|
||||
process.total_read as f64,
|
||||
numerical_query.value,
|
||||
),
|
||||
PrefixType::TWrite => matches_condition(
|
||||
&numerical_query.condition,
|
||||
process.total_write_bytes as f64,
|
||||
process.total_write as f64,
|
||||
numerical_query.value,
|
||||
),
|
||||
#[cfg(feature = "gpu")]
|
||||
|
@ -184,3 +184,8 @@ fn test_styling_sanity_check_2() {
|
||||
fn test_filtering() {
|
||||
run_and_kill(&["-C", "./tests/valid_configs/filtering.toml"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_proc_columns() {
|
||||
run_and_kill(&["-C", "./tests/valid_configs/proc_columns.toml"]);
|
||||
}
|
||||
|
2
tests/valid_configs/proc_columns.toml
Normal file
2
tests/valid_configs/proc_columns.toml
Normal file
@ -0,0 +1,2 @@
|
||||
[processes]
|
||||
columns = ["PID", "Name", "CPU%", "Mem%", "Virt", "Rps", "Wps", "TRead", "Twrite", "User", "State", "Time"]
|
Loading…
x
Reference in New Issue
Block a user