criterion-polyglot 0.1.0

An extension trait for criterion providing benchmark methods for various non-Rust programming languages
Documentation
use std::{any::type_name, ops::Index};

/// `BenchSpec` specifies various code fragments, most optional, that will be run during a
/// benchmarking run for any supported language.
///
/// # Examples
///
/// #### Python
/// ```
/// # use criterion_polyglot::BenchSpec;
/// BenchSpec::new(r#"
///     ## This code will be timed for benchmarking
///     expensive_transform_function(data)
/// "#).with_sample_init(r#"
///     ## This code will be run once per sampling run
///     data = db.fetch_random_sample_data()
/// "#).with_global_init(r#"
///     ## This code will run only once, when the benchmark is started
///     db = connect("db://host/schema")
/// "#)
/// # ;
/// ```
///
/// #### Loading code from external files
/// ```ignore
/// # use criterion_polyglot::BenchSpec;
/// BenchSpec::new(include_str!("compute_bench.go.in"))
///     .with_global_init(include_str!("compute_global_init.go.in"))
/// # ;
/// ```
///
/// *The `.in` suffix is entirely optional, but customarily indicates partial code that requires
/// preprocessing and can't be run independently.*
///
/// # Code Fragments
///
/// #### Timed
///
///   [`BenchSpec::new()`] / [`BenchSpec::from(&str)`](BenchSpec::from::<&str>)
///
///   Timed execution. This is the benchmark itself. Executed in a loop by Criterion hundreds to
///   thousands of times per sampling run.
///
/// #### Sample Initialization
///
///   [`BenchSpec::with_sample_init()`]
///
///   *(Optional)* Not timed. Executed once per sampling run. (Re-)initializes starting conditions
///   for a benchmark: e.g. generating random, unsorted data for a sort algorithm.
///
/// #### Global Initialization
///
///   [`BenchSpec::with_global_init()`]
///
///   *(Optional)* Not timed. Executed once, before benchmarking starts.
///
///   **Example usage:** Load or generate constant data. Instantiate required resources, like a
///   database connection (or, in Zig, an allocator).
///
/// #### Imports
///
///   [`BenchSpec::with_imports()`]
///
///   *(Optional)* Not timed. Placed in the language-specific location for importing modules,
///   including header files, etc.
///
/// #### Declarations
///
///   [`BenchSpec::with_declarations()`]
///
///   *(Optional)* Not timed. Placed at the top level of the benchmark harness, outside of any
///   functions, for languages that do not allow certain declarations inside function bodies
///   (otherwise **Global Initialization** could be used).
///
/// ## Variable Scope
///
/// __*From widest to narrowest scope*__
///
/// * **Imports**: should only contain `#include`/`import`/`use`/etc. statements;
///                available to all other scopes
/// * **Declarations**: variables declared in this scope are available to all fragments
/// * **Global**: variables declared in this scope are available to sample and timed fragments
/// * **Sample**: all Global-declared variables are in scope
/// * **Timed**: all variables declared in Global and Sample fragments are in scope
///
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BenchSpec<'a> {
    pub(crate) timed: &'a str,
    pub(crate) global: Option<&'a str>,
    pub(crate) sample: Option<&'a str>,
    pub(crate) imports: Option<&'a str>,
    pub(crate) decls: Option<&'a str>,
}

macro_rules! option_setter {
    (
        $(#[$meta:meta])*
        $lifetime:lifetime, $fn_name:ident, $var_name:ident
    ) => {
        $(#[$meta])*
        pub fn $fn_name(mut self, code: &$lifetime str) -> Self {
            if self.$var_name.is_some() {
                panic!(concat!("`{}.", stringify!($var_name), "` is already defined"), type_name::<Self>());
            }
            self.$var_name = Some(code);
            self
        }
    };
}

impl<'a> BenchSpec<'a> {
    /// Create a new **Timed** code fragment to be benchmarked.
    ///
    /// This code will be run hundreds to thousands of times by Criterion during the sampling
    /// process. The exact number of iterations is determined during the benchmarking warm-up
    /// process, based on [`Criterion::measurement_time`](criterion::Criterion::measurement_time).
    pub fn new(timed_code: &'a str) -> Self {
        Self::from(timed_code)
    }

    option_setter!(
        /// Add a **Global** initializer for the benchmark.
        ///
        /// This code will be run exactly one time, when the benchmark is started. It can declare
        /// variables, which will remain in scope for all other benchmark code.
        'a, with_global_init, global
    );

    option_setter!(
        /// Add a **Sample** initializer for the benchmark.
        ///
        /// This code will be called each time Criterion begins a sampling run. The number of sampling
        /// runs is determined by [`Criterion::sample_size`](criterion::Criterion::sample_size). Any
        /// variables declared in a **Global** initializer are in scope.
        'a, with_sample_init, sample
    );

    option_setter!(
        /// Add constants or forward **Declarations** for the benchmark.
        ///
        /// This fragment will be placed at the top level of the benchmark harness,
        /// after imports but before the *Global* initializer fragment is run. This fragment is not
        /// necessary for many languages but is useful for languages like C that may require forward
        /// declarations.
        ///
        /// Depending on the languages rule about statements outside functions, this code
        /// may (e.g. Python or Ruby) or may not (e.g. C or Go) contain executable statements.
        'a, with_declarations, decls
    );

    option_setter!(
        /// Add **Imports** for the benchmark.
        ///
        /// This fragment will be placed in the appropriate position to import modules (or the
        /// language-appropriate terminology) into the scope of all other code fragments. This
        /// fragment is only required for languages that do allow imports in function bodies or
        /// within arbitrary blocks (e.g. Go)
        'a, with_imports, imports
    );
}

impl<'a> From<&'a str> for BenchSpec<'a> {
    fn from(timed_code: &'a str) -> Self {
        Self { timed: timed_code, global: None, sample: None, imports: None, decls: None }
    }
}

// HIDDEN: this is just for the harness templating and would not be a good public API
#[doc(hidden)]
impl<'a> Index<&str> for BenchSpec<'a> {
    type Output = str;

    fn index(&self, index: &str) -> &'a str {
        match index {
            "timed" => self.timed,
            "global" => self.global.unwrap_or_default(),
            "sample" => self.sample.unwrap_or_default(),
            "declarations" => self.decls.unwrap_or_default(),
            "imports" => self.imports.unwrap_or_default(),
            _ => unimplemented!("BenchSpec.{index} does not exist"),
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;

    const T: &str = "timed";
    const G: &str = "global";
    const S: &str = "sample";
    const GLOBAL: BenchSpec = BenchSpec { timed: T, global: Some(G), sample: None, imports: None, decls: None };
    const SAMPLE: BenchSpec = BenchSpec { timed: T, global: None, sample: Some(S), imports: None, decls: None };
    const GS: BenchSpec = BenchSpec { timed: T, global: Some(G), sample: Some(S), imports: None, decls: None };
    const BASE: BenchSpec = BenchSpec { timed: T, global: None, sample: None, imports: None, decls: None };

    #[test]
    fn builder() {
        assert_eq!(BASE, BenchSpec::new(T));
        assert_eq!(GLOBAL, BASE.with_global_init(G));
        assert_eq!(SAMPLE, BASE.with_sample_init(S));
        assert_eq!(GS, GLOBAL.with_sample_init(S));
        assert_eq!(GS, BASE.with_sample_init(S).with_global_init(G));
    }

    #[test]
    fn from_trait() {
        assert_eq!(BASE, BenchSpec::from(T));
    }

    #[test]
    #[should_panic(expected = "already defined")]
    fn builder_panic_on_duplicate_global() {
        let _panics = GLOBAL.with_global_init("");
    }

    #[test]
    #[should_panic(expected = "already defined")]
    fn builder_panic_on_duplicate_sample() {
        let _panics = SAMPLE.with_sample_init("");
    }
}