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:
@@ -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)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user