Skip to content

Commit cbc4b7c

Browse files
feat(criterion-compat): add iter_manual to have finer control over the iteration loop
1 parent 0c6ccae commit cbc4b7c

9 files changed

Lines changed: 499 additions & 5 deletions

File tree

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
use codspeed_criterion_compat::{criterion_group, Criterion, IterManualOptions};
2+
3+
fn iter_manual_basic(c: &mut Criterion) {
4+
// rounds=5 is below criterion's sample_size >= 10 floor — proves
5+
// iter_manual fully bypasses the adaptive outer sampler.
6+
c.bench_function("iter_manual_basic", |b| {
7+
b.iter_manual(IterManualOptions::new(5, 100), || {
8+
let mut s = 0u64;
9+
for i in 0..32 {
10+
s = s.wrapping_add(i);
11+
}
12+
s
13+
});
14+
});
15+
}
16+
17+
fn iter_manual_with_setup(c: &mut Criterion) {
18+
c.bench_function("iter_manual_setup", |b| {
19+
b.iter_manual_setup(
20+
IterManualOptions {
21+
rounds: 3,
22+
iterations: 100,
23+
warmup_rounds: 2,
24+
},
25+
|| (0u64..256).collect::<Vec<_>>(),
26+
|input| input.iter().copied().sum::<u64>(),
27+
);
28+
});
29+
}
30+
31+
fn iter_manual_with_teardown(c: &mut Criterion) {
32+
c.bench_function("iter_manual_setup_teardown", |b| {
33+
b.iter_manual_setup_teardown(
34+
IterManualOptions {
35+
rounds: 7,
36+
iterations: 50,
37+
warmup_rounds: 1,
38+
},
39+
|| (0u64..128).collect::<Vec<_>>(),
40+
|input| input.iter().copied().sum::<u64>(),
41+
|input| drop(input),
42+
);
43+
});
44+
}
45+
46+
#[cfg(feature = "async_futures")]
47+
fn iter_manual_async_basic(c: &mut Criterion) {
48+
use codspeed_criterion_compat::async_executor::FuturesExecutor;
49+
c.bench_function("iter_manual_async_basic", |b| {
50+
b.to_async(FuturesExecutor)
51+
.iter_manual(IterManualOptions::new(5, 100), || async {
52+
let mut s = 0u64;
53+
for i in 0..32 {
54+
s = s.wrapping_add(i);
55+
}
56+
s
57+
});
58+
});
59+
}
60+
61+
#[cfg(feature = "async_futures")]
62+
fn iter_manual_async_with_setup(c: &mut Criterion) {
63+
use codspeed_criterion_compat::async_executor::FuturesExecutor;
64+
c.bench_function("iter_manual_async_setup", |b| {
65+
b.to_async(FuturesExecutor).iter_manual_setup(
66+
IterManualOptions {
67+
rounds: 3,
68+
iterations: 100,
69+
warmup_rounds: 2,
70+
},
71+
|| (0u64..256).collect::<Vec<_>>(),
72+
|input| {
73+
let sum = input.iter().copied().sum::<u64>();
74+
async move { sum }
75+
},
76+
);
77+
});
78+
}
79+
80+
#[cfg(feature = "async_futures")]
81+
fn iter_manual_async_with_teardown(c: &mut Criterion) {
82+
use codspeed_criterion_compat::async_executor::FuturesExecutor;
83+
c.bench_function("iter_manual_async_setup_teardown", |b| {
84+
b.to_async(FuturesExecutor).iter_manual_setup_teardown(
85+
IterManualOptions {
86+
rounds: 7,
87+
iterations: 50,
88+
warmup_rounds: 1,
89+
},
90+
|| (0u64..128).collect::<Vec<_>>(),
91+
|input| {
92+
let sum = input.iter().copied().sum::<u64>();
93+
async move { sum }
94+
},
95+
|input| async move { drop(input) },
96+
);
97+
});
98+
}
99+
100+
#[cfg(not(feature = "async_futures"))]
101+
fn iter_manual_async_basic(_c: &mut Criterion) {}
102+
#[cfg(not(feature = "async_futures"))]
103+
fn iter_manual_async_with_setup(_c: &mut Criterion) {}
104+
#[cfg(not(feature = "async_futures"))]
105+
fn iter_manual_async_with_teardown(_c: &mut Criterion) {}
106+
107+
criterion_group!(
108+
benches,
109+
iter_manual_basic,
110+
iter_manual_with_setup,
111+
iter_manual_with_teardown,
112+
iter_manual_async_basic,
113+
iter_manual_async_with_setup,
114+
iter_manual_async_with_teardown,
115+
);

crates/criterion_compat/benches/criterion_integration/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pub mod compare_functions;
22
pub mod custom_measurement;
33
// pub mod external_process;
4+
pub mod iter_manual;
45
pub mod iter_with_large_drop;
56
pub mod iter_with_large_setup;
67
pub mod iter_with_setup;

crates/criterion_compat/benches/criterion_integration_main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod criterion_integration;
55
criterion_main! {
66
criterion_integration::compare_functions::fibonaccis,
77
// criterion_integration::external_process::benches, FIXME: Currently doesn't work
8+
criterion_integration::iter_manual::benches,
89
criterion_integration::iter_with_large_drop::benches,
910
criterion_integration::iter_with_large_setup::benches,
1011
criterion_integration::iter_with_setup::benches,

crates/criterion_compat/criterion_fork/src/bencher.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ pub struct Bencher<'a, M: Measurement = WallTime> {
4242
pub(crate) value: M::Value, // The measured value
4343
pub(crate) measurement: &'a M, // Reference to the measurement object
4444
pub(crate) elapsed_time: Duration, // How much time did it take to perform the iteration? Used for the warmup period.
45+
// CodSpeed addition: when `iter_manual*` runs, it drives the full
46+
// benchmark itself and deposits the per-round results here. The outer
47+
// sampler in `routine.rs` detects this and skips its adaptive logic.
48+
pub(crate) codspeed_manual: Option<crate::codspeed_iter_manual::ManualMeasurement>,
4549
}
4650
impl<'a, M: Measurement> Bencher<'a, M> {
4751
/// Times a `routine` by executing it many times and timing the total elapsed time.
@@ -459,10 +463,12 @@ impl<'a, M: Measurement> Bencher<'a, M> {
459463
}
460464

461465
/// Async/await variant of the Bencher struct.
466+
// CodSpeed addition: fields are `pub(crate)` so the codspeed_iter_manual module
467+
// can destructure this struct.
462468
#[cfg(feature = "async")]
463469
pub struct AsyncBencher<'a, 'b, A: AsyncExecutor, M: Measurement = WallTime> {
464-
b: &'b mut Bencher<'a, M>,
465-
runner: A,
470+
pub(crate) b: &'b mut Bencher<'a, M>,
471+
pub(crate) runner: A,
466472
}
467473
#[cfg(feature = "async")]
468474
impl<'a, 'b, A: AsyncExecutor, M: Measurement> AsyncBencher<'a, 'b, A, M> {
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
//! CodSpeed addition: manual control over benchmark iteration counts.
2+
//!
3+
//! `iter_manual*` lets the user pin down the exact number of measurement
4+
//! rounds, inner iterations per round, and warmup rounds. It bypasses
5+
//! Criterion's adaptive sampler entirely — see `routine.rs::sample` for the
6+
//! short-circuit.
7+
8+
use std::time::Instant;
9+
10+
use codspeed::instrument_hooks::InstrumentHooks;
11+
12+
#[cfg(feature = "async")]
13+
use crate::async_executor::AsyncExecutor;
14+
use crate::black_box;
15+
use crate::measurement::Measurement;
16+
#[cfg(feature = "async")]
17+
use crate::AsyncBencher;
18+
use crate::Bencher;
19+
20+
#[cfg(feature = "async")]
21+
use std::future::Future;
22+
23+
/// Options for the [`iter_manual`](Bencher::iter_manual) family.
24+
#[derive(Debug, Clone, Copy)]
25+
pub struct IterManualOptions {
26+
/// Number of measurement rounds (each produces one sample).
27+
pub rounds: u64,
28+
/// Number of routine invocations inside the measured region of each round.
29+
pub iterations: u64,
30+
/// Number of unmeasured warmup rounds run before measurement starts.
31+
pub warmup_rounds: u64,
32+
}
33+
34+
impl IterManualOptions {
35+
/// Build options with the given rounds and iterations; `warmup_rounds` defaults to 0.
36+
pub fn new(rounds: u64, iterations: u64) -> Self {
37+
Self {
38+
rounds,
39+
iterations,
40+
warmup_rounds: 0,
41+
}
42+
}
43+
}
44+
45+
/// Captured output of a manual run. Stored on the `Bencher` and read by
46+
/// `routine.rs::sample` to short-circuit the adaptive sampler.
47+
pub(crate) struct ManualMeasurement {
48+
/// One entry per measurement round, in nanoseconds (or whatever the
49+
/// measurement's `to_f64` returns).
50+
pub samples: Vec<f64>,
51+
/// Number of inner iterations per round.
52+
pub iterations: u64,
53+
}
54+
55+
impl<'a, M: Measurement> Bencher<'a, M> {
56+
/// Run `routine` exactly `opts.iterations` times inside each of `opts.rounds`
57+
/// measurement rounds, optionally preceded by `opts.warmup_rounds` unmeasured rounds.
58+
///
59+
/// Criterion's adaptive sampler is bypassed for this benchmark.
60+
#[inline(never)]
61+
pub fn iter_manual<O, R>(&mut self, opts: IterManualOptions, mut routine: R)
62+
where
63+
R: FnMut() -> O,
64+
{
65+
self.iter_manual_setup_teardown(opts, || (), |_| routine(), |_| ());
66+
}
67+
68+
/// Like [`iter_manual`](Self::iter_manual), with a `setup` closure producing
69+
/// fresh input for each round. The routine borrows the input mutably.
70+
#[inline(never)]
71+
pub fn iter_manual_setup<I, O, S, R>(&mut self, opts: IterManualOptions, setup: S, routine: R)
72+
where
73+
S: FnMut() -> I,
74+
R: FnMut(&mut I) -> O,
75+
{
76+
self.iter_manual_setup_teardown(opts, setup, routine, |_| ());
77+
}
78+
79+
/// Like [`iter_manual_setup`](Self::iter_manual_setup), with a `teardown`
80+
/// closure called after each round, outside the measured region.
81+
#[inline(never)]
82+
pub fn iter_manual_setup_teardown<I, O, S, R, T>(
83+
&mut self,
84+
opts: IterManualOptions,
85+
mut setup: S,
86+
mut routine: R,
87+
mut teardown: T,
88+
) where
89+
S: FnMut() -> I,
90+
R: FnMut(&mut I) -> O,
91+
T: FnMut(I),
92+
{
93+
self.iterated = true;
94+
95+
let bench_start = InstrumentHooks::current_timestamp();
96+
let time_start = Instant::now();
97+
98+
for _ in 0..opts.warmup_rounds {
99+
let mut input = black_box(setup());
100+
for _ in 0..opts.iterations {
101+
black_box(routine(&mut input));
102+
}
103+
teardown(input);
104+
}
105+
106+
let mut samples = Vec::with_capacity(opts.rounds as usize);
107+
for _ in 0..opts.rounds {
108+
let mut input = black_box(setup());
109+
let start = self.measurement.start();
110+
for _ in 0..opts.iterations {
111+
black_box(routine(&mut input));
112+
}
113+
let value = self.measurement.end(start);
114+
teardown(input);
115+
samples.push(self.measurement.to_f64(&value));
116+
}
117+
118+
self.elapsed_time = time_start.elapsed();
119+
let bench_end = InstrumentHooks::current_timestamp();
120+
InstrumentHooks::instance().add_benchmark_timestamps(bench_start, bench_end);
121+
122+
self.codspeed_manual = Some(ManualMeasurement {
123+
samples,
124+
iterations: opts.iterations,
125+
});
126+
}
127+
}
128+
129+
#[cfg(feature = "async")]
130+
impl<'a, 'b, A: AsyncExecutor, M: Measurement> AsyncBencher<'a, 'b, A, M> {
131+
/// Async/await variant of [`Bencher::iter_manual`].
132+
#[inline(never)]
133+
pub fn iter_manual<O, R, F>(&mut self, opts: IterManualOptions, mut routine: R)
134+
where
135+
R: FnMut() -> F,
136+
F: Future<Output = O>,
137+
{
138+
self.iter_manual_setup_teardown(opts, || (), |_| routine(), |_| std::future::ready(()));
139+
}
140+
141+
/// Async/await variant of [`Bencher::iter_manual_setup`].
142+
#[inline(never)]
143+
pub fn iter_manual_setup<I, O, S, R, F>(
144+
&mut self,
145+
opts: IterManualOptions,
146+
setup: S,
147+
routine: R,
148+
) where
149+
S: FnMut() -> I,
150+
R: FnMut(&mut I) -> F,
151+
F: Future<Output = O>,
152+
{
153+
self.iter_manual_setup_teardown(opts, setup, routine, |_| std::future::ready(()));
154+
}
155+
156+
/// Async/await variant of [`Bencher::iter_manual_setup_teardown`].
157+
#[inline(never)]
158+
pub fn iter_manual_setup_teardown<I, O, S, R, T, RF, TF>(
159+
&mut self,
160+
opts: IterManualOptions,
161+
mut setup: S,
162+
mut routine: R,
163+
mut teardown: T,
164+
) where
165+
S: FnMut() -> I,
166+
R: FnMut(&mut I) -> RF,
167+
T: FnMut(I) -> TF,
168+
RF: Future<Output = O>,
169+
TF: Future<Output = ()>,
170+
{
171+
let AsyncBencher { b, runner } = self;
172+
runner.block_on(async {
173+
b.iterated = true;
174+
175+
let bench_start = InstrumentHooks::current_timestamp();
176+
let time_start = Instant::now();
177+
178+
for _ in 0..opts.warmup_rounds {
179+
let mut input = black_box(setup());
180+
for _ in 0..opts.iterations {
181+
black_box(routine(&mut input).await);
182+
}
183+
teardown(input).await;
184+
}
185+
186+
let mut samples = Vec::with_capacity(opts.rounds as usize);
187+
for _ in 0..opts.rounds {
188+
let mut input = black_box(setup());
189+
let start = b.measurement.start();
190+
for _ in 0..opts.iterations {
191+
black_box(routine(&mut input).await);
192+
}
193+
let value = b.measurement.end(start);
194+
teardown(input).await;
195+
samples.push(b.measurement.to_f64(&value));
196+
}
197+
198+
b.elapsed_time = time_start.elapsed();
199+
let bench_end = InstrumentHooks::current_timestamp();
200+
InstrumentHooks::instance().add_benchmark_timestamps(bench_start, bench_end);
201+
202+
b.codspeed_manual = Some(ManualMeasurement {
203+
samples,
204+
iterations: opts.iterations,
205+
});
206+
});
207+
}
208+
}

crates/criterion_compat/criterion_fork/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ mod benchmark;
5353
mod benchmark_group;
5454
pub mod async_executor;
5555
mod bencher;
56+
// CodSpeed addition: manual iteration control. See `codspeed_iter_manual.rs`.
57+
mod codspeed_iter_manual;
5658
mod connection;
5759
#[cfg(feature = "csv_output")]
5860
mod csv_report;
@@ -99,6 +101,8 @@ use crate::report::{BencherReport, CliReport, CliVerbosity, Report, ReportContex
99101
pub use crate::bencher::AsyncBencher;
100102
pub use crate::bencher::Bencher;
101103
pub use crate::benchmark_group::{BenchmarkGroup, BenchmarkId};
104+
// CodSpeed addition.
105+
pub use crate::codspeed_iter_manual::IterManualOptions;
102106

103107
static DEBUG_ENABLED: Lazy<bool> = Lazy::new(|| std::env::var_os("CRITERION_DEBUG").is_some());
104108
static GNUPLOT_VERSION: Lazy<Result<Version, VersionError>> = Lazy::new(criterion_plot::version);

0 commit comments

Comments
 (0)