feat: Stress tests, benchmarking, and non-failing queues and inboxes #3
Adds some basic benchmarking, stress tests. They still need to be properly examined to ensure they are testing the correct properties, but fit for "good enough". Implements the HybridChannel type, which features a channel buffer that can withstand overflows. It does so by providing a dequeue behind a mutex. Without overflow, will push messages into the lock free ArrayQueue implemented by crossbeam_queue; when that buffer fills, will use the locking portion provided by the Mutex<VecDequeue>.
In the future we can even further optimize this, perhaps with some linked list implementations of lock-free channels, but, like the benchmarks, this fits the "good enough" bar for now.
2026-01-26 07:14:00 +00:00
|
|
|
//! Stress test utilities and result reporting.
|
|
|
|
|
//!
|
|
|
|
|
//! Provides a simple framework for stress tests with JSON + pretty output.
|
|
|
|
|
|
|
|
|
|
#![allow(dead_code)] // Utilities may not all be used in every test
|
|
|
|
|
|
|
|
|
|
pub mod concurrency;
|
|
|
|
|
pub mod saturation;
|
|
|
|
|
|
|
|
|
|
use std::time::{Duration, Instant};
|
|
|
|
|
|
|
|
|
|
/// Results from a stress test
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct StressResult {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub duration: Duration,
|
|
|
|
|
pub operations: u64,
|
|
|
|
|
pub successes: u64,
|
|
|
|
|
pub failures: u64,
|
|
|
|
|
pub notes: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl StressResult {
|
|
|
|
|
pub fn new(name: impl Into<String>) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
name: name.into(),
|
|
|
|
|
duration: Duration::ZERO,
|
|
|
|
|
operations: 0,
|
|
|
|
|
successes: 0,
|
|
|
|
|
failures: 0,
|
|
|
|
|
notes: Vec::new(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn failure_rate(&self) -> f64 {
|
|
|
|
|
if self.operations == 0 {
|
|
|
|
|
0.0
|
|
|
|
|
} else {
|
|
|
|
|
(self.failures as f64 / self.operations as f64) * 100.0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn throughput(&self) -> f64 {
|
|
|
|
|
let secs = self.duration.as_secs_f64();
|
|
|
|
|
if secs > 0.0 {
|
|
|
|
|
self.operations as f64 / secs
|
|
|
|
|
} else {
|
|
|
|
|
0.0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn note(&mut self, msg: impl Into<String>) {
|
|
|
|
|
self.notes.push(msg.into());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn print(&self) {
|
|
|
|
|
println!("\n{}", "=".repeat(60));
|
|
|
|
|
println!(" STRESS: {}", self.name);
|
|
|
|
|
println!("{}", "=".repeat(60));
|
|
|
|
|
println!(" Duration: {:?}", self.duration);
|
|
|
|
|
println!(" Operations: {}", self.operations);
|
|
|
|
|
println!(" Successes: {}", self.successes);
|
|
|
|
|
println!(" Failures: {}", self.failures);
|
|
|
|
|
println!(" Failure Rate: {:.2}%", self.failure_rate());
|
|
|
|
|
println!(" Throughput: {:.2} ops/sec", self.throughput());
|
|
|
|
|
|
|
|
|
|
if !self.notes.is_empty() {
|
|
|
|
|
println!();
|
|
|
|
|
println!(" Notes:");
|
|
|
|
|
for note in &self.notes {
|
|
|
|
|
println!(" - {}", note);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
println!("{}", "=".repeat(60));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn to_json(&self) -> String {
|
|
|
|
|
format!(
|
|
|
|
|
r#"{{"name":"{}","duration_ms":{},"operations":{},"successes":{},"failures":{},"failure_rate_pct":{:.2},"throughput":{:.2},"notes":{:?}}}"#,
|
|
|
|
|
self.name,
|
|
|
|
|
self.duration.as_millis(),
|
|
|
|
|
self.operations,
|
|
|
|
|
self.successes,
|
|
|
|
|
self.failures,
|
|
|
|
|
self.failure_rate(),
|
|
|
|
|
self.throughput(),
|
|
|
|
|
self.notes
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A simple stress test runner
|
|
|
|
|
pub struct Stress {
|
|
|
|
|
name: String,
|
|
|
|
|
duration: Option<Duration>,
|
|
|
|
|
iterations: Option<u64>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Stress {
|
|
|
|
|
pub fn new(name: impl Into<String>) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
name: name.into(),
|
|
|
|
|
duration: None,
|
|
|
|
|
iterations: None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Run for a fixed duration
|
|
|
|
|
pub fn for_duration(mut self, d: Duration) -> Self {
|
|
|
|
|
self.duration = Some(d);
|
|
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Run for a fixed number of iterations
|
|
|
|
|
pub fn for_iterations(mut self, n: u64) -> Self {
|
|
|
|
|
self.iterations = Some(n);
|
|
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Run the stress test, counting successes and failures
|
|
|
|
|
pub fn run<F>(self, mut f: F) -> StressResult
|
|
|
|
|
where
|
|
|
|
|
F: FnMut() -> bool, // returns true on success, false on failure
|
|
|
|
|
{
|
|
|
|
|
let mut result = StressResult::new(&self.name);
|
|
|
|
|
let start = Instant::now();
|
|
|
|
|
|
|
|
|
|
match (self.duration, self.iterations) {
|
|
|
|
|
(Some(duration), _) => {
|
|
|
|
|
while start.elapsed() < duration {
|
|
|
|
|
if f() {
|
|
|
|
|
result.successes += 1;
|
|
|
|
|
} else {
|
|
|
|
|
result.failures += 1;
|
|
|
|
|
}
|
|
|
|
|
result.operations += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
(None, Some(iterations)) => {
|
|
|
|
|
for _ in 0..iterations {
|
|
|
|
|
if f() {
|
|
|
|
|
result.successes += 1;
|
|
|
|
|
} else {
|
|
|
|
|
result.failures += 1;
|
|
|
|
|
}
|
|
|
|
|
result.operations += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
(None, None) => {
|
|
|
|
|
// Default: 1000 iterations
|
|
|
|
|
for _ in 0..1000 {
|
|
|
|
|
if f() {
|
|
|
|
|
result.successes += 1;
|
|
|
|
|
} else {
|
|
|
|
|
result.failures += 1;
|
|
|
|
|
}
|
|
|
|
|
result.operations += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.duration = start.elapsed();
|
|
|
|
|
result
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test actors used across stress tests
|
2026-02-06 07:06:37 +00:00
|
|
|
use swactor::{actor::ActorInterface, runtime::Ctx};
|
feat: Stress tests, benchmarking, and non-failing queues and inboxes #3
Adds some basic benchmarking, stress tests. They still need to be properly examined to ensure they are testing the correct properties, but fit for "good enough". Implements the HybridChannel type, which features a channel buffer that can withstand overflows. It does so by providing a dequeue behind a mutex. Without overflow, will push messages into the lock free ArrayQueue implemented by crossbeam_queue; when that buffer fills, will use the locking portion provided by the Mutex<VecDequeue>.
In the future we can even further optimize this, perhaps with some linked list implementations of lock-free channels, but, like the benchmarks, this fits the "good enough" bar for now.
2026-01-26 07:14:00 +00:00
|
|
|
|
|
|
|
|
/// An actor that just absorbs messages
|
|
|
|
|
pub struct BlackHole;
|
|
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub struct Msg;
|
|
|
|
|
|
|
|
|
|
impl ActorInterface for BlackHole {
|
|
|
|
|
type Incoming = Msg;
|
|
|
|
|
type Response = ();
|
2026-02-05 15:54:03 +00:00
|
|
|
fn handle(&mut self, _ctx: &Ctx, _msg: Msg) {}
|
feat: Stress tests, benchmarking, and non-failing queues and inboxes #3
Adds some basic benchmarking, stress tests. They still need to be properly examined to ensure they are testing the correct properties, but fit for "good enough". Implements the HybridChannel type, which features a channel buffer that can withstand overflows. It does so by providing a dequeue behind a mutex. Without overflow, will push messages into the lock free ArrayQueue implemented by crossbeam_queue; when that buffer fills, will use the locking portion provided by the Mutex<VecDequeue>.
In the future we can even further optimize this, perhaps with some linked list implementations of lock-free channels, but, like the benchmarks, this fits the "good enough" bar for now.
2026-01-26 07:14:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// An actor that counts messages received
|
|
|
|
|
pub struct Counter {
|
|
|
|
|
pub count: usize,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Counter {
|
|
|
|
|
pub fn new() -> Self {
|
|
|
|
|
Self { count: 0 }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ActorInterface for Counter {
|
|
|
|
|
type Incoming = Msg;
|
|
|
|
|
type Response = ();
|
2026-02-05 15:54:03 +00:00
|
|
|
fn handle(&mut self, _ctx: &Ctx, _msg: Msg) {
|
feat: Stress tests, benchmarking, and non-failing queues and inboxes #3
Adds some basic benchmarking, stress tests. They still need to be properly examined to ensure they are testing the correct properties, but fit for "good enough". Implements the HybridChannel type, which features a channel buffer that can withstand overflows. It does so by providing a dequeue behind a mutex. Without overflow, will push messages into the lock free ArrayQueue implemented by crossbeam_queue; when that buffer fills, will use the locking portion provided by the Mutex<VecDequeue>.
In the future we can even further optimize this, perhaps with some linked list implementations of lock-free channels, but, like the benchmarks, this fits the "good enough" bar for now.
2026-01-26 07:14:00 +00:00
|
|
|
self.count += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|