redbear-power: v1.33 SortMode::RChar / WChar (VFS-level IO)
Adds two new sort modes that target the VFS-level byte counts
exposed by /proc/[pid]/io:rchar and wchar. These differ from
the existing read_bytes/write_bytes:
read_bytes: storage-level bytes that hit the disk
rchar: VFS-level bytes (includes cache hits, tty output)
Useful for the 'is this proc doing lots of syscalls?' question
that read_bytes misses (cache-served reads count toward rchar
but not read_bytes).
- New fields on ProcessInfo: io_rchar_kb: u64, io_wchar_kb: u64
(u64, not Option: rchar/wchar default to 0 if absent on
very old kernels, never sentinel-needed)
- read_io_file() now returns a 4-tuple
(read_bytes, write_bytes, rchar, wchar) instead of 2-tuple
- New SortMode variants RChar and WChar
- cycle: Rss -> Cpu -> Io -> ... -> IoWriteRate -> RChar
-> WChar -> VSize -> Pid -> Name -> Rss
- name(): 'RChr' and 'WChr'
- sort(): descending by io_rchar_kb / io_wchar_kb
- Column-swap: when sort is RChar, MEM column shows RChr value;
when WChar, shows WChr. Default and other modes use RSS.
Test count 123 -> 125 (+2):
- sort_by_rchar_descending (VFS reads, pid 2 with 5000 first)
- sort_by_wchar_descending (VFS writes, pid 1 with 999_999 first)
- sort_cycle and io_name_is_io updated for RChar/WChar
Redox stripped binary: 4,225,896 bytes (+12 KiB from v1.32).
Compile warnings: 55 (unchanged).
This commit is contained in:
@@ -30,6 +30,8 @@ pub enum SortMode {
|
||||
IoRate,
|
||||
IoReadRate,
|
||||
IoWriteRate,
|
||||
RChar,
|
||||
WChar,
|
||||
VSize,
|
||||
Pid,
|
||||
Name,
|
||||
@@ -45,7 +47,9 @@ impl SortMode {
|
||||
SortMode::IoWrite => SortMode::IoRate,
|
||||
SortMode::IoRate => SortMode::IoReadRate,
|
||||
SortMode::IoReadRate => SortMode::IoWriteRate,
|
||||
SortMode::IoWriteRate => SortMode::VSize,
|
||||
SortMode::IoWriteRate => SortMode::RChar,
|
||||
SortMode::RChar => SortMode::WChar,
|
||||
SortMode::WChar => SortMode::VSize,
|
||||
SortMode::VSize => SortMode::Pid,
|
||||
SortMode::Pid => SortMode::Name,
|
||||
SortMode::Name => SortMode::Rss,
|
||||
@@ -61,6 +65,8 @@ impl SortMode {
|
||||
SortMode::IoRate => "IO/s",
|
||||
SortMode::IoReadRate => "R/s",
|
||||
SortMode::IoWriteRate => "W/s",
|
||||
SortMode::RChar => "RChr",
|
||||
SortMode::WChar => "WChr",
|
||||
SortMode::VSize => "VSZ",
|
||||
SortMode::Pid => "PID",
|
||||
SortMode::Name => "Name",
|
||||
@@ -85,6 +91,8 @@ impl SortMode {
|
||||
SortMode::IoRate => sort_by_io_rate_field(processes, |p| p.io_total_rate_kbs()),
|
||||
SortMode::IoReadRate => sort_by_io_rate_field(processes, |p| p.io_read_rate_kbs),
|
||||
SortMode::IoWriteRate => sort_by_io_rate_field(processes, |p| p.io_write_rate_kbs),
|
||||
SortMode::RChar => processes.sort_by(|a, b| b.io_rchar_kb.cmp(&a.io_rchar_kb)),
|
||||
SortMode::WChar => processes.sort_by(|a, b| b.io_wchar_kb.cmp(&a.io_wchar_kb)),
|
||||
SortMode::VSize => processes.sort_by(|a, b| b.vsize_kb.cmp(&a.vsize_kb)),
|
||||
SortMode::Pid => processes.sort_by_key(|p| p.pid),
|
||||
SortMode::Name => processes.sort_by(|a, b| a.comm.cmp(&b.comm)),
|
||||
@@ -283,6 +291,14 @@ pub struct ProcessInfo {
|
||||
/// Cumulative write bytes (KiB) from
|
||||
/// `/proc/[pid]/io:write_bytes`. Same caveats as `io_read_kb`.
|
||||
pub io_write_kb: Option<u64>,
|
||||
/// Cumulative `rchar` bytes (KiB) from `/proc/[pid]/io:rchar`.
|
||||
/// VFS-level read byte count (includes cache hits, tty, etc).
|
||||
/// Always Some — defaults to 0 if the field is absent on the
|
||||
/// kernel (very old kernels).
|
||||
pub io_rchar_kb: u64,
|
||||
/// Cumulative `wchar` bytes (KiB) from `/proc/[pid]/io:wchar`.
|
||||
/// VFS-level write byte count. Always Some.
|
||||
pub io_wchar_kb: u64,
|
||||
/// Read throughput (KiB/s) computed as the delta of `io_read_kb`
|
||||
/// across two reads divided by `dt_secs`. `None` when the prev
|
||||
/// read is missing (first sample after startup) or when
|
||||
@@ -361,22 +377,34 @@ fn read_comm(pid: u32) -> String {
|
||||
.unwrap_or_else(|| "?".to_string())
|
||||
}
|
||||
|
||||
/// Parse `/proc/[pid]/io` content and return `(read_bytes, write_bytes)`
|
||||
/// in bytes (not KiB). Returns `None` if the file cannot be read.
|
||||
/// The conversion to KiB happens in the caller so a single sentinel
|
||||
/// value (`None`) propagates through the field types.
|
||||
fn read_io_file(pid: u32) -> Option<(u64, u64)> {
|
||||
/// Parse `/proc/[pid]/io` content and return the four fields
|
||||
/// we care about: (read_bytes, write_bytes, rchar, wchar). The
|
||||
/// two `bytes` fields are the I/O actually performed (via
|
||||
/// read/write syscalls that hit storage); `rchar`/`wchar` are
|
||||
/// the cumulative byte counts the kernel's VFS layer saw
|
||||
/// (includes reads/writes served from page cache, terminal
|
||||
/// output, etc.). Returns `None` if the file cannot be read.
|
||||
/// `read_bytes`/`write_bytes` are mandatory; `rchar`/`wchar`
|
||||
/// may be absent on older kernels — callers default to 0 in
|
||||
/// that case.
|
||||
fn read_io_file(pid: u32) -> Option<(u64, u64, u64, u64)> {
|
||||
let content = fs::read_to_string(format!("/proc/{pid}/io")).ok()?;
|
||||
let mut read: Option<u64> = None;
|
||||
let mut write: Option<u64> = None;
|
||||
let mut rchar: u64 = 0;
|
||||
let mut wchar: u64 = 0;
|
||||
for line in content.lines() {
|
||||
if let Some(rest) = line.strip_prefix("read_bytes:") {
|
||||
read = rest.trim().parse::<u64>().ok();
|
||||
} else if let Some(rest) = line.strip_prefix("write_bytes:") {
|
||||
write = rest.trim().parse::<u64>().ok();
|
||||
} else if let Some(rest) = line.strip_prefix("rchar:") {
|
||||
rchar = rest.trim().parse::<u64>().unwrap_or(0);
|
||||
} else if let Some(rest) = line.strip_prefix("wchar:") {
|
||||
wchar = rest.trim().parse::<u64>().unwrap_or(0);
|
||||
}
|
||||
}
|
||||
Some((read?, write?))
|
||||
Some((read?, write?, rchar, wchar))
|
||||
}
|
||||
|
||||
/// Compute KiB/s rate from a prev/current sample pair. Returns `None`
|
||||
@@ -415,9 +443,9 @@ fn parse_stat_line(line: &str) -> Option<ProcessInfo> {
|
||||
let num_threads: i64 = fields[17].parse().ok()?;
|
||||
let vsize_bytes: i64 = fields[20].parse().ok()?;
|
||||
let rss_pages: i64 = fields[21].parse().ok()?;
|
||||
let (io_read_bytes, io_write_bytes) = match read_io_file(pid) {
|
||||
Some((r, w)) => (Some(r / 1024), Some(w / 1024)),
|
||||
None => (None, None),
|
||||
let (io_read_bytes, io_write_bytes, rchar_bytes, wchar_bytes) = match read_io_file(pid) {
|
||||
Some((r, w, rc, wc)) => (Some(r / 1024), Some(w / 1024), rc / 1024, wc / 1024),
|
||||
None => (None, None, 0, 0),
|
||||
};
|
||||
Some(ProcessInfo {
|
||||
pid,
|
||||
@@ -434,6 +462,8 @@ fn parse_stat_line(line: &str) -> Option<ProcessInfo> {
|
||||
cpu_pct: 0.0,
|
||||
io_read_kb: io_read_bytes,
|
||||
io_write_kb: io_write_bytes,
|
||||
io_rchar_kb: rchar_bytes,
|
||||
io_wchar_kb: wchar_bytes,
|
||||
io_read_rate_kbs: None,
|
||||
io_write_rate_kbs: None,
|
||||
})
|
||||
@@ -663,7 +693,9 @@ mod sort_unit_tests {
|
||||
assert_eq!(SortMode::IoWrite.next(), SortMode::IoRate);
|
||||
assert_eq!(SortMode::IoRate.next(), SortMode::IoReadRate);
|
||||
assert_eq!(SortMode::IoReadRate.next(), SortMode::IoWriteRate);
|
||||
assert_eq!(SortMode::IoWriteRate.next(), SortMode::VSize);
|
||||
assert_eq!(SortMode::IoWriteRate.next(), SortMode::RChar);
|
||||
assert_eq!(SortMode::RChar.next(), SortMode::WChar);
|
||||
assert_eq!(SortMode::WChar.next(), SortMode::VSize);
|
||||
assert_eq!(SortMode::VSize.next(), SortMode::Pid);
|
||||
assert_eq!(SortMode::Pid.next(), SortMode::Name);
|
||||
assert_eq!(SortMode::Name.next(), SortMode::Rss);
|
||||
@@ -836,7 +868,9 @@ mod io_sort_unit_tests {
|
||||
assert_eq!(SortMode::IoWrite.next(), SortMode::IoRate);
|
||||
assert_eq!(SortMode::IoRate.next(), SortMode::IoReadRate);
|
||||
assert_eq!(SortMode::IoReadRate.next(), SortMode::IoWriteRate);
|
||||
assert_eq!(SortMode::IoWriteRate.next(), SortMode::VSize);
|
||||
assert_eq!(SortMode::IoWriteRate.next(), SortMode::RChar);
|
||||
assert_eq!(SortMode::RChar.next(), SortMode::WChar);
|
||||
assert_eq!(SortMode::WChar.next(), SortMode::VSize);
|
||||
assert_eq!(SortMode::VSize.next(), SortMode::Pid);
|
||||
assert_eq!(SortMode::Pid.next(), SortMode::Name);
|
||||
assert_eq!(SortMode::Name.next(), SortMode::Rss);
|
||||
@@ -850,6 +884,8 @@ mod io_sort_unit_tests {
|
||||
assert_eq!(SortMode::IoRate.name(), "IO/s");
|
||||
assert_eq!(SortMode::IoReadRate.name(), "R/s");
|
||||
assert_eq!(SortMode::IoWriteRate.name(), "W/s");
|
||||
assert_eq!(SortMode::RChar.name(), "RChr");
|
||||
assert_eq!(SortMode::WChar.name(), "WChr");
|
||||
assert_eq!(SortMode::VSize.name(), "VSZ");
|
||||
}
|
||||
|
||||
@@ -1043,6 +1079,51 @@ mod io_sort_unit_tests {
|
||||
assert_eq!(ps[1].pid, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sort_by_rchar_descending() {
|
||||
// RChar sort uses io_rchar_kb (VFS-level reads).
|
||||
let mut ps = vec![
|
||||
ProcessInfo {
|
||||
pid: 1,
|
||||
io_rchar_kb: 100,
|
||||
..Default::default()
|
||||
},
|
||||
ProcessInfo {
|
||||
pid: 2,
|
||||
io_rchar_kb: 5000,
|
||||
..Default::default()
|
||||
},
|
||||
ProcessInfo {
|
||||
pid: 3,
|
||||
io_rchar_kb: 0,
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
SortMode::RChar.sort(&mut ps);
|
||||
assert_eq!(ps[0].pid, 2);
|
||||
assert_eq!(ps[1].pid, 1);
|
||||
assert_eq!(ps[2].pid, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sort_by_wchar_descending() {
|
||||
let mut ps = vec![
|
||||
ProcessInfo {
|
||||
pid: 1,
|
||||
io_wchar_kb: 999_999,
|
||||
..Default::default()
|
||||
},
|
||||
ProcessInfo {
|
||||
pid: 2,
|
||||
io_wchar_kb: 1,
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
SortMode::WChar.sort(&mut ps);
|
||||
assert_eq!(ps[0].pid, 1);
|
||||
assert_eq!(ps[1].pid, 2);
|
||||
}
|
||||
|
||||
fn make_p(ppid: u32, pid: u32) -> ProcessInfo {
|
||||
ProcessInfo {
|
||||
pid,
|
||||
|
||||
@@ -923,10 +923,11 @@ pub fn render_process_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
|
||||
// address space) so the column being sorted IS the column
|
||||
// being shown. No new column is added; this keeps the panel
|
||||
// width bounded.
|
||||
let mem_header = if app.process_sort == crate::process::SortMode::VSize {
|
||||
"VSZ"
|
||||
} else {
|
||||
"RSS"
|
||||
let mem_header = match app.process_sort {
|
||||
crate::process::SortMode::VSize => "VSZ",
|
||||
crate::process::SortMode::RChar => "RChr",
|
||||
crate::process::SortMode::WChar => "WChr",
|
||||
_ => "RSS",
|
||||
};
|
||||
let header_str = format!(
|
||||
" PID STATE PRIO NI THR CPU% IO RATE {:<11} IO-RATE CPU% RSS COMM",
|
||||
@@ -956,10 +957,17 @@ pub fn render_process_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
|
||||
Some(kbs) => crate::process::ProcessInfo::format_rate_kbs(kbs),
|
||||
None => "—".to_string(),
|
||||
};
|
||||
let mem_str = if app.process_sort == crate::process::SortMode::VSize {
|
||||
crate::process::ProcessInfo::format_memory_kb(p.vsize_kb)
|
||||
} else {
|
||||
crate::process::ProcessInfo::format_memory_kb(p.rss_kb)
|
||||
let mem_str = match app.process_sort {
|
||||
crate::process::SortMode::VSize => {
|
||||
crate::process::ProcessInfo::format_memory_kb(p.vsize_kb)
|
||||
}
|
||||
crate::process::SortMode::RChar => {
|
||||
crate::process::ProcessInfo::format_memory_kb(p.io_rchar_kb)
|
||||
}
|
||||
crate::process::SortMode::WChar => {
|
||||
crate::process::ProcessInfo::format_memory_kb(p.io_wchar_kb)
|
||||
}
|
||||
_ => crate::process::ProcessInfo::format_memory_kb(p.rss_kb),
|
||||
};
|
||||
// Three per-PID sparklines: IO-RATE (12 samples),
|
||||
// CPU% (6 samples), RSS (6 samples). The smaller
|
||||
|
||||
Reference in New Issue
Block a user