1use super::config::{BenchmarkResult, DEFAULT_SAMPLE_COUNT, MeasureConfig, MeasureError};
4use super::sink::SampleSink;
5use fluxbench_ipc::{Sample, Throughput};
6#[cfg(feature = "async")]
7use std::sync::Arc;
8use std::sync::Mutex;
9#[cfg(feature = "async")]
10use tokio::runtime::Runtime;
11
12pub struct Bencher {
18 pub(super) config: MeasureConfig,
20 pub(super) samples: Vec<Sample>,
22 pub(super) warmup_iterations: u64,
24 pub(super) iters_per_sample: u64,
26 pub(super) truncated: bool,
28 pub(super) iter_called: bool,
30 pub(super) error: Option<MeasureError>,
32 #[cfg(feature = "async")]
34 pub(super) runtime: Option<Arc<Runtime>>,
35 pub(super) sink: Option<Mutex<Box<dyn SampleSink>>>,
38 pub(super) throughput: Option<Throughput>,
40}
41
42impl Bencher {
43 pub fn new(track_allocations: bool) -> Self {
45 Self::with_sample_count(track_allocations, DEFAULT_SAMPLE_COUNT)
46 }
47
48 pub fn with_config(config: MeasureConfig) -> Self {
52 let config = config.normalized();
53 Self {
54 samples: Vec::with_capacity(config.sample_count),
55 iters_per_sample: config.fixed_iters_per_sample.unwrap_or(0),
56 config,
57 warmup_iterations: 0,
58 truncated: false,
59 iter_called: false,
60 error: None,
61 #[cfg(feature = "async")]
62 runtime: None,
63 sink: None,
64 throughput: None,
65 }
66 }
67
68 pub fn throughput(&mut self, throughput: Throughput) {
73 self.throughput = Some(throughput);
74 }
75
76 pub fn with_sample_sink(mut self, sink: impl SampleSink + 'static) -> Self {
81 self.sink = Some(Mutex::new(Box::new(sink)));
82 self
83 }
84
85 pub(super) fn record_sample(&mut self, sample: Sample) {
87 self.samples.push(sample);
88 if let Some(sink) = self.sink.as_mut() {
89 let sink = match sink.get_mut() {
90 Ok(sink) => sink,
91 Err(poisoned) => poisoned.into_inner(),
92 };
93 sink.on_sample(&sample);
94 }
95 }
96
97 pub fn with_sample_count(track_allocations: bool, sample_count: usize) -> Self {
101 Self::with_config(MeasureConfig {
102 sample_count,
103 track_allocations,
104 ..MeasureConfig::default()
105 })
106 }
107
108 pub fn for_config(
112 track_allocations: bool,
113 sample_count: usize,
114 iters_per_sample: Option<u64>,
115 ) -> Self {
116 Self::with_config(MeasureConfig {
117 sample_count,
118 fixed_iters_per_sample: iters_per_sample,
119 track_allocations,
120 ..MeasureConfig::default()
121 })
122 }
123
124 pub fn with_fixed_iters_per_sample(mut self, iters: u64) -> Self {
128 let iters = iters.max(1);
129 self.config.fixed_iters_per_sample = Some(iters);
130 self.iters_per_sample = iters;
131 self
132 }
133
134 pub fn config(&self) -> &MeasureConfig {
136 &self.config
137 }
138
139 pub fn fixed_iters_per_sample(&self) -> Option<u64> {
141 self.config.fixed_iters_per_sample
142 }
143
144 pub fn target_samples(&self) -> usize {
146 self.config.sample_count
147 }
148
149 pub fn samples(&self) -> &[Sample] {
151 &self.samples
152 }
153
154 pub fn iteration_count(&self) -> u64 {
156 self.samples.iter().map(|s| s.iterations).sum()
157 }
158
159 pub(super) fn begin_iter(&mut self) -> bool {
163 if self.iter_called {
164 self.record_error(MeasureError::MultipleIterCalls);
165 return false;
166 }
167 self.iter_called = true;
168 true
169 }
170
171 pub fn record_error(&mut self, error: MeasureError) {
173 if self.error.is_none() {
174 self.error = Some(error);
175 }
176 }
177
178 pub fn finish(self) -> Result<BenchmarkResult, MeasureError> {
180 if let Some(error) = self.error {
181 return Err(error);
182 }
183 if !self.iter_called {
184 return Err(MeasureError::NoIterCall);
185 }
186 if self.samples.is_empty() {
187 return Err(MeasureError::NoSamples);
188 }
189
190 let iterations = self.samples.iter().map(|s| s.iterations).sum();
191 let total_time_ns = self
192 .samples
193 .iter()
194 .map(|s| s.duration_nanos * s.iterations as f64)
195 .sum::<f64>()
196 .round() as u64;
197
198 Ok(BenchmarkResult {
199 samples: self.samples,
200 iterations,
201 warmup_iterations: self.warmup_iterations,
202 total_time_ns,
203 iters_per_sample: self.iters_per_sample,
204 truncated: self.truncated,
205 throughput: self.throughput,
206 })
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use crate::bencher::config::MIN_SAMPLE_COUNT;
214 use std::sync::Arc;
215
216 #[test]
217 fn throughput_is_none_without_a_call() {
218 let result =
219 crate::bencher::run_benchmark_loop(Bencher::for_config(false, 2, Some(1)), |b| {
220 b.iter(|| 1_u64);
221 })
222 .expect("run succeeds");
223 assert_eq!(result.throughput, None);
224 }
225
226 #[test]
227 fn throughput_before_or_after_iter_reaches_the_result() {
228 let before =
229 crate::bencher::run_benchmark_loop(Bencher::for_config(false, 2, Some(1)), |b| {
230 b.throughput(Throughput::Bytes(64));
231 b.iter(|| 1_u64);
232 })
233 .expect("run succeeds");
234 assert_eq!(before.throughput, Some(Throughput::Bytes(64)));
235
236 let after =
237 crate::bencher::run_benchmark_loop(Bencher::for_config(false, 2, Some(1)), |b| {
238 b.iter(|| 1_u64);
239 b.throughput(Throughput::Elements(3));
240 })
241 .expect("run succeeds");
242 assert_eq!(after.throughput, Some(Throughput::Elements(3)));
243 }
244
245 #[test]
246 fn last_throughput_call_wins() {
247 let result =
248 crate::bencher::run_benchmark_loop(Bencher::for_config(false, 2, Some(1)), |b| {
249 b.throughput(Throughput::Bytes(64));
250 b.throughput(Throughput::Elements(8));
251 b.iter(|| 1_u64);
252 })
253 .expect("run succeeds");
254 assert_eq!(result.throughput, Some(Throughput::Elements(8)));
255 }
256
257 #[test]
258 fn sample_count_floor_is_two() {
259 assert_eq!(
260 Bencher::with_sample_count(false, 0).target_samples(),
261 MIN_SAMPLE_COUNT
262 );
263 assert_eq!(Bencher::with_sample_count(false, 1).target_samples(), 2);
264 assert_eq!(Bencher::with_sample_count(false, 2).target_samples(), 2);
265 }
266
267 #[test]
268 fn fixed_zero_rises_to_one() {
269 let bencher = Bencher::for_config(false, 10, Some(0));
270 assert_eq!(bencher.fixed_iters_per_sample(), Some(1));
271 let bencher = Bencher::new(false).with_fixed_iters_per_sample(0);
272 assert_eq!(bencher.fixed_iters_per_sample(), Some(1));
273 }
274
275 #[test]
276 fn finish_without_iter_is_error() {
277 let err = Bencher::new(false).finish().err();
278 assert_eq!(err, Some(MeasureError::NoIterCall));
279 }
280
281 #[test]
282 fn sink_receives_every_sample_in_order() {
283 let seen = Arc::new(Mutex::new(Vec::new()));
284 let sink_seen = Arc::clone(&seen);
285 let bencher =
286 Bencher::for_config(false, 5, Some(3)).with_sample_sink(move |sample: &Sample| {
287 sink_seen.lock().expect("lock").push(*sample);
288 });
289 let result = crate::bencher::run_benchmark_loop(bencher, |b| {
290 b.iter(|| std::hint::black_box(1u64 + 1));
291 })
292 .expect("run succeeds");
293 let seen = seen.lock().expect("lock");
294 assert_eq!(seen.len(), 5);
295 assert_eq!(seen.as_slice(), result.samples.as_slice());
296 }
297
298 #[test]
299 fn finish_reports_means_and_totals() {
300 let mut bencher = Bencher::with_sample_count(false, 2);
301 bencher.iter_called = true;
302 bencher.iters_per_sample = 4;
303 bencher.samples.push(Sample::new(25.25, 75.25, 8.0, 1.0, 4));
304 bencher.samples.push(Sample::new(10.0, 30.0, 0.0, 0.0, 4));
305 let result = bencher.finish().expect("finish succeeds");
306 assert_eq!(result.iterations, 8);
307 assert_eq!(result.total_time_ns, 141);
308 assert_eq!(result.iters_per_sample, 4);
309 assert!(!result.truncated);
310 }
311}