Skip to main content

fluxbench_core/bencher/
state.rs

1//! The `Bencher` struct: construction, accessors, and result assembly.
2
3use 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
12/// The Bencher provides iteration control for benchmarks.
13///
14/// The benchmark body calls exactly one `iter*` method once.
15/// That method runs warmup and measurement in full before it returns.
16/// Each sample holds per-iteration means over one batch of calls.
17pub struct Bencher {
18    /// Normalized measurement config
19    pub(super) config: MeasureConfig,
20    /// Completed samples. Capacity is `sample_count`. Warmup stores no per-call data.
21    pub(super) samples: Vec<Sample>,
22    /// Routine calls in the warmup phase
23    pub(super) warmup_iterations: u64,
24    /// Routine calls per measurement sample
25    pub(super) iters_per_sample: u64,
26    /// `true` when the hard deadline or a stop request cut measurement short
27    pub(super) truncated: bool,
28    /// `true` once the body called an `iter*` method
29    pub(super) iter_called: bool,
30    /// First error the body caused. `finish` reports it.
31    pub(super) error: Option<MeasureError>,
32    /// Runtime for `iter_async`. `None` until attached or built.
33    #[cfg(feature = "async")]
34    pub(super) runtime: Option<Arc<Runtime>>,
35    /// Receives each sample when it is recorded. The mutex keeps `Bencher: Sync`.
36    /// The engine reaches it through `&mut self`, so it never locks.
37    pub(super) sink: Option<Mutex<Box<dyn SampleSink>>>,
38    /// Work per call the body set with `throughput`. The last call wins.
39    pub(super) throughput: Option<Throughput>,
40}
41
42impl Bencher {
43    /// Create a Bencher with the default config and the given allocation tracking.
44    pub fn new(track_allocations: bool) -> Self {
45        Self::with_sample_count(track_allocations, DEFAULT_SAMPLE_COUNT)
46    }
47
48    /// Create a Bencher from a measurement config.
49    ///
50    /// The config is normalized first: see `MeasureConfig::normalized`.
51    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    /// Set the work one routine call processes. Reports turn it into a rate per second.
69    ///
70    /// Call it anywhere in the body. The last call wins.
71    /// It overrides `throughput_bytes` or `throughput_elements` on `#[flux::bench]` and `#[flux::group]`.
72    pub fn throughput(&mut self, throughput: Throughput) {
73        self.throughput = Some(throughput);
74    }
75
76    /// Send each recorded sample to `sink` during measurement.
77    ///
78    /// The sink runs between samples, outside every timed region.
79    /// Samples still land in the result as before.
80    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    /// Record one sample and pass it to the sink.
86    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    /// Create a Bencher that targets `sample_count` samples. Other values are defaults.
98    ///
99    /// The count has no upper cap. Values below `MIN_SAMPLE_COUNT` rise to it.
100    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    /// Create a Bencher from sample values. Times and iteration limits are defaults.
109    ///
110    /// `Some(n)` iterations per sample selects fixed mode. `None` selects time-based mode.
111    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    /// Fix the iterations per sample. This selects fixed mode: no warmup, no estimation.
125    ///
126    /// A value of 0 rises to 1.
127    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    /// The normalized measurement config
135    pub fn config(&self) -> &MeasureConfig {
136        &self.config
137    }
138
139    /// Fixed iterations per sample, if fixed mode is on
140    pub fn fixed_iters_per_sample(&self) -> Option<u64> {
141        self.config.fixed_iters_per_sample
142    }
143
144    /// Target sample count
145    pub fn target_samples(&self) -> usize {
146        self.config.sample_count
147    }
148
149    /// Collected samples
150    pub fn samples(&self) -> &[Sample] {
151        &self.samples
152    }
153
154    /// Routine calls in the measurement phase so far. Warmup calls are excluded.
155    pub fn iteration_count(&self) -> u64 {
156        self.samples.iter().map(|s| s.iterations).sum()
157    }
158
159    /// Mark the start of an `iter*` call. Returns `false` when the call must not run.
160    ///
161    /// A second call records `MeasureError::MultipleIterCalls`.
162    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    /// Record an error for `finish` to return. The first error wins.
172    pub fn record_error(&mut self, error: MeasureError) {
173        if self.error.is_none() {
174            self.error = Some(error);
175        }
176    }
177
178    /// Assemble the result, or return the first error the body caused.
179    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}