alux_bench_direct/
measure.rs1use alux_bench::{
4 BenchAlg, BenchCase, BenchCases, BenchGroup, BenchRounds, BenchRoutine, BenchSampling, BenchSentRoutine,
5 BenchStated, MeasureBenchAlg,
6};
7use core::time::Duration;
8use derive_new::new as New;
9use std::io::{IsTerminal, Write as _, stderr};
10use std::thread;
11use std::time::Instant;
12
13const LEAST: BenchRounds = BenchRounds::new(1);
15
16const COLUMN: usize = 32;
18
19const SAID: usize = 96;
21
22const SAYING: &str = "\x1b[36m";
24
25const PLAIN: &str = "\x1b[0m";
27
28#[derive(Clone, Debug, Eq, PartialEq)]
30pub struct BenchMeasured {
31 case: BenchCase,
32 rounds: BenchRounds,
33 samples: Vec<Duration>,
34}
35
36impl BenchMeasured {
37 pub const fn case(&self) -> BenchCase {
39 self.case
40 }
41
42 pub fn said(&self) {
44 say_no_more();
45 if let Some(line) = line(self, COLUMN) {
46 println!("{line}");
47 }
48 }
49
50 pub const fn rounds(&self) -> BenchRounds {
52 self.rounds
53 }
54
55 pub fn samples(&self) -> &[Duration] {
57 &self.samples
58 }
59
60 pub fn spread(&self) -> Option<(Duration, Duration, Duration)> {
64 let mut sorted = self.samples.iter().map(|sample| *sample / self.per_round()).collect::<Vec<_>>();
65 sorted.sort_unstable();
66 let shortest = *sorted.first()?;
67 let longest = *sorted.last()?;
68
69 Some((shortest, sorted[sorted.len() / 2], longest))
70 }
71
72 fn per_round(&self) -> u32 {
74 u32::try_from(self.rounds.count()).unwrap_or(u32::MAX).max(1)
75 }
76}
77
78#[derive(Clone, Debug, New)]
83pub struct DirectBench {
84 sampling: BenchSampling,
85 #[new(default)]
86 only: Option<String>,
87}
88
89impl DirectBench {
90 pub fn from_args(sampling: BenchSampling) -> Self {
95 let only = std::env::args().skip(1).find(|argument| !argument.starts_with('-'));
96
97 only.map_or_else(|| Self::new(sampling), |only| Self::new(sampling).only(only))
98 }
99
100 #[must_use]
102 pub fn only(mut self, only: impl Into<String>) -> Self {
103 self.only = Some(only.into());
104
105 self
106 }
107
108 fn measuring(&self, case: BenchCase, routine: &mut dyn FnMut(BenchRounds) -> Duration) -> Option<BenchMeasured> {
113 if !self.stating(case) {
114 return None;
115 }
116
117 let each = self.rounds_of_a_sample(routine);
118 let samples = (0..self.sampling.samples()).map(|_| routine(each)).collect();
119 Some(BenchMeasured { case, rounds: each, samples })
120 }
121
122 fn stating(&self, case: BenchCase) -> bool {
124 self.only.as_ref().is_none_or(|only| name(case).contains(only.as_str()))
125 }
126
127 fn rounds_of_a_sample(&self, routine: &mut dyn FnMut(BenchRounds) -> Duration) -> BenchRounds {
133 let Some(spend) = self.sampling.spend() else {
134 return LEAST;
135 };
136 let warming = Instant::now();
137 let _ = routine(LEAST);
138 let cost = warming.elapsed();
139 let Ok(fits) = u64::try_from(spend.as_nanos() / cost.as_nanos().max(1)) else {
140 return LEAST;
141 };
142
143 BenchRounds::new(fits.max(1))
144 }
145}
146
147impl BenchAlg for DirectBench {
148 type Bench = BenchStated<'static>;
149
150 fn nothing(&self) -> Self::Bench {
152 BenchStated::new()
153 }
154
155 fn group(&self, group: &'static str, cases: BenchCases<'static>) -> Self::Bench {
156 BenchStated::new().group(BenchGroup::new(group, cases))
157 }
158
159 fn then(&self, first: Self::Bench, next: Self::Bench) -> Self::Bench {
160 first.then(next)
161 }
162}
163
164impl MeasureBenchAlg for DirectBench {
165 type Bench = BenchStated<'static>;
166
167 fn measure(&mut self, bench: Self::Bench) {
169 for group in bench.into_groups() {
170 let named = group.group();
171 match group.cases() {
172 BenchCases::OneAtATime(cases) => self.one_at_a_time(named, cases),
173 BenchCases::Together(cases) => self.together(named, cases),
174 }
175 }
176
177 say_no_more();
178 }
179}
180
181impl DirectBench {
182 fn one_at_a_time(&self, group: &'static str, cases: Vec<(&'static str, BenchRoutine<'static>)>) {
184 for (subject, mut routine) in cases {
185 let case = BenchCase::new(group, subject);
186 if !self.stating(case) {
187 continue;
188 }
189 say(&format!("measuring {}", name(case)));
190 if let Some(measured) = self.measuring(case, &mut routine) {
191 measured.said();
192 }
193 }
194 }
195
196 fn together(&self, group: &'static str, cases: Vec<(&'static str, BenchSentRoutine)>) {
202 let cases = cases
203 .into_iter()
204 .map(|(subject, routine)| (BenchCase::new(group, subject), routine))
205 .filter(|(case, _)| self.stating(*case))
206 .collect::<Vec<_>>();
207 if cases.is_empty() {
208 return;
209 }
210
211 let stated = cases.len();
212 say(&measuring_at_once(stated, group));
213 thread::scope(|running| {
214 let running = cases
215 .into_iter()
216 .map(|(case, mut routine)| running.spawn(move || self.measuring(case, &mut routine)))
217 .collect::<Vec<_>>();
218
219 for (done, case) in running.into_iter().enumerate() {
222 if let Some(measured) = case.join().expect("the case measured") {
223 measured.said();
224 }
225
226 let waiting = stated - done - 1;
229 if waiting > 0 {
230 say(&measuring_at_once(waiting, group));
231 }
232 }
233 });
234 }
235}
236
237fn measuring_at_once(cases: usize, group: &str) -> String {
239 let many = if cases == 1 { "case" } else { "cases" };
240
241 format!("measuring {cases} {many} of {group} at the same time")
242}
243
244fn say(saying: &str) {
250 if stderr().is_terminal() {
251 eprint!("\r{SAYING}{saying:SAID$}{PLAIN}\r");
252 let _ = stderr().flush();
253 } else {
254 eprintln!("{saying}");
255 }
256}
257
258fn say_no_more() {
260 if stderr().is_terminal() {
261 eprint!("\r{:SAID$}\r", "");
262 let _ = stderr().flush();
263 }
264}
265
266fn line(measured: &BenchMeasured, width: usize) -> Option<String> {
268 let (shortest, median, longest) = measured.spread()?;
269 let samples = measured.samples().len();
270 let each = measured.rounds().count();
271 let case = name(measured.case());
272
273 Some(format!("{case:width$} {samples:>3} x {each:<6} rounds {median:>12.4?} [{shortest:.4?} {longest:.4?}]"))
274}
275
276fn name(case: BenchCase) -> String {
278 format!("{}/{}", case.group(), case.subject())
279}