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 ) -> io::Result<()> {
92 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 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 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}