1use crate::benchmark::BenchmarkConfig;
2use crate::connection::OutgoingMessage;
3use crate::measurement::Measurement;
4use crate::report::{BenchmarkId, Report, ReportContext};
5use crate::{black_box, ActualSamplingMode, Bencher, Criterion};
6use std::marker::PhantomData;
7use std::time::Duration;
8
9/// PRIVATE
10pub(crate) trait Routine<M: Measurement, T: ?Sized> {
11 /// PRIVATE
12 fn bench(&mut self, m: &M, iters: &[u64], parameter: &T) -> Vec<f64>;
13 /// PRIVATE
14 fn warm_up(&mut self, m: &M, how_long: Duration, parameter: &T) -> (u64, u64);
15
16 /// PRIVATE
17 fn test(&mut self, m: &M, parameter: &T) {
18 self.bench(m, &[1u64], parameter);
19 }
20
21 /// Iterates the benchmarked function for a fixed length of time, but takes no measurements.
22 /// This keeps the overall benchmark suite runtime constant-ish even when running under a
23 /// profiler with an unknown amount of overhead. Since no measurements are taken, it also
24 /// reduces the amount of time the execution spends in Criterion.rs code, which should help
25 /// show the performance of the benchmarked code more clearly as well.
26 fn profile(
27 &mut self,
28 measurement: &M,
29 id: &BenchmarkId,
30 criterion: &Criterion<M>,
31 report_context: &ReportContext,
32 time: Duration,
33 parameter: &T,
34 ) {
35 criterion
36 .report
37 .profile(id, report_context, time.as_nanos() as f64);
38
39 let mut profile_path = report_context.output_directory.clone();
40 if (*crate::CARGO_CRITERION_CONNECTION).is_some() {
41 // If connected to cargo-criterion, generate a cargo-criterion-style path.
42 // This is kind of a hack.
43 profile_path.push("profile");
44 profile_path.push(id.as_directory_name());
45 } else {
46 profile_path.push(id.as_directory_name());
47 profile_path.push("profile");
48 }
49 criterion
50 .profiler
51 .borrow_mut()
52 .start_profiling(id.id(), &profile_path);
53
54 let time = time.as_nanos() as u64;
55
56 // TODO: Some profilers will show the two batches of iterations as
57 // being different code-paths even though they aren't really.
58
59 // Get the warmup time for one second
60 let (wu_elapsed, wu_iters) = self.warm_up(measurement, Duration::from_secs(1), parameter);
61 if wu_elapsed < time {
62 // Initial guess for the mean execution time
63 let met = wu_elapsed as f64 / wu_iters as f64;
64
65 // Guess how many iterations will be required for the remaining time
66 let remaining = (time - wu_elapsed) as f64;
67
68 let iters = remaining / met;
69 let iters = iters as u64;
70
71 self.bench(measurement, &[iters], parameter);
72 }
73
74 criterion
75 .profiler
76 .borrow_mut()
77 .stop_profiling(id.id(), &profile_path);
78
79 criterion.report.terminated(id, report_context);
80 }
81
82 fn sample(
83 &mut self,
84 measurement: &M,
85 id: &BenchmarkId,
86 config: &BenchmarkConfig,
87 criterion: &Criterion<M>,
88 report_context: &ReportContext,
89 parameter: &T,
90 ) -> (ActualSamplingMode, Box<[f64]>, Box<[f64]>) {
91 if config.quick_mode {
92 let minimum_bench_duration = Duration::from_millis(100);
93 let maximum_bench_duration = config.measurement_time; // default: 5 seconds
94 let target_rel_stdev = config.significance_level; // default: 5%, 0.05
95
96 use std::time::Instant;
97 let time_start = Instant::now();
98
99 let sq = |val| val * val;
100 let mut n = 1;
101 let mut t_prev = *self.bench(measurement, &[n], parameter).first().unwrap();
102
103 // Early exit for extremely long running benchmarks:
104 if time_start.elapsed() > maximum_bench_duration {
105 let iters = vec![n as f64, n as f64].into_boxed_slice();
106 // prevent gnuplot bug when all values are equal
107 let elapsed = vec![t_prev, t_prev + 0.000001].into_boxed_slice();
108 return (ActualSamplingMode::Flat, iters, elapsed);
109 }
110
111 // Main data collection loop.
112 loop {
113 let t_now = *self
114 .bench(measurement, &[n * 2], parameter)
115 .first()
116 .unwrap();
117 let t = (t_prev + 2. * t_now) / 5.;
118 let stdev = (sq(t_prev - t) + sq(t_now - 2. * t)).sqrt();
119 // println!("Sample: {} {:.2}", n, stdev / t);
120 let elapsed = time_start.elapsed();
121 if (stdev < target_rel_stdev * t && elapsed > minimum_bench_duration)
122 || elapsed > maximum_bench_duration
123 {
124 let iters = vec![n as f64, (n * 2) as f64].into_boxed_slice();
125 let elapsed = vec![t_prev, t_now].into_boxed_slice();
126 return (ActualSamplingMode::Linear, iters, elapsed);
127 }
128 n *= 2;
129 t_prev = t_now;
130 }
131 }
132 let wu = config.warm_up_time;
133 let m_ns = config.measurement_time.as_nanos();
134
135 criterion
136 .report
137 .warmup(id, report_context, wu.as_nanos() as f64);
138
139 if let Some(conn) = &criterion.connection {
140 conn.send(&OutgoingMessage::Warmup {
141 id: id.into(),
142 nanos: wu.as_nanos() as f64,
143 })
144 .unwrap();
145 }
146
147 let (wu_elapsed, wu_iters) = self.warm_up(measurement, wu, parameter);
148 if crate::debug_enabled() {
149 println!(
150 "\nCompleted {} iterations in {} nanoseconds, estimated execution time is {} ns",
151 wu_iters,
152 wu_elapsed,
153 wu_elapsed as f64 / wu_iters as f64
154 );
155 }
156
157 // Initial guess for the mean execution time
158 let met = wu_elapsed as f64 / wu_iters as f64;
159
160 let n = config.sample_size as u64;
161
162 let actual_sampling_mode = config
163 .sampling_mode
164 .choose_sampling_mode(met, n, m_ns as f64);
165
166 let m_iters = actual_sampling_mode.iteration_counts(met, n, &config.measurement_time);
167
168 let expected_ns = m_iters
169 .iter()
170 .copied()
171 .map(|count| count as f64 * met)
172 .sum();
173
174 // Use saturating_add to handle overflow.
175 let mut total_iters = 0u64;
176 for count in m_iters.iter().copied() {
177 total_iters = total_iters.saturating_add(count);
178 }
179
180 criterion
181 .report
182 .measurement_start(id, report_context, n, expected_ns, total_iters);
183
184 if let Some(conn) = &criterion.connection {
185 conn.send(&OutgoingMessage::MeasurementStart {
186 id: id.into(),
187 sample_count: n,
188 estimate_ns: expected_ns,
189 iter_count: total_iters,
190 })
191 .unwrap();
192 }
193
194 let m_elapsed = self.bench(measurement, &m_iters, parameter);
195
196 let m_iters_f: Vec<f64> = m_iters.iter().map(|&x| x as f64).collect();
197
198 (
199 actual_sampling_mode,
200 m_iters_f.into_boxed_slice(),
201 m_elapsed.into_boxed_slice(),
202 )
203 }
204}
205
206pub struct Function<M: Measurement, F, T>
207where
208 F: FnMut(&mut Bencher<'_, M>, &T),
209 T: ?Sized,
210{
211 f: F,
212 // TODO: Is there some way to remove these?
213 _phantom: PhantomData<T>,
214 _phamtom2: PhantomData<M>,
215}
216impl<M: Measurement, F, T> Function<M, F, T>
217where
218 F: FnMut(&mut Bencher<'_, M>, &T),
219 T: ?Sized,
220{
221 pub fn new(f: F) -> Function<M, F, T> {
222 Function {
223 f,
224 _phantom: PhantomData,
225 _phamtom2: PhantomData,
226 }
227 }
228}
229
230impl<M: Measurement, F, T> Routine<M, T> for Function<M, F, T>
231where
232 F: FnMut(&mut Bencher<'_, M>, &T),
233 T: ?Sized,
234{
235 fn bench(&mut self, m: &M, iters: &[u64], parameter: &T) -> Vec<f64> {
236 let f = &mut self.f;
237
238 let mut b = Bencher {
239 iterated: false,
240 iters: 0,
241 value: m.zero(),
242 measurement: m,
243 elapsed_time: Duration::from_millis(0),
244 };
245
246 iters
247 .iter()
248 .map(|iters| {
249 b.iters = *iters;
250 (*f)(&mut b, black_box(parameter));
251 b.assert_iterated();
252 m.to_f64(&b.value)
253 })
254 .collect()
255 }
256
257 fn warm_up(&mut self, m: &M, how_long: Duration, parameter: &T) -> (u64, u64) {
258 let f = &mut self.f;
259 let mut b = Bencher {
260 iterated: false,
261 iters: 1,
262 value: m.zero(),
263 measurement: m,
264 elapsed_time: Duration::from_millis(0),
265 };
266
267 let mut total_iters = 0;
268 let mut elapsed_time = Duration::from_millis(0);
269 loop {
270 (*f)(&mut b, black_box(parameter));
271
272 b.assert_iterated();
273
274 total_iters += b.iters;
275 elapsed_time += b.elapsed_time;
276 if elapsed_time > how_long {
277 return (elapsed_time.as_nanos() as u64, total_iters);
278 }
279
280 b.iters = b.iters.wrapping_mul(2);
281 }
282 }
283}
284