Skip to content

Commit 9f20fd0

Browse files
feat(criterion-compat): add iter_manual to have finer control over the iteration loop
1 parent 6c54971 commit 9f20fd0

9 files changed

Lines changed: 348 additions & 5 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
use codspeed_criterion_compat::{criterion_group, Criterion, IterManualOptions};
2+
3+
fn iter_manual_simple(c: &mut Criterion) {
4+
c.bench_function("iter_manual_simple", |b| {
5+
b.iter_manual(
6+
IterManualOptions::new().rounds(3).iters(5).warmup(1),
7+
|| std::thread::sleep(std::time::Duration::from_millis(100)),
8+
);
9+
});
10+
}
11+
12+
fn iter_manual_with_external_setup(c: &mut Criterion) {
13+
c.bench_function("iter_manual_with_external_setup", |b| {
14+
// Setup deliberately does a chunk of work so it stands out in flamegraphs.
15+
// The measured region should NOT include this work.
16+
let input: Vec<u64> = (0..10_000u64).map(|i| i.wrapping_mul(31)).collect();
17+
b.iter_manual(
18+
IterManualOptions::new().rounds(3).iters(50).warmup(1),
19+
|| input.iter().copied().sum::<u64>(),
20+
);
21+
});
22+
}
23+
24+
#[cfg(feature = "async_futures")]
25+
fn iter_manual_async(c: &mut Criterion) {
26+
use codspeed_criterion_compat::async_executor::FuturesExecutor;
27+
c.bench_function("iter_manual_async", |b| {
28+
b.to_async(FuturesExecutor).iter_manual(
29+
IterManualOptions::new().rounds(3).iters(100).warmup(2),
30+
|| async { (0u64..256).sum::<u64>() },
31+
);
32+
});
33+
}
34+
35+
#[cfg(not(feature = "async_futures"))]
36+
fn iter_manual_async(_c: &mut Criterion) {}
37+
38+
criterion_group!(
39+
benches,
40+
iter_manual_simple,
41+
iter_manual_with_external_setup,
42+
iter_manual_async,
43+
);

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: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
//! CodSpeed addition: manual control over benchmark sampling.
2+
//!
3+
//! `iter_manual` lets the user pin down the exact number of measurement rounds
4+
//! and iterations per round, bypassing criterion's adaptive sampler. See
5+
//! `routine.rs::sample` for the short-circuit that picks up the result.
6+
7+
use std::time::{Duration, Instant};
8+
9+
use codspeed::instrument_hooks::InstrumentHooks;
10+
11+
#[cfg(feature = "async")]
12+
use crate::async_executor::AsyncExecutor;
13+
use crate::black_box;
14+
use crate::measurement::Measurement;
15+
#[cfg(feature = "async")]
16+
use crate::AsyncBencher;
17+
use crate::Bencher;
18+
19+
#[cfg(feature = "async")]
20+
use std::future::Future;
21+
22+
/// Options for [`Bencher::iter_manual`].
23+
#[derive(Debug, Clone, Copy)]
24+
pub struct IterManualOptions {
25+
rounds: u64,
26+
iters: u64,
27+
warmup_rounds: u64,
28+
}
29+
30+
impl Default for IterManualOptions {
31+
fn default() -> Self {
32+
Self {
33+
rounds: 1,
34+
iters: 1,
35+
warmup_rounds: 0,
36+
}
37+
}
38+
}
39+
40+
impl IterManualOptions {
41+
/// Start with defaults: 1 round, 1 iteration per round, 0 warmup rounds.
42+
pub fn new() -> Self {
43+
Self::default()
44+
}
45+
46+
/// Number of measurement rounds (each produces one sample).
47+
#[must_use]
48+
pub fn rounds(mut self, rounds: u64) -> Self {
49+
self.rounds = rounds;
50+
self
51+
}
52+
53+
/// Number of routine invocations inside a measurement round.
54+
#[must_use]
55+
pub fn iters(mut self, iters: u64) -> Self {
56+
self.iters = iters;
57+
self
58+
}
59+
60+
/// Number of unmeasured warmup rounds run before measurement starts.
61+
#[must_use]
62+
pub fn warmup(mut self, warmup_rounds: u64) -> Self {
63+
self.warmup_rounds = warmup_rounds;
64+
self
65+
}
66+
}
67+
68+
/// Captured output of a manual run. Stored on the `Bencher` and read by
69+
/// `routine.rs::sample` to short-circuit the adaptive sampler.
70+
pub(crate) struct ManualMeasurement {
71+
/// One entry per measurement round, in the units of `Measurement::to_f64`.
72+
pub samples: Vec<f64>,
73+
/// Number of routine invocations per round.
74+
pub iterations: u64,
75+
}
76+
77+
impl<'a, M: Measurement> Bencher<'a, M> {
78+
/// Run `routine` with a precise schedule: `opts.rounds` measurement rounds
79+
/// (each producing one sample), each consisting of `opts.iters` calls to
80+
/// `routine`. Optionally preceded by `opts.warmup_rounds` unmeasured rounds
81+
/// of the same shape.
82+
///
83+
/// Criterion's adaptive sampler is bypassed for this benchmark.
84+
#[inline(never)]
85+
pub fn iter_manual<O, R>(&mut self, opts: IterManualOptions, routine: R)
86+
where
87+
R: FnMut() -> O,
88+
{
89+
self.__codspeed_root_frame__iter_manual(opts, routine);
90+
}
91+
92+
#[inline(never)]
93+
#[allow(missing_docs, non_snake_case)]
94+
pub fn __codspeed_root_frame__iter_manual<O, R>(
95+
&mut self,
96+
opts: IterManualOptions,
97+
mut routine: R,
98+
) where
99+
R: FnMut() -> O,
100+
{
101+
self.iterated = true;
102+
103+
for _ in 0..opts.warmup_rounds {
104+
for _ in 0..opts.iters {
105+
black_box(routine());
106+
}
107+
}
108+
109+
self.elapsed_time = Duration::ZERO;
110+
let mut samples = Vec::with_capacity(opts.rounds as usize);
111+
for _ in 0..opts.rounds {
112+
let bench_start = InstrumentHooks::current_timestamp();
113+
let round_start = Instant::now();
114+
let start = self.measurement.start();
115+
for _ in 0..opts.iters {
116+
black_box(routine());
117+
}
118+
let value = self.measurement.end(start);
119+
self.elapsed_time += round_start.elapsed();
120+
let bench_end = InstrumentHooks::current_timestamp();
121+
InstrumentHooks::instance().add_benchmark_timestamps(bench_start, bench_end);
122+
123+
samples.push(self.measurement.to_f64(&value));
124+
}
125+
126+
self.codspeed_manual = Some(ManualMeasurement {
127+
samples,
128+
iterations: opts.iters,
129+
});
130+
}
131+
}
132+
133+
#[cfg(feature = "async")]
134+
impl<'a, 'b, A: AsyncExecutor, M: Measurement> AsyncBencher<'a, 'b, A, M> {
135+
/// Async/await variant of [`Bencher::iter_manual`].
136+
#[inline(never)]
137+
pub fn iter_manual<O, R, F>(&mut self, opts: IterManualOptions, routine: R)
138+
where
139+
R: FnMut() -> F,
140+
F: Future<Output = O>,
141+
{
142+
self.__codspeed_root_frame__iter_manual(opts, routine);
143+
}
144+
145+
#[inline(never)]
146+
#[allow(missing_docs, non_snake_case)]
147+
pub fn __codspeed_root_frame__iter_manual<O, R, F>(
148+
&mut self,
149+
opts: IterManualOptions,
150+
mut routine: R,
151+
) where
152+
R: FnMut() -> F,
153+
F: Future<Output = O>,
154+
{
155+
let AsyncBencher { b, runner } = self;
156+
runner.block_on(async {
157+
b.iterated = true;
158+
159+
for _ in 0..opts.warmup_rounds {
160+
for _ in 0..opts.iters {
161+
black_box(routine().await);
162+
}
163+
}
164+
165+
b.elapsed_time = Duration::ZERO;
166+
let mut samples = Vec::with_capacity(opts.rounds as usize);
167+
for _ in 0..opts.rounds {
168+
let bench_start = InstrumentHooks::current_timestamp();
169+
let round_start = Instant::now();
170+
let start = b.measurement.start();
171+
for _ in 0..opts.iters {
172+
black_box(routine().await);
173+
}
174+
let value = b.measurement.end(start);
175+
b.elapsed_time += round_start.elapsed();
176+
let bench_end = InstrumentHooks::current_timestamp();
177+
InstrumentHooks::instance().add_benchmark_timestamps(bench_start, bench_end);
178+
179+
samples.push(b.measurement.to_f64(&value));
180+
}
181+
182+
b.codspeed_manual = Some(ManualMeasurement {
183+
samples,
184+
iterations: opts.iters,
185+
});
186+
});
187+
}
188+
}

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)