Got a ton of benchmarking and stress testing code spit out by the LLM. Did a not-so-thorough vetting, not ready for merge into master, but it mostly makes sense. Needs to actually fail on the stress tests, not anticipate failure and call it success. The benchmarks also need a more thorough going over, in order to validate that they make sense. Then we can work on improving our metrics.
343 lines
11 KiB
Rust
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
|
|
}
|