fluxbench_core/bencher/config.rs
1//! Measurement config, errors, and results.
2
3use fluxbench_ipc::{BenchmarkConfig, Sample, Throughput};
4
5/// Default number of samples to collect (matches Criterion)
6pub const DEFAULT_SAMPLE_COUNT: usize = fluxbench_ipc::DEFAULT_SAMPLE_COUNT as usize;
7
8/// Minimum sample count. Two samples give a defined variance.
9pub const MIN_SAMPLE_COUNT: usize = 2;
10
11/// Mode of iteration for the benchmark
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum IterationMode {
14 /// Simple iteration - measure entire closure
15 Simple,
16 /// Iteration with setup - separate setup from measurement
17 WithSetup,
18 /// Iteration with teardown
19 WithTeardown,
20}
21
22/// Config for one benchmark measurement.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct MeasureConfig {
25 /// Warmup budget in nanoseconds. Fixed mode skips warmup.
26 pub warmup_ns: u64,
27 /// Measurement budget in nanoseconds. It sets the iterations per sample.
28 pub measurement_ns: u64,
29 /// Samples to collect. Values below `MIN_SAMPLE_COUNT` rise to it.
30 pub sample_count: usize,
31 /// Fixed iterations per sample. `Some` selects fixed mode: no warmup, no estimate.
32 pub fixed_iters_per_sample: Option<u64>,
33 /// Minimum total measurement iterations. Ignored in fixed mode.
34 pub min_iterations: Option<u64>,
35 /// Maximum total measurement iterations. Ignored in fixed mode.
36 pub max_iterations: Option<u64>,
37 /// Read the allocation counters around each timed region.
38 pub track_allocations: bool,
39}
40
41impl Default for MeasureConfig {
42 fn default() -> Self {
43 Self::from(&BenchmarkConfig::default())
44 }
45}
46
47impl From<&BenchmarkConfig> for MeasureConfig {
48 fn from(config: &BenchmarkConfig) -> Self {
49 Self {
50 warmup_ns: config.warmup_time_ns,
51 measurement_ns: config.measurement_time_ns,
52 sample_count: config.sample_count as usize,
53 fixed_iters_per_sample: config.iters_per_sample,
54 min_iterations: config.min_iterations,
55 max_iterations: config.max_iterations,
56 track_allocations: config.track_allocations,
57 }
58 }
59}
60
61impl MeasureConfig {
62 /// Raise the sample count to `MIN_SAMPLE_COUNT` and a fixed count of 0 to 1.
63 pub fn normalized(mut self) -> Self {
64 self.sample_count = self.sample_count.max(MIN_SAMPLE_COUNT);
65 self.fixed_iters_per_sample = self.fixed_iters_per_sample.map(|n| n.max(1));
66 self
67 }
68}
69
70/// Error from one benchmark measurement.
71#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
72pub enum MeasureError {
73 /// The benchmark body returned without calling an `iter*` method.
74 #[error("benchmark body never called b.iter*")]
75 NoIterCall,
76 /// The benchmark body called a second `iter*` method. The second call does not run.
77 #[error("benchmark body called more than one b.iter* method")]
78 MultipleIterCalls,
79 /// The measurement collected no samples.
80 #[error("benchmark produced zero samples")]
81 NoSamples,
82 /// The async runtime did not build, or `iter_async` ran inside a runtime context.
83 /// Only the `async` feature produces it.
84 #[error("async runtime error: {0}")]
85 AsyncRuntime(String),
86}
87
88/// Result of a single benchmark run
89#[derive(Debug, Clone)]
90pub struct BenchmarkResult {
91 /// Collected samples. Each holds per-iteration means over `iters_per_sample` calls.
92 pub samples: Vec<Sample>,
93 /// Routine calls in the measurement phase. Warmup calls are excluded.
94 pub iterations: u64,
95 /// Routine calls in the warmup phase
96 pub warmup_iterations: u64,
97 /// Sum of each sample's mean times its iterations, in nanoseconds
98 pub total_time_ns: u64,
99 /// Routine calls per sample
100 pub iters_per_sample: u64,
101 /// `true` when the hard deadline or a SIGTERM stop request ended measurement
102 /// before `sample_count` samples
103 pub truncated: bool,
104 /// Work per call the body set with `Bencher::throughput`. `None` without a call.
105 pub throughput: Option<Throughput>,
106}