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:
Clement Tsang 2025-07-30 00:24:21 -04:00 committed by GitHub
parent d06f239b5f
commit 4d935bdd70
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 106 additions and 72 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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")]

View File

@ -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"]);
}

View File

@ -0,0 +1,2 @@
[processes]
columns = ["PID", "Name", "CPU%", "Mem%", "Virt", "Rps", "Wps", "TRead", "Twrite", "User", "State", "Time"]