criterion_polyglot/spec.rs
1use std::{any::type_name, ops::Index};
2
3/// `BenchSpec` specifies various code fragments, most optional, that will be run during a
4/// benchmarking run for any supported language.
5///
6/// # Examples
7///
8/// #### Python
9/// ```
10/// # use criterion_polyglot::BenchSpec;
11/// BenchSpec::new(r#"
12/// ## This code will be timed for benchmarking
13/// expensive_transform_function(data)
14/// "#).with_sample_init(r#"
15/// ## This code will be run once per sampling run
16/// data = db.fetch_random_sample_data()
17/// "#).with_global_init(r#"
18/// ## This code will run only once, when the benchmark is started
19/// db = connect("db://host/schema")
20/// "#)
21/// # ;
22/// ```
23///
24/// #### Loading code from external files
25/// ```ignore
26/// # use criterion_polyglot::BenchSpec;
27/// BenchSpec::new(include_str!("compute_bench.go.in"))
28/// .with_global_init(include_str!("compute_global_init.go.in"))
29/// # ;
30/// ```
31///
32/// *The `.in` suffix is entirely optional, but customarily indicates partial code that requires
33/// preprocessing and can't be run independently.*
34///
35/// # Code Fragments
36///
37/// #### Timed
38///
39/// [`BenchSpec::new()`] / [`BenchSpec::from(&str)`](BenchSpec::from::<&str>)
40///
41/// Timed execution. This is the benchmark itself. Executed in a loop by Criterion hundreds to
42/// thousands of times per sampling run.
43///
44/// #### Sample Initialization
45///
46/// [`BenchSpec::with_sample_init()`]
47///
48/// *(Optional)* Not timed. Executed once per sampling run. (Re-)initializes starting conditions
49/// for a benchmark: e.g. generating random, unsorted data for a sort algorithm.
50///
51/// #### Global Initialization
52///
53/// [`BenchSpec::with_global_init()`]
54///
55/// *(Optional)* Not timed. Executed once, before benchmarking starts.
56///
57/// **Example usage:** Load or generate constant data. Instantiate required resources, like a
58/// database connection (or, in Zig, an allocator).
59///
60/// #### Imports
61///
62/// [`BenchSpec::with_imports()`]
63///
64/// *(Optional)* Not timed. Placed in the language-specific location for importing modules,
65/// including header files, etc.
66///
67/// #### Declarations
68///
69/// [`BenchSpec::with_declarations()`]
70///
71/// *(Optional)* Not timed. Placed at the top level of the benchmark harness, outside of any
72/// functions, for languages that do not allow certain declarations inside function bodies
73/// (otherwise **Global Initialization** could be used).
74///
75/// ## Variable Scope
76///
77/// __*From widest to narrowest scope*__
78///
79/// * **Imports**: should only contain `#include`/`import`/`use`/etc. statements;
80/// available to all other scopes
81/// * **Declarations**: variables declared in this scope are available to all fragments
82/// * **Global**: variables declared in this scope are available to sample and timed fragments
83/// * **Sample**: all Global-declared variables are in scope
84/// * **Timed**: all variables declared in Global and Sample fragments are in scope
85///
86#[derive(Clone, Copy, Debug, PartialEq, Eq)]
87pub struct BenchSpec<'a> {
88 pub(crate) timed: &'a str,
89 pub(crate) global: Option<&'a str>,
90 pub(crate) sample: Option<&'a str>,
91 pub(crate) imports: Option<&'a str>,
92 pub(crate) decls: Option<&'a str>,
93}
94
95macro_rules! option_setter {
96 (
97 $(#[$meta:meta])*
98 $lifetime:lifetime, $fn_name:ident, $var_name:ident
99 ) => {
100 $(#[$meta])*
101 pub fn $fn_name(mut self, code: &$lifetime str) -> Self {
102 if self.$var_name.is_some() {
103 panic!(concat!("`{}.", stringify!($var_name), "` is already defined"), type_name::<Self>());
104 }
105 self.$var_name = Some(code);
106 self
107 }
108 };
109}
110
111impl<'a> BenchSpec<'a> {
112 /// Create a new **Timed** code fragment to be benchmarked.
113 ///
114 /// This code will be run hundreds to thousands of times by Criterion during the sampling
115 /// process. The exact number of iterations is determined during the benchmarking warm-up
116 /// process, based on [`Criterion::measurement_time`](criterion::Criterion::measurement_time).
117 pub fn new(timed_code: &'a str) -> Self {
118 Self::from(timed_code)
119 }
120
121 option_setter!(
122 /// Add a **Global** initializer for the benchmark.
123 ///
124 /// This code will be run exactly one time, when the benchmark is started. It can declare
125 /// variables, which will remain in scope for all other benchmark code.
126 'a, with_global_init, global
127 );
128
129 option_setter!(
130 /// Add a **Sample** initializer for the benchmark.
131 ///
132 /// This code will be called each time Criterion begins a sampling run. The number of sampling
133 /// runs is determined by [`Criterion::sample_size`](criterion::Criterion::sample_size). Any
134 /// variables declared in a **Global** initializer are in scope.
135 'a, with_sample_init, sample
136 );
137
138 option_setter!(
139 /// Add constants or forward **Declarations** for the benchmark.
140 ///
141 /// This fragment will be placed at the top level of the benchmark harness,
142 /// after imports but before the *Global* initializer fragment is run. This fragment is not
143 /// necessary for many languages but is useful for languages like C that may require forward
144 /// declarations.
145 ///
146 /// Depending on the languages rule about statements outside functions, this code
147 /// may (e.g. Python or Ruby) or may not (e.g. C or Go) contain executable statements.
148 'a, with_declarations, decls
149 );
150
151 option_setter!(
152 /// Add **Imports** for the benchmark.
153 ///
154 /// This fragment will be placed in the appropriate position to import modules (or the
155 /// language-appropriate terminology) into the scope of all other code fragments. This
156 /// fragment is only required for languages that do allow imports in function bodies or
157 /// within arbitrary blocks (e.g. Go)
158 'a, with_imports, imports
159 );
160}
161
162impl<'a> From<&'a str> for BenchSpec<'a> {
163 fn from(timed_code: &'a str) -> Self {
164 Self { timed: timed_code, global: None, sample: None, imports: None, decls: None }
165 }
166}
167
168// HIDDEN: this is just for the harness templating and would not be a good public API
169#[doc(hidden)]
170impl<'a> Index<&str> for BenchSpec<'a> {
171 type Output = str;
172
173 fn index(&self, index: &str) -> &'a str {
174 match index {
175 "timed" => self.timed,
176 "global" => self.global.unwrap_or_default(),
177 "sample" => self.sample.unwrap_or_default(),
178 "declarations" => self.decls.unwrap_or_default(),
179 "imports" => self.imports.unwrap_or_default(),
180 _ => unimplemented!("BenchSpec.{index} does not exist"),
181 }
182 }
183}
184
185#[cfg(test)]
186mod test {
187 use super::*;
188
189 const T: &str = "timed";
190 const G: &str = "global";
191 const S: &str = "sample";
192 const GLOBAL: BenchSpec = BenchSpec { timed: T, global: Some(G), sample: None, imports: None, decls: None };
193 const SAMPLE: BenchSpec = BenchSpec { timed: T, global: None, sample: Some(S), imports: None, decls: None };
194 const GS: BenchSpec = BenchSpec { timed: T, global: Some(G), sample: Some(S), imports: None, decls: None };
195 const BASE: BenchSpec = BenchSpec { timed: T, global: None, sample: None, imports: None, decls: None };
196
197 #[test]
198 fn builder() {
199 assert_eq!(BASE, BenchSpec::new(T));
200 assert_eq!(GLOBAL, BASE.with_global_init(G));
201 assert_eq!(SAMPLE, BASE.with_sample_init(S));
202 assert_eq!(GS, GLOBAL.with_sample_init(S));
203 assert_eq!(GS, BASE.with_sample_init(S).with_global_init(G));
204 }
205
206 #[test]
207 fn from_trait() {
208 assert_eq!(BASE, BenchSpec::from(T));
209 }
210
211 #[test]
212 #[should_panic(expected = "already defined")]
213 fn builder_panic_on_duplicate_global() {
214 let _panics = GLOBAL.with_global_init("");
215 }
216
217 #[test]
218 #[should_panic(expected = "already defined")]
219 fn builder_panic_on_duplicate_sample() {
220 let _panics = SAMPLE.with_sample_init("");
221 }
222}