Compare commits
No commits in common. "cf107315bc8c69ac06fe7811412cc247653ebb54" and "7c7947c823c77285a8275c11cbd426cf4c0a60e4" have entirely different histories.
cf107315bc
...
7c7947c823
31 changed files with 2786 additions and 1391 deletions
198
ARCHITECTURE.md
Normal file
198
ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
# Swactor Architecture
|
||||
|
||||
## System Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Runtime │
|
||||
│ (composes everything) │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Address Map │ │
|
||||
│ │ ActorAddress → WorkerId │ │
|
||||
│ │ (shared across all workers, read-heavy) │ │
|
||||
│ └──────┬──────────────────┬──────────────────────┬─────────────────────┘ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ Worker 0 │ │ Worker 1 │ ... │ Worker N │ │
|
||||
│ │ (thread) │ │ (thread) │ │ (thread) │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ ┌─────────┐ │ │ ┌─────────┐ │ │ ┌─────────┐ │ │
|
||||
│ │ │ Actor A │ │ │ │ Actor C │ │ │ │ Actor E │ │ │
|
||||
│ │ │ [═════] │ │ │ │ [═════] │ │ │ │ [═════] │ │ │
|
||||
│ │ │ mailbox │ │ │ │ mailbox │ │ │ │ mailbox │ │ │
|
||||
│ │ └─────────┘ │ │ └─────────┘ │ │ └─────────┘ │ │
|
||||
│ │ ┌─────────┐ │ │ ┌─────────┐ │ │ ┌─────────┐ │ │
|
||||
│ │ │ Actor B │ │ │ │ Actor D │ │ │ │ Actor F │ │ │
|
||||
│ │ │ [═════] │ │ │ │ [═════] │ │ │ │ [═════] │ │ │
|
||||
│ │ │ mailbox │ │ │ │ mailbox │ │ │ │ mailbox │ │ │
|
||||
│ │ └─────────┘ │ │ └─────────┘ │ │ └─────────┘ │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ ┌─────────┐ │ │ ┌─────────┐ │ │ ┌─────────┐ │ │
|
||||
│ │ │Transfer │◄├────├─┤Transfer │◄├───────├─┤Transfer │ │ │
|
||||
│ │ │ Queue │ │ │ │ Queue │ │ │ │ Queue │ │ │
|
||||
│ │ │ (MPSC) │─├────├►│ (MPSC) │─├───────├►│ (MPSC) │ │ │
|
||||
│ │ └─────────┘ │ │ └─────────┘ │ │ └─────────┘ │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
|
||||
══ = VecDeque (no atomics)
|
||||
```
|
||||
|
||||
## Message Flow
|
||||
|
||||
```
|
||||
SAME WORKER (fast path — zero atomics)
|
||||
═══════════════════════════════════════
|
||||
|
||||
Actor A Actor B
|
||||
handle() { mailbox (VecDeque)
|
||||
ctx.send(addr_B, msg) ▲
|
||||
│ │
|
||||
├─ address_map[addr_B] │
|
||||
│ → Worker 0 (that's me!) │
|
||||
│ │
|
||||
└─ mailbox_B.push(msg) ──────┘
|
||||
} no atomics, no envelope
|
||||
|
||||
|
||||
CROSS-WORKER (one atomic hop)
|
||||
═══════════════════════════════
|
||||
|
||||
Actor A (Worker 0) Worker 1 Actor C (Worker 1)
|
||||
handle() { transfer queue mailbox (VecDeque)
|
||||
ctx.send(addr_C, msg) ▲ ▲
|
||||
│ │ │
|
||||
├─ address_map[addr_C] │ │
|
||||
│ → Worker 1 (not me) │ │
|
||||
│ │ │
|
||||
└─ envelope(addr_C, msg) ──────┘ │
|
||||
(atomic push) │ │
|
||||
└── worker 1 pops ─────┘
|
||||
and distributes
|
||||
(local, no atomic)
|
||||
}
|
||||
```
|
||||
|
||||
## Worker Loop
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Worker Thread │
|
||||
│ │
|
||||
│ loop { │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ 1. DRAIN TRANSFER QUEUE │ │
|
||||
│ │ while let Some((addr, env)) = │ │
|
||||
│ │ transfer_queue.pop() │ │
|
||||
│ │ { │ │
|
||||
│ │ local_actors[addr].mailbox │ │
|
||||
│ │ .push(env.unpack()) │ │
|
||||
│ │ } │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ 2. TICK ACTORS │ │
|
||||
│ │ for actor in &mut actor_pool { │ │
|
||||
│ │ let n = drain_count(actor); │ │
|
||||
│ │ for _ in 0..n { │ │
|
||||
│ │ let msg = actor.mailbox.pop();│ │
|
||||
│ │ actor.handle(&ctx, msg); │ │
|
||||
│ │ } │ │
|
||||
│ │ } │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ 3. IDLE? │ │
|
||||
│ │ if no messages processed: │ │
|
||||
│ │ spin → yield → park │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
│ } │
|
||||
└───────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## File Tree
|
||||
|
||||
```
|
||||
src/
|
||||
├── lib.rs # crate root, feature flags, public exports
|
||||
├── error.rs # Error type
|
||||
│
|
||||
├── actor.rs # Message trait, ActorInterface trait, ActorAddress
|
||||
│ # - ActorInterface::handle(&mut self, ctx: &dyn Context, msg)
|
||||
│ # - actors depend ONLY on Context, nothing else
|
||||
│
|
||||
├── context.rs # Context trait — the "syscall interface" for actors
|
||||
│ # - send(), self_addr(), spawn()
|
||||
│ # - this is ALL actors can see of the framework
|
||||
│
|
||||
├── envelope.rs # Envelope type — type erasure for cross-thread messages
|
||||
│ # - wraps typed messages for the transfer queue
|
||||
│ # - unwraps back to concrete type at destination
|
||||
│
|
||||
├── address_map.rs # ActorAddress → WorkerId mapping
|
||||
│ # - shared read-heavy structure
|
||||
│ # - written on spawn, read on every send
|
||||
│
|
||||
├── transfer.rs # Transfer queue — per-worker MPSC
|
||||
│ # - the ONE concurrent data structure on the hot path
|
||||
│ # - carries (ActorAddress, Envelope) pairs
|
||||
│
|
||||
├── worker/
|
||||
│ ├── mod.rs # Worker struct and worker loop
|
||||
│ │ # - owns actor pool + transfer queue
|
||||
│ │ # - the thread boundary: concurrent outside, local inside
|
||||
│ │ # - drain transfer queue → tick actors → backoff
|
||||
│ │
|
||||
│ ├── mailbox.rs # VecDeque-based local mailbox
|
||||
│ │ # - NO atomics, NO Arc, NO crossbeam
|
||||
│ │ # - only touched by the owning worker thread
|
||||
│ │
|
||||
│ └── pool.rs # Actor pool — stores actors assigned to this worker
|
||||
│ # - local HashMap or Vec for ActorAddress → Actor lookup
|
||||
│ # - insert on spawn, remove on shutdown
|
||||
│
|
||||
├── runtime.rs # Runtime — the composition point
|
||||
│ # - creates workers, address map
|
||||
│ # - implements Context (delegates to address map + transfer queues)
|
||||
│ # - public API: new(), spawn(), send_to(), run(), tick(), shutdown()
|
||||
│
|
||||
├── config.rs # RuntimeConfig — tuning knobs
|
||||
│ # - num_threads, max_actors, mailbox capacity
|
||||
│ # - drain strategy, backoff policy
|
||||
│ # - placement strategy (round-robin, caller-affinity, etc.)
|
||||
│
|
||||
└── placement.rs # Actor placement strategy
|
||||
# - decides which worker a new actor goes to
|
||||
# - round-robin, least-loaded, caller-affinity
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
| Component | File(s) | What It Does | Concurrent? |
|
||||
|---|---|---|---|
|
||||
| **Worker** | `worker/mod.rs` | Owns a thread, a pool of actors, their mailboxes, and a transfer queue. Runs the tick loop. Everything inside is single-threaded. | No (that's the point) |
|
||||
| **Mailbox** | `worker/mailbox.rs` | `VecDeque<M>` per actor. Zero atomics. Only the owning worker reads/writes. | No |
|
||||
| **Actor Pool** | `worker/pool.rs` | Stores actors on this worker. Local lookup by address. | No |
|
||||
| **Transfer Queue** | `transfer.rs` | MPSC queue per worker. The only atomic boundary. Other workers push, this worker pops. | Yes (the ONE place) |
|
||||
| **Address Map** | `address_map.rs` | Maps ActorAddress → WorkerId. Read on every cross-thread send, written on spawn. | Yes (read-heavy) |
|
||||
| **Envelope** | `envelope.rs` | Type-erases messages for the transfer queue. Unwrapped at destination. | No (data format) |
|
||||
| **Context** | `context.rs` | Trait that actors see. `send()`, `self_addr()`, `spawn()`. Hides all framework internals. | N/A (trait) |
|
||||
| **Runtime** | `runtime.rs` | Wires it all together. Creates workers, holds address map, exposes public API. | Minimal (delegates) |
|
||||
| **Placement** | `placement.rs` | Decides which worker gets a new actor. | No (called at spawn time) |
|
||||
|
||||
## Single-Threaded / WASM Mode
|
||||
|
||||
One worker. No transfer queue needed. No address map needed (everything is local). The system collapses to:
|
||||
|
||||
```
|
||||
Worker 0
|
||||
┌───────────────────────┐
|
||||
│ Actor A [mailbox] │
|
||||
│ Actor B [mailbox] │ All sends are local.
|
||||
│ Actor C [mailbox] │ All mailboxes are VecDeque.
|
||||
│ │ Zero atomics anywhere.
|
||||
│ tick() drives loop │
|
||||
└───────────────────────┘
|
||||
```
|
||||
535
Cargo.lock
generated
535
Cargo.lock
generated
|
|
@ -2,158 +2,12 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anes"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "ciborium"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"ciborium-ll",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-io"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-ll"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"half",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
|
||||
|
||||
[[package]]
|
||||
name = "criterion"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
|
||||
dependencies = [
|
||||
"anes",
|
||||
"cast",
|
||||
"ciborium",
|
||||
"clap",
|
||||
"criterion-plot",
|
||||
"is-terminal",
|
||||
"itertools",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"oorandom",
|
||||
"plotters",
|
||||
"rayon",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tinytemplate",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion-plot"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
|
||||
dependencies = [
|
||||
"cast",
|
||||
"itertools",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.12"
|
||||
|
|
@ -169,18 +23,6 @@ version = "0.8.21"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
|
|
@ -192,59 +34,6 @@ dependencies = [
|
|||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crunchy",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.85"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.180"
|
||||
|
|
@ -252,230 +41,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
version = "11.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||
|
||||
[[package]]
|
||||
name = "plotters"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"plotters-backend",
|
||||
"plotters-svg",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "plotters-backend"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
|
||||
|
||||
[[package]]
|
||||
name = "plotters-svg"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
|
||||
dependencies = [
|
||||
"plotters-backend",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "swactor"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"crossbeam-queue",
|
||||
"crossbeam-utils",
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinytemplate"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -483,108 +61,3 @@ name = "wasi"
|
|||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.85"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445"
|
||||
|
|
|
|||
10
Cargo.toml
10
Cargo.toml
|
|
@ -17,10 +17,8 @@ stress = [] # Enable stress tests
|
|||
getrandom = { version = "0.2", optional = true }
|
||||
crossbeam-queue = "0.3.12"
|
||||
crossbeam-utils = "0.8.21"
|
||||
smallvec = "1.13"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
|
||||
[[bench]]
|
||||
name = "runtime_benchmarks"
|
||||
harness = false
|
||||
[[bin]]
|
||||
name = "bench"
|
||||
path = "benches/main.rs"
|
||||
|
|
|
|||
44
TODOs.md
Normal file
44
TODOs.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
### Profiling and Benchmarking
|
||||
|
||||
- Research as to modern art on benching and profiling
|
||||
- Implement an MVP here.
|
||||
- Bench/profile against a suite of tests selected for generality across actor framework usecases
|
||||
|
||||
- Identify hot paths and bottlenecks
|
||||
- e.g. pretty sure the router is a major bottleneck, what else
|
||||
- follow through the entire message cycle:
|
||||
Parent process -> Convert to swactor::Message/Envelope -> Router -> Delivery -> Processing -> etc.
|
||||
Identify every small detail on which you may be able to improve, any unneeded processing or branching
|
||||
|
||||
- (optional) Visualization tools:
|
||||
- make some pretty stuff for tracing messages, actor activity, router activity, etc.
|
||||
|
||||
### Usage
|
||||
|
||||
- After benching and profiling, cleaning up the most egregious wrongdoings we will:
|
||||
- actually implement our own projects in the framework, ones I actually find useful personally
|
||||
|
||||
- Optimization pipeline:
|
||||
- Once we have well-established benches and profiles for general cases, build a set of tools that can auto-optimize
|
||||
for given use cases. Tuning, for example, the channel buffers, router behavior, message consumption behavior, etc.
|
||||
|
||||
### Chores
|
||||
|
||||
- go over all the FIXMEs littered about. Add comments.
|
||||
- add misc features as they come up. Prefer tools for understanding execution flows, visualizing flows, and adding
|
||||
robustness, over ergonomics. Better to be slightly clunky but fast and optimized, than vice versa.
|
||||
|
||||
### Far future
|
||||
|
||||
- make language bindings. e.g., an npm package, python bindings, etc.
|
||||
|
||||
### Optimization
|
||||
|
||||
- Localize actors and inboxes:
|
||||
- Because the entire runtime is message driven, the happy path must be fast. Even lock free, when we have to go through
|
||||
several calls of an atomic ring buffer in order to process a single message, its unnecessary.
|
||||
Parent process -> router -> actor -> router -> inbox -> parent process; every transfer going through an atomic buffer.
|
||||
|
||||
- to do this, design heavily around a localized worker thread. Actors on a working thread should have their inbox localized, they
|
||||
should be 'sticky' to that thread (FILO queue?), and we should route messages based on core locality. Future optimizations can include
|
||||
a tunable algorithm that puts actors that frequently communicate together on the same thread.
|
||||
270
benches/harness.rs
Normal file
270
benches/harness.rs
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
//! Manual benchmark harness - zero dependencies, full control.
|
||||
//!
|
||||
//! Provides statistical analysis of benchmark runs including:
|
||||
//! - Mean, median, min, max
|
||||
//! - Standard deviation
|
||||
//! - Percentiles (P50, P90, P99, P99.9)
|
||||
//! - Throughput calculations
|
||||
//! - Outlier detection and removal
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Results from a single benchmark run
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BenchResult {
|
||||
pub name: String,
|
||||
pub iterations: usize,
|
||||
pub total_time: Duration,
|
||||
pub times: Vec<Duration>,
|
||||
/// Optional: elements processed (for throughput calculation)
|
||||
pub elements: Option<u64>,
|
||||
}
|
||||
|
||||
/// Statistical summary of benchmark results
|
||||
#[derive(Debug)]
|
||||
pub struct Stats {
|
||||
pub mean: Duration,
|
||||
pub median: Duration,
|
||||
pub min: Duration,
|
||||
pub max: Duration,
|
||||
pub std_dev: Duration,
|
||||
pub p50: Duration,
|
||||
pub p90: Duration,
|
||||
pub p99: Duration,
|
||||
pub p999: Duration,
|
||||
pub throughput: Option<f64>, // elements per second
|
||||
}
|
||||
|
||||
impl BenchResult {
|
||||
/// Calculate statistics from the raw timing data
|
||||
pub fn stats(&self) -> Stats {
|
||||
let mut sorted: Vec<Duration> = self.times.clone();
|
||||
sorted.sort();
|
||||
|
||||
let n = sorted.len();
|
||||
assert!(n > 0, "Cannot compute stats on empty results");
|
||||
|
||||
let sum: Duration = sorted.iter().sum();
|
||||
let mean = sum / n as u32;
|
||||
|
||||
let median = if n % 2 == 0 {
|
||||
(sorted[n / 2 - 1] + sorted[n / 2]) / 2
|
||||
} else {
|
||||
sorted[n / 2]
|
||||
};
|
||||
|
||||
// Standard deviation
|
||||
let mean_nanos = mean.as_nanos() as f64;
|
||||
let variance: f64 = sorted
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let diff = t.as_nanos() as f64 - mean_nanos;
|
||||
diff * diff
|
||||
})
|
||||
.sum::<f64>()
|
||||
/ n as f64;
|
||||
let std_dev = Duration::from_nanos(variance.sqrt() as u64);
|
||||
|
||||
// Percentiles
|
||||
let percentile = |p: f64| -> Duration {
|
||||
let idx = ((p / 100.0) * (n - 1) as f64).round() as usize;
|
||||
sorted[idx.min(n - 1)]
|
||||
};
|
||||
|
||||
let throughput = self.elements.map(|e| {
|
||||
let secs = self.total_time.as_secs_f64();
|
||||
if secs > 0.0 {
|
||||
(e * self.iterations as u64) as f64 / secs
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
});
|
||||
|
||||
Stats {
|
||||
mean,
|
||||
median,
|
||||
min: sorted[0],
|
||||
max: sorted[n - 1],
|
||||
std_dev,
|
||||
p50: percentile(50.0),
|
||||
p90: percentile(90.0),
|
||||
p99: percentile(99.0),
|
||||
p999: percentile(99.9),
|
||||
throughput,
|
||||
}
|
||||
}
|
||||
|
||||
/// Pretty print the results
|
||||
pub fn print(&self) {
|
||||
let stats = self.stats();
|
||||
|
||||
println!("\n{}", "=".repeat(60));
|
||||
println!(" {}", self.name);
|
||||
println!("{}", "=".repeat(60));
|
||||
println!(" Iterations: {}", self.iterations);
|
||||
println!(" Total time: {:?}", self.total_time);
|
||||
println!();
|
||||
println!(" Mean: {:?}", stats.mean);
|
||||
println!(" Median: {:?}", stats.median);
|
||||
println!(" Std Dev: {:?}", stats.std_dev);
|
||||
println!(" Min: {:?}", stats.min);
|
||||
println!(" Max: {:?}", stats.max);
|
||||
println!();
|
||||
println!(" P50: {:?}", stats.p50);
|
||||
println!(" P90: {:?}", stats.p90);
|
||||
println!(" P99: {:?}", stats.p99);
|
||||
println!(" P99.9: {:?}", stats.p999);
|
||||
|
||||
if let Some(throughput) = stats.throughput {
|
||||
println!();
|
||||
println!(" Throughput: {:.2} ops/sec", throughput);
|
||||
if throughput > 1_000_000.0 {
|
||||
println!(" {:.2} M ops/sec", throughput / 1_000_000.0);
|
||||
} else if throughput > 1_000.0 {
|
||||
println!(" {:.2} K ops/sec", throughput / 1_000.0);
|
||||
}
|
||||
}
|
||||
println!("{}", "=".repeat(60));
|
||||
}
|
||||
}
|
||||
|
||||
/// A benchmark builder for configuring and running benchmarks
|
||||
pub struct Bench {
|
||||
name: String,
|
||||
warmup_iters: usize,
|
||||
bench_iters: usize,
|
||||
elements_per_iter: Option<u64>,
|
||||
}
|
||||
|
||||
impl Bench {
|
||||
pub fn new(name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
warmup_iters: 3,
|
||||
bench_iters: 100,
|
||||
elements_per_iter: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set number of warmup iterations (default: 3)
|
||||
pub fn warmup(mut self, n: usize) -> Self {
|
||||
self.warmup_iters = n;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set number of benchmark iterations (default: 100)
|
||||
pub fn iters(mut self, n: usize) -> Self {
|
||||
self.bench_iters = n;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set elements per iteration for throughput calculation
|
||||
pub fn elements(mut self, n: u64) -> Self {
|
||||
self.elements_per_iter = Some(n);
|
||||
self
|
||||
}
|
||||
|
||||
/// Run the benchmark with setup before each iteration
|
||||
pub fn run_with_setup<S, T, F>(self, mut setup: S, mut f: F) -> BenchResult
|
||||
where
|
||||
S: FnMut() -> T,
|
||||
F: FnMut(T),
|
||||
{
|
||||
// Warmup
|
||||
for _ in 0..self.warmup_iters {
|
||||
let state = setup();
|
||||
f(state);
|
||||
}
|
||||
|
||||
// Benchmark
|
||||
let mut times = Vec::with_capacity(self.bench_iters);
|
||||
let total_start = Instant::now();
|
||||
|
||||
for _ in 0..self.bench_iters {
|
||||
let state = setup();
|
||||
let start = Instant::now();
|
||||
f(state);
|
||||
times.push(start.elapsed());
|
||||
}
|
||||
|
||||
let total_time = total_start.elapsed();
|
||||
|
||||
BenchResult {
|
||||
name: self.name,
|
||||
iterations: self.bench_iters,
|
||||
total_time,
|
||||
times,
|
||||
elements: self.elements_per_iter,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A collection of benchmarks to run together
|
||||
pub struct BenchSuite {
|
||||
name: String,
|
||||
results: Vec<BenchResult>,
|
||||
}
|
||||
|
||||
impl BenchSuite {
|
||||
pub fn new(name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
results: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, result: BenchResult) {
|
||||
self.results.push(result);
|
||||
}
|
||||
|
||||
pub fn print_summary(&self) {
|
||||
println!("\n{}", "#".repeat(70));
|
||||
println!("# BENCHMARK SUITE: {}", self.name);
|
||||
println!("{}", "#".repeat(70));
|
||||
|
||||
for result in &self.results {
|
||||
result.print();
|
||||
}
|
||||
|
||||
// Summary table
|
||||
println!("\n{}", "-".repeat(70));
|
||||
println!(" SUMMARY");
|
||||
println!("{}", "-".repeat(70));
|
||||
println!(
|
||||
" {:30} {:>12} {:>12} {:>12}",
|
||||
"Benchmark", "Mean", "P99", "Throughput"
|
||||
);
|
||||
println!("{}", "-".repeat(70));
|
||||
|
||||
for result in &self.results {
|
||||
let stats = result.stats();
|
||||
let throughput_str = stats
|
||||
.throughput
|
||||
.map(|t| {
|
||||
if t > 1_000_000.0 {
|
||||
format!("{:.2}M/s", t / 1_000_000.0)
|
||||
} else if t > 1_000.0 {
|
||||
format!("{:.2}K/s", t / 1_000.0)
|
||||
} else {
|
||||
format!("{:.2}/s", t)
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
|
||||
println!(
|
||||
" {:30} {:>12.2?} {:>12.2?} {:>12}",
|
||||
result.name, stats.mean, stats.p99, throughput_str
|
||||
);
|
||||
}
|
||||
println!("{}", "-".repeat(70));
|
||||
}
|
||||
}
|
||||
|
||||
/// Prevent the compiler from optimizing away a value
|
||||
#[inline(never)]
|
||||
pub fn black_box<T>(x: T) -> T {
|
||||
// Use inline assembly to prevent optimization
|
||||
// This is a simplified version - in practice, reads from the value
|
||||
let ptr = &x as *const T;
|
||||
unsafe { std::ptr::read_volatile(ptr) }
|
||||
}
|
||||
46
benches/main.rs
Normal file
46
benches/main.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
//! Swactor Benchmark Suite
|
||||
//!
|
||||
//! A manual benchmark harness for measuring runtime performance.
|
||||
//! Zero external dependencies - just std::time.
|
||||
//!
|
||||
//! Run with: cargo run --bin bench --release
|
||||
//!
|
||||
//! Options:
|
||||
//! --throughput Run throughput benchmarks only
|
||||
//! --scaling Run scaling benchmarks only
|
||||
//! --all Run all benchmarks (default)
|
||||
|
||||
mod harness;
|
||||
mod throughput;
|
||||
mod scaling;
|
||||
|
||||
use std::env;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
println!("============================================================");
|
||||
println!(" SWACTOR BENCHMARK SUITE");
|
||||
println!("============================================================");
|
||||
println!();
|
||||
|
||||
// Parse arguments
|
||||
let run_throughput = args.contains(&"--throughput".to_string())
|
||||
|| args.contains(&"--all".to_string())
|
||||
|| args.len() == 1;
|
||||
let run_scaling = args.contains(&"--scaling".to_string())
|
||||
|| args.contains(&"--all".to_string())
|
||||
|| args.len() == 1;
|
||||
|
||||
if run_throughput {
|
||||
let suite = throughput::run_all();
|
||||
suite.print_summary();
|
||||
}
|
||||
|
||||
if run_scaling {
|
||||
let suite = scaling::run_all();
|
||||
suite.print_summary();
|
||||
}
|
||||
|
||||
println!("\nBenchmarks complete.");
|
||||
}
|
||||
|
|
@ -1,276 +0,0 @@
|
|||
use criterion::{
|
||||
criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput,
|
||||
};
|
||||
use swactor::{
|
||||
actor::{ActorAddress, ActorInterface},
|
||||
config::RuntimeConfig,
|
||||
runtime::{Ctx, Runtime},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn make_config(max_actors: usize, max_messages: usize) -> RuntimeConfig {
|
||||
RuntimeConfig {
|
||||
max_actors,
|
||||
actor_max_messages: max_messages,
|
||||
num_threads: 1,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Message types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone)]
|
||||
struct NoopMessage;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct PingMessage {
|
||||
reply_to: ActorAddress,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct PongMessage;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CountMessage(u64);
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RingMessage {
|
||||
hops: u64,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Actor types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct NoopActor;
|
||||
|
||||
impl ActorInterface for NoopActor {
|
||||
type Incoming = NoopMessage;
|
||||
type Response = ();
|
||||
fn handle(&mut self, _ctx: &Ctx, _msg: NoopMessage) {}
|
||||
}
|
||||
|
||||
struct EchoActor;
|
||||
|
||||
impl ActorInterface for EchoActor {
|
||||
type Incoming = PingMessage;
|
||||
type Response = ();
|
||||
fn handle(&mut self, ctx: &Ctx, msg: PingMessage) {
|
||||
let _ = ctx.send(msg.reply_to, PongMessage);
|
||||
}
|
||||
}
|
||||
|
||||
struct SinkActor;
|
||||
|
||||
impl ActorInterface for SinkActor {
|
||||
type Incoming = CountMessage;
|
||||
type Response = ();
|
||||
fn handle(&mut self, _ctx: &Ctx, _msg: CountMessage) {}
|
||||
}
|
||||
|
||||
struct RingActor {
|
||||
next: ActorAddress,
|
||||
}
|
||||
|
||||
impl ActorInterface for RingActor {
|
||||
type Incoming = RingMessage;
|
||||
type Response = ();
|
||||
fn handle(&mut self, ctx: &Ctx, msg: RingMessage) {
|
||||
let _ = ctx.send(self.next, RingMessage { hops: msg.hops + 1 });
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Latency benchmarks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn latency_benchmarks(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("latency");
|
||||
|
||||
// A1 — Spawn latency
|
||||
group.bench_function("spawn", |b| {
|
||||
b.iter_batched(
|
||||
|| Runtime::new(make_config(1_000, 1_000)),
|
||||
|rt| {
|
||||
rt.spawn(NoopActor).unwrap();
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
// A2 — Message round-trip
|
||||
group.bench_function("message_roundtrip", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let rt = Runtime::new(make_config(1_000, 1_000));
|
||||
let addr = rt.spawn(EchoActor).unwrap();
|
||||
rt.tick(); // register actor
|
||||
let inbox = rt.new_inbox::<PongMessage>().unwrap();
|
||||
let inbox_addr = *inbox.addr();
|
||||
(rt, addr, inbox, inbox_addr)
|
||||
},
|
||||
|(rt, addr, inbox, inbox_addr)| {
|
||||
rt.send_to(addr, PingMessage { reply_to: inbox_addr }).unwrap();
|
||||
for _ in 0..20 {
|
||||
rt.tick();
|
||||
if inbox.try_recv().is_some() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
panic!("PongMessage not received within 20 ticks");
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
// A3 — Fire-and-forget send
|
||||
group.bench_function("send_fire_and_forget", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let rt = Runtime::new(make_config(1_000, 100_000));
|
||||
let addr = rt.spawn(NoopActor).unwrap();
|
||||
rt.tick(); // register actor
|
||||
(rt, addr)
|
||||
},
|
||||
|(rt, addr)| {
|
||||
rt.send_to(addr, NoopMessage).unwrap();
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
// A4 — Inbox creation
|
||||
group.bench_function("inbox_creation", |b| {
|
||||
b.iter_batched(
|
||||
|| Runtime::new(make_config(1_000, 1_000)),
|
||||
|rt| {
|
||||
rt.new_inbox::<NoopMessage>().unwrap();
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Throughput benchmarks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn throughput_benchmarks(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("throughput");
|
||||
|
||||
// B1 — Single-actor throughput
|
||||
for n in [100, 1_000, 10_000] {
|
||||
group.throughput(Throughput::Elements(n as u64));
|
||||
group.bench_with_input(BenchmarkId::new("single_actor", n), &n, |b, &n| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let rt = Runtime::new(make_config(100, n + 100));
|
||||
let addr = rt.spawn(SinkActor).unwrap();
|
||||
rt.tick(); // register actor
|
||||
for i in 0..n {
|
||||
rt.send_to(addr, CountMessage(i as u64)).unwrap();
|
||||
}
|
||||
rt
|
||||
},
|
||||
|rt| {
|
||||
for _ in 0..50 {
|
||||
rt.tick();
|
||||
}
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// B2 — Multi-actor throughput
|
||||
for (actors, msgs_per) in [(10, 100), (100, 100), (100, 1_000)] {
|
||||
let total = actors * msgs_per;
|
||||
group.throughput(Throughput::Elements(total as u64));
|
||||
let param = format!("{actors}x{msgs_per}");
|
||||
group.bench_with_input(BenchmarkId::new("multi_actor", ¶m), &(actors, msgs_per), |b, &(actors, msgs_per)| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let rt = Runtime::new(make_config(actors + 100, msgs_per + 100));
|
||||
let addrs: Vec<_> = (0..actors)
|
||||
.map(|_| rt.spawn(SinkActor).unwrap())
|
||||
.collect();
|
||||
rt.tick(); // register actors
|
||||
for &addr in &addrs {
|
||||
for i in 0..msgs_per {
|
||||
rt.send_to(addr, CountMessage(i as u64)).unwrap();
|
||||
}
|
||||
}
|
||||
rt
|
||||
},
|
||||
|rt| {
|
||||
for _ in 0..100 {
|
||||
rt.tick();
|
||||
}
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// B3 — Ring throughput
|
||||
for ring_size in [10usize, 100, 500] {
|
||||
group.throughput(Throughput::Elements((ring_size + 1) as u64));
|
||||
group.bench_with_input(BenchmarkId::new("ring", ring_size), &ring_size, |b, &ring_size| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let rt = Runtime::new(make_config(ring_size + 100, 100));
|
||||
let inbox = rt.new_inbox::<RingMessage>().unwrap();
|
||||
// Build the ring: last actor sends to inbox, each prior actor sends to the next
|
||||
let mut next_addr = *inbox.addr();
|
||||
let mut entry_addr = next_addr;
|
||||
for _ in 0..ring_size {
|
||||
let addr = rt.spawn(RingActor { next: next_addr }).unwrap();
|
||||
entry_addr = addr;
|
||||
next_addr = addr;
|
||||
}
|
||||
rt.tick(); // register all actors
|
||||
(rt, entry_addr, inbox)
|
||||
},
|
||||
|(rt, entry_addr, inbox)| {
|
||||
rt.send_to(entry_addr, RingMessage { hops: 0 }).unwrap();
|
||||
for _ in 0..(ring_size + 10) {
|
||||
rt.tick();
|
||||
if inbox.try_recv().is_some() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
panic!("RingMessage not received within tick budget");
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// B4 — Spawn throughput
|
||||
for n in [100, 1_000, 5_000] {
|
||||
group.throughput(Throughput::Elements(n as u64));
|
||||
group.bench_with_input(BenchmarkId::new("spawn", n), &n, |b, &n| {
|
||||
b.iter_batched(
|
||||
|| Runtime::new(make_config(n + 100, 1_000)),
|
||||
|rt| {
|
||||
for _ in 0..n {
|
||||
rt.spawn(NoopActor).unwrap();
|
||||
}
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, latency_benchmarks, throughput_benchmarks);
|
||||
criterion_main!(benches);
|
||||
278
benches/scaling.rs
Normal file
278
benches/scaling.rs
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
//! Scaling benchmarks for the swactor runtime.
|
||||
//!
|
||||
//! These benchmarks measure how performance scales with:
|
||||
//! - Number of actors
|
||||
//! - Number of worker threads
|
||||
//! - Message payload size
|
||||
|
||||
use crate::harness::{black_box, Bench, BenchSuite};
|
||||
use std::thread;
|
||||
use swactor::{
|
||||
Ctx,
|
||||
actor::ActorInterface,
|
||||
runtime::{Runtime, RuntimeConfig},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Test Actors
|
||||
// ============================================================================
|
||||
|
||||
/// A counter actor that just increments on each message
|
||||
struct CounterActor {
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl CounterActor {
|
||||
fn new() -> Self {
|
||||
Self { count: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Increment;
|
||||
|
||||
impl ActorInterface for CounterActor {
|
||||
type Incoming = Increment;
|
||||
type Response = ();
|
||||
|
||||
fn handle(&mut self, _ctx: &Ctx, _msg: Increment) {
|
||||
self.count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
struct SharedCounter {
|
||||
count: std::sync::Arc<std::sync::atomic::AtomicUsize>,
|
||||
}
|
||||
|
||||
impl ActorInterface for SharedCounter {
|
||||
type Incoming = Increment;
|
||||
type Response = ();
|
||||
|
||||
fn handle(&mut self, _ctx: &Ctx, _msg: Increment) {
|
||||
self.count
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
/// An actor that handles variable-sized payloads
|
||||
struct PayloadActor {
|
||||
bytes_received: usize,
|
||||
}
|
||||
|
||||
impl PayloadActor {
|
||||
fn new() -> Self {
|
||||
Self { bytes_received: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Payload(Vec<u8>);
|
||||
|
||||
impl ActorInterface for PayloadActor {
|
||||
type Incoming = Payload;
|
||||
type Response = ();
|
||||
|
||||
fn handle(&mut self, _ctx: &Ctx, msg: Payload) {
|
||||
self.bytes_received += msg.0.len();
|
||||
black_box(&msg.0);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
/// Benchmark: How throughput scales with actor count
|
||||
pub fn bench_actor_count_scaling(suite: &mut BenchSuite) {
|
||||
let messages_per_actor = 100u64;
|
||||
|
||||
for actor_count in [10u64, 100, 500, 1000] {
|
||||
let name = format!("scaling_{}_actors", actor_count);
|
||||
let total_messages = actor_count * messages_per_actor;
|
||||
|
||||
let result = Bench::new(&name)
|
||||
.warmup(2)
|
||||
.iters(10)
|
||||
.elements(total_messages)
|
||||
.run_with_setup(
|
||||
|| {
|
||||
let config = RuntimeConfig {
|
||||
max_actors: (actor_count as usize) + 100,
|
||||
actor_max_messages: (total_messages as usize) * 3,
|
||||
num_threads: 1,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
|
||||
// Spawn actors
|
||||
let mut actors = Vec::with_capacity(actor_count as usize);
|
||||
for _ in 0..actor_count {
|
||||
let addr = runtime.spawn(CounterActor::new()).unwrap();
|
||||
actors.push(addr);
|
||||
}
|
||||
|
||||
// Process spawns
|
||||
for _ in 0..(actor_count * 2) {
|
||||
runtime.tick();
|
||||
}
|
||||
|
||||
(runtime, actors, messages_per_actor)
|
||||
},
|
||||
|(runtime, actors, msgs_per)| {
|
||||
// Distribute messages across all actors
|
||||
for _ in 0..msgs_per {
|
||||
for actor in &actors {
|
||||
let _ = runtime.send_to::<Increment>(*actor, Increment);
|
||||
}
|
||||
}
|
||||
|
||||
// Process all
|
||||
let total = actors.len() as u64 * msgs_per;
|
||||
for _ in 0..(total * 3) {
|
||||
runtime.tick();
|
||||
}
|
||||
black_box(());
|
||||
},
|
||||
);
|
||||
|
||||
suite.add(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Benchmark: How throughput scales with thread count (multithreaded runtime)
|
||||
pub fn bench_thread_count_scaling(suite: &mut BenchSuite) {
|
||||
let actor_count = 100u64;
|
||||
let messages_per_actor = 500u64;
|
||||
let total_messages = actor_count * messages_per_actor;
|
||||
|
||||
for thread_count in [2usize, 4, 8] {
|
||||
let name = format!("scaling_{}_threads", thread_count);
|
||||
|
||||
let result = Bench::new(&name)
|
||||
.warmup(1)
|
||||
.iters(5)
|
||||
.elements(total_messages)
|
||||
.run_with_setup(
|
||||
|| {
|
||||
let counter = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
let config = RuntimeConfig {
|
||||
max_actors: (actor_count as usize) + 100,
|
||||
actor_max_messages: (total_messages as usize) * 3,
|
||||
num_threads: thread_count,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
|
||||
let mut actors = Vec::with_capacity(actor_count as usize);
|
||||
for _ in 0..actor_count {
|
||||
let addr = runtime
|
||||
.spawn(SharedCounter {
|
||||
count: counter.clone(),
|
||||
})
|
||||
.unwrap();
|
||||
actors.push(addr);
|
||||
}
|
||||
|
||||
let handle = runtime.run().unwrap();
|
||||
|
||||
for actor in &actors {
|
||||
loop {
|
||||
if handle
|
||||
.runtime
|
||||
.send_to::<Increment>(*actor, Increment)
|
||||
.is_ok()
|
||||
{
|
||||
break;
|
||||
}
|
||||
thread::yield_now();
|
||||
}
|
||||
}
|
||||
|
||||
while counter.load(std::sync::atomic::Ordering::Relaxed) < actors.len() {
|
||||
thread::yield_now();
|
||||
}
|
||||
counter.store(0, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
(handle, actors, counter)
|
||||
},
|
||||
|(handle, actors, counter)| {
|
||||
for _ in 0..messages_per_actor {
|
||||
for actor in &actors {
|
||||
let _ = handle.runtime.send_to::<Increment>(*actor, Increment);
|
||||
}
|
||||
}
|
||||
|
||||
while counter.load(std::sync::atomic::Ordering::Relaxed)
|
||||
< total_messages as usize
|
||||
{
|
||||
thread::yield_now();
|
||||
}
|
||||
|
||||
handle.shutdown();
|
||||
handle.join();
|
||||
black_box(());
|
||||
},
|
||||
);
|
||||
|
||||
suite.add(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Benchmark: How throughput scales with message payload size
|
||||
pub fn bench_payload_size_scaling(suite: &mut BenchSuite) {
|
||||
let message_count = 1_000u64;
|
||||
|
||||
for payload_size in [64usize, 1024, 16384, 65536] {
|
||||
let name = format!("payload_{}B", payload_size);
|
||||
let payload = vec![0u8; payload_size];
|
||||
|
||||
let result = Bench::new(&name)
|
||||
.warmup(2)
|
||||
.iters(20)
|
||||
.elements(message_count)
|
||||
.run_with_setup(
|
||||
|| {
|
||||
let config = RuntimeConfig {
|
||||
max_actors: 10,
|
||||
actor_max_messages: (message_count as usize) * 2,
|
||||
num_threads: 1,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
let sink = runtime.spawn(PayloadActor::new()).unwrap();
|
||||
|
||||
// Process spawn
|
||||
for _ in 0..10 {
|
||||
runtime.tick();
|
||||
}
|
||||
|
||||
(runtime, sink, payload.clone())
|
||||
},
|
||||
|(runtime, sink, payload)| {
|
||||
for _ in 0..message_count {
|
||||
let _ = runtime.send_to::<Payload>(sink, Payload(payload.clone()));
|
||||
}
|
||||
|
||||
for _ in 0..(message_count * 3) {
|
||||
runtime.tick();
|
||||
}
|
||||
black_box(());
|
||||
},
|
||||
);
|
||||
|
||||
suite.add(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Run all scaling benchmarks
|
||||
pub fn run_all() -> BenchSuite {
|
||||
let mut suite = BenchSuite::new("Scaling Benchmarks");
|
||||
|
||||
println!("\nRunning actor count scaling benchmarks...");
|
||||
bench_actor_count_scaling(&mut suite);
|
||||
|
||||
println!("Running thread count scaling benchmarks...");
|
||||
bench_thread_count_scaling(&mut suite);
|
||||
|
||||
println!("Running payload size scaling benchmarks...");
|
||||
bench_payload_size_scaling(&mut suite);
|
||||
|
||||
suite
|
||||
}
|
||||
337
benches/throughput.rs
Normal file
337
benches/throughput.rs
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
//! 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<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: &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(3)
|
||||
.iters(20)
|
||||
.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,
|
||||
};
|
||||
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
|
||||
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,
|
||||
actor_max_messages: 100,
|
||||
num_threads: 1,
|
||||
};
|
||||
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 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,
|
||||
actor_max_messages: (fan_count as usize)
|
||||
* (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 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::<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,
|
||||
actor_max_messages: total_messages * 3,
|
||||
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 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::<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,
|
||||
actor_max_messages: 10_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 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::<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
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
use swactor::{
|
||||
Ctx,
|
||||
actor::{ActorAddress, ActorInterface},
|
||||
runtime::{Ctx, Runtime, RuntimeConfig},
|
||||
runtime::{Runtime, RuntimeConfig},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use swactor::{
|
||||
Ctx,
|
||||
actor::{ActorAddress, ActorInterface},
|
||||
runtime::{Ctx, Inbox, Runtime, RuntimeConfig},
|
||||
runtime::{Inbox, Runtime, RuntimeConfig},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
|
|
|
|||
40
src/actor.rs
40
src/actor.rs
|
|
@ -1,11 +1,49 @@
|
|||
use std::any::Any;
|
||||
|
||||
use crate::{get_random, runtime::{ContextInner, Ctx}, worker::Mailbox};
|
||||
use crate::{get_random, worker::mailbox::Mailbox};
|
||||
use crate::context::{ContextInner, Ctx};
|
||||
|
||||
/// The primary trait defining data that can be passed to and from actor processes
|
||||
pub trait Message: 'static + Sized + Clone + Send + Sync {}
|
||||
impl<T: 'static + Sized + Clone + Send + Sync> Message for T {}
|
||||
|
||||
/// The trait that needs to be implemented in order to run a process as an `Actor`
|
||||
///
|
||||
/// The `Incoming` type represents `Messages` that can be delivered to the `Actor`.
|
||||
///
|
||||
/// The `Response` type represents possible `Messages` the actor may attempt to reply with.
|
||||
///
|
||||
/// The `fn handle(..)` is where you implement the logic for handling `Incoming` messages
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use swactor::{Ctx, actor::{ActorAddress, ActorInterface}};
|
||||
///
|
||||
/// struct Greeter {
|
||||
/// num_greeted: usize,
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Clone)] // required to auto implement `Message`
|
||||
/// struct GreetMessage {
|
||||
/// who: String,
|
||||
/// return_addr: ActorAddress,
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Clone)]
|
||||
/// struct GreetResponse(String);
|
||||
///
|
||||
/// impl ActorInterface for Greeter {
|
||||
/// type Incoming = GreetMessage;
|
||||
/// type Response = GreetResponse;
|
||||
///
|
||||
/// fn handle(&mut self, ctx: &Ctx, msg: Self::Incoming) {
|
||||
/// let response = GreetResponse(format!("Hello, {}!", msg.who).to_string());
|
||||
/// if let Ok(_) = ctx.send(msg.return_addr, response) {
|
||||
/// self.num_greeted += 1;
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub trait ActorInterface: 'static + Send {
|
||||
type Incoming: Message;
|
||||
type Response: Message;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::RwLock;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use crate::actor::ActorAddress;
|
||||
|
||||
|
|
@ -51,26 +50,6 @@ impl AddressMap {
|
|||
}
|
||||
}
|
||||
|
||||
/// Round-robin actor placement strategy.
|
||||
pub(crate) struct Placement {
|
||||
next: AtomicUsize,
|
||||
num_workers: usize,
|
||||
}
|
||||
|
||||
impl Placement {
|
||||
pub fn new(num_workers: usize) -> Self {
|
||||
Self {
|
||||
next: AtomicUsize::new(0),
|
||||
num_workers,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_worker(&self) -> WorkerId {
|
||||
let id = self.next.fetch_add(1, Ordering::Relaxed) % self.num_workers;
|
||||
WorkerId(id)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -108,13 +87,4 @@ mod tests {
|
|||
map.insert(addr1, WorkerId(0));
|
||||
assert_eq!(map.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_robin() {
|
||||
let p = Placement::new(3);
|
||||
assert_eq!(p.next_worker(), WorkerId(0));
|
||||
assert_eq!(p.next_worker(), WorkerId(1));
|
||||
assert_eq!(p.next_worker(), WorkerId(2));
|
||||
assert_eq!(p.next_worker(), WorkerId(0));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use crossbeam_queue::{ArrayQueue, SegQueue};
|
||||
|
||||
pub struct HybridChannel<T> {
|
||||
ring: ArrayQueue<T>,
|
||||
overflow: SegQueue<T>,
|
||||
overflow_len: AtomicUsize,
|
||||
}
|
||||
|
||||
impl<T> HybridChannel<T> {
|
||||
|
|
@ -13,6 +15,7 @@ impl<T> HybridChannel<T> {
|
|||
Self {
|
||||
ring: ArrayQueue::new(capacity),
|
||||
overflow: SegQueue::new(),
|
||||
overflow_len: AtomicUsize::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -21,6 +24,7 @@ impl<T> HybridChannel<T> {
|
|||
Ok(()) => Ok(()),
|
||||
Err(v) => {
|
||||
self.overflow.push(v);
|
||||
self.overflow_len.fetch_add(1, Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -33,12 +37,16 @@ impl<T> HybridChannel<T> {
|
|||
|
||||
match self.overflow.pop() {
|
||||
Some(value) => {
|
||||
self.overflow_len.fetch_sub(1, Ordering::Relaxed);
|
||||
Some(value)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.ring.len() + self.overflow_len.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Receiver<T> {
|
||||
|
|
@ -51,6 +59,11 @@ impl<T> Receiver<T> {
|
|||
|
||||
Self { queue }
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.queue.len()
|
||||
}
|
||||
|
||||
pub fn try_recv(&self) -> Option<T> {
|
||||
return self.queue.pop();
|
||||
}
|
||||
|
|
@ -1,35 +1,8 @@
|
|||
/// Backoff policy for worker threads when idle.
|
||||
///
|
||||
/// Workers spin → yield → sleep with increasing delay when no work is available.
|
||||
pub struct BackoffPolicy {
|
||||
/// Number of idle ticks before switching from spin to yield.
|
||||
pub spin_threshold: u32,
|
||||
/// Number of idle ticks before switching from yield to sleep.
|
||||
pub yield_threshold: u32,
|
||||
/// Microseconds added per tick beyond the yield threshold.
|
||||
pub sleep_increment_us: u64,
|
||||
/// Maximum sleep duration in microseconds.
|
||||
pub sleep_max_us: u64,
|
||||
}
|
||||
|
||||
impl Default for BackoffPolicy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
spin_threshold: 64,
|
||||
yield_threshold: 256,
|
||||
sleep_increment_us: 50,
|
||||
sleep_max_us: 1000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The tunable settings for the runtime.
|
||||
pub struct RuntimeConfig {
|
||||
pub max_actors: usize,
|
||||
pub actor_max_messages: usize,
|
||||
pub num_threads: usize,
|
||||
pub mailbox_waterlevel: usize,
|
||||
pub backoff_policy: BackoffPolicy,
|
||||
}
|
||||
|
||||
/// 8kB for the `Box<..>` before counting the rest of the memory
|
||||
|
|
@ -40,16 +13,12 @@ const DEFAULT_MAX_ACTORS: usize = 1_000;
|
|||
/// 1_000 * 16kB = 16MB
|
||||
const DEFAULT_ACTOR_MAX_MESSAGES: usize = 1_000;
|
||||
|
||||
const DEFAULT_MAILBOX_WATERLEVEL: usize = 10;
|
||||
|
||||
impl Default for RuntimeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_actors: DEFAULT_MAX_ACTORS,
|
||||
actor_max_messages: DEFAULT_ACTOR_MAX_MESSAGES,
|
||||
num_threads: 1,
|
||||
mailbox_waterlevel: DEFAULT_MAILBOX_WATERLEVEL,
|
||||
backoff_policy: BackoffPolicy::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
45
src/context.rs
Normal file
45
src/context.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
use std::any::Any;
|
||||
|
||||
use crate::actor::{Actor, ActorAddress, ActorInterface, AnyActor, Message};
|
||||
use crate::worker::mailbox::Mailbox;
|
||||
use crate::Error;
|
||||
|
||||
/// Object-safe inner trait for sending type-erased messages.
|
||||
pub(crate) trait ContextInner {
|
||||
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>;
|
||||
}
|
||||
|
||||
/// Actor syscall interface — passed to `ActorInterface::handle()`.
|
||||
///
|
||||
/// Wraps a `&dyn ContextInner` to solve the object-safety problem while
|
||||
/// providing a typed public API.
|
||||
pub struct Ctx<'a> {
|
||||
inner: &'a dyn ContextInner,
|
||||
self_addr: ActorAddress,
|
||||
}
|
||||
|
||||
impl<'a> Ctx<'a> {
|
||||
pub(crate) fn new(inner: &'a dyn ContextInner, self_addr: ActorAddress) -> Self {
|
||||
Self { inner, self_addr }
|
||||
}
|
||||
|
||||
/// Returns the address of the actor currently being ticked.
|
||||
pub fn self_addr(&self) -> ActorAddress {
|
||||
self.self_addr
|
||||
}
|
||||
|
||||
/// Send a typed message to an actor address.
|
||||
pub fn send<M: Message>(&self, addr: ActorAddress, msg: M) -> Result<(), Error> {
|
||||
self.inner.send_any(addr, Box::new(msg))
|
||||
}
|
||||
|
||||
/// Spawn a new actor, returning its address.
|
||||
pub fn spawn<A: ActorInterface>(&self, actor: A) -> Result<ActorAddress, Error> {
|
||||
let addr = ActorAddress::new_random();
|
||||
let actor = Actor::new(addr, Mailbox::new(), actor);
|
||||
let boxed: Box<dyn AnyActor> = Box::new(actor);
|
||||
self.inner.spawn_any(addr, boxed)?;
|
||||
Ok(addr)
|
||||
}
|
||||
}
|
||||
49
src/envelope.rs
Normal file
49
src/envelope.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
use std::any::Any;
|
||||
|
||||
use crate::actor::ActorAddress;
|
||||
|
||||
/// A type-erased message envelope for cross-worker delivery.
|
||||
///
|
||||
/// Uses `Box` (no atomic refcount) and move semantics (no clone).
|
||||
pub(crate) struct Envelope {
|
||||
dest: ActorAddress,
|
||||
payload: Box<dyn Any + Send>,
|
||||
}
|
||||
|
||||
impl Envelope {
|
||||
pub fn new(dest: ActorAddress, payload: Box<dyn Any + Send>) -> Self {
|
||||
Self { dest, payload }
|
||||
}
|
||||
|
||||
pub fn dest(&self) -> ActorAddress {
|
||||
self.dest
|
||||
}
|
||||
|
||||
pub fn downcast<M: 'static>(self) -> Option<M> {
|
||||
self.payload.downcast::<M>().ok().map(|b| *b)
|
||||
}
|
||||
|
||||
pub fn into_payload(self) -> Box<dyn Any + Send> {
|
||||
self.payload
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn envelope_roundtrip() {
|
||||
let addr = ActorAddress::default();
|
||||
let env = Envelope::new(addr, Box::new(42u64));
|
||||
assert_eq!(env.dest(), addr);
|
||||
assert_eq!(env.downcast::<u64>(), Some(42u64));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelope_wrong_type_returns_none() {
|
||||
let addr = ActorAddress::default();
|
||||
let env = Envelope::new(addr, Box::new(42u64));
|
||||
assert!(env.downcast::<String>().is_none());
|
||||
}
|
||||
}
|
||||
|
|
@ -5,8 +5,12 @@ pub(crate) mod channel;
|
|||
pub(crate) mod error;
|
||||
pub use error::Error;
|
||||
|
||||
pub mod context;
|
||||
pub use context::Ctx;
|
||||
|
||||
pub(crate) mod envelope;
|
||||
pub(crate) mod address_map;
|
||||
pub(crate) mod placement;
|
||||
pub mod config;
|
||||
|
||||
pub mod runtime;
|
||||
|
|
|
|||
37
src/placement.rs
Normal file
37
src/placement.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use crate::address_map::WorkerId;
|
||||
|
||||
/// Round-robin actor placement strategy.
|
||||
pub(crate) struct Placement {
|
||||
next: AtomicUsize,
|
||||
num_workers: usize,
|
||||
}
|
||||
|
||||
impl Placement {
|
||||
pub fn new(num_workers: usize) -> Self {
|
||||
Self {
|
||||
next: AtomicUsize::new(0),
|
||||
num_workers,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_worker(&self) -> WorkerId {
|
||||
let id = self.next.fetch_add(1, Ordering::Relaxed) % self.num_workers;
|
||||
WorkerId(id)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn round_robin() {
|
||||
let p = Placement::new(3);
|
||||
assert_eq!(p.next_worker(), WorkerId(0));
|
||||
assert_eq!(p.next_worker(), WorkerId(1));
|
||||
assert_eq!(p.next_worker(), WorkerId(2));
|
||||
assert_eq!(p.next_worker(), WorkerId(0));
|
||||
}
|
||||
}
|
||||
265
src/runtime.rs
265
src/runtime.rs
|
|
@ -6,14 +6,66 @@ use std::sync::{Arc, RwLock};
|
|||
use std::thread::{self, JoinHandle};
|
||||
|
||||
use crate::actor::{Actor, ActorAddress, ActorInterface, AnyActor, Message};
|
||||
use crate::address_map::{AddressMap, Placement, WorkerId};
|
||||
use crate::address_map::{AddressMap, WorkerId};
|
||||
use crate::channel::{Receiver, Sender};
|
||||
// Re-export config types so existing code using `runtime::RuntimeConfig` still works
|
||||
pub use crate::config::{BackoffPolicy, RuntimeConfig};
|
||||
use crate::worker::Mailbox;
|
||||
use crate::worker::{TickContext, Worker};
|
||||
// Re-export RuntimeConfig so existing code using `runtime::RuntimeConfig` still works
|
||||
pub use crate::config::RuntimeConfig;
|
||||
use crate::context::ContextInner;
|
||||
use crate::envelope::Envelope;
|
||||
use crate::placement::Placement;
|
||||
use crate::worker::mailbox::Mailbox;
|
||||
use crate::worker::Worker;
|
||||
use crate::Error;
|
||||
|
||||
// ─── SenderT trait (moved from router.rs) ────────────────────────────────────
|
||||
|
||||
/// Type-erased sender for external inboxes.
|
||||
pub(crate) trait SenderT: Send + Sync {
|
||||
fn try_send_any(&self, msg: Box<dyn Any + Send>);
|
||||
}
|
||||
|
||||
impl<M: Message> SenderT for Sender<M> {
|
||||
fn try_send_any(&self, msg: Box<dyn Any + Send>) {
|
||||
if let Ok(typed) = msg.downcast::<M>() {
|
||||
let _ = Sender::try_send(self, *typed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── InboxRegistry ───────────────────────────────────────────────────────────
|
||||
|
||||
/// Registry of external inboxes — replaces the Router's role for non-actor receivers.
|
||||
pub(crate) struct InboxRegistry {
|
||||
senders: RwLock<HashMap<ActorAddress, Arc<dyn SenderT>>>,
|
||||
}
|
||||
|
||||
impl InboxRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
senders: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(&self, addr: ActorAddress, sender: Arc<dyn SenderT>) {
|
||||
self.senders.write().unwrap().insert(addr, sender);
|
||||
}
|
||||
|
||||
pub fn try_deliver(
|
||||
&self,
|
||||
addr: ActorAddress,
|
||||
msg: Box<dyn Any + Send>,
|
||||
) -> Result<(), Error> {
|
||||
let senders = self.senders.read().unwrap();
|
||||
if let Some(sender) = senders.get(&addr) {
|
||||
sender.try_send_any(msg);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::from("Address not found"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Inbox ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Generic message inbox for receiving messages outside of the runtime.
|
||||
pub struct Inbox<M: Message> {
|
||||
|
|
@ -31,74 +83,6 @@ impl<M: Message> Inbox<M> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Handle for dealing with a runtime that has started via the `Runtime::run()` method.
|
||||
pub struct RuntimeHandle {
|
||||
pub runtime: Arc<Runtime>,
|
||||
threads: Vec<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl RuntimeHandle {
|
||||
pub fn join(self) {
|
||||
for handle in self.threads {
|
||||
let _ = handle.join();
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple helper, calls the inner `Runtime::shutdown()` method
|
||||
pub fn shutdown(&self) {
|
||||
self.runtime.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/// Actor syscall interface — passed to `ActorInterface::handle()`.
|
||||
///
|
||||
/// Wraps a `&dyn ContextInner` to solve the object-safety problem while
|
||||
/// providing a typed public API.
|
||||
pub struct Ctx<'a> {
|
||||
inner: &'a dyn ContextInner,
|
||||
self_addr: ActorAddress,
|
||||
}
|
||||
|
||||
impl<'a> Ctx<'a> {
|
||||
pub(crate) fn new(inner: &'a dyn ContextInner, self_addr: ActorAddress) -> Self {
|
||||
Self { inner, self_addr }
|
||||
}
|
||||
|
||||
/// Returns the address of the actor currently being ticked.
|
||||
pub fn self_addr(&self) -> ActorAddress {
|
||||
self.self_addr
|
||||
}
|
||||
|
||||
/// Send a typed message to an actor address.
|
||||
pub fn send<M: Message>(&self, addr: ActorAddress, msg: M) -> Result<(), Error> {
|
||||
self.inner.send_any(addr, Box::new(msg))
|
||||
}
|
||||
|
||||
/// Spawn a new actor, returning its address.
|
||||
pub fn spawn<A: ActorInterface>(&self, actor: A) -> Result<ActorAddress, Error> {
|
||||
let addr = ActorAddress::new_random();
|
||||
let waterlevel = self.inner.mailbox_waterlevel();
|
||||
let actor = Actor::new(addr, Mailbox::new(waterlevel), actor);
|
||||
let boxed: Box<dyn AnyActor> = Box::new(actor);
|
||||
self.inner.spawn_any(addr, boxed)?;
|
||||
Ok(addr)
|
||||
}
|
||||
}
|
||||
|
||||
/// Type-erased sender for external inboxes.
|
||||
pub(crate) trait SenderT: Send + Sync {
|
||||
fn try_send_any(&self, msg: Box<dyn Any + Send>);
|
||||
}
|
||||
|
||||
impl<M: Message> SenderT for Sender<M> {
|
||||
fn try_send_any(&self, msg: Box<dyn Any + Send>) {
|
||||
if let Ok(typed) = msg.downcast::<M>() {
|
||||
let _ = Sender::try_send(self, *typed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ─── Runtime ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// The `Runtime` struct is the primary gateway for interacting with the framework.
|
||||
|
|
@ -187,7 +171,7 @@ impl Runtime {
|
|||
let addr = ActorAddress::new_random();
|
||||
let worker_id = self.placement.next_worker();
|
||||
self.address_map.insert(addr, worker_id);
|
||||
let actor = Actor::new(addr, Mailbox::new(self.config.mailbox_waterlevel), actor);
|
||||
let actor = Actor::new(addr, Mailbox::new(), actor);
|
||||
let boxed: Box<dyn AnyActor> = Box::new(actor);
|
||||
self.spawn_txs[worker_id.as_usize()]
|
||||
.try_send((addr, boxed))
|
||||
|
|
@ -221,15 +205,13 @@ impl Runtime {
|
|||
/// Drive one tick of the single-threaded worker.
|
||||
pub fn tick(&self) {
|
||||
if let Some(ref worker) = self.single_worker {
|
||||
let tc = TickContext {
|
||||
address_map: &self.address_map,
|
||||
transfer_txs: &self.transfer_txs,
|
||||
spawn_txs: &self.spawn_txs,
|
||||
placement: &self.placement,
|
||||
inbox_registry: &self.inbox_registry,
|
||||
config: &self.config,
|
||||
};
|
||||
worker.borrow_mut().tick_once(&tc);
|
||||
worker.borrow_mut().tick_once(
|
||||
&self.address_map,
|
||||
&self.transfer_txs,
|
||||
&self.spawn_txs,
|
||||
&self.placement,
|
||||
&self.inbox_registry,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -259,15 +241,7 @@ impl Runtime {
|
|||
for mut worker in workers {
|
||||
let rt_clone = rt.clone();
|
||||
let handle = thread::spawn(move || {
|
||||
let tc = TickContext {
|
||||
address_map: &rt_clone.address_map,
|
||||
transfer_txs: &rt_clone.transfer_txs,
|
||||
spawn_txs: &rt_clone.spawn_txs,
|
||||
placement: &rt_clone.placement,
|
||||
inbox_registry: &rt_clone.inbox_registry,
|
||||
config: &rt_clone.config,
|
||||
};
|
||||
worker.run(&tc, &rt_clone.is_running, &rt_clone.config.backoff_policy);
|
||||
worker_loop(&mut worker, &rt_clone);
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
|
@ -284,78 +258,55 @@ impl Runtime {
|
|||
}
|
||||
}
|
||||
|
||||
/// Worker thread loop for multi-threaded runtime.
|
||||
/// Uses spin → yield → park backoff to reduce CPU usage when idle.
|
||||
fn worker_loop(worker: &mut Worker, rt: &Runtime) {
|
||||
let mut idle_count: u32 = 0;
|
||||
|
||||
while rt.is_running.load(Ordering::Acquire) {
|
||||
let did_work = worker.tick_once(
|
||||
&rt.address_map,
|
||||
&rt.transfer_txs,
|
||||
&rt.spawn_txs,
|
||||
&rt.placement,
|
||||
&rt.inbox_registry,
|
||||
);
|
||||
|
||||
/// A type-erased message envelope for cross-worker delivery.
|
||||
///
|
||||
/// Uses `Box` (no atomic refcount) and move semantics (no clone).
|
||||
pub(crate) struct Envelope {
|
||||
dest: ActorAddress,
|
||||
payload: Box<dyn Any + Send>,
|
||||
}
|
||||
|
||||
impl Envelope {
|
||||
pub fn new(dest: ActorAddress, payload: Box<dyn Any + Send>) -> Self {
|
||||
Self { dest, payload }
|
||||
}
|
||||
|
||||
pub fn dest(&self) -> ActorAddress {
|
||||
self.dest
|
||||
}
|
||||
|
||||
pub fn downcast<M: 'static>(self) -> Option<M> {
|
||||
self.payload.downcast::<M>().ok().map(|b| *b)
|
||||
}
|
||||
|
||||
pub fn into_payload(self) -> Box<dyn Any + Send> {
|
||||
self.payload
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ─── InboxRegistry ───────────────────────────────────────────────────────────
|
||||
|
||||
/// Registry of external inboxes — replaces the Router's role for non-actor receivers.
|
||||
pub(crate) struct InboxRegistry {
|
||||
senders: RwLock<HashMap<ActorAddress, Arc<dyn SenderT>>>,
|
||||
}
|
||||
|
||||
impl InboxRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
senders: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(&self, addr: ActorAddress, sender: Arc<dyn SenderT>) {
|
||||
self.senders.write().unwrap().insert(addr, sender);
|
||||
}
|
||||
|
||||
pub fn try_deliver(
|
||||
&self,
|
||||
addr: ActorAddress,
|
||||
msg: Box<dyn Any + Send>,
|
||||
) -> Result<(), Error> {
|
||||
let senders = self.senders.read().unwrap();
|
||||
if let Some(sender) = senders.get(&addr) {
|
||||
sender.try_send_any(msg);
|
||||
Ok(())
|
||||
if did_work {
|
||||
idle_count = 0;
|
||||
} else {
|
||||
Err(Error::from("Address not found"))
|
||||
idle_count = idle_count.saturating_add(1);
|
||||
if idle_count < 64 {
|
||||
core::hint::spin_loop();
|
||||
} else if idle_count < 256 {
|
||||
thread::yield_now();
|
||||
} else {
|
||||
// Park: sleep briefly, cap at 1ms
|
||||
let micros = std::cmp::min((idle_count - 256) as u64 * 50, 1000);
|
||||
thread::sleep(std::time::Duration::from_micros(micros));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// Object-safe inner trait for sending type-erased messages.
|
||||
pub(crate) trait ContextInner {
|
||||
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 mailbox_waterlevel(&self) -> usize;
|
||||
/// Handle for dealing with a runtime that has started via the `Runtime::run()` method.
|
||||
pub struct RuntimeHandle {
|
||||
pub runtime: Arc<Runtime>,
|
||||
threads: Vec<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl RuntimeHandle {
|
||||
pub fn join(self) {
|
||||
for handle in self.threads {
|
||||
let _ = handle.join();
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple helper, calls the inner `Runtime::shutdown()` method
|
||||
pub fn shutdown(&self) {
|
||||
self.runtime.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextInner for Runtime {
|
||||
fn send_any(&self, addr: ActorAddress, msg: Box<dyn Any + Send>) -> Result<(), Error> {
|
||||
|
|
@ -375,8 +326,4 @@ impl ContextInner for Runtime {
|
|||
.try_send((addr, actor))
|
||||
.map_err(|_| Error::from("Spawn queue full"))
|
||||
}
|
||||
|
||||
fn mailbox_waterlevel(&self) -> usize {
|
||||
self.config.mailbox_waterlevel
|
||||
}
|
||||
}
|
||||
|
|
|
|||
258
src/worker.rs
258
src/worker.rs
|
|
@ -1,258 +0,0 @@
|
|||
use std::any::Any;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::thread;
|
||||
|
||||
use crate::actor::{ActorAddress, AnyActor, Message};
|
||||
use crate::address_map::{AddressMap, Placement, WorkerId};
|
||||
use crate::channel::{Receiver, Sender};
|
||||
use crate::config::{BackoffPolicy, RuntimeConfig};
|
||||
use crate::runtime::{ContextInner, Envelope, InboxRegistry};
|
||||
use crate::Error;
|
||||
|
||||
/// Shared state passed to tick_once — single thin pointer avoids register spill.
|
||||
pub(crate) struct TickContext<'a> {
|
||||
pub address_map: &'a AddressMap,
|
||||
pub transfer_txs: &'a [Sender<Envelope>],
|
||||
pub spawn_txs: &'a [Sender<(ActorAddress, Box<dyn AnyActor>)>],
|
||||
pub placement: &'a Placement,
|
||||
pub inbox_registry: &'a InboxRegistry,
|
||||
pub config: &'a RuntimeConfig,
|
||||
}
|
||||
|
||||
/// A worker owns a set of actors and runs them in a loop.
|
||||
pub(crate) struct Worker {
|
||||
id: WorkerId,
|
||||
pool: ActorPool,
|
||||
transfer_rx: Receiver<Envelope>,
|
||||
spawn_rx: Receiver<(ActorAddress, Box<dyn AnyActor>)>,
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
pub fn new(
|
||||
id: WorkerId,
|
||||
transfer_rx: Receiver<Envelope>,
|
||||
spawn_rx: Receiver<(ActorAddress, Box<dyn AnyActor>)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
pool: ActorPool::new(),
|
||||
transfer_rx,
|
||||
spawn_rx,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run one iteration of the worker loop. Returns `true` if any work was done.
|
||||
pub fn tick_once(&mut self, tc: &TickContext) -> bool {
|
||||
let mut did_work = false;
|
||||
|
||||
// 1. Drain spawn queue → add actors to pool
|
||||
while let Some((addr, actor)) = self.spawn_rx.try_recv() {
|
||||
self.pool.insert(addr, actor);
|
||||
did_work = true;
|
||||
}
|
||||
|
||||
// 2. Drain transfer queue → deliver envelopes to actors
|
||||
while let Some(envelope) = self.transfer_rx.try_recv() {
|
||||
let dest = envelope.dest();
|
||||
let payload = envelope.into_payload();
|
||||
self.pool.deliver(&dest, payload);
|
||||
did_work = true;
|
||||
}
|
||||
|
||||
// 3. Tick all actors with WorkerContext
|
||||
let pending_local: RefCell<Vec<(ActorAddress, Box<dyn Any + Send>)>> =
|
||||
RefCell::new(Vec::new());
|
||||
|
||||
{
|
||||
let worker_ctx = WorkerContext {
|
||||
worker_id: self.id,
|
||||
address_map: tc.address_map,
|
||||
transfer_txs: tc.transfer_txs,
|
||||
spawn_txs: tc.spawn_txs,
|
||||
placement: tc.placement,
|
||||
inbox_registry: tc.inbox_registry,
|
||||
config: tc.config,
|
||||
pending_local: &pending_local,
|
||||
};
|
||||
if self.pool.tick_all(&worker_ctx) {
|
||||
did_work = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Drain pending_local buffer → deliver to local actors
|
||||
let pending = pending_local.into_inner();
|
||||
if !pending.is_empty() {
|
||||
did_work = true;
|
||||
}
|
||||
for (addr, msg) in pending {
|
||||
self.pool.deliver(&addr, msg);
|
||||
}
|
||||
|
||||
did_work
|
||||
}
|
||||
|
||||
pub(crate) fn run(&mut self, tc: &TickContext, is_running: &AtomicBool, backoff: &BackoffPolicy) {
|
||||
let mut idle_count: u32 = 0;
|
||||
while is_running.load(Ordering::Acquire) {
|
||||
let did_work = self.tick_once(tc);
|
||||
if did_work {
|
||||
idle_count = 0;
|
||||
} else {
|
||||
idle_count = idle_count.saturating_add(1);
|
||||
if idle_count < backoff.spin_threshold {
|
||||
// Hot spin
|
||||
} else if idle_count < backoff.yield_threshold {
|
||||
thread::yield_now();
|
||||
} else {
|
||||
let micros = std::cmp::min(
|
||||
(idle_count - backoff.yield_threshold) as u64 * backoff.sleep_increment_us,
|
||||
backoff.sleep_max_us,
|
||||
);
|
||||
thread::sleep(std::time::Duration::from_micros(micros));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `ContextInner` impl for worker threads.
|
||||
///
|
||||
/// Same-worker sends are buffered in `pending_local` (delivered after current tick round).
|
||||
/// Cross-worker sends go through the transfer queue.
|
||||
struct WorkerContext<'a> {
|
||||
worker_id: WorkerId,
|
||||
address_map: &'a AddressMap,
|
||||
transfer_txs: &'a [Sender<Envelope>],
|
||||
spawn_txs: &'a [Sender<(ActorAddress, Box<dyn AnyActor>)>],
|
||||
placement: &'a Placement,
|
||||
inbox_registry: &'a InboxRegistry,
|
||||
config: &'a RuntimeConfig,
|
||||
pending_local: &'a RefCell<Vec<(ActorAddress, Box<dyn Any + Send>)>>,
|
||||
}
|
||||
|
||||
impl ContextInner for WorkerContext<'_> {
|
||||
fn send_any(&self, addr: ActorAddress, msg: Box<dyn Any + Send>) -> Result<(), Error> {
|
||||
match self.address_map.lookup(&addr) {
|
||||
Some(wid) if wid == self.worker_id => {
|
||||
// Same worker: buffer for local delivery (after current tick round)
|
||||
self.pending_local.borrow_mut().push((addr, msg));
|
||||
Ok(())
|
||||
}
|
||||
Some(wid) => {
|
||||
// Cross worker: envelope through transfer queue
|
||||
let envelope = Envelope::new(addr, msg);
|
||||
let _ = self.transfer_txs[wid.as_usize()].try_send(envelope);
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
// Try inbox registry (external inboxes)
|
||||
self.inbox_registry.try_deliver(addr, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_any(&self, addr: ActorAddress, actor: Box<dyn AnyActor>) -> Result<(), Error> {
|
||||
let worker_id = self.placement.next_worker();
|
||||
self.address_map.insert(addr, worker_id);
|
||||
self.spawn_txs[worker_id.as_usize()]
|
||||
.try_send((addr, actor))
|
||||
.map_err(|_| Error::from("Spawn queue full"))
|
||||
}
|
||||
|
||||
fn mailbox_waterlevel(&self) -> usize {
|
||||
self.config.mailbox_waterlevel
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-worker actor storage.
|
||||
pub(crate) struct ActorPool {
|
||||
actors: HashMap<ActorAddress, Box<dyn AnyActor>>,
|
||||
}
|
||||
|
||||
impl ActorPool {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
actors: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, addr: ActorAddress, actor: Box<dyn AnyActor>) {
|
||||
self.actors.insert(addr, actor);
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, addr: &ActorAddress) -> Option<Box<dyn AnyActor>> {
|
||||
self.actors.remove(addr)
|
||||
}
|
||||
|
||||
/// Deliver a type-erased message to the actor at `addr`.
|
||||
/// Returns `true` if the actor was found and the message type matched.
|
||||
pub fn deliver(&mut self, addr: &ActorAddress, msg: Box<dyn Any + Send>) -> bool {
|
||||
if let Some(actor) = self.actors.get_mut(addr) {
|
||||
actor.deliver(msg)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Tick all actors in the pool. Returns `true` if any actor processed messages.
|
||||
pub fn tick_all(&mut self, inner: &dyn ContextInner) -> bool {
|
||||
let mut did_work = false;
|
||||
for actor in self.actors.values_mut() {
|
||||
if actor.tick(inner) {
|
||||
did_work = true;
|
||||
}
|
||||
}
|
||||
did_work
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.actors.len()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
pub struct Mailbox<M: Message> {
|
||||
queue: VecDeque<M>,
|
||||
waterlevel: usize,
|
||||
}
|
||||
|
||||
impl<M: Message> Mailbox<M> {
|
||||
pub fn new(waterlevel: usize) -> Self {
|
||||
Self {
|
||||
queue: VecDeque::new(),
|
||||
waterlevel,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, msg: M) {
|
||||
self.queue.push_back(msg);
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> Option<M> {
|
||||
self.queue.pop_front()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.queue.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.queue.is_empty()
|
||||
}
|
||||
|
||||
/// How many messages to process this tick:
|
||||
/// - `len < waterlevel` → process all (`len`)
|
||||
/// - `len >= waterlevel` → process half (`len >> 1`)
|
||||
pub fn drain_count(&self) -> usize {
|
||||
let len = self.queue.len();
|
||||
if len < self.waterlevel {
|
||||
len
|
||||
} else {
|
||||
len >> 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
54
src/worker/mailbox.rs
Normal file
54
src/worker/mailbox.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
use std::collections::VecDeque;
|
||||
|
||||
use crate::actor::Message;
|
||||
|
||||
const DEFAULT_WATERLEVEL: usize = 10;
|
||||
|
||||
pub struct Mailbox<M: Message> {
|
||||
queue: VecDeque<M>,
|
||||
waterlevel: usize,
|
||||
}
|
||||
|
||||
impl<M: Message> Mailbox<M> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
queue: VecDeque::new(),
|
||||
waterlevel: DEFAULT_WATERLEVEL,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_waterlevel(waterlevel: usize) -> Self {
|
||||
Self {
|
||||
queue: VecDeque::new(),
|
||||
waterlevel,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, msg: M) {
|
||||
self.queue.push_back(msg);
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> Option<M> {
|
||||
self.queue.pop_front()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.queue.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.queue.is_empty()
|
||||
}
|
||||
|
||||
/// How many messages to process this tick:
|
||||
/// - `len < waterlevel` → process all (`len`)
|
||||
/// - `len >= waterlevel` → process half (`len >> 1`)
|
||||
pub fn drain_count(&self) -> usize {
|
||||
let len = self.queue.len();
|
||||
if len < self.waterlevel {
|
||||
len
|
||||
} else {
|
||||
len >> 1
|
||||
}
|
||||
}
|
||||
}
|
||||
138
src/worker/mod.rs
Normal file
138
src/worker/mod.rs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
pub mod mailbox;
|
||||
pub(crate) mod pool;
|
||||
|
||||
use std::any::Any;
|
||||
use std::cell::RefCell;
|
||||
|
||||
use crate::actor::{ActorAddress, AnyActor};
|
||||
use crate::address_map::{AddressMap, WorkerId};
|
||||
use crate::channel::{Receiver, Sender};
|
||||
use crate::context::ContextInner;
|
||||
use crate::envelope::Envelope;
|
||||
use crate::placement::Placement;
|
||||
use crate::runtime::InboxRegistry;
|
||||
use crate::Error;
|
||||
use pool::ActorPool;
|
||||
|
||||
/// A worker owns a set of actors and runs them in a loop.
|
||||
pub(crate) struct Worker {
|
||||
id: WorkerId,
|
||||
pool: ActorPool,
|
||||
transfer_rx: Receiver<Envelope>,
|
||||
spawn_rx: Receiver<(ActorAddress, Box<dyn AnyActor>)>,
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
pub fn new(
|
||||
id: WorkerId,
|
||||
transfer_rx: Receiver<Envelope>,
|
||||
spawn_rx: Receiver<(ActorAddress, Box<dyn AnyActor>)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
pool: ActorPool::new(),
|
||||
transfer_rx,
|
||||
spawn_rx,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run one iteration of the worker loop. Returns `true` if any work was done.
|
||||
pub fn tick_once(
|
||||
&mut self,
|
||||
address_map: &AddressMap,
|
||||
transfer_txs: &[Sender<Envelope>],
|
||||
spawn_txs: &[Sender<(ActorAddress, Box<dyn AnyActor>)>],
|
||||
placement: &Placement,
|
||||
inbox_registry: &InboxRegistry,
|
||||
) -> bool {
|
||||
let mut did_work = false;
|
||||
|
||||
// 1. Drain spawn queue → add actors to pool
|
||||
while let Some((addr, actor)) = self.spawn_rx.try_recv() {
|
||||
self.pool.insert(addr, actor);
|
||||
did_work = true;
|
||||
}
|
||||
|
||||
// 2. Drain transfer queue → deliver envelopes to actors
|
||||
while let Some(envelope) = self.transfer_rx.try_recv() {
|
||||
let dest = envelope.dest();
|
||||
let payload = envelope.into_payload();
|
||||
self.pool.deliver(&dest, payload);
|
||||
did_work = true;
|
||||
}
|
||||
|
||||
// 3. Tick all actors with WorkerContext
|
||||
let pending_local: RefCell<Vec<(ActorAddress, Box<dyn Any + Send>)>> =
|
||||
RefCell::new(Vec::new());
|
||||
|
||||
{
|
||||
let worker_ctx = WorkerContext {
|
||||
worker_id: self.id,
|
||||
address_map,
|
||||
transfer_txs,
|
||||
spawn_txs,
|
||||
placement,
|
||||
inbox_registry,
|
||||
pending_local: &pending_local,
|
||||
};
|
||||
if self.pool.tick_all(&worker_ctx) {
|
||||
did_work = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Drain pending_local buffer → deliver to local actors
|
||||
let pending = pending_local.into_inner();
|
||||
if !pending.is_empty() {
|
||||
did_work = true;
|
||||
}
|
||||
for (addr, msg) in pending {
|
||||
self.pool.deliver(&addr, msg);
|
||||
}
|
||||
|
||||
did_work
|
||||
}
|
||||
}
|
||||
|
||||
/// The `ContextInner` impl for worker threads.
|
||||
///
|
||||
/// Same-worker sends are buffered in `pending_local` (delivered after current tick round).
|
||||
/// Cross-worker sends go through the transfer queue.
|
||||
struct WorkerContext<'a> {
|
||||
worker_id: WorkerId,
|
||||
address_map: &'a AddressMap,
|
||||
transfer_txs: &'a [Sender<Envelope>],
|
||||
spawn_txs: &'a [Sender<(ActorAddress, Box<dyn AnyActor>)>],
|
||||
placement: &'a Placement,
|
||||
inbox_registry: &'a InboxRegistry,
|
||||
pending_local: &'a RefCell<Vec<(ActorAddress, Box<dyn Any + Send>)>>,
|
||||
}
|
||||
|
||||
impl ContextInner for WorkerContext<'_> {
|
||||
fn send_any(&self, addr: ActorAddress, msg: Box<dyn Any + Send>) -> Result<(), Error> {
|
||||
match self.address_map.lookup(&addr) {
|
||||
Some(wid) if wid == self.worker_id => {
|
||||
// Same worker: buffer for local delivery (after current tick round)
|
||||
self.pending_local.borrow_mut().push((addr, msg));
|
||||
Ok(())
|
||||
}
|
||||
Some(wid) => {
|
||||
// Cross worker: envelope through transfer queue
|
||||
let envelope = Envelope::new(addr, msg);
|
||||
let _ = self.transfer_txs[wid.as_usize()].try_send(envelope);
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
// Try inbox registry (external inboxes)
|
||||
self.inbox_registry.try_deliver(addr, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_any(&self, addr: ActorAddress, actor: Box<dyn AnyActor>) -> Result<(), Error> {
|
||||
let worker_id = self.placement.next_worker();
|
||||
self.address_map.insert(addr, worker_id);
|
||||
self.spawn_txs[worker_id.as_usize()]
|
||||
.try_send((addr, actor))
|
||||
.map_err(|_| Error::from("Spawn queue full"))
|
||||
}
|
||||
}
|
||||
51
src/worker/pool.rs
Normal file
51
src/worker/pool.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::actor::{ActorAddress, AnyActor};
|
||||
use crate::context::ContextInner;
|
||||
|
||||
/// Per-worker actor storage.
|
||||
pub(crate) struct ActorPool {
|
||||
actors: HashMap<ActorAddress, Box<dyn AnyActor>>,
|
||||
}
|
||||
|
||||
impl ActorPool {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
actors: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, addr: ActorAddress, actor: Box<dyn AnyActor>) {
|
||||
self.actors.insert(addr, actor);
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, addr: &ActorAddress) -> Option<Box<dyn AnyActor>> {
|
||||
self.actors.remove(addr)
|
||||
}
|
||||
|
||||
/// Deliver a type-erased message to the actor at `addr`.
|
||||
/// Returns `true` if the actor was found and the message type matched.
|
||||
pub fn deliver(&mut self, addr: &ActorAddress, msg: Box<dyn Any + Send>) -> bool {
|
||||
if let Some(actor) = self.actors.get_mut(addr) {
|
||||
actor.deliver(msg)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Tick all actors in the pool. Returns `true` if any actor processed messages.
|
||||
pub fn tick_all(&mut self, inner: &dyn ContextInner) -> bool {
|
||||
let mut did_work = false;
|
||||
for actor in self.actors.values_mut() {
|
||||
if actor.tick(inner) {
|
||||
did_work = true;
|
||||
}
|
||||
}
|
||||
did_work
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.actors.len()
|
||||
}
|
||||
}
|
||||
226
tests/mailbox_tests.rs
Normal file
226
tests/mailbox_tests.rs
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
use swactor::worker::mailbox::Mailbox;
|
||||
|
||||
// ── Basic operations ──
|
||||
|
||||
#[test]
|
||||
fn push_and_pop() {
|
||||
let mut mb = Mailbox::new();
|
||||
mb.push(42i32);
|
||||
assert_eq!(mb.pop(), Some(42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fifo_ordering() {
|
||||
let mut mb = Mailbox::new();
|
||||
mb.push(1);
|
||||
mb.push(2);
|
||||
mb.push(3);
|
||||
assert_eq!(mb.pop(), Some(1));
|
||||
assert_eq!(mb.pop(), Some(2));
|
||||
assert_eq!(mb.pop(), Some(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pop_empty() {
|
||||
let mut mb: Mailbox<i32> = Mailbox::new();
|
||||
assert_eq!(mb.pop(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_messages() {
|
||||
let mut mb = Mailbox::new();
|
||||
for i in 0..100 {
|
||||
mb.push(i);
|
||||
}
|
||||
for i in 0..100 {
|
||||
assert_eq!(mb.pop(), Some(i));
|
||||
}
|
||||
assert_eq!(mb.pop(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interleaved_push_pop() {
|
||||
let mut mb = Mailbox::new();
|
||||
mb.push(1);
|
||||
mb.push(2);
|
||||
assert_eq!(mb.pop(), Some(1));
|
||||
mb.push(3);
|
||||
assert_eq!(mb.pop(), Some(2));
|
||||
assert_eq!(mb.pop(), Some(3));
|
||||
assert_eq!(mb.pop(), None);
|
||||
}
|
||||
|
||||
// ── Drain count / watermark logic ──
|
||||
|
||||
#[test]
|
||||
fn drain_count_empty() {
|
||||
let mb: Mailbox<i32> = Mailbox::new();
|
||||
assert_eq!(mb.drain_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drain_count_below_waterlevel() {
|
||||
let mut mb = Mailbox::new();
|
||||
for i in 0..5 {
|
||||
mb.push(i);
|
||||
}
|
||||
// 5 < 10 (default waterlevel) → process all
|
||||
assert_eq!(mb.drain_count(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drain_count_at_waterlevel() {
|
||||
let mut mb = Mailbox::new();
|
||||
for i in 0..10 {
|
||||
mb.push(i);
|
||||
}
|
||||
// 10 >= 10 → process half → 5
|
||||
assert_eq!(mb.drain_count(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drain_count_above_waterlevel() {
|
||||
let mut mb = Mailbox::new();
|
||||
for i in 0..20 {
|
||||
mb.push(i);
|
||||
}
|
||||
// 20 >= 10 → 20 >> 1 = 10
|
||||
assert_eq!(mb.drain_count(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drain_count_one_message() {
|
||||
let mut mb = Mailbox::new();
|
||||
mb.push(1i32);
|
||||
// 1 < 10 → process all → 1
|
||||
assert_eq!(mb.drain_count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drain_count_just_below_waterlevel() {
|
||||
let mut mb = Mailbox::new();
|
||||
for i in 0..9 {
|
||||
mb.push(i);
|
||||
}
|
||||
// 9 < 10 → process all → 9
|
||||
assert_eq!(mb.drain_count(), 9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drain_count_large() {
|
||||
let mut mb = Mailbox::new();
|
||||
for i in 0..1000 {
|
||||
mb.push(i);
|
||||
}
|
||||
// 1000 >= 10 → 1000 >> 1 = 500
|
||||
assert_eq!(mb.drain_count(), 500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drain_count_custom_waterlevel() {
|
||||
let mut mb = Mailbox::with_waterlevel(4);
|
||||
for i in 0..3 {
|
||||
mb.push(i);
|
||||
}
|
||||
// 3 < 4 → process all → 3
|
||||
assert_eq!(mb.drain_count(), 3);
|
||||
|
||||
mb.push(99);
|
||||
// 4 >= 4 → 4 >> 1 = 2
|
||||
assert_eq!(mb.drain_count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drain_count_updates_after_pop() {
|
||||
let mut mb = Mailbox::new();
|
||||
for i in 0..20 {
|
||||
mb.push(i);
|
||||
}
|
||||
// 20 >= 10 → 10
|
||||
assert_eq!(mb.drain_count(), 10);
|
||||
|
||||
// pop 15, leaving 5
|
||||
for _ in 0..15 {
|
||||
mb.pop();
|
||||
}
|
||||
// 5 < 10 → process all → 5
|
||||
assert_eq!(mb.drain_count(), 5);
|
||||
}
|
||||
|
||||
// ── Properties ──
|
||||
|
||||
#[test]
|
||||
fn len_tracks_pushes() {
|
||||
let mut mb = Mailbox::new();
|
||||
assert_eq!(mb.len(), 0);
|
||||
mb.push(1);
|
||||
assert_eq!(mb.len(), 1);
|
||||
mb.push(2);
|
||||
assert_eq!(mb.len(), 2);
|
||||
mb.push(3);
|
||||
assert_eq!(mb.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn len_tracks_pops() {
|
||||
let mut mb = Mailbox::new();
|
||||
mb.push(1);
|
||||
mb.push(2);
|
||||
mb.push(3);
|
||||
assert_eq!(mb.len(), 3);
|
||||
mb.pop();
|
||||
assert_eq!(mb.len(), 2);
|
||||
mb.pop();
|
||||
assert_eq!(mb.len(), 1);
|
||||
mb.pop();
|
||||
assert_eq!(mb.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_empty_on_new() {
|
||||
let mb: Mailbox<i32> = Mailbox::new();
|
||||
assert!(mb.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_empty_after_drain() {
|
||||
let mut mb = Mailbox::new();
|
||||
mb.push(1);
|
||||
mb.push(2);
|
||||
mb.push(3);
|
||||
assert!(!mb.is_empty());
|
||||
mb.pop();
|
||||
mb.pop();
|
||||
mb.pop();
|
||||
assert!(mb.is_empty());
|
||||
}
|
||||
|
||||
// ── Type tests ──
|
||||
|
||||
#[test]
|
||||
fn works_with_primitive_types() {
|
||||
let mut mb_i32 = Mailbox::new();
|
||||
mb_i32.push(42i32);
|
||||
assert_eq!(mb_i32.pop(), Some(42));
|
||||
|
||||
let mut mb_string = Mailbox::new();
|
||||
mb_string.push(String::from("hello"));
|
||||
assert_eq!(mb_string.pop(), Some(String::from("hello")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn works_with_custom_structs() {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct MyMsg {
|
||||
id: u64,
|
||||
payload: String,
|
||||
}
|
||||
|
||||
let mut mb = Mailbox::new();
|
||||
let msg = MyMsg {
|
||||
id: 1,
|
||||
payload: "test".into(),
|
||||
};
|
||||
mb.push(msg.clone());
|
||||
assert_eq!(mb.pop(), Some(msg));
|
||||
}
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
use swactor::{
|
||||
actor::{ActorAddress, ActorInterface},
|
||||
runtime::{Ctx, Inbox, Runtime, RuntimeConfig},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared test fixtures
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone)]
|
||||
struct EchoMessage {
|
||||
payload: usize,
|
||||
reply_to: ActorAddress,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct EchoResponse(usize);
|
||||
|
||||
struct EchoActor;
|
||||
|
||||
impl ActorInterface for EchoActor {
|
||||
type Incoming = EchoMessage;
|
||||
type Response = EchoResponse;
|
||||
|
||||
fn handle(&mut self, ctx: &Ctx, msg: EchoMessage) {
|
||||
let _ = ctx.send(msg.reply_to, EchoResponse(msg.payload));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn test_single_thread_spawn_actor_and_inbox() {
|
||||
let rt = Runtime::new(RuntimeConfig::default());
|
||||
|
||||
let actor_addr = rt.spawn(EchoActor).expect("spawn echo actor");
|
||||
let inbox: Inbox<EchoResponse> = rt.new_inbox().unwrap();
|
||||
|
||||
rt.send_to(
|
||||
actor_addr,
|
||||
EchoMessage {
|
||||
payload: 42,
|
||||
reply_to: *inbox.addr(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for _ in 0..10 {
|
||||
rt.tick();
|
||||
if let Some(response) = inbox.try_recv() {
|
||||
assert_eq!(response, EchoResponse(42));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
panic!("Did not receive EchoResponse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multi_thread_spawn_actor_and_inbox() {
|
||||
let config = RuntimeConfig {
|
||||
num_threads: 4,
|
||||
..Default::default()
|
||||
};
|
||||
let rt = Runtime::new(config);
|
||||
|
||||
let actor_addr = rt.spawn(EchoActor).expect("spawn echo actor");
|
||||
let inbox: Inbox<EchoResponse> = rt.new_inbox().unwrap();
|
||||
|
||||
rt.send_to(
|
||||
actor_addr,
|
||||
EchoMessage {
|
||||
payload: 99,
|
||||
reply_to: *inbox.addr(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let handle = rt.run().unwrap();
|
||||
|
||||
let check = std::thread::spawn(move || {
|
||||
for _ in 0..100 {
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
if let Some(response) = inbox.try_recv() {
|
||||
handle.shutdown();
|
||||
return Some(response);
|
||||
}
|
||||
}
|
||||
handle.shutdown();
|
||||
None
|
||||
});
|
||||
|
||||
let result = check.join().unwrap();
|
||||
assert_eq!(result, Some(EchoResponse(99)));
|
||||
}
|
||||
130
tests/runtime_tests.rs
Normal file
130
tests/runtime_tests.rs
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
use swactor::{Ctx, actor::{ActorAddress, ActorInterface}, runtime::{Inbox, Runtime, RuntimeConfig}};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct PingMessage {
|
||||
reply_to: ActorAddress,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct PongMessage;
|
||||
|
||||
struct PongActor;
|
||||
|
||||
impl ActorInterface for PongActor {
|
||||
type Incoming = PingMessage;
|
||||
type Response = PongMessage;
|
||||
|
||||
fn handle(&mut self, ctx: &Ctx, msg: PingMessage) {
|
||||
let _ = ctx.send(msg.reply_to, PongMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/// An actor that forwards messages to another address
|
||||
struct ForwarderActor {
|
||||
target: ActorAddress,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ForwardMessage(usize);
|
||||
|
||||
impl ActorInterface for ForwarderActor {
|
||||
type Incoming = ForwardMessage;
|
||||
type Response = ();
|
||||
|
||||
fn handle(&mut self, ctx: &Ctx, msg: ForwardMessage) {
|
||||
let _ = ctx.send(self.target, msg);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_threaded_ping_pong() {
|
||||
let rt = Runtime::new(RuntimeConfig::default());
|
||||
let inbox: Inbox<PongMessage> = rt.new_inbox().unwrap();
|
||||
|
||||
let pong_addr = rt.spawn(PongActor).expect("spawn pong");
|
||||
|
||||
// Send ping
|
||||
rt.send_to(
|
||||
pong_addr,
|
||||
PingMessage {
|
||||
reply_to: *inbox.addr(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Tick until we get a response
|
||||
for _ in 0..10 {
|
||||
rt.tick();
|
||||
if inbox.try_recv().is_some() {
|
||||
return; // Success!
|
||||
}
|
||||
}
|
||||
|
||||
panic!("Did not receive pong response");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_threaded_message_chain() {
|
||||
let rt = Runtime::new(RuntimeConfig::default());
|
||||
let inbox: Inbox<ForwardMessage> = rt.new_inbox().unwrap();
|
||||
|
||||
// Create a chain: A -> B -> C -> inbox
|
||||
let c_addr = rt
|
||||
.spawn(ForwarderActor {
|
||||
target: *inbox.addr(),
|
||||
})
|
||||
.unwrap();
|
||||
let b_addr = rt.spawn(ForwarderActor { target: c_addr }).unwrap();
|
||||
let a_addr = rt.spawn(ForwarderActor { target: b_addr }).unwrap();
|
||||
|
||||
// Send message to start of chain
|
||||
rt.send_to(a_addr, ForwardMessage(42)).unwrap();
|
||||
|
||||
// Tick until message arrives
|
||||
for _ in 0..20 {
|
||||
rt.tick();
|
||||
if let Some(ForwardMessage(val)) = inbox.try_recv() {
|
||||
assert_eq!(val, 42);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
panic!("Message did not traverse the chain");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multithreaded_message_passing() {
|
||||
let config = RuntimeConfig {
|
||||
num_threads: 4,
|
||||
..Default::default()
|
||||
};
|
||||
let rt = Runtime::new(config);
|
||||
let inbox: Inbox<ForwardMessage> = rt.new_inbox().unwrap();
|
||||
|
||||
// Create a longer chain to exercise multi-threading
|
||||
let mut target = *inbox.addr();
|
||||
for _ in 0..20 {
|
||||
target = rt.spawn(ForwarderActor { target }).unwrap();
|
||||
}
|
||||
let start_addr = target;
|
||||
|
||||
// Send message
|
||||
rt.send_to(start_addr, ForwardMessage(999)).unwrap();
|
||||
|
||||
// Spawn thread to check for result and shutdown
|
||||
let ctx = rt.run().unwrap();
|
||||
let inbox_check = std::thread::spawn(move || {
|
||||
for _ in 0..100 {
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
if let Some(ForwardMessage(val)) = inbox.try_recv() {
|
||||
ctx.shutdown();
|
||||
return Some(val);
|
||||
}
|
||||
}
|
||||
ctx.shutdown();
|
||||
None
|
||||
});
|
||||
|
||||
let result = inbox_check.join().unwrap();
|
||||
assert_eq!(result, Some(999));
|
||||
}
|
||||
327
tests/stress/concurrency.rs
Normal file
327
tests/stress/concurrency.rs
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
//! Concurrency stress tests - hunt for race conditions.
|
||||
//!
|
||||
//! These tests target the shutdown races and concurrent access patterns
|
||||
//! that are most likely to expose bugs.
|
||||
|
||||
use super::{BlackHole, Msg};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use swactor::runtime::{Runtime, RuntimeConfig};
|
||||
|
||||
/// Shutdown while messages are in flight.
|
||||
/// Target: AtomicBool ordering bugs, use-after-shutdown.
|
||||
#[test]
|
||||
#[cfg(feature = "stress")]
|
||||
fn shutdown_under_load() {
|
||||
println!("\n>>> STRESS: Shutdown Under Load");
|
||||
|
||||
let mut panics = 0;
|
||||
let mut successes = 0;
|
||||
|
||||
// Run many iterations to catch rare races
|
||||
for iteration in 0..100 {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
let config = RuntimeConfig {
|
||||
max_actors: 100,
|
||||
actor_max_messages: 1000,
|
||||
num_threads: 4,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
|
||||
// Spawn actors
|
||||
let mut actors = Vec::new();
|
||||
for _ in 0..50 {
|
||||
if let Ok(addr) = runtime.spawn(BlackHole) {
|
||||
actors.push(addr);
|
||||
}
|
||||
}
|
||||
|
||||
let handle = runtime.run().unwrap();
|
||||
let rt = handle.runtime.clone();
|
||||
|
||||
// Sender thread - blast messages
|
||||
let actors_clone = actors.clone();
|
||||
let rt_send = rt.clone();
|
||||
let sender = thread::spawn(move || {
|
||||
for _ in 0..1000 {
|
||||
for actor in &actors_clone {
|
||||
let _ = rt_send.send_to::<Msg>(*actor, Msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Random delay before shutdown
|
||||
let delay = Duration::from_micros((iteration * 17) % 500);
|
||||
thread::sleep(delay);
|
||||
|
||||
// Shutdown while sender is still going
|
||||
handle.shutdown();
|
||||
|
||||
// Wait for sender (it should not panic)
|
||||
let _ = sender.join();
|
||||
|
||||
// Join should complete (not hang)
|
||||
handle.join();
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(_) => successes += 1,
|
||||
Err(_) => panics += 1,
|
||||
}
|
||||
}
|
||||
|
||||
println!(" Iterations: 100");
|
||||
println!(" Successes: {}", successes);
|
||||
println!(" Panics: {}", panics);
|
||||
|
||||
if panics > 0 {
|
||||
println!(">>> FAIL: {} panics detected during shutdown\n", panics);
|
||||
} else {
|
||||
println!(">>> PASS: No panics during shutdown under load\n");
|
||||
}
|
||||
|
||||
assert_eq!(panics, 0, "Shutdown under load caused panics");
|
||||
}
|
||||
|
||||
/// Send to actor immediately after spawn.
|
||||
/// Target: Race between spawn registration and first message.
|
||||
#[test]
|
||||
#[cfg(feature = "stress")]
|
||||
fn send_to_newborn() {
|
||||
println!("\n>>> STRESS: Send to Newborn Actor");
|
||||
|
||||
let mut total_spawned = 0;
|
||||
let mut total_send_ok = 0;
|
||||
let mut total_send_fail = 0;
|
||||
|
||||
for _ in 0..100 {
|
||||
let config = RuntimeConfig {
|
||||
max_actors: 1000,
|
||||
actor_max_messages: 100,
|
||||
num_threads: 4,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
let handle = runtime.run().unwrap();
|
||||
|
||||
// Immediately spawn and send
|
||||
for _ in 0..50 {
|
||||
if let Ok(addr) = handle.runtime.spawn(BlackHole) {
|
||||
total_spawned += 1;
|
||||
// Send immediately - actor may not be registered yet
|
||||
if handle.runtime.send_to::<Msg>(addr, Msg).is_ok() {
|
||||
total_send_ok += 1;
|
||||
} else {
|
||||
total_send_fail += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handle.shutdown();
|
||||
handle.join();
|
||||
}
|
||||
|
||||
println!(" Total spawned: {}", total_spawned);
|
||||
println!(" Sends succeeded: {}", total_send_ok);
|
||||
println!(" Sends failed: {}", total_send_fail);
|
||||
|
||||
if total_send_fail > 0 {
|
||||
println!(">>> FAIL: {} messages failed to send\n", total_send_fail);
|
||||
} else {
|
||||
println!(">>> PASS: All messages succeeded\n");
|
||||
}
|
||||
|
||||
assert_eq!(total_send_fail, 0, "Race condition caused failed message delivery");
|
||||
|
||||
println!(">>> Test complete\n");
|
||||
}
|
||||
|
||||
/// FIXME: This test means nothing until we allow killing off actor processes
|
||||
/// Rapid spawn/despawn cycles.
|
||||
/// Target: Queue management under churn.
|
||||
#[test]
|
||||
#[cfg(feature = "stress")]
|
||||
fn rapid_spawn_churn() {
|
||||
println!("\n>>> STRESS: Rapid Spawn Churn");
|
||||
|
||||
let config = RuntimeConfig {
|
||||
max_actors: 100,
|
||||
actor_max_messages: 100,
|
||||
num_threads: 4,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
let handle = runtime.run().unwrap();
|
||||
|
||||
let spawn_count = Arc::new(AtomicUsize::new(0));
|
||||
let fail_count = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Multiple threads spawning actors
|
||||
let mut threads = Vec::new();
|
||||
for _ in 0..4 {
|
||||
let rt = handle.runtime.clone();
|
||||
let spawns = spawn_count.clone();
|
||||
let fails = fail_count.clone();
|
||||
|
||||
threads.push(thread::spawn(move || {
|
||||
for _ in 0..500 {
|
||||
match rt.spawn(BlackHole) {
|
||||
Ok(_) => {
|
||||
spawns.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
Err(_) => {
|
||||
fails.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
// Small yield to increase interleaving
|
||||
thread::yield_now();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Let it churn
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
|
||||
handle.shutdown();
|
||||
|
||||
for t in threads {
|
||||
let _ = t.join();
|
||||
}
|
||||
handle.join();
|
||||
|
||||
let total_spawns = spawn_count.load(Ordering::Relaxed);
|
||||
let total_fails = fail_count.load(Ordering::Relaxed);
|
||||
|
||||
println!(" Spawn attempts: {}", total_spawns + total_fails);
|
||||
println!(" Successes: {}", total_spawns);
|
||||
println!(" Failures: {} (expected - queue fills)", total_fails);
|
||||
println!(">>> Test complete - no panics\n");
|
||||
}
|
||||
|
||||
/// Multiple threads sending to same actor.
|
||||
/// Target: Inbox contention, message ordering.
|
||||
#[test]
|
||||
#[cfg(feature = "stress")]
|
||||
fn inbox_contention() {
|
||||
println!("\n>>> STRESS: Inbox Contention");
|
||||
|
||||
let config = RuntimeConfig {
|
||||
max_actors: 10,
|
||||
actor_max_messages: 100_000,
|
||||
num_threads: 4,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
let target = runtime.spawn(BlackHole).unwrap();
|
||||
let handle = runtime.run().unwrap();
|
||||
|
||||
// Wait for registration
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
|
||||
let send_count = Arc::new(AtomicUsize::new(0));
|
||||
let fail_count = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// 8 threads all sending to same actor
|
||||
let mut threads = Vec::new();
|
||||
for _ in 0..8 {
|
||||
let rt = handle.runtime.clone();
|
||||
let sends = send_count.clone();
|
||||
let fails = fail_count.clone();
|
||||
|
||||
threads.push(thread::spawn(move || {
|
||||
for _ in 0..10_000 {
|
||||
if rt.send_to::<Msg>(target, Msg).is_ok() {
|
||||
sends.fetch_add(1, Ordering::Relaxed);
|
||||
} else {
|
||||
fails.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
for t in threads {
|
||||
let _ = t.join();
|
||||
}
|
||||
|
||||
// Let messages process
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
|
||||
handle.shutdown();
|
||||
handle.join();
|
||||
|
||||
let total_sends = send_count.load(Ordering::Relaxed);
|
||||
let total_fails = fail_count.load(Ordering::Relaxed);
|
||||
|
||||
println!(" Threads: 8");
|
||||
println!(" Msgs per thread: 10,000");
|
||||
println!(" Total sent: {}", total_sends);
|
||||
println!(" Total failed: {}", total_fails);
|
||||
println!(
|
||||
" Success rate: {:.1}%",
|
||||
(total_sends as f64 / (total_sends + total_fails) as f64) * 100.0
|
||||
);
|
||||
println!(">>> Test complete - no panics\n");
|
||||
}
|
||||
|
||||
/// FIXME: Not sure this test is meaningful.
|
||||
/// Shutdown timing fuzz - randomize when shutdown is called.
|
||||
/// Target: Edge cases in shutdown state machine.
|
||||
#[test]
|
||||
#[cfg(feature = "stress")]
|
||||
fn shutdown_timing_fuzz() {
|
||||
println!("\n>>> STRESS: Shutdown Timing Fuzz");
|
||||
|
||||
let mut results = Vec::new();
|
||||
|
||||
for delay_us in [0, 1, 10, 100, 1000, 5000] {
|
||||
let mut ok = 0;
|
||||
let mut fail = 0;
|
||||
|
||||
for _ in 0..20 {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
let config = RuntimeConfig {
|
||||
max_actors: 50,
|
||||
actor_max_messages: 100,
|
||||
num_threads: 4,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
|
||||
for _ in 0..20 {
|
||||
let _ = runtime.spawn(BlackHole);
|
||||
}
|
||||
|
||||
let handle = runtime.run().unwrap();
|
||||
|
||||
// Specific delay
|
||||
if delay_us > 0 {
|
||||
thread::sleep(Duration::from_micros(delay_us));
|
||||
}
|
||||
|
||||
handle.shutdown();
|
||||
handle.join();
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(_) => ok += 1,
|
||||
Err(_) => fail += 1,
|
||||
}
|
||||
}
|
||||
|
||||
results.push((delay_us, ok, fail));
|
||||
}
|
||||
|
||||
println!(" delay_us ok fail");
|
||||
println!(" -------- -- ----");
|
||||
for (delay, ok, fail) in &results {
|
||||
println!(" {:>8} {:>2} {:>4}", delay, ok, fail);
|
||||
}
|
||||
|
||||
let total_fails: i32 = results.iter().map(|(_, _, f)| *f).sum();
|
||||
if total_fails > 0 {
|
||||
println!(
|
||||
"\n>>> FAIL: {} panics across timing variations",
|
||||
total_fails
|
||||
);
|
||||
} else {
|
||||
println!("\n>>> PASS: All timing variations succeeded");
|
||||
}
|
||||
}
|
||||
199
tests/stress/mod.rs
Normal file
199
tests/stress/mod.rs
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
//! Stress test utilities and result reporting.
|
||||
//!
|
||||
//! Provides a simple framework for stress tests with JSON + pretty output.
|
||||
|
||||
#![allow(dead_code)] // Utilities may not all be used in every test
|
||||
|
||||
pub mod concurrency;
|
||||
pub mod saturation;
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Results from a stress test
|
||||
#[derive(Debug)]
|
||||
pub struct StressResult {
|
||||
pub name: String,
|
||||
pub duration: Duration,
|
||||
pub operations: u64,
|
||||
pub successes: u64,
|
||||
pub failures: u64,
|
||||
pub notes: Vec<String>,
|
||||
}
|
||||
|
||||
impl StressResult {
|
||||
pub fn new(name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
duration: Duration::ZERO,
|
||||
operations: 0,
|
||||
successes: 0,
|
||||
failures: 0,
|
||||
notes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn failure_rate(&self) -> f64 {
|
||||
if self.operations == 0 {
|
||||
0.0
|
||||
} else {
|
||||
(self.failures as f64 / self.operations as f64) * 100.0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn throughput(&self) -> f64 {
|
||||
let secs = self.duration.as_secs_f64();
|
||||
if secs > 0.0 {
|
||||
self.operations as f64 / secs
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn note(&mut self, msg: impl Into<String>) {
|
||||
self.notes.push(msg.into());
|
||||
}
|
||||
|
||||
pub fn print(&self) {
|
||||
println!("\n{}", "=".repeat(60));
|
||||
println!(" STRESS: {}", self.name);
|
||||
println!("{}", "=".repeat(60));
|
||||
println!(" Duration: {:?}", self.duration);
|
||||
println!(" Operations: {}", self.operations);
|
||||
println!(" Successes: {}", self.successes);
|
||||
println!(" Failures: {}", self.failures);
|
||||
println!(" Failure Rate: {:.2}%", self.failure_rate());
|
||||
println!(" Throughput: {:.2} ops/sec", self.throughput());
|
||||
|
||||
if !self.notes.is_empty() {
|
||||
println!();
|
||||
println!(" Notes:");
|
||||
for note in &self.notes {
|
||||
println!(" - {}", note);
|
||||
}
|
||||
}
|
||||
println!("{}", "=".repeat(60));
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> String {
|
||||
format!(
|
||||
r#"{{"name":"{}","duration_ms":{},"operations":{},"successes":{},"failures":{},"failure_rate_pct":{:.2},"throughput":{:.2},"notes":{:?}}}"#,
|
||||
self.name,
|
||||
self.duration.as_millis(),
|
||||
self.operations,
|
||||
self.successes,
|
||||
self.failures,
|
||||
self.failure_rate(),
|
||||
self.throughput(),
|
||||
self.notes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple stress test runner
|
||||
pub struct Stress {
|
||||
name: String,
|
||||
duration: Option<Duration>,
|
||||
iterations: Option<u64>,
|
||||
}
|
||||
|
||||
impl Stress {
|
||||
pub fn new(name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
duration: None,
|
||||
iterations: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run for a fixed duration
|
||||
pub fn for_duration(mut self, d: Duration) -> Self {
|
||||
self.duration = Some(d);
|
||||
self
|
||||
}
|
||||
|
||||
/// Run for a fixed number of iterations
|
||||
pub fn for_iterations(mut self, n: u64) -> Self {
|
||||
self.iterations = Some(n);
|
||||
self
|
||||
}
|
||||
|
||||
/// Run the stress test, counting successes and failures
|
||||
pub fn run<F>(self, mut f: F) -> StressResult
|
||||
where
|
||||
F: FnMut() -> bool, // returns true on success, false on failure
|
||||
{
|
||||
let mut result = StressResult::new(&self.name);
|
||||
let start = Instant::now();
|
||||
|
||||
match (self.duration, self.iterations) {
|
||||
(Some(duration), _) => {
|
||||
while start.elapsed() < duration {
|
||||
if f() {
|
||||
result.successes += 1;
|
||||
} else {
|
||||
result.failures += 1;
|
||||
}
|
||||
result.operations += 1;
|
||||
}
|
||||
}
|
||||
(None, Some(iterations)) => {
|
||||
for _ in 0..iterations {
|
||||
if f() {
|
||||
result.successes += 1;
|
||||
} else {
|
||||
result.failures += 1;
|
||||
}
|
||||
result.operations += 1;
|
||||
}
|
||||
}
|
||||
(None, None) => {
|
||||
// Default: 1000 iterations
|
||||
for _ in 0..1000 {
|
||||
if f() {
|
||||
result.successes += 1;
|
||||
} else {
|
||||
result.failures += 1;
|
||||
}
|
||||
result.operations += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.duration = start.elapsed();
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
// Test actors used across stress tests
|
||||
use swactor::{Ctx, actor::ActorInterface};
|
||||
|
||||
/// An actor that just absorbs messages
|
||||
pub struct BlackHole;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Msg;
|
||||
|
||||
impl ActorInterface for BlackHole {
|
||||
type Incoming = Msg;
|
||||
type Response = ();
|
||||
fn handle(&mut self, _ctx: &Ctx, _msg: Msg) {}
|
||||
}
|
||||
|
||||
/// An actor that counts messages received
|
||||
pub struct Counter {
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
impl Counter {
|
||||
pub fn new() -> Self {
|
||||
Self { count: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl ActorInterface for Counter {
|
||||
type Incoming = Msg;
|
||||
type Response = ();
|
||||
fn handle(&mut self, _ctx: &Ctx, _msg: Msg) {
|
||||
self.count += 1;
|
||||
}
|
||||
}
|
||||
172
tests/stress/saturation.rs
Normal file
172
tests/stress/saturation.rs
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
//! Saturation stress tests - find where the runtime breaks.
|
||||
//!
|
||||
//! These tests intentionally push past limits to document failure modes.
|
||||
|
||||
use super::{BlackHole, Counter, Msg, Stress, StressResult};
|
||||
use std::time::Duration;
|
||||
use swactor::runtime::{Runtime, RuntimeConfig};
|
||||
|
||||
/// Blast the transfer queue (replaces router_inbox_overflow)
|
||||
#[test]
|
||||
#[cfg(feature = "stress")]
|
||||
fn transfer_queue_overflow() {
|
||||
println!("\n>>> STRESS: Transfer Queue Overflow");
|
||||
|
||||
let config = RuntimeConfig {
|
||||
max_actors: 10,
|
||||
actor_max_messages: 100, // Tiny buffer
|
||||
num_threads: 1,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
let sink = runtime.spawn(BlackHole).unwrap();
|
||||
|
||||
// Process spawn
|
||||
runtime.tick();
|
||||
|
||||
// Blast messages without processing
|
||||
let mut result = StressResult::new("transfer_queue_overflow");
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
for _ in 0..10_000 {
|
||||
result.operations += 1;
|
||||
if runtime.send_to::<Msg>(sink, Msg).is_ok() {
|
||||
result.successes += 1;
|
||||
} else {
|
||||
result.failures += 1;
|
||||
}
|
||||
}
|
||||
|
||||
result.duration = start.elapsed();
|
||||
result.note(format!("Transfer buffer: 100, Messages sent: 10,000"));
|
||||
|
||||
// With hybrid channel, no failures expected
|
||||
assert_eq!(result.failures, 0, "Hybrid channel should not reject");
|
||||
result.print();
|
||||
println!(">>> PASS: Hybrid channel prevented transfer queue overflow\n");
|
||||
}
|
||||
|
||||
/// Blast a single actor's mailbox via transfer queue
|
||||
#[test]
|
||||
#[cfg(feature = "stress")]
|
||||
fn actor_inbox_overflow() {
|
||||
println!("\n>>> STRESS: Actor Inbox Overflow");
|
||||
|
||||
let config = RuntimeConfig {
|
||||
max_actors: 10,
|
||||
actor_max_messages: 100_000, // Large transfer buffer
|
||||
num_threads: 1,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
let sink = runtime.spawn(Counter::new()).unwrap();
|
||||
|
||||
// Process spawn
|
||||
runtime.tick();
|
||||
|
||||
// Now blast messages
|
||||
let mut sent = 0u64;
|
||||
let mut failed = 0u64;
|
||||
for _ in 0..10_000 {
|
||||
if runtime.send_to::<Msg>(sink, Msg).is_ok() {
|
||||
sent += 1;
|
||||
} else {
|
||||
failed += 1;
|
||||
}
|
||||
// Tick occasionally to let worker deliver
|
||||
if sent % 100 == 0 {
|
||||
runtime.tick();
|
||||
}
|
||||
}
|
||||
|
||||
// Process all remaining messages
|
||||
for _ in 0..5000 {
|
||||
runtime.tick();
|
||||
}
|
||||
|
||||
println!(" Sent: {}", sent);
|
||||
println!(" Failed: {}", failed);
|
||||
|
||||
assert_eq!(failed, 0, "Transfer queue rejected message under load");
|
||||
println!(">>> PASS: No message loss with hybrid channel\n");
|
||||
}
|
||||
|
||||
/// Blast the runtime with actor spawns
|
||||
#[test]
|
||||
#[cfg(feature = "stress")]
|
||||
fn actor_queue_overflow() {
|
||||
println!("\n>>> STRESS: Actor Queue Overflow");
|
||||
|
||||
let config = RuntimeConfig {
|
||||
max_actors: 100, // Small actor queue
|
||||
actor_max_messages: 100,
|
||||
num_threads: 1,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
|
||||
let mut result = StressResult::new("actor_queue_overflow");
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
// Try to spawn 500 actors into 100-slot queue
|
||||
for _ in 0..500 {
|
||||
result.operations += 1;
|
||||
match runtime.spawn(BlackHole) {
|
||||
Ok(_) => result.successes += 1,
|
||||
Err(_) => result.failures += 1,
|
||||
}
|
||||
}
|
||||
|
||||
result.duration = start.elapsed();
|
||||
result.note(format!("Queue capacity: 100, Spawn attempts: 500"));
|
||||
result.print();
|
||||
|
||||
// Note: With hybrid channel (overflow to SegQueue), we expect no failures
|
||||
assert_eq!(
|
||||
result.failures, 0,
|
||||
"Spawned more actors than queue capacity"
|
||||
);
|
||||
println!(">>> PASS: Actor queue correctly handles overflow\n");
|
||||
}
|
||||
|
||||
/// FIXME: IS this actually testing what it should be?
|
||||
/// Sustained overload - run at 2x capacity for extended period.
|
||||
/// Documents: Does the system degrade gracefully or crash?
|
||||
#[test]
|
||||
#[cfg(feature = "stress")]
|
||||
fn sustained_overload() {
|
||||
println!("\n>>> STRESS: Sustained Overload");
|
||||
|
||||
let config = RuntimeConfig {
|
||||
max_actors: 100,
|
||||
actor_max_messages: 1000,
|
||||
num_threads: 1,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
|
||||
// Spawn some actors
|
||||
let mut actors = Vec::new();
|
||||
for _ in 0..50 {
|
||||
if let Ok(addr) = runtime.spawn(Counter::new()) {
|
||||
actors.push(addr);
|
||||
}
|
||||
}
|
||||
|
||||
// Process spawn registrations
|
||||
for _ in 0..200 {
|
||||
runtime.tick();
|
||||
}
|
||||
|
||||
let result = Stress::new("sustained_overload")
|
||||
.for_duration(Duration::from_secs(2))
|
||||
.run(|| {
|
||||
// Send to random actor
|
||||
let idx = (std::time::Instant::now().elapsed().as_nanos() as usize) % actors.len();
|
||||
let success = runtime.send_to::<Msg>(actors[idx], Msg).is_ok();
|
||||
|
||||
// Process some (but not all) - simulating overload
|
||||
runtime.tick();
|
||||
|
||||
success
|
||||
});
|
||||
|
||||
result.print();
|
||||
println!(">>> System survived sustained overload without panic\n");
|
||||
}
|
||||
11
tests/stress_tests.rs
Normal file
11
tests/stress_tests.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
//! Stress test suite for swactor runtime.
|
||||
//!
|
||||
//! Run with: cargo test --features stress stress_ -- --nocapture
|
||||
//!
|
||||
//! These tests are hidden behind the `stress` feature flag because they:
|
||||
//! - Take longer to run
|
||||
//! - Intentionally push the system to failure
|
||||
//! - May produce different results on different machines
|
||||
|
||||
#[cfg(feature = "stress")]
|
||||
mod stress;
|
||||
Loading…
Reference in a new issue