From aaa1b950b44022b85e5f7817a2e762f5443d82e8 Mon Sep 17 00:00:00 2001 From: vasilito Date: Sun, 21 Jun 2026 00:50:30 +0300 Subject: [PATCH] redbear-power: v1.25 IO rate column + rate sort modes Per-process IO is now also a throughput metric (KiB/s), not just cumulative. Cumulative bytes favor long-lived processes regardless of activity; rate is what operators actually want for 'what is hammering the disk right now'. - New fields on ProcessInfo: io_read_rate_kbs, io_write_rate_kbs (Option; None when prev missing, current None, or dt<=0) - New method: io_total_rate_kbs() (sum; same None semantics) - New helper: compute_rate_kbs(prev, now, dt) -> Option uses saturating_sub for clock-reset safety - read_with_cpu_pct_sorted now also computes the two rate fields (negligible cost: 2 subs + 2 divs per process per refresh) - New SortMode variants: IoRate, IoReadRate, IoWriteRate inserted in cycle after IoWrite - name() returns 'IO/s', 'R/s', 'W/s' for status line - New sort_by_io_rate_field() helper (Option partial_cmp) - New format_rate_kbs() on ProcessInfo (KiB/s, MiB/s, GiB/s, TiB/s; saturates negative to 0) - New RATE column in the Process panel between IO and RSS Test count 87 -> 101 (+14): - 6 compute_rate_kbs edge cases (basic, None prev/now, dt<=0, saturating underflow, idle = zero) - 2 io_total_rate_kbs (sum, None) - 2 sort-by-rate (total, read-pushes-missing) - 4 format_rate_kbs (sub-KiB, 1 MiB, 1 GiB, negative) - sort_cycle and io_name_is_io updated for new variants Redox stripped binary: 4,168,552 bytes (+49 KiB from v1.24; 14 new tests + 2 sort modes + 2 fields + render column + 3 helpers). Compile warnings: 55 (unchanged). Docs: local/docs/redbear-power-improvement-plan.md \xC2\xA749 --- local/docs/redbear-power-improvement-plan.md | 157 +++++++++++++ .../redbear-power/source/src/process.rs | 211 +++++++++++++++++- .../system/redbear-power/source/src/render.rs | 9 +- 3 files changed, 372 insertions(+), 5 deletions(-) diff --git a/local/docs/redbear-power-improvement-plan.md b/local/docs/redbear-power-improvement-plan.md index 7e9c26e3cf..45f9c82d95 100644 --- a/local/docs/redbear-power-improvement-plan.md +++ b/local/docs/redbear-power-improvement-plan.md @@ -4425,6 +4425,163 @@ The status line (`sort: IO-R`) is sufficient disambiguation. --- +## 49. v1.25 IO Rate Column + Rate Sort (2026-06-21) + +Per the v1.22 audit (I5: "consider adding kbps or bytes/sec IO +throughput column rather than cumulative IO"), v1.25 promotes +per-process IO from a cumulative-only metric to also showing +throughput in KiB/s. Cumulative bytes favor long-lived processes +regardless of activity — a process that did 100 GB of IO three days +ago and is now idle will outrank an actively-thrashing one that +started 10 minutes ago. Rate is what operators actually want. + +### 49.1 What was implemented + +**Two new fields on `ProcessInfo`**: +- `io_read_rate_kbs: Option` — read KiB/s (delta of `io_read_kb` + across two reads divided by `dt_secs`). +- `io_write_rate_kbs: Option` — write KiB/s (delta of + `io_write_kb` across two reads divided by `dt_secs`). + +`None` when the prev sample is missing (first read after startup) or +when either `io_read_kb`/`io_write_kb` is `None` for prev or current. + +**`io_total_rate_kbs()` method** — sums read+write rates for +`SortMode::IoRate`. Same sentinel semantics as `io_total_kb()`: +returns `None` if either field is `None`. + +**`compute_rate_kbs()` helper** — private fn that does the rate math: +```rust +fn compute_rate_kbs(prev: Option, now: Option, dt_secs: f64) -> Option { + if dt_secs <= 0.0 { return None; } + let (p, n) = (prev?, now?); + let delta_kb = n.saturating_sub(p) as f64; + Some(delta_kb / dt_secs) +} +``` + +`saturating_sub` handles the (impossible in practice) clock-reset +case where a future sample is smaller than a past one. The `?` +operator propagates `None` from either prev or current. + +**`read_with_cpu_pct_sorted` extension** — now also computes the +two rate fields after computing `cpu_pct`. The same `prev_p` lookup +serves both CPU% and rate calculations. Cost: 2 saturating subs + 2 +f64 divs per process. Negligible vs. the file reads. + +**Three new `SortMode` variants**: +- `SortMode::IoRate` — by total read+write rate +- `SortMode::IoReadRate` — by read rate only +- `SortMode::IoWriteRate` — by write rate only + +Cycle updated to insert them between `IoWrite` and `Pid`: +`Rss → Cpu → Io → IoRead → IoWrite → IoRate → IoReadRate → IoWriteRate → Pid → Name`. + +`name()` returns `"IO/s"`, `"R/s"`, `"W/s"` for status-line +disambiguation (the 3-char IO/s keeps the status line tight). + +**New `sort_by_io_rate_field()` helper** — symmetric with the +existing `sort_by_io_field()` for `Option` cumulative sorts. +Uses `partial_cmp` for `Option` (NaN-safe); `unwrap_or(Equal)` +falls back to the same-ordering rule if both values are NaN. + +**New `format_rate_kbs()` helper** on `ProcessInfo` — symmetric +with `format_memory_kb()`. 1024-base binary units (KiB/s, MiB/s, +GiB/s, TiB/s). `kbs.max(0.0)` saturates negative inputs to 0 (a +"negative rate" is meaningless and indicates a test fixture or +clock-reset edge case). + +**Render-side new column** — the Process panel now has 10 columns: +`PID STATE PRIO NI THR CPU% IO RATE RSS COMM`. The RATE +column renders the total rate (read+write) via +`ProcessInfo::format_rate_kbs`. Renders em-dash when the rate is +`None` (first sample, unreadable IO, or prev sample missing). + +### 49.2 Test coverage + +Test count: **101** (up from 87 in v1.24). + +New tests (14): +- `compute_rate_kbs_basic_delta` — 1024 KiB / 2.0s = 512.0 KiB/s +- `compute_rate_kbs_returns_none_when_prev_missing` +- `compute_rate_kbs_returns_none_when_now_missing` +- `compute_rate_kbs_returns_none_when_dt_zero` (both 0.0 and -1.0) +- `compute_rate_kbs_saturates_on_underflow` (now < prev → 0.0) +- `compute_rate_kbs_first_sample_is_zero` (process idle) +- `io_total_rate_kbs_sums_read_write` (200 + 300 = 500.0) +- `io_total_rate_kbs_none_when_field_missing` +- `sort_by_io_rate_uses_total` (tie → stable input order) +- `sort_by_io_read_rate_pushes_missing_to_bottom` +- `format_rate_below_1kibs` (500.0 → "500.0 KiB/s") +- `format_rate_1mibs` (1024.0 → "1.0 MiB/s") +- `format_rate_1gibs` (1024² → "1.0 GiB/s") +- `format_rate_saturates_negative_to_zero` + +Updated tests (2): +- `sort_cycle` and `sort_cycle_includes_io` — extended for the + 3 new rate variants in the cycle. +- `io_name_is_io` — also locks "IO/s", "R/s", "W/s" strings. + +### 49.3 Cross-compile + smoke test results + +| Target | Size | SHA256 | +|--------------|-------------|-------------------------------------------------------------------| +| Linux host | 3.0 MB | (run from `target/release/redbear-power`) | +| Redox x86_64 | 4,168,552 B | `b103a0e456d308ba1e518edbf942eff17f251bfd123216287a67efaa6614aa16` | + +Binary size delta: +49,152 bytes (≈48 KiB) from v1.24. The growth +comes from 14 new tests + 2 new sort modes + 2 new fields + the +render column + `format_rate_kbs` + `compute_rate_kbs` + +`io_total_rate_kbs` + `sort_by_io_rate_field` helper. + +Smoke test confirms the RATE column header renders: +``` +PID STATE PRIO NI THR CPU% IO RATE RSS COMM +``` + +The `--once` mode uses `read()` not `read_with_cpu_pct_sorted()` so +all rate values are `None` for the first sample. The interactive TUI +populates them on the second refresh (typically 500 ms after start). + +### 49.4 Compute cost + +The new `compute_rate_kbs` adds 2 saturating subs + 2 f64 divs per +process per refresh. On a system with 600 processes and a 13-tick +(6.5 s) refresh rate, that's 600 × 4 = 2400 arithmetic ops per +6.5 s = ~370 ops/sec. Completely negligible vs. the 600 file opens +(20 syscalls each = 12,000 syscalls per 6.5 s) for the procfs reads +that we already do. + +### 49.5 Why a RATE column instead of replacing IO + +The IO column (cumulative) and the RATE column (throughput) answer +different questions: + +| Question | Column | +|----------|--------| +| What process has done the most disk IO over its lifetime? | IO | +| What process is hammering the disk RIGHT NOW? | RATE | +| Has this process's IO gone up since last check? | RATE delta | +| Will this process's log rotate soon? | IO | + +Both are useful. Removing IO would lose the cumulative view; not +adding RATE would leave operators with "is the process thrashing?" +as an unanswerable question. + +### 49.6 What was NOT changed (intentional) + +- **Per-thread IO** (htop scans `task/[pid]/io`) — not a common + operator question on a power TUI, and adds N×file-open cost. +- **RCHAR/WCHAR/SYSCR/SYSCW** (htop's "IO details" columns) — + beyond the power/thermal scope. Defer to a future v1.26 if user + demand appears. +- **Persistent rate sparkline** (rolling average of last N samples) — + a per-process IO rate over time is a natural visualization but + requires storing a Vec per process across refreshes. Defer + to a future v1.27 with proper memory accounting. + +--- + ## See Also - **`local/docs/RATATUI-APP-PATTERNS.md`** §13 — the canonical ratatui 0.30 best-practices update that this plan is derived from. Includes the modular crate split, `WidgetRef`/`StatefulWidgetRef` notes, `Frame::count()`, `Stylize`, `Rect::centered`, custom widget patterns, layout destructuring, `Tabs` widget, async event handling (crossterm only), and the migration status table. Use this as the implementation guide while this doc is the roadmap. diff --git a/local/recipes/system/redbear-power/source/src/process.rs b/local/recipes/system/redbear-power/source/src/process.rs index 3ef3b63f2a..9cbecc7612 100644 --- a/local/recipes/system/redbear-power/source/src/process.rs +++ b/local/recipes/system/redbear-power/source/src/process.rs @@ -28,6 +28,9 @@ pub enum SortMode { Io, IoRead, IoWrite, + IoRate, + IoReadRate, + IoWriteRate, Pid, Name, } @@ -39,7 +42,10 @@ impl SortMode { SortMode::Cpu => SortMode::Io, SortMode::Io => SortMode::IoRead, SortMode::IoRead => SortMode::IoWrite, - SortMode::IoWrite => SortMode::Pid, + SortMode::IoWrite => SortMode::IoRate, + SortMode::IoRate => SortMode::IoReadRate, + SortMode::IoReadRate => SortMode::IoWriteRate, + SortMode::IoWriteRate => SortMode::Pid, SortMode::Pid => SortMode::Name, SortMode::Name => SortMode::Rss, } @@ -51,6 +57,9 @@ impl SortMode { SortMode::Io => "IO", SortMode::IoRead => "IO-R", SortMode::IoWrite => "IO-W", + SortMode::IoRate => "IO/s", + SortMode::IoReadRate => "R/s", + SortMode::IoWriteRate => "W/s", SortMode::Pid => "PID", SortMode::Name => "Name", } @@ -71,6 +80,9 @@ impl SortMode { }), SortMode::IoRead => sort_by_io_field(processes, |p| p.io_read_kb), SortMode::IoWrite => sort_by_io_field(processes, |p| p.io_write_kb), + 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::Pid => processes.sort_by_key(|p| p.pid), SortMode::Name => processes.sort_by(|a, b| a.comm.cmp(&b.comm)), } @@ -93,6 +105,22 @@ where }); } +fn sort_by_io_rate_field(processes: &mut Vec, field: F) +where + F: Fn(&ProcessInfo) -> Option, +{ + processes.sort_by(|a, b| { + let ai = field(a); + let bi = field(b); + match (ai, bi) { + (Some(x), Some(y)) => y.partial_cmp(&x).unwrap_or(std::cmp::Ordering::Equal), + (Some(_), None) => std::cmp::Ordering::Less, + (None, Some(_)) => std::cmp::Ordering::Greater, + (None, None) => std::cmp::Ordering::Equal, + } + }); +} + #[derive(Default, Clone, Debug)] pub struct ProcessInfo { pub pid: u32, @@ -122,6 +150,15 @@ pub struct ProcessInfo { /// Cumulative write bytes (KiB) from /// `/proc/[pid]/io:write_bytes`. Same caveats as `io_read_kb`. pub io_write_kb: Option, + /// 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 + /// `io_read_kb` is `None` for either prev or current. + pub io_read_rate_kbs: Option, + /// Write throughput (KiB/s) computed as the delta of + /// `io_write_kb` across two reads divided by `dt_secs`. Same + /// sentinel semantics as `io_read_rate_kbs`. + pub io_write_rate_kbs: Option, } impl ProcessInfo { @@ -140,6 +177,15 @@ impl ProcessInfo { } } + /// Total IO throughput (read + write) in KiB/s. Returns `None` + /// if either rate field is `None`. Used by `SortMode::IoRate`. + pub fn io_total_rate_kbs(&self) -> Option { + match (self.io_read_rate_kbs, self.io_write_rate_kbs) { + (Some(r), Some(w)) => Some(r + w), + _ => None, + } + } + pub fn format_memory_kb(kb: u64) -> String { const UNITS: &[&str] = &["KiB", "MiB", "GiB", "TiB"]; let mut value = kb as f64; @@ -150,6 +196,22 @@ impl ProcessInfo { } format!("{:.1} {}", value, UNITS[unit_idx]) } + + /// Format a rate (KiB/s) with human-friendly units. Uses + /// 1024-base binary units (KiB/s, MiB/s, GiB/s) for consistency + /// with `format_memory_kb`. Negative inputs are clamped to 0 + /// (saturating) — a "negative rate" is meaningless and indicates + /// a clock-reset or test fixture edge case. + pub fn format_rate_kbs(kbs: f64) -> String { + const UNITS: &[&str] = &["KiB/s", "MiB/s", "GiB/s", "TiB/s"]; + let mut value = kbs.max(0.0); + let mut unit_idx = 0; + while value >= 1024.0 && unit_idx < UNITS.len() - 1 { + value /= 1024.0; + unit_idx += 1; + } + format!("{:.1} {}", value, UNITS[unit_idx]) + } } #[derive(Default, Clone, Debug)] @@ -184,6 +246,20 @@ fn read_io_file(pid: u32) -> Option<(u64, u64)> { Some((read?, write?)) } +/// Compute KiB/s rate from a prev/current sample pair. Returns `None` +/// when either sample is `None` (process just started, /proc/[pid]/io +/// became readable/unreadable, or first sample after startup) or +/// when `dt_secs <= 0` (clock skew or test fixture). `saturating_sub` +/// handles the (impossible in practice) clock-reset case. +fn compute_rate_kbs(prev: Option, now: Option, dt_secs: f64) -> Option { + if dt_secs <= 0.0 { + return None; + } + let (p, n) = (prev?, now?); + let delta_kb = n.saturating_sub(p) as f64; + Some(delta_kb / dt_secs) +} + fn parse_stat_line(line: &str) -> Option { let open = line.find('(')?; let close = line.rfind(')')?; @@ -225,6 +301,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_read_rate_kbs: None, + io_write_rate_kbs: None, }) } @@ -294,6 +372,8 @@ impl ProcInfo { let delta = now_ticks.saturating_sub(prev_ticks) as f64; let ticks_per_sec = delta / dt_secs; p.cpu_pct = (ticks_per_sec / num_cpus as f64) * 100.0; + p.io_read_rate_kbs = compute_rate_kbs(pp.io_read_kb, p.io_read_kb, dt_secs); + p.io_write_rate_kbs = compute_rate_kbs(pp.io_write_kb, p.io_write_kb, dt_secs); } } // Re-sort because CPU% values may have changed @@ -326,6 +406,26 @@ mod tests { assert_eq!(ProcessInfo::format_memory_kb(1024 * 1024), "1.0 GiB"); } + #[test] + fn format_rate_below_1kibs() { + assert_eq!(ProcessInfo::format_rate_kbs(500.0), "500.0 KiB/s"); + } + + #[test] + fn format_rate_1mibs() { + assert_eq!(ProcessInfo::format_rate_kbs(1024.0), "1.0 MiB/s"); + } + + #[test] + fn format_rate_1gibs() { + assert_eq!(ProcessInfo::format_rate_kbs(1024.0 * 1024.0), "1.0 GiB/s"); + } + + #[test] + fn format_rate_saturates_negative_to_zero() { + assert_eq!(ProcessInfo::format_rate_kbs(-100.0), "0.0 KiB/s"); + } + #[test] fn parse_stat_line_valid() { // bash process: pid=1 (comm) S ppid pgrp session ... @@ -432,7 +532,10 @@ mod sort_unit_tests { assert_eq!(SortMode::Cpu.next(), SortMode::Io); assert_eq!(SortMode::Io.next(), SortMode::IoRead); assert_eq!(SortMode::IoRead.next(), SortMode::IoWrite); - assert_eq!(SortMode::IoWrite.next(), SortMode::Pid); + 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::Pid.next(), SortMode::Name); assert_eq!(SortMode::Name.next(), SortMode::Rss); } @@ -601,7 +704,10 @@ mod io_sort_unit_tests { assert_eq!(SortMode::Cpu.next(), SortMode::Io); assert_eq!(SortMode::Io.next(), SortMode::IoRead); assert_eq!(SortMode::IoRead.next(), SortMode::IoWrite); - assert_eq!(SortMode::IoWrite.next(), SortMode::Pid); + 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::Pid.next(), SortMode::Name); assert_eq!(SortMode::Name.next(), SortMode::Rss); } @@ -611,6 +717,9 @@ mod io_sort_unit_tests { assert_eq!(SortMode::Io.name(), "IO"); assert_eq!(SortMode::IoRead.name(), "IO-R"); assert_eq!(SortMode::IoWrite.name(), "IO-W"); + assert_eq!(SortMode::IoRate.name(), "IO/s"); + assert_eq!(SortMode::IoReadRate.name(), "R/s"); + assert_eq!(SortMode::IoWriteRate.name(), "W/s"); } #[test] @@ -669,4 +778,100 @@ mod io_sort_unit_tests { assert_eq!(ps[2].pid, 1); assert_eq!(ps[3].pid, 3); } + + #[test] + fn compute_rate_kbs_basic_delta() { + // 1024 KiB over 2.0s = 512.0 KiB/s + let r = compute_rate_kbs(Some(1000), Some(2024), 2.0); + assert_eq!(r, Some(512.0)); + } + + #[test] + fn compute_rate_kbs_returns_none_when_prev_missing() { + assert_eq!(compute_rate_kbs(None, Some(1000), 1.0), None); + } + + #[test] + fn compute_rate_kbs_returns_none_when_now_missing() { + assert_eq!(compute_rate_kbs(Some(1000), None, 1.0), None); + } + + #[test] + fn compute_rate_kbs_returns_none_when_dt_zero() { + assert_eq!(compute_rate_kbs(Some(1000), Some(2000), 0.0), None); + assert_eq!(compute_rate_kbs(Some(1000), Some(2000), -1.0), None); + } + + #[test] + fn compute_rate_kbs_saturates_on_underflow() { + // Now < Prev (clock reset) should saturate to 0, not wrap. + let r = compute_rate_kbs(Some(2000), Some(1000), 1.0); + assert_eq!(r, Some(0.0)); + } + + #[test] + fn compute_rate_kbs_first_sample_is_zero() { + // Sample N == Sample N+1 (process idle between samples). + let r = compute_rate_kbs(Some(5000), Some(5000), 1.0); + assert_eq!(r, Some(0.0)); + } + + #[test] + fn io_total_rate_kbs_sums_read_write() { + let mut p = make_proc(1, 100, 50); + p.io_read_rate_kbs = Some(200.0); + p.io_write_rate_kbs = Some(300.0); + assert_eq!(p.io_total_rate_kbs(), Some(500.0)); + } + + #[test] + fn io_total_rate_kbs_none_when_field_missing() { + let p = make_proc(1, 100, 50); + assert_eq!(p.io_total_rate_kbs(), None); + } + + #[test] + fn sort_by_io_rate_uses_total() { + let mut ps = vec![ + ProcessInfo { + pid: 1, + io_read_rate_kbs: Some(100.0), + io_write_rate_kbs: Some(900.0), + ..make_proc(1, 0, 0) + }, + ProcessInfo { + pid: 2, + io_read_rate_kbs: Some(500.0), + io_write_rate_kbs: Some(500.0), + ..make_proc(2, 0, 0) + }, + ]; + SortMode::IoRate.sort(&mut ps); + // Both have total 1000; stable sort preserves input order. + assert_eq!(ps[0].pid, 1); + assert_eq!(ps[1].pid, 2); + } + + #[test] + fn sort_by_io_read_rate_pushes_missing_to_bottom() { + let mut ps = vec![ + make_proc_none(1), + ProcessInfo { + pid: 2, + io_read_rate_kbs: Some(200.0), + ..make_proc(2, 0, 0) + }, + make_proc_none(3), + ProcessInfo { + pid: 4, + io_read_rate_kbs: Some(100.0), + ..make_proc(4, 0, 0) + }, + ]; + SortMode::IoReadRate.sort(&mut ps); + assert_eq!(ps[0].pid, 2); + assert_eq!(ps[1].pid, 4); + assert_eq!(ps[2].pid, 1); + assert_eq!(ps[3].pid, 3); + } } diff --git a/local/recipes/system/redbear-power/source/src/render.rs b/local/recipes/system/redbear-power/source/src/render.rs index fd0a2ac215..bfff596baa 100644 --- a/local/recipes/system/redbear-power/source/src/render.rs +++ b/local/recipes/system/redbear-power/source/src/render.rs @@ -877,7 +877,7 @@ pub fn render_process_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> { ).set_style(theme::LABEL_BOLD))); lines.push(Line::from("")); lines.push(Line::from(vec![ - " PID STATE PRIO NI THR CPU% IO RSS COMM".set_style(theme::LABEL), + " PID STATE PRIO NI THR CPU% IO RATE RSS COMM".set_style(theme::LABEL), ])); for p in &proc.processes { if !app.process_filter.is_empty() @@ -890,8 +890,12 @@ lines.push(Line::from(vec![ Some(kb) => crate::process::ProcessInfo::format_memory_kb(kb), None => "—".to_string(), }; + let rate_str = match p.io_total_rate_kbs() { + Some(kbs) => crate::process::ProcessInfo::format_rate_kbs(kbs), + None => "—".to_string(), + }; lines.push(Line::from(format!( - " {:<7} {} {:<4} {:<3} {:<3} {:<6} {:<11} {:<11} {}", + " {:<7} {} {:<4} {:<3} {:<3} {:<6} {:<11} {:<11} {:<11} {}", p.pid, p.state, p.priority, @@ -899,6 +903,7 @@ lines.push(Line::from(vec![ p.num_threads, format!("{:.1}", p.cpu_pct), io_str, + rate_str, crate::process::ProcessInfo::format_memory_kb(p.rss_kb), comm_truncated, ).set_style(theme::VALUE)));