1#[derive(Clone, Copy, Debug, PartialEq)]
10pub struct Measured {
11 pub bytes_out: u64,
12 pub compress_ns: u64,
13 pub decompress_ns: u64,
14}
15
16#[derive(Clone, Debug, PartialEq)]
18pub enum Outcome {
19 Ok(Measured),
20 Failed(String),
21}
22
23#[derive(Clone, Debug, PartialEq)]
27pub struct Row {
28 pub format: &'static str,
29 pub level: Option<i32>,
30 pub backend: &'static str,
31 pub outcome: Outcome,
32}
33
34#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
36pub struct Winners {
37 pub smallest: Option<usize>,
38 pub fastest_compress: Option<usize>,
39 pub fastest_decompress: Option<usize>,
40 pub balance: Option<usize>,
41}
42
43fn measured(row: &Row) -> Option<&Measured> {
44 match &row.outcome {
45 Outcome::Ok(m) => Some(m),
46 Outcome::Failed(_) => None,
47 }
48}
49
50fn ratio(best: u64, value: u64) -> f64 {
52 best.max(1) as f64 / value.max(1) as f64
53}
54
55pub fn balance_scores(rows: &[Row]) -> Vec<Option<f64>> {
58 let best = |f: fn(&Measured) -> u64| rows.iter().filter_map(measured).map(f).min();
59 let (Some(b_out), Some(b_c), Some(b_d)) = (
60 best(|m| m.bytes_out),
61 best(|m| m.compress_ns),
62 best(|m| m.decompress_ns),
63 ) else {
64 return vec![None; rows.len()];
65 };
66 rows.iter()
67 .map(|row| {
68 measured(row).map(|m| {
69 (ratio(b_out, m.bytes_out)
70 + ratio(b_c, m.compress_ns)
71 + ratio(b_d, m.decompress_ns))
72 / 3.0
73 })
74 })
75 .collect()
76}
77
78pub fn winners(rows: &[Row]) -> Winners {
81 let scores = balance_scores(rows);
82 let by_min = |key: fn(&Measured) -> u64| {
83 rows.iter()
84 .enumerate()
85 .filter_map(|(i, r)| measured(r).map(|m| (i, m)))
86 .min_by_key(|(i, m)| (key(m), m.bytes_out, *i))
87 .map(|(i, _)| i)
88 };
89 let balance = rows
90 .iter()
91 .enumerate()
92 .filter_map(|(i, r)| Some((i, measured(r)?, scores[i]?)))
93 .reduce(|best, cand| {
94 if cand.2 > best.2 || (cand.2 == best.2 && cand.1.bytes_out < best.1.bytes_out) {
97 cand
98 } else {
99 best
100 }
101 })
102 .map(|(i, _, _)| i);
103 Winners {
104 smallest: by_min(|m| m.bytes_out),
105 fastest_compress: by_min(|m| m.compress_ns),
106 fastest_decompress: by_min(|m| m.decompress_ns),
107 balance,
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 fn ok(bytes_out: u64, compress_ns: u64, decompress_ns: u64) -> Row {
116 Row {
117 format: "x",
118 level: Some(1),
119 backend: "pure",
120 outcome: Outcome::Ok(Measured {
121 bytes_out,
122 compress_ns,
123 decompress_ns,
124 }),
125 }
126 }
127
128 fn failed() -> Row {
129 Row {
130 format: "x",
131 level: None,
132 backend: "pure",
133 outcome: Outcome::Failed("boom".into()),
134 }
135 }
136
137 fn abc() -> Vec<Row> {
138 vec![ok(100, 10, 10), ok(50, 20, 20), ok(200, 5, 40)]
139 }
140
141 fn close(got: Option<f64>, want: f64) {
142 let got = got.expect("a score");
143 assert!((got - want).abs() < 1e-9, "{got} vs {want}");
144 }
145
146 #[test]
147 fn balance_weights_the_three_metrics_equally() {
148 let rows = abc();
149 let s = balance_scores(&rows);
150 close(s[0], (0.5 + 0.5 + 1.0) / 3.0);
151 close(s[1], (1.0 + 0.25 + 0.5) / 3.0);
152 close(s[2], (0.25 + 1.0 + 0.25) / 3.0);
153 assert_eq!(winners(&rows).balance, Some(0));
154 }
155
156 #[test]
157 fn winners_name_each_column() {
158 let w = winners(&abc());
159 assert_eq!(w.smallest, Some(1));
160 assert_eq!(w.fastest_compress, Some(2));
161 assert_eq!(w.fastest_decompress, Some(0));
162 }
163
164 #[test]
165 fn failed_rows_never_score_or_win() {
166 let mut rows = abc();
167 rows.insert(0, failed());
168 assert_eq!(balance_scores(&rows)[0], None);
169 let w = winners(&rows);
170 for idx in [
171 w.smallest,
172 w.fastest_compress,
173 w.fastest_decompress,
174 w.balance,
175 ] {
176 assert!(idx.expect("a winner") > 0);
177 }
178 }
179
180 #[test]
181 fn ties_go_to_the_smaller_output() {
182 let rows = vec![ok(100, 10, 10), ok(60, 10, 10)];
183 assert_eq!(winners(&rows).fastest_compress, Some(1));
184 assert_eq!(winners(&rows).fastest_decompress, Some(1));
185 assert_eq!(winners(&rows).balance, Some(1));
186 }
187
188 #[test]
191 fn a_balance_tie_goes_to_the_smaller_output() {
192 let rows = vec![ok(100, 10, 40), ok(50, 20, 40)];
193 let s = balance_scores(&rows);
194 assert_eq!(s[0], s[1], "a real tie");
195 assert_eq!(winners(&rows).balance, Some(1));
196 }
197
198 #[test]
199 fn ties_on_everything_go_to_the_earlier_row() {
200 let rows = vec![ok(7, 7, 7), ok(7, 7, 7)];
201 let w = winners(&rows);
202 assert_eq!(w.smallest, Some(0));
203 assert_eq!(w.balance, Some(0));
204 }
205
206 #[test]
207 fn all_failed_gives_no_winners() {
208 let rows = vec![failed(), failed()];
209 assert_eq!(winners(&rows), Winners::default());
210 assert_eq!(balance_scores(&rows), vec![None, None]);
211 assert_eq!(winners(&[]), Winners::default());
212 }
213
214 #[test]
215 fn zero_values_do_not_divide_by_zero() {
216 let rows = vec![ok(10, 10, 10), ok(0, 0, 0)];
217 for s in balance_scores(&rows) {
218 assert!(s.expect("a score").is_finite());
219 }
220 assert_eq!(balance_scores(&rows)[1], Some(1.0));
222 let w = winners(&rows);
223 assert_eq!(w.smallest, Some(1));
224 assert_eq!(w.fastest_compress, Some(1));
225 assert_eq!(w.fastest_decompress, Some(1));
226 assert_eq!(w.balance, Some(1));
227 }
228}