//! 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::{ Ctx, 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: &Ctx, _msg: Ping) { self.count += 1; } } /// A forwarding actor that passes messages along a chain struct ForwardActor { next: Option, } 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: &Ctx, msg: Ping) { if let Some(next) = self.next { let _ = ctx.send(next, msg); } } } // ============================================================================ // Benchmarks // ============================================================================ /// Benchmark: Messages sent through the runtime 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(5) .iters(100) .elements(msg_count) .run_with_setup( || { // Setup: create runtime and sink actor let config = RuntimeConfig { max_actors: 100, actor_max_messages: (msg_count as usize) * 2, num_threads: 1, ..Default::default() }; 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::(sink, Ping); } // Process until done 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(5) .iters(100) .elements(actor_count) .run_with_setup( || { let config = RuntimeConfig { max_actors: 1000, actor_max_messages: 100, num_threads: 1, ..Default::default() }; Runtime::new(config) }, |runtime| { for _ in 0..actor_count { let _ = runtime.spawn(SinkActor::new()); } // Process spawns 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, messages_per_receiver, warmup_n, iters_n) in [ (10u64, 500u64, 5usize, 50usize), (100, 200, 5, 40), (500, 100, 5, 30), ] { let name = format!("fanout_1_to_{}", fan_count); let result = Bench::new(&name) .warmup(warmup_n) .iters(iters_n) .elements(fan_count * messages_per_receiver) .run_with_setup( || { let config = RuntimeConfig { max_actors: (fan_count as usize) + 10, actor_max_messages: (fan_count as usize) * (messages_per_receiver as usize) * 2, num_threads: 1, ..Default::default() }; 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 spawns 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::(*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, messages_per_sender, warmup_n, iters_n) in [ (10u64, 300u64, 5usize, 50usize), (100, 100, 5, 30), (500, 100, 5, 20), ] { let name = format!("fanin_{}_to_1", sender_count); let result = Bench::new(&name) .warmup(warmup_n) .iters(iters_n) .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, actor_max_messages: total_messages * 3, num_threads: 1, ..Default::default() }; 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 spawns 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::(*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, laps, warmup_n, iters_n) in [ (10u64, 100u64, 5usize, 100usize), (100, 20, 5, 50), (500, 10, 5, 40), ] { let name = format!("ring_{}_actors", ring_size); let result = Bench::new(&name) .warmup(warmup_n) .iters(iters_n) .elements(ring_size * laps) .run_with_setup( || { let config = RuntimeConfig { max_actors: (ring_size as usize) + 10, actor_max_messages: 10_000, num_threads: 1, ..Default::default() }; let runtime = Runtime::new(config); // First, spawn all actors without links let mut actors: Vec = 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 spawns 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 transfer queue + inbox overhead) for _ in 0..laps { for actor in &actors { let _ = runtime.send_to::(*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 }