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<f64>; 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<f64> 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<f64> 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
This commit is contained in:
@@ -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<f64>` — read KiB/s (delta of `io_read_kb`
|
||||
across two reads divided by `dt_secs`).
|
||||
- `io_write_rate_kbs: Option<f64>` — 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<u64>, now: Option<u64>, dt_secs: f64) -> Option<f64> {
|
||||
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<u64>` cumulative sorts.
|
||||
Uses `partial_cmp` for `Option<f64>` (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<Sample> 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.
|
||||
|
||||
@@ -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<F>(processes: &mut Vec<ProcessInfo>, field: F)
|
||||
where
|
||||
F: Fn(&ProcessInfo) -> Option<f64>,
|
||||
{
|
||||
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<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
|
||||
/// `io_read_kb` is `None` for either prev or current.
|
||||
pub io_read_rate_kbs: Option<f64>,
|
||||
/// 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<f64>,
|
||||
}
|
||||
|
||||
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<f64> {
|
||||
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<u64>, now: Option<u64>, dt_secs: f64) -> Option<f64> {
|
||||
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<ProcessInfo> {
|
||||
let open = line.find('(')?;
|
||||
let close = line.rfind(')')?;
|
||||
@@ -225,6 +301,8 @@ fn parse_stat_line(line: &str) -> Option<ProcessInfo> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
|
||||
Reference in New Issue
Block a user