use ratatui::{
layout::{Constraint, Direction, Layout, Margin, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Cell, Clear, Paragraph, Row, ScrollbarState, Table, TableState},
Frame,
};
use super::app::{all_columns, App, CounterRate, DeviceClass, PortThroughput, TableColumn};
use super::theme::ThemeColors;
use crate::gpu::GpuMetrics;
use crate::stat::EXTRA_COUNTERS;
const HELP_KEYS: &[(&str, &str)] = &[
("↑ / k", "Move up"),
("↓ / j", "Move down"),
("← / →", "Scroll columns"),
("Enter", "Toggle detail panel"),
("Esc", "Close detail / quit"),
("t", "Cycle theme"),
("Tab / S-Tab", "Switch device class (RDMA/XGMI/NVLink)"),
("a", "Toggle rolling average"),
("+ / =", "Increase avg window (+1s)"),
("-", "Decrease avg window (-1s)"),
("w", "Set avg window (custom)"),
("< / >", "Refresh faster / slower (±0.5s)"),
("c", "Configure columns"),
("r", "Record Perfetto trace (start/stop)"),
("h", "Toggle this help"),
("q", "Quit"),
];
const RDMA_LINK_GBPS: f64 = 100.0;
pub fn draw(frame: &mut Frame, app: &mut App) {
let tc = app.theme.colors();
if tc.bg != ratatui::style::Color::Reset {
frame.render_widget(
Block::default().style(Style::default().bg(tc.bg)),
frame.area(),
);
}
if app.seen_tabs.len() >= 2 {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(5),
Constraint::Length(1),
Constraint::Min(5),
Constraint::Length(1),
])
.split(frame.area());
draw_header(frame, app, chunks[0], &tc);
draw_tab_bar(frame, app, chunks[1], &tc);
draw_body(frame, app, chunks[2], &tc);
draw_status_bar(frame, app, chunks[3], &tc);
} else {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(5),
Constraint::Min(5),
Constraint::Length(1),
])
.split(frame.area());
draw_header(frame, app, chunks[0], &tc);
draw_body(frame, app, chunks[1], &tc);
draw_status_bar(frame, app, chunks[2], &tc);
}
if app.show_help {
draw_help_popup(frame, &tc);
}
if app.show_window_input {
draw_window_input_popup(frame, app, &tc);
}
if app.show_column_picker {
draw_column_picker(frame, app, &tc);
}
}
fn draw_body(frame: &mut Frame, app: &mut App, area: Rect, tc: &ThemeColors) {
if app.show_detail {
let split = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(area);
draw_table(frame, app, split[0], tc);
draw_detail(frame, app, split[1], tc);
} else {
draw_table(frame, app, area, tc);
}
}
fn header_line1(app: &App, tc: &ThemeColors) -> Line<'static> {
Line::from(vec![
styled(" rdmatop ", tc.accent, true),
styled(
&format!(
"- {} │ {} │ load average: {}",
app.sysinfo.hostname, app.sysinfo.uptime, app.sysinfo.load_avg
),
tc.muted,
false,
),
])
}
fn header_line2(app: &App, tc: &ThemeColors) -> Line<'static> {
let display = app.display_throughputs();
let n = display.len();
let total_tx: f64 = display.iter().map(|t| t.tx_gbps).sum();
let total_rx: f64 = display.iter().map(|t| t.rx_gbps).sum();
let total_drops: f64 = display.iter().map(|t| t.rx_drops_per_sec).sum();
let drop_color = if total_drops > 0.0 {
tc.error
} else {
tc.muted
};
let avg_label = if app.show_rolling_avg {
format!(
" │ avg:{}s({}/{})",
app.rolling_avg.window_secs,
app.rolling_avg.sample_count(),
app.rolling_avg.window_secs,
)
} else {
String::new()
};
let status = format!(
" │ refresh: {:.1}s │ theme: {}",
app.refresh_interval.as_secs_f64(),
app.theme.label()
);
let label = app.active_tab.label();
Line::from(vec![
styled(
&format!(" {}: {} device{}", label, n, if n == 1 { "" } else { "s" }),
tc.fg,
false,
),
styled(" │ TX: ", tc.muted, false),
styled(&format!("{:.2} Gbps", total_tx), tc.good, false),
styled(" │ RX: ", tc.muted, false),
styled(&format!("{:.2} Gbps", total_rx), tc.good, false),
styled(" │ Drops: ", tc.muted, false),
styled(&format!("{:.0}/s", total_drops), drop_color, false),
styled(&status, tc.muted, false),
styled(&avg_label, tc.accent, false),
])
}
fn cpu_bar(pct: f32, width: usize, tc: &ThemeColors) -> Vec<Span<'static>> {
let filled = ((pct / 100.0) * width as f32).round() as usize;
let empty = width.saturating_sub(filled);
let color = if pct > 80.0 {
tc.error
} else if pct > 50.0 {
tc.warning
} else {
tc.good
};
vec![
styled("[", tc.muted, false),
styled(&"|".repeat(filled), color, false),
styled(&" ".repeat(empty), tc.muted, false),
styled(&format!("{:>5.1}%]", pct), color, false),
]
}
fn mem_bar(used: u64, total: u64, pct: f32, width: usize, tc: &ThemeColors) -> Vec<Span<'static>> {
let filled = ((pct / 100.0) * width as f32).round() as usize;
let empty = width.saturating_sub(filled);
let color = if pct > 80.0 {
tc.error
} else if pct > 50.0 {
tc.warning
} else {
tc.good
};
let label = if total >= 1024 {
format!("{:.1}/{:.1}G]", used as f64 / 1024.0, total as f64 / 1024.0)
} else {
format!("{}/{}M]", used, total)
};
vec![
styled("[", tc.muted, false),
styled(&"|".repeat(filled), color, false),
styled(&" ".repeat(empty), tc.muted, false),
styled(&label, color, false),
]
}
fn header_line3(app: &App, tc: &ThemeColors) -> Line<'static> {
let s = &app.sysinfo;
let mut spans = vec![styled(" CPU ", tc.muted, false)];
spans.extend(cpu_bar(s.cpu_pct, 20, tc));
spans.push(styled(" Mem ", tc.muted, false));
spans.extend(mem_bar(s.mem_used_mb, s.mem_total_mb, s.mem_pct, 20, tc));
spans.push(styled(" Net ", tc.muted, false));
spans.push(styled(
&format!(
"↓{}/s ↑{}/s",
fmt_bytes_short(s.net.rx_bytes_per_sec),
fmt_bytes_short(s.net.tx_bytes_per_sec),
),
tc.fg,
false,
));
Line::from(spans)
}
fn draw_header(frame: &mut Frame, app: &App, area: Rect, tc: &ThemeColors) {
let block = Block::default()
.borders(Borders::ALL)
.border_set(super::glyphs::border())
.border_style(Style::default().fg(tc.border));
let lines = vec![
header_line1(app, tc),
header_line2(app, tc),
header_line3(app, tc),
];
frame.render_widget(Paragraph::new(lines).block(block), area);
}
fn tab_bar_line(app: &App, tc: &ThemeColors) -> Line<'static> {
let mut spans: Vec<Span<'static>> = vec![styled(" ", tc.muted, false)];
for (i, tab) in app.seen_tabs.iter().enumerate() {
if i > 0 {
spans.push(styled(" │ ", tc.muted, false));
}
let label = tab.label();
if *tab == app.active_tab {
spans.push(Span::styled(
format!(" {} ", label),
Style::default()
.fg(Color::Black)
.bg(tc.accent)
.add_modifier(Modifier::BOLD),
));
} else {
spans.push(styled(&format!(" {} ", label), tc.muted, false));
}
}
Line::from(spans)
}
fn draw_tab_bar(frame: &mut Frame, app: &App, area: Rect, tc: &ThemeColors) {
frame.render_widget(Paragraph::new(tab_bar_line(app, tc)), area);
}
fn gbps_bar(gbps: f64, link_gbps: Option<f64>, width: usize) -> String {
let max = link_gbps.filter(|&r| r > 0.0).unwrap_or(RDMA_LINK_GBPS);
let ratio = (gbps / max).clamp(0.0, 1.0);
ratio_bar(ratio, width)
}
fn ratio_bar(ratio: f64, width: usize) -> String {
let filled = (ratio.clamp(0.0, 1.0) * width as f64).round() as usize;
let (fill, empty) = super::glyphs::meter();
let mut bar = String::with_capacity(width * fill.len_utf8());
bar.extend(std::iter::repeat_n(fill, filled));
bar.extend(std::iter::repeat_n(empty, width - filled));
bar
}
fn pct_gauge_text(pct: Option<u32>, bar_w: usize) -> String {
match pct {
Some(p) => format!("{} {:>3}%", ratio_bar(p as f64 / 100.0, bar_w), p),
None => "-".to_string(),
}
}
fn scaled_col_widths(natural: &[u16], mins: &[u16], avail: u16) -> Vec<u16> {
debug_assert_eq!(natural.len(), mins.len());
if natural.is_empty() {
return Vec::new();
}
let gaps = natural.len().saturating_sub(1) as u16;
let total: u16 = natural.iter().sum::<u16>() + gaps;
if avail >= total {
return natural.to_vec();
}
let mut out: Vec<u16> = natural
.iter()
.zip(mins)
.map(|(&nat, &min)| {
let scaled = (nat as u32 * avail as u32 + total as u32 / 2) / total as u32;
(scaled as u16).max(min)
})
.collect();
let mut sum: u16 = out.iter().sum();
while sum + gaps > avail {
let Some(i) = (0..out.len())
.filter(|&i| out[i] > mins[i])
.max_by_key(|&i| out[i] - mins[i])
else {
break; };
out[i] -= 1;
sum -= 1;
}
out
}
fn gpu_col_widths(avail: u16) -> [u16; 12] {
const BAR: u16 = super::app::BAR_WIDTH as u16;
const NATURAL: [u16; 12] = [9, 16, 16, 16, 5, 6, BAR, 7, BAR, 7, 6, 5];
const MIN: [u16; 12] = [7, 8, 9, 9, 4, 4, 6, 6, 6, 6, 5, 4];
scaled_col_widths(&NATURAL, &MIN, avail)
.try_into()
.expect("12 naturals in, 12 widths out")
}
fn rdma_col_min(col: &TableColumn) -> u16 {
match col {
TableColumn::TxBar | TableColumn::RxBar => 6,
_ => {
let n = col.width();
(n * 3 / 5).max(4).min(n)
}
}
}
fn column_cell(
col: &TableColumn,
t: &PortThroughput,
tc: &ThemeColors,
col_w: u16,
) -> Cell<'static> {
match col {
TableColumn::Device => Cell::from(t.dev_name.clone()).style(Style::default().fg(tc.fg)),
TableColumn::Port => {
let port_text = t.port_label.clone().unwrap_or_else(|| t.port.to_string());
Cell::from(port_text).style(Style::default().fg(tc.muted))
}
TableColumn::State => Cell::from(state_label(t.state.as_deref()))
.style(Style::default().fg(state_color(t.state.as_deref(), tc))),
TableColumn::TxBar => Cell::from(gbps_bar(t.tx_gbps, t.link_gbps, col_w as usize))
.style(Style::default().fg(throughput_color(t.tx_gbps, t.link_gbps, tc))),
TableColumn::TxGbps => Cell::from(format!("{:.2}", t.tx_gbps))
.style(Style::default().fg(throughput_color(t.tx_gbps, t.link_gbps, tc))),
TableColumn::RxBar => Cell::from(gbps_bar(t.rx_gbps, t.link_gbps, col_w as usize))
.style(Style::default().fg(throughput_color(t.rx_gbps, t.link_gbps, tc))),
TableColumn::RxGbps => Cell::from(format!("{:.2}", t.rx_gbps))
.style(Style::default().fg(throughput_color(t.rx_gbps, t.link_gbps, tc))),
TableColumn::TxPps => {
Cell::from(format_pps(t.tx_pkts_per_sec)).style(Style::default().fg(tc.fg))
}
TableColumn::RxPps => {
Cell::from(format_pps(t.rx_pkts_per_sec)).style(Style::default().fg(tc.fg))
}
TableColumn::Drops => {
let c = if t.rx_drops_per_sec > 0.0 {
tc.error
} else {
tc.muted
};
Cell::from(format!("{:.0}", t.rx_drops_per_sec)).style(Style::default().fg(c))
}
TableColumn::Counter(name) => {
let rate = t
.counter_rates
.iter()
.find(|r| &r.name == name)
.map(|r| r.rate)
.unwrap_or(0.0);
let is_bytes = t
.counter_rates
.iter()
.find(|r| &r.name == name)
.map(|r| r.is_bytes)
.unwrap_or(false);
let text = if is_bytes {
format_bytes(rate)
} else {
format_rate(rate)
};
let color = if rate > 0.0 { tc.fg } else { tc.muted };
Cell::from(text).style(Style::default().fg(color))
}
}
}
fn gpu_meta_name(t: &PortThroughput) -> String {
if let Some(ref m) = t.xgmi {
return m.gpu_name.clone();
}
if let Some(ref m) = t.nvlink {
return m.gpu_name.clone();
}
String::new()
}
fn gpu_errs(t: &PortThroughput) -> String {
let counter = |name: &str| {
t.counter_rates
.iter()
.find(|c| c.name == name)
.map(|c| c.value)
.unwrap_or(0)
};
if t.xgmi.is_some() {
return format!("{}/{}", counter("xgmi_wafl_ce"), counter("xgmi_wafl_ue"));
}
let sum: u64 = t
.counter_rates
.iter()
.filter(|c| c.name.starts_with("nvlink_"))
.map(|c| c.value)
.sum();
sum.to_string()
}
fn gpu_metrics(t: &PortThroughput) -> Option<&GpuMetrics> {
let nv = t.nvlink.as_ref().and_then(|m| m.metrics.as_ref());
nv.or_else(|| t.xgmi.as_ref().and_then(|m| m.metrics.as_ref()))
}
fn gpu_row_cells(t: &PortThroughput, tc: &ThemeColors, widths: &[u16; 12]) -> Vec<Cell<'static>> {
let m = gpu_metrics(t);
let util = m.and_then(|m| m.util_pct);
let mem = m.and_then(|m| match (m.vram_used_mb, m.vram_total_mb) {
(Some(used), Some(total)) if total > 0 => {
Some((used as f64 / total as f64 * 100.0).round() as u32)
}
_ => None,
});
let pct_cell = |p: Option<u32>, col_w: u16| {
let color = match p {
Some(v) => capacity_color(v as f64 / 100.0, tc),
None => tc.muted,
};
let bar_w = (col_w as usize).saturating_sub(5).max(3);
Cell::from(pct_gauge_text(p, bar_w)).style(Style::default().fg(color))
};
let temp_cell = match m.and_then(|m| m.temp_c) {
Some(t) => Cell::from(format!("{}C", t)).style(Style::default().fg(temp_color(t, tc))),
None => Cell::from("-").style(Style::default().fg(tc.muted)),
};
let pwr_cell = match m.and_then(|m| m.power_w) {
Some(p) => Cell::from(format!("{:.0}W", p)).style(Style::default().fg(tc.fg)),
None => Cell::from("-").style(Style::default().fg(tc.muted)),
};
vec![
Cell::from(t.dev_name.clone()).style(Style::default().fg(tc.fg)),
Cell::from(gpu_meta_name(t)).style(Style::default().fg(tc.muted)),
pct_cell(util, widths[2]),
pct_cell(mem, widths[3]),
temp_cell,
pwr_cell,
Cell::from(gbps_bar(t.tx_gbps, t.link_gbps, widths[6] as usize))
.style(Style::default().fg(throughput_color(t.tx_gbps, t.link_gbps, tc))),
Cell::from(format!("{:.2}", t.tx_gbps)).style(Style::default().fg(throughput_color(
t.tx_gbps,
t.link_gbps,
tc,
))),
Cell::from(gbps_bar(t.rx_gbps, t.link_gbps, widths[8] as usize))
.style(Style::default().fg(throughput_color(t.rx_gbps, t.link_gbps, tc))),
Cell::from(format!("{:.2}", t.rx_gbps)).style(Style::default().fg(throughput_color(
t.rx_gbps,
t.link_gbps,
tc,
))),
Cell::from(t.port_label.clone().unwrap_or_default()).style(Style::default().fg(tc.fg)),
Cell::from(gpu_errs(t)).style(Style::default().fg(tc.warning)),
]
}
fn draw_startup_placeholder(
frame: &mut Frame,
app: &App,
area: Rect,
tc: &ThemeColors,
title: String,
) {
let text = if app.sampler_error.is_some() {
"sampling failed - see status bar"
} else {
"initializing devices..."
};
let block = Block::default()
.borders(Borders::ALL)
.border_set(super::glyphs::border())
.border_style(Style::default().fg(tc.border))
.title(title)
.title_style(Style::default().fg(tc.accent));
let msg = Paragraph::new(text)
.style(Style::default().fg(tc.muted))
.block(block);
frame.render_widget(msg, area);
}
fn draw_gpu_table(frame: &mut Frame, app: &mut App, area: Rect, tc: &ThemeColors) {
let display = app.display_throughputs().to_vec();
let label = app.active_tab.label();
let title = if app.show_rolling_avg {
format!(
" {} Throughput (avg {}s) ",
label, app.rolling_avg.window_secs
)
} else {
format!(" {} Throughput ", label)
};
if display.is_empty() && !app.has_data {
draw_startup_placeholder(frame, app, area, tc, title);
return;
}
let header = Row::new(vec![
"Device", "Name", "Util", "Mem", "Temp", "Pwr", "TX", "Gbps", "RX", "Gbps", "Links", "Errs",
])
.style(
Style::default()
.fg(tc.header_fg)
.add_modifier(Modifier::BOLD),
)
.height(1);
let avail = area.width.saturating_sub(2 + 2);
let widths = gpu_col_widths(avail);
let rows: Vec<Row> = display
.iter()
.map(|t| Row::new(gpu_row_cells(t, tc, &widths)))
.collect();
let table = Table::new(rows, widths.map(Constraint::Length))
.header(header)
.block(
Block::default()
.borders(Borders::ALL)
.border_set(super::glyphs::border())
.border_style(Style::default().fg(tc.border))
.title(title)
.title_style(Style::default().fg(tc.accent)),
)
.row_highlight_style(
Style::default()
.bg(tc.highlight_bg)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(super::glyphs::cursor());
let mut state = TableState::default();
if !display.is_empty() {
let viewport = area.height.saturating_sub(4) as usize;
if viewport > 0 {
if app.selected_row >= app.table_offset + viewport {
app.table_offset = app.selected_row + 1 - viewport;
} else if app.selected_row < app.table_offset {
app.table_offset = app.selected_row;
}
}
state.select(Some(app.selected_row));
*state.offset_mut() = app.table_offset;
}
frame.render_stateful_widget(table, area, &mut state);
if display.len() > area.height.saturating_sub(4) as usize {
let mut v_scroll = ScrollbarState::new(display.len()).position(app.selected_row);
frame.render_stateful_widget(
super::glyphs::v_scrollbar()
.thumb_style(Style::default().fg(tc.accent))
.track_style(Style::default().fg(tc.border)),
area.inner(Margin {
vertical: 1,
horizontal: 0,
}),
&mut v_scroll,
);
}
}
fn draw_table(frame: &mut Frame, app: &mut App, area: Rect, tc: &ThemeColors) {
if app.active_tab != DeviceClass::Rdma {
draw_gpu_table(frame, app, area, tc);
return;
}
let display = app.display_throughputs().to_vec();
let title = if app.show_rolling_avg {
format!(" RDMA Throughput (avg {}s) ", app.rolling_avg.window_secs)
} else {
" RDMA Throughput ".to_string()
};
if display.is_empty() && !app.has_data {
draw_startup_placeholder(frame, app, area, tc, title);
return;
}
let default_cols = super::app::default_columns();
let (cols_to_render, show_scrollbars) = if app.show_detail {
(default_cols.iter().collect::<Vec<_>>(), false)
} else {
let all_cols = &app.columns;
let avail = area.width.saturating_sub(4) as usize;
let visible: Vec<&super::app::TableColumn> = all_cols
.iter()
.skip(app.h_scroll)
.scan(0usize, |used, col| {
let sep = if *used > 0 { 1 } else { 0 };
let w = col.width() as usize + sep;
if *used + w <= avail {
*used += w;
Some(col)
} else {
None
}
})
.collect();
let mut max_offset = 0;
for start in 0..all_cols.len() {
let total_w: usize = all_cols[start..]
.iter()
.map(|c| c.width() as usize + 1)
.sum();
if total_w > avail {
max_offset = start + 1;
} else {
break;
}
}
app.h_scroll_max = max_offset;
if app.h_scroll > app.h_scroll_max {
app.h_scroll = app.h_scroll_max;
}
(visible, true)
};
let header = Row::new(
cols_to_render
.iter()
.map(|c| c.header_label())
.collect::<Vec<_>>(),
)
.style(
Style::default()
.fg(tc.header_fg)
.add_modifier(Modifier::BOLD),
)
.height(1);
let natural: Vec<u16> = cols_to_render.iter().map(|c| c.width()).collect();
let mins: Vec<u16> = cols_to_render.iter().map(|c| rdma_col_min(c)).collect();
let col_widths = scaled_col_widths(&natural, &mins, area.width.saturating_sub(2 + 2));
let rows: Vec<Row> = display
.iter()
.map(|t| {
Row::new(
cols_to_render
.iter()
.zip(&col_widths)
.map(|(c, &w)| column_cell(c, t, tc, w))
.collect::<Vec<_>>(),
)
})
.collect();
let widths: Vec<Constraint> = col_widths.iter().map(|&w| Constraint::Length(w)).collect();
let table = Table::new(rows, widths)
.header(header)
.block(
Block::default()
.borders(Borders::ALL)
.border_set(super::glyphs::border())
.border_style(Style::default().fg(tc.border))
.title(title)
.title_style(Style::default().fg(tc.accent)),
)
.row_highlight_style(
Style::default()
.bg(tc.highlight_bg)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(super::glyphs::cursor());
let mut state = TableState::default();
if !display.is_empty() {
let viewport = area.height.saturating_sub(4) as usize; if viewport > 0 {
if app.selected_row >= app.table_offset + viewport {
app.table_offset = app.selected_row + 1 - viewport;
} else if app.selected_row < app.table_offset {
app.table_offset = app.selected_row;
}
}
state.select(Some(app.selected_row));
*state.offset_mut() = app.table_offset;
}
frame.render_stateful_widget(table, area, &mut state);
if !show_scrollbars {
return;
}
if display.len() > area.height.saturating_sub(4) as usize {
let mut v_scroll = ScrollbarState::new(display.len()).position(app.selected_row);
frame.render_stateful_widget(
super::glyphs::v_scrollbar()
.thumb_style(Style::default().fg(tc.accent))
.track_style(Style::default().fg(tc.border)),
area.inner(Margin {
vertical: 1,
horizontal: 0,
}),
&mut v_scroll,
);
}
let all_cols = &app.columns;
if all_cols.len() > cols_to_render.len() || app.h_scroll > 0 {
let mut h_scroll = ScrollbarState::new(all_cols.len()).position(app.h_scroll);
frame.render_stateful_widget(
super::glyphs::h_scrollbar()
.thumb_style(Style::default().fg(tc.accent))
.track_style(Style::default().fg(tc.border)),
area.inner(Margin {
vertical: 0,
horizontal: 1,
}),
&mut h_scroll,
);
}
}
fn sparkline_str(data: &[f64], width: usize) -> String {
let max = data.iter().copied().fold(0.0f64, f64::max);
sparkline_scaled(data, width, max)
}
fn sparkline_scaled(data: &[f64], width: usize, max: f64) -> String {
const U: &[char] = &[' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
const A: &[char] = &[' ', '.', '.', ':', ':', '=', '=', '#', '#'];
let bars = if super::glyphs::unicode() { U } else { A };
let max = max.max(0.01);
let start = data.len().saturating_sub(width);
let mut s = String::with_capacity(width);
for &v in &data[start..] {
let idx = ((v / max) * 8.0).round() as usize;
s.push(bars[idx.min(8)]);
}
while s.chars().count() < width {
s.insert(0, ' ');
}
s
}
fn append_gpu_health(
lines: &mut Vec<Line<'static>>,
metrics: Option<&crate::gpu::GpuMetrics>,
history: Option<&super::app::DeviceHistory>,
spark_w: usize,
tc: &ThemeColors,
) {
let Some(m) = metrics else { return };
if let Some(h) = history {
if !h.util.is_empty() {
let pct_span = match m.util_pct {
Some(u) => styled(
&format!(" {}%", u),
capacity_color(u as f64 / 100.0, tc),
false,
),
None => styled(" -%", tc.muted, false),
};
lines.push(Line::from(vec![
styled(" Util ", tc.muted, false),
styled(&sparkline_scaled(&h.util, spark_w, 100.0), tc.accent, false),
pct_span,
]));
}
}
let mem = match (m.vram_used_mb, m.vram_total_mb) {
(Some(u), Some(t)) if t > 0 => {
format!("{:.1}/{:.1}G", u as f64 / 1024.0, t as f64 / 1024.0)
}
_ => "-".to_string(),
};
let temp = m.temp_c.map_or("-".to_string(), |t| format!("{}C", t));
let pwr = m.power_w.map_or("-".to_string(), |p| format!("{:.0}W", p));
let clk = m.clock_mhz.map_or("-".to_string(), |c| format!("{}MHz", c));
lines.push(Line::from(styled(
&format!(" Mem {} Temp {} Pwr {} Clk {}", mem, temp, pwr, clk),
tc.fg,
false,
)));
}
fn build_detail_lines(
t: &PortThroughput,
procs: &[&crate::stat::ProcessRdmaInfo],
history: Option<&super::app::DeviceHistory>,
tc: &ThemeColors,
show_avg: bool,
avg_window: usize,
) -> Vec<Line<'static>> {
let mut lines = build_device_header(t, history, tc, show_avg, avg_window);
append_active_counters(&mut lines, t, tc);
append_process_table(&mut lines, procs, tc);
lines
}
fn build_nvlink_detail_lines(
t: &PortThroughput,
meta: &super::app::NvLinkThroughputMeta,
history: Option<&super::app::DeviceHistory>,
tc: &ThemeColors,
show_avg: bool,
avg_window: usize,
) -> Vec<Line<'static>> {
let spark_w = 30;
let (tx_spark, rx_spark) = match history {
Some(h) => (sparkline_str(&h.tx, spark_w), sparkline_str(&h.rx, spark_w)),
None => (" ".repeat(spark_w), " ".repeat(spark_w)),
};
let avg_label = if show_avg {
format!(" [avg {}s]", avg_window)
} else {
String::new()
};
let mut lines: Vec<Line<'static>> = Vec::new();
lines.push(Line::from(vec![
styled(" Device: ", tc.muted, false),
styled(&t.dev_name, tc.accent, true),
styled(" [NVLink] ", tc.accent, false),
styled(
&format!("{}/{} active", meta.active_links, meta.links.len()),
tc.fg,
false,
),
styled(&avg_label, tc.accent, false),
]));
lines.push(Line::from(vec![
styled(" TX: ", tc.muted, false),
styled(&format!("{:.2} Gbps ", t.tx_gbps), tc.good, false),
styled(&tx_spark, tc.good, false),
]));
lines.push(Line::from(vec![
styled(" RX: ", tc.muted, false),
styled(&format!("{:.2} Gbps ", t.rx_gbps), tc.accent, false),
styled(&rx_spark, tc.accent, false),
]));
append_gpu_health(&mut lines, meta.metrics.as_ref(), history, spark_w, tc);
lines.push(Line::from(""));
lines.push(Line::from(styled(
" Per-lane throughput is not available from NVML on some drivers; values below show either per-lane rate or the GPU aggregate.",
tc.muted,
false,
)));
lines.push(Line::from(vec![styled(
" Lane State Ver TX MB/s RX MB/s Remote",
tc.header_fg,
true,
)]));
for link in &meta.links {
let link_tx_mb_s = match link.tx_bytes {
Some(bytes) => bytes as f64 / 1_000_000.0,
None => t.tx_gbps * 1000.0 / 8.0,
};
let link_rx_mb_s = match link.rx_bytes {
Some(bytes) => bytes as f64 / 1_000_000.0,
None => t.rx_gbps * 1000.0 / 8.0,
};
let state_label = if link.is_active {
"enabled"
} else {
"disabled"
};
let state_color = if link.is_active { tc.good } else { tc.muted };
let ver_label = link
.version
.map(|v| v.to_string())
.unwrap_or_else(|| "-".to_string());
let remote_label = match (&link.remote_pci_bdf, &link.remote_device_type) {
(Some(bdf), ty) => format!("{} {}", ty.label(), bdf),
(None, ty) => match ty {
crate::nvlink::RemoteDeviceType::Unknown => "-".to_string(),
_ => ty.label().to_string(),
},
};
lines.push(Line::from(vec![
styled(&format!(" {:>4}", link.link_id), tc.accent, false),
styled(&format!(" {:<8}", state_label), state_color, false),
styled(&format!(" {:>3}", ver_label), tc.fg, false),
styled(&format!(" {:>6.1}", link_tx_mb_s), tc.fg, false),
styled(&format!(" {:>6.1}", link_rx_mb_s), tc.fg, false),
styled(&format!(" {}", remote_label), tc.muted, false),
]));
}
lines.push(Line::from(""));
let mut crc: u64 = 0;
let mut replay: u64 = 0;
let mut recovery: u64 = 0;
for link in &meta.links {
crc = crc.saturating_add(link.crc_error_count.unwrap_or(0));
replay = replay.saturating_add(link.replay_error_count.unwrap_or(0));
recovery = recovery.saturating_add(link.recovery_error_count.unwrap_or(0));
}
lines.push(Line::from(vec![
styled(" Errors: ", tc.muted, false),
styled(&format!("replay={}", replay), tc.warning, false),
styled(" ", tc.muted, false),
styled(&format!("recovery={}", recovery), tc.warning, false),
styled(" ", tc.muted, false),
styled(&format!("crc={}", crc), tc.warning, false),
]));
lines
}
fn build_xgmi_detail_lines(
t: &PortThroughput,
meta: &super::app::XgmiThroughputMeta,
history: Option<&super::app::DeviceHistory>,
tc: &ThemeColors,
show_avg: bool,
avg_window: usize,
) -> Vec<Line<'static>> {
let spark_w = 30;
let (tx_spark, rx_spark) = match history {
Some(h) => (sparkline_str(&h.tx, spark_w), sparkline_str(&h.rx, spark_w)),
None => (" ".repeat(spark_w), " ".repeat(spark_w)),
};
let avg_label = if show_avg {
format!(" [avg {}s]", avg_window)
} else {
String::new()
};
let mut lines: Vec<Line<'static>> = Vec::new();
lines.push(Line::from(vec![
styled(" Device: ", tc.muted, false),
styled(&t.dev_name, tc.accent, true),
styled(" [XGMI] ", tc.accent, false),
styled(
&format!("{}/{} active", meta.active_links, meta.links.len()),
tc.fg,
false,
),
styled(&avg_label, tc.accent, false),
]));
lines.push(Line::from(vec![
styled(" TX: ", tc.muted, false),
styled(&format!("{:.2} Gbps ", t.tx_gbps), tc.good, false),
styled(&tx_spark, tc.good, false),
]));
lines.push(Line::from(vec![
styled(" RX: ", tc.muted, false),
styled(&format!("{:.2} Gbps ", t.rx_gbps), tc.accent, false),
styled(&rx_spark, tc.accent, false),
]));
append_gpu_health(&mut lines, meta.metrics.as_ref(), history, spark_w, tc);
lines.push(Line::from(""));
lines.push(Line::from(vec![styled(
" Peer State Gbps TX MB/s RX MB/s Remote",
tc.header_fg,
true,
)]));
for link in &meta.links {
let mb_s = |bytes: Option<u64>| match bytes {
Some(b) => format!("{:>7.1}", b as f64 / 1_000_000.0),
None => format!("{:>7}", "-"),
};
let state_label = if link.is_active { "up" } else { "down" };
let state_color = if link.is_active { tc.good } else { tc.muted };
let gbps_label = link
.speed_gbps
.map(|v| format!("{:.0}", v))
.unwrap_or_else(|| "-".to_string());
let remote_label = link
.remote_pci_bdf
.as_ref()
.map(|bdf| format!("GPU {}", bdf))
.unwrap_or_else(|| "-".to_string());
lines.push(Line::from(vec![
styled(&format!(" {:>4}", link.link_id), tc.accent, false),
styled(&format!(" {:<6}", state_label), state_color, false),
styled(&format!(" {:>4}", gbps_label), tc.fg, false),
styled(&format!(" {}", mb_s(link.tx_bytes)), tc.fg, false),
styled(&format!(" {}", mb_s(link.rx_bytes)), tc.fg, false),
styled(&format!(" {}", remote_label), tc.muted, false),
]));
}
lines.push(Line::from(""));
let counter = |name: &str| -> u64 {
t.counter_rates
.iter()
.find(|c| c.name == name)
.map(|c| c.value)
.unwrap_or(0)
};
lines.push(Line::from(vec![
styled(" Errors: ", tc.muted, false),
styled(
&format!("wafl_ce={}", counter("xgmi_wafl_ce")),
tc.warning,
false,
),
styled(" ", tc.muted, false),
styled(
&format!("wafl_ue={}", counter("xgmi_wafl_ue")),
tc.warning,
false,
),
]));
lines
}
fn build_device_header(
t: &PortThroughput,
history: Option<&super::app::DeviceHistory>,
tc: &ThemeColors,
show_avg: bool,
avg_window: usize,
) -> Vec<Line<'static>> {
let spark_w = 30;
let (tx_spark, rx_spark) = match history {
Some(h) => (sparkline_str(&h.tx, spark_w), sparkline_str(&h.rx, spark_w)),
None => (" ".repeat(spark_w), " ".repeat(spark_w)),
};
let mode_label = if show_avg {
format!(" [avg {}s]", avg_window)
} else {
String::new()
};
let state = t.state.as_deref();
let state_text = format!(" {} {}", state_label(state), state.unwrap_or("-"));
vec![
Line::from(vec![
styled(" Device: ", tc.muted, false),
styled(&format!("{}/{}", t.dev_name, t.port), tc.accent, true),
styled(&state_text, state_color(state, tc), false),
styled(&mode_label, tc.accent, false),
]),
Line::from(vec![
styled(" TX: ", tc.muted, false),
styled(&format!("{:.2} Gbps ", t.tx_gbps), tc.good, false),
styled(&tx_spark, tc.good, false),
]),
Line::from(vec![
styled(" RX: ", tc.muted, false),
styled(&format!("{:.2} Gbps ", t.rx_gbps), tc.accent, false),
styled(&rx_spark, tc.accent, false),
]),
Line::from(""),
]
}
fn append_active_counters(lines: &mut Vec<Line<'static>>, t: &PortThroughput, tc: &ThemeColors) {
let mut counters: Vec<_> = t
.counter_rates
.iter()
.filter(|r| r.value > 0 || EXTRA_COUNTERS.contains(&r.name.as_str()))
.collect();
counters.sort_by(|a, b| {
b.rate
.partial_cmp(&a.rate)
.unwrap_or(std::cmp::Ordering::Equal)
.then(b.value.cmp(&a.value))
.then(a.name.cmp(&b.name))
});
if !counters.is_empty() {
for r in &counters {
lines.push(counter_rate_line(r, tc));
}
lines.push(Line::from(""));
}
}
const PROC_HEADER: &str =
" PID USER NI S VIRT RES SHR MEM% QPs THR COMMAND";
fn append_process_table(
lines: &mut Vec<Line<'static>>,
procs: &[&crate::stat::ProcessRdmaInfo],
tc: &ThemeColors,
) {
lines.push(Line::from(vec![styled(PROC_HEADER, tc.header_fg, true)]));
if procs.is_empty() {
lines.push(Line::from(styled(" (no RDMA processes)", tc.muted, false)));
} else {
for p in procs {
lines.push(process_line(p, tc));
}
}
}
fn process_line(p: &crate::stat::ProcessRdmaInfo, tc: &ThemeColors) -> Line<'static> {
let state_color = match p.state {
'R' => tc.good,
'S' | 'I' => tc.muted,
'D' => tc.warning,
'Z' | 'T' => tc.error,
_ => tc.fg,
};
Line::from(vec![
styled(&format!(" {:<7}", p.pid), tc.accent, false),
styled(&format!(" {:<8}", truncate(&p.user, 8)), tc.fg, false),
styled(&format!(" {:<2}", p.nice), tc.muted, false),
styled(&format!(" {:>1}", p.state), state_color, false),
styled(&format!(" {:>8}", fmt_mem_kb(p.virt_kb)), tc.fg, false),
styled(&format!(" {:>8}", fmt_mem_kb(p.res_kb)), tc.good, false),
styled(&format!(" {:>6}", fmt_mem_kb(p.shr_kb)), tc.fg, false),
styled(&format!(" {:>4.1}", p.mem_pct), tc.fg, false),
styled(&format!(" {:>5}", p.qp_count), tc.accent, false),
styled(&format!(" {:>4}", p.threads), tc.muted, false),
styled(&format!(" {}", truncate(&p.cmdline, 40)), tc.fg, false),
])
}
fn build_selected_detail_lines(
t: &PortThroughput,
procs: &[&crate::stat::ProcessRdmaInfo],
history: Option<&super::app::DeviceHistory>,
tc: &ThemeColors,
show_avg: bool,
avg_window: usize,
) -> Vec<Line<'static>> {
if let Some(ref meta) = t.nvlink {
return build_nvlink_detail_lines(t, meta, history, tc, show_avg, avg_window);
}
if let Some(ref meta) = t.xgmi {
return build_xgmi_detail_lines(t, meta, history, tc, show_avg, avg_window);
}
build_detail_lines(t, procs, history, tc, show_avg, avg_window)
}
fn draw_detail(frame: &mut Frame, app: &mut App, area: Rect, tc: &ThemeColors) {
let display = app.display_throughputs();
let t = match display.get(app.selected_row).cloned() {
Some(t) => t,
None => {
frame.render_widget(
Block::default()
.borders(Borders::ALL)
.border_set(super::glyphs::border())
.border_style(Style::default().fg(tc.border))
.title(" Detail "),
area,
);
return;
}
};
let history = app.history.get(&t.dev_name);
let procs = app.selected_device_processes();
let lines = build_selected_detail_lines(
&t,
&procs,
history,
tc,
app.show_rolling_avg,
app.rolling_avg.window_secs,
);
let visible = area.height.saturating_sub(2);
app.detail_max_scroll = (lines.len() as u16).saturating_sub(visible);
let title = if app.show_rolling_avg {
format!(" {} (avg {}s) ", t.dev_name, app.rolling_avg.window_secs)
} else {
format!(" {} ", t.dev_name)
};
let block = Block::default()
.borders(Borders::ALL)
.border_set(super::glyphs::border())
.border_style(Style::default().fg(tc.border))
.title(title)
.title_style(Style::default().fg(tc.accent).add_modifier(Modifier::BOLD));
frame.render_widget(
Paragraph::new(lines)
.block(block)
.scroll((app.detail_scroll, 0)),
area,
);
}
fn counter_rate_line(r: &CounterRate, tc: &ThemeColors) -> Line<'static> {
let rate_str = if r.is_bytes {
format_bytes(r.rate)
} else {
format_rate(r.rate)
};
let color = counter_color(r, tc);
Line::from(vec![
Span::styled(format!(" {:<35}", r.name), Style::default().fg(tc.fg)),
Span::styled(format!("{:>12}", rate_str), Style::default().fg(color)),
Span::styled(format!(" Δ {}", r.delta), Style::default().fg(tc.muted)),
])
}
fn counter_color(r: &CounterRate, tc: &ThemeColors) -> ratatui::style::Color {
let is_error = r.name.contains("err") || r.name.contains("drop");
let is_warn = r.name.contains("retrans")
|| r.name.contains("unresponsive")
|| r.name.contains("impaired");
match (r.delta > 0, is_error, is_warn) {
(true, true, _) => tc.error,
(true, _, true) => tc.warning,
(true, _, _) => tc.good,
_ => tc.muted,
}
}
fn draw_status_bar(frame: &mut Frame, app: &App, area: Rect, tc: &ThemeColors) {
let hint = if app.show_detail {
"Enter/Esc:close"
} else {
"Enter:detail"
};
let avg_hint = if app.show_rolling_avg {
format!(
" a:avg[ON {}s] +/-:window w:set",
app.rolling_avg.window_secs
)
} else {
" a:avg w:set".to_string()
};
let keys = super::glyphs::tr(&format!(
" ↑↓/jk:nav {} t:theme <>:refresh r:rec h:help q:quit{}",
hint, avg_hint
))
.into_owned();
let mode_style = Style::default()
.fg(tc.status_fg)
.bg(tc.status_bg)
.add_modifier(Modifier::BOLD);
let mode = Span::styled(" NORMAL ", mode_style);
let keys = Span::styled(keys, Style::default().fg(tc.muted));
let mut spans = vec![mode];
if let Some((secs, samples)) = app.recording_progress() {
spans.push(Span::styled(
super::glyphs::tr(&format!(
" ● REC {}:{:02} ({}) ",
secs / 60,
secs % 60,
samples
))
.into_owned(),
Style::default().fg(tc.error).add_modifier(Modifier::BOLD),
));
} else if let Some(msg) = &app.record_status {
spans.push(Span::styled(
format!(" {} ", msg),
Style::default().fg(tc.fg),
));
}
if let Some(err) = &app.sampler_error {
spans.push(Span::styled(
super::glyphs::tr(&format!(" sampler died: {} ", err)).into_owned(),
Style::default().fg(tc.error).add_modifier(Modifier::BOLD),
));
} else if let Some(secs) = app.stale_secs() {
spans.push(Span::styled(
format!(" STALE {}s - sampler stalled ", secs),
Style::default().fg(tc.error).add_modifier(Modifier::BOLD),
));
}
spans.push(keys);
frame.render_widget(Paragraph::new(Line::from(spans)), area);
}
fn draw_help_popup(frame: &mut Frame, tc: &ThemeColors) {
let area = frame.area();
let mut content_w = 0;
for (_, desc) in HELP_KEYS {
content_w = content_w.max(16 + desc.chars().count());
}
let w = (content_w as u16 + 2).min(area.width.saturating_sub(4));
let h = (HELP_KEYS.len() as u16 + 2).min(area.height.saturating_sub(4));
let popup = centered_rect(area, w, h);
frame.render_widget(Clear, popup);
let lines: Vec<Line> = HELP_KEYS
.iter()
.map(|(key, desc)| help_line(key, desc, tc))
.collect();
let block = Block::default()
.borders(Borders::ALL)
.border_set(super::glyphs::border())
.border_style(Style::default().fg(tc.accent))
.title(" Help (h/Esc to close) ")
.title_style(Style::default().fg(tc.accent).add_modifier(Modifier::BOLD));
frame.render_widget(Paragraph::new(lines).block(block), popup);
}
fn draw_window_input_popup(frame: &mut Frame, app: &App, tc: &ThemeColors) {
let area = frame.area();
let w = 40.min(area.width.saturating_sub(4));
let h = 5.min(area.height.saturating_sub(4));
let popup = centered_rect(area, w, h);
frame.render_widget(Clear, popup);
let lines = vec![
Line::from(vec![
styled(" Window (1-300s): ", tc.muted, false),
styled(&app.window_input_buf, tc.accent, true),
styled("▏", tc.accent, false),
]),
Line::from(styled(" Enter:confirm Esc:cancel", tc.muted, false)),
];
let block = Block::default()
.borders(Borders::ALL)
.border_set(super::glyphs::border())
.border_style(Style::default().fg(tc.accent))
.title(" Set Avg Window ")
.title_style(Style::default().fg(tc.accent).add_modifier(Modifier::BOLD));
frame.render_widget(Paragraph::new(lines).block(block), popup);
}
fn draw_column_picker(frame: &mut Frame, app: &App, tc: &ThemeColors) {
let all = all_columns();
let area = frame.area();
let w = 45.min(area.width.saturating_sub(4));
let h = ((all.len() + 4) as u16).min(area.height.saturating_sub(4));
let popup = centered_rect(area, w, h);
frame.render_widget(Clear, popup);
let lines: Vec<Line> = all
.iter()
.enumerate()
.map(|(i, col)| {
let enabled = app.columns.contains(col);
let marker = if enabled { "[x]" } else { "[ ]" };
let cursor = if i == app.column_picker_cursor {
"▶"
} else {
" "
};
let color = if i == app.column_picker_cursor {
tc.accent
} else if enabled {
tc.fg
} else {
tc.muted
};
Line::from(styled(
&format!(" {} {} {}", cursor, marker, col.label()),
color,
i == app.column_picker_cursor,
))
})
.collect();
let block = Block::default()
.borders(Borders::ALL)
.border_set(super::glyphs::border())
.border_style(Style::default().fg(tc.accent))
.title(" Columns (Space:toggle Esc:close) ")
.title_style(Style::default().fg(tc.accent).add_modifier(Modifier::BOLD));
frame.render_widget(Paragraph::new(lines).block(block), popup);
}
fn help_line(key: &str, desc: &str, tc: &ThemeColors) -> Line<'static> {
Line::from(vec![
styled(&format!(" {:<14}", key), tc.accent, false),
styled(desc, tc.fg, false),
])
}
fn centered_rect(area: Rect, w: u16, h: u16) -> Rect {
Rect::new(
area.x + (area.width.saturating_sub(w)) / 2,
area.y + (area.height.saturating_sub(h)) / 2,
w,
h,
)
}
fn styled(text: &str, color: ratatui::style::Color, bold: bool) -> Span<'static> {
let s = Style::default().fg(color);
Span::styled(
super::glyphs::tr(text).into_owned(),
if bold {
s.add_modifier(Modifier::BOLD)
} else {
s
},
)
}
fn fmt_bytes_short(bps: f64) -> String {
if bps >= 1_000_000_000.0 {
format!("{:.1}G", bps / 1_000_000_000.0)
} else if bps >= 1_000_000.0 {
format!("{:.1}M", bps / 1_000_000.0)
} else if bps >= 1_000.0 {
format!("{:.1}K", bps / 1_000.0)
} else {
format!("{:.0}B", bps)
}
}
fn format_bytes(bps: f64) -> String {
if bps >= 1_073_741_824.0 {
format!("{:.2} GB/s", bps / 1_073_741_824.0)
} else if bps >= 1_048_576.0 {
format!("{:.2} MB/s", bps / 1_048_576.0)
} else if bps >= 1024.0 {
format!("{:.2} KB/s", bps / 1024.0)
} else {
format!("{:.0} B/s", bps)
}
}
fn format_pps(pps: f64) -> String {
if pps >= 1_000_000.0 {
format!("{:.2}M", pps / 1_000_000.0)
} else if pps >= 1_000.0 {
format!("{:.1}K", pps / 1_000.0)
} else {
format!("{:.0}", pps)
}
}
fn format_rate(rate: f64) -> String {
if rate >= 1_000_000.0 {
format!("{:.2}M/s", rate / 1_000_000.0)
} else if rate >= 1_000.0 {
format!("{:.1}K/s", rate / 1_000.0)
} else {
format!("{:.1}/s", rate)
}
}
fn gbps_color(gbps: f64, tc: &ThemeColors) -> ratatui::style::Color {
if gbps >= 10.0 {
tc.good
} else if gbps >= 1.0 {
tc.warning
} else {
tc.muted
}
}
fn capacity_color(ratio: f64, tc: &ThemeColors) -> ratatui::style::Color {
if ratio >= 0.85 {
tc.error
} else if ratio >= 0.70 {
tc.warning
} else if ratio >= 0.40 {
tc.good
} else if ratio >= 0.05 {
tc.good_dim
} else {
tc.muted
}
}
fn state_label(state: Option<&str>) -> String {
match state {
Some(_) => if super::glyphs::unicode() { "●" } else { "*" }.into(),
None => "-".into(),
}
}
fn state_color(state: Option<&str>, tc: &ThemeColors) -> ratatui::style::Color {
match state {
Some("ACTIVE") | Some("ACTIVE_DEFER") => tc.good,
Some("DOWN") => tc.error,
Some("INIT") | Some("ARMED") => tc.warning,
_ => tc.muted,
}
}
fn throughput_color(gbps: f64, link_gbps: Option<f64>, tc: &ThemeColors) -> ratatui::style::Color {
match link_gbps.filter(|&c| c > 0.0) {
Some(cap) => capacity_color(gbps / cap, tc),
None => gbps_color(gbps, tc),
}
}
fn temp_color(temp_c: i64, tc: &ThemeColors) -> ratatui::style::Color {
if temp_c >= 85 {
tc.error
} else if temp_c >= 70 {
tc.warning
} else {
tc.good
}
}
fn fmt_mem_kb(kb: u64) -> String {
if kb >= 1_048_576 {
format!("{:.1}G", kb as f64 / 1_048_576.0)
} else if kb >= 1024 {
format!("{:.0}M", kb as f64 / 1024.0)
} else {
format!("{}K", kb)
}
}
fn truncate(s: &str, max: usize) -> String {
if s.chars().count() <= max {
return s.to_string();
}
let dots = super::glyphs::tr("…");
let keep = max.saturating_sub(dots.chars().count());
let cut: String = s.chars().take(keep).collect();
format!("{cut}{dots}")
}
#[cfg(test)]
mod tab_bar_tests {
use super::*;
use crate::tui::app::{App, DeviceClass};
use crate::tui::theme::Theme;
fn line_text(line: &Line<'_>) -> String {
line.spans.iter().map(|s| s.content.as_ref()).collect()
}
#[test]
fn tab_bar_shows_all_seen_tabs() {
let mut app = App::new();
app.seen_tabs = vec![DeviceClass::Rdma, DeviceClass::Xgmi];
app.active_tab = DeviceClass::Rdma;
let tc = Theme::Default.colors();
let bar = tab_bar_line(&app, &tc);
let text = line_text(&bar);
assert!(text.contains("RDMA"), "bar: {text:?}");
assert!(text.contains("XGMI"), "bar: {text:?}");
assert!(text.contains('│') || text.contains('|'), "bar: {text:?}");
}
#[test]
fn active_tab_is_highlighted() {
let mut app = App::new();
app.seen_tabs = vec![DeviceClass::Rdma, DeviceClass::Xgmi];
app.active_tab = DeviceClass::Xgmi;
let tc = Theme::Default.colors();
let bar = tab_bar_line(&app, &tc);
let xgmi_span = bar
.spans
.iter()
.find(|s| s.content.contains("XGMI"))
.expect("XGMI span");
let rdma_span = bar
.spans
.iter()
.find(|s| s.content.contains("RDMA"))
.expect("RDMA span");
assert!(
xgmi_span.style.add_modifier.contains(Modifier::BOLD),
"active XGMI span should be bold"
);
assert!(
!rdma_span.style.add_modifier.contains(Modifier::BOLD),
"inactive RDMA span should not be bold"
);
}
}
#[cfg(test)]
mod nvlink_detail_tests {
use super::*;
use crate::nvlink::{LinkSnapshot, RemoteDeviceType};
use crate::tui::app::{CounterRate, NvLinkThroughputMeta};
use crate::tui::theme::Theme;
#[allow(clippy::too_many_arguments)]
fn make_link(
id: u32,
active: bool,
remote_type: RemoteDeviceType,
remote_bdf: Option<&str>,
tx_bytes: Option<u64>,
rx_bytes: Option<u64>,
crc: Option<u64>,
replay: Option<u64>,
recovery: Option<u64>,
version: Option<u32>,
) -> LinkSnapshot {
LinkSnapshot {
link_id: id,
is_active: active,
version,
speed_gbps: if active { Some(50.0) } else { None },
remote_device_type: remote_type,
remote_pci_bdf: remote_bdf.map(|s| s.to_string()),
tx_bytes,
rx_bytes,
crc_error_count: crc,
replay_error_count: replay,
recovery_error_count: recovery,
}
}
fn make_port(
dev_name: &str,
links: Vec<LinkSnapshot>,
tx_rate_bps: f64,
rx_rate_bps: f64,
) -> (PortThroughput, NvLinkThroughputMeta) {
let counter_rates: Vec<CounterRate> = links
.iter()
.flat_map(|l| {
vec![
CounterRate {
name: format!("nvlink_tx_l{}", l.link_id),
value: l.tx_bytes.unwrap_or(0),
delta: 0,
rate: tx_rate_bps,
is_bytes: true,
},
CounterRate {
name: format!("nvlink_rx_l{}", l.link_id),
value: l.rx_bytes.unwrap_or(0),
delta: 0,
rate: rx_rate_bps,
is_bytes: true,
},
]
})
.collect();
let active = links.iter().filter(|l| l.is_active).count() as u32;
let meta = NvLinkThroughputMeta {
gpu_index: 0,
gpu_name: "H100".to_string(),
active_links: active,
links: links.clone(),
metrics: None,
};
let port = PortThroughput {
dev_name: dev_name.to_string(),
port: active,
link_gbps: Some(50.0),
state: None,
tx_gbps: 0.0,
rx_gbps: 0.0,
tx_pkts_per_sec: 0.0,
rx_pkts_per_sec: 0.0,
rx_drops_per_sec: 0.0,
counter_rates,
port_label: Some(format!("{}/{}", active, links.len())),
nvlink: Some(meta.clone()),
xgmi: None,
class: DeviceClass::Nvlink,
};
(port, meta)
}
fn join_lines(lines: &[Line<'static>]) -> String {
let mut s = String::new();
for line in lines {
s.push_str(&line.to_string());
s.push('\n');
}
s
}
#[test]
fn header_contains_device_and_active_count() {
let links = vec![
make_link(
0,
true,
RemoteDeviceType::Switch,
Some("0000:01:00.0"),
None,
None,
None,
None,
None,
Some(3),
),
make_link(
1,
true,
RemoteDeviceType::Switch,
Some("0000:01:00.0"),
None,
None,
None,
None,
None,
Some(3),
),
make_link(
2,
false,
RemoteDeviceType::Unknown,
None,
None,
None,
None,
None,
None,
None,
),
];
let (port, meta) = make_port("nvidia0", links, 0.0, 0.0);
let tc = Theme::Default.colors();
let lines = build_nvlink_detail_lines(&port, &meta, None, &tc, false, 0);
let header = lines[0].to_string();
assert!(
header.contains("Device: nvidia0"),
"header missing device label: {header:?}"
);
assert!(
header.contains("[NVLink]"),
"header missing NVLink tag: {header:?}"
);
assert!(
header.contains("2/3 active"),
"header missing active/total count: {header:?}"
);
assert!(
!header.contains("[avg"),
"avg label should be hidden: {header:?}"
);
}
#[test]
fn inactive_link_renders_disabled_and_dash_remote() {
let links = vec![make_link(
0,
false,
RemoteDeviceType::Unknown,
None,
None,
None,
None,
None,
None,
None,
)];
let (port, meta) = make_port("nvidia0", links, 0.0, 0.0);
let tc = Theme::Default.colors();
let lines = build_nvlink_detail_lines(&port, &meta, None, &tc, false, 0);
let blob = join_lines(&lines);
assert!(blob.contains("disabled"), "missing 'disabled': {blob}");
let lane_row = lines
.iter()
.find(|l| l.to_string().contains("disabled"))
.expect("lane row with disabled");
let s = lane_row.to_string();
assert!(
s.trim_end().ends_with('-'),
"inactive row should end with '-': {s:?}"
);
}
#[test]
fn active_link_renders_remote_pci_bdf() {
let links = vec![make_link(
2,
true,
RemoteDeviceType::Switch,
Some("0000:01:00.0"),
None,
None,
None,
None,
None,
Some(4),
)];
let (port, meta) = make_port("nvidia0", links, 0.0, 0.0);
let tc = Theme::Default.colors();
let lines = build_nvlink_detail_lines(&port, &meta, None, &tc, false, 0);
let blob = join_lines(&lines);
assert!(
blob.contains("NVSwitch 0000:01:00.0"),
"active switch link missing remote label: {blob}"
);
assert!(
blob.contains("enabled"),
"active link row should be 'enabled': {blob}"
);
}
#[test]
fn error_line_sums_counters_across_links() {
let links = vec![
make_link(
0,
true,
RemoteDeviceType::Switch,
Some("0000:01:00.0"),
None,
None,
Some(3),
Some(5),
Some(2),
Some(4),
),
make_link(
1,
true,
RemoteDeviceType::Switch,
Some("0000:01:00.0"),
None,
None,
Some(7),
Some(11),
Some(13),
Some(4),
),
make_link(
2,
false,
RemoteDeviceType::Unknown,
None,
None,
None,
None,
None,
None,
None,
),
];
let (port, meta) = make_port("nvidia0", links, 0.0, 0.0);
let tc = Theme::Default.colors();
let lines = build_nvlink_detail_lines(&port, &meta, None, &tc, false, 0);
let error_line = lines
.iter()
.find(|l| l.to_string().contains("Errors:"))
.expect("Errors line")
.to_string();
assert!(
error_line.contains("crc=10"),
"crc sum wrong: {error_line:?}"
);
assert!(
error_line.contains("replay=16"),
"replay sum wrong: {error_line:?}"
);
assert!(
error_line.contains("recovery=15"),
"recovery sum wrong: {error_line:?}"
);
}
#[test]
fn rolling_average_label_appears_when_enabled() {
let links = vec![make_link(
0,
true,
RemoteDeviceType::Switch,
Some("0000:01:00.0"),
None,
None,
None,
None,
None,
Some(4),
)];
let (port, meta) = make_port("nvidia0", links, 0.0, 0.0);
let tc = Theme::Default.colors();
let lines = build_nvlink_detail_lines(&port, &meta, None, &tc, true, 5);
let header = lines[0].to_string();
assert!(
header.contains("[avg 5s]"),
"rolling avg label missing: {header:?}"
);
}
#[test]
fn header_and_lanes_show_aggregate_rates() {
let links = vec![
make_link(
0,
true,
RemoteDeviceType::Switch,
Some("0000:01:00.0"),
None,
None,
None,
None,
None,
Some(3),
),
make_link(
1,
true,
RemoteDeviceType::Gpu,
Some("0000:02:00.0"),
None,
None,
None,
None,
None,
Some(3),
),
make_link(
2,
false,
RemoteDeviceType::Unknown,
None,
None,
None,
None,
None,
None,
None,
),
];
let meta = NvLinkThroughputMeta {
gpu_index: 0,
gpu_name: "H100".to_string(),
active_links: 2,
links: links.clone(),
metrics: None,
};
let port = PortThroughput {
dev_name: "nvidia0".to_string(),
port: 2,
link_gbps: Some(100.0),
state: None,
tx_gbps: 1.0,
rx_gbps: 1.0,
tx_pkts_per_sec: 0.0,
rx_pkts_per_sec: 0.0,
rx_drops_per_sec: 0.0,
counter_rates: Vec::new(),
port_label: Some("2/3".to_string()),
nvlink: Some(meta.clone()),
xgmi: None,
class: DeviceClass::Nvlink,
};
let tc = Theme::Default.colors();
let lines = build_nvlink_detail_lines(&port, &meta, None, &tc, false, 0);
let tx_line = lines[1].to_string();
let rx_line = lines[2].to_string();
assert!(
tx_line.contains("1.00 Gbps"),
"TX header missing aggregate rate: {tx_line:?}"
);
assert!(
rx_line.contains("1.00 Gbps"),
"RX header missing aggregate rate: {rx_line:?}"
);
let expected_mb_s = "125.0";
let blob = join_lines(&lines);
assert!(
blob.contains("Per-lane throughput is not available from NVML"),
"missing muted note about per-lane throughput: {blob}"
);
for link in &links {
let lane_row = lines
.iter()
.find(|l| {
let s = l.to_string();
s.contains(&format!("{:>4}", link.link_id))
})
.unwrap_or_else(|| panic!("missing lane row for link {}", link.link_id))
.to_string();
assert!(
lane_row.contains(&expected_mb_s.to_string()),
"lane {} row missing aggregate MB/s value {}: {:?}",
link.link_id,
expected_mb_s,
lane_row
);
}
let occurrences = lines
.iter()
.filter(|l| l.to_string().contains(expected_mb_s))
.count();
assert_eq!(
occurrences, 3,
"expected the aggregate MB/s value in 3 lane rows, got {occurrences}"
);
}
#[test]
fn lanes_show_per_lane_rates_when_present() {
let links = vec![
make_link(
0,
true,
RemoteDeviceType::Switch,
Some("0000:01:00.0"),
Some(10_000_000), Some(20_000_000), None,
None,
None,
Some(3),
),
make_link(
1,
true,
RemoteDeviceType::Gpu,
Some("0000:02:00.0"),
Some(30_000_000), Some(40_000_000), None,
None,
None,
Some(3),
),
];
let meta = NvLinkThroughputMeta {
gpu_index: 0,
gpu_name: "H100".to_string(),
active_links: 2,
links: links.clone(),
metrics: None,
};
let port = PortThroughput {
dev_name: "nvidia0".to_string(),
port: 2,
link_gbps: Some(100.0),
state: None,
tx_gbps: 1.0,
rx_gbps: 1.0,
tx_pkts_per_sec: 0.0,
rx_pkts_per_sec: 0.0,
rx_drops_per_sec: 0.0,
counter_rates: Vec::new(),
port_label: Some("2/2".to_string()),
nvlink: Some(meta.clone()),
xgmi: None,
class: DeviceClass::Nvlink,
};
let tc = Theme::Default.colors();
let lines = build_nvlink_detail_lines(&port, &meta, None, &tc, false, 0);
let lane0_row = lines
.iter()
.find(|l| l.to_string().starts_with(" 0 "))
.unwrap()
.to_string();
assert!(
lane0_row.contains(" 10.0"),
"lane 0 missing TX: {lane0_row:?}"
);
assert!(
lane0_row.contains(" 20.0"),
"lane 0 missing RX: {lane0_row:?}"
);
let lane1_row = lines
.iter()
.find(|l| l.to_string().starts_with(" 1 "))
.unwrap()
.to_string();
assert!(
lane1_row.contains(" 30.0"),
"lane 1 missing TX: {lane1_row:?}"
);
assert!(
lane1_row.contains(" 40.0"),
"lane 1 missing RX: {lane1_row:?}"
);
}
}
#[cfg(test)]
mod xgmi_detail_tests {
use super::*;
use crate::tui::app::{PortThroughput, XgmiThroughputMeta};
use crate::tui::theme::Theme;
use crate::xgmi::XgmiLinkSnapshot;
fn line_text(line: &Line<'_>) -> String {
line.spans.iter().map(|s| s.content.as_ref()).collect()
}
fn mk_link(id: u32, active: bool, tx_rate: u64, rx_rate: u64) -> XgmiLinkSnapshot {
XgmiLinkSnapshot {
link_id: id,
is_active: active,
speed_gbps: Some(512.0),
bit_rate_gbps: Some(32.0),
remote_pci_bdf: Some(format!("0000:{:02x}:00.0", 0x15 + id)),
tx_bytes: Some(tx_rate),
rx_bytes: Some(rx_rate),
}
}
fn mk_row(links: Vec<XgmiLinkSnapshot>) -> (PortThroughput, XgmiThroughputMeta) {
let active = links.iter().filter(|l| l.is_active).count() as u32;
let meta = XgmiThroughputMeta {
gpu_index: 0,
gpu_name: "MI325X".to_string(),
active_links: active,
links,
metrics: None,
};
let port = PortThroughput {
dev_name: "amdgpu0".to_string(),
port: active,
link_gbps: Some(512.0 * active as f64),
state: None,
tx_gbps: 1.0,
rx_gbps: 2.0,
tx_pkts_per_sec: 0.0,
rx_pkts_per_sec: 0.0,
rx_drops_per_sec: 0.0,
counter_rates: vec![
crate::tui::app::CounterRate {
name: "xgmi_wafl_ce".to_string(),
value: 7,
delta: 0,
rate: 0.0,
is_bytes: false,
},
crate::tui::app::CounterRate {
name: "xgmi_wafl_ue".to_string(),
value: 1,
delta: 0,
rate: 0.0,
is_bytes: false,
},
],
port_label: Some(format!("{}/{}", active, meta.links.len())),
nvlink: None,
xgmi: Some(meta.clone()),
class: DeviceClass::Xgmi,
};
(port, meta)
}
#[test]
fn header_shows_device_and_active_links() {
let tc = Theme::Default.colors();
let (port, meta) = mk_row(vec![mk_link(0, true, 0, 0), mk_link(1, false, 0, 0)]);
let lines = build_xgmi_detail_lines(&port, &meta, None, &tc, false, 0);
let header = line_text(&lines[0]);
assert!(header.contains("amdgpu0"), "header: {header:?}");
assert!(header.contains("[XGMI]"), "header: {header:?}");
assert!(header.contains("1/2 active"), "header: {header:?}");
}
#[test]
fn per_link_rows_show_rate_and_remote() {
let tc = Theme::Default.colors();
let (port, meta) = mk_row(vec![mk_link(0, true, 2_000_000, 4_000_000)]);
let lines = build_xgmi_detail_lines(&port, &meta, None, &tc, false, 0);
let text: Vec<String> = lines.iter().map(line_text).collect();
let row = text
.iter()
.find(|l| l.contains("0000:15:00.0"))
.expect("link row");
assert!(row.contains("up"), "row: {row:?}");
assert!(row.contains("2.0"), "row: {row:?}");
assert!(row.contains("4.0"), "row: {row:?}");
}
#[test]
fn inactive_link_rendered_down() {
let tc = Theme::Default.colors();
let (port, meta) = mk_row(vec![mk_link(0, false, 0, 0)]);
let lines = build_xgmi_detail_lines(&port, &meta, None, &tc, false, 0);
let text: Vec<String> = lines.iter().map(line_text).collect();
assert!(text.iter().any(|l| l.contains("down")), "lines: {text:?}");
}
#[test]
fn errors_line_shows_wafl_counts() {
let tc = Theme::Default.colors();
let (port, meta) = mk_row(vec![mk_link(0, true, 0, 0)]);
let lines = build_xgmi_detail_lines(&port, &meta, None, &tc, false, 0);
let text: Vec<String> = lines.iter().map(line_text).collect();
let err = text
.iter()
.find(|l| l.contains("wafl_ce"))
.expect("errors line");
assert!(err.contains("wafl_ce=7"), "err: {err:?}");
assert!(err.contains("wafl_ue=1"), "err: {err:?}");
}
#[test]
fn missing_counters_render_as_dash() {
let tc = Theme::Default.colors();
let mut link = mk_link(0, true, 0, 0);
link.tx_bytes = None;
link.rx_bytes = None;
let (port, meta) = mk_row(vec![link]);
let lines = build_xgmi_detail_lines(&port, &meta, None, &tc, false, 0);
let text: Vec<String> = lines.iter().map(line_text).collect();
let row = text
.iter()
.find(|l| l.contains("0000:15:00.0"))
.expect("link row");
assert!(row.matches('-').count() >= 2, "row: {row:?}");
}
}
#[cfg(test)]
mod gpu_table_tests {
use super::*;
use crate::tui::app::{CounterRate, DeviceClass, NvLinkThroughputMeta, XgmiThroughputMeta};
fn make_counter(name: &str, value: u64) -> CounterRate {
CounterRate {
name: name.to_string(),
value,
delta: 0,
rate: 0.0,
is_bytes: false,
}
}
fn xgmi_row(ce: u64, ue: u64) -> PortThroughput {
PortThroughput {
dev_name: "amdgpu0".to_string(),
port: 7,
link_gbps: Some(3584.0),
state: None,
tx_gbps: 0.0,
rx_gbps: 0.0,
tx_pkts_per_sec: 0.0,
rx_pkts_per_sec: 0.0,
rx_drops_per_sec: 0.0,
counter_rates: vec![
make_counter("xgmi_wafl_ce", ce),
make_counter("xgmi_wafl_ue", ue),
],
port_label: Some("7/7".to_string()),
nvlink: None,
xgmi: Some(XgmiThroughputMeta {
gpu_index: 0,
gpu_name: "MI325X".to_string(),
active_links: 7,
links: vec![],
metrics: None,
}),
class: DeviceClass::Xgmi,
}
}
fn nvlink_row(crc: u64, replay: u64) -> PortThroughput {
PortThroughput {
dev_name: "nvidia0".to_string(),
port: 18,
link_gbps: Some(900.0),
state: None,
tx_gbps: 0.0,
rx_gbps: 0.0,
tx_pkts_per_sec: 0.0,
rx_pkts_per_sec: 0.0,
rx_drops_per_sec: 0.0,
counter_rates: vec![
make_counter("nvlink_crc_l0", crc),
make_counter("nvlink_replay_l1", replay),
],
port_label: Some("18/18".to_string()),
nvlink: Some(NvLinkThroughputMeta {
gpu_index: 0,
gpu_name: "H100".to_string(),
active_links: 18,
links: vec![],
metrics: None,
}),
xgmi: None,
class: DeviceClass::Nvlink,
}
}
fn rdma_row() -> PortThroughput {
PortThroughput {
dev_name: "mlx5_0".to_string(),
port: 1,
link_gbps: Some(100.0),
state: None,
tx_gbps: 0.0,
rx_gbps: 0.0,
tx_pkts_per_sec: 0.0,
rx_pkts_per_sec: 0.0,
rx_drops_per_sec: 0.0,
counter_rates: vec![],
port_label: None,
nvlink: None,
xgmi: None,
class: DeviceClass::Rdma,
}
}
#[test]
fn gpu_meta_name_xgmi() {
assert_eq!(gpu_meta_name(&xgmi_row(7, 1)), "MI325X");
}
#[test]
fn gpu_meta_name_nvlink() {
assert_eq!(gpu_meta_name(&nvlink_row(2, 3)), "H100");
}
#[test]
fn gpu_meta_name_plain_rdma() {
assert_eq!(gpu_meta_name(&rdma_row()), "");
}
#[test]
fn gpu_errs_xgmi_ce_ue() {
assert_eq!(gpu_errs(&xgmi_row(7, 1)), "7/1");
}
#[test]
fn gpu_errs_xgmi_missing_counters() {
let mut row = xgmi_row(0, 0);
row.counter_rates.clear();
assert_eq!(gpu_errs(&row), "0/0");
}
#[test]
fn gpu_errs_nvlink_sum() {
assert_eq!(gpu_errs(&nvlink_row(2, 3)), "5");
}
#[test]
fn gpu_errs_nvlink_missing_counters() {
let mut row = nvlink_row(0, 0);
row.counter_rates.clear();
assert_eq!(gpu_errs(&row), "0");
}
}
#[cfg(test)]
mod color_tests {
use super::*;
use crate::tui::theme::Theme;
#[test]
fn capacity_color_steps() {
let tc = Theme::Default.colors();
assert_eq!(capacity_color(0.049, &tc), tc.muted);
assert_eq!(capacity_color(0.05, &tc), tc.good_dim);
assert_eq!(capacity_color(0.399, &tc), tc.good_dim);
assert_eq!(capacity_color(0.40, &tc), tc.good);
assert_eq!(capacity_color(0.699, &tc), tc.good);
assert_eq!(capacity_color(0.70, &tc), tc.warning);
assert_eq!(capacity_color(0.849, &tc), tc.warning);
assert_eq!(capacity_color(0.85, &tc), tc.error);
}
#[test]
fn throughput_color_falls_back_to_absolute_without_capacity() {
let tc = Theme::Default.colors();
assert_eq!(throughput_color(388.0, Some(400.0), &tc), tc.error);
assert_eq!(throughput_color(388.0, None, &tc), tc.good);
assert_eq!(throughput_color(0.4, None, &tc), tc.muted);
}
#[test]
fn temp_color_thresholds() {
let tc = Theme::Default.colors();
assert_eq!(temp_color(69, &tc), tc.good);
assert_eq!(temp_color(70, &tc), tc.warning);
assert_eq!(temp_color(84, &tc), tc.warning);
assert_eq!(temp_color(85, &tc), tc.error);
}
}
#[cfg(test)]
mod gauge_tests {
use super::*;
#[test]
fn pct_gauge_text_formats_percent_with_bar() {
let text = pct_gauge_text(Some(87), 10);
assert!(text.contains("87%"), "{text:?}");
let (fill, _) = crate::tui::glyphs::meter();
assert!(text.contains(fill), "expected bar glyph in {text:?}");
}
#[test]
fn pct_gauge_text_none_is_dash() {
assert_eq!(pct_gauge_text(None, 10), "-");
}
#[test]
fn gpu_col_widths_wide_returns_natural() {
const BAR: u16 = crate::tui::app::BAR_WIDTH as u16;
assert_eq!(
gpu_col_widths(200),
[9, 16, 16, 16, 5, 6, BAR, 7, BAR, 7, 6, 5]
);
}
#[test]
fn gpu_col_widths_narrow_fits_avail_and_respects_minimums() {
const MIN: [u16; 12] = [7, 8, 9, 9, 4, 4, 6, 6, 6, 6, 5, 4];
let widths = gpu_col_widths(100);
let total: u16 = widths.iter().sum::<u16>() + 11; assert!(total <= 100, "scaled widths too wide: {total}");
for (i, (&w, &min)) in widths.iter().zip(MIN.iter()).enumerate() {
assert!(w >= min, "col {i}: width {w} below min {min}");
}
}
#[test]
fn gpu_col_widths_below_min_floor_pins_every_column_at_min() {
const MIN: [u16; 12] = [7, 8, 9, 9, 4, 4, 6, 6, 6, 6, 5, 4];
assert_eq!(gpu_col_widths(80), MIN);
}
#[test]
fn scaled_col_widths_naturals_when_wide_fits_when_narrow() {
let natural = [10u16, 20, 6];
let mins = [4u16, 8, 3];
assert_eq!(scaled_col_widths(&natural, &mins, 100), natural);
let scaled = scaled_col_widths(&natural, &mins, 24);
let total: u16 = scaled.iter().sum::<u16>() + 2; assert!(total <= 24, "scaled widths too wide: {total}");
for (i, (&w, &min)) in scaled.iter().zip(mins.iter()).enumerate() {
assert!(w >= min, "col {i}: width {w} below min {min}");
}
}
}