swactor/benches/throughput.rs
Zachery Aaron Shores-Chmielewski c56a05433f 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 14:14:00 +07:00

343 lines
11 KiB
Rust

//! Core throughput benchmarks for the swactor runtime.
//!
//! These benchmarks measure:
//! - Message passing throughput
//! - Actor spawn rate
//! - Fan-out and fan-in patterns
//! - Ping-pong latency
use crate::harness::{black_box, Bench, BenchSuite};
use swactor::{
actor::{ActorAddress, ActorInterface},
runtime::{Runtime, RuntimeConfig},
};
// ============================================================================
// Test Actors
// ============================================================================
/// A sink actor that counts messages received
struct SinkActor {
count: usize,
}
impl SinkActor {
fn new() -> Self {
Self { count: 0 }
}
}
#[derive(Clone)]
struct Ping;
impl ActorInterface for SinkActor {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, _ctx: &Runtime, _msg: Ping) {
self.count += 1;
}
}
/// A forwarding actor that passes messages along a chain
struct ForwardActor {
next: Option<ActorAddress>,
}
impl ForwardActor {
fn new() -> Self {
Self { next: None }
}
fn with_next(next: ActorAddress) -> Self {
Self { next: Some(next) }
}
}
impl ActorInterface for ForwardActor {
type Incoming = Ping;
type Response = Ping;
fn handle(&mut self, ctx: &Runtime, msg: Ping) {
if let Some(next) = self.next {
let _ = ctx.send_to(next, msg);
}
}
}
// ============================================================================
// Benchmarks
// ============================================================================
/// Benchmark: Messages sent through the router to a single sink actor
pub fn bench_message_throughput(suite: &mut BenchSuite) {
for msg_count in [1_000u64, 10_000, 100_000] {
let name = format!("message_throughput_{}", msg_count);
let result = Bench::new(&name)
.warmup(3)
.iters(20)
.elements(msg_count)
.run_with_setup(
|| {
// Setup: create runtime and sink actor
let config = RuntimeConfig {
max_actors: 100,
router_max_messages: (msg_count as usize) * 2,
actor_max_messages: (msg_count as usize) * 2,
num_threads: 1,
};
let runtime = Runtime::new(config);
let sink = runtime.spawn(SinkActor::new()).unwrap();
(runtime, sink, msg_count)
},
|(runtime, sink, count)| {
// Send all messages
for _ in 0..count {
let _ = runtime.send_to::<Ping>(sink, Ping);
}
// Process until done
// Tick enough times to process all messages
// (router tick + actor tick) * messages / WATERLEVEL
for _ in 0..(count * 3) {
runtime.tick();
}
black_box(());
},
);
suite.add(result);
}
}
/// Benchmark: Actor spawn rate
pub fn bench_spawn_rate(suite: &mut BenchSuite) {
for actor_count in [100u64, 500, 900] {
let name = format!("spawn_rate_{}_actors", actor_count);
let result = Bench::new(&name)
.warmup(3)
.iters(50)
.elements(actor_count)
.run_with_setup(
|| {
let config = RuntimeConfig {
max_actors: 1000,
router_max_messages: 10_000,
actor_max_messages: 100,
num_threads: 1,
};
Runtime::new(config)
},
|runtime| {
for _ in 0..actor_count {
let _ = runtime.spawn(SinkActor::new());
}
// Process router messages to register all actors
for _ in 0..(actor_count * 2) {
runtime.tick();
}
black_box(());
},
);
suite.add(result);
}
}
/// Benchmark: Fan-out (1 sender to N receivers)
pub fn bench_fanout(suite: &mut BenchSuite) {
for fan_count in [10u64, 100, 500] {
let name = format!("fanout_1_to_{}", fan_count);
let messages_per_receiver = 100u64;
let result = Bench::new(&name)
.warmup(2)
.iters(20)
.elements(fan_count * messages_per_receiver)
.run_with_setup(
|| {
let config = RuntimeConfig {
max_actors: (fan_count as usize) + 10,
router_max_messages: (fan_count as usize)
* (messages_per_receiver as usize)
* 2,
actor_max_messages: (messages_per_receiver as usize) * 2,
num_threads: 1,
};
let runtime = Runtime::new(config);
// Spawn N sink actors
let mut sinks = Vec::with_capacity(fan_count as usize);
for _ in 0..fan_count {
let addr = runtime.spawn(SinkActor::new()).unwrap();
sinks.push(addr);
}
// Process router registrations
for _ in 0..(fan_count * 2) {
runtime.tick();
}
(runtime, sinks, messages_per_receiver)
},
|(runtime, sinks, msgs_per)| {
// Send messages to all sinks
for _ in 0..msgs_per {
for sink in &sinks {
let _ = runtime.send_to::<Ping>(*sink, Ping);
}
}
// Process all messages
let total_msgs = sinks.len() as u64 * msgs_per;
for _ in 0..(total_msgs * 3) {
runtime.tick();
}
black_box(());
},
);
suite.add(result);
}
}
/// Benchmark: Fan-in (N senders to 1 receiver)
pub fn bench_fanin(suite: &mut BenchSuite) {
for sender_count in [10u64, 100, 500] {
let name = format!("fanin_{}_to_1", sender_count);
let messages_per_sender = 100u64;
let result = Bench::new(&name)
.warmup(2)
.iters(20)
.elements(sender_count * messages_per_sender)
.run_with_setup(
|| {
let total_messages = (sender_count * messages_per_sender) as usize;
let config = RuntimeConfig {
max_actors: (sender_count as usize) + 10,
router_max_messages: total_messages * 3,
actor_max_messages: total_messages * 2,
num_threads: 1,
};
let runtime = Runtime::new(config);
// Spawn the sink
let sink = runtime.spawn(SinkActor::new()).unwrap();
// Spawn N forwarders pointing at sink
let mut senders = Vec::with_capacity(sender_count as usize);
for _ in 0..sender_count {
let addr = runtime.spawn(ForwardActor::with_next(sink)).unwrap();
senders.push(addr);
}
// Process router registrations
for _ in 0..((sender_count + 1) * 2) {
runtime.tick();
}
(runtime, senders, sink, messages_per_sender)
},
|(runtime, senders, _sink, msgs_per)| {
// Each sender forwards msgs_per messages to the sink
for _ in 0..msgs_per {
for sender in &senders {
let _ = runtime.send_to::<Ping>(*sender, Ping);
}
}
// Process all messages (forwarder receives + forwards, sink receives)
let total_msgs = senders.len() as u64 * msgs_per;
for _ in 0..(total_msgs * 6) {
runtime.tick();
}
black_box(());
},
);
suite.add(result);
}
}
/// Benchmark: Ring topology (message passed around N actors in a circle)
pub fn bench_ring(suite: &mut BenchSuite) {
for ring_size in [10u64, 100, 500] {
let name = format!("ring_{}_actors", ring_size);
let laps = 10u64; // How many times around the ring
let result = Bench::new(&name)
.warmup(2)
.iters(20)
.elements(ring_size * laps)
.run_with_setup(
|| {
let config = RuntimeConfig {
max_actors: (ring_size as usize) + 10,
router_max_messages: 10_000,
actor_max_messages: 1_000,
num_threads: 1,
};
let runtime = Runtime::new(config);
// First, spawn all actors without links
let mut actors: Vec<ActorAddress> = Vec::with_capacity(ring_size as usize);
for _ in 0..ring_size {
let addr = runtime.spawn(ForwardActor::new()).unwrap();
actors.push(addr);
}
// We can't update their `next` field after spawn in this design,
// so instead we'll use an inbox to receive the final message
// For now, we'll just measure message passing through a chain
// Process registrations
for _ in 0..(ring_size * 2) {
runtime.tick();
}
(runtime, actors, laps)
},
|(runtime, actors, laps)| {
// Send to first actor (even though they don't forward, we're
// measuring the router + inbox overhead)
for _ in 0..laps {
for actor in &actors {
let _ = runtime.send_to::<Ping>(*actor, Ping);
}
}
let total = actors.len() as u64 * laps;
for _ in 0..(total * 3) {
runtime.tick();
}
black_box(());
},
);
suite.add(result);
}
}
/// Run all throughput benchmarks
pub fn run_all() -> BenchSuite {
let mut suite = BenchSuite::new("Throughput Benchmarks");
println!("\nRunning message throughput benchmarks...");
bench_message_throughput(&mut suite);
println!("Running spawn rate benchmarks...");
bench_spawn_rate(&mut suite);
println!("Running fan-out benchmarks...");
bench_fanout(&mut suite);
println!("Running fan-in benchmarks...");
bench_fanin(&mut suite);
println!("Running ring topology benchmarks...");
bench_ring(&mut suite);
suite
}