testbench/
lib.rs

1//! Testing and benchmarking tools for concurrent Rust code
2//!
3//! This crate groups together a bunch of utilities which I've found useful when
4//! testing and benchmarking Rust concurrency primitives in the triple_buffer
5//! and spmc_buffer crates.
6//!
7//! If it proves popular, other testing and benchmarking tools may be added,
8//! based on user demand.
9//!
10//! # Examples
11//!
12//! For examples of this crate at work, look at its "tests" and "benchs"
13//! submodules, which showcase expected usage.
14
15#![warn(
16    anonymous_parameters,
17    missing_copy_implementations,
18    missing_debug_implementations,
19    missing_docs,
20    nonstandard_style,
21    rust_2018_idioms,
22    single_use_lifetimes,
23    trivial_casts,
24    trivial_numeric_casts,
25    unreachable_pub,
26    unused_extern_crates,
27    unused_qualifications,
28    variant_size_differences
29)]
30
31pub mod noinline;
32pub mod race_cell;
33
34use std::sync::{
35    atomic::{AtomicBool, Ordering},
36    Barrier,
37};
38
39/// Test that running two operations concurrently works
40///
41/// When testing multi-threaded constructs, such as synchronization primitives,
42/// checking that the code works when run sequentially is insufficient.
43/// Concurrent interactions must also be tested, by running some operations
44/// in parallel across multiple threads.
45///
46/// Ideally, this function would take as input a variable-size set of functors
47/// to be run in parallel. Since Rust does not support variadic generics yet,
48/// however, multiple versions of this function must be provided, each
49/// associated with a different functor tuple size.
50///
51/// # Panics
52///
53/// This function will propagate panics from the inner functors.
54///
55pub fn concurrent_test_2(f1: impl FnOnce() + Send, f2: impl FnOnce() + Send) {
56    let barrier = Barrier::new(2);
57    std::thread::scope(|s| {
58        s.spawn(|| {
59            barrier.wait();
60            noinline::call_once(f1);
61        });
62        barrier.wait();
63        noinline::call_once(f2);
64    })
65}
66
67/// Test that running three operations concurrently works
68///
69/// This is a variant of concurrent_test_2 that works with three functors
70/// instead of two. It is hoped that future evolutions of Rust will render this
71/// (light) code duplication obsolete, in favor of some variadic design.
72///
73/// # Panics
74///
75/// This function will propagate panics from the inner functors.
76///
77pub fn concurrent_test_3(
78    f1: impl FnOnce() + Send,
79    f2: impl FnOnce() + Send,
80    f3: impl FnOnce() + Send,
81) {
82    let barrier = Barrier::new(3);
83    std::thread::scope(|s| {
84        s.spawn(|| {
85            barrier.wait();
86            noinline::call_once(f1);
87        });
88        s.spawn(|| {
89            barrier.wait();
90            noinline::call_once(f2);
91        });
92        barrier.wait();
93        noinline::call_once(f3);
94    })
95}
96
97/// Perform some operation while another is running in a loop in another thread
98///
99/// For multithreaded code, benchmarking the performance of isolated operations
100/// is usually only half of the story. Synchronization and memory contention can
101/// also have a large impact on performance.
102///
103/// For this reason, it is often useful to also measure the performance of one
104/// operation as another "antagonist" operation is also running in a background
105/// thread. This function helps you with setting up such an antagonist thread.
106///
107/// Note that the antagonist function must be designed in such a way as not to
108/// be optimized out by the compiler when run in a tight loop. Here are some
109/// ways to do this:
110///
111/// - You can hide the fact that the code is run in a loop by preventing the
112///   compiler from inlining it there, see this crate's `noinline::call_mut()`.
113/// - You can obscure the fact that inputs are always the same and outputs are
114///   are not used by using `core::hint::black_box()` on nightly Rust, or its
115///   emulation by the Criterion benchmarking crate.
116/// - You can generate inputs that the compiler cannot guess using a random
117///   number generator, and use your outputs by sending them through some sort
118///   of reduction function (sum, min, max...) and checking the result.
119///
120pub fn run_under_contention<AntagonistResult, BenchmarkResult>(
121    mut antagonist: impl FnMut() -> AntagonistResult + Send,
122    mut benchmark: impl FnMut() -> BenchmarkResult,
123) -> BenchmarkResult {
124    let start_barrier = Barrier::new(2);
125    let continue_flag = AtomicBool::new(true);
126    std::thread::scope(|s| {
127        s.spawn(|| {
128            start_barrier.wait();
129            while continue_flag.load(Ordering::Relaxed) {
130                antagonist();
131            }
132        });
133        start_barrier.wait();
134        let result = benchmark();
135        continue_flag.store(false, Ordering::Relaxed);
136        result
137    })
138}
139
140/// Examples of concurrent testing code
141#[cfg(test)]
142mod tests {
143    use std::{
144        sync::atomic::{AtomicUsize, Ordering},
145        time::Duration,
146    };
147
148    // Check the behaviour of concurrent atomic swaps and fetch-adds
149    #[test]
150    fn swap_and_fetch_add() {
151        // Amount of atomic operations to check
152        const ATOMIC_OPS_COUNT: usize = 100_000_000;
153
154        // Create a shared atomic variable
155        let atom = AtomicUsize::new(0);
156
157        // Check that concurrent atomic operations work correctly
158        let mut last_value = 0;
159        super::concurrent_test_2(
160            || {
161                // One thread continuously increments the atomic variable...
162                for _ in 1..=ATOMIC_OPS_COUNT {
163                    let former_atom = atom.fetch_add(1, Ordering::Relaxed);
164                    assert!((former_atom == 0) || (former_atom == last_value));
165                    last_value = former_atom + 1;
166                }
167            },
168            || {
169                // ...as another continuously resets it to zero
170                for _ in 1..=ATOMIC_OPS_COUNT {
171                    let former_atom = atom.swap(0, Ordering::Relaxed);
172                    assert!(former_atom <= ATOMIC_OPS_COUNT);
173                }
174            },
175        );
176    }
177
178    // Check the behaviour of concurrent fetch-and/or/xors
179    #[test]
180    fn fetch_and_or_xor() {
181        // Amount of atomic operations to check
182        const ATOMIC_OPS_COUNT: usize = 30_000_000;
183
184        // Create a shared atomic variable. Even though this is an atomic Usize,
185        // we will only use the 16 low-order bits for maximal portability.
186        let atom = AtomicUsize::new(0);
187
188        // Masks used by each atomic operation
189        const AND_MASK: usize = 0b0000_0000_0000_0000; // Clear all bits
190        const XOR_MASK: usize = 0b0000_1111_0000_1111; // Flip some bits
191        const OR_MASK: usize = 0b1111_0000_1111_0000; // Set other bits
192
193        // Check that concurrent atomic operations work correctly by ensuring
194        // that at any point in time, only the 16 low-order bits can be set, and
195        // the grouped sets of bits in the masks above are either all set or
196        // all cleared in any observable state.
197        super::concurrent_test_3(
198            || {
199                // One thread runs fetch-ands in a loop...
200                for _ in 1..=ATOMIC_OPS_COUNT {
201                    let old_val = atom.fetch_and(AND_MASK, Ordering::Relaxed);
202                    assert_eq!(old_val & 0b1111_1111_1111_1111, old_val);
203                    assert!((old_val & XOR_MASK == XOR_MASK) || (old_val & XOR_MASK == 0));
204                    assert!((old_val & OR_MASK == OR_MASK) || (old_val & OR_MASK == 0));
205                }
206            },
207            || {
208                // ...another runs fetch-ors in a loop...
209                for _ in 1..=ATOMIC_OPS_COUNT {
210                    let old_val = atom.fetch_or(OR_MASK, Ordering::Relaxed);
211                    assert_eq!(old_val & 0b1111_1111_1111_1111, old_val);
212                    assert!((old_val & XOR_MASK == XOR_MASK) || (old_val & XOR_MASK == 0));
213                    assert!((old_val & OR_MASK == OR_MASK) || (old_val & OR_MASK == 0));
214                }
215            },
216            || {
217                // ...and the last one runs fetch-xors in a loop...
218                for _ in 1..=ATOMIC_OPS_COUNT {
219                    let old_val = atom.fetch_xor(XOR_MASK, Ordering::Relaxed);
220                    assert_eq!(old_val & 0b1111_1111_1111_1111, old_val);
221                    assert!((old_val & XOR_MASK == XOR_MASK) || (old_val & XOR_MASK == 0));
222                    assert!((old_val & OR_MASK == OR_MASK) || (old_val & OR_MASK == 0));
223                }
224            },
225        );
226    }
227
228    // Show how adversarial code is actually run in concurrent "benchmarking"
229    #[test]
230    fn antagonist_showcase() {
231        let atom = AtomicUsize::new(0);
232        super::run_under_contention(
233            || atom.fetch_add(1, Ordering::Relaxed),
234            || std::thread::sleep(Duration::from_millis(100)),
235        );
236        assert!(atom.load(Ordering::Relaxed) > 100000);
237    }
238}