use crate::stats::TagType;
use crate::{Metric, MetricSet, metric_names};
use std::time::Duration;
use std::{fmt::Write as _, io};
pub fn sparkline(values: &[f32], width: usize) -> String {
if values.is_empty() || width == 0 {
return String::new();
}
let blocks = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
let (min, max) = values
.iter()
.fold((f32::INFINITY, f32::NEG_INFINITY), |(mn, mx), &v| {
(mn.min(v), mx.max(v))
});
let span = (max - min).max(1e-12);
let step = (values.len() as f32 / width as f32).max(1.0);
let mut out = String::with_capacity(width);
let mut idx = 0.0;
for _ in 0..width {
let i = f32::floor(idx) as usize;
let v = *values.get(i.min(values.len() - 1)).unwrap_or(&min);
let level = (((v - min) / span) * ((blocks.len() - 1) as f32)).round() as usize;
out.push(blocks[level.min(blocks.len() - 1)]);
idx += step;
}
out
}
pub fn render_dashboard(metrics: &MetricSet) -> io::Result<String> {
let mut out = String::new();
let mut push_val = |name: &'static str, label: &str| {
if let Some(m) = metrics.get(name) {
let mu = m.mean();
write!(out, " {label}: {:.3}", mu).unwrap();
}
};
push_val(metric_names::CARRYOVER_RATE, "carryover");
push_val(metric_names::DIVERSITY_RATIO, "diversity");
let mut push_int = |name: &'static str, label: &str| {
if let Some(m) = metrics.get(name) {
write!(out, " {label}: {}", m.last_value() as i64).unwrap();
}
};
push_int(metric_names::UNIQUE_MEMBERS, "unique_members");
push_int(metric_names::UNIQUE_SCORES, "unique_scores");
if let Some(m) = metrics.get(metric_names::BEST_SCORE_IMPROVEMENT) {
write!(out, " improvements: {}", m.count() as i64).unwrap();
}
if let Some(m) = metrics.get(metric_names::TIME) {
if let Some(mu) = m.times().and_then(|t| t.mean()) {
write!(out, " iter_time(mean): {}", fmt_duration(mu)).unwrap();
}
}
Ok(if out.is_empty() {
"—".into()
} else {
out.replace('\n', "")
})
}
fn render_table_header(mut out: String) -> io::Result<String> {
writeln!(
out,
"{:<26} | {:<6} | {:<10} | {:<10} | {:<10} | {:<6} | {:<12} | {:<10} | {:<10} | {:<10} | {:<10}",
"Name", "Type", "Mean", "Min", "Max", "N", "Total", "StdDev", "Skew", "Kurt", "Entr"
).unwrap();
writeln!(out, "{}", "-".repeat(145)).unwrap();
Ok(out)
}
pub fn render_metric_rows_full(
out: &mut String,
name: &str,
m: &Metric,
tag: TagType,
) -> io::Result<()> {
if let Some(dist) = m.distributions()
&& tag == TagType::Distribution
{
writeln!(
out,
"{:<26} | {:<6} | {:<10.3} | {:<10.3} | {:<10.3} | {:<6} | {:<12.3} | {:<10.3} | {:<10.3} | {:<10.3} | {:<10.3}",
name,
"dist",
dist.mean().unwrap_or_default(),
dist.min().unwrap_or_default(),
dist.max().unwrap_or_default(),
dist.count(),
dist.sum().unwrap() / 1_000.0, dist.stddev().unwrap_or(0.0),
dist.skewness().unwrap_or(0.0),
dist.kurtosis().unwrap_or(0.0),
"-",
).unwrap();
}
if let Some(stat) = m.stats()
&& tag == TagType::Statistic
{
writeln!(
out,
"{:<26} | {:<6} | {:<10.3} | {:<10.3} | {:<10.3} | {:<6} | {:<12.3} | {:<10.3} | {:<10.3} | {:<10.3} | {:<10}",
name,
"value",
stat.mean().unwrap_or_default(),
stat.min().unwrap_or_default(),
stat.max().unwrap_or_default(),
stat.count(),
stat.sum().unwrap() / 1_000.0, stat.stddev().unwrap_or(0.0),
stat.skewness().unwrap_or(0.0),
stat.kurtosis().unwrap_or(0.0),
"-",
).unwrap();
}
if let Some(t) = m.times()
&& tag == TagType::Time
{
writeln!(
out,
"{:<26} | {:<6} | {:<10} | {:<10} | {:<10} | {:<6} | {:<12} | {:<10} | {:<10} | {:<10} | {:<10}",
name,
"time",
fmt_duration(t.mean().unwrap_or_default()),
fmt_duration(t.min().unwrap_or_default()),
fmt_duration(t.max().unwrap_or_default()),
t.count(),
fmt_duration(t.sum().unwrap_or_default()),
fmt_duration(t.stddev().unwrap_or_default()),
"-",
"-",
"-",
).unwrap();
}
Ok(())
}
fn render_tagged(ms: &MetricSet, tag: TagType, title: &str) -> io::Result<String> {
let mut out = String::new();
writeln!(out, "== {} ==", title).unwrap();
out = render_table_header(out)?;
let mut items: Vec<_> = ms.iter_tagged(tag).collect();
items.sort_by(|a, b| a.0.cmp(b.0));
for (name, m) in items {
render_metric_rows_full(&mut out, name, m, tag)?;
}
Ok(out)
}
pub fn render_full(metrics: &MetricSet) -> io::Result<String> {
let mut out = String::new();
let summary = metrics.summary();
let dash = render_dashboard(metrics)?;
writeln!(
out,
"----- Metrics ----- ({} :: {}) \n{}",
summary.metrics, summary.updates, dash
)
.unwrap();
let dist = render_tagged(metrics, TagType::Distribution, "Distributions")?;
writeln!(out, "\n{}", dist).unwrap();
let generation = render_tagged(metrics, TagType::Statistic, "Statistics")?;
writeln!(out, "\n{}", generation).unwrap();
let life = render_tagged(metrics, TagType::Time, "Times")?;
writeln!(out, "\n{}", life).unwrap();
Ok(out)
}
pub fn fmt_duration(d: Duration) -> String {
let ns = d.as_nanos();
if ns == 0 {
"0ns".into()
} else if ns < 1_000 {
format!("{ns}ns")
} else if ns < 1_000_000 {
format!("{:.3}µs", ns as f64 / 1e3)
} else if ns < 1_000_000_000 {
format!("{:.3}ms", ns as f64 / 1e6)
} else {
format!("{:.3}s", ns as f64 / 1e9)
}
}