redbear-power: v1.32 CPU% and RSS per-PID history (3 sparklines)
Generalizes the v1.31 io_history pattern to two more metrics: cpu_history and rss_history. Each Process tab row now shows three sparklines: IO-RATE (12 samples, full 78s of IO rate history) CPU% (6 samples, last 39s of CPU usage) RSS (6 samples, last 39s of memory) The two new sparklines are 6 chars wide (vs 12 for IO-RATE) to keep the panel within a 120-col terminal. Implementation: - Two new BTreeMap<u32, VecDeque<u64>> fields on App - update_io_history() now updates all three maps in a single 3-pass sweep (reap, append, normalize) for all metrics - Extracted private helpers push_sample() and normalize_history() for the per-metric work; both operate on the map type generically - New render::sparkline_short() helper: renders the last 'width' samples of a history, padding with leading spaces for short histories Test count 121 -> 123 (+2): - update_io_history_populates_cpu_and_rss_for_each_pid (every PID gets cpu/rss entries, not just PIDs with non-zero values) - update_io_history_reaps_all_three_maps (phantom-PID reap spans all three maps) Redox stripped binary: 4,213,608 bytes (+12 KiB from v1.31). Compile warnings: 55 (unchanged).
This commit is contained in:
@@ -151,6 +151,12 @@ pub meminfo: crate::meminfo::MemInfo,
|
||||
/// PIDs that exit have their entries reaped on the next
|
||||
/// refresh. `BTreeMap` for stable iteration order.
|
||||
pub io_history: std::collections::BTreeMap<u32, std::collections::VecDeque<u64>>,
|
||||
/// Per-PID CPU% history (raw f64 bits, 0..=200+ — values above
|
||||
/// 100% are valid for multi-threaded procs aggregated as a
|
||||
/// single ratio). Same shape as `io_history`.
|
||||
pub cpu_history: std::collections::BTreeMap<u32, std::collections::VecDeque<u64>>,
|
||||
/// Per-PID RSS history (raw f64 bits, KiB). Same shape.
|
||||
pub rss_history: std::collections::BTreeMap<u32, std::collections::VecDeque<u64>>,
|
||||
/// Cursor index into the visible (post-filter) process list.
|
||||
/// Distinct from `table_state` which tracks the Per-CPU tab.
|
||||
pub process_cursor: usize,
|
||||
@@ -323,6 +329,8 @@ impl App {
|
||||
process_tree: false,
|
||||
folded: std::collections::BTreeSet::new(),
|
||||
io_history: std::collections::BTreeMap::new(),
|
||||
cpu_history: std::collections::BTreeMap::new(),
|
||||
rss_history: std::collections::BTreeMap::new(),
|
||||
process_cursor: 0,
|
||||
pid_detail: None,
|
||||
refresh_counter: 0,
|
||||
@@ -744,45 +752,65 @@ impl App {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Update the per-PID IO rate history from the current
|
||||
/// `self.processes`. Each PID's history is a `VecDeque<u64>`
|
||||
/// of raw f64-bit rate samples, normalized to u8 0..=255
|
||||
/// against the history's max before being passed to the
|
||||
/// renderer. PIDs that exited since the last refresh are
|
||||
/// reaped.
|
||||
/// Update all per-PID history maps (IO rate, CPU%, RSS) from
|
||||
/// the current `self.processes`. Each map is a
|
||||
/// `VecDeque<u64>` of raw f64-bit samples, normalized to u8
|
||||
/// 0..=255 against the per-history max before being passed to
|
||||
/// the renderer. PIDs that exited since the last refresh are
|
||||
/// reaped from all three maps.
|
||||
pub fn update_io_history(&mut self) {
|
||||
// 1. Reap: drop history for PIDs no longer present.
|
||||
// Compute the set of current PIDs once; reuse for all three
|
||||
// reaps.
|
||||
let current_pids: std::collections::BTreeSet<u32> = self
|
||||
.processes
|
||||
.processes
|
||||
.iter()
|
||||
.map(|p| p.pid)
|
||||
.collect();
|
||||
self.io_history
|
||||
.retain(|pid, _| current_pids.contains(pid));
|
||||
|
||||
// 2. Append a new raw f64-bit sample for each PID with a
|
||||
// known rate. Storing the raw bits (not a pre-clamped
|
||||
// u8) preserves precision for the normalization step.
|
||||
for p in &self.processes.processes {
|
||||
let rate_kbs = match p.io_total_rate_kbs() {
|
||||
Some(r) => r.max(0.0),
|
||||
None => continue, // skip: no data this tick
|
||||
};
|
||||
let history = self
|
||||
.io_history
|
||||
.entry(p.pid)
|
||||
.or_insert_with(|| std::collections::VecDeque::with_capacity(PROCESS_IO_HISTORY_LEN));
|
||||
if history.len() >= PROCESS_IO_HISTORY_LEN {
|
||||
history.pop_front();
|
||||
}
|
||||
history.push_back(rate_kbs.to_bits());
|
||||
for map in [&mut self.io_history, &mut self.cpu_history, &mut self.rss_history] {
|
||||
map.retain(|pid, _| current_pids.contains(pid));
|
||||
}
|
||||
|
||||
// 3. Normalize each history in place to u8 0..=255 against
|
||||
// its max. A separate pass lets the max be computed
|
||||
// once per history rather than per sample.
|
||||
for history in self.io_history.values_mut() {
|
||||
// Append one sample per PID per metric.
|
||||
for p in &self.processes.processes {
|
||||
if let Some(rate) = p.io_total_rate_kbs() {
|
||||
Self::push_sample(&mut self.io_history, p.pid, rate);
|
||||
}
|
||||
Self::push_sample(&mut self.cpu_history, p.pid, p.cpu_pct);
|
||||
Self::push_sample(&mut self.rss_history, p.pid, p.rss_kb as f64);
|
||||
}
|
||||
|
||||
// Normalize each map in place against its per-history max.
|
||||
for map in [&mut self.io_history, &mut self.cpu_history, &mut self.rss_history] {
|
||||
Self::normalize_history(map);
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a new f64 sample into the per-PID history map for
|
||||
/// `pid`. The capacity is bounded at `PROCESS_IO_HISTORY_LEN`.
|
||||
fn push_sample(
|
||||
map: &mut std::collections::BTreeMap<u32, std::collections::VecDeque<u64>>,
|
||||
pid: u32,
|
||||
value: f64,
|
||||
) {
|
||||
let history = map
|
||||
.entry(pid)
|
||||
.or_insert_with(|| std::collections::VecDeque::with_capacity(PROCESS_IO_HISTORY_LEN));
|
||||
if history.len() >= PROCESS_IO_HISTORY_LEN {
|
||||
history.pop_front();
|
||||
}
|
||||
history.push_back(value.to_bits());
|
||||
}
|
||||
|
||||
/// Normalize a per-PID history map in place: each value
|
||||
/// becomes (raw / max) * 255, rounded to u8. A separate pass
|
||||
/// for each history means the max is computed once per
|
||||
/// history, not per sample. All-zero histories are left
|
||||
/// as-is (rendered as empty bars by the sparkline helper).
|
||||
fn normalize_history(
|
||||
map: &mut std::collections::BTreeMap<u32, std::collections::VecDeque<u64>>,
|
||||
) {
|
||||
for history in map.values_mut() {
|
||||
let max_bits = history.iter().copied().max().unwrap_or(0);
|
||||
if max_bits == 0 {
|
||||
continue;
|
||||
@@ -979,4 +1007,35 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_io_history_populates_cpu_and_rss_for_each_pid() {
|
||||
// CPU% is on every ProcessInfo; should populate for all
|
||||
// PIDs. RSS is also on every ProcessInfo.
|
||||
let mut app = make_app_with_processes(3);
|
||||
// make_app_with_processes leaves cpu_pct=0 and rss_kb=0
|
||||
// (the Default). After update, all 3 PIDs should appear
|
||||
// in both cpu_history and rss_history (even if values
|
||||
// are zero, an entry is created).
|
||||
app.update_io_history();
|
||||
for pid in [100u32, 101, 102] {
|
||||
assert!(app.cpu_history.contains_key(&pid),
|
||||
"cpu_history missing pid {pid}");
|
||||
assert!(app.rss_history.contains_key(&pid),
|
||||
"rss_history missing pid {pid}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_io_history_reaps_all_three_maps() {
|
||||
let mut app = make_app_with_processes(2);
|
||||
// Seed a phantom PID across all three maps.
|
||||
for map in [&mut app.io_history, &mut app.cpu_history, &mut app.rss_history] {
|
||||
map.insert(9999, std::collections::VecDeque::from(vec![1.0f64.to_bits()]));
|
||||
}
|
||||
app.update_io_history();
|
||||
assert!(!app.io_history.contains_key(&9999));
|
||||
assert!(!app.cpu_history.contains_key(&9999));
|
||||
assert!(!app.rss_history.contains_key(&9999));
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,34 @@ pub fn io_rate_sparkline(values: &[u8]) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
/// Render a short sparkline (CPU% or RSS history) at a fixed
|
||||
/// width. Takes the LAST `width` samples of the history; if the
|
||||
/// history is shorter, pads with leading spaces. Returns `width`
|
||||
/// chars total. Used for the compact 6-char CPU% and RSS
|
||||
/// sparklines next to the 12-char IO-RATE sparkline.
|
||||
fn sparkline_short(
|
||||
history: &std::collections::BTreeMap<u32, std::collections::VecDeque<u64>>,
|
||||
pid: &u32,
|
||||
width: usize,
|
||||
) -> String {
|
||||
history
|
||||
.get(pid)
|
||||
.map(|hist| {
|
||||
let bytes: Vec<u8> = hist
|
||||
.iter()
|
||||
.map(|bits| f64::from_bits(*bits).max(0.0).min(255.0) as u8)
|
||||
.collect();
|
||||
let start = bytes.len().saturating_sub(width);
|
||||
let slice = &bytes[start..];
|
||||
let mut out = io_rate_sparkline(slice);
|
||||
while out.chars().count() < width {
|
||||
out.insert(0, ' ');
|
||||
}
|
||||
out
|
||||
})
|
||||
.unwrap_or_else(|| " ".repeat(width))
|
||||
}
|
||||
|
||||
/// Build a fixed-width horizontal bar that visualizes a 0..=100
|
||||
/// value. The bar grows from the left, with the rightmost cells
|
||||
/// remaining as light filler so the user can read the proportion
|
||||
@@ -901,7 +929,7 @@ pub fn render_process_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
|
||||
"RSS"
|
||||
};
|
||||
let header_str = format!(
|
||||
" PID STATE PRIO NI THR CPU% IO RATE {:<11} IO-RATE COMM",
|
||||
" PID STATE PRIO NI THR CPU% IO RATE {:<11} IO-RATE CPU% RSS COMM",
|
||||
mem_header
|
||||
);
|
||||
lines.push(Line::from(vec![
|
||||
@@ -933,11 +961,11 @@ pub fn render_process_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
|
||||
} else {
|
||||
crate::process::ProcessInfo::format_memory_kb(p.rss_kb)
|
||||
};
|
||||
// Per-PID IO rate sparkline (12 samples, normalized to
|
||||
// 0..=255 against the history's max). Empty for PIDs
|
||||
// with no history yet. The history stores raw f64 bits
|
||||
// as u64; we re-interpret and convert to u8 here.
|
||||
let sparkline = app
|
||||
// Three per-PID sparklines: IO-RATE (12 samples),
|
||||
// CPU% (6 samples), RSS (6 samples). The smaller
|
||||
// CPU/RSS sparklines are 6 chars wide to keep the
|
||||
// panel within a 120-col terminal.
|
||||
let io_spark = app
|
||||
.io_history
|
||||
.get(&p.pid)
|
||||
.map(|hist| {
|
||||
@@ -948,6 +976,8 @@ pub fn render_process_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
|
||||
io_rate_sparkline(&bytes)
|
||||
})
|
||||
.unwrap_or_else(|| " ".repeat(crate::app::PROCESS_IO_HISTORY_LEN));
|
||||
let cpu_spark = sparkline_short(&app.cpu_history, &p.pid, 6);
|
||||
let rss_spark = sparkline_short(&app.rss_history, &p.pid, 6);
|
||||
let prefix = if app.process_tree {
|
||||
tree_prefix(p.pid, p.ppid, &proc.processes, &app.folded)
|
||||
} else {
|
||||
@@ -959,7 +989,7 @@ pub fn render_process_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
|
||||
theme::VALUE
|
||||
};
|
||||
lines.push(Line::from(format!(
|
||||
" {}{:<7} {} {:<4} {:<3} {:<3} {:<6} {:<11} {:<11} {:<11} {:<12} {}",
|
||||
" {}{:<7} {} {:<4} {:<3} {:<3} {:<6} {:<11} {:<11} {:<11} {:<12} {:<6} {:<6} {}",
|
||||
prefix,
|
||||
p.pid,
|
||||
p.state,
|
||||
@@ -970,7 +1000,9 @@ pub fn render_process_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
|
||||
io_str,
|
||||
rate_str,
|
||||
mem_str,
|
||||
sparkline,
|
||||
io_spark,
|
||||
cpu_spark,
|
||||
rss_spark,
|
||||
comm_truncated,
|
||||
).set_style(row_style)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user