diff options
Diffstat (limited to 'crates/memview/src/ui.rs')
| -rw-r--r-- | crates/memview/src/ui.rs | 151 |
1 files changed, 109 insertions, 42 deletions
diff --git a/crates/memview/src/ui.rs b/crates/memview/src/ui.rs index d170d74..1da1e58 100644 --- a/crates/memview/src/ui.rs +++ b/crates/memview/src/ui.rs @@ -1,5 +1,5 @@ use crate::app::{App, FlatProcessRow, FlatSharedRow, FlatTmpfsRow, Hotkey, RowFold}; -use crate::model::{Bytes, MeminfoEntry, ObjectUsage, Pid, Snapshot, TmpfsMount}; +use crate::model::{Bytes, Meminfo, MeminfoEntry, ObjectUsage, Pid, Snapshot, TmpfsMount}; use crate::search::SearchRole; use ratatui::Frame; use ratatui::layout::{Constraint, Direction, Layout, Rect}; @@ -190,7 +190,6 @@ fn render_loading(frame: &mut Frame<'_>, app: &App, area: Rect) { } fn render_overview(frame: &mut Frame<'_>, _app: &App, snapshot: &Snapshot, area: Rect) { - let capacity = snapshot.meminfo.get("MemTotal"); let columns = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(52), Constraint::Percentage(48)]) @@ -204,7 +203,7 @@ fn render_overview(frame: &mut Frame<'_>, _app: &App, snapshot: &Snapshot, area: .meminfo .entries .iter() - .map(|entry| row_meminfo(entry, capacity)) + .map(|entry| row_meminfo(entry, &snapshot.meminfo)) .collect::<Vec<_>>(); frame.render_widget( Table::new( @@ -222,59 +221,37 @@ fn render_overview(frame: &mut Frame<'_>, _app: &App, snapshot: &Snapshot, area: ); let overview_rows = vec![ - summary_row( - "Σ process PSS", - snapshot.overview.process_pss_total, - capacity, - ), - summary_row( - "Σ process USS", - snapshot.overview.process_uss_total, - capacity, - ), - summary_row( - "Σ process RSS", - snapshot.overview.process_rss_total, - capacity, - ), + summary_row("Σ process PSS", snapshot.overview.process_pss_total), + summary_row("Σ process USS", snapshot.overview.process_uss_total), + summary_row("Σ process RSS", snapshot.overview.process_rss_total), summary_row( "Σ process SwapPSS", snapshot.overview.process_swap_pss_total, - capacity, ), summary_row( "Σ process PSS anon", snapshot.overview.process_pss_anon_total, - capacity, ), summary_row( "Σ process PSS file", snapshot.overview.process_pss_file_total, - capacity, ), summary_row( "Σ process PSS shmem", snapshot.overview.process_pss_shmem_total, - capacity, - ), - summary_row( - "Σ tmpfs allocated", - snapshot.overview.tmpfs_allocated_total, - capacity, ), - summary_row("Σ SysV shm RSS", snapshot.overview.sysv_rss_total, capacity), + summary_row("Σ tmpfs allocated", snapshot.overview.tmpfs_allocated_total), + summary_row("Σ SysV shm RSS", snapshot.overview.sysv_rss_total), summary_text_row("processes", &snapshot.overview.process_count.to_string()), summary_text_row("SysV segments", &snapshot.sysv_segments.len().to_string()), summary_text_row("scan millis", &snapshot.elapsed.as_millis().to_string()), - summary_row( + summary_text_row( "inaccessible rollups", - Bytes(snapshot.overview.inaccessible_rollups as u64), - capacity, + &snapshot.overview.inaccessible_rollups.to_string(), ), - summary_row( + summary_text_row( "inaccessible maps", - Bytes(snapshot.overview.inaccessible_maps as u64), - capacity, + &snapshot.overview.inaccessible_maps.to_string(), ), ]; frame.render_widget( @@ -633,22 +610,23 @@ fn render_shared(frame: &mut Frame<'_>, app: &App, snapshot: &Snapshot, area: Re } } -fn row_meminfo(entry: &MeminfoEntry, total: Bytes) -> Row<'static> { +fn row_meminfo(entry: &MeminfoEntry, meminfo: &Meminfo) -> Row<'static> { + let total = meminfo.get("MemTotal"); + let color = MeminfoTone::for_key(&entry.key).color(entry.value, meminfo); Row::new(vec![ Cell::from(entry.key.clone()), - usage_cell(entry.value, total), - Cell::from(format!("{:.1}", entry.value.pct_of(total))) - .style(Style::default().fg(usage_color(entry.value, total))), + Cell::from(entry.value.human_iec()).style(Style::default().fg(color)), + Cell::from(format!("{:.1}", entry.value.pct_of(total))).style(Style::default().fg(color)), ]) - .style(usage_style(false, entry.value, total)) + .style(Style::default().fg(color)) } -fn summary_row(label: &str, value: Bytes, total: Bytes) -> Row<'static> { +fn summary_row(label: &str, value: Bytes) -> Row<'static> { Row::new(vec![ Cell::from(label.to_string()), - usage_cell(value, total), + Cell::from(value.human_iec()).style(Style::default().fg(FG)), ]) - .style(usage_style(false, value, total)) + .style(Style::default().fg(FG)) } fn summary_text_row(label: &str, value: &str) -> Row<'static> { @@ -845,6 +823,54 @@ fn usage_color(value: Bytes, total: Bytes) -> Color { blend_rgb((246, 248, 250), (232, 58, 46), (pct - 0.03) / 0.97) } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum MeminfoTone { + Neutral, + Reserve, + Pressure, +} + +impl MeminfoTone { + #[must_use] + fn for_key(key: &str) -> Self { + match key { + "MemAvailable" => Self::Reserve, + "Dirty" | "Writeback" | "WritebackTmp" | "Unevictable" | "Mlocked" | "SUnreclaim" + | "KernelStack" | "PageTables" | "SecPageTables" | "NFS_Unstable" | "Bounce" + | "HardwareCorrupted" | "SwapCached" | "Zswap" | "Zswapped" => Self::Pressure, + _ => Self::Neutral, + } + } + + #[must_use] + fn color(self, value: Bytes, meminfo: &Meminfo) -> Color { + match self { + Self::Neutral => neutral_meminfo_color(value), + Self::Reserve => reserve_color(value, meminfo.get("MemTotal")), + Self::Pressure => usage_color(value, meminfo.get("MemTotal")), + } + } +} + +fn neutral_meminfo_color(value: Bytes) -> Color { + if value.0 == 0 { MUTED } else { FG } +} + +fn reserve_color(value: Bytes, total: Bytes) -> Color { + if value.0 == 0 || total.0 == 0 { + return MUTED; + } + + let pct = (value.as_f64() / total.as_f64()).clamp(0.0, 1.0); + if pct >= 0.10 { + return FG; + } + if pct <= 0.03 { + return HOT; + } + blend_rgb((232, 58, 46), (246, 248, 250), (pct - 0.03) / 0.07) +} + fn blend_rgb(start: (u8, u8, u8), end: (u8, u8, u8), t: f64) -> Color { Color::Rgb( blend_channel(start.0, end.0, t), @@ -1074,3 +1100,44 @@ fn slice_window<T>(items: &[T], selected: usize, height: usize) -> SliceWindow<' slice: &items[start..end], } } + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::BTreeMap; + + fn meminfo(total: Bytes) -> Meminfo { + Meminfo { + entries: Vec::new(), + table: BTreeMap::from([("MemTotal".to_string(), total)]), + } + } + + #[test] + fn overview_meminfo_capacity_and_empty_like_rows_are_not_hot() { + let meminfo = meminfo(Bytes(100)); + + assert_eq!(MeminfoTone::for_key("MemTotal"), MeminfoTone::Neutral); + assert_eq!(MeminfoTone::for_key("MemFree"), MeminfoTone::Neutral); + assert_eq!(MeminfoTone::for_key("SwapFree"), MeminfoTone::Neutral); + assert_eq!(MeminfoTone::for_key("MemAvailable"), MeminfoTone::Reserve); + + assert_eq!(MeminfoTone::Neutral.color(Bytes(100), &meminfo), FG); + assert_eq!(MeminfoTone::Neutral.color(Bytes(0), &meminfo), MUTED); + assert_eq!(MeminfoTone::Reserve.color(Bytes(40), &meminfo), FG); + assert_eq!(MeminfoTone::Reserve.color(Bytes(2), &meminfo), HOT); + } + + #[test] + fn overview_meminfo_hot_rows_are_explicit_pressure_counters() { + let meminfo = meminfo(Bytes(100)); + + assert_eq!(MeminfoTone::for_key("Dirty"), MeminfoTone::Pressure); + assert_eq!(MeminfoTone::for_key("SUnreclaim"), MeminfoTone::Pressure); + assert_eq!(MeminfoTone::for_key("AnonPages"), MeminfoTone::Neutral); + assert_eq!( + MeminfoTone::Pressure.color(Bytes(0), &meminfo), + usage_color(Bytes(0), Bytes(100)) + ); + } +} |