Skip to content

Commit 26cfa70

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

9 files changed

Lines changed: 374 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_unstable(
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_unstable(
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_unstable(
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_unstable*` 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: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
//! CodSpeed addition: manual control over benchmark sampling.
2+
//!
3+
//! `iter_manual_unstable` lets the user pin down the exact number of measurement
4+
//! rounds 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_unstable`].
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+
/// This bypasses criterion's adaptive sampler entirely: the schedule you
84+
/// pass is exactly what runs.
85+
///
86+
/// **Unstable.** This API is still under development and its name,
87+
/// signature, and behavior may change in future releases.
88+
#[inline(never)]
89+
pub fn iter_manual_unstable<O, R>(&mut self, opts: IterManualOptions, routine: R)
90+
where
91+
R: FnMut() -> O,
92+
{
93+
self.__codspeed_root_frame__iter_manual_unstable(opts, routine);
94+
}
95+
96+
#[inline(never)]
97+
#[allow(missing_docs, non_snake_case)]
98+
pub fn __codspeed_root_frame__iter_manual_unstable<O, R>(
99+
&mut self,
100+
opts: IterManualOptions,
101+
mut routine: R,
102+
) where
103+
R: FnMut() -> O,
104+
{
105+
self.iterated = true;
106+
107+
for _ in 0..opts.warmup_rounds {
108+
for _ in 0..opts.iters {
109+
black_box(routine());
110+
}
111+
}
112+
113+
self.elapsed_time = Duration::ZERO;
114+
let mut samples = Vec::with_capacity(opts.rounds as usize);
115+
for _ in 0..opts.rounds {
116+
let bench_start = InstrumentHooks::current_timestamp();
117+
let round_start = Instant::now();
118+
let start = self.measurement.start();
119+
for _ in 0..opts.iters {
120+
black_box(routine());
121+
}
122+
let value = self.measurement.end(start);
123+
self.elapsed_time += round_start.elapsed();
124+
let bench_end = InstrumentHooks::current_timestamp();
125+
InstrumentHooks::instance().add_benchmark_timestamps(bench_start, bench_end);
126+
127+
samples.push(self.measurement.to_f64(&value));
128+
}
129+
130+
self.codspeed_manual = Some(ManualMeasurement {
131+
samples,
132+
iterations: opts.iters,
133+
});
134+
}
135+
}
136+
137+
#[cfg(feature = "async")]
138+
impl<'a, 'b, A: AsyncExecutor, M: Measurement> AsyncBencher<'a, 'b, A, M> {
139+
/// Async/await variant of [`Bencher::iter_manual_unstable`]. Bypasses
140+
/// criterion's adaptive sampler and runs the exact schedule you pass.
141+
///
142+
/// **Unstable.** This API is still under development and its name,
143+
/// signature, and behavior may change in future releases.
144+
#[inline(never)]
145+
pub fn iter_manual_unstable<O, R, F>(&mut self, opts: IterManualOptions, routine: R)
146+
where
147+
R: FnMut() -> F,
148+
F: Future<Output = O>,
149+
{
150+
self.__codspeed_root_frame__iter_manual_unstable(opts, routine);
151+
}
152+
153+
#[inline(never)]
154+
#[allow(missing_docs, non_snake_case)]
155+
pub fn __codspeed_root_frame__iter_manual_unstable<O, R, F>(
156+
&mut self,
157+
opts: IterManualOptions,
158+
mut routine: R,
159+
) where
160+
R: FnMut() -> F,
161+
F: Future<Output = O>,
162+
{
163+
let AsyncBencher { b, runner } = self;
164+
runner.block_on(async {
165+
b.iterated = true;
166+
167+
for _ in 0..opts.warmup_rounds {
168+
for _ in 0..opts.iters {
169+
black_box(routine().await);
170+
}
171+
}
172+
173+
b.elapsed_time = Duration::ZERO;
174+
let mut samples = Vec::with_capacity(opts.rounds as usize);
175+
for _ in 0..opts.rounds {
176+
let bench_start = InstrumentHooks::current_timestamp();
177+
let round_start = Instant::now();
178+
let start = b.measurement.start();
179+
for _ in 0..opts.iters {
180+
black_box(routine().await);
181+
}
182+
let value = b.measurement.end(start);
183+
b.elapsed_time += round_start.elapsed();
184+
let bench_end = InstrumentHooks::current_timestamp();
185+
InstrumentHooks::instance().add_benchmark_timestamps(bench_start, bench_end);
186+
187+
samples.push(b.measurement.to_f64(&value));
188+
}
189+
190+
b.codspeed_manual = Some(ManualMeasurement {
191+
samples,
192+
iterations: opts.iters,
193+
});
194+
});
195+
}
196+
}

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)