Skip to main content

radiate_core/stats/
fmt.rs

1use crate::stats::TagKind;
2use crate::{Metric, MetricSet, metric_names};
3use std::time::Duration;
4use std::{fmt::Write as _, io};
5
6pub fn sparkline(values: &[f32], width: usize) -> String {
7    if values.is_empty() || width == 0 {
8        return String::new();
9    }
10
11    let blocks = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
12    let (min, max) = values
13        .iter()
14        .fold((f32::INFINITY, f32::NEG_INFINITY), |(mn, mx), &v| {
15            (mn.min(v), mx.max(v))
16        });
17
18    let span = (max - min).max(1e-12);
19    let step = (values.len() as f32 / width as f32).max(1.0);
20    let mut out = String::with_capacity(width);
21
22    let mut idx = 0.0;
23    for _ in 0..width {
24        let i = f32::floor(idx) as usize;
25        let v = *values.get(i.min(values.len() - 1)).unwrap_or(&min);
26        let level = (((v - min) / span) * ((blocks.len() - 1) as f32)).round() as usize;
27        out.push(blocks[level.min(blocks.len() - 1)]);
28        idx += step;
29    }
30    out
31}
32
33pub fn render_dashboard(metrics: &MetricSet) -> io::Result<String> {
34    let mut out = String::new();
35
36    let mut push_val = |name: &'static str, label: &str| {
37        if let Some(m) = metrics.get(name) {
38            if let Some(mu) = m.value_mean() {
39                write!(out, "  {label}: {:.3}", mu).unwrap();
40            } else if m.count() > 0 {
41                write!(out, "  {label}: {:.3}", m.last_value()).unwrap();
42            }
43        }
44    };
45
46    push_val(metric_names::CARRYOVER_RATE, "carryover");
47    push_val(metric_names::DIVERSITY_RATIO, "diversity");
48
49    let mut push_int = |name: &'static str, label: &str| {
50        if let Some(m) = metrics.get(name) {
51            write!(out, "  {label}: {}", m.last_value() as i64).unwrap();
52        }
53    };
54
55    push_int(metric_names::UNIQUE_MEMBERS, "unique_members");
56    push_int(metric_names::UNIQUE_SCORES, "unique_scores");
57
58    if let Some(m) = metrics.get(metric_names::BEST_SCORE_IMPROVEMENT) {
59        write!(out, "  improvements: {}", m.count() as i64).unwrap();
60    }
61
62    if let Some(m) = metrics.get(metric_names::TIME) {
63        if let Some(mu) = m.time_mean() {
64            write!(out, "  iter_time(mean): {}", fmt_duration(mu)).unwrap();
65        }
66    }
67
68    Ok(if out.is_empty() {
69        "—".into()
70    } else {
71        out.replace('\n', "")
72    })
73}
74
75fn render_table_header(mut out: String) -> io::Result<String> {
76    writeln!(
77        out,
78        "{:<24} | {:<6} | {:<10} | {:<10} | {:<10} | {:<6} | {:<12} | {:<10} | {:<10} | {:<10} | {:<10}",
79        "Name", "Type", "Mean", "Min", "Max", "N", "Total", "StdDev", "Skew", "Kurt", "Entr"
80    ).unwrap();
81    writeln!(out, "{}", "-".repeat(145)).unwrap();
82    Ok(out)
83}
84
85pub fn render_metric_rows_full(
86    out: &mut String,
87    name: &str,
88    m: &Metric,
89    tag: TagKind,
90    // include_spark: bool,
91) -> io::Result<()> {
92    // let inner = m.inner();
93
94    if name == super::set::METRIC_SET {
95        if let Some(s) = m.statistic() {
96            writeln!(
97                out,
98                "Metric Set [metrics: {}, updates: {:.0}]",
99                s.sum(),
100                s.sum()
101            )
102            .unwrap();
103        }
104    }
105
106    // Value row
107    if let Some(stat) = m.statistic()
108        && tag == TagKind::Statistic
109    {
110        writeln!(
111            out,
112            "{:<24} | {:<6} | {:<10.3} | {:<10.3} | {:<10.3} | {:<6} | {:<12} | {:<10.3} | {:<10.3} | {:<10.3} | {:<10}",
113            name,
114            "value",
115            stat.mean(),
116            stat.min(),
117            stat.max(),
118            stat.count(),
119            "-",
120            stat.std_dev(),
121            stat.skewness(),
122            stat.kurtosis(),
123            "-",
124        ).unwrap();
125    }
126
127    // Time row
128    if let Some(t) = m.time_statistic()
129        && tag == TagKind::Time
130    {
131        writeln!(
132            out,
133            "{:<24} | {:<6} | {:<10} | {:<10} | {:<10} | {:<6} | {:<12} | {:<10} | {:<10} | {:<10} | {:<10}",
134            name,
135            "time",
136            fmt_duration(t.mean()),
137            fmt_duration(t.min()),
138            fmt_duration(t.max()),
139            t.count(),
140            fmt_duration(t.sum()),
141            fmt_duration(t.standard_deviation()),
142            "-",
143            "-",
144            "-",
145        ).unwrap();
146    }
147
148    Ok(())
149}
150
151fn render_tagged(ms: &MetricSet, tag: TagKind, title: &str) -> io::Result<String> {
152    let mut out = String::new();
153    writeln!(out, "== {} ==", title).unwrap();
154    out = render_table_header(out)?;
155
156    let mut items: Vec<_> = ms.iter_tagged(tag).collect();
157    items.sort_by(|a, b| a.0.cmp(b.0));
158
159    for (name, m) in items {
160        render_metric_rows_full(&mut out, name, m, tag)?;
161    }
162
163    Ok(out)
164}
165
166pub fn render_full(metrics: &MetricSet) -> io::Result<String> {
167    let mut out = String::new();
168
169    let dash = render_dashboard(metrics)?;
170    writeln!(out, "[metrics]{}", dash).unwrap();
171
172    let generation = render_tagged(metrics, TagKind::Statistic, "Statistics")?;
173    writeln!(out, "\n{}", generation).unwrap();
174
175    let life = render_tagged(metrics, TagKind::Time, "Times")?;
176    writeln!(out, "\n{}", life).unwrap();
177
178    Ok(out)
179}
180
181pub fn fmt_duration(d: Duration) -> String {
182    let ns = d.as_nanos();
183    if ns == 0 {
184        "0ns".into()
185    } else if ns < 1_000 {
186        format!("{ns}ns")
187    } else if ns < 1_000_000 {
188        format!("{:.3}µs", ns as f64 / 1e3)
189    } else if ns < 1_000_000_000 {
190        format!("{:.3}ms", ns as f64 / 1e6)
191    } else {
192        format!("{:.3}s", ns as f64 / 1e9)
193    }
194}