This guide provides a detailed overview of bench_matrix, its core concepts, and how to use its API to create and run parameterized benchmarks with the Criterion harness in Rust.
- Core Concepts
- Quick Start Examples
- Defining Parameters and Configurations
- Main API Sections
- Customizing Benchmark Execution
- Error Handling
Understanding these concepts is key to effectively using bench_matrix:
- Parameter Axis: A
Vec<MatrixCellValue>representing all possible values for a single dimension of your benchmark configuration. For example, an axis could define different buffer sizes:vec![MatrixCellValue::Unsigned(64), MatrixCellValue::Unsigned(128)]. - Parameter Names: An optional
Vec<String>where each string is a human-readable name for the corresponding parameter axis. These names are used bybench_matrixto generate descriptive benchmark IDs in Criterion (e.g.,MySuite/Algorithm-QuickSort_DataSize-1000). MatrixCellValue: An enum (Tag,String,Int,Unsigned,Bool) representing a single, discrete value within a parameter axis.AbstractCombination: A struct holding aVec<MatrixCellValue>, where each cell value is taken from a different parameter axis. This represents one unique configuration to be benchmarked.- Configuration Extraction (
ExtractorFn): A user-provided function that takes anAbstractCombinationand converts it into a concrete, strongly-typed configuration struct (Cfg) that your benchmark logic will consume. This is the crucial bridge between the generic framework and your specific code. - Benchmark Suites (
SyncBenchmarkSuite,AsyncBenchmarkSuite): These are the main entry points for defining and running parameterized benchmarks. They create a single Criterion benchmark group and register each parameter combination as a separate, named benchmark within it. - Benchmark Lifecycle Functions: You provide these functions to the suites:
- Setup Function (
setup_fn): Prepares the necessary state (S) and an optional context (CtxT) for a benchmark sample (a batch of iterations). This runs once per sample and is excluded from timing. - Benchmark Logic Function (
benchmark_logic_fn): Contains the actual code to be measured. It receives theSandCtxT, performs operations, and returns the updatedS,CtxT, and the measuredDuration. - Teardown Function (
teardown_fn): Cleans up resources after the benchmark sample. This runs once per sample and is excluded from timing. - Global Setup/Teardown Functions (
GlobalSetupFn,GlobalTeardownFn): These run once per concrete configuration (Cfg), bracketing all benchmark definitions for that specific configuration. They are ideal for expensive setup that can be shared across multiple samples of the same configuration.
- Setup Function (
- User-Defined Types (
Cfg,S,CtxT):Cfg: Your custom struct holding the specific parameters for a benchmark variant (e.g.,packet_size,algorithm_type).S(State): Your custom struct holding the state needed for the benchmark (e.g., a data buffer, a list of connections).CtxT(Context): Your custom struct for carrying context across iterations within a single sample if needed (e.g., counting total operations performed).
This example benchmarks a simple data processing task with varying data sizes and processing intensities.
// In your benches/my_sync_bench.rs
use bench_matrix::{
criterion_runner::sync_suite::SyncBenchmarkSuite,
AbstractCombination, MatrixCellValue, SyncSetupFn, SyncBenchmarkLogicFn, SyncTeardownFn,
};
use criterion::{criterion_group, criterion_main, Criterion, Throughput};
use std::time::{Duration, Instant};
// 1. Define Configuration, State, and Context
#[derive(Debug, Clone)]
pub struct ConfigSync {
pub data_elements: usize,
pub intensity_level: String,
}
#[derive(Debug, Default)]
struct SyncContext { items_processed: usize }
struct SyncState { dataset: Vec<u64> }
// 2. Implement Extractor Function
fn extract_config(combo: &AbstractCombination) -> Result<ConfigSync, String> {
Ok(ConfigSync {
data_elements: combo.get_u64(0)? as usize, // Corresponds to "Elements"
intensity_level: combo.get_string(1)?.to_string(), // Corresponds to "Intensity"
})
}
// 3. Implement Lifecycle Functions
fn setup_fn(cfg: &ConfigSync) -> Result<(SyncContext, SyncState), String> {
// Setup logic here...
Ok((SyncContext::default(), SyncState { dataset: vec![0; cfg.data_elements] }))
}
fn benchmark_logic_fn(mut ctx: SyncContext, state: SyncState, _cfg: &ConfigSync) -> (SyncContext, SyncState, Duration) {
let start = Instant::now();
// Your benchmark logic...
ctx.items_processed += state.dataset.len();
(ctx, state, start.elapsed())
}
fn teardown_fn(_ctx: SyncContext, _state: SyncState, _cfg: &ConfigSync) {
// Teardown logic here...
}
// 4. Define Benchmark Suite in main benchmark function
fn benchmark_sync(c: &mut Criterion) {
let parameter_axes = vec![
vec![MatrixCellValue::Unsigned(100), MatrixCellValue::Unsigned(1000)],
vec![MatrixCellValue::String("Low".to_string()), MatrixCellValue::String("High".to_string())],
];
let parameter_names = vec!["Elements".to_string(), "Intensity".to_string()];
let suite = SyncBenchmarkSuite::new(
c, "MySyncSuite".to_string(), None, parameter_axes,
Box::new(extract_config),
setup_fn,
benchmark_logic_fn,
teardown_fn,
)
.parameter_names(parameter_names) // Set names using the builder method
.throughput(|cfg: &ConfigSync| Throughput::Elements(cfg.data_elements as u64));
suite.run();
}
criterion_group!(benches, benchmark_sync);
criterion_main!(benches);Resulting benchmark ID example: MySyncSuite/Elements-100_Intensity-High
This example simulates an asynchronous network operation.
// In your benches/my_async_bench.rs
use bench_matrix::{
criterion_runner::async_suite::AsyncBenchmarkSuite,
AbstractCombination, MatrixCellValue, AsyncSetupFn, AsyncBenchmarkLogicFn, AsyncTeardownFn
};
use criterion::{criterion_group, criterion_main, Criterion, Throughput};
use std::{future::Future, pin::Pin, time::{Duration, Instant}};
use tokio::runtime::Runtime;
// 1. Define Configuration, State, and Context
#[derive(Debug, Clone)]
pub struct ConfigAsync {
pub packet_size_bytes: u32,
pub concurrent_ops: u16,
}
#[derive(Debug, Default)]
struct AsyncContext { ops_this_iteration: u32 }
struct AsyncState { data: Vec<u8> }
// 2. Implement Extractor Function
fn extract_config_async(combo: &AbstractCombination) -> Result<ConfigAsync, String> {
Ok(ConfigAsync {
packet_size_bytes: combo.get_u64(0)? as u32, // Corresponds to "PktSize"
concurrent_ops: combo.get_u64(1)? as u16, // Corresponds to "ConcurrentOps"
})
}
// 3. Implement Async Lifecycle Functions
fn setup_fn_async(_rt: &Runtime, cfg: &ConfigAsync) -> Pin<Box<dyn Future<Output = Result<(AsyncContext, AsyncState), String>> + Send>> {
let cfg_clone = cfg.clone();
Box::pin(async move {
Ok((AsyncContext::default(), AsyncState { data: vec![0; cfg_clone.packet_size_bytes as usize] }))
})
}
// ... (benchmark_logic_fn_async and teardown_fn_async implementation omitted)
// 4. Define Benchmark Suite
fn benchmark_async(c: &mut Criterion) {
let rt = Runtime::new().expect("Failed to create Tokio runtime");
let parameter_axes = vec![
vec![MatrixCellValue::Unsigned(64), MatrixCellValue::Unsigned(512)],
vec![MatrixCellValue::Unsigned(1), MatrixCellValue::Unsigned(4)],
];
let parameter_names = vec!["PktSize".to_string(), "ConcurrentOps".to_string()];
let suite = AsyncBenchmarkSuite::new(
c, &rt, "MyAsyncSuite".to_string(), None, parameter_axes,
Box::new(extract_config_async),
setup_fn_async, benchmark_logic_fn_async, teardown_fn_async,
)
.parameter_names(parameter_names)
.throughput(|cfg: &ConfigAsync| Throughput::Elements(cfg.concurrent_ops as u64));
suite.run();
}
criterion_group!(benches, benchmark_async);
criterion_main!(benches);Resulting benchmark ID example: MyAsyncSuite/PktSize-64_ConcurrentOps-1
An enum that represents a single value on a parameter axis.
- Variants:
Tag(String),String(String),Int(i64),Unsigned(u64),Bool(bool). - Usage: It includes
From<T>implementations for native types like&'static str,u64,bool, etc., making axis definitions more ergonomic.
You define your parameter space as a Vec<Vec<MatrixCellValue>>. Each inner vector is an axis. You can optionally provide a Vec<String> of the same length containing human-readable names for these axes.
A struct containing a Vec<MatrixCellValue>, representing one complete benchmark variant. It's the input to your ExtractorFn.
- Key Methods:
get_u64(index),get_string(index), etc.: For safely extracting typed values by index.id_suffix()andid_suffix_with_names(): Used internally to create benchmark IDs.
This function is your responsibility. It bridges bench_matrix's generic representation to your specific code.
- Signature:
Fn(&AbstractCombination) -> Result<Cfg, Err> - Purpose: To take an
AbstractCombinationand produce your strongly-typedCfgstruct. You will use theget_*methods on the combination to access values by index.
The generate_combinations function is used to create an iterator over all unique combinations from a set of parameter axes. This is called internally by the suites but is part of the public API.
- Signature:
pub fn generate_combinations(axes: &[Vec<MatrixCellValue>]) -> CombinationIterator - Description: Takes a slice of axes and returns a
CombinationIterator. This iterator is lazy, meaning it generates combinations on the fly, making it highly memory-efficient. It also implementsExactSizeIterator, so you can call.len()to get the total number of combinations without consuming it.
- Description: Orchestrates benchmarks of synchronous code. It creates a single benchmark group and registers each parameter combination as a separate benchmark within that group.
- Constructor:
pub fn new(...) -> Self. Requires a&mut Criterion, a suite name, parameter axes, and the lifecycle function pointers. - Key Type Aliases:
SyncSetupFn,SyncBenchmarkLogicFn,SyncTeardownFn. - Execution: The
pub fn run(mut self)method consumes the suite and executes all defined benchmark combinations.
- Description: Orchestrates benchmarks of asynchronous code. Like the sync suite, it creates one group for all variants. It requires a reference to a
tokio::runtime::Runtime. - Constructor:
pub fn new(...) -> Self. Requires a&mut Criterion,&Runtime, a suite name, axes, and async lifecycle function pointers. - Key Type Aliases: These all involve
Pin<Box<dyn Future<...>>>:AsyncSetupFn: Async logic to set up state for a benchmark sample.AsyncBenchmarkLogicFn: The async code to be benchmarked.AsyncTeardownFn: Async logic to clean up after a benchmark sample.
- Execution: The
pub fn run(mut self)method consumes the suite and executes the benchmarks.
Both SyncBenchmarkSuite and AsyncBenchmarkSuite use a builder pattern, allowing you to chain these methods after new().
While parameter_names can be passed to the new constructor as None, it's often cleaner to use the dedicated builder method. This is the recommended approach.
pub fn parameter_names(self, names: Vec<String>) -> Self(Available on both suites)
These functions are executed once per concrete Cfg variant, outside of the Criterion sampling loop.
pub fn global_setup(self, f: impl FnMut(&Cfg) -> Result<(), String> + 'static) -> Selfpub fn global_teardown(self, f: impl FnMut(&Cfg) -> Result<(), String> + 'static) -> Self
This allows you to configure properties of the entire benchmark group, such as sample size, measurement time, or plot settings.
pub fn configure_criterion_group(self, f: impl for<'g> Fn(&mut BenchmarkGroup<'g, WallTime>) + 'static) -> Self- Provides a closure to customize the main
criterion::BenchmarkGroupfor the entire suite. Example:.configure_criterion_group(|group| group.sample_size(100).measurement_time(Duration::from_secs(5)))
- Provides a closure to customize the main
Set the throughput for each benchmark variant dynamically based on its configuration.
pub fn throughput(self, f: impl Fn(&Cfg) -> Throughput + 'static) -> Self- Provides a closure to calculate
criterion::Throughputfor each individual benchmark variant based on itsCfg. For example,Throughput::Bytes(cfg.packet_size as u64).
- Provides a closure to calculate
bench_matrix is designed to be robust, preventing a single faulty configuration from halting the entire benchmark suite.
- Extraction & Global Setup Failures: If your
ExtractorFnorGlobalSetupFnreturns anErr,bench_matrixwill print a descriptive error message tostderrand skip all benchmarks for that specific combination. The suite will then continue with the next combination. - Per-Sample Setup Failures: If the
setup_fn(sync or async) called within Criterion's sampling loop returns anErr, the suite willpanic!with a detailed message. This is considered a non-recoverable error for that specific benchmark variant. - User Logic: You are responsible for handling errors within your
benchmark_logic_fnandteardown_fnas appropriate for your use case.