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