par_bench/lib.rs
1#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3
4//! Multi-threaded benchmark execution framework for performance testing.
5//!
6//! This package provides utilities to execute multi-threaded benchmarks with precise control
7//! over thread groups, state management, and measurement timing. It is designed to integrate
8//! with benchmarking frameworks like Criterion while handling the complexities of coordinated
9//! multi-threaded execution.
10//!
11//! The core functionality includes:
12//! - [`Run`] - Configurable multi-threaded benchmark execution with builder pattern API
13//! - [`ThreadPool`] - Pre-warmed thread pool to eliminate thread creation overhead in benchmarks
14//! - [`RunMeta`] - Metadata about the benchmark run, including group information and iteration counts
15//! - [`RunSummary`] - Results from benchmark execution, including timing and measurement data
16//!
17//! This package is not meant for use in production, serving only as a development tool for
18//! benchmarking and performance analysis.
19//!
20//! # Operating principles
21//!
22//! ## Thread groups
23//!
24//! Benchmarks can divide threads into equal-sized groups, allowing for scenarios where different
25//! groups perform different roles (e.g., readers vs writers, producers vs consumers). Each thread
26//! receives metadata about which group it belongs to and can behave differently based on this.
27//!
28//! ## State management
29//!
30//! The framework supports multiple levels of state:
31//! - **Thread State**: Created once per thread, shared across all iterations
32//! - **Iteration State**: Created for each iteration, allowing per-iteration setup
33//! - **Cleanup State**: Returned by iteration functions, dropped after measurement
34//!
35//! ## Measurement timing
36//!
37//! Measurement wrappers allow precise control over what gets measured. The framework separates
38//! preparation (unmeasured) from execution (measured) phases, ensuring benchmarks capture only
39//! the intended work.
40//!
41//! # Basic example
42//!
43//! ```
44//! use std::sync::Arc;
45//! use std::sync::atomic::{AtomicU64, Ordering};
46//!
47//! use many_cpus::SystemHardware;
48//! use par_bench::{Run, ThreadPool};
49//!
50//! # fn main() {
51//! // Create a thread pool with default processor set
52//! let mut pool = ThreadPool::new(&SystemHardware::current().processors());
53//!
54//! // Shared counter for all threads to increment
55//! let counter = Arc::new(AtomicU64::new(0));
56//!
57//! let run = Run::new()
58//! .prepare_thread({
59//! let counter = Arc::clone(&counter);
60//! move |_| Arc::clone(&counter)
61//! })
62//! .prepare_iter(|args| Arc::clone(args.thread_state()))
63//! .iter(|mut args| {
64//! // This is the measured work
65//! args.iter_state().fetch_add(1, Ordering::Relaxed);
66//! });
67//!
68//! // Execute 1000 iterations across all threads
69//! let results = run.execute_on(&mut pool, 1000);
70//! println!("Mean duration: {:?}", results.mean_duration());
71//! # }
72//! ```
73//!
74//! # Multi-group example
75//!
76//! ```
77//! use std::sync::Arc;
78//! use std::sync::atomic::{AtomicU64, Ordering};
79//!
80//! use many_cpus::SystemHardware;
81//! use new_zealand::nz;
82//! use par_bench::{Run, ThreadPool};
83//!
84//! # fn main() {
85//! # if let Some(processors) = SystemHardware::current().processors().to_builder().take(nz!(4)) {
86//! let mut pool = ThreadPool::new(&processors);
87//!
88//! let reader_count = Arc::new(AtomicU64::new(0));
89//! let writer_count = Arc::new(AtomicU64::new(0));
90//!
91//! let run = Run::new()
92//! .groups(nz!(2)) // Divide 4 threads into 2 groups of 2 threads each
93//! .prepare_thread({
94//! let reader_count = Arc::clone(&reader_count);
95//! let writer_count = Arc::clone(&writer_count);
96//! move |args| {
97//! if args.meta().group_index() == 0 {
98//! ("reader", Arc::clone(&reader_count))
99//! } else {
100//! ("writer", Arc::clone(&writer_count))
101//! }
102//! }
103//! })
104//! .prepare_iter(|args| args.thread_state().clone())
105//! .iter(|mut args| {
106//! let (role, counter) = args.take_iter_state();
107//! match role {
108//! "reader" => {
109//! // Reader work
110//! counter.fetch_add(1, Ordering::Relaxed);
111//! }
112//! "writer" => {
113//! // Writer work
114//! counter.fetch_add(10, Ordering::Relaxed);
115//! }
116//! _ => unreachable!(),
117//! }
118//! });
119//!
120//! let results = run.execute_on(&mut pool, 100);
121//! println!("Results: {:?}", results.mean_duration());
122//! # }
123//! # }
124//! ```
125//!
126//! # Resource usage tracking
127//!
128#![cfg_attr(
129 any(feature = "alloc_tracker", feature = "all_the_time"),
130 doc = "When either the `alloc_tracker` or `all_the_time` features are enabled, the [`ResourceUsageExt`]"
131)]
132#![cfg_attr(
133 not(any(feature = "alloc_tracker", feature = "all_the_time")),
134 doc = "When either the `alloc_tracker` or `all_the_time` features are enabled, the `ResourceUsageExt`"
135)]
136//! extension trait becomes available, providing convenient resource usage tracking for benchmarks:
137//!
138//! ```ignore
139//! use alloc_tracker::{Allocator, Session as AllocSession};
140//! use all_the_time::Session as TimeSession;
141//! use par_bench::{ResourceUsageExt, Run, ThreadPool};
142//!
143//! #[global_allocator]
144//! static ALLOCATOR: Allocator<std::alloc::System> = Allocator::system();
145//!
146//! let allocs = AllocSession::new();
147//! let processor_time = TimeSession::new();
148//! let mut pool = ThreadPool::new(&SystemHardware::current().processors().take_all().unwrap());
149//!
150//! let results = Run::new()
151//! .measure_resource_usage(|measure| {
152//! measure
153//! .allocs(&allocs, "my_operation")
154//! .processor_time(&processor_time, "my_operation")
155//! })
156//! .iter(|_| {
157//! let _data = vec![1, 2, 3, 4, 5]; // This allocates memory
158//!
159//! // Perform processor-intensive work
160//! let mut sum = 0;
161//! for i in 0..1000 {
162//! sum += i * i;
163//! }
164//! std::hint::black_box(sum);
165//! })
166//! .execute_on(&mut pool, 1000);
167//!
168//! // Access the combined resource usage data
169//! for output in results.measure_outputs() {
170//! if let Some(alloc_report) = output.allocs() {
171//! println!("Allocation data available");
172//! }
173//! if let Some(time_report) = output.processor_time() {
174//! println!("Processor time data available");
175//! }
176//! }
177//! ```
178//!
179//! You can also use just one type of measurement:
180//!
181//! ```ignore
182//! // Just allocation tracking
183//! let results = Run::new()
184//! .measure_resource_usage(|measure| {
185//! measure.allocs(&allocs, "alloc_only")
186//! })
187//! .iter(|_| { /* work */ })
188//! .execute_on(&mut pool, 1000);
189//!
190//! // Just processor time tracking
191//! let results = Run::new()
192//! .measure_resource_usage(|measure| {
193//! measure.processor_time(&processor_time, "time_only")
194//! })
195//! .iter(|_| { /* work */ })
196//! .execute_on(&mut pool, 1000);
197//! ```
198
199mod run;
200mod run_configured;
201mod run_configured_criterion;
202mod run_meta;
203mod threadpool;
204
205// These are in a separate module because 99% of the time the user never needs to name
206// these types, so it makes sense to de-emphasize them in the API documentation.
207pub mod args;
208pub mod configure;
209
210#[cfg(any(feature = "alloc_tracker", feature = "all_the_time"))]
211mod resource_usage_ext;
212
213#[cfg(any(feature = "alloc_tracker", feature = "all_the_time"))]
214pub use resource_usage_ext::*;
215pub use run::*;
216pub use run_configured::*;
217pub use run_meta::*;
218pub use threadpool::*;