redbear-power: v1.28 virtual size sort (activates vsize_kb)

Closes the v1.23-deferred 'vsize_kb' future-use. Adds SortMode::VSize
that sorts by virtual size, and swaps the Process panel's MEM
column to show VSZ (instead of RSS) when that sort is active.

The column-swap pattern (the column being sorted IS the column
being shown) keeps the panel at 10 columns instead of growing
to 11. htop uses the same pattern: when you sort by a field,
that field's column expands to show the data. No new column
means no terminal-width pressure at 1280x720 framebuffer.

The 'ppid' field's #[allow(dead_code)] is also removed (now
actively read by sort_tree + tree_prefix from v1.27). Both
fields now have proper doc comments explaining their use
(vs the v1.23 'reserved for future use' placeholder).

VSZ is a virtual address-space metric (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).
This caveat is now documented in the field's doc comment to
prevent operator confusion.

Test count 105 -> 107 (+2):
- sort_by_vsize_descending (basic)
- sort_by_vsize_uses_vsize_not_rss (contract test: huge VSZ +
  tiny RSS sorts above tiny VSZ + huge RSS; catches any
  'optimization' that uses the larger of the two fields)
- sort_cycle and io_name_is_io updated for VSize

Redox stripped binary: 4,189,032 bytes (+4 KiB from v1.27).
Compile warnings: 55 (no net change; the 2 removed
#[allow(dead_code)] annotations cancel against 2 new
warnings that did not exist before because the fields were
only accessed from the parse path).

Docs: local/docs/redbear-power-improvement-plan.md \xC2\xA752
This commit is contained in:
2026-06-21 01:46:11 +03:00
parent 0990a6996a
commit df3021575e
3 changed files with 183 additions and 13 deletions
+104 -1
View File
@@ -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.
---
@@ -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,
@@ -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)));
}