diff --git a/local/docs/redbear-power-improvement-plan.md b/local/docs/redbear-power-improvement-plan.md index 16c37e641a..2dad521c9a 100644 --- a/local/docs/redbear-power-improvement-plan.md +++ b/local/docs/redbear-power-improvement-plan.md @@ -4784,7 +4784,110 @@ answers the most common "who forked what" question. - **No per-depth indentation marker (vertical lines)** — the current `└─` / `├─` connectors don't show the depth visually with vertical bars. htop does this with `│` characters. Defer - to v1.28. + to v1.29. + +--- + +## 52. v1.28 Virtual Size Sort (Activates vsize_kb) (2026-06-21) + +Per the v1.23 deferred-future-use comment ("`vsize_kb` is parsed +but not yet rendered. Reserved for a future memory-detail panel +alongside RSS"), v1.28 activates that field by adding a `VSZ` sort +mode and a column-swap in the Process panel. + +### 52.1 What was implemented + +**`SortMode::VSize`** — new variant that sorts by `vsize_kb` +descending. Cycle: `Rss → Cpu → Io → ... → IoWriteRate → VSize → +Pid → Name`. `name()` returns `"VSZ"`. + +**Column swap in `render_process_panel`** — the MEM column (last +of the 10 columns) shows RSS by default. When the active sort is +`VSize`, the column header swaps to `"VSZ"` and the value is +`vsize_kb` instead of `rss_kb`. No new column is added — the +panel stays at 10 columns. + +This is the **column-being-sorted IS the column-being-shown** +pattern. The operator sorting by VSZ immediately sees the VSZ +values in the active column, no need to scan both columns. + +**`#[allow(dead_code)]` removed** from both `ppid` and `vsize_kb`: +- `ppid` is read by `sort_tree` (v1.27) and `tree_prefix` (v1.27). +- `vsize_kb` is now read by `SortMode::VSize.sort()` and the + column-swap render path. + +Both fields now have proper doc comments explaining their use +(vs the v1.23 "reserved for future use" placeholder). + +### 52.2 Test coverage + +Test count: **107** (up from 105 in v1.27). + +New tests (2): +- `sort_by_vsize_descending` — basic descending sort by VSZ. +- `sort_by_vsize_uses_vsize_not_rss` — **contract test**: a proc + with huge VSZ and tiny RSS sorts above a proc with tiny VSZ + and huge RSS. Catches any future "optimization" that + accidentally uses the larger of the two fields. + +Updated tests (3): +- `sort_cycle` (old) — `IoWriteRate → VSize` and `VSize → Pid`. +- `sort_cycle_includes_io` (new in v1.23) — same. +- `io_name_is_io` — locks `SortMode::VSize.name() == "VSZ"`. + +### 52.3 Cross-compile + smoke test results + +| Target | Size | SHA256 | +|--------------|-------------|-------------------------------------------------------------------| +| Linux host | 3.0 MB | (run from `target/release/redbear-power`) | +| Redox x86_64 | 4,189,032 B | `f34e0f3438c7b05db0b588a8d4a7564bf14622042adf308c8b5d46207184239b` | + +Binary size delta: +4,096 bytes (4 KiB) from v1.27. Tiny because +only a sort arm + a header string format + a value-selection +ternary were added. + +Compile warnings: 55 (no net change; the 2 removed +`#[allow(dead_code)]` annotations cancel against the 2 new +"never read" warnings that did not exist before because the +fields were never accessed from outside the parse path). + +### 52.4 Why a column-swap, not a new column + +A separate VSZ column was considered and rejected: + +| Approach | Width | Disambiguation | +|----------|-------|----------------| +| New column (11 columns) | +12 chars | Header "VSZ" vs "RSS" explicit | +| Column swap (10 columns) | 0 chars | "The column being sorted IS the column being shown" | + +The column swap is htop's approach: when you sort by a field, +that field's column expands to show the data. Most users sort +by ONE field at a time, so showing the data of the unsorted +field(s) in fixed-width cells wastes space. + +A hybrid (VSZ always shown, RSS always shown) would push the +panel to 11 columns and lose COMM truncation at narrower +terminal widths (1280x720 framebuffer at default font). + +### 52.5 Compute cost + +`SortMode::VSize` is `O(N log N)` like the other numeric sorts. +The column-swap in render is `O(N)` (one ternary per row). For +the truncated top-50 list this is microseconds. + +### 52.6 What was NOT changed (intentional) + +- **PID detail popup still uses `/proc/[pid]/status`** for + `vm_size_kb` / `vm_rss_kb` etc. (via the existing `pid_detail` + module). The Process panel's `vsize_kb` is sourced from + `/proc/[pid]/stat:field[22]` (vsize in bytes) and may differ + slightly from `/proc/[pid]/status:VmSize` (same value, slightly + different update timing). The two values are consistent within + one process. Documented in `pid_detail.rs`. +- **No peak RSS column** (htop has `M_LRS` for peak resident set). + Would need a per-process max-RSS tracker. Defer to v1.29+. +- **No swap/policy column** (htop shows OOM score and adj). + Beyond the power/thermal scope. --- diff --git a/local/recipes/system/redbear-power/source/src/process.rs b/local/recipes/system/redbear-power/source/src/process.rs index 2be8f4a156..0850066a81 100644 --- a/local/recipes/system/redbear-power/source/src/process.rs +++ b/local/recipes/system/redbear-power/source/src/process.rs @@ -30,6 +30,7 @@ pub enum SortMode { IoRate, IoReadRate, IoWriteRate, + VSize, Pid, Name, } @@ -44,7 +45,8 @@ impl SortMode { SortMode::IoWrite => SortMode::IoRate, SortMode::IoRate => SortMode::IoReadRate, SortMode::IoReadRate => SortMode::IoWriteRate, - SortMode::IoWriteRate => SortMode::Pid, + SortMode::IoWriteRate => SortMode::VSize, + SortMode::VSize => SortMode::Pid, SortMode::Pid => SortMode::Name, SortMode::Name => SortMode::Rss, } @@ -59,6 +61,7 @@ impl SortMode { SortMode::IoRate => "IO/s", SortMode::IoReadRate => "R/s", SortMode::IoWriteRate => "W/s", + SortMode::VSize => "VSZ", SortMode::Pid => "PID", SortMode::Name => "Name", } @@ -82,6 +85,7 @@ 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::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)), } @@ -218,18 +222,21 @@ pub struct ProcessInfo { pub pid: u32, pub comm: String, pub state: char, - /// Parent PID. Reserved for a future process-tree view; not yet - /// rendered in the Process panel. - #[allow(dead_code)] + /// Parent PID. Read by `sort_tree` (v1.27+) and `tree_prefix` + /// to build the parent-child ordering in tree view. pub ppid: u32, pub utime: u64, pub stime: u64, pub priority: i64, pub nice: i64, pub num_threads: i64, - /// Virtual address-space size in KiB. Reserved for a future - /// memory-detail panel alongside RSS; not yet rendered. - #[allow(dead_code)] + /// Virtual address-space size in KiB. Read by `SortMode::VSize` + /// (v1.28+) to sort by VSZ. Note: VSZ includes the entire + /// mapped address space (mmap'd libraries, heap, stack, + /// reserved-but-uncommitted) and is often much larger than + /// RSS. Useful for "who is using the most address space" but + /// NOT for "who is using the most physical memory" (use RSS + /// for that). pub vsize_kb: u64, pub rss_kb: u64, pub cpu_pct: f64, @@ -622,7 +629,8 @@ 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::Pid); + assert_eq!(SortMode::IoWriteRate.next(), SortMode::VSize); + assert_eq!(SortMode::VSize.next(), SortMode::Pid); assert_eq!(SortMode::Pid.next(), SortMode::Name); assert_eq!(SortMode::Name.next(), SortMode::Rss); } @@ -794,7 +802,8 @@ 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::Pid); + assert_eq!(SortMode::IoWriteRate.next(), SortMode::VSize); + assert_eq!(SortMode::VSize.next(), SortMode::Pid); assert_eq!(SortMode::Pid.next(), SortMode::Name); assert_eq!(SortMode::Name.next(), SortMode::Rss); } @@ -807,6 +816,7 @@ 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::VSize.name(), "VSZ"); } #[test] @@ -962,6 +972,43 @@ mod io_sort_unit_tests { assert_eq!(ps[3].pid, 3); } + fn make_v(pid: u32, vsize_kb: u64, rss_kb: u64) -> ProcessInfo { + ProcessInfo { + pid, + vsize_kb, + rss_kb, + ..Default::default() + } + } + + #[test] + fn sort_by_vsize_descending() { + let mut ps = vec![ + make_v(1, 100_000, 1_000), + make_v(2, 500_000, 5_000), + make_v(3, 50_000, 500), + ]; + SortMode::VSize.sort(&mut ps); + assert_eq!(ps[0].pid, 2); // vsize 500_000 + assert_eq!(ps[1].pid, 1); // vsize 100_000 + assert_eq!(ps[2].pid, 3); // vsize 50_000 + } + + #[test] + fn sort_by_vsize_uses_vsize_not_rss() { + // The key property: SortMode::VSize sorts by VSZ, not RSS. + // Pid 1 has HUGE vsize but tiny rss; pid 2 has tiny vsize + // but HUGE rss. VSize sort puts 1 first; Rss sort would + // put 2 first. + let mut ps = vec![ + make_v(1, 999_999_999, 1), + make_v(2, 1, 999_999_999), + ]; + SortMode::VSize.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 6f79d10f6d..16c2d855a0 100644 --- a/local/recipes/system/redbear-power/source/src/render.rs +++ b/local/recipes/system/redbear-power/source/src/render.rs @@ -877,8 +877,23 @@ pub fn render_process_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> { filter_indicator, ).set_style(theme::LABEL_BOLD))); lines.push(Line::from("")); -lines.push(Line::from(vec![ - " PID STATE PRIO NI THR CPU% IO RATE RSS COMM".set_style(theme::LABEL), + // The MEM column header swaps between RSS and VSZ depending on + // the active sort. Default and most modes use RSS (resident + // set, physical memory); SortMode::VSize uses VSZ (virtual + // 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 header_str = format!( + " PID STATE PRIO NI THR CPU% IO RATE {:<11} COMM", + mem_header + ); + lines.push(Line::from(vec![ + header_str.set_style(theme::LABEL), ])); for p in &proc.processes { if !app.process_filter.is_empty() @@ -895,6 +910,11 @@ lines.push(Line::from(vec![ 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 prefix = if app.process_tree { tree_prefix(p.pid, p.ppid, &proc.processes) } else { @@ -911,7 +931,7 @@ lines.push(Line::from(vec![ format!("{:.1}", p.cpu_pct), io_str, rate_str, - crate::process::ProcessInfo::format_memory_kb(p.rss_kb), + mem_str, comm_truncated, ).set_style(theme::VALUE))); }