use std::time::Duration;
use crate::lookout::app::CPU_CEILING_FLOOR;
use crate::lookout::pane_sheep::{scale_top, window};
use super::super::super::cell;
use super::super::layout::{CPU_ROWS, GUTTER, MEM_ROWS};
fn window_label(d: Duration) -> String {
let secs = d.as_secs();
format!("{}m{:02}s", secs / 60, secs % 60)
}
pub(super) fn cpu_header_text(history_len: usize, body_cells: usize) -> String {
let full = window_label(window(body_cells));
if history_len < body_cells {
format!(
"collecting \u{b7} {} of {full}",
window_label(window(history_len))
)
} else {
format!("{full}, one 2s sample per column")
}
}
pub(super) fn cpu_chart_rows(history: &[f32], body_cells: usize) -> Vec<String> {
let window_slice = &history[history.len().saturating_sub(body_cells)..];
let peak = window_slice.iter().copied().fold(0.0_f32, f32::max);
let ceiling = scale_top(f64::from(peak), f64::from(CPU_CEILING_FLOOR));
let bars = cell::chart(history, ceiling as f32, body_cells, CPU_ROWS);
gutter_lines(bars, ceiling, GutterCadence::Alternating, |value| {
format!("{value:.0}%")
})
}
pub(super) fn mem_header_text(max_memory: Option<u64>, window_peak: u64) -> String {
match max_memory {
Some(limit) => format!(
"rss same window \u{b7} ceiling {}",
crate::output::human_bytes(limit)
),
None => format!(
"rss same window \u{b7} no limit set \u{b7} scaled to peak {}",
crate::output::human_bytes(window_peak)
),
}
}
pub(super) fn mem_chart_rows(
history: &[u64],
max_memory: Option<u64>,
body_cells: usize,
) -> (Vec<String>, Option<usize>) {
let window_slice = &history[history.len().saturating_sub(body_cells)..];
let window_peak = window_slice.iter().copied().max().unwrap_or(0);
let peak_for_scale = max_memory.map_or(window_peak, |limit| limit.max(window_peak));
const MIB: f64 = (1u64 << 20) as f64;
let ceiling_mib = scale_top(peak_for_scale as f64 / MIB, 0.0);
let ceiling = ceiling_mib * MIB;
let samples: Vec<f32> = history.iter().map(|&bytes| bytes as f32).collect();
let bars = cell::chart(&samples, ceiling as f32, body_cells, MEM_ROWS);
let mut lines = gutter_lines(bars, ceiling, GutterCadence::EveryRow, |value| {
crate::output::human_bytes(value as u64)
});
let marked = max_memory.map(|limit| {
if ceiling <= 0.0 {
return 0;
}
let band = ceiling / MEM_ROWS as f64;
((ceiling - limit as f64) / band)
.round()
.clamp(0.0, (MEM_ROWS - 1) as f64) as usize
});
if let Some(row) = marked {
let limit = max_memory.unwrap_or_default();
let label = format!("{:>7} ", crate::output::human_bytes(limit));
lines[row] = format!("{label}{} ceiling", "\u{254c}".repeat(body_cells));
}
(lines, marked)
}
#[derive(Clone, Copy)]
enum GutterCadence {
Alternating,
EveryRow,
}
fn gutter_lines(
bars: Vec<String>,
ceiling: f64,
cadence: GutterCadence,
format_value: impl Fn(f64) -> String,
) -> Vec<String> {
let rows = bars.len();
let last = rows.saturating_sub(1);
bars.into_iter()
.enumerate()
.map(|(i, bar)| {
let labelled = match cadence {
GutterCadence::Alternating => i % 2 == 0,
GutterCadence::EveryRow => true,
};
let gutter = if i == last {
format!("{:>7} ", "0")
} else if labelled {
#[allow(clippy::cast_precision_loss)] let value = ceiling * (rows - i) as f64 / rows as f64;
format!("{:>7} ", format_value(value))
} else {
format!("{:>7} ", "|")
};
format!("{gutter}{bar}")
})
.collect()
}
pub(super) fn axis_row(body_cells: usize) -> String {
let label = "now";
let dashes = body_cells.saturating_sub(label.len());
format!("{}{}{label}", " ".repeat(GUTTER), "\u{2500}".repeat(dashes))
}
#[cfg(test)]
mod tests {
use super::super::chart_body_cells;
use super::*;
fn header_at(width: u16) -> String {
let body = chart_body_cells(width);
cpu_header_text(body, body)
}
fn header_with_samples(history_len: usize, body_cells: usize) -> String {
cpu_header_text(history_len, body_cells)
}
fn mem_rows_with_limit(max_memory: Option<u64>) -> (Vec<String>, Option<usize>) {
let history = [20 << 20, 25 << 20, 30 << 20, 40 << 20, 48 << 20];
mem_chart_rows(&history, max_memory, 20)
}
fn mem_header_with_limit(max_memory: Option<u64>, window_peak: u64) -> String {
mem_header_text(max_memory, window_peak)
}
#[test]
fn the_cpu_header_states_the_drawn_window() {
assert!(header_at(160).contains("4m40s, one 2s sample per column"));
assert!(header_at(140).contains("4m00s, one 2s sample per column"));
}
#[test]
fn a_partial_buffer_says_how_much_it_has() {
assert!(header_with_samples(35, 140).contains("collecting \u{b7} 1m10s of 4m40s"));
}
#[test]
fn the_memory_chart_labels_a_real_ceiling() {
let (rows, marked) = mem_rows_with_limit(Some(52 << 20));
assert_eq!(
marked,
Some(2),
"a 52M limit against this fixture's 48M peak scales to a 100M \
ceiling, 20M per row, so the marked row is nearest at 2, not \
the top"
);
assert!(rows[2].contains("ceiling"), "got {rows:?}");
assert!(
rows[2].contains("52.0M"),
"the marked row states the limit's own value, not the ladder \
row (60.0M) it happens to land nearest: {:?}",
rows[2]
);
}
#[test]
fn the_marked_row_is_the_nearest_one_not_the_floor() {
let history = [10 << 20; 20];
let (rows, marked) = mem_chart_rows(&history, Some(64 << 20), 20);
assert_eq!(marked, Some(2), "60.0M is nearer 64M than 80.0M is");
assert!(rows[2].contains("64.0M"), "got {:?}", rows[2]);
}
#[test]
fn the_memory_chart_states_its_substitute_denominator() {
let header = mem_header_with_limit(None, 48 << 20);
assert!(header.contains("no limit set"));
assert!(header.contains("scaled to peak"));
assert!(
!mem_rows_with_limit(None)
.0
.iter()
.any(|row| row.contains("ceiling"))
);
}
#[test]
fn the_gutter_labels_more_than_the_ends() {
let history = [10.0_f32, 20.0, 30.0, 40.0, 45.0];
let rows = cpu_chart_rows(&history, 20);
let labelled: Vec<&str> = rows
.iter()
.map(|row| row[..GUTTER].trim())
.filter(|cell| !cell.is_empty())
.collect();
assert!(
labelled.len() > 2,
"expected more than just the top and bottom label: {rows:?}"
);
assert!(
rows.iter().any(|row| row[..GUTTER].contains('|')),
"expected a bare tick between labels: {rows:?}"
);
}
#[test]
fn the_gutter_cadence_differs_by_chart() {
let (mem_rows, _) = mem_rows_with_limit(None);
let labelled_mem = mem_rows
.iter()
.filter(|row| {
let cell = row[..GUTTER].trim();
!cell.is_empty() && cell != "|"
})
.count();
assert_eq!(
labelled_mem, MEM_ROWS,
"every memory row should carry a label: {mem_rows:?}"
);
let cpu_history = [10.0_f32, 20.0, 30.0, 40.0, 45.0];
let cpu_rows = cpu_chart_rows(&cpu_history, 20);
let ticked_cpu = cpu_rows
.iter()
.filter(|row| row[..GUTTER].contains('|'))
.count();
assert!(
ticked_cpu > 0,
"the CPU gutter should still alternate with bare ticks: {cpu_rows:?}"
);
assert!(
ticked_cpu < CPU_ROWS,
"the CPU gutter should not label every row: {cpu_rows:?}"
);
}
}