344 lines
11 KiB
Rust
344 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
|
||
|
|
}
|