diff --git a/local/recipes/system/redbear-power/source/src/process.rs b/local/recipes/system/redbear-power/source/src/process.rs index 2a94e1a836..0670e31e3f 100644 --- a/local/recipes/system/redbear-power/source/src/process.rs +++ b/local/recipes/system/redbear-power/source/src/process.rs @@ -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, + /// 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 = None; let mut write: Option = 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::().ok(); } else if let Some(rest) = line.strip_prefix("write_bytes:") { write = rest.trim().parse::().ok(); + } else if let Some(rest) = line.strip_prefix("rchar:") { + rchar = rest.trim().parse::().unwrap_or(0); + } else if let Some(rest) = line.strip_prefix("wchar:") { + wchar = rest.trim().parse::().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 { 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 { 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, diff --git a/local/recipes/system/redbear-power/source/src/render.rs b/local/recipes/system/redbear-power/source/src/render.rs index 07e52c08a4..747dcfcba4 100644 --- a/local/recipes/system/redbear-power/source/src/render.rs +++ b/local/recipes/system/redbear-power/source/src/render.rs @@ -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