common/
histogram_panel.rs1use hdrhistogram::Histogram;
10
11pub struct PanelUnit<'a> {
15 pub label: &'a str,
16 pub histogram: &'a Histogram<u64>,
17 pub interval: std::time::Duration,
18}
19
20const ROWS_PER_UNIT: usize = 8;
24const BAR_WIDTH: usize = 24;
26
27const SEPARATOR: &str = "-----------------------";
28
29#[must_use]
33pub fn render_histogram_panel(units: &[PanelUnit]) -> String {
34 let visible: Vec<_> = units.iter().filter(|u| !u.histogram.is_empty()).collect();
35 if visible.is_empty() {
36 return String::new();
37 }
38 let mut out = String::new();
39 out.push('\n');
40 out.push_str(SEPARATOR);
41 for unit in &visible {
42 out.push('\n');
43 render_unit(&mut out, unit);
44 }
45 out
46}
47
48fn render_unit(out: &mut String, unit: &PanelUnit) {
49 let n = unit.histogram.len();
50 out.push_str(&format!(
51 "{label} distribution (last {secs:.1}s, n={n}):\n",
52 label = unit.label,
53 secs = unit.interval.as_secs_f64(),
54 n = n,
55 ));
56 let bands: Vec<(u64, u64)> = unit
60 .histogram
61 .iter_log(1, 2.0)
62 .map(|v| (v.value_iterated_to(), v.count_since_last_iteration()))
63 .filter(|&(_, c)| c > 0)
64 .collect();
65 if bands.is_empty() {
66 out.push_str(" (no samples)\n");
67 return;
68 }
69 let mode_idx = bands
70 .iter()
71 .enumerate()
72 .max_by_key(|(_, (_, c))| *c)
73 .map(|(i, _)| i)
74 .unwrap_or(0);
75 let half = ROWS_PER_UNIT / 2;
76 let lo = mode_idx.saturating_sub(half);
77 let hi = (lo + ROWS_PER_UNIT).min(bands.len());
78 let lo = hi.saturating_sub(ROWS_PER_UNIT);
79 let visible_bands = &bands[lo..hi];
80 let max_count = visible_bands.iter().map(|&(_, c)| c).max().unwrap_or(1);
81 for &(value, count) in visible_bands {
82 let bar_len = ((count * BAR_WIDTH as u64) / max_count) as usize;
83 let bar = "█".repeat(bar_len);
84 out.push_str(&format!(
85 " {value:>8} {bar:<bar_width$} {count}\n",
86 value = format_micros(value),
87 bar = bar,
88 bar_width = BAR_WIDTH,
89 count = count,
90 ));
91 }
92}
93
94fn format_micros(v: u64) -> String {
95 if v < 1_000 {
96 format!("{v}µs")
97 } else if v < 1_000_000 {
98 format!("{:.1}ms", v as f64 / 1_000.0)
99 } else {
100 format!("{:.1}s", v as f64 / 1_000_000.0)
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 fn make_hist(samples: &[u64]) -> Histogram<u64> {
109 let mut h = Histogram::<u64>::new_with_bounds(1, 3_600_000_000, 3).unwrap();
110 for &v in samples {
111 h.record(v).unwrap();
112 }
113 h
114 }
115
116 #[test]
117 fn empty_units_produce_empty_output() {
118 let h = make_hist(&[]);
119 let units = [PanelUnit {
120 label: "src-stat",
121 histogram: &h,
122 interval: std::time::Duration::from_secs(1),
123 }];
124 assert_eq!(render_histogram_panel(&units), "");
125 }
126
127 #[test]
128 fn renders_per_unit_header_and_bands() {
129 let mut samples = vec![100u64; 70];
133 samples.extend(vec![200u64; 30]);
134 let h = make_hist(&samples);
135 let units = [PanelUnit {
136 label: "src-stat",
137 histogram: &h,
138 interval: std::time::Duration::from_secs(1),
139 }];
140 let out = render_histogram_panel(&units);
141 assert!(out.contains("src-stat distribution"), "got: {out}");
142 assert!(out.contains("n=100"), "got: {out}");
143 assert!(
144 out.contains('█'),
145 "expected at least one bar block, got: {out}"
146 );
147 }
148
149 #[test]
150 fn empty_unit_is_skipped_among_active_units() {
151 let h_empty = make_hist(&[]);
152 let h_full = make_hist(&[100, 200, 300]);
153 let units = [
154 PanelUnit {
155 label: "idle",
156 histogram: &h_empty,
157 interval: std::time::Duration::from_secs(1),
158 },
159 PanelUnit {
160 label: "active",
161 histogram: &h_full,
162 interval: std::time::Duration::from_secs(1),
163 },
164 ];
165 let out = render_histogram_panel(&units);
166 assert!(!out.contains("idle"), "idle unit must be hidden: {out}");
167 assert!(out.contains("active"), "active unit must show: {out}");
168 }
169}