cfuzz (#22)
This commit is contained in:
parent
9beca6c5dc
commit
09504d0b67
11 changed files with 391 additions and 199 deletions
|
|
@ -3,32 +3,6 @@ use std::collections::VecDeque;
|
||||||
use criterion::{
|
use criterion::{
|
||||||
criterion_group, criterion_main, BenchmarkId, Criterion, Throughput,
|
criterion_group, criterion_main, BenchmarkId, Criterion, Throughput,
|
||||||
};
|
};
|
||||||
use swactor::worker::drain_count;
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// drain_count O(1) verification
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
fn bench_drain_count(c: &mut Criterion) {
|
|
||||||
let mut group = c.benchmark_group("drain_count");
|
|
||||||
|
|
||||||
// Below waterlevel
|
|
||||||
group.bench_function("below", |b| {
|
|
||||||
b.iter(|| std::hint::black_box(drain_count(50, 100)));
|
|
||||||
});
|
|
||||||
|
|
||||||
// At waterlevel
|
|
||||||
group.bench_function("at", |b| {
|
|
||||||
b.iter(|| std::hint::black_box(drain_count(100, 100)));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Above waterlevel
|
|
||||||
group.bench_function("above", |b| {
|
|
||||||
b.iter(|| std::hint::black_box(drain_count(500, 100)));
|
|
||||||
});
|
|
||||||
|
|
||||||
group.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// VecDeque push throughput (mirrors old mailbox_push)
|
// VecDeque push throughput (mirrors old mailbox_push)
|
||||||
|
|
@ -79,43 +53,9 @@ fn vecdeque_pop(c: &mut Criterion) {
|
||||||
group.finish();
|
group.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Simulated actor tick: drain_count + pop N from VecDeque
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
fn simulated_actor_tick(c: &mut Criterion) {
|
|
||||||
let mut group = c.benchmark_group("simulated_actor_tick");
|
|
||||||
|
|
||||||
for (wl, fill) in [(10, 5), (10, 10), (10, 50), (100, 200)] {
|
|
||||||
let param = format!("wl={wl},fill={fill}");
|
|
||||||
group.bench_function(BenchmarkId::from_parameter(¶m), |b| {
|
|
||||||
b.iter_batched(
|
|
||||||
|| {
|
|
||||||
let mut q: VecDeque<u64> = VecDeque::new();
|
|
||||||
for i in 0..fill {
|
|
||||||
q.push_back(i as u64);
|
|
||||||
}
|
|
||||||
q
|
|
||||||
},
|
|
||||||
|mut q| {
|
|
||||||
let n = drain_count(q.len(), wl);
|
|
||||||
for _ in 0..n {
|
|
||||||
std::hint::black_box(q.pop_front());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
criterion::BatchSize::SmallInput,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
group.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
criterion_group!(
|
criterion_group!(
|
||||||
benches,
|
benches,
|
||||||
bench_drain_count,
|
|
||||||
vecdeque_push,
|
vecdeque_push,
|
||||||
vecdeque_pop,
|
vecdeque_pop,
|
||||||
simulated_actor_tick,
|
|
||||||
);
|
);
|
||||||
criterion_main!(benches);
|
criterion_main!(benches);
|
||||||
|
|
|
||||||
|
|
@ -227,8 +227,6 @@ pub struct PyRuntimeConfig {
|
||||||
#[pyo3(get, set)]
|
#[pyo3(get, set)]
|
||||||
actor_max_messages: usize,
|
actor_max_messages: usize,
|
||||||
#[pyo3(get, set)]
|
#[pyo3(get, set)]
|
||||||
mailbox_waterlevel: usize,
|
|
||||||
#[pyo3(get, set)]
|
|
||||||
spin_threshold: u32,
|
spin_threshold: u32,
|
||||||
#[pyo3(get, set)]
|
#[pyo3(get, set)]
|
||||||
yield_threshold: u32,
|
yield_threshold: u32,
|
||||||
|
|
@ -246,7 +244,6 @@ impl PyRuntimeConfig {
|
||||||
num_threads = 1,
|
num_threads = 1,
|
||||||
max_actors = 1_000,
|
max_actors = 1_000,
|
||||||
actor_max_messages = 1_000,
|
actor_max_messages = 1_000,
|
||||||
mailbox_waterlevel = 10,
|
|
||||||
spin_threshold = 64,
|
spin_threshold = 64,
|
||||||
yield_threshold = 256,
|
yield_threshold = 256,
|
||||||
sleep_increment_us = 50,
|
sleep_increment_us = 50,
|
||||||
|
|
@ -256,7 +253,6 @@ impl PyRuntimeConfig {
|
||||||
num_threads: usize,
|
num_threads: usize,
|
||||||
max_actors: usize,
|
max_actors: usize,
|
||||||
actor_max_messages: usize,
|
actor_max_messages: usize,
|
||||||
mailbox_waterlevel: usize,
|
|
||||||
spin_threshold: u32,
|
spin_threshold: u32,
|
||||||
yield_threshold: u32,
|
yield_threshold: u32,
|
||||||
sleep_increment_us: u64,
|
sleep_increment_us: u64,
|
||||||
|
|
@ -266,7 +262,6 @@ impl PyRuntimeConfig {
|
||||||
num_threads,
|
num_threads,
|
||||||
max_actors,
|
max_actors,
|
||||||
actor_max_messages,
|
actor_max_messages,
|
||||||
mailbox_waterlevel,
|
|
||||||
spin_threshold,
|
spin_threshold,
|
||||||
yield_threshold,
|
yield_threshold,
|
||||||
sleep_increment_us,
|
sleep_increment_us,
|
||||||
|
|
@ -281,7 +276,6 @@ impl From<PyRuntimeConfig> for RuntimeConfig {
|
||||||
num_threads: py.num_threads,
|
num_threads: py.num_threads,
|
||||||
max_actors: py.max_actors,
|
max_actors: py.max_actors,
|
||||||
actor_max_messages: py.actor_max_messages,
|
actor_max_messages: py.actor_max_messages,
|
||||||
mailbox_waterlevel: py.mailbox_waterlevel,
|
|
||||||
backoff_policy: BackoffPolicy {
|
backoff_policy: BackoffPolicy {
|
||||||
spin_threshold: py.spin_threshold,
|
spin_threshold: py.spin_threshold,
|
||||||
yield_threshold: py.yield_threshold,
|
yield_threshold: py.yield_threshold,
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,6 @@ enum RawAction {
|
||||||
#[derive(Debug, Arbitrary)]
|
#[derive(Debug, Arbitrary)]
|
||||||
struct FuzzInput {
|
struct FuzzInput {
|
||||||
max_actors: u8,
|
max_actors: u8,
|
||||||
mailbox_waterlevel: u8,
|
|
||||||
scenarios: Vec<Scenario>,
|
scenarios: Vec<Scenario>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -773,7 +772,6 @@ impl fmt::Debug for FuzzState {
|
||||||
|
|
||||||
fuzz_target!(|input: FuzzInput| {
|
fuzz_target!(|input: FuzzInput| {
|
||||||
let max_actors = (input.max_actors as usize).max(1).min(200);
|
let max_actors = (input.max_actors as usize).max(1).min(200);
|
||||||
let mailbox_waterlevel = (input.mailbox_waterlevel as usize).max(1).min(50);
|
|
||||||
|
|
||||||
let interval = log_interval();
|
let interval = log_interval();
|
||||||
let run = if interval > 0 {
|
let run = if interval > 0 {
|
||||||
|
|
@ -783,7 +781,6 @@ fuzz_target!(|input: FuzzInput| {
|
||||||
|
|
||||||
let config = RuntimeConfig {
|
let config = RuntimeConfig {
|
||||||
max_actors,
|
max_actors,
|
||||||
mailbox_waterlevel,
|
|
||||||
num_threads: 1,
|
num_threads: 1,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
@ -806,7 +803,7 @@ fuzz_target!(|input: FuzzInput| {
|
||||||
let depth: usize = stats.workers.iter().map(|w| w.mailbox_depth).sum();
|
let depth: usize = stats.workers.iter().map(|w| w.mailbox_depth).sum();
|
||||||
let processed: u64 = stats.workers.iter().map(|w| w.messages_processed).sum();
|
let processed: u64 = stats.workers.iter().map(|w| w.messages_processed).sum();
|
||||||
eprintln!("\
|
eprintln!("\
|
||||||
\n=== Run #{run} | cap={max_actors} waterlevel={mailbox_waterlevel} ===
|
\n=== Run #{run} | cap={max_actors} ===
|
||||||
{trace}\
|
{trace}\
|
||||||
--- {spawned} spawned, {sent} sent, {recv} received, \
|
--- {spawned} spawned, {sent} sent, {recv} received, \
|
||||||
{alive} alive, {depth} queued, {processed} processed ---\n",
|
{alive} alive, {depth} queued, {processed} processed ---\n",
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,6 @@ where
|
||||||
pub trait ContextInner {
|
pub trait ContextInner {
|
||||||
fn send_any(&self, addr: ActorAddress, msg: Box<dyn Any + Send>) -> Result<(), Error>;
|
fn send_any(&self, addr: ActorAddress, msg: Box<dyn Any + Send>) -> Result<(), Error>;
|
||||||
fn spawn_any(&self, addr: ActorAddress, actor: Box<dyn AnyActor>) -> Result<(), Error>;
|
fn spawn_any(&self, addr: ActorAddress, actor: Box<dyn AnyActor>) -> Result<(), Error>;
|
||||||
fn mailbox_waterlevel(&self) -> usize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Actor syscall interface — passed to `ActorInterface::handle()`.
|
/// Actor syscall interface — passed to `ActorInterface::handle()`.
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,10 @@ impl<T> HybridChannel<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push(&self, value: T) -> Result<(), T> {
|
pub fn push(&self, value: T) -> Result<(), T> {
|
||||||
|
if !self.overflow.is_empty() {
|
||||||
|
self.overflow.push(value);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
match self.ring.push(value) {
|
match self.ring.push(value) {
|
||||||
Ok(()) => Ok(()),
|
Ok(()) => Ok(()),
|
||||||
Err(v) => {
|
Err(v) => {
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ pub struct RuntimeConfig {
|
||||||
pub max_actors: usize,
|
pub max_actors: usize,
|
||||||
pub actor_max_messages: usize,
|
pub actor_max_messages: usize,
|
||||||
pub num_threads: usize,
|
pub num_threads: usize,
|
||||||
pub mailbox_waterlevel: usize,
|
|
||||||
pub backoff_policy: BackoffPolicy,
|
pub backoff_policy: BackoffPolicy,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,15 +39,12 @@ const DEFAULT_MAX_ACTORS: usize = 1_000;
|
||||||
/// 1_000 * 16kB = 16MB
|
/// 1_000 * 16kB = 16MB
|
||||||
const DEFAULT_ACTOR_MAX_MESSAGES: usize = 1_000;
|
const DEFAULT_ACTOR_MAX_MESSAGES: usize = 1_000;
|
||||||
|
|
||||||
const DEFAULT_MAILBOX_WATERLEVEL: usize = 10;
|
|
||||||
|
|
||||||
impl Default for RuntimeConfig {
|
impl Default for RuntimeConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
max_actors: DEFAULT_MAX_ACTORS,
|
max_actors: DEFAULT_MAX_ACTORS,
|
||||||
actor_max_messages: DEFAULT_ACTOR_MAX_MESSAGES,
|
actor_max_messages: DEFAULT_ACTOR_MAX_MESSAGES,
|
||||||
num_threads: 1,
|
num_threads: 1,
|
||||||
mailbox_waterlevel: DEFAULT_MAILBOX_WATERLEVEL,
|
|
||||||
backoff_policy: BackoffPolicy::default(),
|
backoff_policy: BackoffPolicy::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -253,13 +253,8 @@ fn wrong_type_silently_dropped() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn backpressure_drains_half() {
|
fn all_messages_drain_in_one_tick() {
|
||||||
let config = RuntimeConfig {
|
run(&[
|
||||||
mailbox_waterlevel: 4,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
run_with(config, &[
|
|
||||||
Step::Spawn(1),
|
Step::Spawn(1),
|
||||||
Step::Tick,
|
Step::Tick,
|
||||||
|
|
||||||
|
|
@ -268,15 +263,7 @@ fn backpressure_drains_half() {
|
||||||
Step::Send(1, 4), Step::Send(1, 5), Step::Send(1, 6), Step::Send(1, 7),
|
Step::Send(1, 4), Step::Send(1, 5), Step::Send(1, 6), Step::Send(1, 7),
|
||||||
Step::Send(1, 8), Step::Send(1, 9),
|
Step::Send(1, 8), Step::Send(1, 9),
|
||||||
|
|
||||||
// Tick 1: 10 in mailbox, drain_count(10, 4) = 5
|
// All 10 processed in a single tick
|
||||||
Step::Tick,
|
|
||||||
Step::Expect { pool_len: 1, depth: 5, processed: 5 },
|
|
||||||
|
|
||||||
// Tick 2: 5 remaining, drain_count(5, 4) = 2
|
|
||||||
Step::Tick,
|
|
||||||
Step::Expect { pool_len: 1, depth: 3, processed: 7 },
|
|
||||||
|
|
||||||
// Tick 3: 3 remaining, drain_count(3, 4) = 3 (below waterlevel → all)
|
|
||||||
Step::Tick,
|
Step::Tick,
|
||||||
Step::Expect { pool_len: 1, depth: 0, processed: 10 },
|
Step::Expect { pool_len: 1, depth: 0, processed: 10 },
|
||||||
Step::ExpectHandled(1, 10),
|
Step::ExpectHandled(1, 10),
|
||||||
|
|
|
||||||
|
|
@ -66,15 +66,12 @@ pub struct Runtime {
|
||||||
placement: Placement,
|
placement: Placement,
|
||||||
is_running: AtomicBool,
|
is_running: AtomicBool,
|
||||||
worker_stats: Vec<Arc<WorkerStats>>,
|
worker_stats: Vec<Arc<WorkerStats>>,
|
||||||
/// Single-threaded mode: worker stored inline
|
/// Workers available for tick(). run() drains this and moves workers to threads.
|
||||||
single_worker: Option<RefCell<Worker>>,
|
tick_workers: RefCell<Vec<Worker>>,
|
||||||
/// Multi-threaded mode: workers waiting to be assigned to threads by run()
|
|
||||||
pending_workers: Option<Vec<Worker>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Safety: RefCell<Worker> is only accessed from the thread that owns the Runtime
|
// Safety: RefCell<Vec<Worker>> is only accessed from the owning thread via tick().
|
||||||
// in single-threaded mode. In multi-threaded mode, single_worker is None and
|
// After run() the RefCell is empty and not accessed by worker threads.
|
||||||
// pending_workers is consumed by run() before Arc sharing.
|
|
||||||
unsafe impl Sync for Runtime {}
|
unsafe impl Sync for Runtime {}
|
||||||
|
|
||||||
impl Runtime {
|
impl Runtime {
|
||||||
|
|
@ -111,9 +108,6 @@ impl Runtime {
|
||||||
workers.push(Worker::new(WorkerId(i), transfer_rx, spawn_rx, stats));
|
workers.push(Worker::new(WorkerId(i), transfer_rx, spawn_rx, stats));
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.num_threads < 2 {
|
|
||||||
// Single-threaded: store one worker inline
|
|
||||||
let worker = workers.remove(0);
|
|
||||||
Self {
|
Self {
|
||||||
config,
|
config,
|
||||||
address_map,
|
address_map,
|
||||||
|
|
@ -123,23 +117,7 @@ impl Runtime {
|
||||||
placement,
|
placement,
|
||||||
is_running: AtomicBool::new(false),
|
is_running: AtomicBool::new(false),
|
||||||
worker_stats,
|
worker_stats,
|
||||||
single_worker: Some(RefCell::new(worker)),
|
tick_workers: RefCell::new(workers),
|
||||||
pending_workers: None,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Multi-threaded: stash workers for run()
|
|
||||||
Self {
|
|
||||||
config,
|
|
||||||
address_map,
|
|
||||||
inbox_registry,
|
|
||||||
transfer_txs,
|
|
||||||
spawn_txs,
|
|
||||||
placement,
|
|
||||||
is_running: AtomicBool::new(false),
|
|
||||||
worker_stats,
|
|
||||||
single_worker: None,
|
|
||||||
pending_workers: Some(workers),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,8 +157,13 @@ impl Runtime {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drive one tick of the single-threaded worker.
|
/// Drive one tick of the single-threaded worker.
|
||||||
|
///
|
||||||
|
/// Panics if called on a multi-threaded runtime — use `run()` instead.
|
||||||
pub fn tick(&self) {
|
pub fn tick(&self) {
|
||||||
if let Some(ref worker) = self.single_worker {
|
assert!(
|
||||||
|
self.config.num_threads < 2,
|
||||||
|
"tick() is only valid for single-threaded runtimes; use run() for multi-threaded"
|
||||||
|
);
|
||||||
let tc = TickContext {
|
let tc = TickContext {
|
||||||
address_map: &self.address_map,
|
address_map: &self.address_map,
|
||||||
transfer_txs: &self.transfer_txs,
|
transfer_txs: &self.transfer_txs,
|
||||||
|
|
@ -189,7 +172,8 @@ impl Runtime {
|
||||||
inbox_registry: &self.inbox_registry,
|
inbox_registry: &self.inbox_registry,
|
||||||
config: &self.config,
|
config: &self.config,
|
||||||
};
|
};
|
||||||
worker.borrow_mut().tick_once(&tc);
|
for worker in self.tick_workers.borrow_mut().iter_mut() {
|
||||||
|
worker.tick_once(&tc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -198,17 +182,10 @@ impl Runtime {
|
||||||
///
|
///
|
||||||
/// Works in both single-threaded and multi-threaded configurations.
|
/// Works in both single-threaded and multi-threaded configurations.
|
||||||
/// In single-threaded mode, one background thread is spawned.
|
/// In single-threaded mode, one background thread is spawned.
|
||||||
pub fn run(mut self) -> Result<RuntimeHandle, Error> {
|
pub fn run(self) -> Result<RuntimeHandle, Error> {
|
||||||
self.is_running.store(true, Ordering::Release);
|
self.is_running.store(true, Ordering::Release);
|
||||||
|
|
||||||
let mut workers: Vec<Worker> = Vec::new();
|
let workers: Vec<Worker> = self.tick_workers.replace(Vec::new());
|
||||||
|
|
||||||
if let Some(w) = self.single_worker.take() {
|
|
||||||
workers.push(w.into_inner());
|
|
||||||
}
|
|
||||||
if let Some(ws) = self.pending_workers.take() {
|
|
||||||
workers.extend(ws);
|
|
||||||
}
|
|
||||||
|
|
||||||
let rt = Arc::new(self);
|
let rt = Arc::new(self);
|
||||||
let mut handles: Vec<JoinHandle<()>> = Vec::with_capacity(workers.len());
|
let mut handles: Vec<JoinHandle<()>> = Vec::with_capacity(workers.len());
|
||||||
|
|
@ -294,8 +271,4 @@ impl ContextInner for Runtime {
|
||||||
.try_send((addr, actor))
|
.try_send((addr, actor))
|
||||||
.map_err(|_| Error::from("Spawn queue full"))
|
.map_err(|_| Error::from("Spawn queue full"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mailbox_waterlevel(&self) -> usize {
|
|
||||||
self.config.mailbox_waterlevel
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,14 @@ impl Worker {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Drain pending_local buffer → deliver to local actors
|
// 4. Drain spawn queue again — actors spawned during step 3
|
||||||
|
// must be in the pool before pending_local delivery.
|
||||||
|
while let Some((addr, actor)) = self.spawn_rx.try_recv() {
|
||||||
|
self.pool.insert(addr, actor);
|
||||||
|
did_work = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Drain pending_local buffer → deliver to local actors
|
||||||
let pending = pending_local.into_inner();
|
let pending = pending_local.into_inner();
|
||||||
if !pending.is_empty() {
|
if !pending.is_empty() {
|
||||||
did_work = true;
|
did_work = true;
|
||||||
|
|
@ -80,7 +87,7 @@ impl Worker {
|
||||||
self.pool.deliver(&addr, msg);
|
self.pool.deliver(&addr, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Publish stats
|
// 6. Publish stats
|
||||||
self.stats.num_actors.store(self.pool.len(), Ordering::Relaxed);
|
self.stats.num_actors.store(self.pool.len(), Ordering::Relaxed);
|
||||||
self.stats.total_mailbox_depth.store(self.pool.total_mailbox_depth(), Ordering::Relaxed);
|
self.stats.total_mailbox_depth.store(self.pool.total_mailbox_depth(), Ordering::Relaxed);
|
||||||
self.stats.messages_processed.fetch_add(processed as u64, Ordering::Relaxed);
|
self.stats.messages_processed.fetch_add(processed as u64, Ordering::Relaxed);
|
||||||
|
|
@ -152,20 +159,6 @@ impl ContextInner for WorkerContext<'_> {
|
||||||
.map_err(|_| Error::from("Spawn queue full"))
|
.map_err(|_| Error::from("Spawn queue full"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mailbox_waterlevel(&self) -> usize {
|
|
||||||
self.tc.config.mailbox_waterlevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// How many messages to process this tick:
|
|
||||||
/// - `len < waterlevel` → process all (`len`)
|
|
||||||
/// - `len >= waterlevel` → process half (`len >> 1`)
|
|
||||||
pub fn drain_count(len: usize, waterlevel: usize) -> usize {
|
|
||||||
if len < waterlevel {
|
|
||||||
len
|
|
||||||
} else {
|
|
||||||
len >> 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ActorSlot {
|
struct ActorSlot {
|
||||||
|
|
@ -211,18 +204,17 @@ impl ActorPool {
|
||||||
pub fn tick_all(&mut self, inner: &dyn ContextInner) -> usize {
|
pub fn tick_all(&mut self, inner: &dyn ContextInner) -> usize {
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
for (&addr, slot) in self.actors.iter_mut() {
|
for (&addr, slot) in self.actors.iter_mut() {
|
||||||
let len = slot.mailbox.len();
|
|
||||||
let n = drain_count(len, inner.mailbox_waterlevel());
|
|
||||||
if n > 0 {
|
|
||||||
let ctx = Ctx::new(inner, addr);
|
let ctx = Ctx::new(inner, addr);
|
||||||
for _ in 0..n {
|
while let Some(msg) = slot.mailbox.pop_front() {
|
||||||
if let Some(msg) = slot.mailbox.pop_front() {
|
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
slot.actor.handle_any(&ctx, msg);
|
slot.actor.handle_any(&ctx, msg);
|
||||||
|
}));
|
||||||
|
if result.is_err() {
|
||||||
|
eprintln!("swactor: actor {addr} panicked in handler");
|
||||||
|
}
|
||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
count
|
count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -156,9 +156,314 @@ fn send_to_unknown_address_fails() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn actor_spawns_child_and_delegates() {
|
fn actor_spawns_child_and_delegates() {
|
||||||
// Uses 2 workers so parent and child land on different workers,
|
let rt = Runtime::new(RuntimeConfig {
|
||||||
// avoiding the single-worker timing issue where pending_local
|
num_threads: 1,
|
||||||
// delivery precedes spawn-queue draining.
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let parent = rt.spawn(DelegateActor).expect("spawn parent");
|
||||||
|
let inbox: Inbox<DoubleResponse> = rt.new_inbox().unwrap();
|
||||||
|
|
||||||
|
rt.send_to(
|
||||||
|
parent,
|
||||||
|
DelegateRequest {
|
||||||
|
value: 7,
|
||||||
|
reply_to: *inbox.addr(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
for _ in 0..10 {
|
||||||
|
rt.tick();
|
||||||
|
if let Some(resp) = inbox.try_recv() {
|
||||||
|
assert_eq!(resp, DoubleResponse(14));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
panic!("Did not receive DoubleResponse");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multiple_actors_independent_mailboxes() {
|
||||||
|
let rt = Runtime::new(RuntimeConfig::default());
|
||||||
|
|
||||||
|
let inbox_a: Inbox<EchoResponse> = rt.new_inbox().unwrap();
|
||||||
|
let inbox_b: Inbox<EchoResponse> = rt.new_inbox().unwrap();
|
||||||
|
let inbox_c: Inbox<EchoResponse> = rt.new_inbox().unwrap();
|
||||||
|
|
||||||
|
let actor_a = rt.spawn(EchoActor).unwrap();
|
||||||
|
let actor_b = rt.spawn(EchoActor).unwrap();
|
||||||
|
let actor_c = rt.spawn(EchoActor).unwrap();
|
||||||
|
|
||||||
|
rt.send_to(actor_a, EchoMessage { payload: 10, reply_to: *inbox_a.addr() }).unwrap();
|
||||||
|
rt.send_to(actor_b, EchoMessage { payload: 20, reply_to: *inbox_b.addr() }).unwrap();
|
||||||
|
rt.send_to(actor_c, EchoMessage { payload: 30, reply_to: *inbox_c.addr() }).unwrap();
|
||||||
|
|
||||||
|
for _ in 0..10 {
|
||||||
|
rt.tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(inbox_a.try_recv(), Some(EchoResponse(10)));
|
||||||
|
assert_eq!(inbox_b.try_recv(), Some(EchoResponse(20)));
|
||||||
|
assert_eq!(inbox_c.try_recv(), Some(EchoResponse(30)));
|
||||||
|
// No cross-contamination
|
||||||
|
assert_eq!(inbox_a.try_recv(), None);
|
||||||
|
assert_eq!(inbox_b.try_recv(), None);
|
||||||
|
assert_eq!(inbox_c.try_recv(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Additional fixtures for spawn/delegate tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Three-deep chain: Parent → Child → Grandchild → reply_to
|
||||||
|
struct ChainActor {
|
||||||
|
depth: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct ChainRequest {
|
||||||
|
remaining: usize,
|
||||||
|
reply_to: ActorAddress,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
struct ChainDone(usize);
|
||||||
|
|
||||||
|
impl ActorInterface for ChainActor {
|
||||||
|
type Incoming = ChainRequest;
|
||||||
|
type Response = ChainDone;
|
||||||
|
|
||||||
|
fn handle(&mut self, ctx: &Ctx, msg: ChainRequest) {
|
||||||
|
if msg.remaining == 0 {
|
||||||
|
let _ = ctx.send(msg.reply_to, ChainDone(self.depth));
|
||||||
|
} else {
|
||||||
|
let child = ctx
|
||||||
|
.spawn(ChainActor {
|
||||||
|
depth: self.depth + 1,
|
||||||
|
})
|
||||||
|
.expect("spawn chain child");
|
||||||
|
let _ = ctx.send(
|
||||||
|
child,
|
||||||
|
ChainRequest {
|
||||||
|
remaining: msg.remaining - 1,
|
||||||
|
reply_to: msg.reply_to,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawns N children and sends work to each. Each child replies to reply_to.
|
||||||
|
struct FanOutActor;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct FanOutRequest {
|
||||||
|
count: usize,
|
||||||
|
reply_to: ActorAddress,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActorInterface for FanOutActor {
|
||||||
|
type Incoming = FanOutRequest;
|
||||||
|
type Response = ();
|
||||||
|
|
||||||
|
fn handle(&mut self, ctx: &Ctx, msg: FanOutRequest) {
|
||||||
|
for i in 0..msg.count {
|
||||||
|
let child = ctx.spawn(DoubleActor).expect("spawn fan-out child");
|
||||||
|
let _ = ctx.send(
|
||||||
|
child,
|
||||||
|
DoubleRequest {
|
||||||
|
value: i + 1,
|
||||||
|
reply_to: msg.reply_to,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Actor A: spawns a child, sends work to it, and also forwards the child's
|
||||||
|
/// address to a "buddy" so the buddy can send to the child too.
|
||||||
|
struct SpawnAndBroadcastActor;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct SpawnAndBroadcastRequest {
|
||||||
|
buddy: ActorAddress,
|
||||||
|
reply_to: ActorAddress,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sent from A to buddy B, carrying the child's address.
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct ForwardToChild {
|
||||||
|
child: ActorAddress,
|
||||||
|
reply_to: ActorAddress,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActorInterface for SpawnAndBroadcastActor {
|
||||||
|
type Incoming = SpawnAndBroadcastRequest;
|
||||||
|
type Response = ();
|
||||||
|
|
||||||
|
fn handle(&mut self, ctx: &Ctx, msg: SpawnAndBroadcastRequest) {
|
||||||
|
let child = ctx.spawn(DoubleActor).expect("spawn child");
|
||||||
|
// Send work from self
|
||||||
|
let _ = ctx.send(
|
||||||
|
child,
|
||||||
|
DoubleRequest {
|
||||||
|
value: 10,
|
||||||
|
reply_to: msg.reply_to,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// Tell buddy about the child
|
||||||
|
let _ = ctx.send(
|
||||||
|
msg.buddy,
|
||||||
|
ForwardToChild {
|
||||||
|
child,
|
||||||
|
reply_to: msg.reply_to,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Buddy actor B: receives a child address and sends work to it.
|
||||||
|
struct BuddyActor;
|
||||||
|
|
||||||
|
impl ActorInterface for BuddyActor {
|
||||||
|
type Incoming = ForwardToChild;
|
||||||
|
type Response = ();
|
||||||
|
|
||||||
|
fn handle(&mut self, ctx: &Ctx, msg: ForwardToChild) {
|
||||||
|
let _ = ctx.send(
|
||||||
|
msg.child,
|
||||||
|
DoubleRequest {
|
||||||
|
value: 20,
|
||||||
|
reply_to: msg.reply_to,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Bug-fix regression tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inbox_preserves_fifo_across_overflow() {
|
||||||
|
let rt = Runtime::new(RuntimeConfig {
|
||||||
|
actor_max_messages: 2,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let inbox: Inbox<u64> = rt.new_inbox().unwrap();
|
||||||
|
let addr = *inbox.addr();
|
||||||
|
|
||||||
|
// Push 4 items: ring gets [1,2], overflow gets [3,4].
|
||||||
|
for v in 1..=4u64 {
|
||||||
|
rt.send_to(addr, v).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drain the ring.
|
||||||
|
assert_eq!(inbox.try_recv(), Some(1));
|
||||||
|
assert_eq!(inbox.try_recv(), Some(2));
|
||||||
|
|
||||||
|
// Push a new item — must land behind 3 and 4 in overflow.
|
||||||
|
rt.send_to(addr, 5u64).unwrap();
|
||||||
|
|
||||||
|
let rest: Vec<u64> = std::iter::from_fn(|| inbox.try_recv()).collect();
|
||||||
|
assert_eq!(rest, vec![3, 4, 5]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delegate_works_on_single_worker() {
|
||||||
|
let rt = Runtime::new(RuntimeConfig {
|
||||||
|
num_threads: 1,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let parent = rt.spawn(DelegateActor).expect("spawn parent");
|
||||||
|
let inbox: Inbox<DoubleResponse> = rt.new_inbox().unwrap();
|
||||||
|
|
||||||
|
rt.send_to(
|
||||||
|
parent,
|
||||||
|
DelegateRequest {
|
||||||
|
value: 5,
|
||||||
|
reply_to: *inbox.addr(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
for _ in 0..10 {
|
||||||
|
rt.tick();
|
||||||
|
if let Some(resp) = inbox.try_recv() {
|
||||||
|
assert_eq!(resp, DoubleResponse(10));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
panic!("Did not receive DoubleResponse on single worker");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_chain_three_deep() {
|
||||||
|
let rt = Runtime::new(RuntimeConfig {
|
||||||
|
num_threads: 1,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let root = rt.spawn(ChainActor { depth: 0 }).expect("spawn root");
|
||||||
|
let inbox: Inbox<ChainDone> = rt.new_inbox().unwrap();
|
||||||
|
|
||||||
|
rt.send_to(
|
||||||
|
root,
|
||||||
|
ChainRequest {
|
||||||
|
remaining: 2,
|
||||||
|
reply_to: *inbox.addr(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
for _ in 0..50 {
|
||||||
|
rt.tick();
|
||||||
|
if let Some(resp) = inbox.try_recv() {
|
||||||
|
assert_eq!(resp, ChainDone(2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
panic!("Did not receive ChainDone from grandchild");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_fan_out() {
|
||||||
|
let rt = Runtime::new(RuntimeConfig {
|
||||||
|
num_threads: 1,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let fan = rt.spawn(FanOutActor).expect("spawn fan-out");
|
||||||
|
let inbox: Inbox<DoubleResponse> = rt.new_inbox().unwrap();
|
||||||
|
|
||||||
|
rt.send_to(
|
||||||
|
fan,
|
||||||
|
FanOutRequest {
|
||||||
|
count: 5,
|
||||||
|
reply_to: *inbox.addr(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
for _ in 0..50 {
|
||||||
|
rt.tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut results: Vec<usize> = std::iter::from_fn(|| inbox.try_recv())
|
||||||
|
.map(|r| r.0)
|
||||||
|
.collect();
|
||||||
|
results.sort();
|
||||||
|
assert_eq!(results, vec![2, 4, 6, 8, 10]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delegate_works_cross_worker() {
|
||||||
let config = RuntimeConfig {
|
let config = RuntimeConfig {
|
||||||
num_threads: 2,
|
num_threads: 2,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
@ -196,34 +501,40 @@ fn actor_spawns_child_and_delegates() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn multiple_actors_independent_mailboxes() {
|
fn multiple_senders_to_new_child() {
|
||||||
let rt = Runtime::new(RuntimeConfig::default());
|
let rt = Runtime::new(RuntimeConfig {
|
||||||
|
num_threads: 1,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
let inbox_a: Inbox<EchoResponse> = rt.new_inbox().unwrap();
|
let buddy = rt.spawn(BuddyActor).expect("spawn buddy");
|
||||||
let inbox_b: Inbox<EchoResponse> = rt.new_inbox().unwrap();
|
let parent = rt.spawn(SpawnAndBroadcastActor).expect("spawn parent");
|
||||||
let inbox_c: Inbox<EchoResponse> = rt.new_inbox().unwrap();
|
let inbox: Inbox<DoubleResponse> = rt.new_inbox().unwrap();
|
||||||
|
|
||||||
let actor_a = rt.spawn(EchoActor).unwrap();
|
rt.send_to(
|
||||||
let actor_b = rt.spawn(EchoActor).unwrap();
|
parent,
|
||||||
let actor_c = rt.spawn(EchoActor).unwrap();
|
SpawnAndBroadcastRequest {
|
||||||
|
buddy,
|
||||||
|
reply_to: *inbox.addr(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
rt.send_to(actor_a, EchoMessage { payload: 10, reply_to: *inbox_a.addr() }).unwrap();
|
for _ in 0..50 {
|
||||||
rt.send_to(actor_b, EchoMessage { payload: 20, reply_to: *inbox_b.addr() }).unwrap();
|
|
||||||
rt.send_to(actor_c, EchoMessage { payload: 30, reply_to: *inbox_c.addr() }).unwrap();
|
|
||||||
|
|
||||||
for _ in 0..10 {
|
|
||||||
rt.tick();
|
rt.tick();
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(inbox_a.try_recv(), Some(EchoResponse(10)));
|
let mut results: Vec<usize> = std::iter::from_fn(|| inbox.try_recv())
|
||||||
assert_eq!(inbox_b.try_recv(), Some(EchoResponse(20)));
|
.map(|r| r.0)
|
||||||
assert_eq!(inbox_c.try_recv(), Some(EchoResponse(30)));
|
.collect();
|
||||||
// No cross-contamination
|
results.sort();
|
||||||
assert_eq!(inbox_a.try_recv(), None);
|
assert_eq!(results, vec![20, 40]);
|
||||||
assert_eq!(inbox_b.try_recv(), None);
|
|
||||||
assert_eq!(inbox_c.try_recv(), None);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Distribution tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn round_robin_distributes_across_workers() {
|
fn round_robin_distributes_across_workers() {
|
||||||
let config = RuntimeConfig {
|
let config = RuntimeConfig {
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@ class TestRuntimeConfig(unittest.TestCase):
|
||||||
self.assertEqual(cfg.num_threads, 1)
|
self.assertEqual(cfg.num_threads, 1)
|
||||||
self.assertEqual(cfg.max_actors, 1000)
|
self.assertEqual(cfg.max_actors, 1000)
|
||||||
self.assertEqual(cfg.actor_max_messages, 1000)
|
self.assertEqual(cfg.actor_max_messages, 1000)
|
||||||
self.assertEqual(cfg.mailbox_waterlevel, 10)
|
|
||||||
self.assertEqual(cfg.spin_threshold, 64)
|
self.assertEqual(cfg.spin_threshold, 64)
|
||||||
self.assertEqual(cfg.yield_threshold, 256)
|
self.assertEqual(cfg.yield_threshold, 256)
|
||||||
self.assertEqual(cfg.sleep_increment_us, 50)
|
self.assertEqual(cfg.sleep_increment_us, 50)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue