Compare commits
6
Commits
v0.6.0
...
8f2d513940
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f2d513940 | ||
|
|
ca1c98336e | ||
|
|
95306c7f60 | ||
|
|
1262cc30e3 | ||
|
|
461fe4b768 | ||
|
|
b937f1f50f |
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "smarm"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
edition = "2021"
|
||||
rust-version = "1.95"
|
||||
|
||||
|
||||
@@ -1,35 +1,38 @@
|
||||
# smarm
|
||||
|
||||
> SMARM — Smarm, Marks Actor Runtime Machinery. A proof-of-concept green-thread actor runtime for Rust.
|
||||
> SMARM: Smarm, Marks Actor Runtime Machinery. A proof-of-concept green-thread actor runtime for Rust.
|
||||
|
||||
Implements the core ideas in [`Achitecture.md`](.docs/Architecture.md): green-thread actors on a
|
||||
shared heap, scheduled cooperatively, communicating only by `Send` messages.
|
||||
Erlang's isolation model without Erlang's copying GC, Rust's zero-copy
|
||||
ownership transfers without async's function colouring.
|
||||
SMARM is my attempt to implement the erlang/OTP philosophy in the Rust programming language. This has yielded a fault-tolerant, fast, and scalable runtime. This runtime allows the creation of asynchronous applications in Rust without the function coloring associated with the async/await system. It encourages the writing of simple, synchronous code, and largely elides the need for lifetime annotations.
|
||||
|
||||
The scheduler is multi-threaded — one OS thread per available CPU, all drawing
|
||||
from a shared run queue. The single-threaded `run()` entry point is kept as a
|
||||
convenience wrapper around `runtime::init(Config::exact(1)).run(f)`.
|
||||
|
||||
## What's here
|
||||
## Overview
|
||||
|
||||
SMARM implements green-thread actors on a shared heap, communicating only by `Send` messages. By sharing the heap, SMARM avoids the copying overhead of Erlang, which is safe to do due to Rust's borrow checker.
|
||||
|
||||
On top of the core runtime mechanics, SMARM also provides a library of primitives for making applications closely inspired by erlang/OTP. This includes generic servers (gen_servers), generic state machines (gen_statem), and supervision trees.
|
||||
|
||||
Supervision trees are the core primitive to allow your application to survive an unexpected panic. Supervisors are processes dedicated to monitoring other processes, which can restart these should they fail. This means that when set up properly an application may 'self-heal' when encountering unforeseen circumstances.
|
||||
|
||||
SMARM is not cooperatively scheduled; it uses preemption. This means a heavy task will not starve out other lighter tasks. Everything will make steady progress, which translates to very beneficial behaviour under (over)load: average latency goes up, but tail latency does not blow up.
|
||||
|
||||
To help diagnose these unforeseen circumstances, smarm may be compiled with its `tracing` feature, which emits a full trace using [Perfetto](https://perfetto.dev/).
|
||||
|
||||
Should you want to optimize your application, SMARM is unusually well poised to help. As the runtime functionally controls time, SMARM comes with a built in causal profiler, under the `causal` feature.
|
||||
|
||||
I also built a Phoenix-Framework inspired HTTP 1.1 library on top of SMARM called [URUS](https://git.kalsbeek.dev/Markk116/urus), which implements Pub/Sub, Channels, and basic amenities like Websockets and Server Sent Events.
|
||||
|
||||
## Limitations
|
||||
|
||||
This runtime requires naked assembly to function, and has thus far only been implemented for x86-64 assembly. It expects an operating system that supports virtual address space, and is therefore not (yet) suited for embedded targets. The IO implementation is currently based around the Linux kernel's `epoll` mechanism, meaning it requires a (GNU+)Linux distribution to run.
|
||||
|
||||
The preemption mechanism works by wrapping the memory allocator and checking how many CPU cycles you have used compared to your timeslice budget. This allows preemption to fire in most normal code, but tight zero-allocation loops do not get caught and require manual insertion of `check!()` if you want preemption to function.
|
||||
|
||||
This library is still in its early stages, and while I try my best with loom and tests, stable operation cannot be guaranteed. Therefore it is not (yet) recommended for production use.
|
||||
|
||||
At this moment, stack memory for each green thread is capped. Uncapping this may lead to performance benefits for deeply recursive algorithms that in a traditional async runtime might require pointer-chases through the heap. This is as yet unrealised.
|
||||
|
||||
At this stage, the codebase is largely LLM-generated, which is obvious if you start to read through the internals. While I did the design, and I keep the LLM under tight rein, the codebase is not in a state that I am very happy with. This also goes for the documentation.
|
||||
|
||||
| Module | What it does |
|
||||
|--------------|------------------------------------------------------------------------|
|
||||
| `stack` | `mmap`'d growable stack with guard page; SIGSEGV on overflow |
|
||||
| `context` | `#[naked]` x86-64 context-switch shims, callee-saved regs only |
|
||||
| `preempt` | Allocator-driven preemption; `check!()` macro for no-alloc loops |
|
||||
| `pid` | `(index, generation)` PIDs; stale handles are detectable, not silent |
|
||||
| `actor` | Trampoline + `catch_unwind` boundary at the actor entry point |
|
||||
| `scheduler` | Run queue, slot table, spawn/join, parking, idle path |
|
||||
| `channel` | Unbounded MPSC channel; `recv` parks the actor; `recv_timeout` bounds it; `select`/`select_timeout` park on many receivers at once (ready-index, priority order) |
|
||||
| `mutex` | `Mutex<T>` with mandatory timeout; FIFO waiters; parks the green thread |
|
||||
| `timer` | Min-heap of `(deadline, reason)`; `Sleep` and `WaitTimeout` reasons |
|
||||
| `io` | `block_on_io` for blocking work; `wait_readable`/`wait_writable` + `read`/`write` via epoll |
|
||||
| `supervisor` | `Signal::Exit`/`Panic`/`Stopped` funnelled to a parent; `OneForOne`/`OneForAll`/`RestForOne` strategies + restart-intensity cap |
|
||||
| `monitor` | `monitor(pid)` → `Monitor { id, target, rx }`; one-shot `Down` via `rx`; `demonitor(&m)` tears one registration down; unidirectional death notice |
|
||||
| `link` | bidirectional `link`/`unlink`; abnormal death propagates (cooperative stop, or an `ExitSignal` message under `trap_exit`) |
|
||||
| `gen_server` | `call`/`call_timeout` (sync request-reply) / `cast` (async) over one inbox; `handle_info` over static info arms + `handle_down` via `Watcher`-fed monitors, selected ahead of the inbox; `ServerRef`/`ServerBuilder` + `init`/`terminate` hooks; server-down via channel closure |
|
||||
| `registry` | `register`/`whereis`/`name_of`: name ↔ pid bimap; lazy generation-checked cleanup |
|
||||
|
||||
## Quick taste
|
||||
|
||||
@@ -67,12 +70,9 @@ benches/
|
||||
|
||||
## Building and running
|
||||
|
||||
Standard Cargo. Requires Rust 1.95 or newer (the `#[naked]` attribute went stable
|
||||
in 1.88; we use a few unrelated post-1.88 features). `master` is x86-64 Linux
|
||||
only. An experimental, **untested** aarch64 context-switch backend lives on the
|
||||
`arm-port` branch (extracted into a `target_arch`-gated `src/arch/`); it has not
|
||||
been validated on hardware yet. macOS remains on the deferred list because of the
|
||||
epoll dependency.
|
||||
Standard Cargo. Requires Rust 1.95 or newer (the `#[naked]` attribute went stable in 1.88; we use a few unrelated post-1.88 features). I have worked hard to keep this library as dependency-free as possible. `master` is x86-64 Linux only. An experimental, **untested** aarch64 context-switch backend lives on the `arm-port` branch (extracted into a `target_arch`-gated `src/arch/`); it has not been validated on hardware yet. macOS remains on the deferred list because of the epoll dependency.
|
||||
|
||||
|
||||
|
||||
```sh
|
||||
cargo test # all tests
|
||||
@@ -80,26 +80,29 @@ cargo test --test mutex # one module
|
||||
cargo bench # primes benchmark vs tokio
|
||||
```
|
||||
|
||||
## What's not here
|
||||
|
||||
See the **Defer** section of `Architecture.md`.
|
||||
`join!` for handle groups, stack growth via remap,
|
||||
hierarchical timer wheel, fd-wait timeouts, `Signal::Timeout`. Each is
|
||||
mechanism we know how to add; none belongs in this iteration.
|
||||
|
||||
## Docs
|
||||
|
||||
| Document | What it covers |
|
||||
|---|---|
|
||||
| [`Architecture.md`](./docs/Architecture.md) | Design intent, runtime model, and deferred work |
|
||||
| [`smarm - Deep Dive.html`](./docs/smarm%20-%20Deep%20Dive.html) | Generated walkthrough of the system; good starting point |
|
||||
| [`smarm - Deep Dive.html`](./docs/smarm%20-%20Deep%20Dive.html) | Generated walkthrough of the system; good starting point if you want to learn about the internals |
|
||||
| [`BENCHMARKS_AND_TUNING.md`](./docs/BENCHMARKS_AND_TUNING.md) | Where smarm wins and loses vs tokio, preemption knob recommendations |
|
||||
| [`benchmarks.md`](./docs/benchmarks.md) | Raw benchmark results, methodology, and tuning experiment log |
|
||||
|
||||
## Coming up
|
||||
|
||||
Clustering: clustering multiple SMARM nodes together is in the pipeline.
|
||||
SMARM-BEAM Interop: Running SMARM as a supervised node under the BEAM via a Rustler NIF works, including message passing and supervision trees that span the runtimes. However, this library is still too unstable to release.
|
||||
SMARM is an interesting platform for implementing a 'dataflow' library, but work on this has not yet started.
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
This is a personal proof-of-concept. There's no PR workflow. If you fork it and do something interesting, just send me an email. If it's nice, I'll upstream the changes.
|
||||
This started as a personal proof-of-concept, but it is starting to outgrow that name. If you want to contribute, please get in contact to discuss what you want to work on. Code without prior communication is not welcome.
|
||||
|
||||
|
||||
## A note on open source
|
||||
|
||||
An open source project is a gift, and by giving it, it is no longer mine. I highly enourage you to fork it, to make it your own. This repository, however, is still mine.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
+67
-26
@@ -26,7 +26,9 @@ use std::time::Instant;
|
||||
const ITERS: u32 = 15;
|
||||
|
||||
fn available_threads() -> usize {
|
||||
std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1)
|
||||
std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(1)
|
||||
}
|
||||
|
||||
fn env_sets() -> u32 {
|
||||
@@ -108,17 +110,15 @@ fn bench_chained_smarm(threads: usize) -> (u64, u128) {
|
||||
fn bench_chained_tokio_current() -> (u64, u128) {
|
||||
let counter = Arc::new(AtomicU64::new(0));
|
||||
let c2 = counter.clone();
|
||||
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.build()
|
||||
.unwrap();
|
||||
let start = Instant::now();
|
||||
let local = tokio::task::LocalSet::new();
|
||||
local.block_on(&rt, async move {
|
||||
// Use a oneshot done channel like tokio's own chained_spawn bench.
|
||||
let (done_tx, done_rx) = tokio::sync::oneshot::channel();
|
||||
fn iter(
|
||||
c: Arc<AtomicU64>,
|
||||
done: tokio::sync::oneshot::Sender<()>,
|
||||
n: u64,
|
||||
) {
|
||||
fn iter(c: Arc<AtomicU64>, done: tokio::sync::oneshot::Sender<()>, n: u64) {
|
||||
if n == 0 {
|
||||
let _ = done.send(());
|
||||
} else {
|
||||
@@ -186,7 +186,9 @@ fn bench_yield_smarm(threads: usize) -> (u64, u128) {
|
||||
}
|
||||
|
||||
fn bench_yield_tokio_current() -> (u64, u128) {
|
||||
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.build()
|
||||
.unwrap();
|
||||
let start = Instant::now();
|
||||
let local = tokio::task::LocalSet::new();
|
||||
local.block_on(&rt, async move {
|
||||
@@ -235,11 +237,22 @@ const PRIME_N: u64 = 400_000;
|
||||
const PRIME_WORKERS: u64 = 64;
|
||||
|
||||
fn is_prime(n: u64) -> bool {
|
||||
if n < 2 { return false; }
|
||||
if n < 4 { return true; }
|
||||
if n % 2 == 0 { return false; }
|
||||
if n < 2 {
|
||||
return false;
|
||||
}
|
||||
if n < 4 {
|
||||
return true;
|
||||
}
|
||||
if n % 2 == 0 {
|
||||
return false;
|
||||
}
|
||||
let mut i = 3u64;
|
||||
while i * i <= n { if n % i == 0 { return false; } i += 2; }
|
||||
while i * i <= n {
|
||||
if n % i == 0 {
|
||||
return false;
|
||||
}
|
||||
i += 2;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -250,7 +263,11 @@ fn count_primes(lo: u64, hi: u64) -> u64 {
|
||||
fn primes_slice(w: u64) -> (u64, u64) {
|
||||
let per = PRIME_N / PRIME_WORKERS;
|
||||
let lo = w * per;
|
||||
let hi = if w + 1 == PRIME_WORKERS { PRIME_N } else { lo + per };
|
||||
let hi = if w + 1 == PRIME_WORKERS {
|
||||
PRIME_N
|
||||
} else {
|
||||
lo + per
|
||||
};
|
||||
(lo, hi)
|
||||
}
|
||||
|
||||
@@ -267,7 +284,9 @@ fn bench_primes_smarm(threads: usize) -> (u64, u128) {
|
||||
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed);
|
||||
}));
|
||||
}
|
||||
for h in handles { h.join().unwrap(); }
|
||||
for h in handles {
|
||||
h.join().unwrap();
|
||||
}
|
||||
});
|
||||
(total.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||
}
|
||||
@@ -275,7 +294,9 @@ fn bench_primes_smarm(threads: usize) -> (u64, u128) {
|
||||
fn bench_primes_tokio_current() -> (u64, u128) {
|
||||
let total = Arc::new(AtomicU64::new(0));
|
||||
let t2 = total.clone();
|
||||
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.build()
|
||||
.unwrap();
|
||||
let start = Instant::now();
|
||||
let local = tokio::task::LocalSet::new();
|
||||
local.block_on(&rt, async move {
|
||||
@@ -287,7 +308,9 @@ fn bench_primes_tokio_current() -> (u64, u128) {
|
||||
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed);
|
||||
}));
|
||||
}
|
||||
for h in handles { let _ = h.await; }
|
||||
for h in handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
});
|
||||
(total.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||
}
|
||||
@@ -309,7 +332,9 @@ fn bench_primes_tokio_multi() -> (u64, u128) {
|
||||
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed);
|
||||
}));
|
||||
}
|
||||
for h in handles { let _ = h.await; }
|
||||
for h in handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
});
|
||||
(total.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||
}
|
||||
@@ -344,7 +369,9 @@ fn bench_pp_smarm(threads: usize) -> (u64, u128) {
|
||||
}
|
||||
|
||||
fn bench_pp_tokio_current() -> (u64, u128) {
|
||||
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.build()
|
||||
.unwrap();
|
||||
let start = Instant::now();
|
||||
let local = tokio::task::LocalSet::new();
|
||||
local.block_on(&rt, async move {
|
||||
@@ -395,7 +422,6 @@ fn bench_pp_tokio_multi() -> (u64, u128) {
|
||||
// main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Knob helper — reads SMARM_ALLOC_INTERVAL / SMARM_TIMESLICE_CYCLES env vars
|
||||
// so the sweep script can override the preemption knobs without recompiling.
|
||||
@@ -404,10 +430,14 @@ fn bench_pp_tokio_multi() -> (u64, u128) {
|
||||
fn bench_cfg(threads: usize) -> smarm::runtime::Config {
|
||||
let mut cfg = smarm::runtime::Config::exact(threads);
|
||||
if let Ok(v) = std::env::var("SMARM_ALLOC_INTERVAL") {
|
||||
if let Ok(n) = v.parse::<u32>() { cfg = cfg.alloc_interval(n); }
|
||||
if let Ok(n) = v.parse::<u32>() {
|
||||
cfg = cfg.alloc_interval(n);
|
||||
}
|
||||
}
|
||||
if let Ok(v) = std::env::var("SMARM_TIMESLICE_CYCLES") {
|
||||
if let Ok(n) = v.parse::<u64>() { cfg = cfg.timeslice_cycles(n); }
|
||||
if let Ok(n) = v.parse::<u64>() {
|
||||
cfg = cfg.timeslice_cycles(n);
|
||||
}
|
||||
}
|
||||
cfg
|
||||
}
|
||||
@@ -417,7 +447,10 @@ fn main() {
|
||||
println!("smarm general benchmarks");
|
||||
println!("available parallelism: {n} threads");
|
||||
let sets = env_sets();
|
||||
println!("ITERS={ITERS}×{sets} sets = {} samples (+1 warmup, discarded)", ITERS * sets);
|
||||
println!(
|
||||
"ITERS={ITERS}×{sets} sets = {} samples (+1 warmup, discarded)",
|
||||
ITERS * sets
|
||||
);
|
||||
println!(
|
||||
"CHAIN_DEPTH={CHAIN_DEPTH}, YIELD_TASKS={YIELD_TASKS}×{YIELD_ROUNDS}, \
|
||||
PRIME_N={PRIME_N}/{PRIME_WORKERS} workers, PP_ROUNDS={PP_ROUNDS}"
|
||||
@@ -426,21 +459,29 @@ fn main() {
|
||||
// ---- 1. chained_spawn ----
|
||||
print_header(&format!("chained_spawn: depth {CHAIN_DEPTH}"));
|
||||
run_n("smarm 1-thread", ITERS, || bench_chained_smarm(1));
|
||||
run_n(&format!("smarm {n}-thread"), ITERS, || bench_chained_smarm(n));
|
||||
run_n(&format!("smarm {n}-thread"), ITERS, || {
|
||||
bench_chained_smarm(n)
|
||||
});
|
||||
run_n("tokio current_thread", ITERS, bench_chained_tokio_current);
|
||||
run_n("tokio multi-thread", ITERS, bench_chained_tokio_multi);
|
||||
|
||||
// ---- 2. yield_many ----
|
||||
print_header(&format!("yield_many: {YIELD_TASKS} tasks × {YIELD_ROUNDS} yields"));
|
||||
print_header(&format!(
|
||||
"yield_many: {YIELD_TASKS} tasks × {YIELD_ROUNDS} yields"
|
||||
));
|
||||
run_n("smarm 1-thread", ITERS, || bench_yield_smarm(1));
|
||||
run_n(&format!("smarm {n}-thread"), ITERS, || bench_yield_smarm(n));
|
||||
run_n("tokio current_thread", ITERS, bench_yield_tokio_current);
|
||||
run_n("tokio multi-thread", ITERS, bench_yield_tokio_multi);
|
||||
|
||||
// ---- 3. fan_out_compute ----
|
||||
print_header(&format!("fan_out_compute: primes in [2, {PRIME_N}) across {PRIME_WORKERS}"));
|
||||
print_header(&format!(
|
||||
"fan_out_compute: primes in [2, {PRIME_N}) across {PRIME_WORKERS}"
|
||||
));
|
||||
run_n("smarm 1-thread", ITERS, || bench_primes_smarm(1));
|
||||
run_n(&format!("smarm {n}-thread"), ITERS, || bench_primes_smarm(n));
|
||||
run_n(&format!("smarm {n}-thread"), ITERS, || {
|
||||
bench_primes_smarm(n)
|
||||
});
|
||||
run_n("tokio current_thread", ITERS, bench_primes_tokio_current);
|
||||
run_n("tokio multi-thread", ITERS, bench_primes_tokio_multi);
|
||||
|
||||
|
||||
+90
-41
@@ -64,11 +64,22 @@ const PRIME_N: u64 = 400_000;
|
||||
const WORKERS: u64 = 64;
|
||||
|
||||
fn is_prime(n: u64) -> bool {
|
||||
if n < 2 { return false; }
|
||||
if n < 4 { return true; }
|
||||
if n % 2 == 0 { return false; }
|
||||
if n < 2 {
|
||||
return false;
|
||||
}
|
||||
if n < 4 {
|
||||
return true;
|
||||
}
|
||||
if n % 2 == 0 {
|
||||
return false;
|
||||
}
|
||||
let mut i = 3u64;
|
||||
while i * i <= n { if n % i == 0 { return false; } i += 2; }
|
||||
while i * i <= n {
|
||||
if n % i == 0 {
|
||||
return false;
|
||||
}
|
||||
i += 2;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -96,7 +107,9 @@ fn bench_primes_smarm(threads: usize) -> (u64, u128) {
|
||||
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed);
|
||||
}));
|
||||
}
|
||||
for h in handles { h.join().unwrap(); }
|
||||
for h in handles {
|
||||
h.join().unwrap();
|
||||
}
|
||||
});
|
||||
(total.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||
}
|
||||
@@ -104,7 +117,9 @@ fn bench_primes_smarm(threads: usize) -> (u64, u128) {
|
||||
fn bench_primes_tokio_current() -> (u64, u128) {
|
||||
let total = Arc::new(AtomicU64::new(0));
|
||||
let t2 = total.clone();
|
||||
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.build()
|
||||
.unwrap();
|
||||
let start = Instant::now();
|
||||
let local = tokio::task::LocalSet::new();
|
||||
local.block_on(&rt, async move {
|
||||
@@ -116,7 +131,9 @@ fn bench_primes_tokio_current() -> (u64, u128) {
|
||||
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed);
|
||||
}));
|
||||
}
|
||||
for h in handles { let _ = h.await; }
|
||||
for h in handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
});
|
||||
(total.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||
}
|
||||
@@ -138,17 +155,21 @@ fn bench_primes_tokio_multi() -> (u64, u128) {
|
||||
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed);
|
||||
}));
|
||||
}
|
||||
for h in handles { let _ = h.await; }
|
||||
for h in handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
});
|
||||
(total.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||
}
|
||||
|
||||
fn bench_primes_baseline() -> (u64, u128) {
|
||||
let start = Instant::now();
|
||||
let total: u64 = (0..WORKERS).map(|w| {
|
||||
let (lo, hi) = primes_slice(w);
|
||||
count_primes(lo, hi)
|
||||
}).sum();
|
||||
let total: u64 = (0..WORKERS)
|
||||
.map(|w| {
|
||||
let (lo, hi) = primes_slice(w);
|
||||
count_primes(lo, hi)
|
||||
})
|
||||
.sum();
|
||||
(total, start.elapsed().as_micros())
|
||||
}
|
||||
|
||||
@@ -167,15 +188,17 @@ fn bench_pingpong_smarm(threads: usize) -> (u64, u128) {
|
||||
tx_a.send(0).unwrap();
|
||||
loop {
|
||||
let v = rx_b.recv().unwrap();
|
||||
if v >= PING_ROUNDS { break; }
|
||||
if v >= PING_ROUNDS {
|
||||
break;
|
||||
}
|
||||
tx_a.send(v + 1).unwrap();
|
||||
}
|
||||
});
|
||||
let hb = smarm::spawn(move || {
|
||||
loop {
|
||||
let v = rx_a.recv().unwrap();
|
||||
tx_b.send(v + 1).unwrap();
|
||||
if v + 1 >= PING_ROUNDS { break; }
|
||||
let hb = smarm::spawn(move || loop {
|
||||
let v = rx_a.recv().unwrap();
|
||||
tx_b.send(v + 1).unwrap();
|
||||
if v + 1 >= PING_ROUNDS {
|
||||
break;
|
||||
}
|
||||
});
|
||||
ha.join().unwrap();
|
||||
@@ -198,7 +221,9 @@ fn bench_pingpong_tokio_current() -> (u64, u128) {
|
||||
tx_a.send(0).unwrap();
|
||||
loop {
|
||||
let v = rx_b.recv().await.unwrap();
|
||||
if v >= PING_ROUNDS { break; }
|
||||
if v >= PING_ROUNDS {
|
||||
break;
|
||||
}
|
||||
tx_a.send(v + 1).unwrap();
|
||||
}
|
||||
});
|
||||
@@ -206,7 +231,9 @@ fn bench_pingpong_tokio_current() -> (u64, u128) {
|
||||
loop {
|
||||
let v = rx_a.recv().await.unwrap();
|
||||
tx_b.send(v + 1).unwrap();
|
||||
if v + 1 >= PING_ROUNDS { break; }
|
||||
if v + 1 >= PING_ROUNDS {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
let _ = ha.await;
|
||||
@@ -229,7 +256,9 @@ fn bench_pingpong_tokio_multi() -> (u64, u128) {
|
||||
tx_a.send(0).unwrap();
|
||||
loop {
|
||||
let v = rx_b.recv().await.unwrap();
|
||||
if v >= PING_ROUNDS { break; }
|
||||
if v >= PING_ROUNDS {
|
||||
break;
|
||||
}
|
||||
tx_a.send(v + 1).unwrap();
|
||||
}
|
||||
});
|
||||
@@ -237,7 +266,9 @@ fn bench_pingpong_tokio_multi() -> (u64, u128) {
|
||||
loop {
|
||||
let v = rx_a.recv().await.unwrap();
|
||||
tx_b.send(v + 1).unwrap();
|
||||
if v + 1 >= PING_ROUNDS { break; }
|
||||
if v + 1 >= PING_ROUNDS {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
let _ = ha.await;
|
||||
@@ -264,7 +295,9 @@ fn bench_spawn_smarm(threads: usize) -> (u64, u128) {
|
||||
cc.fetch_add(1, Ordering::Relaxed);
|
||||
}));
|
||||
}
|
||||
for h in handles { h.join().unwrap(); }
|
||||
for h in handles {
|
||||
h.join().unwrap();
|
||||
}
|
||||
});
|
||||
(counter.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||
}
|
||||
@@ -272,7 +305,9 @@ fn bench_spawn_smarm(threads: usize) -> (u64, u128) {
|
||||
fn bench_spawn_tokio_current() -> (u64, u128) {
|
||||
let counter = Arc::new(AtomicU64::new(0));
|
||||
let c = counter.clone();
|
||||
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.build()
|
||||
.unwrap();
|
||||
let start = Instant::now();
|
||||
let local = tokio::task::LocalSet::new();
|
||||
local.block_on(&rt, async move {
|
||||
@@ -283,7 +318,9 @@ fn bench_spawn_tokio_current() -> (u64, u128) {
|
||||
cc.fetch_add(1, Ordering::Relaxed);
|
||||
}));
|
||||
}
|
||||
for h in handles { let _ = h.await; }
|
||||
for h in handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
});
|
||||
(counter.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||
}
|
||||
@@ -304,7 +341,9 @@ fn bench_spawn_tokio_multi() -> (u64, u128) {
|
||||
cc.fetch_add(1, Ordering::Relaxed);
|
||||
}));
|
||||
}
|
||||
for h in handles { let _ = h.await; }
|
||||
for h in handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
});
|
||||
(counter.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||
}
|
||||
@@ -320,24 +359,34 @@ fn main() {
|
||||
println!("PRIME_N={PRIME_N}, WORKERS={WORKERS}, PING_ROUNDS={PING_ROUNDS}, SPAWN_COUNT={SPAWN_COUNT}");
|
||||
|
||||
// ---- Primes ----
|
||||
print_header(&format!("Fan-out/fan-in: count primes in [2, {PRIME_N}) across {WORKERS} workers"));
|
||||
run_n("baseline (serial)", ITERS, bench_primes_baseline);
|
||||
run_n("smarm single-thread", ITERS, || bench_primes_smarm(1));
|
||||
run_n(&format!("smarm {n}-thread"), ITERS, || bench_primes_smarm(n));
|
||||
run_n("tokio current_thread", ITERS, bench_primes_tokio_current);
|
||||
run_n("tokio multi-thread", ITERS, bench_primes_tokio_multi);
|
||||
print_header(&format!(
|
||||
"Fan-out/fan-in: count primes in [2, {PRIME_N}) across {WORKERS} workers"
|
||||
));
|
||||
run_n("baseline (serial)", ITERS, bench_primes_baseline);
|
||||
run_n("smarm single-thread", ITERS, || bench_primes_smarm(1));
|
||||
run_n(&format!("smarm {n}-thread"), ITERS, || {
|
||||
bench_primes_smarm(n)
|
||||
});
|
||||
run_n("tokio current_thread", ITERS, bench_primes_tokio_current);
|
||||
run_n("tokio multi-thread", ITERS, bench_primes_tokio_multi);
|
||||
|
||||
// ---- Ping-pong ----
|
||||
print_header(&format!("Ping-pong: {PING_ROUNDS} round-trips between two actors"));
|
||||
run_n("smarm single-thread", ITERS, || bench_pingpong_smarm(1));
|
||||
run_n(&format!("smarm {n}-thread"), ITERS, || bench_pingpong_smarm(n));
|
||||
run_n("tokio current_thread", ITERS, bench_pingpong_tokio_current);
|
||||
run_n("tokio multi-thread", ITERS, bench_pingpong_tokio_multi);
|
||||
print_header(&format!(
|
||||
"Ping-pong: {PING_ROUNDS} round-trips between two actors"
|
||||
));
|
||||
run_n("smarm single-thread", ITERS, || bench_pingpong_smarm(1));
|
||||
run_n(&format!("smarm {n}-thread"), ITERS, || {
|
||||
bench_pingpong_smarm(n)
|
||||
});
|
||||
run_n("tokio current_thread", ITERS, bench_pingpong_tokio_current);
|
||||
run_n("tokio multi-thread", ITERS, bench_pingpong_tokio_multi);
|
||||
|
||||
// ---- Spawn throughput ----
|
||||
print_header(&format!("Spawn throughput: {SPAWN_COUNT} actors spawned and joined"));
|
||||
run_n("smarm single-thread", ITERS, || bench_spawn_smarm(1));
|
||||
print_header(&format!(
|
||||
"Spawn throughput: {SPAWN_COUNT} actors spawned and joined"
|
||||
));
|
||||
run_n("smarm single-thread", ITERS, || bench_spawn_smarm(1));
|
||||
run_n(&format!("smarm {n}-thread"), ITERS, || bench_spawn_smarm(n));
|
||||
run_n("tokio current_thread", ITERS, bench_spawn_tokio_current);
|
||||
run_n("tokio multi-thread", ITERS, bench_spawn_tokio_multi);
|
||||
run_n("tokio current_thread", ITERS, bench_spawn_tokio_current);
|
||||
run_n("tokio multi-thread", ITERS, bench_spawn_tokio_multi);
|
||||
}
|
||||
|
||||
+24
-7
@@ -16,12 +16,20 @@ const WORKERS: u64 = 16;
|
||||
const ITERATIONS: u32 = 5;
|
||||
|
||||
fn is_prime(n: u64) -> bool {
|
||||
if n < 2 { return false; }
|
||||
if n < 4 { return true; }
|
||||
if n % 2 == 0 { return false; }
|
||||
if n < 2 {
|
||||
return false;
|
||||
}
|
||||
if n < 4 {
|
||||
return true;
|
||||
}
|
||||
if n % 2 == 0 {
|
||||
return false;
|
||||
}
|
||||
let mut i = 3u64;
|
||||
while i * i <= n {
|
||||
if n % i == 0 { return false; }
|
||||
if n % i == 0 {
|
||||
return false;
|
||||
}
|
||||
i += 2;
|
||||
}
|
||||
true
|
||||
@@ -30,7 +38,9 @@ fn is_prime(n: u64) -> bool {
|
||||
fn count_primes_in(lo: u64, hi: u64) -> u64 {
|
||||
let mut count = 0u64;
|
||||
for n in lo..hi {
|
||||
if is_prime(n) { count += 1; }
|
||||
if is_prime(n) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
@@ -38,7 +48,11 @@ fn count_primes_in(lo: u64, hi: u64) -> u64 {
|
||||
fn slice(worker: u64) -> (u64, u64) {
|
||||
let per = N / WORKERS;
|
||||
let lo = worker * per;
|
||||
let hi = if worker + 1 == WORKERS { N } else { (worker + 1) * per };
|
||||
let hi = if worker + 1 == WORKERS {
|
||||
N
|
||||
} else {
|
||||
(worker + 1) * per
|
||||
};
|
||||
(lo, hi)
|
||||
}
|
||||
|
||||
@@ -125,7 +139,10 @@ fn main() {
|
||||
"Counting primes in [2, {}) across {} workers, {} iterations each\n",
|
||||
N, WORKERS, ITERATIONS
|
||||
);
|
||||
println!("{:>12} | {:>15} | {:>16} | {:>15} | {:>15}", "runtime", "primes found", "median", "min", "max");
|
||||
println!(
|
||||
"{:>12} | {:>15} | {:>16} | {:>15} | {:>15}",
|
||||
"runtime", "primes found", "median", "min", "max"
|
||||
);
|
||||
println!("{}", "-".repeat(80));
|
||||
|
||||
run_n("baseline", ITERATIONS, bench_baseline);
|
||||
|
||||
+44
-7
@@ -27,12 +27,19 @@ use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
fn env_usize(key: &str, default: usize) -> usize {
|
||||
std::env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
std::env::var(key)
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn env_threads() -> Vec<usize> {
|
||||
std::env::var("SMARM_BENCH_THREADS")
|
||||
.map(|v| v.split_whitespace().filter_map(|t| t.parse().ok()).collect())
|
||||
.map(|v| {
|
||||
v.split_whitespace()
|
||||
.filter_map(|t| t.parse().ok())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(|_| vec![1, 2, 4])
|
||||
}
|
||||
|
||||
@@ -53,7 +60,11 @@ fn drive<Q: Send + Sync + 'static>(
|
||||
for p in 0..producers {
|
||||
let q = q.clone();
|
||||
// Give the last producer the remainder.
|
||||
let n = if p == producers - 1 { items - per * (producers - 1) } else { per };
|
||||
let n = if p == producers - 1 {
|
||||
items - per * (producers - 1)
|
||||
} else {
|
||||
per
|
||||
};
|
||||
hs.push(std::thread::spawn(move || {
|
||||
let pid = Pid::new(p as u32, 0);
|
||||
for _ in 0..n {
|
||||
@@ -132,7 +143,12 @@ fn main() {
|
||||
for &t in &threads_sweep {
|
||||
for (p, c) in ratios_for(t) {
|
||||
for s in ["mutex", "mpmc", "striped"] {
|
||||
cases.push(Case { structure: s, threads: t, producers: p, consumers: c });
|
||||
cases.push(Case {
|
||||
structure: s,
|
||||
threads: t,
|
||||
producers: p,
|
||||
consumers: c,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,7 +163,14 @@ fn main() {
|
||||
if case.threads < 2 {
|
||||
drive_single(&*q, MutexQueue::push, MutexQueue::pop, items)
|
||||
} else {
|
||||
drive(q, MutexQueue::push, MutexQueue::pop, case.producers, case.consumers, items)
|
||||
drive(
|
||||
q,
|
||||
MutexQueue::push,
|
||||
MutexQueue::pop,
|
||||
case.producers,
|
||||
case.consumers,
|
||||
items,
|
||||
)
|
||||
}
|
||||
}
|
||||
"mpmc" => {
|
||||
@@ -155,7 +178,14 @@ fn main() {
|
||||
if case.threads < 2 {
|
||||
drive_single(&*q, MpmcRing::push, MpmcRing::pop, items)
|
||||
} else {
|
||||
drive(q, MpmcRing::push, MpmcRing::pop, case.producers, case.consumers, items)
|
||||
drive(
|
||||
q,
|
||||
MpmcRing::push,
|
||||
MpmcRing::pop,
|
||||
case.producers,
|
||||
case.consumers,
|
||||
items,
|
||||
)
|
||||
}
|
||||
}
|
||||
"striped" => {
|
||||
@@ -163,7 +193,14 @@ fn main() {
|
||||
if case.threads < 2 {
|
||||
drive_single(&*q, StripedRing::push, StripedRing::pop, items)
|
||||
} else {
|
||||
drive(q, StripedRing::push, StripedRing::pop, case.producers, case.consumers, items)
|
||||
drive(
|
||||
q,
|
||||
StripedRing::push,
|
||||
StripedRing::pop,
|
||||
case.producers,
|
||||
case.consumers,
|
||||
items,
|
||||
)
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
|
||||
+21
-4
@@ -54,12 +54,19 @@ fn variant() -> &'static str {
|
||||
}
|
||||
|
||||
fn env_usize(key: &str, default: usize) -> usize {
|
||||
std::env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
std::env::var(key)
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn env_threads() -> Vec<usize> {
|
||||
std::env::var("SMARM_BENCH_THREADS")
|
||||
.map(|v| v.split_whitespace().filter_map(|t| t.parse().ok()).collect())
|
||||
.map(|v| {
|
||||
v.split_whitespace()
|
||||
.filter_map(|t| t.parse().ok())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(|_| vec![1, 2, 4])
|
||||
}
|
||||
|
||||
@@ -238,12 +245,22 @@ fn main() {
|
||||
);
|
||||
println!(
|
||||
"RQCSV,runtime,{},{},{},{},{},{},{}",
|
||||
variant(), slot_str, name, t, work, mid.us, per_s
|
||||
variant(),
|
||||
slot_str,
|
||||
name,
|
||||
t,
|
||||
work,
|
||||
mid.us,
|
||||
per_s
|
||||
);
|
||||
if slot {
|
||||
println!(
|
||||
"RQSLOT,{},{},{},{},{}",
|
||||
variant(), name, t, mid.hits, mid.displacements
|
||||
variant(),
|
||||
name,
|
||||
t,
|
||||
mid.hits,
|
||||
mid.displacements
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+55
-19
@@ -37,7 +37,9 @@ use std::time::Instant;
|
||||
const ITERS: u32 = 15;
|
||||
|
||||
fn available_threads() -> usize {
|
||||
std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1)
|
||||
std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(1)
|
||||
}
|
||||
|
||||
fn env_sets() -> u32 {
|
||||
@@ -116,7 +118,9 @@ fn bench_recurse_smarm(threads: usize) -> (u64, u128) {
|
||||
fn bench_recurse_tokio_current() -> (u64, u128) {
|
||||
let counter = Arc::new(AtomicU64::new(0));
|
||||
let c2 = counter.clone();
|
||||
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.build()
|
||||
.unwrap();
|
||||
let start = Instant::now();
|
||||
let local = tokio::task::LocalSet::new();
|
||||
local.block_on(&rt, async move {
|
||||
@@ -199,7 +203,9 @@ fn bench_hot_smarm() -> (u64, u128) {
|
||||
}
|
||||
|
||||
fn bench_hot_tokio_current() -> (u64, u128) {
|
||||
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.build()
|
||||
.unwrap();
|
||||
let start = Instant::now();
|
||||
let local = tokio::task::LocalSet::new();
|
||||
local.block_on(&rt, async move {
|
||||
@@ -249,7 +255,9 @@ fn bench_unc_smarm() -> (u64, u128) {
|
||||
}
|
||||
|
||||
fn bench_unc_tokio_current() -> (u64, u128) {
|
||||
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.build()
|
||||
.unwrap();
|
||||
let start = Instant::now();
|
||||
let local = tokio::task::LocalSet::new();
|
||||
local.block_on(&rt, async move {
|
||||
@@ -297,8 +305,12 @@ fn bench_panic_smarm(threads: usize) -> (u64, u128) {
|
||||
}
|
||||
for h in handles {
|
||||
match h.join() {
|
||||
Ok(()) => { ok2.fetch_add(1, Ordering::Relaxed); }
|
||||
Err(_) => { err2.fetch_add(1, Ordering::Relaxed); }
|
||||
Ok(()) => {
|
||||
ok2.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
Err(_) => {
|
||||
err2.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -312,7 +324,9 @@ fn bench_panic_tokio_current() -> (u64, u128) {
|
||||
let err = Arc::new(AtomicU64::new(0));
|
||||
let ok2 = ok.clone();
|
||||
let err2 = err.clone();
|
||||
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.build()
|
||||
.unwrap();
|
||||
let prev_hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(|_| {}));
|
||||
let start = Instant::now();
|
||||
@@ -328,8 +342,12 @@ fn bench_panic_tokio_current() -> (u64, u128) {
|
||||
}
|
||||
for h in handles {
|
||||
match h.await {
|
||||
Ok(()) => { ok2.fetch_add(1, Ordering::Relaxed); }
|
||||
Err(_) => { err2.fetch_add(1, Ordering::Relaxed); }
|
||||
Ok(()) => {
|
||||
ok2.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
Err(_) => {
|
||||
err2.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -361,8 +379,12 @@ fn bench_panic_tokio_multi() -> (u64, u128) {
|
||||
}
|
||||
for h in handles {
|
||||
match h.await {
|
||||
Ok(()) => { ok2.fetch_add(1, Ordering::Relaxed); }
|
||||
Err(_) => { err2.fetch_add(1, Ordering::Relaxed); }
|
||||
Ok(()) => {
|
||||
ok2.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
Err(_) => {
|
||||
err2.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -375,7 +397,6 @@ fn bench_panic_tokio_multi() -> (u64, u128) {
|
||||
// main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Knob helper — reads SMARM_ALLOC_INTERVAL / SMARM_TIMESLICE_CYCLES env vars
|
||||
// so the sweep script can override the preemption knobs without recompiling.
|
||||
@@ -384,10 +405,14 @@ fn bench_panic_tokio_multi() -> (u64, u128) {
|
||||
fn bench_cfg(threads: usize) -> smarm::runtime::Config {
|
||||
let mut cfg = smarm::runtime::Config::exact(threads);
|
||||
if let Ok(v) = std::env::var("SMARM_ALLOC_INTERVAL") {
|
||||
if let Ok(n) = v.parse::<u32>() { cfg = cfg.alloc_interval(n); }
|
||||
if let Ok(n) = v.parse::<u32>() {
|
||||
cfg = cfg.alloc_interval(n);
|
||||
}
|
||||
}
|
||||
if let Ok(v) = std::env::var("SMARM_TIMESLICE_CYCLES") {
|
||||
if let Ok(n) = v.parse::<u64>() { cfg = cfg.timeslice_cycles(n); }
|
||||
if let Ok(n) = v.parse::<u64>() {
|
||||
cfg = cfg.timeslice_cycles(n);
|
||||
}
|
||||
}
|
||||
cfg
|
||||
}
|
||||
@@ -397,7 +422,10 @@ fn main() {
|
||||
println!("smarm smarm-favored benchmarks");
|
||||
println!("available parallelism: {n} threads");
|
||||
let sets = env_sets();
|
||||
println!("ITERS={ITERS}×{sets} sets = {} samples (+1 warmup, discarded)", ITERS * sets);
|
||||
println!(
|
||||
"ITERS={ITERS}×{sets} sets = {} samples (+1 warmup, discarded)",
|
||||
ITERS * sets
|
||||
);
|
||||
println!(
|
||||
"RECURSE_DEPTH={RECURSE_DEPTH}, HOT_YIELDS={HOT_YIELDS}×2, \
|
||||
UNCONT_MSGS={UNCONT_MSGS}, PANIC_TASKS={PANIC_TASKS}"
|
||||
@@ -406,22 +434,30 @@ fn main() {
|
||||
// ---- 9. deep_recursion ----
|
||||
print_header(&format!("deep_recursion: depth {RECURSE_DEPTH}"));
|
||||
run_n("smarm 1-thread", ITERS, || bench_recurse_smarm(1));
|
||||
run_n(&format!("smarm {n}-thread"), ITERS, || bench_recurse_smarm(n));
|
||||
run_n(&format!("smarm {n}-thread"), ITERS, || {
|
||||
bench_recurse_smarm(n)
|
||||
});
|
||||
run_n("tokio current_thread", ITERS, bench_recurse_tokio_current);
|
||||
run_n("tokio multi-thread", ITERS, bench_recurse_tokio_multi);
|
||||
|
||||
// ---- 10. yield_in_hot_loop ----
|
||||
print_header(&format!("yield_in_hot_loop: 2 actors × {HOT_YIELDS} yields (single thread)"));
|
||||
print_header(&format!(
|
||||
"yield_in_hot_loop: 2 actors × {HOT_YIELDS} yields (single thread)"
|
||||
));
|
||||
run_n("smarm 1-thread", ITERS, bench_hot_smarm);
|
||||
run_n("tokio current_thread", ITERS, bench_hot_tokio_current);
|
||||
|
||||
// ---- 11. uncontended_channel ----
|
||||
print_header(&format!("uncontended_channel: 1→1, {UNCONT_MSGS} msgs (single thread)"));
|
||||
print_header(&format!(
|
||||
"uncontended_channel: 1→1, {UNCONT_MSGS} msgs (single thread)"
|
||||
));
|
||||
run_n("smarm 1-thread", ITERS, bench_unc_smarm);
|
||||
run_n("tokio current_thread", ITERS, bench_unc_tokio_current);
|
||||
|
||||
// ---- 12. catch_unwind_panics ----
|
||||
print_header(&format!("catch_unwind_panics: {PANIC_TASKS} tasks, 50% panic"));
|
||||
print_header(&format!(
|
||||
"catch_unwind_panics: {PANIC_TASKS} tasks, 50% panic"
|
||||
));
|
||||
run_n("smarm 1-thread", ITERS, || bench_panic_smarm(1));
|
||||
run_n(&format!("smarm {n}-thread"), ITERS, || bench_panic_smarm(n));
|
||||
run_n("tokio current_thread", ITERS, bench_panic_tokio_current);
|
||||
|
||||
+30
-5
@@ -73,7 +73,10 @@ fn variant() -> &'static str {
|
||||
}
|
||||
|
||||
fn env_usize(key: &str, default: usize) -> usize {
|
||||
std::env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
std::env::var(key)
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
@@ -226,7 +229,11 @@ fn main() {
|
||||
let mean_cyc = pooled_cyc.iter().map(|&v| v as f64).sum::<f64>() / n.max(1) as f64;
|
||||
// Derived effective frequency: cycles per ns = GHz. Cross-checks the two
|
||||
// lenses against the box's known base clock.
|
||||
let derived_ghz = if mean_ns > 0.0 { mean_cyc / mean_ns } else { 0.0 };
|
||||
let derived_ghz = if mean_ns > 0.0 {
|
||||
mean_cyc / mean_ns
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let p50 = pct(&pooled_ns, 50.0);
|
||||
let p90 = pct(&pooled_ns, 90.0);
|
||||
@@ -241,8 +248,14 @@ fn main() {
|
||||
" rounds={} warmup={} runs={} (instrumentation floor: {} ns / {} cyc, subtracted)",
|
||||
rounds, warmup, runs, floor_ns, floor_cyc
|
||||
);
|
||||
println!(" {:<10} {:<10} {:<10} {:<10} {:<10}", "p50 ns", "p90 ns", "p99 ns", "min ns", "max ns");
|
||||
println!(" {:<10} {:<10} {:<10} {:<10} {:<10}", p50, p90, p99, lo, hi);
|
||||
println!(
|
||||
" {:<10} {:<10} {:<10} {:<10} {:<10}",
|
||||
"p50 ns", "p90 ns", "p99 ns", "min ns", "max ns"
|
||||
);
|
||||
println!(
|
||||
" {:<10} {:<10} {:<10} {:<10} {:<10}",
|
||||
p50, p90, p99, lo, hi
|
||||
);
|
||||
println!(
|
||||
" mean {:.1} ns | mean {:.0} cyc | derived {:.3} GHz",
|
||||
mean_ns, mean_cyc, derived_ghz
|
||||
@@ -251,6 +264,18 @@ fn main() {
|
||||
// Greppable line — same spirit as SPINCSV.
|
||||
println!(
|
||||
"SWITCHCSV,{},{},{},{},{},{},{},{},{},{},{:.1},{:.0},{:.3}",
|
||||
variant(), mode, rounds, runs, n, p50, p90, p99, lo, hi, mean_ns, mean_cyc, derived_ghz
|
||||
variant(),
|
||||
mode,
|
||||
rounds,
|
||||
runs,
|
||||
n,
|
||||
p50,
|
||||
p90,
|
||||
p99,
|
||||
lo,
|
||||
hi,
|
||||
mean_ns,
|
||||
mean_cyc,
|
||||
derived_ghz
|
||||
);
|
||||
}
|
||||
|
||||
+107
-35
@@ -36,7 +36,9 @@ use std::time::{Duration, Instant};
|
||||
const ITERS: u32 = 15;
|
||||
|
||||
fn available_threads() -> usize {
|
||||
std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1)
|
||||
std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(1)
|
||||
}
|
||||
|
||||
fn env_sets() -> u32 {
|
||||
@@ -84,8 +86,8 @@ fn run_n<F: FnMut() -> (u64, u128)>(name: &str, n: u32, mut f: F) {
|
||||
// 5. spawn_storm_busy — workers loaded, then storm of zero-work spawns
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const STORM_BACKGROUND: u64 = 8; // number of background "busy" actors
|
||||
const STORM_SPAWN: u64 = 10_000; // zero-work spawns to time
|
||||
const STORM_BACKGROUND: u64 = 8; // number of background "busy" actors
|
||||
const STORM_SPAWN: u64 = 10_000; // zero-work spawns to time
|
||||
|
||||
fn bench_storm_smarm(threads: usize) -> (u64, u128) {
|
||||
let counter = Arc::new(AtomicU64::new(0));
|
||||
@@ -114,11 +116,15 @@ fn bench_storm_smarm(threads: usize) -> (u64, u128) {
|
||||
cc.fetch_add(1, Ordering::Relaxed);
|
||||
}));
|
||||
}
|
||||
for h in handles { h.join().unwrap(); }
|
||||
for h in handles {
|
||||
h.join().unwrap();
|
||||
}
|
||||
|
||||
// Tear down background.
|
||||
s2.store(true, Ordering::Relaxed);
|
||||
for h in bg_handles { h.join().unwrap(); }
|
||||
for h in bg_handles {
|
||||
h.join().unwrap();
|
||||
}
|
||||
});
|
||||
(counter.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||
}
|
||||
@@ -129,7 +135,9 @@ fn bench_storm_tokio_current() -> (u64, u128) {
|
||||
let c2 = counter.clone();
|
||||
let s2 = stop.clone();
|
||||
|
||||
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.build()
|
||||
.unwrap();
|
||||
let start = Instant::now();
|
||||
let local = tokio::task::LocalSet::new();
|
||||
local.block_on(&rt, async move {
|
||||
@@ -149,9 +157,13 @@ fn bench_storm_tokio_current() -> (u64, u128) {
|
||||
cc.fetch_add(1, Ordering::Relaxed);
|
||||
}));
|
||||
}
|
||||
for h in handles { let _ = h.await; }
|
||||
for h in handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
s2.store(true, Ordering::Relaxed);
|
||||
for h in bg_handles { let _ = h.await; }
|
||||
for h in bg_handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
});
|
||||
(counter.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||
}
|
||||
@@ -184,9 +196,13 @@ fn bench_storm_tokio_multi() -> (u64, u128) {
|
||||
cc.fetch_add(1, Ordering::Relaxed);
|
||||
}));
|
||||
}
|
||||
for h in handles { let _ = h.await; }
|
||||
for h in handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
s2.store(true, Ordering::Relaxed);
|
||||
for h in bg_handles { let _ = h.await; }
|
||||
for h in bg_handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
});
|
||||
(counter.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||
}
|
||||
@@ -219,14 +235,21 @@ fn bench_mpsc_smarm(threads: usize) -> (u64, u128) {
|
||||
}
|
||||
let _ = count; // discard; run() closure must return ()
|
||||
});
|
||||
for h in prod_handles { h.join().unwrap(); }
|
||||
for h in prod_handles {
|
||||
h.join().unwrap();
|
||||
}
|
||||
let _ = consumer.join().unwrap();
|
||||
});
|
||||
(MPSC_PRODUCERS * MPSC_PER_PRODUCER, start.elapsed().as_micros())
|
||||
(
|
||||
MPSC_PRODUCERS * MPSC_PER_PRODUCER,
|
||||
start.elapsed().as_micros(),
|
||||
)
|
||||
}
|
||||
|
||||
fn bench_mpsc_tokio_current() -> (u64, u128) {
|
||||
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.build()
|
||||
.unwrap();
|
||||
let start = Instant::now();
|
||||
let local = tokio::task::LocalSet::new();
|
||||
local.block_on(&rt, async move {
|
||||
@@ -248,10 +271,15 @@ fn bench_mpsc_tokio_current() -> (u64, u128) {
|
||||
}
|
||||
count
|
||||
});
|
||||
for h in prod_handles { let _ = h.await; }
|
||||
for h in prod_handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
let _ = consumer.await;
|
||||
});
|
||||
(MPSC_PRODUCERS * MPSC_PER_PRODUCER, start.elapsed().as_micros())
|
||||
(
|
||||
MPSC_PRODUCERS * MPSC_PER_PRODUCER,
|
||||
start.elapsed().as_micros(),
|
||||
)
|
||||
}
|
||||
|
||||
fn bench_mpsc_tokio_multi() -> (u64, u128) {
|
||||
@@ -279,10 +307,15 @@ fn bench_mpsc_tokio_multi() -> (u64, u128) {
|
||||
}
|
||||
count
|
||||
});
|
||||
for h in prod_handles { let _ = h.await; }
|
||||
for h in prod_handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
let _ = consumer.await;
|
||||
});
|
||||
(MPSC_PRODUCERS * MPSC_PER_PRODUCER, start.elapsed().as_micros())
|
||||
(
|
||||
MPSC_PRODUCERS * MPSC_PER_PRODUCER,
|
||||
start.elapsed().as_micros(),
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -308,7 +341,9 @@ fn bench_timers_smarm(threads: usize) -> (u64, u128) {
|
||||
smarm::sleep(Duration::from_millis(ms));
|
||||
}));
|
||||
}
|
||||
for h in handles { h.join().unwrap(); }
|
||||
for h in handles {
|
||||
h.join().unwrap();
|
||||
}
|
||||
});
|
||||
(TIMER_ACTORS, start.elapsed().as_micros())
|
||||
}
|
||||
@@ -328,7 +363,9 @@ fn bench_timers_tokio_current() -> (u64, u128) {
|
||||
tokio::time::sleep(Duration::from_millis(ms)).await;
|
||||
}));
|
||||
}
|
||||
for h in handles { let _ = h.await; }
|
||||
for h in handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
});
|
||||
(TIMER_ACTORS, start.elapsed().as_micros())
|
||||
}
|
||||
@@ -348,7 +385,9 @@ fn bench_timers_tokio_multi() -> (u64, u128) {
|
||||
tokio::time::sleep(Duration::from_millis(ms)).await;
|
||||
}));
|
||||
}
|
||||
for h in handles { let _ = h.await; }
|
||||
for h in handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
});
|
||||
(TIMER_ACTORS, start.elapsed().as_micros())
|
||||
}
|
||||
@@ -361,11 +400,22 @@ const SCALING_N: u64 = 400_000;
|
||||
const SCALING_WORKERS: u64 = 64;
|
||||
|
||||
fn is_prime(n: u64) -> bool {
|
||||
if n < 2 { return false; }
|
||||
if n < 4 { return true; }
|
||||
if n % 2 == 0 { return false; }
|
||||
if n < 2 {
|
||||
return false;
|
||||
}
|
||||
if n < 4 {
|
||||
return true;
|
||||
}
|
||||
if n % 2 == 0 {
|
||||
return false;
|
||||
}
|
||||
let mut i = 3u64;
|
||||
while i * i <= n { if n % i == 0 { return false; } i += 2; }
|
||||
while i * i <= n {
|
||||
if n % i == 0 {
|
||||
return false;
|
||||
}
|
||||
i += 2;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -376,7 +426,11 @@ fn count_primes(lo: u64, hi: u64) -> u64 {
|
||||
fn scaling_slice(w: u64) -> (u64, u64) {
|
||||
let per = SCALING_N / SCALING_WORKERS;
|
||||
let lo = w * per;
|
||||
let hi = if w + 1 == SCALING_WORKERS { SCALING_N } else { lo + per };
|
||||
let hi = if w + 1 == SCALING_WORKERS {
|
||||
SCALING_N
|
||||
} else {
|
||||
lo + per
|
||||
};
|
||||
(lo, hi)
|
||||
}
|
||||
|
||||
@@ -393,7 +447,9 @@ fn bench_scaling_smarm(threads: usize) -> (u64, u128) {
|
||||
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed);
|
||||
}));
|
||||
}
|
||||
for h in handles { h.join().unwrap(); }
|
||||
for h in handles {
|
||||
h.join().unwrap();
|
||||
}
|
||||
});
|
||||
(total.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||
}
|
||||
@@ -415,7 +471,9 @@ fn bench_scaling_tokio_multi(threads: usize) -> (u64, u128) {
|
||||
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed);
|
||||
}));
|
||||
}
|
||||
for h in handles { let _ = h.await; }
|
||||
for h in handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
});
|
||||
(total.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||
}
|
||||
@@ -424,7 +482,6 @@ fn bench_scaling_tokio_multi(threads: usize) -> (u64, u128) {
|
||||
// main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Knob helper — reads SMARM_ALLOC_INTERVAL / SMARM_TIMESLICE_CYCLES env vars
|
||||
// so the sweep script can override the preemption knobs without recompiling.
|
||||
@@ -433,10 +490,14 @@ fn bench_scaling_tokio_multi(threads: usize) -> (u64, u128) {
|
||||
fn bench_cfg(threads: usize) -> smarm::runtime::Config {
|
||||
let mut cfg = smarm::runtime::Config::exact(threads);
|
||||
if let Ok(v) = std::env::var("SMARM_ALLOC_INTERVAL") {
|
||||
if let Ok(n) = v.parse::<u32>() { cfg = cfg.alloc_interval(n); }
|
||||
if let Ok(n) = v.parse::<u32>() {
|
||||
cfg = cfg.alloc_interval(n);
|
||||
}
|
||||
}
|
||||
if let Ok(v) = std::env::var("SMARM_TIMESLICE_CYCLES") {
|
||||
if let Ok(n) = v.parse::<u64>() { cfg = cfg.timeslice_cycles(n); }
|
||||
if let Ok(n) = v.parse::<u64>() {
|
||||
cfg = cfg.timeslice_cycles(n);
|
||||
}
|
||||
}
|
||||
cfg
|
||||
}
|
||||
@@ -446,7 +507,10 @@ fn main() {
|
||||
println!("smarm tokio-favored benchmarks");
|
||||
println!("available parallelism: {n} threads");
|
||||
let sets = env_sets();
|
||||
println!("ITERS={ITERS}×{sets} sets = {} samples (+1 warmup, discarded)", ITERS * sets);
|
||||
println!(
|
||||
"ITERS={ITERS}×{sets} sets = {} samples (+1 warmup, discarded)",
|
||||
ITERS * sets
|
||||
);
|
||||
println!(
|
||||
"STORM_BACKGROUND={STORM_BACKGROUND}, STORM_SPAWN={STORM_SPAWN}, \
|
||||
MPSC={MPSC_PRODUCERS}×{MPSC_PER_PRODUCER}, \
|
||||
@@ -477,7 +541,9 @@ fn main() {
|
||||
"many_timers: {TIMER_ACTORS} actors sleeping {TIMER_MIN_MS}–{TIMER_MAX_MS} ms"
|
||||
));
|
||||
run_n("smarm 1-thread", ITERS, || bench_timers_smarm(1));
|
||||
run_n(&format!("smarm {n}-thread"), ITERS, || bench_timers_smarm(n));
|
||||
run_n(&format!("smarm {n}-thread"), ITERS, || {
|
||||
bench_timers_smarm(n)
|
||||
});
|
||||
run_n("tokio current_thread", ITERS, bench_timers_tokio_current);
|
||||
run_n("tokio multi-thread", ITERS, bench_timers_tokio_multi);
|
||||
|
||||
@@ -487,13 +553,19 @@ fn main() {
|
||||
));
|
||||
let sweep: Vec<usize> = {
|
||||
let mut v = vec![1usize, 2, 4];
|
||||
if n > 4 && !v.contains(&n) { v.push(n); }
|
||||
if n > 4 && !v.contains(&n) {
|
||||
v.push(n);
|
||||
}
|
||||
v.into_iter().filter(|t| *t <= n).collect()
|
||||
};
|
||||
for t in &sweep {
|
||||
run_n(&format!("smarm {t}-thread"), ITERS, || bench_scaling_smarm(*t));
|
||||
run_n(&format!("smarm {t}-thread"), ITERS, || {
|
||||
bench_scaling_smarm(*t)
|
||||
});
|
||||
}
|
||||
for t in &sweep {
|
||||
run_n(&format!("tokio multi {t}-thread"), ITERS, || bench_scaling_tokio_multi(*t));
|
||||
run_n(&format!("tokio multi {t}-thread"), ITERS, || {
|
||||
bench_scaling_tokio_multi(*t)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,9 @@ fn main() {
|
||||
println!("calibration: {per_us} work iters/µs");
|
||||
let work_us = move |us: u64| work_iters(us * per_us);
|
||||
|
||||
let cores = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
|
||||
let cores = std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(1);
|
||||
println!("cores: {cores}");
|
||||
if cores < 4 {
|
||||
println!("probe: SKIPPED (needs the stages in parallel)");
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
|
||||
#![deny(dead_code, unreachable_patterns)]
|
||||
|
||||
use smarm::gen_statem::{spawn, Cx, GenStatemRef, Machine, Reply, Resolution, Step};
|
||||
use smarm::run;
|
||||
use smarm::gen_statem::{spawn, Cx, Machine, Reply, Resolution, Step, GenStatemRef};
|
||||
|
||||
// === user types ============================================================
|
||||
|
||||
@@ -123,7 +123,11 @@ impl DoorSm {
|
||||
fn start(init: Door) -> GenStatemRef<DoorSm> {
|
||||
spawn(DoorSm {
|
||||
state: init,
|
||||
data: Data { enters: 0, pushes: 0, knocks: 0 },
|
||||
data: Data {
|
||||
enters: 0,
|
||||
pushes: 0,
|
||||
knocks: 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -193,16 +197,12 @@ impl Machine for DoorSm {
|
||||
(Door::Closed, Ev::Cast(Cast::Push | Cast::Unlock(_))) => Resolution::Unhandled,
|
||||
|
||||
// --- Locked (branching row: handler picks within UnlockOutcome) -
|
||||
(Door::Locked, Ev::Cast(Cast::Unlock(key))) => {
|
||||
Resolution::To(on_unlock(key).into())
|
||||
}
|
||||
(Door::Locked, Ev::Cast(Cast::Unlock(key))) => Resolution::To(on_unlock(key).into()),
|
||||
// Routed out in phase 1; listed only to keep this match total.
|
||||
(Door::Locked, Ev::Cast(Cast::Knock)) => {
|
||||
unreachable!("postponed event is replayed, not dispatched here")
|
||||
}
|
||||
(Door::Locked, Ev::Cast(Cast::Push | Cast::Pull | Cast::Lock)) => {
|
||||
Resolution::Unhandled
|
||||
}
|
||||
(Door::Locked, Ev::Cast(Cast::Push | Cast::Pull | Cast::Lock)) => Resolution::Unhandled,
|
||||
|
||||
// --- state-independent queries (reply, then stay) ---------------
|
||||
(_, Ev::Call(Call::GetState(r))) => {
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
// dispatch's own unreachable_patterns internally.
|
||||
|
||||
use smarm::gen_statem;
|
||||
use smarm::run;
|
||||
use smarm::gen_statem::Reply;
|
||||
use smarm::run;
|
||||
|
||||
// === user types (identical to gen_statem_expanded.rs) =========================
|
||||
|
||||
@@ -135,7 +135,14 @@ gen_statem! {
|
||||
|
||||
fn main() {
|
||||
run(|| {
|
||||
let door = DoorSm::start(Door::Closed, Data { enters: 0, pushes: 0, knocks: 0 });
|
||||
let door = DoorSm::start(
|
||||
Door::Closed,
|
||||
Data {
|
||||
enters: 0,
|
||||
pushes: 0,
|
||||
knocks: 0,
|
||||
},
|
||||
);
|
||||
|
||||
door.send(Ev::Cast(Cast::Lock)).unwrap(); // Closed -> Locked
|
||||
door.send(Ev::Cast(Cast::Knock)).unwrap(); // Locked: postponed (not yet counted)
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
//! every use — so the address keeps working across a supervised restart, with
|
||||
//! no stale [`GenServerRef`] to refresh.
|
||||
|
||||
use smarm::{call, cast, run, whereis_server, GenServer, GenServerBuilder, GenServerName, GenServerRef};
|
||||
use smarm::{
|
||||
call, cast, run, whereis_server, GenServer, GenServerBuilder, GenServerName, GenServerRef,
|
||||
};
|
||||
|
||||
/// A counter server: synchronous `Get`, asynchronous `Inc` / `Add`.
|
||||
struct Counter {
|
||||
|
||||
+13
-3
@@ -15,7 +15,9 @@
|
||||
//! `call`, nothing more.
|
||||
|
||||
use smarm::observer::{self, ObserverReply, ObserverRequest};
|
||||
use smarm::{channel, register, run, spawn, ActorState, Name, RuntimeSnapshot, RuntimeTree, TreeNode};
|
||||
use smarm::{
|
||||
channel, register, run, spawn, ActorState, Name, RuntimeSnapshot, RuntimeTree, TreeNode,
|
||||
};
|
||||
|
||||
const ECHO: Name<u64> = Name::new("echo");
|
||||
|
||||
@@ -31,7 +33,11 @@ fn state_glyph(s: ActorState) -> &'static str {
|
||||
|
||||
/// A `ps`-style table over the flat snapshot.
|
||||
fn print_snapshot(snap: &RuntimeSnapshot) {
|
||||
println!("snapshot (format v{}, {} actors)", snap.format_version, snap.actors.len());
|
||||
println!(
|
||||
"snapshot (format v{}, {} actors)",
|
||||
snap.format_version,
|
||||
snap.actors.len()
|
||||
);
|
||||
println!(
|
||||
" {:<10} {:<9} {:<10} {:>4} {:>4} {:>4} {:>4} {:>5} {}",
|
||||
"pid", "state", "parent", "mon", "lnk", "joi", "mbox", "msgs", "names"
|
||||
@@ -52,7 +58,11 @@ fn print_snapshot(snap: &RuntimeSnapshot) {
|
||||
a.joiners,
|
||||
a.mailbox_depth,
|
||||
a.messages_received,
|
||||
if a.names.is_empty() { "-".to_string() } else { a.names.join(",") },
|
||||
if a.names.is_empty() {
|
||||
"-".to_string()
|
||||
} else {
|
||||
a.names.join(",")
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -99,7 +99,7 @@ pub extern "C-unwind" fn trampoline() {
|
||||
};
|
||||
|
||||
let outcome = match panic::catch_unwind(panic::AssertUnwindSafe(b)) {
|
||||
Ok(()) => Outcome::Exit,
|
||||
Ok(()) => Outcome::Exit,
|
||||
Err(payload) => {
|
||||
if payload.is::<StopSentinel>() {
|
||||
Outcome::Stopped
|
||||
|
||||
+20
-10
@@ -342,8 +342,7 @@ mod inner {
|
||||
// Count the loss in would-be delta terms so the audit's columns
|
||||
// compare directly against `injected_cycles`.
|
||||
DISCARD_OVERMAX_N.fetch_add(1, Ordering::Relaxed);
|
||||
DISCARD_OVERMAX_CYCLES
|
||||
.fetch_add(interval.saturating_mul(pct) / 100, Ordering::Relaxed);
|
||||
DISCARD_OVERMAX_CYCLES.fetch_add(interval.saturating_mul(pct) / 100, Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
let delta = interval.saturating_mul(pct) / 100;
|
||||
@@ -419,8 +418,7 @@ mod inner {
|
||||
let gap = preempt::rdtsc()
|
||||
.saturating_sub(desched_tsc)
|
||||
.min(MAX_SAMPLE_CYCLES);
|
||||
OFFCPU_IN_SITE_CYCLES
|
||||
.fetch_add(gap.saturating_mul(pct) / 100, Ordering::Relaxed);
|
||||
OFFCPU_IN_SITE_CYCLES.fetch_add(gap.saturating_mul(pct) / 100, Ordering::Relaxed);
|
||||
OFFCPU_IN_SITE_N.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
@@ -533,7 +531,9 @@ mod inner {
|
||||
park_forgiven_cycles: self
|
||||
.park_forgiven_cycles
|
||||
.saturating_sub(before.park_forgiven_cycles),
|
||||
drop_park_cycles: self.drop_park_cycles.saturating_sub(before.drop_park_cycles),
|
||||
drop_park_cycles: self
|
||||
.drop_park_cycles
|
||||
.saturating_sub(before.drop_park_cycles),
|
||||
drop_park_n: self.drop_park_n.saturating_sub(before.drop_park_n),
|
||||
drop_yield_cycles: self
|
||||
.drop_yield_cycles
|
||||
@@ -542,12 +542,18 @@ mod inner {
|
||||
discard_overmax_cycles: self
|
||||
.discard_overmax_cycles
|
||||
.saturating_sub(before.discard_overmax_cycles),
|
||||
discard_overmax_n: self.discard_overmax_n.saturating_sub(before.discard_overmax_n),
|
||||
discard_unarmed_n: self.discard_unarmed_n.saturating_sub(before.discard_unarmed_n),
|
||||
discard_overmax_n: self
|
||||
.discard_overmax_n
|
||||
.saturating_sub(before.discard_overmax_n),
|
||||
discard_unarmed_n: self
|
||||
.discard_unarmed_n
|
||||
.saturating_sub(before.discard_unarmed_n),
|
||||
offcpu_in_site_cycles: self
|
||||
.offcpu_in_site_cycles
|
||||
.saturating_sub(before.offcpu_in_site_cycles),
|
||||
offcpu_in_site_n: self.offcpu_in_site_n.saturating_sub(before.offcpu_in_site_n),
|
||||
offcpu_in_site_n: self
|
||||
.offcpu_in_site_n
|
||||
.saturating_sub(before.offcpu_in_site_n),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -795,7 +801,9 @@ mod inner {
|
||||
let cell = results
|
||||
.iter()
|
||||
.find(|r| r.site == site && r.speedup_pct == speedup_pct)?;
|
||||
let base = results.iter().find(|r| r.site == site && r.speedup_pct == 0)?;
|
||||
let base = results
|
||||
.iter()
|
||||
.find(|r| r.site == site && r.speedup_pct == 0)?;
|
||||
let rate = normalized_rate(cell, point)?;
|
||||
let b = normalized_rate(base, point)?;
|
||||
if b <= 0.0 {
|
||||
@@ -946,7 +954,9 @@ macro_rules! progress {
|
||||
macro_rules! causal_site {
|
||||
($name:literal) => {{
|
||||
static __SMARM_SITE: ::std::sync::OnceLock<u32> = ::std::sync::OnceLock::new();
|
||||
$crate::causal::SiteGuard::enter(*__SMARM_SITE.get_or_init(|| $crate::causal::site_id($name)))
|
||||
$crate::causal::SiteGuard::enter(
|
||||
*__SMARM_SITE.get_or_init(|| $crate::causal::site_id($name)),
|
||||
)
|
||||
}};
|
||||
}
|
||||
|
||||
|
||||
+51
-27
@@ -104,7 +104,12 @@ pub fn channel<T>() -> (Sender<T>, Receiver<T>) {
|
||||
senders: 1,
|
||||
receiver_alive: true,
|
||||
}));
|
||||
(Sender { inner: inner.clone() }, Receiver { inner })
|
||||
(
|
||||
Sender {
|
||||
inner: inner.clone(),
|
||||
},
|
||||
Receiver { inner },
|
||||
)
|
||||
}
|
||||
|
||||
struct Inner<T> {
|
||||
@@ -178,7 +183,9 @@ impl std::error::Error for RecvTimeoutError {}
|
||||
impl<T> Clone for Sender<T> {
|
||||
fn clone(&self) -> Self {
|
||||
self.inner.lock().senders += 1;
|
||||
Sender { inner: self.inner.clone() }
|
||||
Sender {
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,10 +255,18 @@ impl<T> Sender<T> {
|
||||
g.parked_receiver.take()
|
||||
};
|
||||
if let Some((pid, epoch)) = unpark {
|
||||
crate::te!(crate::trace::Event::Send { sender: crate::actor::current_pid().unwrap_or(crate::pid::Pid::new(u32::MAX, u32::MAX)), receiver: Some(pid) });
|
||||
crate::te!(crate::trace::Event::Send {
|
||||
sender: crate::actor::current_pid()
|
||||
.unwrap_or(crate::pid::Pid::new(u32::MAX, u32::MAX)),
|
||||
receiver: Some(pid)
|
||||
});
|
||||
crate::scheduler::unpark_at(pid, epoch);
|
||||
} else {
|
||||
crate::te!(crate::trace::Event::Send { sender: crate::actor::current_pid().unwrap_or(crate::pid::Pid::new(u32::MAX, u32::MAX)), receiver: None });
|
||||
crate::te!(crate::trace::Event::Send {
|
||||
sender: crate::actor::current_pid()
|
||||
.unwrap_or(crate::pid::Pid::new(u32::MAX, u32::MAX)),
|
||||
receiver: None
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -290,10 +305,12 @@ impl<T> Receiver<T> {
|
||||
// Release the lock before parking: the unparker will need it.
|
||||
crate::scheduler::park_current();
|
||||
// Woken up. Record it before looping to check the queue.
|
||||
crate::te!(crate::trace::Event::RecvWake(match crate::actor::current_pid() {
|
||||
Some(p) => p,
|
||||
None => panic!("smarm: RecvWake outside an actor (core corrupt)"),
|
||||
}));
|
||||
crate::te!(crate::trace::Event::RecvWake(
|
||||
match crate::actor::current_pid() {
|
||||
Some(p) => p,
|
||||
None => panic!("smarm: RecvWake outside an actor (core corrupt)"),
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,10 +364,12 @@ impl<T> Receiver<T> {
|
||||
crate::scheduler::insert_wait_timer(deadline, me, target, epoch);
|
||||
|
||||
crate::scheduler::park_current();
|
||||
crate::te!(crate::trace::Event::RecvWake(match crate::actor::current_pid() {
|
||||
Some(p) => p,
|
||||
None => panic!("smarm: RecvWake outside an actor (core corrupt)"),
|
||||
}));
|
||||
crate::te!(crate::trace::Event::RecvWake(
|
||||
match crate::actor::current_pid() {
|
||||
Some(p) => p,
|
||||
None => panic!("smarm: RecvWake outside an actor (core corrupt)"),
|
||||
}
|
||||
));
|
||||
let mut g = self.inner.lock();
|
||||
if let Some(v) = g.queue.pop_front() {
|
||||
crate::preempt::note_message_received();
|
||||
@@ -412,10 +431,12 @@ impl<T> Receiver<T> {
|
||||
}
|
||||
// Release the lock before parking: the unparker will need it.
|
||||
crate::scheduler::park_current();
|
||||
crate::te!(crate::trace::Event::RecvWake(match crate::actor::current_pid() {
|
||||
Some(p) => p,
|
||||
None => panic!("smarm: RecvWake outside an actor (core corrupt)"),
|
||||
}));
|
||||
crate::te!(crate::trace::Event::RecvWake(
|
||||
match crate::actor::current_pid() {
|
||||
Some(p) => p,
|
||||
None => panic!("smarm: RecvWake outside an actor (core corrupt)"),
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -616,7 +637,12 @@ pub fn try_select(arms: &[&dyn Selectable]) -> std::io::Result<usize> {
|
||||
// Channel-only selects skip all of it: `eager` is false, the guard
|
||||
// is disarmed, and the loser-arm self-cleaning story is unchanged.
|
||||
let eager = arms.iter().any(|a| a.sel_eager_cleanup());
|
||||
let mut guard = UnregisterGuard { arms, me, epoch, armed: eager };
|
||||
let mut guard = UnregisterGuard {
|
||||
arms,
|
||||
me,
|
||||
epoch,
|
||||
armed: eager,
|
||||
};
|
||||
|
||||
crate::scheduler::park_current();
|
||||
|
||||
@@ -687,11 +713,7 @@ impl Drop for UnregisterGuard<'_> {
|
||||
// unregistered eagerly so none are left dangling. `Err` = an arm failed to
|
||||
// register; same unwind (earlier fd arms unregistered, wait retired).
|
||||
// `Ok(None)` = every arm registered successfully; the caller parks.
|
||||
fn register_arms(
|
||||
me: Pid,
|
||||
epoch: u32,
|
||||
arms: &[&dyn Selectable],
|
||||
) -> std::io::Result<Option<usize>> {
|
||||
fn register_arms(me: Pid, epoch: u32, arms: &[&dyn Selectable]) -> std::io::Result<Option<usize>> {
|
||||
for (i, arm) in arms.iter().enumerate() {
|
||||
let registered = match arm.sel_register(me, epoch) {
|
||||
Ok(r) => r,
|
||||
@@ -736,10 +758,7 @@ impl crate::timer::TimerTarget for SelectTimeout {
|
||||
/// Panics if `arms` is empty, if called outside an actor, or if an fd arm
|
||||
/// fails to register (see [`try_select_timeout`] for the fallible form; a
|
||||
/// channel-only select can never fail).
|
||||
pub fn select_timeout(
|
||||
arms: &[&dyn Selectable],
|
||||
timeout: std::time::Duration,
|
||||
) -> Option<usize> {
|
||||
pub fn select_timeout(arms: &[&dyn Selectable], timeout: std::time::Duration) -> Option<usize> {
|
||||
match try_select_timeout(arms, timeout) {
|
||||
Ok(r) => r,
|
||||
Err(e) => panic!(
|
||||
@@ -776,7 +795,12 @@ pub fn try_select_timeout(
|
||||
// would leave those fds unusable until a kernel event happened to
|
||||
// clear them.
|
||||
let eager = arms.iter().any(|a| a.sel_eager_cleanup());
|
||||
let mut guard = UnregisterGuard { arms, me, epoch, armed: eager };
|
||||
let mut guard = UnregisterGuard {
|
||||
arms,
|
||||
me,
|
||||
epoch,
|
||||
armed: eager,
|
||||
};
|
||||
|
||||
crate::scheduler::park_current();
|
||||
|
||||
|
||||
+26
-11
@@ -16,10 +16,18 @@ thread_local! {
|
||||
static ACTOR_SP: Cell<usize> = const { Cell::new(0) };
|
||||
}
|
||||
|
||||
fn get_scheduler_sp() -> usize { SCHEDULER_SP.with(|c| c.get()) }
|
||||
fn set_scheduler_sp(v: usize) { SCHEDULER_SP.with(|c| c.set(v)) }
|
||||
pub fn get_actor_sp() -> usize { ACTOR_SP.with(|c| c.get()) }
|
||||
pub fn set_actor_sp(v: usize) { ACTOR_SP.with(|c| c.set(v)) }
|
||||
fn get_scheduler_sp() -> usize {
|
||||
SCHEDULER_SP.with(|c| c.get())
|
||||
}
|
||||
fn set_scheduler_sp(v: usize) {
|
||||
SCHEDULER_SP.with(|c| c.set(v))
|
||||
}
|
||||
pub fn get_actor_sp() -> usize {
|
||||
ACTOR_SP.with(|c| c.get())
|
||||
}
|
||||
pub fn set_actor_sp(v: usize) {
|
||||
ACTOR_SP.with(|c| c.set(v))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Initial stack layout
|
||||
@@ -49,13 +57,20 @@ pub fn set_actor_sp(v: usize) { ACTOR_SP.with(|c| c.set(v)) }
|
||||
pub fn init_actor_stack(top: *mut u8, entry: extern "C-unwind" fn()) -> usize {
|
||||
unsafe {
|
||||
let mut sp = (top as usize & !15) - 8;
|
||||
sp -= 8; (sp as *mut usize).write(entry as usize); // ret target
|
||||
sp -= 8; (sp as *mut usize).write(0); // rbx
|
||||
sp -= 8; (sp as *mut usize).write(0); // rbp
|
||||
sp -= 8; (sp as *mut usize).write(0); // r12
|
||||
sp -= 8; (sp as *mut usize).write(0); // r13
|
||||
sp -= 8; (sp as *mut usize).write(0); // r14
|
||||
sp -= 8; (sp as *mut usize).write(0); // r15
|
||||
sp -= 8;
|
||||
(sp as *mut usize).write(entry as usize); // ret target
|
||||
sp -= 8;
|
||||
(sp as *mut usize).write(0); // rbx
|
||||
sp -= 8;
|
||||
(sp as *mut usize).write(0); // rbp
|
||||
sp -= 8;
|
||||
(sp as *mut usize).write(0); // r12
|
||||
sp -= 8;
|
||||
(sp as *mut usize).write(0); // r13
|
||||
sp -= 8;
|
||||
(sp as *mut usize).write(0); // r14
|
||||
sp -= 8;
|
||||
(sp as *mut usize).write(0); // r15
|
||||
sp
|
||||
}
|
||||
}
|
||||
|
||||
+78
-34
@@ -178,7 +178,9 @@
|
||||
//! from any handler via [`Watcher::watch`]) because monitors are inherently
|
||||
//! created at runtime. The idle window is set once, in `init`.
|
||||
|
||||
use crate::channel::{channel, select, select_timeout, Receiver, RecvTimeoutError, Selectable, Sender};
|
||||
use crate::channel::{
|
||||
channel, select, select_timeout, Receiver, RecvTimeoutError, Selectable, Sender,
|
||||
};
|
||||
use crate::monitor::{demonitor, monitor, Down, Monitor};
|
||||
use crate::pid::Pid;
|
||||
use crate::registry::{register_with, resolve_named_sender, RegisterError};
|
||||
@@ -273,7 +275,10 @@ pub struct GenServerRef<G: GenServer> {
|
||||
|
||||
impl<G: GenServer> Clone for GenServerRef<G> {
|
||||
fn clone(&self) -> Self {
|
||||
GenServerRef { tx: self.tx.clone(), pid: self.pid }
|
||||
GenServerRef {
|
||||
tx: self.tx.clone(),
|
||||
pid: self.pid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,7 +417,9 @@ impl<G: GenServer> GenServerCtx<G> {
|
||||
/// A clonable handle to the loop's monitor intake. Store it in the state
|
||||
/// during `init` to watch monitors from later handlers.
|
||||
pub fn watcher(&self) -> Watcher<G> {
|
||||
Watcher { tx: self.sys_tx.clone() }
|
||||
Watcher {
|
||||
tx: self.sys_tx.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Shorthand for `ctx.watcher().watch(m)` when watching during `init`.
|
||||
@@ -426,7 +433,10 @@ impl<G: GenServer> GenServerCtx<G> {
|
||||
/// [`tick_every`](TimerHandle::tick_every) /
|
||||
/// [`cancel`](TimerHandle::cancel) from any later handler.
|
||||
pub fn timer(&self) -> TimerHandle<G> {
|
||||
TimerHandle { sys_tx: self.sys_tx.clone(), reg: self.reg.clone() }
|
||||
TimerHandle {
|
||||
sys_tx: self.sys_tx.clone(),
|
||||
reg: self.reg.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a quiet-period window: if the loop goes `after` without dispatching
|
||||
@@ -518,7 +528,10 @@ pub struct TimerHandle<G: GenServer> {
|
||||
// Manual Clone for the same reason as `Watcher`: no `G: Clone` needed.
|
||||
impl<G: GenServer> Clone for TimerHandle<G> {
|
||||
fn clone(&self) -> Self {
|
||||
TimerHandle { sys_tx: self.sys_tx.clone(), reg: self.reg.clone() }
|
||||
TimerHandle {
|
||||
sys_tx: self.sys_tx.clone(),
|
||||
reg: self.reg.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -569,8 +582,18 @@ impl<G: GenServer> TimerHandle<G> {
|
||||
// First instance fires after `every`; the payload is produced loop-side
|
||||
// from `make` on fire, so the tick carries only the stable id.
|
||||
let sub = send_after_to(every, self.sys_tx.clone(), Sys::Tick(local));
|
||||
reg.periodics.insert(local, Periodic { every, live: sub, make });
|
||||
debug_assert!(reg.rearm_tx.is_some(), "rearm_tx must be Some while periodics is non-empty");
|
||||
reg.periodics.insert(
|
||||
local,
|
||||
Periodic {
|
||||
every,
|
||||
live: sub,
|
||||
make,
|
||||
},
|
||||
);
|
||||
debug_assert!(
|
||||
reg.rearm_tx.is_some(),
|
||||
"rearm_tx must be Some while periodics is non-empty"
|
||||
);
|
||||
local
|
||||
}
|
||||
|
||||
@@ -617,7 +640,9 @@ pub struct Watcher<G: GenServer> {
|
||||
// regardless of the server type (it clones only the inner sender).
|
||||
impl<G: GenServer> Clone for Watcher<G> {
|
||||
fn clone(&self) -> Self {
|
||||
Watcher { tx: self.tx.clone() }
|
||||
Watcher {
|
||||
tx: self.tx.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -691,7 +716,10 @@ impl<G: GenServer> GenServerBuilder<G> {
|
||||
/// live server). Consumes the builder, carrying its `with_info` / `under`
|
||||
/// configuration through.
|
||||
pub fn named(self, name: GenServerName<G>) -> NamedGenServerBuilder<G> {
|
||||
NamedGenServerBuilder { builder: self, name: name.as_str() }
|
||||
NamedGenServerBuilder {
|
||||
builder: self,
|
||||
name: name.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Private shared body behind [`start`](Self::start) and
|
||||
@@ -700,18 +728,24 @@ impl<G: GenServer> GenServerBuilder<G> {
|
||||
/// under the name before returning.
|
||||
fn spawn_server(self) -> GenServerRef<G> {
|
||||
let (tx, rx) = channel::<Envelope<G>>();
|
||||
let GenServerBuilder { state, infos, supervisor, stack_opts } = self;
|
||||
let GenServerBuilder {
|
||||
state,
|
||||
infos,
|
||||
supervisor,
|
||||
stack_opts,
|
||||
} = self;
|
||||
let handle = match supervisor {
|
||||
Some(sup) => {
|
||||
crate::scheduler::spawn_under_with(sup, stack_opts, move || {
|
||||
server_loop::<G>(rx, state, infos)
|
||||
})
|
||||
}
|
||||
None => crate::scheduler::spawn_with(stack_opts, move || {
|
||||
Some(sup) => crate::scheduler::spawn_under_with(sup, stack_opts, move || {
|
||||
server_loop::<G>(rx, state, infos)
|
||||
}),
|
||||
None => {
|
||||
crate::scheduler::spawn_with(stack_opts, move || server_loop::<G>(rx, state, infos))
|
||||
}
|
||||
};
|
||||
GenServerRef { tx, pid: handle.pid() }
|
||||
GenServerRef {
|
||||
tx,
|
||||
pid: handle.pid(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -739,7 +773,10 @@ impl<G> GenServerName<G> {
|
||||
/// associated constants at call sites.
|
||||
#[inline]
|
||||
pub const fn new(name: &'static str) -> Self {
|
||||
Self { name, _marker: PhantomData }
|
||||
Self {
|
||||
name,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// The underlying registry key.
|
||||
@@ -933,7 +970,11 @@ fn server_loop<G: GenServer>(
|
||||
// Bind the ctx so the idle window set during init can be read back, then
|
||||
// drop it — that drops the loop's own Sys sender, so a state that cloned no
|
||||
// Watcher/TimerHandle lets the arm auto-close (the unused-ctx behaviour).
|
||||
let ctx = GenServerCtx { sys_tx, reg: reg.clone(), idle: Cell::new(None) };
|
||||
let ctx = GenServerCtx {
|
||||
sys_tx,
|
||||
reg: reg.clone(),
|
||||
idle: Cell::new(None),
|
||||
};
|
||||
guard.0.init(&ctx);
|
||||
let idle = ctx.idle.get();
|
||||
drop(ctx);
|
||||
@@ -983,13 +1024,12 @@ fn server_loop<G: GenServer>(
|
||||
// inbox. The slice is rebuilt each iteration because the monitor
|
||||
// and info sets shrink/grow. Mirrors the fast-path inbox park
|
||||
// above; keep them in sync.
|
||||
let nd = monitors.len(); // monitor band: [0, nd)
|
||||
let nw = sys_open as usize; // system arm: [nd, nd+nw)
|
||||
// info band: [nd+nw, nd+nw+ni)
|
||||
// inbox arm: [nd+nw+ni]
|
||||
let nd = monitors.len(); // monitor band: [0, nd)
|
||||
let nw = sys_open as usize; // system arm: [nd, nd+nw)
|
||||
// info band: [nd+nw, nd+nw+ni)
|
||||
// inbox arm: [nd+nw+ni]
|
||||
let sel = {
|
||||
let mut arms: Vec<&dyn Selectable> =
|
||||
Vec::with_capacity(nd + nw + infos.len() + 1);
|
||||
let mut arms: Vec<&dyn Selectable> = Vec::with_capacity(nd + nw + infos.len() + 1);
|
||||
for m in &monitors {
|
||||
arms.push(&m.rx);
|
||||
}
|
||||
@@ -1001,9 +1041,7 @@ fn server_loop<G: GenServer>(
|
||||
}
|
||||
arms.push(&rx);
|
||||
match idle_deadline {
|
||||
Some(dl) => {
|
||||
select_timeout(&arms, dl.saturating_duration_since(Instant::now()))
|
||||
}
|
||||
Some(dl) => select_timeout(&arms, dl.saturating_duration_since(Instant::now())),
|
||||
None => Some(select(&arms)),
|
||||
}
|
||||
};
|
||||
@@ -1034,8 +1072,12 @@ fn server_loop<G: GenServer>(
|
||||
// live set tracks only still-pending timers, then
|
||||
// dispatch.
|
||||
match reg.lock() {
|
||||
Ok(mut g) => { g.oneshots.remove(&id); }
|
||||
Err(e) => panic!("smarm: gen_server reg lock poisoned (core corrupt): {e}"),
|
||||
Ok(mut g) => {
|
||||
g.oneshots.remove(&id);
|
||||
}
|
||||
Err(e) => {
|
||||
panic!("smarm: gen_server reg lock poisoned (core corrupt): {e}")
|
||||
}
|
||||
}
|
||||
guard.0.handle_timer(msg);
|
||||
reset_idle(&mut idle_deadline);
|
||||
@@ -1049,7 +1091,9 @@ fn server_loop<G: GenServer>(
|
||||
let msg = {
|
||||
let mut g = match reg.lock() {
|
||||
Ok(g) => g,
|
||||
Err(e) => panic!("smarm: gen_server reg lock poisoned (core corrupt): {e}"),
|
||||
Err(e) => panic!(
|
||||
"smarm: gen_server reg lock poisoned (core corrupt): {e}"
|
||||
),
|
||||
};
|
||||
let r = &mut *g;
|
||||
if let Some(p) = r.periodics.get_mut(&id) {
|
||||
@@ -1057,9 +1101,9 @@ fn server_loop<G: GenServer>(
|
||||
let msg = (p.make)();
|
||||
let tx = match r.rearm_tx.as_ref() {
|
||||
Some(tx) => tx.clone(),
|
||||
None => panic!(
|
||||
"smarm: live periodic without rearm_tx (logic bug)"
|
||||
),
|
||||
None => {
|
||||
panic!("smarm: live periodic without rearm_tx (logic bug)")
|
||||
}
|
||||
};
|
||||
p.live = send_after_to(every, tx, Sys::Tick(id));
|
||||
Some(msg)
|
||||
|
||||
+25
-10
@@ -219,7 +219,11 @@ struct Timers {
|
||||
|
||||
impl Timers {
|
||||
fn new() -> Self {
|
||||
Timers { next_local: 0, state: None, named: HashMap::new() }
|
||||
Timers {
|
||||
next_local: 0,
|
||||
state: None,
|
||||
named: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn mint(&mut self) -> u64 {
|
||||
@@ -248,7 +252,11 @@ pub struct Cx<Ev> {
|
||||
|
||||
impl<Ev> Cx<Ev> {
|
||||
fn new(sys_tx: Sender<Sys>, reg: Arc<Mutex<Timers>>) -> Self {
|
||||
Cx { sys_tx, reg, _ev: PhantomData }
|
||||
Cx {
|
||||
sys_tx,
|
||||
reg,
|
||||
_ev: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Arm the **state timeout**: fire a `state_timeout` event after `after` in
|
||||
@@ -387,7 +395,10 @@ pub struct GenStatemRef<M: Machine> {
|
||||
|
||||
impl<M: Machine> Clone for GenStatemRef<M> {
|
||||
fn clone(&self) -> Self {
|
||||
GenStatemRef { tx: self.tx.clone(), pid: self.pid }
|
||||
GenStatemRef {
|
||||
tx: self.tx.clone(),
|
||||
pid: self.pid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,13 +454,13 @@ pub fn spawn<M: Machine>(machine: M) -> GenStatemRef<M> {
|
||||
/// a `_with` variant like the scheduler's own spawns.
|
||||
///
|
||||
/// Panics if called outside `Runtime::run()`.
|
||||
pub fn spawn_with<M: Machine>(
|
||||
opts: crate::scheduler::SpawnOpts,
|
||||
machine: M,
|
||||
) -> GenStatemRef<M> {
|
||||
pub fn spawn_with<M: Machine>(opts: crate::scheduler::SpawnOpts, machine: M) -> GenStatemRef<M> {
|
||||
let (tx, rx) = channel::<M::Ev>();
|
||||
let handle = crate::scheduler::spawn_with(opts, move || statem_loop(rx, machine));
|
||||
GenStatemRef { tx, pid: handle.pid() }
|
||||
GenStatemRef {
|
||||
tx,
|
||||
pid: handle.pid(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The machine actor body: `on_start`, then one `handle` per event until the
|
||||
@@ -488,7 +499,9 @@ fn statem_loop<M: Machine>(rx: Receiver<M::Ev>, mut machine: M) {
|
||||
Sys::StateTimeout(local) => {
|
||||
let mut t = match reg.lock() {
|
||||
Ok(g) => g,
|
||||
Err(e) => panic!("smarm: gen_statem reg lock poisoned (core corrupt): {e}"),
|
||||
Err(e) => panic!(
|
||||
"smarm: gen_statem reg lock poisoned (core corrupt): {e}"
|
||||
),
|
||||
};
|
||||
match t.state {
|
||||
Some((live, _)) if live == local => {
|
||||
@@ -501,7 +514,9 @@ fn statem_loop<M: Machine>(rx: Receiver<M::Ev>, mut machine: M) {
|
||||
Sys::Timeout(name, local) => {
|
||||
let mut t = match reg.lock() {
|
||||
Ok(g) => g,
|
||||
Err(e) => panic!("smarm: gen_statem reg lock poisoned (core corrupt): {e}"),
|
||||
Err(e) => panic!(
|
||||
"smarm: gen_statem reg lock poisoned (core corrupt): {e}"
|
||||
),
|
||||
};
|
||||
match t.named.get(name) {
|
||||
Some(&(live, _)) if live == local => {
|
||||
|
||||
+17
-4
@@ -239,7 +239,10 @@ pub fn snapshot() -> RuntimeSnapshot {
|
||||
actors.push(info);
|
||||
}
|
||||
}
|
||||
RuntimeSnapshot { format_version: SNAPSHOT_FORMAT_VERSION, actors }
|
||||
RuntimeSnapshot {
|
||||
format_version: SNAPSHOT_FORMAT_VERSION,
|
||||
actors,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -387,7 +390,10 @@ pub fn tree() -> RuntimeTree {
|
||||
/// want to inspect again) and want the tree view of it without re-reading
|
||||
/// the runtime.
|
||||
pub fn tree_from(snap: RuntimeSnapshot) -> RuntimeTree {
|
||||
let RuntimeSnapshot { format_version, actors } = snap;
|
||||
let RuntimeSnapshot {
|
||||
format_version,
|
||||
actors,
|
||||
} = snap;
|
||||
|
||||
let mut index_of: HashMap<Pid, usize> = HashMap::with_capacity(actors.len());
|
||||
for (i, a) in actors.iter().enumerate() {
|
||||
@@ -418,7 +424,10 @@ pub fn tree_from(snap: RuntimeSnapshot) -> RuntimeTree {
|
||||
.into_iter()
|
||||
.filter_map(|i| build_node(i, &children_of, &orphaned, &mut slots))
|
||||
.collect();
|
||||
RuntimeTree { format_version, roots: root_nodes }
|
||||
RuntimeTree {
|
||||
format_version,
|
||||
roots: root_nodes,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_node(
|
||||
@@ -436,5 +445,9 @@ fn build_node(
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Some(TreeNode { info, orphaned: orphaned[i], children })
|
||||
Some(TreeNode {
|
||||
info,
|
||||
orphaned: orphaned[i],
|
||||
children,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -136,7 +136,6 @@ pub struct IoThread {
|
||||
waiters: Waiters,
|
||||
|
||||
// ----- Epoll machinery -----
|
||||
|
||||
/// The epollfd, owned by `IoThread`. Callable cross-thread via
|
||||
/// `epoll_ctl` per the man page.
|
||||
epollfd: RawFd,
|
||||
@@ -147,7 +146,6 @@ pub struct IoThread {
|
||||
shutdown_write: RawFd,
|
||||
|
||||
// ----- Threads -----
|
||||
|
||||
pool_thread: Option<OsJoinHandle<()>>,
|
||||
epoll_thread: Option<OsJoinHandle<()>>,
|
||||
}
|
||||
@@ -284,9 +282,8 @@ impl IoThread {
|
||||
events,
|
||||
u64: fd as u64,
|
||||
};
|
||||
let r = unsafe {
|
||||
libc::epoll_ctl(self.epollfd, libc::EPOLL_CTL_ADD, fd, &mut ev as *mut _)
|
||||
};
|
||||
let r =
|
||||
unsafe { libc::epoll_ctl(self.epollfd, libc::EPOLL_CTL_ADD, fd, &mut ev as *mut _) };
|
||||
if r < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
@@ -398,12 +395,7 @@ fn epoll_loop(epollfd: RawFd, waiters: Waiters, rt: Weak<RuntimeInner>) {
|
||||
|
||||
loop {
|
||||
let n = unsafe {
|
||||
libc::epoll_wait(
|
||||
epollfd,
|
||||
events.as_mut_ptr(),
|
||||
MAX_EVENTS as libc::c_int,
|
||||
-1,
|
||||
)
|
||||
libc::epoll_wait(epollfd, events.as_mut_ptr(), MAX_EVENTS as libc::c_int, -1)
|
||||
};
|
||||
|
||||
if n < 0 {
|
||||
@@ -438,12 +430,7 @@ fn epoll_loop(epollfd: RawFd, waiters: Waiters, rt: Weak<RuntimeInner>) {
|
||||
let entry = w.remove(&fd);
|
||||
if entry.is_some() {
|
||||
unsafe {
|
||||
libc::epoll_ctl(
|
||||
epollfd,
|
||||
libc::EPOLL_CTL_DEL,
|
||||
fd,
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
libc::epoll_ctl(epollfd, libc::EPOLL_CTL_DEL, fd, std::ptr::null_mut());
|
||||
}
|
||||
}
|
||||
entry
|
||||
|
||||
+38
-33
@@ -11,36 +11,36 @@
|
||||
//!
|
||||
//! See `LOOM.md` for the design intent and the deferred-for-later list.
|
||||
|
||||
pub mod stack;
|
||||
pub(crate) mod signal;
|
||||
pub mod context;
|
||||
pub mod preempt;
|
||||
pub mod pid;
|
||||
pub mod actor;
|
||||
pub mod causal;
|
||||
pub mod channel;
|
||||
pub mod scheduler;
|
||||
pub mod supervisor;
|
||||
pub mod timer;
|
||||
pub mod io;
|
||||
pub mod mutex;
|
||||
pub mod monitor;
|
||||
pub mod registry;
|
||||
pub mod pg;
|
||||
pub mod link;
|
||||
pub mod context;
|
||||
pub mod gen_server;
|
||||
pub mod gen_statem;
|
||||
pub mod introspect;
|
||||
pub mod io;
|
||||
pub mod link;
|
||||
pub mod monitor;
|
||||
pub mod mutex;
|
||||
#[cfg(feature = "observer")]
|
||||
pub mod observer;
|
||||
pub mod runtime;
|
||||
pub(crate) mod park;
|
||||
pub mod pg;
|
||||
pub mod pid;
|
||||
pub mod preempt;
|
||||
pub(crate) mod raw_mutex;
|
||||
pub(crate) mod slot_state;
|
||||
pub(crate) mod sync_shim;
|
||||
pub mod registry;
|
||||
#[doc(hidden)] // pub only so benches/rq_micro.rs can drive the raw structures
|
||||
pub mod run_queue;
|
||||
pub mod runtime;
|
||||
pub mod scheduler;
|
||||
pub(crate) mod signal;
|
||||
pub(crate) mod slot_state;
|
||||
pub mod stack;
|
||||
pub mod supervisor;
|
||||
pub(crate) mod sync_shim;
|
||||
pub mod timer;
|
||||
pub mod trace;
|
||||
pub mod causal;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Global allocator
|
||||
@@ -59,34 +59,39 @@ pub use channel::{
|
||||
};
|
||||
pub use gen_server::{
|
||||
call, cast, shutdown, whereis_server, CallError, CallTimeoutError, CastError, GenServer,
|
||||
NamedGenServerBuilder, GenServerBuilder, GenServerCtx, GenServerName, GenServerRef, TimerHandle, Watcher,
|
||||
GenServerBuilder, GenServerCtx, GenServerName, GenServerRef, NamedGenServerBuilder,
|
||||
TimerHandle, Watcher,
|
||||
};
|
||||
pub use gen_statem::{
|
||||
CallError as GenStatemCallError, Cx, Machine, Reply, Resolution, SendError as GenStatemSendError,
|
||||
GenStatemRef,
|
||||
CallError as GenStatemCallError, Cx, GenStatemRef, Machine, Reply, Resolution,
|
||||
SendError as GenStatemSendError,
|
||||
};
|
||||
pub use introspect::{StackInfo,
|
||||
pub use introspect::{
|
||||
actor_info, snapshot, tree, tree_from, ActorInfo, ActorState, RuntimeSnapshot, RuntimeTree,
|
||||
TreeNode, SNAPSHOT_FORMAT_VERSION,
|
||||
StackInfo, TreeNode, SNAPSHOT_FORMAT_VERSION,
|
||||
};
|
||||
pub use link::{link, trap_exit, unlink, ExitSignal};
|
||||
pub use monitor::{
|
||||
demonitor, mark_watchable, monitor, terminal_reason, Down, DownReason, Monitor, MonitorId,
|
||||
};
|
||||
pub use mutex::{LockTimeout, Mutex, MutexGuard};
|
||||
#[cfg(feature = "observer")]
|
||||
pub use observer::{ObserverReply, ObserverRequest};
|
||||
pub use link::{link, trap_exit, unlink, ExitSignal};
|
||||
pub use monitor::{demonitor, monitor, Down, DownReason, Monitor, MonitorId};
|
||||
pub use mutex::{LockTimeout, Mutex, MutexGuard};
|
||||
pub use pg::{
|
||||
dispatch, join, leave, members, members_as, pick, pick_as, Incarnation, Member, NodeId,
|
||||
};
|
||||
pub use pid::{Addressable, Erased, Name, Pid, RawPid};
|
||||
pub use pg::{dispatch, join, leave, members, members_as, pick, pick_as, Incarnation, Member, NodeId};
|
||||
pub use registry::{
|
||||
install, lookup_as, register, send, send_dyn, send_to, unregister, whereis, RegisterError,
|
||||
SendError,
|
||||
install, lookup_as, register, resolve_name, send, send_dyn, send_to, unregister, whereis,
|
||||
NameResolution, RegisterError, SendError,
|
||||
};
|
||||
pub use runtime::{init, Config, Runtime};
|
||||
pub use scheduler::{
|
||||
block_on_io, cancel_timer, request_stop, run, self_pid, send_after, send_after_named,
|
||||
send_after_named_wall, send_after_wall, sleep, sleep_wall,
|
||||
spawn, spawn_addr, spawn_addr_with, spawn_under, spawn_under_with, spawn_with,
|
||||
wait_readable, wait_readable_timeout, wait_writable,
|
||||
wait_writable_timeout, yield_now, FdArm, JoinError, JoinHandle, SpawnOpts,
|
||||
send_after_named_wall, send_after_wall, sleep, sleep_wall, spawn, spawn_addr, spawn_addr_with,
|
||||
spawn_under, spawn_under_with, spawn_with, try_spawn, try_spawn_under_with, wait_readable,
|
||||
wait_readable_timeout, wait_writable, wait_writable_timeout, yield_now, FdArm, JoinError,
|
||||
JoinHandle, SpawnError, SpawnOpts,
|
||||
};
|
||||
pub use supervisor::{ChildSpec, OneForOne, Restart, Signal, Strategy};
|
||||
pub use timer::TimerId;
|
||||
|
||||
+4
-1
@@ -157,7 +157,10 @@ pub fn link<A>(target: Pid<A>) {
|
||||
});
|
||||
match my_trap {
|
||||
Some(tx) => {
|
||||
let _ = tx.send(ExitSignal { from: target, reason: DownReason::NoProc });
|
||||
let _ = tx.send(ExitSignal {
|
||||
from: target,
|
||||
reason: DownReason::NoProc,
|
||||
});
|
||||
}
|
||||
None => request_stop(me),
|
||||
}
|
||||
|
||||
+62
-1
@@ -171,12 +171,73 @@ pub fn monitor<A>(target: Pid<A>) -> Monitor {
|
||||
});
|
||||
|
||||
if !registered {
|
||||
let _ = tx.send(Down { pid: target, reason: DownReason::NoProc });
|
||||
let _ = tx.send(Down {
|
||||
pid: target,
|
||||
reason: DownReason::NoProc,
|
||||
});
|
||||
}
|
||||
|
||||
Monitor { id, target, rx }
|
||||
}
|
||||
|
||||
/// Flag `target`'s tenancy as watchable: its death will stamp the slot's
|
||||
/// terminal record (see [`terminal_reason`]), exactly as registering a name
|
||||
/// does. The bridge calls this wherever a smarm pid is *encoded across the
|
||||
/// boundary* — a contract reply, an introspection listing — because BEAM can
|
||||
/// only watch pids it holds, and can only hold pids that crossed. Keeping the
|
||||
/// bit rare is what keeps the record alive: anonymous never-exported churn
|
||||
/// (holder threads, egress tasks) stays ineligible and cannot evict a
|
||||
/// watchable tenancy's record from a LIFO-recycled slot.
|
||||
///
|
||||
/// Generation-checked and live-screened: marking a pid whose tenancy already
|
||||
/// ended is a no-op — its record either exists (it was flagged before dying)
|
||||
/// or is honestly unknowable. Same `Runtime::run()` context contract as
|
||||
/// [`monitor`].
|
||||
pub fn mark_watchable<A>(target: Pid<A>) {
|
||||
let target = target.erase();
|
||||
with_runtime(|inner| {
|
||||
if let Some(slot) = inner.slot_at(target) {
|
||||
// Cold lock FIRST: finalize publishes Done and checks the
|
||||
// watchable bit under this same lock, so the mark either lands
|
||||
// before finalize reads it (the death stamps) or observes the
|
||||
// tenancy already dead (no-op). No lost-stamp window between an
|
||||
// unlocked liveness read and the flag set.
|
||||
let mut cold = slot.cold.lock();
|
||||
if slot.is_live_for(target) {
|
||||
cold.watchable = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// The terminal [`DownReason`] of the tenancy `target` names, if that tenancy
|
||||
/// ever registered a name and is the *most recent named* death of its slot:
|
||||
/// finalize stamps the slot with `(generation, reason)` for once-registered
|
||||
/// tenancies (anonymous green-thread churn does not stamp — nor evict), and
|
||||
/// the record survives reclaim and the next tenant's install, until the next
|
||||
/// *named* tenant of the slot itself dies. `None` means the pid never lived,
|
||||
/// is still alive, never held a name, or its record was overwritten by a
|
||||
/// later named tenancy's death — callers fall back to `NoProc` semantics.
|
||||
///
|
||||
/// This exists for watch-installers that raced their target's death (bridge
|
||||
/// soak signature 4): a `NoProc` observed at install time can be upgraded to
|
||||
/// the real reason while the record still matches, which is exactly what an
|
||||
/// install that had won the race would have delivered. It does NOT change
|
||||
/// [`monitor`]'s own semantics — monitoring a stale pid still queues `NoProc`,
|
||||
/// the same shape Erlang gives — the upgrade is the caller's deliberate act.
|
||||
/// Same context contract as [`monitor`]: must run inside `Runtime::run()`.
|
||||
pub fn terminal_reason<A>(target: Pid<A>) -> Option<DownReason> {
|
||||
let target = target.erase();
|
||||
with_runtime(|inner| {
|
||||
let slot = inner.slot_at(target)?;
|
||||
let cold = slot.cold.lock();
|
||||
match cold.terminal {
|
||||
Some((generation, reason)) if generation == target.generation() => Some(reason),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Cancel the monitor `m`. Returns `Some(id)` if a live registration was found
|
||||
/// and removed, so no `Down` will arrive on `m.rx` from here on. Returns
|
||||
/// `None` if there was nothing left to remove: the target had already gone
|
||||
|
||||
+29
-10
@@ -158,7 +158,11 @@ impl TimerTarget for MutexCore {
|
||||
if st.holder == Some(pid) {
|
||||
return;
|
||||
}
|
||||
match st.waiters.iter().position(|w| w.pid == pid && w.epoch == epoch) {
|
||||
match st
|
||||
.waiters
|
||||
.iter()
|
||||
.position(|w| w.pid == pid && w.epoch == epoch)
|
||||
{
|
||||
Some(pos) => {
|
||||
st.waiters.remove(pos);
|
||||
true
|
||||
@@ -246,7 +250,10 @@ impl<T> Mutex<T> {
|
||||
Some(v) => v,
|
||||
None => panic!("smarm: Mutex value missing on free fast path (core corrupt)"),
|
||||
};
|
||||
return Ok(MutexGuard { mutex: self, value: Some(value) });
|
||||
return Ok(MutexGuard {
|
||||
mutex: self,
|
||||
value: Some(value),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +294,10 @@ impl<T> Mutex<T> {
|
||||
Some(v) => v,
|
||||
None => panic!("smarm: Mutex value missing after grant (core corrupt)"),
|
||||
};
|
||||
Ok(MutexGuard { mutex: self, value: Some(value) })
|
||||
Ok(MutexGuard {
|
||||
mutex: self,
|
||||
value: Some(value),
|
||||
})
|
||||
} else {
|
||||
Err(LockTimeout)
|
||||
}
|
||||
@@ -315,7 +325,10 @@ impl<T> Mutex<T> {
|
||||
Some(v) => v,
|
||||
None => panic!("smarm: Mutex value missing on try_lock free path (core corrupt)"),
|
||||
};
|
||||
Some(MutexGuard { mutex: self, value: Some(value) })
|
||||
Some(MutexGuard {
|
||||
mutex: self,
|
||||
value: Some(value),
|
||||
})
|
||||
}
|
||||
|
||||
/// Blocking fallback used when called outside the smarm runtime.
|
||||
@@ -329,10 +342,15 @@ impl<T> Mutex<T> {
|
||||
Ok(mut g) => g.take(),
|
||||
Err(e) => panic!("smarm: mutex value lock poisoned (core corrupt): {e}"),
|
||||
};
|
||||
if let Some(v) = v { break v; }
|
||||
if let Some(v) = v {
|
||||
break v;
|
||||
}
|
||||
std::thread::yield_now();
|
||||
};
|
||||
Ok(MutexGuard { mutex: self, value: Some(value) })
|
||||
Ok(MutexGuard {
|
||||
mutex: self,
|
||||
value: Some(value),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,7 +360,10 @@ impl<T> Clone for Mutex<T> {
|
||||
/// lock and one protected value; locking through any clone excludes
|
||||
/// every other clone.
|
||||
fn clone(&self) -> Self {
|
||||
Self { core: self.core.clone(), value: self.value.clone() }
|
||||
Self {
|
||||
core: self.core.clone(),
|
||||
value: self.value.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,9 +409,7 @@ impl<T: std::fmt::Debug> std::fmt::Debug for MutexGuard<'_, T> {
|
||||
Some(v) => v,
|
||||
None => panic!("smarm: MutexGuard value missing (core corrupt)"),
|
||||
};
|
||||
f.debug_tuple("MutexGuard")
|
||||
.field(value)
|
||||
.finish()
|
||||
f.debug_tuple("MutexGuard").field(value).finish()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+38
-15
@@ -105,7 +105,9 @@ mod parker {
|
||||
|
||||
impl Parker {
|
||||
pub(super) fn new() -> Self {
|
||||
Self { state: AtomicU32::new(EMPTY) }
|
||||
Self {
|
||||
state: AtomicU32::new(EMPTY),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` = woken (permit consumed), `false` = timed out.
|
||||
@@ -211,7 +213,10 @@ mod parker {
|
||||
|
||||
impl Parker {
|
||||
pub(super) fn new() -> Self {
|
||||
Self { permit: Mutex::new(false), cv: Condvar::new() }
|
||||
Self {
|
||||
permit: Mutex::new(false),
|
||||
cv: Condvar::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` = woken (permit consumed), `false` = timed out.
|
||||
@@ -395,12 +400,10 @@ impl Coordinator {
|
||||
// The CAS is the exactly-one guarantee: whoever clears the bit
|
||||
// owns the wake; a racing wake_one retries on the observed value
|
||||
// (coherence: a failed CAS can never read older than `mask`).
|
||||
match self.idle.compare_exchange(
|
||||
mask,
|
||||
mask & !bit,
|
||||
Ordering::AcqRel,
|
||||
Ordering::Acquire,
|
||||
) {
|
||||
match self
|
||||
.idle
|
||||
.compare_exchange(mask, mask & !bit, Ordering::AcqRel, Ordering::Acquire)
|
||||
{
|
||||
Ok(_) => {
|
||||
self.parkers[id].unpark();
|
||||
return true;
|
||||
@@ -467,7 +470,8 @@ impl Coordinator {
|
||||
// with the deadline still NO_DEADLINE compares `new < MAX` = true
|
||||
// and over-wakes — the benign direction. (Under the mandated timer
|
||||
// serialization this interleaving cannot occur anyway.)
|
||||
self.tk_armed.store(self.deadline_nanos(deadline), Ordering::SeqCst);
|
||||
self.tk_armed
|
||||
.store(self.deadline_nanos(deadline), Ordering::SeqCst);
|
||||
true
|
||||
}
|
||||
|
||||
@@ -578,7 +582,10 @@ mod tests {
|
||||
let t0 = Instant::now();
|
||||
let r = c.park(0, None, || false);
|
||||
assert_eq!(r, ParkResult::Woken);
|
||||
assert!(t0.elapsed() < Duration::from_millis(100), "park blocked despite permit");
|
||||
assert!(
|
||||
t0.elapsed() < Duration::from_millis(100),
|
||||
"park blocked despite permit"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -632,14 +639,20 @@ mod tests {
|
||||
// Wait until all four are published idle.
|
||||
let t0 = Instant::now();
|
||||
while c.idle_mask().count_ones() != N as u32 {
|
||||
assert!(t0.elapsed() < Duration::from_secs(5), "threads never parked");
|
||||
assert!(
|
||||
t0.elapsed() < Duration::from_secs(5),
|
||||
"threads never parked"
|
||||
);
|
||||
std::thread::yield_now();
|
||||
}
|
||||
assert!(c.wake_one());
|
||||
// Exactly one wakes; give the others a beat to (incorrectly) wake.
|
||||
let t0 = Instant::now();
|
||||
while woken.load(O::SeqCst) == 0 {
|
||||
assert!(t0.elapsed() < Duration::from_secs(5), "wake_one woke nobody");
|
||||
assert!(
|
||||
t0.elapsed() < Duration::from_secs(5),
|
||||
"wake_one woke nobody"
|
||||
);
|
||||
std::thread::yield_now();
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
@@ -706,7 +719,10 @@ mod tests {
|
||||
assert_eq!(c.armed_deadline_nanos(), c.deadline_nanos(d2));
|
||||
c.disarm_timer(0);
|
||||
assert_eq!(c.armed_deadline_nanos(), NO_DEADLINE);
|
||||
assert!(c.try_arm_timer(1, d1), "role must be re-takeable after disarm");
|
||||
assert!(
|
||||
c.try_arm_timer(1, d1),
|
||||
"role must be re-takeable after disarm"
|
||||
);
|
||||
c.disarm_timer(1);
|
||||
}
|
||||
|
||||
@@ -721,7 +737,10 @@ mod tests {
|
||||
let t0 = Instant::now();
|
||||
let r = c.park(0, Some(far), || false);
|
||||
assert_eq!(r, ParkResult::Woken, "re-arm wake lost");
|
||||
assert!(t0.elapsed() < Duration::from_secs(5), "slept toward the stale deadline");
|
||||
assert!(
|
||||
t0.elapsed() < Duration::from_secs(5),
|
||||
"slept toward the stale deadline"
|
||||
);
|
||||
c.disarm_timer(0);
|
||||
}
|
||||
|
||||
@@ -796,7 +815,11 @@ mod tests {
|
||||
assert!(c.try_arm_timer(0, far));
|
||||
c.note_deadline(near);
|
||||
let r = c.park(0, Some(far), || false);
|
||||
assert_eq!(r, ParkResult::Woken, "re-arm wake lost through note_deadline");
|
||||
assert_eq!(
|
||||
r,
|
||||
ParkResult::Woken,
|
||||
"re-arm wake lost through note_deadline"
|
||||
);
|
||||
c.disarm_timer(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +221,9 @@ pub(crate) struct ProcessGroups {
|
||||
|
||||
impl ProcessGroups {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { groups: HashMap::new() }
|
||||
Self {
|
||||
groups: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert `ms` into `group`. Idempotent on the *member*: if the member is
|
||||
@@ -323,20 +325,33 @@ impl ProcessGroups {
|
||||
fn members_where(&self, group: &str, mut is_live: impl FnMut(Pid) -> bool) -> Vec<Pid> {
|
||||
self.groups
|
||||
.get(group)
|
||||
.map(|v| v.iter().map(|e| e.member.pid).filter(|&p| is_live(p)).collect())
|
||||
.map(|v| {
|
||||
v.iter()
|
||||
.map(|e| e.member.pid)
|
||||
.filter(|&p| is_live(p))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The first live member of `group` in insertion order — stateless
|
||||
/// first-live `pick`, with the same read-path backstop as `members_where`.
|
||||
fn first_member_where(&self, group: &str, mut is_live: impl FnMut(Pid) -> bool) -> Option<Pid> {
|
||||
self.groups.get(group)?.iter().map(|e| e.member.pid).find(|&p| is_live(p))
|
||||
self.groups
|
||||
.get(group)?
|
||||
.iter()
|
||||
.map(|e| e.member.pid)
|
||||
.find(|&p| is_live(p))
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the full member identity for `pid` from runtime identity.
|
||||
fn member_for(inner: &crate::runtime::RuntimeInner, pid: Pid) -> Member {
|
||||
Member { node: inner.node_id, incarnation: inner.incarnation, pid }
|
||||
Member {
|
||||
node: inner.node_id,
|
||||
incarnation: inner.incarnation,
|
||||
pid,
|
||||
}
|
||||
}
|
||||
|
||||
/// Is `pid` a live actor right now? Generation-checked atomic slot-word read,
|
||||
@@ -367,7 +382,10 @@ pub fn join<A>(group: impl Into<String>, pid: Pid<A>) -> bool {
|
||||
let mon = monitor(pid);
|
||||
|
||||
let (rejected, reaped) = with_runtime(|inner| {
|
||||
let ms = Membership { member: member_for(inner, pid), monitor: mon };
|
||||
let ms = Membership {
|
||||
member: member_for(inner, pid),
|
||||
monitor: mon,
|
||||
};
|
||||
let mut pg = inner.process_groups.lock();
|
||||
let reaped = pg.reap_group(&group);
|
||||
let rejected = pg.join(&group, ms);
|
||||
@@ -507,7 +525,11 @@ mod tests {
|
||||
let (tx, rx) = channel::<Down>();
|
||||
let ms = Membership {
|
||||
member: member(index, generation),
|
||||
monitor: Monitor { id: MonitorId(0), target: pid, rx },
|
||||
monitor: Monitor {
|
||||
id: MonitorId(0),
|
||||
target: pid,
|
||||
rx,
|
||||
},
|
||||
};
|
||||
(ms, tx)
|
||||
}
|
||||
@@ -518,7 +540,10 @@ mod tests {
|
||||
let (a, _ta) = synth(1, 0);
|
||||
let (b, _tb) = synth(1, 0);
|
||||
assert!(pg.join("workers", a).is_none(), "first join inserts");
|
||||
assert!(pg.join("workers", b).is_some(), "second identical join is handed back");
|
||||
assert!(
|
||||
pg.join("workers", b).is_some(),
|
||||
"second identical join is handed back"
|
||||
);
|
||||
assert_eq!(pg.members_of("workers"), vec![member(1, 0)]);
|
||||
}
|
||||
|
||||
@@ -542,7 +567,10 @@ mod tests {
|
||||
let (a, _ta) = synth(1, 0);
|
||||
let (b, _tb) = synth(1, 1);
|
||||
assert!(pg.join("g", a).is_none());
|
||||
assert!(pg.join("g", b).is_none(), "different generation is a distinct member");
|
||||
assert!(
|
||||
pg.join("g", b).is_none(),
|
||||
"different generation is a distinct member"
|
||||
);
|
||||
assert_eq!(pg.members_of("g"), vec![member(1, 0), member(1, 1)]);
|
||||
}
|
||||
|
||||
@@ -555,16 +583,27 @@ mod tests {
|
||||
pg.join("g", b);
|
||||
assert!(pg.leave("g", member(1, 0)).is_some());
|
||||
assert_eq!(pg.members_of("g"), vec![member(2, 0)]);
|
||||
assert!(pg.leave("g", member(1, 0)).is_none(), "second leave finds nothing");
|
||||
assert!(
|
||||
pg.leave("g", member(1, 0)).is_none(),
|
||||
"second leave finds nothing"
|
||||
);
|
||||
assert!(pg.leave("g", member(2, 0)).is_some());
|
||||
assert!(pg.members_of("g").is_empty(), "group is now empty");
|
||||
assert!(pg.leave("never", member(9, 0)).is_none(), "leaving an unknown group is a no-op");
|
||||
assert!(
|
||||
pg.leave("never", member(9, 0)).is_none(),
|
||||
"leaving an unknown group is a no-op"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_where_sweeps_every_group() {
|
||||
let mut pg = ProcessGroups::new();
|
||||
for (g, (m, _t)) in [("a", synth(1, 0)), ("a", synth(2, 0)), ("b", synth(1, 0)), ("c", synth(3, 0))] {
|
||||
for (g, (m, _t)) in [
|
||||
("a", synth(1, 0)),
|
||||
("a", synth(2, 0)),
|
||||
("b", synth(1, 0)),
|
||||
("c", synth(3, 0)),
|
||||
] {
|
||||
pg.join(g, m);
|
||||
}
|
||||
// Death of pid index 1 (any generation) evicts it everywhere.
|
||||
@@ -582,8 +621,16 @@ mod tests {
|
||||
let pid = Pid::new(1, 0);
|
||||
let (tx, rx) = channel::<Down>();
|
||||
let dead = Membership {
|
||||
member: Member { node: DEFAULT_NODE_ID, incarnation: Incarnation::new(7), pid },
|
||||
monitor: Monitor { id: MonitorId(0), target: pid, rx },
|
||||
member: Member {
|
||||
node: DEFAULT_NODE_ID,
|
||||
incarnation: Incarnation::new(7),
|
||||
pid,
|
||||
},
|
||||
monitor: Monitor {
|
||||
id: MonitorId(0),
|
||||
target: pid,
|
||||
rx,
|
||||
},
|
||||
};
|
||||
let _keep = tx;
|
||||
let (live, _tl) = synth(2, 0);
|
||||
@@ -614,9 +661,17 @@ mod tests {
|
||||
pg.join("b", b1);
|
||||
// pid 1 dies: its group-a monitor receives a Down. Its group-b monitor
|
||||
// has not — reap must still sweep pid 1 out of b by the pid predicate.
|
||||
ta1.send(Down { pid: Pid::new(1, 0), reason: DownReason::Exit }).unwrap();
|
||||
ta1.send(Down {
|
||||
pid: Pid::new(1, 0),
|
||||
reason: DownReason::Exit,
|
||||
})
|
||||
.unwrap();
|
||||
let evicted = pg.reap_group("a");
|
||||
assert_eq!(evicted.len(), 2, "pid 1's memberships in both a and b are evicted");
|
||||
assert_eq!(
|
||||
evicted.len(),
|
||||
2,
|
||||
"pid 1's memberships in both a and b are evicted"
|
||||
);
|
||||
assert_eq!(pg.members_of("a"), vec![member(2, 0)]);
|
||||
assert!(pg.members_of("b").is_empty(), "swept from b too; pruned");
|
||||
}
|
||||
@@ -646,8 +701,16 @@ mod tests {
|
||||
let dead = Pid::new(1, 0);
|
||||
let oracle = |pid: Pid| pid != dead;
|
||||
|
||||
assert_eq!(pg.members_where("g", oracle), vec![Pid::new(2, 0)], "dead pid filtered from read");
|
||||
assert_eq!(pg.first_member_where("g", oracle), Some(Pid::new(2, 0)), "pick skips the dead first member");
|
||||
assert_eq!(
|
||||
pg.members_where("g", oracle),
|
||||
vec![Pid::new(2, 0)],
|
||||
"dead pid filtered from read"
|
||||
);
|
||||
assert_eq!(
|
||||
pg.first_member_where("g", oracle),
|
||||
Some(Pid::new(2, 0)),
|
||||
"pick skips the dead first member"
|
||||
);
|
||||
|
||||
// Backstop does not evict — that stays the monitor's job; raw storage
|
||||
// still holds both until reap runs.
|
||||
|
||||
+12
-3
@@ -79,7 +79,10 @@ impl Pid<Erased> {
|
||||
/// here; typing happens at typed-actor boundaries via [`Pid::from_raw`].
|
||||
#[inline]
|
||||
pub const fn new(index: u32, generation: u32) -> Self {
|
||||
Self { raw: RawPid::new(index, generation), _marker: PhantomData }
|
||||
Self {
|
||||
raw: RawPid::new(index, generation),
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +93,10 @@ impl<A> Pid<A> {
|
||||
/// resolution paths.
|
||||
#[inline]
|
||||
pub(crate) const fn from_raw(raw: RawPid) -> Self {
|
||||
Self { raw, _marker: PhantomData }
|
||||
Self {
|
||||
raw,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// The raw identity, dropping the actor type — the key for identity-only
|
||||
@@ -192,7 +198,10 @@ impl<M> Name<M> {
|
||||
/// associated constants at call sites.
|
||||
#[inline]
|
||||
pub const fn new(name: &'static str) -> Self {
|
||||
Self { name, _marker: PhantomData }
|
||||
Self {
|
||||
name,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// The underlying registry key.
|
||||
|
||||
+4
-1
@@ -166,7 +166,10 @@ impl<T> RawMutex<T> {
|
||||
{
|
||||
self.lock_slow();
|
||||
}
|
||||
RawMutexGuard { m: self, prev_preempt }
|
||||
RawMutexGuard {
|
||||
m: self,
|
||||
prev_preempt,
|
||||
}
|
||||
}
|
||||
|
||||
#[cold]
|
||||
|
||||
+107
-15
@@ -1,4 +1,3 @@
|
||||
|
||||
//! Give an actor a name so other actors can find it and message it.
|
||||
//!
|
||||
//! Without the registry, the only way to reach an actor is to already be
|
||||
@@ -196,7 +195,9 @@ impl<M> std::fmt::Display for SendError<M> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SendError::Unresolved(_) => write!(f, "no live actor registered under that name"),
|
||||
SendError::Dead(_) => write!(f, "the addressed actor is no longer the live incarnation"),
|
||||
SendError::Dead(_) => {
|
||||
write!(f, "the addressed actor is no longer the live incarnation")
|
||||
}
|
||||
SendError::NoChannel(_) => write!(f, "actor has no channel for this message type"),
|
||||
SendError::Closed(_) => write!(f, "the actor's channel for this type is closed"),
|
||||
SendError::NoMember(_) => write!(f, "no live member in the process group"),
|
||||
@@ -243,7 +244,10 @@ struct Mailbox {
|
||||
|
||||
impl Mailbox {
|
||||
fn new(pid: Pid) -> Self {
|
||||
Self { pid, channels: HashMap::new() }
|
||||
Self {
|
||||
pid,
|
||||
channels: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Clone the `Sender<M>` for this actor, if it has one. Called **under the
|
||||
@@ -294,7 +298,10 @@ pub(crate) struct Registry {
|
||||
|
||||
impl Registry {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { by_index: HashMap::new(), by_name: HashMap::new() }
|
||||
Self {
|
||||
by_index: HashMap::new(),
|
||||
by_name: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Drop a dead holder's artifacts: every name bound to it, and its
|
||||
@@ -303,7 +310,11 @@ impl Registry {
|
||||
/// wholesale on pid mismatch) and is left untouched.
|
||||
fn prune_holder(&mut self, holder: Pid) {
|
||||
self.by_name.retain(|_, p| *p != holder);
|
||||
if self.by_index.get(&holder.index()).is_some_and(|mb| mb.pid == holder) {
|
||||
if self
|
||||
.by_index
|
||||
.get(&holder.index())
|
||||
.is_some_and(|mb| mb.pid == holder)
|
||||
{
|
||||
self.by_index.remove(&holder.index());
|
||||
}
|
||||
}
|
||||
@@ -351,7 +362,11 @@ impl Registry {
|
||||
.iter()
|
||||
.filter_map(|(&n, &p)| (p == mb.pid).then_some(n))
|
||||
.collect();
|
||||
Some(MailboxInfo { pid: mb.pid, names, depth: depth.min(u32::MAX as usize) as u32 })
|
||||
Some(MailboxInfo {
|
||||
pid: mb.pid,
|
||||
names,
|
||||
depth: depth.min(u32::MAX as usize) as u32,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,6 +403,16 @@ pub(crate) fn register_with<M: Send + 'static>(
|
||||
tx: Sender<M>,
|
||||
) -> Result<(), RegisterError> {
|
||||
with_runtime(|inner| {
|
||||
// Stamp-eligibility for the terminal record (soak sig 4): flag the
|
||||
// tenancy BEFORE the binding lands and outside the registry lock (no
|
||||
// nesting), so no successfully-registered actor can die unflagged.
|
||||
// A register that then fails leaves a harmless overshoot; a stale
|
||||
// `me` is screened by the same live() the binding requires below.
|
||||
if live(inner, me) {
|
||||
if let Some(slot) = inner.slot_at(me) {
|
||||
slot.cold.lock().watchable = true;
|
||||
}
|
||||
}
|
||||
let mut reg = inner.registry.lock();
|
||||
if !live(inner, me) {
|
||||
return Err(RegisterError::NoProc);
|
||||
@@ -418,13 +443,19 @@ pub(crate) fn register_with<M: Send + 'static>(
|
||||
/// index from a dead prior incarnation (pid mismatch) is replaced wholesale.
|
||||
/// Caller holds the registry lock and has established that `me` is live.
|
||||
fn publish_channel<M: Send + 'static>(reg: &mut Registry, me: Pid, tx: Sender<M>) {
|
||||
let mb = reg.by_index.entry(me.index()).or_insert_with(|| Mailbox::new(me));
|
||||
let mb = reg
|
||||
.by_index
|
||||
.entry(me.index())
|
||||
.or_insert_with(|| Mailbox::new(me));
|
||||
if mb.pid != me {
|
||||
*mb = Mailbox::new(me);
|
||||
}
|
||||
mb.channels.insert(
|
||||
TypeId::of::<M>(),
|
||||
Channel { sender: Box::new(tx), msg_type: type_name::<M>() },
|
||||
Channel {
|
||||
sender: Box::new(tx),
|
||||
msg_type: type_name::<M>(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -463,7 +494,10 @@ pub fn install<A: Addressable>(tx: Sender<A::Msg>) -> Pid<A> {
|
||||
pub(crate) fn install_for<M: Send + 'static>(pid: Pid, tx: Sender<M>) {
|
||||
with_runtime(|inner| {
|
||||
let mut reg = inner.registry.lock();
|
||||
debug_assert!(live(inner, pid), "install_for: pid must be a freshly spawned, live actor");
|
||||
debug_assert!(
|
||||
live(inner, pid),
|
||||
"install_for: pid must be a freshly spawned, live actor"
|
||||
);
|
||||
publish_channel::<M>(&mut reg, pid, tx);
|
||||
});
|
||||
}
|
||||
@@ -486,6 +520,47 @@ pub fn whereis(name: &str) -> Option<Pid> {
|
||||
})
|
||||
}
|
||||
|
||||
/// What a name is bound to, three-valued (bridge soak signature 4).
|
||||
///
|
||||
/// [`Live`](NameResolution::Live) is [`whereis`]'s `Some`.
|
||||
/// [`Corpse`](NameResolution::Corpse) carries the *stored* holder pid of a
|
||||
/// dead-but-unpruned binding — a state Erlang cannot represent (its name
|
||||
/// death unregisters atomically; smarm's prune is lazy), captured here before
|
||||
/// the prune that `whereis` performs discards it, so the caller can consult
|
||||
/// [`terminal_reason`](crate::monitor::terminal_reason) for the tenancy's
|
||||
/// real down reason. [`Unbound`](NameResolution::Unbound) matches Erlang's
|
||||
/// unregistered name.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum NameResolution {
|
||||
/// The stored holder is live (generation-checked); the binding stands.
|
||||
Live(Pid),
|
||||
/// The stored holder is dead. The binding was pruned on the way out —
|
||||
/// the name heals exactly as `whereis` heals it; only the evidence is
|
||||
/// returned instead of discarded. A second resolve is `Unbound`.
|
||||
Corpse(Pid),
|
||||
/// No binding stored (never registered, or already pruned by any reader).
|
||||
Unbound,
|
||||
}
|
||||
|
||||
/// Resolve `name` like [`whereis`], but keep the corpse: the dead-holder arm
|
||||
/// returns the stored pid it pruned instead of a bare `None`. Same lock
|
||||
/// discipline and pruning behavior as `whereis`; same `Runtime::run()`
|
||||
/// context contract.
|
||||
pub fn resolve_name(name: &str) -> NameResolution {
|
||||
with_runtime(|inner| {
|
||||
let mut reg = inner.registry.lock();
|
||||
let Some(&pid) = reg.by_name.get(name) else {
|
||||
return NameResolution::Unbound;
|
||||
};
|
||||
if live(inner, pid) {
|
||||
NameResolution::Live(pid)
|
||||
} else {
|
||||
reg.prune_holder(pid);
|
||||
NameResolution::Corpse(pid)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Like [`whereis`], but returns a *typed* [`Pid<A>`] instead of a bare
|
||||
/// [`Pid`], so a follow-up [`send_to`] is compile-checked instead of needing
|
||||
/// the untyped [`send_dyn`] escape hatch. `None` if the name is unbound or its
|
||||
@@ -522,7 +597,10 @@ pub(crate) fn resolve_named_sender<M: Send + 'static>(name: &str) -> Option<(Pid
|
||||
}
|
||||
// A live holder's mailbox is its own (publish replaces wholesale on
|
||||
// pid mismatch, and one live actor per slot), so index lookup is safe.
|
||||
let tx = reg.by_index.get(&pid.index()).and_then(Mailbox::clone_sender::<M>)?;
|
||||
let tx = reg
|
||||
.by_index
|
||||
.get(&pid.index())
|
||||
.and_then(Mailbox::clone_sender::<M>)?;
|
||||
Some((pid, tx))
|
||||
})
|
||||
}
|
||||
@@ -535,7 +613,11 @@ pub fn unregister(name: &str) -> Option<Pid> {
|
||||
with_runtime(|inner| {
|
||||
let mut reg = inner.registry.lock();
|
||||
let pid = reg.by_name.remove(name)?;
|
||||
if live(inner, pid) { Some(pid) } else { None }
|
||||
if live(inner, pid) {
|
||||
Some(pid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -567,12 +649,17 @@ pub fn send<M: Send + 'static>(name: Name<M>, msg: M) -> Result<(), SendError<M>
|
||||
reg.prune_holder(pid);
|
||||
return Err(SendError::Unresolved(msg));
|
||||
}
|
||||
match reg.by_index.get(&pid.index()).and_then(Mailbox::clone_sender::<M>) {
|
||||
match reg
|
||||
.by_index
|
||||
.get(&pid.index())
|
||||
.and_then(Mailbox::clone_sender::<M>)
|
||||
{
|
||||
Some(tx) => tx,
|
||||
None => return Err(SendError::NoChannel(msg)),
|
||||
}
|
||||
};
|
||||
tx.send(msg).map_err(|crate::channel::SendError(m)| SendError::Closed(m))
|
||||
tx.send(msg)
|
||||
.map_err(|crate::channel::SendError(m)| SendError::Closed(m))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -596,7 +683,11 @@ fn send_to_pid<M: Send + 'static>(
|
||||
match reg.by_index.get(&pid.index()).map(|m| m.pid) {
|
||||
// Exact incarnation, still alive: its `M` channel, or NoChannel.
|
||||
Some(stored) if stored == pid && live(inner, pid) => {
|
||||
match reg.by_index.get(&pid.index()).and_then(Mailbox::clone_sender::<M>) {
|
||||
match reg
|
||||
.by_index
|
||||
.get(&pid.index())
|
||||
.and_then(Mailbox::clone_sender::<M>)
|
||||
{
|
||||
Some(tx) => tx,
|
||||
None => return Err(SendError::NoChannel(msg)),
|
||||
}
|
||||
@@ -611,7 +702,8 @@ fn send_to_pid<M: Send + 'static>(
|
||||
_ => return Err(SendError::Dead(msg)),
|
||||
}
|
||||
};
|
||||
tx.send(msg).map_err(|crate::channel::SendError(m)| SendError::Closed(m))
|
||||
tx.send(msg)
|
||||
.map_err(|crate::channel::SendError(m)| SendError::Closed(m))
|
||||
}
|
||||
|
||||
/// Deliver `msg` directly to the exact actor identified by `pid`. Unlike
|
||||
|
||||
+28
-5
@@ -222,7 +222,10 @@ impl MpmcRing {
|
||||
if diff == 0 {
|
||||
// Our turn: claim the position.
|
||||
match self.enqueue_pos.0.compare_exchange_weak(
|
||||
pos, pos + 1, Ordering::Relaxed, Ordering::Relaxed,
|
||||
pos,
|
||||
pos + 1,
|
||||
Ordering::Relaxed,
|
||||
Ordering::Relaxed,
|
||||
) {
|
||||
Ok(_) => {
|
||||
// SAFETY: the claim gives us exclusive write access
|
||||
@@ -250,7 +253,10 @@ impl MpmcRing {
|
||||
let diff = seq as isize - (pos + 1) as isize;
|
||||
if diff == 0 {
|
||||
match self.dequeue_pos.0.compare_exchange_weak(
|
||||
pos, pos + 1, Ordering::Relaxed, Ordering::Relaxed,
|
||||
pos,
|
||||
pos + 1,
|
||||
Ordering::Relaxed,
|
||||
Ordering::Relaxed,
|
||||
) {
|
||||
Ok(_) => {
|
||||
// SAFETY: the claim gives us exclusive read access;
|
||||
@@ -464,19 +470,36 @@ mod tests {
|
||||
|
||||
let popped = popped.lock().unwrap();
|
||||
assert_eq!(popped.len(), total, "count mismatch");
|
||||
let set: HashSet<u64> = popped.iter().map(|p| ((p.index() as u64) << 32) | p.generation() as u64).collect();
|
||||
let set: HashSet<u64> = popped
|
||||
.iter()
|
||||
.map(|p| ((p.index() as u64) << 32) | p.generation() as u64)
|
||||
.collect();
|
||||
assert_eq!(set.len(), total, "duplicate or lost element");
|
||||
assert_eq!(pop(&q), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mpmc_exactly_once_contended() {
|
||||
exactly_once(MpmcRing::new(8, 4096), |q, p| q.push(p), |q| q.pop(), 4, 4, 1000);
|
||||
exactly_once(
|
||||
MpmcRing::new(8, 4096),
|
||||
|q, p| q.push(p),
|
||||
|q| q.pop(),
|
||||
4,
|
||||
4,
|
||||
1000,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn striped_exactly_once_contended() {
|
||||
exactly_once(StripedRing::new(8, 4096), |q, p| q.push(p), |q| q.pop(), 4, 4, 1000);
|
||||
exactly_once(
|
||||
StripedRing::new(8, 4096),
|
||||
|q, p| q.push(p),
|
||||
|q| q.pop(),
|
||||
4,
|
||||
4,
|
||||
1000,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+136
-32
@@ -112,10 +112,11 @@
|
||||
//! deadline, so an expiry wakes one scheduler, not a herd.
|
||||
|
||||
use crate::actor::{
|
||||
clear_current_pid, is_actor_done, reset_actor_done, set_current_actor_box,
|
||||
set_current_pid, take_last_outcome, Actor, Outcome,
|
||||
clear_current_pid, is_actor_done, reset_actor_done, set_current_actor_box, set_current_pid,
|
||||
take_last_outcome, Actor, Outcome,
|
||||
};
|
||||
use crate::channel::Sender;
|
||||
use crate::context::{get_actor_sp, set_actor_sp, switch_to_actor};
|
||||
use crate::io::IoThread;
|
||||
use crate::monitor::{Down, DownReason, MonitorId};
|
||||
use crate::pid::Pid;
|
||||
@@ -124,11 +125,8 @@ use crate::raw_mutex::RawMutex;
|
||||
use crate::slot_state::{StateWord, Status, Unpark};
|
||||
use crate::supervisor::Signal;
|
||||
use crate::timer::Timers;
|
||||
use crate::context::{get_actor_sp, set_actor_sp, switch_to_actor};
|
||||
|
||||
use std::sync::atomic::{
|
||||
AtomicBool, AtomicPtr, AtomicU32, AtomicU64, AtomicUsize, Ordering,
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, AtomicU64, AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
|
||||
@@ -175,7 +173,9 @@ impl Config {
|
||||
pub fn exact(n: usize) -> Self {
|
||||
assert!(n >= 1, "scheduler thread count must be ≥ 1");
|
||||
Self {
|
||||
min: n, max: n, exact: Some(n),
|
||||
min: n,
|
||||
max: n,
|
||||
exact: Some(n),
|
||||
alloc_interval: crate::preempt::DEFAULT_ALLOC_INTERVAL,
|
||||
timeslice_cycles: crate::preempt::DEFAULT_TIMESLICE_CYCLES,
|
||||
stack_pool_cap: n * 4,
|
||||
@@ -196,7 +196,9 @@ impl Config {
|
||||
assert!(e >= 1, "exact must be ≥ 1");
|
||||
}
|
||||
Self {
|
||||
min, max, exact,
|
||||
min,
|
||||
max,
|
||||
exact,
|
||||
alloc_interval: crate::preempt::DEFAULT_ALLOC_INTERVAL,
|
||||
timeslice_cycles: crate::preempt::DEFAULT_TIMESLICE_CYCLES,
|
||||
stack_pool_cap: max * 4,
|
||||
@@ -322,7 +324,9 @@ impl Default for Config {
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(1);
|
||||
Self {
|
||||
min: 1, max: avail, exact: None,
|
||||
min: 1,
|
||||
max: avail,
|
||||
exact: None,
|
||||
alloc_interval: crate::preempt::DEFAULT_ALLOC_INTERVAL,
|
||||
timeslice_cycles: crate::preempt::DEFAULT_TIMESLICE_CYCLES,
|
||||
stack_pool_cap: avail * 4,
|
||||
@@ -376,7 +380,9 @@ pub struct RuntimeStats {
|
||||
impl RuntimeStats {
|
||||
/// Sum of run queue lengths across all scheduler threads.
|
||||
pub fn total_run_queue_len(&self) -> u64 {
|
||||
self.inner.stats.iter()
|
||||
self.inner
|
||||
.stats
|
||||
.iter()
|
||||
.map(|s| s.run_queue_len.load(Ordering::Relaxed))
|
||||
.sum()
|
||||
}
|
||||
@@ -400,7 +406,9 @@ impl RuntimeStats {
|
||||
/// scheduler threads. Counters are reset at the start of each `run()`,
|
||||
/// so after a run this reads that run's total.
|
||||
pub fn slot_hits(&self) -> u64 {
|
||||
self.inner.stats.iter()
|
||||
self.inner
|
||||
.stats
|
||||
.iter()
|
||||
.map(|s| s.slot_hits.load(Ordering::Relaxed))
|
||||
.sum()
|
||||
}
|
||||
@@ -408,7 +416,9 @@ impl RuntimeStats {
|
||||
/// RFC 005: total slot occupants displaced to the shared queue, summed
|
||||
/// across scheduler threads. Reset at the start of each `run()`.
|
||||
pub fn slot_displacements(&self) -> u64 {
|
||||
self.inner.stats.iter()
|
||||
self.inner
|
||||
.stats
|
||||
.iter()
|
||||
.map(|s| s.slot_displacements.load(Ordering::Relaxed))
|
||||
.sum()
|
||||
}
|
||||
@@ -472,6 +482,28 @@ pub(crate) struct SlotCold {
|
||||
/// epoch-matched unpark.
|
||||
pub(crate) waiters: Vec<(Pid, u32)>,
|
||||
pub(crate) outcome: Option<Outcome>,
|
||||
/// The slot's most recent *watchable-tenancy* death: `(generation,
|
||||
/// reason)`, stamped by `finalize_actor` — but only for a tenancy whose
|
||||
/// `watchable` bit was set — and deliberately never cleared: a new
|
||||
/// tenant's install leaves it standing (it describes the previous
|
||||
/// tenancy), and only the next *watchable* death overwrites it.
|
||||
/// Anonymous green-thread churn must not evict it: the free list is
|
||||
/// LIFO, so the just-freed slot is the first recycled, and an
|
||||
/// unconditional stamp made a watchable tenancy's record the
|
||||
/// shortest-lived data in the runtime. Read generation-matched via
|
||||
/// [`terminal_reason`](crate::monitor::terminal_reason), so a watch that
|
||||
/// raced its target's death can recover the real down reason instead of
|
||||
/// a blanket `NoProc` (bridge soak signatures 4 and 5).
|
||||
pub(crate) terminal: Option<(u32, DownReason)>,
|
||||
/// Stamp eligibility for `terminal` above: someone could plausibly hold
|
||||
/// a watch on this tenancy. Two set-sites, both while the tenancy is
|
||||
/// live: `register_with` *before* the binding lands (no successfully
|
||||
/// registered actor can die unflagged; a failed register's overshoot is
|
||||
/// harmless), and [`mark_watchable`](crate::monitor::mark_watchable) —
|
||||
/// the bridge calls it wherever a pid is encoded across the boundary,
|
||||
/// because BEAM can only watch pids it holds and can only hold pids
|
||||
/// that crossed. Reset at reclaim.
|
||||
pub(crate) watchable: bool,
|
||||
pub(crate) supervisor_channel: Option<Sender<Signal>>,
|
||||
/// Watchers registered via `monitor()`, each tagged with its
|
||||
/// `MonitorId` so `demonitor` can remove exactly one. Each receives one
|
||||
@@ -626,6 +658,8 @@ impl Slot {
|
||||
actor: None,
|
||||
waiters: Vec::new(),
|
||||
outcome: None,
|
||||
terminal: None,
|
||||
watchable: false,
|
||||
supervisor_channel: None,
|
||||
monitors: Vec::new(),
|
||||
links: Vec::new(),
|
||||
@@ -689,7 +723,8 @@ impl Slot {
|
||||
#[inline]
|
||||
pub(crate) fn record_message(&self) {
|
||||
let v = self.messages_received.load(Ordering::Relaxed);
|
||||
self.messages_received.store(v.wrapping_add(1), Ordering::Relaxed);
|
||||
self.messages_received
|
||||
.store(v.wrapping_add(1), Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Read the received-message tally (Relaxed; cross-thread snapshot read).
|
||||
@@ -708,7 +743,8 @@ impl Slot {
|
||||
#[inline]
|
||||
pub(crate) fn add_budget(&self, cycles: u64) {
|
||||
let v = self.budget_cycles.load(Ordering::Relaxed);
|
||||
self.budget_cycles.store(v.wrapping_add(cycles), Ordering::Relaxed);
|
||||
self.budget_cycles
|
||||
.store(v.wrapping_add(cycles), Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Read the accumulated budget cycles (Relaxed). Always 0 unless the
|
||||
@@ -847,7 +883,6 @@ impl Slot {
|
||||
Some(*unsafe { Box::from_raw(raw) })
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1153,10 +1188,29 @@ impl RuntimeInner {
|
||||
MonitorId(self.next_monitor_id.fetch_add(1, Ordering::Relaxed) + 1)
|
||||
}
|
||||
|
||||
/// Pop a vacant slot index, or `None` when the slab is full. The claim
|
||||
/// is atomic — a single pop under the free-list lock — so callers get
|
||||
/// claim-or-report semantics with no check-then-spawn TOCTOU: whoever
|
||||
/// gets `Some` owns that slot, full stop.
|
||||
pub(crate) fn try_allocate_slot(&self) -> Option<u32> {
|
||||
self.free.lock().pop()
|
||||
}
|
||||
|
||||
/// Return a slot claimed by [`try_allocate_slot`](Self::try_allocate_slot)
|
||||
/// that never had an actor installed into it (e.g. stack allocation
|
||||
/// panicked between claim and install). NOT for dead actors — their
|
||||
/// slots go back through `reclaim_slot`, which handles generation bump,
|
||||
/// waiter/monitor/link teardown, and stack recycling.
|
||||
pub(crate) fn return_vacant_slot(&self, idx: u32) {
|
||||
self.free.lock().push(idx);
|
||||
}
|
||||
|
||||
/// Pop a vacant slot index, or die loudly. The fixed slab is a deliberate
|
||||
/// v0.5 simplification (ROADMAP: "Deferred"); the panic names the fix.
|
||||
/// Callers that can shed load instead use [`try_allocate_slot`]
|
||||
/// (Self::try_allocate_slot) via `scheduler::try_spawn`.
|
||||
pub(crate) fn allocate_slot(&self) -> u32 {
|
||||
match self.free.lock().pop() {
|
||||
match self.try_allocate_slot() {
|
||||
Some(idx) => idx,
|
||||
None => panic!(
|
||||
"smarm: actor slot table exhausted — {} actors are live \
|
||||
@@ -1246,11 +1300,13 @@ impl Runtime {
|
||||
|
||||
// Re-initialise shared state for this run.
|
||||
assert_eq!(
|
||||
self.inner.run_queue.len(), 0,
|
||||
self.inner.run_queue.len(),
|
||||
0,
|
||||
"run() called while previous run still active"
|
||||
);
|
||||
debug_assert_eq!(
|
||||
self.inner.live_actors.load(Ordering::Acquire), 0,
|
||||
self.inner.live_actors.load(Ordering::Acquire),
|
||||
0,
|
||||
"run() called while previous run still active"
|
||||
);
|
||||
// RFC 018: the IO producers reach the runtime (slot table + unpark)
|
||||
@@ -1397,7 +1453,9 @@ impl Runtime {
|
||||
|
||||
/// Snapshot of runtime statistics for introspection / tests.
|
||||
pub fn stats(&self) -> RuntimeStats {
|
||||
RuntimeStats { inner: self.inner.clone() }
|
||||
RuntimeStats {
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1432,7 +1490,10 @@ thread_local! {
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub(crate) enum YieldIntent { Yield, Park }
|
||||
pub(crate) enum YieldIntent {
|
||||
Yield,
|
||||
Park,
|
||||
}
|
||||
|
||||
pub(crate) fn set_yield_intent(i: YieldIntent) {
|
||||
YIELD_INTENT.with(|c| c.set(i));
|
||||
@@ -1467,7 +1528,10 @@ pub const ROOT_PID: Pid = Pid::new(u32::MAX, u32::MAX);
|
||||
/// for writes after reclaim — so an over-eager mark costs a cancel-write,
|
||||
/// never data.
|
||||
fn maybe_shrink_stack(slot: &Slot) {
|
||||
let parks = slot.parks_since_shrink.load(Ordering::Relaxed).saturating_add(1);
|
||||
let parks = slot
|
||||
.parks_since_shrink
|
||||
.load(Ordering::Relaxed)
|
||||
.saturating_add(1);
|
||||
slot.parks_since_shrink.store(parks, Ordering::Relaxed);
|
||||
|
||||
let sp = slot.sp.load(Ordering::Relaxed);
|
||||
@@ -1566,12 +1630,19 @@ pub(crate) fn install_actor(
|
||||
// publish below.
|
||||
let (diag_reserve, diag_guard) = stack.shape();
|
||||
let diag_top = stack.top() as usize;
|
||||
slot.stop_ptr.store(Arc::as_ptr(&stop) as *mut _, Ordering::Release);
|
||||
slot.stop_ptr
|
||||
.store(Arc::as_ptr(&stop) as *mut _, Ordering::Release);
|
||||
{
|
||||
let mut cold = slot.cold.lock();
|
||||
debug_assert!(cold.actor.is_none(), "install over live actor");
|
||||
debug_assert!(cold.waiters.is_empty() && cold.monitors.is_empty() && cold.links.is_empty());
|
||||
cold.actor = Some(Actor { pid, stack, supervisor, stop, trap: None });
|
||||
cold.actor = Some(Actor {
|
||||
pid,
|
||||
stack,
|
||||
supervisor,
|
||||
stop,
|
||||
trap: None,
|
||||
});
|
||||
cold.outstanding_handles = 1;
|
||||
cold.outcome = None;
|
||||
cold.pending_io_result = None;
|
||||
@@ -1583,9 +1654,11 @@ pub(crate) fn install_actor(
|
||||
slot.parks_since_shrink.store(0, Ordering::Relaxed);
|
||||
slot.shrink_count.store(0, Ordering::Relaxed);
|
||||
slot.diag_stack_top.store(diag_top, Ordering::Relaxed);
|
||||
slot.diag_stack_reserve.store(diag_reserve, Ordering::Relaxed);
|
||||
slot.diag_stack_reserve
|
||||
.store(diag_reserve, Ordering::Relaxed);
|
||||
slot.diag_stack_guard.store(diag_guard, Ordering::Relaxed);
|
||||
slot.diag_pid.store(((idx as u64) << 32) | gen as u64, Ordering::Relaxed);
|
||||
slot.diag_pid
|
||||
.store(((idx as u64) << 32) | gen as u64, Ordering::Relaxed);
|
||||
slot.store_closure(closure);
|
||||
slot.reset_counters();
|
||||
inner.live_actors.fetch_add(1, Ordering::Relaxed);
|
||||
@@ -1594,7 +1667,10 @@ pub(crate) fn install_actor(
|
||||
// Release store orders everything above before any Acquire reader.
|
||||
slot.word.publish_queued(gen);
|
||||
inner.enqueue(pid);
|
||||
crate::te!(crate::trace::Event::Spawn { parent: supervisor, child: pid });
|
||||
crate::te!(crate::trace::Event::Spawn {
|
||||
parent: supervisor,
|
||||
child: pid
|
||||
});
|
||||
pid
|
||||
}
|
||||
|
||||
@@ -1611,14 +1687,19 @@ pub(crate) fn install_actor(
|
||||
/// is released — a last-sender drop can unpark a receiver, which takes the
|
||||
/// run-queue mutex; legal under a cold lock, but pointless to nest.
|
||||
pub(crate) fn reclaim_slot(inner: &RuntimeInner, pid: Pid) {
|
||||
let Some(slot) = inner.slot_at(pid) else { return };
|
||||
let Some(slot) = inner.slot_at(pid) else {
|
||||
return;
|
||||
};
|
||||
let dropped_outside;
|
||||
{
|
||||
let mut cold = slot.cold.lock();
|
||||
if slot.status_for(pid) != Status::Done || cold.outstanding_handles != 0 {
|
||||
return; // already reclaimed, or not yet eligible
|
||||
}
|
||||
debug_assert!(cold.actor.is_none(), "reclaiming a slot that still owns an actor");
|
||||
debug_assert!(
|
||||
cold.actor.is_none(),
|
||||
"reclaiming a slot that still owns an actor"
|
||||
);
|
||||
dropped_outside = (
|
||||
cold.outcome.take(),
|
||||
cold.supervisor_channel.take(),
|
||||
@@ -1628,6 +1709,7 @@ pub(crate) fn reclaim_slot(inner: &RuntimeInner, pid: Pid) {
|
||||
cold.waiters.clear();
|
||||
cold.monitors.clear();
|
||||
cold.links.clear();
|
||||
cold.watchable = false;
|
||||
slot.reset_counters();
|
||||
slot.stop_ptr.store(std::ptr::null_mut(), Ordering::Release);
|
||||
// The generation bump IS the reclaim: every stale pid is dead from
|
||||
@@ -1667,6 +1749,18 @@ fn finalize_actor(inner: &Arc<RuntimeInner>, pid: Pid, outcome: Outcome) {
|
||||
None => panic!("finalize_actor: actor vanished"),
|
||||
};
|
||||
cold.outcome = Some(joiner_outcome);
|
||||
// Terminal record (soak sig 4): stamped before the generation ever
|
||||
// bumps, under the cold lock, so a reader that resolved this pid can
|
||||
// recover the reason after the slot moves on — but only for a
|
||||
// tenancy that ever held a name. The free list is LIFO, so the slot
|
||||
// this death frees is the very next one recycled; if every green
|
||||
// thread's exit stamped too, the churn behind any real workload
|
||||
// would evict a watchable tenancy's record in well under the race
|
||||
// window this exists to cover. Overwritten only by the slot's next
|
||||
// *watchable* death.
|
||||
if cold.watchable {
|
||||
cold.terminal = Some((pid.generation(), down_reason));
|
||||
}
|
||||
slot.stop_ptr.store(std::ptr::null_mut(), Ordering::Release);
|
||||
// Done is published under the cold lock, so join's
|
||||
// check-Done-or-register-waiter (also under it) can never miss: it
|
||||
@@ -1704,7 +1798,10 @@ fn finalize_actor(inner: &Arc<RuntimeInner>, pid: Pid, outcome: Outcome) {
|
||||
// Notify monitors. Sent outside any slot lock: `send` may unpark a parked
|
||||
// receiver, which takes the run-queue mutex.
|
||||
for (_, m) in monitors {
|
||||
let _ = m.send(Down { pid, reason: down_reason });
|
||||
let _ = m.send(Down {
|
||||
pid,
|
||||
reason: down_reason,
|
||||
});
|
||||
}
|
||||
|
||||
// Walk linked peers ONE AT A TIME (cold locks are leaves). For every
|
||||
@@ -1736,7 +1833,10 @@ fn finalize_actor(inner: &Arc<RuntimeInner>, pid: Pid, outcome: Outcome) {
|
||||
};
|
||||
match trap {
|
||||
Some(Some(tx)) => {
|
||||
let _ = tx.send(crate::link::ExitSignal { from: pid, reason: down_reason });
|
||||
let _ = tx.send(crate::link::ExitSignal {
|
||||
from: pid,
|
||||
reason: down_reason,
|
||||
});
|
||||
}
|
||||
Some(None) => crate::scheduler::request_stop(peer),
|
||||
None => {}
|
||||
@@ -1892,7 +1992,9 @@ fn schedule_loop(inner: &Arc<RuntimeInner>, slot_idx: usize) {
|
||||
let io_out = inner.io_outstanding.load(Ordering::Acquire)
|
||||
+ inner.io_fd_waiters.load(Ordering::Acquire);
|
||||
|
||||
stats.run_queue_len.store(inner.run_queue.len(), Ordering::Relaxed);
|
||||
stats
|
||||
.run_queue_len
|
||||
.store(inner.run_queue.len(), Ordering::Relaxed);
|
||||
let pop = match inner.run_queue.pop() {
|
||||
Some(pid) => Pop::Got(pid),
|
||||
None => {
|
||||
@@ -2044,7 +2146,9 @@ fn schedule_loop(inner: &Arc<RuntimeInner>, slot_idx: usize) {
|
||||
}
|
||||
|
||||
// Update per-thread stats: record who's on-CPU.
|
||||
stats.current_pid_index.store(pid.index(), Ordering::Relaxed);
|
||||
stats
|
||||
.current_pid_index
|
||||
.store(pid.index(), Ordering::Relaxed);
|
||||
|
||||
set_actor_sp(sp);
|
||||
set_current_pid(pid);
|
||||
|
||||
+181
-78
@@ -68,9 +68,7 @@
|
||||
use crate::actor::current_pid;
|
||||
use crate::channel::Sender;
|
||||
use crate::pid::{Name, Pid};
|
||||
use crate::runtime::{
|
||||
self, RuntimeInner, YieldIntent, RUNTIME,
|
||||
};
|
||||
use crate::runtime::{self, RuntimeInner, YieldIntent, RUNTIME};
|
||||
use crate::supervisor::Signal;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
@@ -152,7 +150,9 @@ pub struct JoinHandle {
|
||||
|
||||
impl JoinHandle {
|
||||
/// The identity of the actor this handle refers to.
|
||||
pub fn pid(&self) -> Pid { self.pid }
|
||||
pub fn pid(&self) -> Pid {
|
||||
self.pid
|
||||
}
|
||||
|
||||
/// Block the calling actor until the spawned actor finishes, then
|
||||
/// report how it finished: `Ok(())` if it returned normally or stopped
|
||||
@@ -182,12 +182,10 @@ impl JoinHandle {
|
||||
crate::slot_state::Status::Stale => {
|
||||
panic!("join: target slot has been reused")
|
||||
}
|
||||
crate::slot_state::Status::Done => {
|
||||
Some(match cold.outcome.take() {
|
||||
Some(outcome) => outcome,
|
||||
None => panic!("Done slot must have outcome"),
|
||||
})
|
||||
}
|
||||
crate::slot_state::Status::Done => Some(match cold.outcome.take() {
|
||||
Some(outcome) => outcome,
|
||||
None => panic!("Done slot must have outcome"),
|
||||
}),
|
||||
crate::slot_state::Status::Live => {
|
||||
// begin_wait is lock-free, legal under the cold lock;
|
||||
// registering under it makes the epoch atomic with
|
||||
@@ -227,8 +225,7 @@ impl JoinHandle {
|
||||
match slot.status_for(self.pid) {
|
||||
crate::slot_state::Status::Stale => false,
|
||||
status => {
|
||||
cold.outstanding_handles =
|
||||
cold.outstanding_handles.saturating_sub(1);
|
||||
cold.outstanding_handles = cold.outstanding_handles.saturating_sub(1);
|
||||
cold.outstanding_handles == 0
|
||||
&& status == crate::slot_state::Status::Done
|
||||
}
|
||||
@@ -281,6 +278,37 @@ pub struct SpawnOpts {
|
||||
pub guard_size: Option<usize>,
|
||||
}
|
||||
|
||||
/// Why [`try_spawn`] could not start an actor.
|
||||
///
|
||||
/// Marked `non_exhaustive`: today the only refusal is a full slab, but a
|
||||
/// future variant (say, a shutdown-in-progress refusal) must not be a
|
||||
/// breaking change for shed-path `match`es.
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SpawnError {
|
||||
/// The fixed actor slab ([`Config::max_actors`]
|
||||
/// (crate::runtime::Config::max_actors)) is full: every slot is claimed
|
||||
/// by a live actor. This is a routine overload condition, not an
|
||||
/// invariant violation — shed the unit of work (close the socket,
|
||||
/// return a 503) and try again once actors have died.
|
||||
AtCapacity,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for SpawnError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
SpawnError::AtCapacity => {
|
||||
write!(
|
||||
f,
|
||||
"actor slab at capacity (`Config::max_actors` live actors)"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for SpawnError {}
|
||||
|
||||
/// Start a new actor running `f`, and return a [`JoinHandle`] for it.
|
||||
///
|
||||
/// The new actor runs concurrently with its caller and with every other
|
||||
@@ -305,9 +333,7 @@ pub fn spawn(f: impl FnOnce() + Send + 'static) -> JoinHandle {
|
||||
|
||||
/// [`spawn`] with per-actor stack shape overrides (RFC 019).
|
||||
pub fn spawn_with(opts: SpawnOpts, f: impl FnOnce() + Send + 'static) -> JoinHandle {
|
||||
let parent = current_pid().unwrap_or_else(|| {
|
||||
with_runtime(|_| crate::runtime::ROOT_PID)
|
||||
});
|
||||
let parent = current_pid().unwrap_or_else(|| with_runtime(|_| crate::runtime::ROOT_PID));
|
||||
spawn_under_with(parent, opts, f)
|
||||
}
|
||||
|
||||
@@ -339,7 +365,77 @@ pub fn spawn_under_with<A>(
|
||||
crate::runtime::install_actor(inner, idx, sp, stack, supervisor, closure)
|
||||
});
|
||||
|
||||
JoinHandle { pid, consumed: false }
|
||||
JoinHandle {
|
||||
pid,
|
||||
consumed: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// [`spawn`] that reports a full actor slab instead of panicking.
|
||||
///
|
||||
/// Behaviour parity with [`spawn`] in every case except one: when the fixed
|
||||
/// slab ([`Config::max_actors`](crate::runtime::Config::max_actors)) is
|
||||
/// full, this returns [`Err(SpawnError::AtCapacity)`](SpawnError::AtCapacity)
|
||||
/// where `spawn` panics the calling actor. Use it at load-shedding call
|
||||
/// sites — an accept loop spawning one actor per connection, a request
|
||||
/// admission point — where "at capacity" is a routine overload condition to
|
||||
/// handle (reject the unit of work), not an invariant violation. Internal
|
||||
/// and bounded spawn sites should keep [`spawn`]: there, the panic is a
|
||||
/// correct loud invariant check.
|
||||
///
|
||||
/// The claim is atomic (claim-or-report): there is no
|
||||
/// check-then-spawn race against other spawners for the last slot, so no
|
||||
/// headroom margin is needed.
|
||||
pub fn try_spawn(f: impl FnOnce() + Send + 'static) -> Result<JoinHandle, SpawnError> {
|
||||
let parent = current_pid().unwrap_or_else(|| with_runtime(|_| crate::runtime::ROOT_PID));
|
||||
try_spawn_under_with(parent, SpawnOpts::default(), f)
|
||||
}
|
||||
|
||||
/// [`try_spawn`] with an explicit supervisor and per-actor stack shape
|
||||
/// overrides — the full-control core the other `try_` surface is built on
|
||||
/// (mirrors [`spawn_under_with`]).
|
||||
pub fn try_spawn_under_with<A>(
|
||||
supervisor: Pid<A>,
|
||||
opts: SpawnOpts,
|
||||
f: impl FnOnce() + Send + 'static,
|
||||
) -> Result<JoinHandle, SpawnError> {
|
||||
let supervisor = supervisor.erase();
|
||||
// Slot FIRST — deliberately the reverse of `spawn`'s stack-first order:
|
||||
// under overload the Err arm is the HOT path, and a rejection must cost
|
||||
// one mutex pop, not an mmap/pool-pop + init + recycle per shed unit of
|
||||
// work. The claim is a single atomic pop (no TOCTOU; see
|
||||
// `try_allocate_slot`).
|
||||
let idx = match with_runtime(|inner| inner.try_allocate_slot()) {
|
||||
Some(idx) => idx,
|
||||
None => return Err(SpawnError::AtCapacity),
|
||||
};
|
||||
// Between claim and install the slot is owned by this frame alone; if
|
||||
// stack allocation panics in that window the slot must go back or it
|
||||
// leaks for the life of the runtime (and would trip the run()-teardown
|
||||
// slot-leak debug_assert).
|
||||
struct ReturnOnUnwind(Option<u32>);
|
||||
impl Drop for ReturnOnUnwind {
|
||||
fn drop(&mut self) {
|
||||
if let Some(idx) = self.0 {
|
||||
with_runtime(|inner| inner.return_vacant_slot(idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut claimed = ReturnOnUnwind(Some(idx));
|
||||
|
||||
let stack = with_runtime(|inner| crate::runtime::acquire_stack(inner, opts));
|
||||
let sp = init_actor_stack(stack.top(), crate::actor::trampoline);
|
||||
let closure: crate::runtime::Closure = Box::new(f);
|
||||
|
||||
claimed.0 = None; // install_actor takes ownership of the slot from here
|
||||
let pid = with_runtime(|inner| {
|
||||
crate::runtime::install_actor(inner, idx, sp, stack, supervisor, closure)
|
||||
});
|
||||
|
||||
Ok(JoinHandle {
|
||||
pid,
|
||||
consumed: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Spawn an actor that other actors can message directly by its [`Pid<A>`],
|
||||
@@ -566,11 +662,9 @@ pub fn sleep(duration: std::time::Duration) {
|
||||
let _np = NoPreempt::enter();
|
||||
let epoch = begin_wait();
|
||||
let deadline = crate::timer::deadline_from_now(duration);
|
||||
with_runtime(|inner| {
|
||||
match inner.timers.lock() {
|
||||
Ok(mut timers) => timers.insert_sleep(deadline, me, epoch),
|
||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||
}
|
||||
with_runtime(|inner| match inner.timers.lock() {
|
||||
Ok(mut timers) => timers.insert_sleep(deadline, me, epoch),
|
||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||
});
|
||||
park_current();
|
||||
}
|
||||
@@ -588,11 +682,9 @@ pub fn sleep_wall(duration: std::time::Duration) {
|
||||
let _np = NoPreempt::enter();
|
||||
let epoch = begin_wait();
|
||||
let deadline = crate::timer::deadline_from_now(duration);
|
||||
with_runtime(|inner| {
|
||||
match inner.timers.lock() {
|
||||
Ok(mut timers) => timers.insert_sleep_wall(deadline, me, epoch),
|
||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||
}
|
||||
with_runtime(|inner| match inner.timers.lock() {
|
||||
Ok(mut timers) => timers.insert_sleep_wall(deadline, me, epoch),
|
||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||
});
|
||||
park_current();
|
||||
}
|
||||
@@ -608,15 +700,13 @@ pub fn insert_wait_timer(
|
||||
target: std::sync::Arc<dyn crate::timer::TimerTarget>,
|
||||
epoch: u32,
|
||||
) {
|
||||
with_runtime(|inner| {
|
||||
match inner.timers.lock() {
|
||||
Ok(mut timers) => timers.insert(
|
||||
deadline,
|
||||
pid,
|
||||
crate::timer::Reason::WaitTimeout { target, epoch },
|
||||
),
|
||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||
}
|
||||
with_runtime(|inner| match inner.timers.lock() {
|
||||
Ok(mut timers) => timers.insert(
|
||||
deadline,
|
||||
pid,
|
||||
crate::timer::Reason::WaitTimeout { target, epoch },
|
||||
),
|
||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -646,11 +736,9 @@ pub fn send_after<A: crate::pid::Addressable>(
|
||||
let fire = Box::new(move || {
|
||||
let _ = crate::registry::send_to(dest, msg);
|
||||
});
|
||||
with_runtime(|inner| {
|
||||
match inner.timers.lock() {
|
||||
Ok(mut timers) => timers.insert_send(deadline, dest.erase(), fire),
|
||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||
}
|
||||
with_runtime(|inner| match inner.timers.lock() {
|
||||
Ok(mut timers) => timers.insert_send(deadline, dest.erase(), fire),
|
||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -670,11 +758,9 @@ pub fn send_after_named<M: Send + 'static>(
|
||||
let fire = Box::new(move || {
|
||||
let _ = crate::registry::send(dest, msg);
|
||||
});
|
||||
with_runtime(|inner| {
|
||||
match inner.timers.lock() {
|
||||
Ok(mut timers) => timers.insert_send(deadline, armed_by, fire),
|
||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||
}
|
||||
with_runtime(|inner| match inner.timers.lock() {
|
||||
Ok(mut timers) => timers.insert_send(deadline, armed_by, fire),
|
||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -693,11 +779,9 @@ pub fn send_after_wall<A: crate::pid::Addressable>(
|
||||
let fire = Box::new(move || {
|
||||
let _ = crate::registry::send_to(dest, msg);
|
||||
});
|
||||
with_runtime(|inner| {
|
||||
match inner.timers.lock() {
|
||||
Ok(mut timers) => timers.insert_send_wall(deadline, dest.erase(), fire),
|
||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||
}
|
||||
with_runtime(|inner| match inner.timers.lock() {
|
||||
Ok(mut timers) => timers.insert_send_wall(deadline, dest.erase(), fire),
|
||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -714,11 +798,9 @@ pub fn send_after_named_wall<M: Send + 'static>(
|
||||
let fire = Box::new(move || {
|
||||
let _ = crate::registry::send(dest, msg);
|
||||
});
|
||||
with_runtime(|inner| {
|
||||
match inner.timers.lock() {
|
||||
Ok(mut timers) => timers.insert_send_wall(deadline, armed_by, fire),
|
||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||
}
|
||||
with_runtime(|inner| match inner.timers.lock() {
|
||||
Ok(mut timers) => timers.insert_send_wall(deadline, armed_by, fire),
|
||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -738,11 +820,9 @@ pub(crate) fn send_after_to<T: Send + 'static>(
|
||||
let fire = Box::new(move || {
|
||||
let _ = tx.send(msg);
|
||||
});
|
||||
with_runtime(|inner| {
|
||||
match inner.timers.lock() {
|
||||
Ok(mut timers) => timers.insert_send(deadline, armed_by, fire),
|
||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||
}
|
||||
with_runtime(|inner| match inner.timers.lock() {
|
||||
Ok(mut timers) => timers.insert_send(deadline, armed_by, fire),
|
||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -750,11 +830,9 @@ pub(crate) fn send_after_to<T: Send + 'static>(
|
||||
/// it fires. Returns `true` if the timer was still pending and delivery is
|
||||
/// now prevented, `false` if it had already fired or was already cancelled.
|
||||
pub fn cancel_timer(id: crate::timer::TimerId) -> bool {
|
||||
with_runtime(|inner| {
|
||||
match inner.timers.lock() {
|
||||
Ok(mut timers) => timers.cancel(id),
|
||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||
}
|
||||
with_runtime(|inner| match inner.timers.lock() {
|
||||
Ok(mut timers) => timers.cancel(id),
|
||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -820,7 +898,8 @@ where
|
||||
};
|
||||
let mut cold = slot.cold.lock();
|
||||
debug_assert_eq!(
|
||||
slot.generation(), me.generation(),
|
||||
slot.generation(),
|
||||
me.generation(),
|
||||
"block_on_io: own slot reused mid-park"
|
||||
);
|
||||
match cold.pending_io_result.take() {
|
||||
@@ -946,12 +1025,20 @@ pub struct FdArm {
|
||||
impl FdArm {
|
||||
/// An arm that becomes ready when `fd` is readable.
|
||||
pub fn readable(fd: std::os::fd::RawFd) -> Self {
|
||||
FdArm { fd, readable: true, writable: false }
|
||||
FdArm {
|
||||
fd,
|
||||
readable: true,
|
||||
writable: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// An arm that becomes ready when `fd` is writable.
|
||||
pub fn writable(fd: std::os::fd::RawFd) -> Self {
|
||||
FdArm { fd, readable: false, writable: true }
|
||||
FdArm {
|
||||
fd,
|
||||
readable: false,
|
||||
writable: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -976,9 +1063,7 @@ impl crate::channel::Selectable for FdArm {
|
||||
match io.as_mut() {
|
||||
Some(io) => {
|
||||
inner.io_fd_waiters.fetch_add(1, Ordering::AcqRel);
|
||||
let r = io.epoll_register(
|
||||
self.fd, pid, epoch, self.readable, self.writable,
|
||||
);
|
||||
let r = io.epoll_register(self.fd, pid, epoch, self.readable, self.writable);
|
||||
if r.is_err() {
|
||||
inner.io_fd_waiters.fetch_sub(1, Ordering::AcqRel);
|
||||
}
|
||||
@@ -1033,7 +1118,11 @@ fn poll_events(fd: std::os::fd::RawFd, readable: bool, writable: bool) -> std::i
|
||||
if writable {
|
||||
events |= libc::POLLOUT;
|
||||
}
|
||||
let mut pfd = libc::pollfd { fd, events, revents: 0 };
|
||||
let mut pfd = libc::pollfd {
|
||||
fd,
|
||||
events,
|
||||
revents: 0,
|
||||
};
|
||||
loop {
|
||||
let r = unsafe { libc::poll(&mut pfd, 1, 0) };
|
||||
if r < 0 {
|
||||
@@ -1081,7 +1170,11 @@ pub fn wait_writable_timeout(
|
||||
pub fn read(fd: std::os::fd::RawFd, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
wait_readable(fd)?;
|
||||
let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, buf.len()) };
|
||||
if n < 0 { Err(std::io::Error::last_os_error()) } else { Ok(n as usize) }
|
||||
if n < 0 {
|
||||
Err(std::io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(n as usize)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience wrapper: park until `fd` is writable, then perform the
|
||||
@@ -1090,7 +1183,11 @@ pub fn read(fd: std::os::fd::RawFd, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
pub fn write(fd: std::os::fd::RawFd, buf: &[u8]) -> std::io::Result<usize> {
|
||||
wait_writable(fd)?;
|
||||
let n = unsafe { libc::write(fd, buf.as_ptr() as *const _, buf.len()) };
|
||||
if n < 0 { Err(std::io::Error::last_os_error()) } else { Ok(n as usize) }
|
||||
if n < 0 {
|
||||
Err(std::io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(n as usize)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1099,12 +1196,15 @@ pub fn write(fd: std::os::fd::RawFd, buf: &[u8]) -> std::io::Result<usize> {
|
||||
|
||||
pub fn register_supervisor_channel(pid: Pid, sender: Sender<Signal>) {
|
||||
with_runtime(|inner| {
|
||||
let slot = inner.slot_at(pid)
|
||||
let slot = inner
|
||||
.slot_at(pid)
|
||||
.unwrap_or_else(|| panic!("register_supervisor_channel: pid {:?} not found", pid));
|
||||
let mut cold = slot.cold.lock();
|
||||
assert_eq!(
|
||||
slot.generation(), pid.generation(),
|
||||
"register_supervisor_channel: pid {:?} not found", pid
|
||||
slot.generation(),
|
||||
pid.generation(),
|
||||
"register_supervisor_channel: pid {:?} not found",
|
||||
pid
|
||||
);
|
||||
cold.supervisor_channel = Some(sender);
|
||||
});
|
||||
@@ -1177,6 +1277,9 @@ mod send_after_to_tests {
|
||||
crate::sleep(Duration::from_millis(30));
|
||||
r2.store(true, Ordering::SeqCst);
|
||||
});
|
||||
assert!(reached.load(Ordering::SeqCst), "runtime survived the dead-channel fire");
|
||||
assert!(
|
||||
reached.load(Ordering::SeqCst),
|
||||
"runtime survived the dead-channel fire"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+12
-3
@@ -214,7 +214,10 @@ struct Buf {
|
||||
|
||||
impl Buf {
|
||||
fn new() -> Self {
|
||||
Buf { b: [0; 320], len: 0 }
|
||||
Buf {
|
||||
b: [0; 320],
|
||||
len: 0,
|
||||
}
|
||||
}
|
||||
fn s(&mut self, s: &str) {
|
||||
for &c in s.as_bytes() {
|
||||
@@ -274,8 +277,14 @@ mod tests {
|
||||
#[test]
|
||||
fn inside_guard_both_edges() {
|
||||
assert_eq!(classify(GUARD_LO, TOP, RESERVE, GUARD), FaultClass::Guard);
|
||||
assert_eq!(classify(GUARD_HI - 1, TOP, RESERVE, GUARD), FaultClass::Guard);
|
||||
assert_eq!(classify(GUARD_LO + GUARD / 2, TOP, RESERVE, GUARD), FaultClass::Guard);
|
||||
assert_eq!(
|
||||
classify(GUARD_HI - 1, TOP, RESERVE, GUARD),
|
||||
FaultClass::Guard
|
||||
);
|
||||
assert_eq!(
|
||||
classify(GUARD_LO + GUARD / 2, TOP, RESERVE, GUARD),
|
||||
FaultClass::Guard
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+9
-9
@@ -188,8 +188,7 @@ impl StateWord {
|
||||
loop {
|
||||
let w = self.load();
|
||||
debug_assert!(
|
||||
matches!(word_state(w), ST_RUNNING | ST_RUNNING_NOTIFIED)
|
||||
&& word_gen(w) == gen,
|
||||
matches!(word_state(w), ST_RUNNING | ST_RUNNING_NOTIFIED) && word_gen(w) == gen,
|
||||
"yield return from invalid word {w:#x}"
|
||||
);
|
||||
if self
|
||||
@@ -247,8 +246,7 @@ impl StateWord {
|
||||
loop {
|
||||
let w = self.load();
|
||||
debug_assert!(
|
||||
matches!(word_state(w), ST_RUNNING | ST_RUNNING_NOTIFIED)
|
||||
&& word_gen(w) == gen,
|
||||
matches!(word_state(w), ST_RUNNING | ST_RUNNING_NOTIFIED) && word_gen(w) == gen,
|
||||
"begin_wait from invalid word {w:#x}"
|
||||
);
|
||||
let next = word_epoch(w).wrapping_add(1) & EPOCH_MASK;
|
||||
@@ -342,8 +340,7 @@ impl StateWord {
|
||||
loop {
|
||||
let w = self.load();
|
||||
debug_assert!(
|
||||
matches!(word_state(w), ST_RUNNING | ST_RUNNING_NOTIFIED)
|
||||
&& word_gen(w) == gen,
|
||||
matches!(word_state(w), ST_RUNNING | ST_RUNNING_NOTIFIED) && word_gen(w) == gen,
|
||||
"clear_notify from invalid word {w:#x}"
|
||||
);
|
||||
if word_state(w) != ST_RUNNING_NOTIFIED {
|
||||
@@ -372,8 +369,7 @@ impl StateWord {
|
||||
pub(crate) fn set_done(&self, gen: u32) {
|
||||
let prev = self.0.swap(pack(gen, 0, ST_DONE), Ordering::AcqRel);
|
||||
debug_assert!(
|
||||
matches!(word_state(prev), ST_RUNNING | ST_RUNNING_NOTIFIED)
|
||||
&& word_gen(prev) == gen,
|
||||
matches!(word_state(prev), ST_RUNNING | ST_RUNNING_NOTIFIED) && word_gen(prev) == gen,
|
||||
"finalize from invalid word {prev:#x}"
|
||||
);
|
||||
}
|
||||
@@ -538,7 +534,11 @@ mod loom_tests {
|
||||
// not a pending notification.
|
||||
assert!(word.try_claim(0));
|
||||
assert_eq!(word.unpark(0, Some(epoch)), Unpark::Noop);
|
||||
assert_eq!(word_state(word.load()), ST_RUNNING, "stale epoch notified a live run");
|
||||
assert_eq!(
|
||||
word_state(word.load()),
|
||||
ST_RUNNING,
|
||||
"stale epoch notified a live run"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+12
-5
@@ -57,16 +57,19 @@ impl Stack {
|
||||
}
|
||||
let base = base as *mut u8;
|
||||
|
||||
let ret = unsafe {
|
||||
libc::mprotect(base as *mut libc::c_void, guard_size, libc::PROT_NONE)
|
||||
};
|
||||
let ret = unsafe { libc::mprotect(base as *mut libc::c_void, guard_size, libc::PROT_NONE) };
|
||||
if ret != 0 {
|
||||
let err = io::Error::last_os_error();
|
||||
unsafe { libc::munmap(base as *mut libc::c_void, total_size) };
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
Ok(Self { base, total_size, stack_size, guard_size })
|
||||
Ok(Self {
|
||||
base,
|
||||
total_size,
|
||||
stack_size,
|
||||
guard_size,
|
||||
})
|
||||
}
|
||||
|
||||
/// 16-byte-aligned top of the usable region.
|
||||
@@ -177,7 +180,11 @@ pub(crate) fn shrink_range(hwm: usize, sp: usize, page: usize) -> Option<(usize,
|
||||
/// with `stack_size` page-rounded by `Stack::new` the result is always
|
||||
/// page-aligned. Checked math: `retain ≥ stack_size` (notably the default
|
||||
/// 64 KiB reserve with the 64 KiB RETAIN) and overflow collapse to `None`.
|
||||
pub(crate) fn retain_range(stack_size: usize, retain: usize, page: usize) -> Option<(usize, usize)> {
|
||||
pub(crate) fn retain_range(
|
||||
stack_size: usize,
|
||||
retain: usize,
|
||||
page: usize,
|
||||
) -> Option<(usize, usize)> {
|
||||
debug_assert!(page.is_power_of_two());
|
||||
let retain = retain.checked_add(page - 1)? & !(page - 1); // page_up(retain)
|
||||
let len = stack_size.checked_sub(retain)?;
|
||||
|
||||
+8
-6
@@ -123,9 +123,9 @@ pub enum Signal {
|
||||
impl std::fmt::Debug for Signal {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Signal::Exit(pid) => write!(f, "Signal::Exit({:?})", pid),
|
||||
Signal::Exit(pid) => write!(f, "Signal::Exit({:?})", pid),
|
||||
Signal::Panic(pid, _) => write!(f, "Signal::Panic({:?}, ..)", pid),
|
||||
Signal::Stopped(pid) => write!(f, "Signal::Stopped({:?})", pid),
|
||||
Signal::Stopped(pid) => write!(f, "Signal::Stopped({:?})", pid),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,9 +133,9 @@ impl std::fmt::Debug for Signal {
|
||||
impl Signal {
|
||||
pub fn pid(&self) -> Pid {
|
||||
match self {
|
||||
Signal::Exit(p) => *p,
|
||||
Signal::Exit(p) => *p,
|
||||
Signal::Panic(p, _) => *p,
|
||||
Signal::Stopped(p) => *p,
|
||||
Signal::Stopped(p) => *p,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,7 +169,10 @@ pub struct ChildSpec {
|
||||
|
||||
impl ChildSpec {
|
||||
pub fn new(restart: Restart, start: impl Fn() + Send + Sync + 'static) -> Self {
|
||||
Self { start: Arc::new(start), restart }
|
||||
Self {
|
||||
start: Arc::new(start),
|
||||
restart,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,4 +395,3 @@ impl OneForOne {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -129,7 +129,9 @@ impl Ord for Entry {
|
||||
// Earlier deadline first; ties broken by insertion order so the
|
||||
// ordering is total. `Reason` and `Pid` deliberately don't
|
||||
// participate.
|
||||
self.deadline.cmp(&other.deadline).then_with(|| self.seq.cmp(&other.seq))
|
||||
self.deadline
|
||||
.cmp(&other.deadline)
|
||||
.then_with(|| self.seq.cmp(&other.seq))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+44
-28
@@ -16,13 +16,17 @@
|
||||
#[cfg(feature = "smarm-trace")]
|
||||
#[macro_export]
|
||||
macro_rules! te {
|
||||
($kind:expr) => { $crate::trace::record($kind) };
|
||||
($kind:expr) => {
|
||||
$crate::trace::record($kind)
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "smarm-trace"))]
|
||||
#[macro_export]
|
||||
macro_rules! te {
|
||||
($kind:expr) => { () };
|
||||
($kind:expr) => {
|
||||
()
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "smarm-trace")]
|
||||
@@ -68,8 +72,8 @@ mod inner {
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
struct Record {
|
||||
nanos: u64, // ns since open()
|
||||
tid: u64, // OS thread id
|
||||
nanos: u64, // ns since open()
|
||||
tid: u64, // OS thread id
|
||||
event: Event,
|
||||
}
|
||||
|
||||
@@ -84,8 +88,8 @@ mod inner {
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
struct Global {
|
||||
sender: mpsc::Sender<Msg>,
|
||||
start: Instant,
|
||||
sender: mpsc::Sender<Msg>,
|
||||
start: Instant,
|
||||
}
|
||||
|
||||
static GLOBAL: Mutex<Option<Global>> = Mutex::new(None);
|
||||
@@ -95,7 +99,7 @@ mod inner {
|
||||
// The start Instant is copied alongside it — also one mutex hit per thread.
|
||||
// record() never touches GLOBAL after that.
|
||||
struct LocalState {
|
||||
tx: mpsc::Sender<Msg>,
|
||||
tx: mpsc::Sender<Msg>,
|
||||
start: Instant,
|
||||
}
|
||||
|
||||
@@ -109,8 +113,8 @@ mod inner {
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
pub fn open() {
|
||||
let path = std::env::var("SMARM_TRACE_FILE")
|
||||
.unwrap_or_else(|_| "smarm_trace.json".to_owned());
|
||||
let path =
|
||||
std::env::var("SMARM_TRACE_FILE").unwrap_or_else(|_| "smarm_trace.json".to_owned());
|
||||
|
||||
let (tx, rx) = mpsc::channel::<Msg>();
|
||||
let start = Instant::now();
|
||||
@@ -164,8 +168,11 @@ mod inner {
|
||||
// which would try to re-acquire inner.shared (already held at many
|
||||
// te!() call sites) -> deadlock. Guard at the very top, before any
|
||||
// allocation-capable call.
|
||||
let was_enabled = crate::preempt::PREEMPTION_ENABLED
|
||||
.with(|e| { let v = e.get(); e.set(false); v });
|
||||
let was_enabled = crate::preempt::PREEMPTION_ENABLED.with(|e| {
|
||||
let v = e.get();
|
||||
e.set(false);
|
||||
v
|
||||
});
|
||||
|
||||
LOCAL_STATE.with(|cell| {
|
||||
let mut opt = cell.borrow_mut();
|
||||
@@ -182,7 +189,7 @@ mod inner {
|
||||
}
|
||||
if let Some(ls) = opt.as_ref() {
|
||||
let nanos = ls.start.elapsed().as_nanos() as u64;
|
||||
let tid = os_tid();
|
||||
let tid = os_tid();
|
||||
let _ = ls.tx.send(Msg::Event(Record { nanos, tid, event }));
|
||||
}
|
||||
});
|
||||
@@ -197,7 +204,10 @@ mod inner {
|
||||
fn drain_thread(rx: mpsc::Receiver<Msg>, path: &str) {
|
||||
let f = match std::fs::File::create(path) {
|
||||
Ok(f) => f,
|
||||
Err(e) => { eprintln!("[smarm-trace] create failed: {}", e); return; }
|
||||
Err(e) => {
|
||||
eprintln!("[smarm-trace] create failed: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let mut w = std::io::BufWriter::new(f);
|
||||
let _ = writeln!(w, "{{\"traceEvents\":[");
|
||||
@@ -210,7 +220,9 @@ mod inner {
|
||||
Ok(Msg::Event(r)) => {
|
||||
let (name, actor_idx) = chrome_fields(&r.event);
|
||||
let ts_us = r.nanos as f64 / 1000.0;
|
||||
if !first { let _ = w.write_all(b",\n"); }
|
||||
if !first {
|
||||
let _ = w.write_all(b",\n");
|
||||
}
|
||||
first = false;
|
||||
let _ = write!(w,
|
||||
"{{\"ph\":\"i\",\"ts\":{:.3},\"pid\":{},\"tid\":{},\"name\":{:?},\"s\":\"g\"}}",
|
||||
@@ -234,27 +246,31 @@ mod inner {
|
||||
|
||||
fn chrome_fields(ev: &Event) -> (String, u32) {
|
||||
match ev {
|
||||
Event::Spawn { parent, child } =>
|
||||
(format!("spawn c={}", child.index()), parent.index()),
|
||||
Event::Resume(p) => ("resume".into(), p.index()),
|
||||
Event::Yield(p) => ("yield".into(), p.index()),
|
||||
Event::Park(p) => ("park".into(), p.index()),
|
||||
Event::Done(p) => ("done".into(), p.index()),
|
||||
Event::UnparkDirect(p) => ("unpark_direct".into(), p.index()),
|
||||
Event::UnparkDeferred(p) => ("unpark_deferred".into(), p.index()),
|
||||
Event::Spawn { parent, child } => {
|
||||
(format!("spawn c={}", child.index()), parent.index())
|
||||
}
|
||||
Event::Resume(p) => ("resume".into(), p.index()),
|
||||
Event::Yield(p) => ("yield".into(), p.index()),
|
||||
Event::Park(p) => ("park".into(), p.index()),
|
||||
Event::Done(p) => ("done".into(), p.index()),
|
||||
Event::UnparkDirect(p) => ("unpark_direct".into(), p.index()),
|
||||
Event::UnparkDeferred(p) => ("unpark_deferred".into(), p.index()),
|
||||
Event::UnparkFlagConsumed(p) => ("unpark_flag_consumed".into(), p.index()),
|
||||
Event::Send { sender, receiver } => (
|
||||
format!("send rx={}", receiver
|
||||
.map(|p| p.index().to_string())
|
||||
.unwrap_or_else(|| "none".into())),
|
||||
format!(
|
||||
"send rx={}",
|
||||
receiver
|
||||
.map(|p| p.index().to_string())
|
||||
.unwrap_or_else(|| "none".into())
|
||||
),
|
||||
sender.index(),
|
||||
),
|
||||
Event::RecvPark(p) => ("recv_park".into(), p.index()),
|
||||
Event::RecvWake(p) => ("recv_wake".into(), p.index()),
|
||||
Event::Enqueue(p) => ("enqueue".into(), p.index()),
|
||||
Event::Dequeue(p) => ("dequeue".into(), p.index()),
|
||||
Event::Enqueue(p) => ("enqueue".into(), p.index()),
|
||||
Event::Dequeue(p) => ("dequeue".into(), p.index()),
|
||||
Event::SlotPush(p) => ("slot_push".into(), p.index()),
|
||||
Event::SlotPop(p) => ("slot_pop".into(), p.index()),
|
||||
Event::SlotPop(p) => ("slot_pop".into(), p.index()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+24
-6
@@ -49,8 +49,14 @@ fn looping_actor_on_check_is_stopped() {
|
||||
}
|
||||
let _ = h.join();
|
||||
});
|
||||
assert!(saw_stopped.load(Ordering::SeqCst), "expected DownReason::Stopped");
|
||||
assert!(dropped.load(Ordering::SeqCst), "Drop guard must run during the cancellation unwind");
|
||||
assert!(
|
||||
saw_stopped.load(Ordering::SeqCst),
|
||||
"expected DownReason::Stopped"
|
||||
);
|
||||
assert!(
|
||||
dropped.load(Ordering::SeqCst),
|
||||
"Drop guard must run during the cancellation unwind"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -79,8 +85,14 @@ fn parked_on_recv_actor_is_stopped() {
|
||||
}
|
||||
let _ = h.join();
|
||||
});
|
||||
assert!(saw_stopped.load(Ordering::SeqCst), "expected DownReason::Stopped");
|
||||
assert!(dropped.load(Ordering::SeqCst), "Drop guard must run on cancellation of a parked actor");
|
||||
assert!(
|
||||
saw_stopped.load(Ordering::SeqCst),
|
||||
"expected DownReason::Stopped"
|
||||
);
|
||||
assert!(
|
||||
dropped.load(Ordering::SeqCst),
|
||||
"Drop guard must run on cancellation of a parked actor"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -185,6 +197,12 @@ fn stop_flagged_while_queued_lands_at_first_park() {
|
||||
.recv_timeout(Duration::from_secs(10))
|
||||
.expect("runtime deadlocked: stop against a QUEUED actor was lost at its first park");
|
||||
|
||||
assert!(saw_stopped.load(Ordering::SeqCst), "expected DownReason::Stopped");
|
||||
assert!(dropped.load(Ordering::SeqCst), "Drop guard must run during the cancellation unwind");
|
||||
assert!(
|
||||
saw_stopped.load(Ordering::SeqCst),
|
||||
"expected DownReason::Stopped"
|
||||
);
|
||||
assert!(
|
||||
dropped.load(Ordering::SeqCst),
|
||||
"Drop guard must run during the cancellation unwind"
|
||||
);
|
||||
}
|
||||
|
||||
+25
-26
@@ -24,7 +24,11 @@ fn progress_point_counts() {
|
||||
h.join().unwrap();
|
||||
let after = smarm::causal::progress_snapshot();
|
||||
let delta = |name: &str| {
|
||||
after.iter().find(|(n, _)| n == name).map(|(_, c)| *c).unwrap()
|
||||
after
|
||||
.iter()
|
||||
.find(|(n, _)| n == name)
|
||||
.map(|(_, c)| *c)
|
||||
.unwrap()
|
||||
- before
|
||||
.iter()
|
||||
.find(|(n, _)| n == name)
|
||||
@@ -45,21 +49,12 @@ fn site_guard_nesting_restores() {
|
||||
assert_eq!(smarm::causal::current_site_name(), None);
|
||||
{
|
||||
let _outer = smarm::causal_site!("outer");
|
||||
assert_eq!(
|
||||
smarm::causal::current_site_name().as_deref(),
|
||||
Some("outer")
|
||||
);
|
||||
assert_eq!(smarm::causal::current_site_name().as_deref(), Some("outer"));
|
||||
{
|
||||
let _inner = smarm::causal_site!("inner");
|
||||
assert_eq!(
|
||||
smarm::causal::current_site_name().as_deref(),
|
||||
Some("inner")
|
||||
);
|
||||
assert_eq!(smarm::causal::current_site_name().as_deref(), Some("inner"));
|
||||
}
|
||||
assert_eq!(
|
||||
smarm::causal::current_site_name().as_deref(),
|
||||
Some("outer")
|
||||
);
|
||||
assert_eq!(smarm::causal::current_site_name().as_deref(), Some("outer"));
|
||||
}
|
||||
assert_eq!(smarm::causal::current_site_name(), None);
|
||||
});
|
||||
@@ -88,10 +83,7 @@ fn virtual_speedup_ledger() {
|
||||
let bystander = smarm::spawn(move || {
|
||||
while !stop2.load(Ordering::Relaxed) {
|
||||
smarm::check!();
|
||||
out2.store(
|
||||
smarm::causal::my_absorbed_delay_cycles(),
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
out2.store(smarm::causal::my_absorbed_delay_cycles(), Ordering::Relaxed);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -166,10 +158,7 @@ fn runnable_bystander_pays_delay() {
|
||||
while !stop_b.load(Ordering::Relaxed) {
|
||||
iters2.fetch_add(1, Ordering::Relaxed);
|
||||
smarm::check!();
|
||||
absorbed2.store(
|
||||
smarm::causal::my_absorbed_delay_cycles(),
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
absorbed2.store(smarm::causal::my_absorbed_delay_cycles(), Ordering::Relaxed);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -177,8 +166,7 @@ fn runnable_bystander_pays_delay() {
|
||||
let i0 = iters.load(Ordering::Relaxed);
|
||||
let t = std::time::Instant::now();
|
||||
smarm::sleep(Duration::from_millis(150));
|
||||
let rate =
|
||||
(iters.load(Ordering::Relaxed) - i0) as f64 / t.elapsed().as_secs_f64();
|
||||
let rate = (iters.load(Ordering::Relaxed) - i0) as f64 / t.elapsed().as_secs_f64();
|
||||
out.store(rate as u64, Ordering::Relaxed);
|
||||
};
|
||||
|
||||
@@ -448,7 +436,10 @@ fn timer_deadline_shifts_with_injected_delay() {
|
||||
|
||||
// Raw deadline passed, effective deadline not: nothing fires, entry kept.
|
||||
assert!(t.pop_due(now + Duration::from_millis(60)).is_empty());
|
||||
assert!(!t.is_empty(), "shifted entry must be re-queued, not dropped");
|
||||
assert!(
|
||||
!t.is_empty(),
|
||||
"shifted entry must be re-queued, not dropped"
|
||||
);
|
||||
|
||||
// Past raw + injected (with margin) it must fire. Chase in case a
|
||||
// parallel test injected more debt meanwhile.
|
||||
@@ -503,7 +494,11 @@ fn wall_timer_ignores_injected_delay() {
|
||||
// Just past the raw deadline: the wall entry fires, the virtual one is
|
||||
// re-queued at its shifted deadline.
|
||||
let due = t.pop_due(now + Duration::from_millis(60));
|
||||
assert_eq!(due.len(), 1, "exactly the wall entry must fire at raw deadline");
|
||||
assert_eq!(
|
||||
due.len(),
|
||||
1,
|
||||
"exactly the wall entry must fire at raw deadline"
|
||||
);
|
||||
assert_eq!(due[0].pid, Pid::new(0, 0));
|
||||
assert!(!t.is_empty(), "virtual sibling must remain queued, shifted");
|
||||
}
|
||||
@@ -623,7 +618,11 @@ fn wall_send_after_ignores_injected_delay() {
|
||||
|
||||
// Just past the raw deadline: only the wall send pops; run its thunk.
|
||||
let due = t.pop_due(now + Duration::from_millis(60));
|
||||
assert_eq!(due.len(), 1, "exactly the wall send must fire at raw deadline");
|
||||
assert_eq!(
|
||||
due.len(),
|
||||
1,
|
||||
"exactly the wall send must fire at raw deadline"
|
||||
);
|
||||
for e in due {
|
||||
if let smarm::timer::Reason::Send { fire } = e.reason {
|
||||
fire();
|
||||
|
||||
+18
-10
@@ -154,7 +154,10 @@ fn channel_ops_interleaved_with_monitor_churn_multi_thread() {
|
||||
}
|
||||
consumer.join().unwrap();
|
||||
});
|
||||
assert_eq!(total.load(std::sync::atomic::Ordering::Relaxed), (0..32).sum::<i64>());
|
||||
assert_eq!(
|
||||
total.load(std::sync::atomic::Ordering::Relaxed),
|
||||
(0..32).sum::<i64>()
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -220,7 +223,10 @@ fn recv_timeout_reports_disconnected_on_close() {
|
||||
fn recv_timeout_zero_duration_is_a_bounded_poll() {
|
||||
run(|| {
|
||||
let (_tx, rx) = channel::<i64>();
|
||||
assert_eq!(rx.recv_timeout(Duration::ZERO), Err(RecvTimeoutError::Timeout));
|
||||
assert_eq!(
|
||||
rx.recv_timeout(Duration::ZERO),
|
||||
Err(RecvTimeoutError::Timeout)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -262,15 +268,17 @@ fn recv_timeout_many_waiters_multi_thread() {
|
||||
let (tx, rx) = channel::<i64>();
|
||||
let got = got2.clone();
|
||||
let timed_out = timed_out2.clone();
|
||||
handles.push(spawn(move || match rx.recv_timeout(Duration::from_millis(100)) {
|
||||
Ok(v) => {
|
||||
assert_eq!(v, i);
|
||||
got.fetch_add(1, Ordering::Relaxed);
|
||||
handles.push(spawn(move || {
|
||||
match rx.recv_timeout(Duration::from_millis(100)) {
|
||||
Ok(v) => {
|
||||
assert_eq!(v, i);
|
||||
got.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
Err(RecvTimeoutError::Timeout) => {
|
||||
timed_out.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
Err(e) => panic!("unexpected: {e}"),
|
||||
}
|
||||
Err(RecvTimeoutError::Timeout) => {
|
||||
timed_out.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
Err(e) => panic!("unexpected: {e}"),
|
||||
}));
|
||||
if i % 2 == 0 {
|
||||
handles.push(spawn(move || {
|
||||
|
||||
+30
-11
@@ -11,9 +11,15 @@ thread_local! {
|
||||
static LOG: Cell<u64> = const { Cell::new(0) };
|
||||
}
|
||||
|
||||
fn log(v: u64) { LOG.with(|c| c.set(c.get() | v)); }
|
||||
fn get_log() -> u64 { LOG.with(|c| c.get()) }
|
||||
fn reset_log() { LOG.with(|c| c.set(0)); }
|
||||
fn log(v: u64) {
|
||||
LOG.with(|c| c.set(c.get() | v));
|
||||
}
|
||||
fn get_log() -> u64 {
|
||||
LOG.with(|c| c.get())
|
||||
}
|
||||
fn reset_log() {
|
||||
LOG.with(|c| c.set(0));
|
||||
}
|
||||
|
||||
extern "C-unwind" fn actor_simple() {
|
||||
log(0x1);
|
||||
@@ -56,7 +62,7 @@ fn actor_yields_and_resumes() {
|
||||
use std::sync::OnceLock;
|
||||
|
||||
static REG_BEFORE: OnceLock<[u64; 4]> = OnceLock::new();
|
||||
static REG_AFTER: OnceLock<[u64; 4]> = OnceLock::new();
|
||||
static REG_AFTER: OnceLock<[u64; 4]> = OnceLock::new();
|
||||
|
||||
extern "C-unwind" fn actor_reg_check() {
|
||||
unsafe {
|
||||
@@ -73,7 +79,10 @@ extern "C-unwind" fn actor_reg_check() {
|
||||
REG_BEFORE.set([s0, s1, s2, s3]).ok();
|
||||
switch_to_scheduler();
|
||||
|
||||
let a0: u64; let a1: u64; let a2: u64; let a3: u64;
|
||||
let a0: u64;
|
||||
let a1: u64;
|
||||
let a2: u64;
|
||||
let a3: u64;
|
||||
core::arch::asm!(
|
||||
"mov {a0}, r12", "mov {a1}, r13", "mov {a2}, r14", "mov {a3}, r15",
|
||||
a0 = out(reg) a0, a1 = out(reg) a1, a2 = out(reg) a2, a3 = out(reg) a3,
|
||||
@@ -88,8 +97,14 @@ fn callee_saved_registers_survive_yield() {
|
||||
let stack = Stack::new(64 * 1024, 4096).unwrap();
|
||||
let sp = init_actor_stack(stack.top(), actor_reg_check);
|
||||
set_actor_sp(sp);
|
||||
unsafe { switch_to_actor(); switch_to_actor(); }
|
||||
assert_eq!(REG_BEFORE.get().copied().unwrap(), REG_AFTER.get().copied().unwrap());
|
||||
unsafe {
|
||||
switch_to_actor();
|
||||
switch_to_actor();
|
||||
}
|
||||
assert_eq!(
|
||||
REG_BEFORE.get().copied().unwrap(),
|
||||
REG_AFTER.get().copied().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
// Two actors, independent stacks.
|
||||
@@ -123,14 +138,18 @@ fn two_actors_dont_corrupt_each_other() {
|
||||
let sp_a = init_actor_stack(stack_a.top(), actor_a);
|
||||
let sp_b = init_actor_stack(stack_b.top(), actor_b);
|
||||
|
||||
set_actor_sp(sp_a); unsafe { switch_to_actor() };
|
||||
set_actor_sp(sp_a);
|
||||
unsafe { switch_to_actor() };
|
||||
let sp_a = get_actor_sp();
|
||||
|
||||
set_actor_sp(sp_b); unsafe { switch_to_actor() };
|
||||
set_actor_sp(sp_b);
|
||||
unsafe { switch_to_actor() };
|
||||
let sp_b = get_actor_sp();
|
||||
|
||||
set_actor_sp(sp_a); unsafe { switch_to_actor() };
|
||||
set_actor_sp(sp_b); unsafe { switch_to_actor() };
|
||||
set_actor_sp(sp_a);
|
||||
unsafe { switch_to_actor() };
|
||||
set_actor_sp(sp_b);
|
||||
unsafe { switch_to_actor() };
|
||||
|
||||
assert_eq!(A_VAL.with(|c| c.get()), 0xA00D);
|
||||
assert_eq!(B_VAL.with(|c| c.get()), 0xB00D);
|
||||
|
||||
+22
-7
@@ -11,8 +11,8 @@
|
||||
//! OUTSIDE `run` — an in-actor assertion alone passes vacuously.
|
||||
|
||||
use smarm::{
|
||||
channel, run, select, select_timeout, spawn, try_select, wait_readable,
|
||||
wait_readable_timeout, wait_writable_timeout, yield_now, FdArm,
|
||||
channel, run, select, select_timeout, spawn, try_select, wait_readable, wait_readable_timeout,
|
||||
wait_writable_timeout, yield_now, FdArm,
|
||||
};
|
||||
use std::os::fd::RawFd;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
@@ -33,7 +33,10 @@ impl Pipe {
|
||||
let mut fds: [libc::c_int; 2] = [0; 2];
|
||||
let r = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC | libc::O_NONBLOCK) };
|
||||
assert_eq!(r, 0, "pipe2 failed");
|
||||
Pipe { read: fds[0], write: fds[1] }
|
||||
Pipe {
|
||||
read: fds[0],
|
||||
write: fds[1],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,12 +256,18 @@ fn wait_readable_timeout_times_out_then_succeeds_with_data() {
|
||||
let (rfd, wfd) = (p.read, p.write);
|
||||
|
||||
let start = Instant::now();
|
||||
assert_eq!(wait_readable_timeout(rfd, Duration::from_millis(30)).unwrap(), false);
|
||||
assert_eq!(
|
||||
wait_readable_timeout(rfd, Duration::from_millis(30)).unwrap(),
|
||||
false
|
||||
);
|
||||
assert!(start.elapsed() >= Duration::from_millis(30));
|
||||
|
||||
// Timed-out wait must leave the fd clean; ready path returns true.
|
||||
assert_eq!(raw_write(wfd, b"d"), 1);
|
||||
assert_eq!(wait_readable_timeout(rfd, Duration::from_secs(5)).unwrap(), true);
|
||||
assert_eq!(
|
||||
wait_readable_timeout(rfd, Duration::from_secs(5)).unwrap(),
|
||||
true
|
||||
);
|
||||
let mut buf = [0u8; 1];
|
||||
assert_eq!(raw_read(rfd, &mut buf), 1);
|
||||
ok2.store(true, Ordering::SeqCst);
|
||||
@@ -274,7 +283,10 @@ fn wait_readable_timeout_wakes_on_late_data() {
|
||||
let p = Pipe::new();
|
||||
let (rfd, wfd) = (p.read, p.write);
|
||||
let h = spawn(move || {
|
||||
assert_eq!(wait_readable_timeout(rfd, Duration::from_secs(5)).unwrap(), true);
|
||||
assert_eq!(
|
||||
wait_readable_timeout(rfd, Duration::from_secs(5)).unwrap(),
|
||||
true
|
||||
);
|
||||
let mut buf = [0u8; 1];
|
||||
assert_eq!(raw_read(rfd, &mut buf), 1);
|
||||
got2.store(buf[0] as u32, Ordering::SeqCst);
|
||||
@@ -292,7 +304,10 @@ fn wait_writable_timeout_ready_now_on_empty_pipe() {
|
||||
run(move || {
|
||||
let p = Pipe::new();
|
||||
// An empty pipe's write end is writable: ready-now path, no park.
|
||||
assert_eq!(wait_writable_timeout(p.write, Duration::from_secs(5)).unwrap(), true);
|
||||
assert_eq!(
|
||||
wait_writable_timeout(p.write, Duration::from_secs(5)).unwrap(),
|
||||
true
|
||||
);
|
||||
ok2.store(true, Ordering::SeqCst);
|
||||
});
|
||||
assert!(ok.load(Ordering::SeqCst));
|
||||
|
||||
+59
-15
@@ -403,7 +403,10 @@ fn worker_pool_down_reaches_handle_down() {
|
||||
let got = Arc::new(Mutex::new(Vec::new()));
|
||||
let got2 = got.clone();
|
||||
run(move || {
|
||||
let server = start(Pool { watcher: None, log: Vec::new() });
|
||||
let server = start(Pool {
|
||||
watcher: None,
|
||||
log: Vec::new(),
|
||||
});
|
||||
server.cast(PoolCast::SpawnDoomedWorker).unwrap();
|
||||
let _ = server.call(()).unwrap(); // sync point: cast handled, worker live
|
||||
*got2.lock().unwrap() = server.call(()).unwrap();
|
||||
@@ -421,7 +424,10 @@ fn watch_dead_pid_is_noproc_down() {
|
||||
let h = spawn(|| {});
|
||||
let dead = h.pid();
|
||||
h.join().unwrap();
|
||||
let server = start(Pool { watcher: None, log: Vec::new() });
|
||||
let server = start(Pool {
|
||||
watcher: None,
|
||||
log: Vec::new(),
|
||||
});
|
||||
server.cast(PoolCast::Watch(dead)).unwrap();
|
||||
*got2.lock().unwrap() = server.call(()).unwrap();
|
||||
});
|
||||
@@ -497,7 +503,12 @@ impl GenServer for Timed {
|
||||
}
|
||||
|
||||
fn timed(fired: Arc<Mutex<Vec<u32>>>, cancel_won: Arc<Mutex<Option<bool>>>) -> Timed {
|
||||
Timed { timer: None, fired, cancel_won, last: None }
|
||||
Timed {
|
||||
timer: None,
|
||||
fired,
|
||||
cancel_won,
|
||||
last: None,
|
||||
}
|
||||
}
|
||||
|
||||
// A one-shot armed from a handler fires into handle_timer with its payload.
|
||||
@@ -534,7 +545,11 @@ fn cancel_before_fire_suppresses_it() {
|
||||
let count = server.call(()).unwrap();
|
||||
assert_eq!(count, 0, "cancelled timer must not fire");
|
||||
});
|
||||
assert_eq!(*cancel_won.lock().unwrap(), Some(true), "cancel beat the fire");
|
||||
assert_eq!(
|
||||
*cancel_won.lock().unwrap(),
|
||||
Some(true),
|
||||
"cancel beat the fire"
|
||||
);
|
||||
assert!(fired.lock().unwrap().is_empty());
|
||||
}
|
||||
|
||||
@@ -549,11 +564,16 @@ fn tick_every_rearms_repeatedly() {
|
||||
run(move || {
|
||||
let cw = Arc::new(Mutex::new(None));
|
||||
let server = start(timed(f2, cw));
|
||||
server.cast(TkCast::Tick(Duration::from_millis(20))).unwrap();
|
||||
server
|
||||
.cast(TkCast::Tick(Duration::from_millis(20)))
|
||||
.unwrap();
|
||||
let _ = server.call(()).unwrap(); // sync: periodic armed
|
||||
smarm::sleep(Duration::from_millis(130)); // ~6 periods
|
||||
let count = server.call(()).unwrap();
|
||||
assert!(count >= 3, "periodic should have re-armed several times, got {count}");
|
||||
assert!(
|
||||
count >= 3,
|
||||
"periodic should have re-armed several times, got {count}"
|
||||
);
|
||||
});
|
||||
// Every tick delivered the same payload.
|
||||
assert!(fired.lock().unwrap().iter().all(|&v| v == 9));
|
||||
@@ -568,7 +588,9 @@ fn cancel_stops_a_periodic() {
|
||||
let c2 = cancel_won.clone();
|
||||
run(move || {
|
||||
let server = start(timed(f2, c2));
|
||||
server.cast(TkCast::Tick(Duration::from_millis(20))).unwrap();
|
||||
server
|
||||
.cast(TkCast::Tick(Duration::from_millis(20)))
|
||||
.unwrap();
|
||||
let _ = server.call(()).unwrap();
|
||||
smarm::sleep(Duration::from_millis(70)); // a few ticks
|
||||
server.cast(TkCast::CancelLast).unwrap();
|
||||
@@ -616,11 +638,17 @@ fn idle_fires_repeatedly_on_quiet() {
|
||||
let idles = Arc::new(Mutex::new(0));
|
||||
let i2 = idles.clone();
|
||||
run(move || {
|
||||
let server = start(Idler { window: Duration::from_millis(25), idles: i2 });
|
||||
let server = start(Idler {
|
||||
window: Duration::from_millis(25),
|
||||
idles: i2,
|
||||
});
|
||||
smarm::sleep(Duration::from_millis(130)); // quiet ⇒ ~5 windows
|
||||
drop(server); // keep the server alive across the quiet span
|
||||
});
|
||||
assert!(*idles.lock().unwrap() >= 2, "idle should re-arm and fire several times");
|
||||
assert!(
|
||||
*idles.lock().unwrap() >= 2,
|
||||
"idle should re-arm and fire several times"
|
||||
);
|
||||
}
|
||||
|
||||
// Traffic within the window keeps idle from firing; only once the inbox goes
|
||||
@@ -632,7 +660,10 @@ fn traffic_resets_the_idle_window() {
|
||||
let before_quiet = Arc::new(Mutex::new(u32::MAX));
|
||||
let bq = before_quiet.clone();
|
||||
run(move || {
|
||||
let server = start(Idler { window: Duration::from_millis(60), idles: i2 });
|
||||
let server = start(Idler {
|
||||
window: Duration::from_millis(60),
|
||||
idles: i2,
|
||||
});
|
||||
// Poke every 25ms (< 60ms window) for ~100ms: each cast resets the
|
||||
// window before it can elapse.
|
||||
for _ in 0..4 {
|
||||
@@ -643,8 +674,15 @@ fn traffic_resets_the_idle_window() {
|
||||
smarm::sleep(Duration::from_millis(140)); // now genuinely quiet
|
||||
drop(server);
|
||||
});
|
||||
assert_eq!(*before_quiet.lock().unwrap(), 0, "steady traffic must suppress idle");
|
||||
assert!(*idles.lock().unwrap() >= 1, "idle fires once the inbox falls quiet");
|
||||
assert_eq!(
|
||||
*before_quiet.lock().unwrap(),
|
||||
0,
|
||||
"steady traffic must suppress idle"
|
||||
);
|
||||
assert!(
|
||||
*idles.lock().unwrap() >= 1,
|
||||
"idle fires once the inbox falls quiet"
|
||||
);
|
||||
}
|
||||
|
||||
// RFC 015 §4.7 — no armed timer survives loop exit. A server with a live
|
||||
@@ -658,15 +696,21 @@ fn no_timer_survives_exit() {
|
||||
let f_read = fired.clone();
|
||||
run(move || {
|
||||
let server = start(timed(f_server, Arc::new(Mutex::new(None))));
|
||||
server.cast(TkCast::Tick(Duration::from_millis(15))).unwrap();
|
||||
server
|
||||
.cast(TkCast::Tick(Duration::from_millis(15)))
|
||||
.unwrap();
|
||||
let _ = server.call(()).unwrap(); // sync: periodic armed
|
||||
smarm::sleep(Duration::from_millis(45)); // a couple of ticks
|
||||
let mon = smarm::monitor(server.pid());
|
||||
drop(server); // inbox closes → loop exits → guard drains timers
|
||||
// Clean Down ⇒ the loop returned without the no-leak assert aborting.
|
||||
// Clean Down ⇒ the loop returned without the no-leak assert aborting.
|
||||
assert!(mon.rx.recv().is_ok());
|
||||
let at_exit = f_read.lock().unwrap().len();
|
||||
smarm::sleep(Duration::from_millis(90)); // would be several more ticks
|
||||
assert_eq!(f_read.lock().unwrap().len(), at_exit, "no tick may fire after exit");
|
||||
assert_eq!(
|
||||
f_read.lock().unwrap().len(),
|
||||
at_exit,
|
||||
"no tick may fire after exit"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
+91
-15
@@ -100,7 +100,15 @@ fn state_timeout_fires() {
|
||||
let got = Arc::new(Mutex::new(0u32));
|
||||
let got2 = got.clone();
|
||||
run(move || {
|
||||
let m = TimerSm::start(T::Idle, TData { enters: 0, st_fires: 0, named_fires: 0, st_window: 5 });
|
||||
let m = TimerSm::start(
|
||||
T::Idle,
|
||||
TData {
|
||||
enters: 0,
|
||||
st_fires: 0,
|
||||
named_fires: 0,
|
||||
st_window: 5,
|
||||
},
|
||||
);
|
||||
m.send(Ev2::Cast(TCast::Arm)).unwrap(); // -> Armed, arms 5ms state-timeout
|
||||
smarm::sleep(Duration::from_millis(40)); // let it fire
|
||||
*got2.lock().unwrap() = m.call(|r| Ev2::Call(TCall::StFires(r))).unwrap();
|
||||
@@ -116,13 +124,25 @@ fn state_timeout_auto_resets_on_transition() {
|
||||
let got2 = got.clone();
|
||||
run(move || {
|
||||
// Long window so the explicit Disarm beats it comfortably.
|
||||
let m = TimerSm::start(T::Idle, TData { enters: 0, st_fires: 0, named_fires: 0, st_window: 50 });
|
||||
let m = TimerSm::start(
|
||||
T::Idle,
|
||||
TData {
|
||||
enters: 0,
|
||||
st_fires: 0,
|
||||
named_fires: 0,
|
||||
st_window: 50,
|
||||
},
|
||||
);
|
||||
m.send(Ev2::Cast(TCast::Arm)).unwrap(); // -> Armed, arms 50ms state-timeout
|
||||
m.send(Ev2::Cast(TCast::Disarm)).unwrap(); // -> Idle, auto-resets it
|
||||
smarm::sleep(Duration::from_millis(80)); // past the original window
|
||||
*got2.lock().unwrap() = m.call(|r| Ev2::Call(TCall::StFires(r))).unwrap();
|
||||
});
|
||||
assert_eq!(*got.lock().unwrap(), 0, "auto-reset cancelled the pending state-timeout");
|
||||
assert_eq!(
|
||||
*got.lock().unwrap(),
|
||||
0,
|
||||
"auto-reset cancelled the pending state-timeout"
|
||||
);
|
||||
}
|
||||
|
||||
// A named timeout survives a state change: armed in Idle, it still fires after
|
||||
@@ -133,14 +153,26 @@ fn named_timeout_survives_transition() {
|
||||
let got2 = got.clone();
|
||||
run(move || {
|
||||
// Armed's own state-timeout is long so it doesn't interfere.
|
||||
let m = TimerSm::start(T::Idle, TData { enters: 0, st_fires: 0, named_fires: 0, st_window: 200 });
|
||||
let m = TimerSm::start(
|
||||
T::Idle,
|
||||
TData {
|
||||
enters: 0,
|
||||
st_fires: 0,
|
||||
named_fires: 0,
|
||||
st_window: 200,
|
||||
},
|
||||
);
|
||||
m.send(Ev2::Cast(TCast::Ping(20))).unwrap(); // arm "ping" for 20ms (in Idle)
|
||||
m.send(Ev2::Cast(TCast::Arm)).unwrap(); // -> Armed (ping must survive this)
|
||||
m.send(Ev2::Cast(TCast::Disarm)).unwrap(); // -> Idle (and this)
|
||||
smarm::sleep(Duration::from_millis(60)); // let "ping" fire
|
||||
*got2.lock().unwrap() = m.call(|r| Ev2::Call(TCall::NamedFires(r))).unwrap();
|
||||
});
|
||||
assert_eq!(*got.lock().unwrap(), 1, "named timeout fired across the transitions");
|
||||
assert_eq!(
|
||||
*got.lock().unwrap(),
|
||||
1,
|
||||
"named timeout fired across the transitions"
|
||||
);
|
||||
}
|
||||
|
||||
// Cancelling a named timeout before its window prevents the fire.
|
||||
@@ -149,13 +181,25 @@ fn named_timeout_cancel() {
|
||||
let got = Arc::new(Mutex::new(99u32));
|
||||
let got2 = got.clone();
|
||||
run(move || {
|
||||
let m = TimerSm::start(T::Idle, TData { enters: 0, st_fires: 0, named_fires: 0, st_window: 200 });
|
||||
let m = TimerSm::start(
|
||||
T::Idle,
|
||||
TData {
|
||||
enters: 0,
|
||||
st_fires: 0,
|
||||
named_fires: 0,
|
||||
st_window: 200,
|
||||
},
|
||||
);
|
||||
m.send(Ev2::Cast(TCast::Ping(30))).unwrap(); // arm "ping" for 30ms
|
||||
m.send(Ev2::Cast(TCast::CancelPing)).unwrap(); // cancel before it fires
|
||||
smarm::sleep(Duration::from_millis(60)); // past the original window
|
||||
*got2.lock().unwrap() = m.call(|r| Ev2::Call(TCall::NamedFires(r))).unwrap();
|
||||
});
|
||||
assert_eq!(*got.lock().unwrap(), 0, "cancel prevented the named-timeout fire");
|
||||
assert_eq!(
|
||||
*got.lock().unwrap(),
|
||||
0,
|
||||
"cancel prevented the named-timeout fire"
|
||||
);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
@@ -170,15 +214,27 @@ fn cast_then_call_roundtrip() {
|
||||
let got2 = got.clone();
|
||||
run(move || {
|
||||
// Long state-timeout window so it never fires during the test.
|
||||
let m = TimerSm::start(T::Idle, TData { enters: 0, st_fires: 0, named_fires: 0, st_window: 10_000 });
|
||||
m.send(Ev2::Cast(TCast::Arm)).unwrap(); // Idle -> Armed (enter)
|
||||
let m = TimerSm::start(
|
||||
T::Idle,
|
||||
TData {
|
||||
enters: 0,
|
||||
st_fires: 0,
|
||||
named_fires: 0,
|
||||
st_window: 10_000,
|
||||
},
|
||||
);
|
||||
m.send(Ev2::Cast(TCast::Arm)).unwrap(); // Idle -> Armed (enter)
|
||||
m.send(Ev2::Cast(TCast::Disarm)).unwrap(); // Armed -> Idle (enter)
|
||||
m.send(Ev2::Cast(TCast::Arm)).unwrap(); // Idle -> Armed (enter)
|
||||
m.send(Ev2::Cast(TCast::Arm)).unwrap(); // Idle -> Armed (enter)
|
||||
m.send(Ev2::Cast(TCast::Disarm)).unwrap(); // Armed -> Idle (enter)
|
||||
// enters = 1 (start) + 4 transitions = 5.
|
||||
// enters = 1 (start) + 4 transitions = 5.
|
||||
*got2.lock().unwrap() = m.call(|r| Ev2::Call(TCall::Enters(r))).unwrap();
|
||||
});
|
||||
assert_eq!(*got.lock().unwrap(), 5, "one enter on start, one per real transition");
|
||||
assert_eq!(
|
||||
*got.lock().unwrap(),
|
||||
5,
|
||||
"one enter on start, one per real transition"
|
||||
);
|
||||
}
|
||||
|
||||
// `enter` fires once on start and once per *real* transition; a stay (a call
|
||||
@@ -188,7 +244,15 @@ fn enter_on_start_and_each_transition_but_not_stay() {
|
||||
let got = Arc::new(Mutex::new((0u32, 0u32, 0u32)));
|
||||
let got2 = got.clone();
|
||||
run(move || {
|
||||
let m = TimerSm::start(T::Idle, TData { enters: 0, st_fires: 0, named_fires: 0, st_window: 10_000 }); // enter -> 1
|
||||
let m = TimerSm::start(
|
||||
T::Idle,
|
||||
TData {
|
||||
enters: 0,
|
||||
st_fires: 0,
|
||||
named_fires: 0,
|
||||
st_window: 10_000,
|
||||
},
|
||||
); // enter -> 1
|
||||
let after_start = m.call(|r| Ev2::Call(TCall::Enters(r))).unwrap();
|
||||
// A stay (a counter read returns `prev`) must not bump enters.
|
||||
let _ = m.call(|r| Ev2::Call(TCall::StFires(r))).unwrap();
|
||||
@@ -208,7 +272,15 @@ fn call_to_panicking_handler_is_down() {
|
||||
let got = Arc::new(Mutex::new(None::<Result<u32, CallError>>));
|
||||
let got2 = got.clone();
|
||||
run(move || {
|
||||
let m = TimerSm::start(T::Idle, TData { enters: 0, st_fires: 0, named_fires: 0, st_window: 10_000 });
|
||||
let m = TimerSm::start(
|
||||
T::Idle,
|
||||
TData {
|
||||
enters: 0,
|
||||
st_fires: 0,
|
||||
named_fires: 0,
|
||||
st_window: 10_000,
|
||||
},
|
||||
);
|
||||
let r = m.call(|rep| Ev2::Call(TCall::Boom(rep)));
|
||||
*got2.lock().unwrap() = Some(r);
|
||||
});
|
||||
@@ -299,7 +371,11 @@ fn postponed_call_answered_after_transition() {
|
||||
smarm::sleep(Duration::from_millis(20)); // let the child wake with its reply
|
||||
*g2.lock().unwrap() = *taken.lock().unwrap();
|
||||
});
|
||||
assert_eq!(*got.lock().unwrap(), Some(42), "postponed call answered by the Filled state");
|
||||
assert_eq!(
|
||||
*got.lock().unwrap(),
|
||||
Some(42),
|
||||
"postponed call answered by the Filled state"
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
|
||||
+40
-9
@@ -56,7 +56,11 @@ fn snapshot_lists_actors_with_parent_edge() {
|
||||
|
||||
// The root itself is on-CPU (it's running this code) and rooted under
|
||||
// the forest sentinel.
|
||||
let root = snap.actors.iter().find(|a| a.pid == me).expect("root present");
|
||||
let root = snap
|
||||
.actors
|
||||
.iter()
|
||||
.find(|a| a.pid == me)
|
||||
.expect("root present");
|
||||
assert_eq!(root.state, ActorState::Running);
|
||||
assert_eq!(root.supervisor, smarm::Pid::new(u32::MAX, u32::MAX));
|
||||
|
||||
@@ -200,7 +204,11 @@ fn tree_places_child_under_its_spawner() {
|
||||
|
||||
// The root is parented at the forest sentinel, so it's a genuine root,
|
||||
// and the worker it spawned hangs beneath it.
|
||||
let root = t.roots.iter().find(|n| n.info.pid == me).expect("root in forest");
|
||||
let root = t
|
||||
.roots
|
||||
.iter()
|
||||
.find(|n| n.info.pid == me)
|
||||
.expect("root in forest");
|
||||
assert!(!root.orphaned);
|
||||
assert!(
|
||||
root.children.iter().any(|c| c.info.pid == h.pid()),
|
||||
@@ -258,14 +266,25 @@ fn tree_from_nests_children_and_reroots_orphans() {
|
||||
let t = tree_from(snap);
|
||||
assert_eq!(t.roots.len(), 2);
|
||||
|
||||
let root = t.roots.iter().find(|n| n.info.pid == root_pid).expect("root present");
|
||||
let root = t
|
||||
.roots
|
||||
.iter()
|
||||
.find(|n| n.info.pid == root_pid)
|
||||
.expect("root present");
|
||||
assert!(!root.orphaned);
|
||||
assert_eq!(root.children.len(), 1);
|
||||
assert_eq!(root.children[0].info.pid, child);
|
||||
assert!(!root.children[0].orphaned);
|
||||
|
||||
let o = t.roots.iter().find(|n| n.info.pid == orphan).expect("orphan re-rooted");
|
||||
assert!(o.orphaned, "an actor whose parent is absent must be flagged orphaned");
|
||||
let o = t
|
||||
.roots
|
||||
.iter()
|
||||
.find(|n| n.info.pid == orphan)
|
||||
.expect("orphan re-rooted");
|
||||
assert!(
|
||||
o.orphaned,
|
||||
"an actor whose parent is absent must be flagged orphaned"
|
||||
);
|
||||
assert!(o.children.is_empty());
|
||||
}
|
||||
|
||||
@@ -398,7 +417,11 @@ fn stack_info_reports_defaults_and_sampled_depth() {
|
||||
let info = spin_until(h.pid(), |a| a.state == ActorState::Parked);
|
||||
let s = info.stack;
|
||||
assert_eq!(s.reserve, 64 * 1024, "default reserve");
|
||||
assert_eq!(s.guard, 1024 * 1024, "default guard (kernel stack_guard_gap convention)");
|
||||
assert_eq!(
|
||||
s.guard,
|
||||
1024 * 1024,
|
||||
"default guard (kernel stack_guard_gap convention)"
|
||||
);
|
||||
assert!(
|
||||
s.depth_high_water >= 8 * 4096,
|
||||
"hwm sampled at the deep yield: expected ≥ 32 KiB, got {}",
|
||||
@@ -432,7 +455,10 @@ fn stack_info_shrink_counters_are_live() {
|
||||
let spike = 768 * 4096;
|
||||
assert!(spike > SHRINK_THRESHOLD);
|
||||
let worker = spawn_with(
|
||||
SpawnOpts { stack_reserve: Some(8 * 1024 * 1024), ..SpawnOpts::default() },
|
||||
SpawnOpts {
|
||||
stack_reserve: Some(8 * 1024 * 1024),
|
||||
..SpawnOpts::default()
|
||||
},
|
||||
move || {
|
||||
std::hint::black_box(burn_stack_yielding(768));
|
||||
for _ in 0..(SHRINK_COOLDOWN + 8) {
|
||||
@@ -457,9 +483,14 @@ fn stack_info_shrink_counters_are_live() {
|
||||
spin_until(wpid, |a| a.state == ActorState::Parked);
|
||||
park_tx.send(()).unwrap();
|
||||
}
|
||||
let info = spin_until(wpid, |a| a.state == ActorState::Parked && a.stack.shrinks >= 1);
|
||||
let info = spin_until(wpid, |a| {
|
||||
a.state == ActorState::Parked && a.stack.shrinks >= 1
|
||||
});
|
||||
let s = info.stack;
|
||||
assert!(s.shrinks >= 1, "cooldown was crossed with a spike above threshold");
|
||||
assert!(
|
||||
s.shrinks >= 1,
|
||||
"cooldown was crossed with a spike above threshold"
|
||||
);
|
||||
assert!(
|
||||
s.parks_since_shrink < SHRINK_COOLDOWN,
|
||||
"counter must reset at shrink: {}",
|
||||
|
||||
+13
-3
@@ -56,8 +56,16 @@ fn other_actors_run_while_block_on_io_is_in_flight() {
|
||||
let pos_2 = v.iter().position(|&x| x == 2).unwrap();
|
||||
let pos_3 = v.iter().position(|&x| x == 3).unwrap();
|
||||
let pos_4 = v.iter().position(|&x| x == 4).unwrap();
|
||||
assert!(pos_2 < pos_4, "B's first step ran after A resumed: {:?}", *v);
|
||||
assert!(pos_3 < pos_4, "B's second step ran after A resumed: {:?}", *v);
|
||||
assert!(
|
||||
pos_2 < pos_4,
|
||||
"B's first step ran after A resumed: {:?}",
|
||||
*v
|
||||
);
|
||||
assert!(
|
||||
pos_3 < pos_4,
|
||||
"B's second step ran after A resumed: {:?}",
|
||||
*v
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -76,7 +84,9 @@ fn many_concurrent_block_on_io_calls_all_complete() {
|
||||
cc.fetch_add(n, Ordering::SeqCst);
|
||||
}));
|
||||
}
|
||||
for h in handles { h.join().unwrap(); }
|
||||
for h in handles {
|
||||
h.join().unwrap();
|
||||
}
|
||||
});
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 10);
|
||||
}
|
||||
|
||||
+11
-4
@@ -144,8 +144,7 @@ fn write_sugar_sends_bytes_to_pipe() {
|
||||
// Pipe is empty + has buffer space, so this returns immediately
|
||||
// after wait_writable wakes (which happens fast because the
|
||||
// kernel marks an empty pipe as immediately writable).
|
||||
let n = smarm::scheduler::write(p_writer.write, b"smarm")
|
||||
.expect("write failed");
|
||||
let n = smarm::scheduler::write(p_writer.write, b"smarm").expect("write failed");
|
||||
assert_eq!(n, 5);
|
||||
c.fetch_add(1, Ordering::SeqCst);
|
||||
});
|
||||
@@ -209,10 +208,18 @@ fn other_actors_run_while_one_is_parked_on_wait_readable() {
|
||||
let pos_lit_a = v.iter().position(|&c| c == b'a').unwrap();
|
||||
let big_b_count = v.iter().filter(|&&c| c == b'B').count();
|
||||
assert_eq!(big_b_count, 3, "B should have made 3 steps: {:?}", *v);
|
||||
assert!(pos_big_a < pos_lit_a, "A pre-park before A post-park: {:?}", *v);
|
||||
assert!(
|
||||
pos_big_a < pos_lit_a,
|
||||
"A pre-park before A post-park: {:?}",
|
||||
*v
|
||||
);
|
||||
// At least the last B step should be before A resumes.
|
||||
let last_big_b = v.iter().rposition(|&c| c == b'B').unwrap();
|
||||
assert!(last_big_b < pos_lit_a, "B should finish before A resumes: {:?}", *v);
|
||||
assert!(
|
||||
last_big_b < pos_lit_a,
|
||||
"B should finish before A resumes: {:?}",
|
||||
*v
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
+10
-4
@@ -57,7 +57,10 @@ fn linked_pair_one_panics_other_is_stopped() {
|
||||
panic!("boom");
|
||||
});
|
||||
|
||||
let dn = down_b.rx.recv().expect("monitor channel closed before Down");
|
||||
let dn = down_b
|
||||
.rx
|
||||
.recv()
|
||||
.expect("monitor channel closed before Down");
|
||||
assert_eq!(dn.pid, b, "Down reported the wrong pid");
|
||||
if matches!(dn.reason, DownReason::Stopped) {
|
||||
s.store(true, Ordering::SeqCst);
|
||||
@@ -117,7 +120,7 @@ fn normal_exit_does_not_propagate() {
|
||||
let a = ha.pid();
|
||||
link(a);
|
||||
yield_now(); // let A run to completion and finalize
|
||||
// A exited normally: nothing should have landed on the inbox.
|
||||
// A exited normally: nothing should have landed on the inbox.
|
||||
if let Ok(None) = inbox.try_recv() {
|
||||
e.store(true, Ordering::SeqCst);
|
||||
}
|
||||
@@ -152,7 +155,10 @@ fn link_to_dead_pid_stops_a_nontrapping_caller() {
|
||||
});
|
||||
let b = hb.pid();
|
||||
let down_b = monitor(b);
|
||||
let dn = down_b.rx.recv().expect("monitor channel closed before Down");
|
||||
let dn = down_b
|
||||
.rx
|
||||
.recv()
|
||||
.expect("monitor channel closed before Down");
|
||||
if matches!(dn.reason, DownReason::Stopped) {
|
||||
s.store(true, Ordering::SeqCst);
|
||||
}
|
||||
@@ -208,7 +214,7 @@ fn unlink_prevents_propagation() {
|
||||
panic!("boom"); // abnormal, but the link is gone
|
||||
});
|
||||
yield_now(); // let A link, unlink, and panic
|
||||
// Unlinked before death → no ExitSignal should have arrived.
|
||||
// Unlinked before death → no ExitSignal should have arrived.
|
||||
if let Ok(None) = inbox.try_recv() {
|
||||
sv.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
+27
-6
@@ -67,7 +67,10 @@ fn monitor_already_dead_target_is_noproc() {
|
||||
// and its generation bumped, so `pid` is now stale.
|
||||
h.join().unwrap();
|
||||
let down = monitor(pid);
|
||||
let d = down.rx.recv().expect("NoProc Down should be delivered immediately");
|
||||
let d = down
|
||||
.rx
|
||||
.recv()
|
||||
.expect("NoProc Down should be delivered immediately");
|
||||
assert_eq!(d.pid, pid);
|
||||
if matches!(d.reason, DownReason::NoProc) {
|
||||
o.store(true, Ordering::SeqCst);
|
||||
@@ -91,7 +94,11 @@ fn multiple_monitors_all_notified() {
|
||||
}
|
||||
}
|
||||
});
|
||||
assert_eq!(count.load(Ordering::SeqCst), 3, "every monitor should see the Down");
|
||||
assert_eq!(
|
||||
count.load(Ordering::SeqCst),
|
||||
3,
|
||||
"every monitor should see the Down"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -103,8 +110,15 @@ fn demonitor_stops_delivery() {
|
||||
let h = spawn(|| {});
|
||||
let pid = h.pid();
|
||||
let m = monitor(pid);
|
||||
assert_eq!(demonitor(&m), Some(m.id), "live registration should be removed");
|
||||
assert!(m.rx.recv().is_err(), "no Down should arrive after demonitor");
|
||||
assert_eq!(
|
||||
demonitor(&m),
|
||||
Some(m.id),
|
||||
"live registration should be removed"
|
||||
);
|
||||
assert!(
|
||||
m.rx.recv().is_err(),
|
||||
"no Down should arrive after demonitor"
|
||||
);
|
||||
let _ = h.join();
|
||||
});
|
||||
}
|
||||
@@ -122,7 +136,10 @@ fn demonitor_one_of_many() {
|
||||
let _ = h.join();
|
||||
assert!(matches!(ms[0].rx.recv().unwrap().reason, DownReason::Exit));
|
||||
assert!(matches!(ms[2].rx.recv().unwrap().reason, DownReason::Exit));
|
||||
assert!(ms[1].rx.recv().is_err(), "demonitored channel should be closed");
|
||||
assert!(
|
||||
ms[1].rx.recv().is_err(),
|
||||
"demonitored channel should be closed"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -136,7 +153,11 @@ fn demonitor_after_fire_is_none() {
|
||||
let m = monitor(pid);
|
||||
let d = m.rx.recv().expect("Down before close");
|
||||
assert!(matches!(d.reason, DownReason::Exit));
|
||||
assert_eq!(demonitor(&m), None, "already-fired monitor has nothing to remove");
|
||||
assert_eq!(
|
||||
demonitor(&m),
|
||||
None,
|
||||
"already-fired monitor has nothing to remove"
|
||||
);
|
||||
let _ = h.join();
|
||||
});
|
||||
}
|
||||
|
||||
+16
-4
@@ -3,9 +3,9 @@
|
||||
//! needs to be able to park.
|
||||
|
||||
use smarm::{run, spawn, yield_now, LockTimeout, Mutex};
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex as StdMutex;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -111,8 +111,16 @@ fn contended_lock_parks_until_holder_releases() {
|
||||
let pos_b_locked = v.iter().position(|s| *s == "B_locked").unwrap();
|
||||
|
||||
assert!(pos_a_locked < pos_b_try, "log: {:?}", *v);
|
||||
assert!(pos_b_try < pos_a_dropped, "B should attempt before A drops: {:?}", *v);
|
||||
assert!(pos_a_dropped < pos_b_locked, "B should lock only after A drops: {:?}", *v);
|
||||
assert!(
|
||||
pos_b_try < pos_a_dropped,
|
||||
"B should attempt before A drops: {:?}",
|
||||
*v
|
||||
);
|
||||
assert!(
|
||||
pos_a_dropped < pos_b_locked,
|
||||
"B should lock only after A drops: {:?}",
|
||||
*v
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -209,7 +217,11 @@ fn waiters_are_granted_the_lock_in_fifo_order() {
|
||||
});
|
||||
|
||||
let v = order.lock().unwrap().clone();
|
||||
assert_eq!(v, vec![1, 2, 3, 4], "waiters should acquire in arrival order");
|
||||
assert_eq!(
|
||||
v,
|
||||
vec![1, 2, 3, 4],
|
||||
"waiters should acquire in arrival order"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
+5
-3
@@ -77,8 +77,7 @@ fn observer_reports_none_for_a_forged_pid() {
|
||||
// An index that is not in the slab at all — the verb relays the
|
||||
// primitive's `None` faithfully.
|
||||
let forged = smarm::Pid::new(u32::MAX - 1, 0);
|
||||
let ObserverReply::ActorInfo(none) =
|
||||
obs.call(ObserverRequest::ActorInfo(forged)).unwrap()
|
||||
let ObserverReply::ActorInfo(none) = obs.call(ObserverRequest::ActorInfo(forged)).unwrap()
|
||||
else {
|
||||
panic!("ActorInfo verb must reply ActorInfo");
|
||||
};
|
||||
@@ -113,7 +112,10 @@ fn observer_sees_a_parked_actor_as_parked() {
|
||||
}
|
||||
smarm::yield_now();
|
||||
}
|
||||
assert!(parked, "observer should eventually report the worker as Parked");
|
||||
assert!(
|
||||
parked,
|
||||
"observer should eventually report the worker as Parked"
|
||||
);
|
||||
|
||||
gate_tx.send(()).unwrap();
|
||||
worker.join().unwrap();
|
||||
|
||||
+13
-3
@@ -44,7 +44,10 @@ fn a_dead_actor_vanishes_from_every_group_it_joined() {
|
||||
// Drain-on-contact: touching g1 detects the death and sweeps the pid
|
||||
// out of every group (g2 included), not just g1.
|
||||
assert!(members("g1").is_empty(), "evicted from the touched group");
|
||||
assert!(members("g2").is_empty(), "and swept from the untouched group");
|
||||
assert!(
|
||||
members("g2").is_empty(),
|
||||
"and swept from the untouched group"
|
||||
);
|
||||
assert_eq!(pick("g1"), None);
|
||||
});
|
||||
}
|
||||
@@ -83,7 +86,11 @@ fn live_members_survive_a_peers_death() {
|
||||
tx_a.send(()).unwrap();
|
||||
a.join().unwrap();
|
||||
|
||||
assert_eq!(members("svc"), vec![b.pid()], "only the dead peer is reaped");
|
||||
assert_eq!(
|
||||
members("svc"),
|
||||
vec![b.pid()],
|
||||
"only the dead peer is reaped"
|
||||
);
|
||||
assert_eq!(pick("svc"), Some(b.pid()));
|
||||
|
||||
tx_b.send(()).unwrap();
|
||||
@@ -125,7 +132,10 @@ fn joining_an_already_dead_pid_is_evicted_on_next_contact() {
|
||||
// monitor() on a gone pid queues a NoProc Down immediately, so the
|
||||
// membership is reaped the next time the group is touched.
|
||||
join("late", pid);
|
||||
assert!(members("late").is_empty(), "dead-at-join member is reaped on read");
|
||||
assert!(
|
||||
members("late").is_empty(),
|
||||
"dead-at-join member is reaped on read"
|
||||
);
|
||||
assert_eq!(pick("late"), None);
|
||||
});
|
||||
}
|
||||
|
||||
+10
-2
@@ -41,7 +41,11 @@ fn stop_storm_does_not_poison_runtime() {
|
||||
}
|
||||
c.fetch_add(1, Ordering::SeqCst);
|
||||
});
|
||||
assert_eq!(completed.load(Ordering::SeqCst), 1, "root completed cleanly");
|
||||
assert_eq!(
|
||||
completed.load(Ordering::SeqCst),
|
||||
1,
|
||||
"root completed cleanly"
|
||||
);
|
||||
}
|
||||
|
||||
/// The sharper repro: a stop-flagged actor whose *next allocation* is the
|
||||
@@ -85,5 +89,9 @@ fn self_stop_during_spawn_does_not_poison_shared_mutex() {
|
||||
}
|
||||
c.fetch_add(1, Ordering::SeqCst);
|
||||
});
|
||||
assert_eq!(completed.load(Ordering::SeqCst), 1, "root completed cleanly");
|
||||
assert_eq!(
|
||||
completed.load(Ordering::SeqCst),
|
||||
1,
|
||||
"root completed cleanly"
|
||||
);
|
||||
}
|
||||
|
||||
+15
-4
@@ -43,10 +43,21 @@ fn check_yields_when_timeslice_expired() {
|
||||
let pos_big_b = v.iter().position(|&c| c == b'B').unwrap();
|
||||
let pos_lit_a = v.iter().position(|&c| c == b'a').unwrap();
|
||||
let pos_lit_b = v.iter().position(|&c| c == b'b').unwrap();
|
||||
assert!(pos_big_a < pos_lit_a, "A's tail ran before B's head: {:?}", *v);
|
||||
assert!(pos_big_b < pos_lit_b, "B's tail ran before A's head: {:?}", *v);
|
||||
assert!(pos_big_a.max(pos_big_b) < pos_lit_a.min(pos_lit_b),
|
||||
"preemption didn't interleave: {:?}", *v);
|
||||
assert!(
|
||||
pos_big_a < pos_lit_a,
|
||||
"A's tail ran before B's head: {:?}",
|
||||
*v
|
||||
);
|
||||
assert!(
|
||||
pos_big_b < pos_lit_b,
|
||||
"B's tail ran before A's head: {:?}",
|
||||
*v
|
||||
);
|
||||
assert!(
|
||||
pos_big_a.max(pos_big_b) < pos_lit_a.min(pos_lit_b),
|
||||
"preemption didn't interleave: {:?}",
|
||||
*v
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+13
-4
@@ -65,7 +65,10 @@ fn name_held_by_live_actor_is_taken() {
|
||||
ready_rx.recv().unwrap();
|
||||
// Root tries to claim a live actor's name for itself -> NameTaken.
|
||||
let (tx_b, _rx_b) = channel::<u64>();
|
||||
assert_eq!(register(SVC, tx_b), Err(RegisterError::NameTaken { holder: a.pid() }));
|
||||
assert_eq!(
|
||||
register(SVC, tx_b),
|
||||
Err(RegisterError::NameTaken { holder: a.pid() })
|
||||
);
|
||||
send(SVC, 0).unwrap(); // release a (delivers to the holder, a)
|
||||
a.join().unwrap();
|
||||
});
|
||||
@@ -105,7 +108,10 @@ fn dead_holder_is_pruned_and_name_taken_over() {
|
||||
fn send_errors_unresolved_and_no_channel() {
|
||||
run(|| {
|
||||
// No actor at all.
|
||||
assert!(matches!(send(Name::<u64>::new("ghost"), 1u64), Err(SendError::Unresolved(_))));
|
||||
assert!(matches!(
|
||||
send(Name::<u64>::new("ghost"), 1u64),
|
||||
Err(SendError::Unresolved(_))
|
||||
));
|
||||
|
||||
let (ready_tx, ready_rx) = channel::<()>();
|
||||
let (tx, rx) = channel::<u64>();
|
||||
@@ -227,8 +233,11 @@ fn send_dyn_delivers_and_reports_wrong_type() {
|
||||
ready_rx.recv().unwrap();
|
||||
let p = h.pid(); // a bare Pid<Erased>, as if recovered off a Down
|
||||
send_dyn::<u64>(p, 3u64).unwrap(); // right type: delivered
|
||||
// Live actor, but it has no channel for &str — the genuinely-fallible case.
|
||||
assert!(matches!(send_dyn::<&'static str>(p, "nope"), Err(SendError::NoChannel(_))));
|
||||
// Live actor, but it has no channel for &str — the genuinely-fallible case.
|
||||
assert!(matches!(
|
||||
send_dyn::<&'static str>(p, "nope"),
|
||||
Err(SendError::NoChannel(_))
|
||||
));
|
||||
done_tx.send(()).unwrap();
|
||||
h.join().unwrap();
|
||||
});
|
||||
|
||||
+77
-26
@@ -14,10 +14,17 @@
|
||||
//! - No slot leaks under high spawn/join churn
|
||||
//! - Panic on one scheduler thread doesn't kill others
|
||||
|
||||
use smarm::{channel, runtime::{Config, Runtime}, spawn, yield_now, JoinHandle};
|
||||
use std::sync::{atomic::{AtomicBool, AtomicU64, Ordering}, Arc};
|
||||
use std::time::Duration;
|
||||
use smarm::{
|
||||
channel,
|
||||
runtime::{Config, Runtime},
|
||||
spawn, yield_now, JoinHandle,
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -29,7 +36,9 @@ fn rt(n: usize) -> Runtime {
|
||||
}
|
||||
|
||||
/// Convenient single-threaded runtime (regression guard).
|
||||
fn rt1() -> Runtime { rt(1) }
|
||||
fn rt1() -> Runtime {
|
||||
rt(1)
|
||||
}
|
||||
|
||||
/// Multi-threaded runtime using all available parallelism.
|
||||
fn rt_par() -> Runtime {
|
||||
@@ -79,7 +88,9 @@ fn config_min_1_max_1_is_single_threaded() {
|
||||
fn runtime_run_executes_closure() {
|
||||
let flag = Arc::new(AtomicBool::new(false));
|
||||
let f = flag.clone();
|
||||
rt(1).run(move || { f.store(true, Ordering::SeqCst); });
|
||||
rt(1).run(move || {
|
||||
f.store(true, Ordering::SeqCst);
|
||||
});
|
||||
assert!(flag.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
@@ -111,8 +122,12 @@ fn runtime_can_be_used_multiple_times_sequentially() {
|
||||
let b = Arc::new(AtomicU64::new(0));
|
||||
let ac = a.clone();
|
||||
let bc = b.clone();
|
||||
r.run(move || { ac.fetch_add(1, Ordering::SeqCst); });
|
||||
r.run(move || { bc.fetch_add(1, Ordering::SeqCst); });
|
||||
r.run(move || {
|
||||
ac.fetch_add(1, Ordering::SeqCst);
|
||||
});
|
||||
r.run(move || {
|
||||
bc.fetch_add(1, Ordering::SeqCst);
|
||||
});
|
||||
assert_eq!(a.load(Ordering::SeqCst), 1);
|
||||
assert_eq!(b.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
@@ -126,7 +141,9 @@ fn exact_1_spawn_join_works() {
|
||||
let v = Arc::new(AtomicU64::new(0));
|
||||
let vc = v.clone();
|
||||
rt1().run(move || {
|
||||
let h = spawn(move || { vc.store(42, Ordering::SeqCst); });
|
||||
let h = spawn(move || {
|
||||
vc.store(42, Ordering::SeqCst);
|
||||
});
|
||||
h.join().unwrap();
|
||||
});
|
||||
assert_eq!(v.load(Ordering::SeqCst), 42);
|
||||
@@ -155,7 +172,9 @@ fn exact_1_panic_captured() {
|
||||
let s = saw_err.clone();
|
||||
rt1().run(move || {
|
||||
let h = spawn(|| panic!("oops"));
|
||||
if h.join().is_err() { s.store(true, Ordering::SeqCst); }
|
||||
if h.join().is_err() {
|
||||
s.store(true, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
assert!(saw_err.load(Ordering::SeqCst));
|
||||
}
|
||||
@@ -176,7 +195,9 @@ fn multi_thread_all_actors_complete() {
|
||||
cc.fetch_add(1, Ordering::SeqCst);
|
||||
}));
|
||||
}
|
||||
for h in handles { h.join().unwrap(); }
|
||||
for h in handles {
|
||||
h.join().unwrap();
|
||||
}
|
||||
});
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 100);
|
||||
}
|
||||
@@ -221,7 +242,9 @@ fn multi_thread_many_channels_no_lost_wakeups() {
|
||||
tx.send(1).unwrap();
|
||||
}));
|
||||
}
|
||||
for h in handles { h.join().unwrap(); }
|
||||
for h in handles {
|
||||
h.join().unwrap();
|
||||
}
|
||||
});
|
||||
assert_eq!(count.load(Ordering::SeqCst), PAIRS as u64);
|
||||
}
|
||||
@@ -247,7 +270,9 @@ fn multi_thread_mutex_contention_no_deadlock() {
|
||||
}
|
||||
}));
|
||||
}
|
||||
for h in handles { h.join().unwrap(); }
|
||||
for h in handles {
|
||||
h.join().unwrap();
|
||||
}
|
||||
let g = m.lock_timeout(Duration::from_secs(1)).unwrap();
|
||||
t.store(*g, Ordering::SeqCst);
|
||||
});
|
||||
@@ -262,7 +287,9 @@ fn multi_thread_join_across_threads() {
|
||||
rt_par().run(move || {
|
||||
let h = spawn(move || {
|
||||
// Do some work to make scheduling interesting.
|
||||
for _ in 0..10 { yield_now(); }
|
||||
for _ in 0..10 {
|
||||
yield_now();
|
||||
}
|
||||
vc.store(1, Ordering::SeqCst);
|
||||
});
|
||||
h.join().unwrap();
|
||||
@@ -279,8 +306,7 @@ fn multi_thread_join_across_threads() {
|
||||
|
||||
#[test]
|
||||
fn actors_run_on_multiple_os_threads() {
|
||||
let thread_ids: Arc<smarm::Mutex<HashSet<u64>>> =
|
||||
Arc::new(smarm::Mutex::new(HashSet::new()));
|
||||
let thread_ids: Arc<smarm::Mutex<HashSet<u64>>> = Arc::new(smarm::Mutex::new(HashSet::new()));
|
||||
|
||||
rt_par().run({
|
||||
let ids = thread_ids.clone();
|
||||
@@ -294,11 +320,15 @@ fn actors_run_on_multiple_os_threads() {
|
||||
g.insert(tid);
|
||||
}));
|
||||
}
|
||||
for h in handles { h.join().unwrap(); }
|
||||
for h in handles {
|
||||
h.join().unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let n = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
|
||||
let n = std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(1);
|
||||
|
||||
let ids = thread_ids.lock_timeout(Duration::from_secs(1)).unwrap();
|
||||
// If we have >1 scheduler threads, we expect >1 OS thread IDs.
|
||||
@@ -326,11 +356,17 @@ fn scheduler_stats_run_queue_len_is_observable() {
|
||||
// run() completes (queue len == 0 at quiescence).
|
||||
let r = rt_par();
|
||||
r.run(|| {
|
||||
for _ in 0..10 { spawn(|| {}); }
|
||||
for _ in 0..10 {
|
||||
spawn(|| {});
|
||||
}
|
||||
// Don't join — let them drain naturally.
|
||||
});
|
||||
let stats = r.stats();
|
||||
assert_eq!(stats.total_run_queue_len(), 0, "queue should be empty after run()");
|
||||
assert_eq!(
|
||||
stats.total_run_queue_len(),
|
||||
0,
|
||||
"queue should be empty after run()"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -359,7 +395,9 @@ fn panic_in_actor_does_not_kill_runtime() {
|
||||
}));
|
||||
}
|
||||
let _ = bad.join(); // expect Err
|
||||
for h in good_handles { h.join().unwrap(); }
|
||||
for h in good_handles {
|
||||
h.join().unwrap();
|
||||
}
|
||||
});
|
||||
assert_eq!(completed.load(Ordering::SeqCst), 10);
|
||||
}
|
||||
@@ -379,9 +417,11 @@ fn no_slot_leak_under_churn() {
|
||||
rt_par().run(move || {
|
||||
for _ in 0..500 {
|
||||
let cc = c.clone();
|
||||
spawn(move || { cc.fetch_add(1, Ordering::SeqCst); })
|
||||
.join()
|
||||
.unwrap();
|
||||
spawn(move || {
|
||||
cc.fetch_add(1, Ordering::SeqCst);
|
||||
})
|
||||
.join()
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 500);
|
||||
@@ -474,7 +514,11 @@ fn multi_thread_timer_only_no_pipe_contention() {
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(count.load(Ordering::SeqCst), ACTORS as u64, "not all actors completed");
|
||||
assert_eq!(
|
||||
count.load(Ordering::SeqCst),
|
||||
ACTORS as u64,
|
||||
"not all actors completed"
|
||||
);
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
assert!(
|
||||
@@ -515,7 +559,10 @@ fn runtime_reusable_after_root_panic() {
|
||||
let ran = Arc::new(AtomicBool::new(false));
|
||||
let ran_t = ran.clone();
|
||||
r.run(move || ran_t.store(true, Ordering::Relaxed));
|
||||
assert!(ran.load(Ordering::Relaxed), "runtime unusable after root panic");
|
||||
assert!(
|
||||
ran.load(Ordering::Relaxed),
|
||||
"runtime unusable after root panic"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -528,7 +575,11 @@ fn runtime_reusable_after_root_panic() {
|
||||
fn burn_stack(frames: usize) -> u64 {
|
||||
let mut local = [0u8; 4096];
|
||||
local[0] = frames as u8;
|
||||
let below = if frames == 0 { 0 } else { burn_stack(frames - 1) };
|
||||
let below = if frames == 0 {
|
||||
0
|
||||
} else {
|
||||
burn_stack(frames - 1)
|
||||
};
|
||||
std::hint::black_box(&mut local);
|
||||
below.wrapping_add(local[0] as u64)
|
||||
}
|
||||
|
||||
+7
-4
@@ -14,7 +14,9 @@ use std::sync::Arc;
|
||||
fn root_actor_runs() {
|
||||
let captured = Arc::new(AtomicI64::new(0));
|
||||
let c = captured.clone();
|
||||
run(move || { c.store(99, Ordering::SeqCst); });
|
||||
run(move || {
|
||||
c.store(99, Ordering::SeqCst);
|
||||
});
|
||||
assert_eq!(captured.load(Ordering::SeqCst), 99);
|
||||
}
|
||||
|
||||
@@ -27,7 +29,9 @@ fn spawn_and_join_returns_exit() {
|
||||
let captured = Arc::new(AtomicI64::new(0));
|
||||
let c = captured.clone();
|
||||
run(move || {
|
||||
let h = spawn(move || { c.store(7, Ordering::SeqCst); });
|
||||
let h = spawn(move || {
|
||||
c.store(7, Ordering::SeqCst);
|
||||
});
|
||||
let res = h.join();
|
||||
assert!(res.is_ok(), "join returned {:?}", res);
|
||||
});
|
||||
@@ -68,8 +72,7 @@ fn yield_now_interleaves_actors() {
|
||||
|
||||
#[test]
|
||||
fn self_pid_is_stable_within_an_actor() {
|
||||
let pid_cell: Arc<std::sync::Mutex<Option<smarm::Pid>>> =
|
||||
Arc::new(std::sync::Mutex::new(None));
|
||||
let pid_cell: Arc<std::sync::Mutex<Option<smarm::Pid>>> = Arc::new(std::sync::Mutex::new(None));
|
||||
let p2 = pid_cell.clone();
|
||||
run(move || {
|
||||
let h = spawn(move || {
|
||||
|
||||
+14
-3
@@ -19,7 +19,12 @@ fn ready_arm_returns_immediately_without_parking() {
|
||||
txa.send(42).unwrap();
|
||||
let i = select(&[&rxb, &rxa]);
|
||||
assert_eq!(i, 1);
|
||||
out2.store(rxa.try_recv().unwrap().expect("ready arm must hold a message"), Ordering::SeqCst);
|
||||
out2.store(
|
||||
rxa.try_recv()
|
||||
.unwrap()
|
||||
.expect("ready arm must hold a message"),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
});
|
||||
assert_eq!(out.load(Ordering::SeqCst), 42);
|
||||
}
|
||||
@@ -276,7 +281,10 @@ fn select_timeout_ready_arm_wins_without_arming_a_timer() {
|
||||
let (txa, rxa) = channel::<i64>();
|
||||
let (_keep_b, rxb) = channel::<i64>();
|
||||
txa.send(5).unwrap();
|
||||
assert_eq!(select_timeout(&[&rxb, &rxa], Duration::from_millis(500)), Some(1));
|
||||
assert_eq!(
|
||||
select_timeout(&[&rxb, &rxa], Duration::from_millis(500)),
|
||||
Some(1)
|
||||
);
|
||||
assert_eq!(rxa.try_recv().unwrap(), Some(5));
|
||||
});
|
||||
}
|
||||
@@ -342,7 +350,10 @@ fn select_timeout_closed_arm_is_ready_not_a_timeout() {
|
||||
let (_keep_a, rxa) = channel::<i64>();
|
||||
let (txb, rxb) = channel::<i64>();
|
||||
drop(txb);
|
||||
assert_eq!(select_timeout(&[&rxa, &rxb], Duration::from_millis(200)), Some(1));
|
||||
assert_eq!(
|
||||
select_timeout(&[&rxa, &rxb], Duration::from_millis(200)),
|
||||
Some(1)
|
||||
);
|
||||
assert!(rxb.try_recv().is_err());
|
||||
});
|
||||
}
|
||||
|
||||
+34
-11
@@ -6,9 +6,7 @@
|
||||
//! big reserve behaviorally takes effect (deep recursion completes).
|
||||
|
||||
use smarm::runtime::{Config, DEFAULT_STACK_GUARD, DEFAULT_STACK_RESERVE};
|
||||
use smarm::{
|
||||
self_pid, spawn, spawn_under_with, spawn_with, GenServerBuilder, SpawnOpts,
|
||||
};
|
||||
use smarm::{self_pid, spawn, spawn_under_with, spawn_with, GenServerBuilder, SpawnOpts};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -45,7 +43,10 @@ fn spawn_with_overrides_reserve_and_guard() {
|
||||
#[test]
|
||||
fn spawn_with_partial_override_keeps_config_default_for_the_rest() {
|
||||
rt1().run(|| {
|
||||
let opts = SpawnOpts { stack_reserve: Some(1024 * 1024), ..SpawnOpts::default() };
|
||||
let opts = SpawnOpts {
|
||||
stack_reserve: Some(1024 * 1024),
|
||||
..SpawnOpts::default()
|
||||
};
|
||||
let h = spawn_with(opts, || {
|
||||
let shape = smarm::introspect::stack_shape(self_pid()).unwrap();
|
||||
assert_eq!(shape, (1024 * 1024, DEFAULT_STACK_GUARD));
|
||||
@@ -57,7 +58,10 @@ fn spawn_with_partial_override_keeps_config_default_for_the_rest() {
|
||||
#[test]
|
||||
fn spawn_with_rounds_to_pages() {
|
||||
rt1().run(|| {
|
||||
let opts = SpawnOpts { stack_reserve: Some(64 * 1024 + 1), guard_size: Some(4097) };
|
||||
let opts = SpawnOpts {
|
||||
stack_reserve: Some(64 * 1024 + 1),
|
||||
guard_size: Some(4097),
|
||||
};
|
||||
let h = spawn_with(opts, || {
|
||||
let (reserve, guard) = smarm::introspect::stack_shape(self_pid()).unwrap();
|
||||
assert_eq!(reserve % 4096, 0);
|
||||
@@ -73,7 +77,10 @@ fn spawn_with_rounds_to_pages() {
|
||||
fn spawn_under_with_takes_opts() {
|
||||
rt1().run(|| {
|
||||
let me = self_pid();
|
||||
let opts = SpawnOpts { stack_reserve: Some(128 * 1024), ..SpawnOpts::default() };
|
||||
let opts = SpawnOpts {
|
||||
stack_reserve: Some(128 * 1024),
|
||||
..SpawnOpts::default()
|
||||
};
|
||||
let h = spawn_under_with(me, opts, || {
|
||||
let (reserve, _) = smarm::introspect::stack_shape(self_pid()).unwrap();
|
||||
assert_eq!(reserve, 128 * 1024);
|
||||
@@ -90,7 +97,10 @@ fn spawn_under_with_takes_opts() {
|
||||
fn custom_stack_never_enters_the_pool() {
|
||||
rt1().run(|| {
|
||||
spawn_with(
|
||||
SpawnOpts { stack_reserve: Some(512 * 1024), guard_size: Some(128 * 1024) },
|
||||
SpawnOpts {
|
||||
stack_reserve: Some(512 * 1024),
|
||||
guard_size: Some(128 * 1024),
|
||||
},
|
||||
|| {},
|
||||
)
|
||||
.join()
|
||||
@@ -122,7 +132,11 @@ fn default_stack_is_recycled() {
|
||||
fn burn_stack(frames: usize) -> u64 {
|
||||
let mut local = [0u8; 4096];
|
||||
local[0] = frames as u8;
|
||||
let below = if frames == 0 { 0 } else { burn_stack(frames - 1) };
|
||||
let below = if frames == 0 {
|
||||
0
|
||||
} else {
|
||||
burn_stack(frames - 1)
|
||||
};
|
||||
std::hint::black_box(&mut local);
|
||||
below.wrapping_add(local[0] as u64)
|
||||
}
|
||||
@@ -134,7 +148,10 @@ fn big_reserve_behaviorally_takes_effect() {
|
||||
let done = Arc::new(AtomicBool::new(false));
|
||||
let done2 = done.clone();
|
||||
spawn_with(
|
||||
SpawnOpts { stack_reserve: Some(8 * 1024 * 1024), ..SpawnOpts::default() },
|
||||
SpawnOpts {
|
||||
stack_reserve: Some(8 * 1024 * 1024),
|
||||
..SpawnOpts::default()
|
||||
},
|
||||
move || {
|
||||
std::hint::black_box(burn_stack(256));
|
||||
done2.store(true, Ordering::SeqCst);
|
||||
@@ -167,7 +184,10 @@ impl smarm::GenServer for Echo {
|
||||
fn gen_server_builder_stack_opts() {
|
||||
rt1().run(|| {
|
||||
let server = GenServerBuilder::new(Echo)
|
||||
.stack_opts(SpawnOpts { stack_reserve: Some(256 * 1024), ..SpawnOpts::default() })
|
||||
.stack_opts(SpawnOpts {
|
||||
stack_reserve: Some(256 * 1024),
|
||||
..SpawnOpts::default()
|
||||
})
|
||||
.start();
|
||||
let (reserve, guard) = server.call(()).unwrap();
|
||||
assert_eq!(reserve, 256 * 1024);
|
||||
@@ -200,7 +220,10 @@ impl smarm::Machine for Probe {
|
||||
fn gen_statem_spawn_with_stack_opts() {
|
||||
rt1().run(|| {
|
||||
let m = smarm::gen_statem::spawn_with(
|
||||
SpawnOpts { stack_reserve: Some(256 * 1024), ..SpawnOpts::default() },
|
||||
SpawnOpts {
|
||||
stack_reserve: Some(256 * 1024),
|
||||
..SpawnOpts::default()
|
||||
},
|
||||
Probe,
|
||||
);
|
||||
let (tx, rx) = smarm::channel::channel();
|
||||
|
||||
+30
-6
@@ -78,14 +78,18 @@ fn run_as_child_if_requested() {
|
||||
Ok("wide_guard_top") => {
|
||||
// One byte below the usable region, 64 KiB guard: must fault.
|
||||
let s = Stack::new(64 * 1024, 64 * 1024).unwrap();
|
||||
unsafe { s.usable_base().sub(1).write_volatile(0xAB); }
|
||||
unsafe {
|
||||
s.usable_base().sub(1).write_volatile(0xAB);
|
||||
}
|
||||
std::process::exit(0);
|
||||
}
|
||||
Ok("wide_guard_bottom") => {
|
||||
// The very bottom page of a 64 KiB guard: an unprobed C-style
|
||||
// leap over a small guard lands here — must still fault.
|
||||
let s = Stack::new(64 * 1024, 64 * 1024).unwrap();
|
||||
unsafe { s.usable_base().sub(64 * 1024).write_volatile(0xAB); }
|
||||
unsafe {
|
||||
s.usable_base().sub(64 * 1024).write_volatile(0xAB);
|
||||
}
|
||||
std::process::exit(0);
|
||||
}
|
||||
Ok("stack_overflow") => {
|
||||
@@ -120,7 +124,12 @@ fn guard_page_causes_sigsegv() {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
assert_eq!(status.signal(), Some(11), "expected SIGSEGV, got: {:?}", status);
|
||||
assert_eq!(
|
||||
status.signal(),
|
||||
Some(11),
|
||||
"expected SIGSEGV, got: {:?}",
|
||||
status
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +140,12 @@ fn stack_overflow_causes_sigsegv() {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
assert_eq!(status.signal(), Some(11), "expected SIGSEGV, got: {:?}", status);
|
||||
assert_eq!(
|
||||
status.signal(),
|
||||
Some(11),
|
||||
"expected SIGSEGV, got: {:?}",
|
||||
status
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +189,12 @@ fn wide_guard_faults_at_top() {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
assert_eq!(status.signal(), Some(11), "expected SIGSEGV, got: {:?}", status);
|
||||
assert_eq!(
|
||||
status.signal(),
|
||||
Some(11),
|
||||
"expected SIGSEGV, got: {:?}",
|
||||
status
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,6 +205,11 @@ fn wide_guard_faults_at_bottom() {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
assert_eq!(status.signal(), Some(11), "expected SIGSEGV, got: {:?}", status);
|
||||
assert_eq!(
|
||||
status.signal(),
|
||||
Some(11),
|
||||
"expected SIGSEGV, got: {:?}",
|
||||
status
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+16
-4
@@ -42,9 +42,15 @@ fn run_as_child_if_requested() {
|
||||
let opts = match mode.as_str() {
|
||||
"rust_overflow" | "ffi_tier1" => SpawnOpts::default(),
|
||||
// Small guard: the canary's 96 KiB displacement clears it.
|
||||
"ffi_tier2" => SpawnOpts { guard_size: Some(4096), ..SpawnOpts::default() },
|
||||
"ffi_tier2" => SpawnOpts {
|
||||
guard_size: Some(4096),
|
||||
..SpawnOpts::default()
|
||||
},
|
||||
// Enough reserve: the same frame simply fits.
|
||||
"ffi_clean" => SpawnOpts { stack_reserve: Some(256 * 1024), ..SpawnOpts::default() },
|
||||
"ffi_clean" => SpawnOpts {
|
||||
stack_reserve: Some(256 * 1024),
|
||||
..SpawnOpts::default()
|
||||
},
|
||||
other => panic!("unknown subtest {other}"),
|
||||
};
|
||||
let is_rust = mode == "rust_overflow";
|
||||
@@ -92,8 +98,14 @@ fn rust_overflow_dies_with_tier1_message() {
|
||||
err.contains("overflowed its stack") && err.contains("in the guard region"),
|
||||
"missing tier-1 diagnostic; stderr:\n{err}"
|
||||
);
|
||||
assert!(err.contains("reserve=65536"), "wrong reserve in message:\n{err}");
|
||||
assert!(err.contains("guard=1048576"), "wrong guard in message:\n{err}");
|
||||
assert!(
|
||||
err.contains("reserve=65536"),
|
||||
"wrong reserve in message:\n{err}"
|
||||
);
|
||||
assert!(
|
||||
err.contains("guard=1048576"),
|
||||
"wrong guard in message:\n{err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+16
-6
@@ -24,7 +24,11 @@ const RESERVE: usize = 4 * 1024 * 1024;
|
||||
fn burn_stack(frames: usize) -> u64 {
|
||||
let mut local = [0u8; 4096];
|
||||
local[0] = frames as u8;
|
||||
let below = if frames == 0 { 0 } else { burn_stack(frames - 1) };
|
||||
let below = if frames == 0 {
|
||||
0
|
||||
} else {
|
||||
burn_stack(frames - 1)
|
||||
};
|
||||
std::hint::black_box(&mut local);
|
||||
below.wrapping_add(local[0] as u64)
|
||||
}
|
||||
@@ -33,10 +37,13 @@ fn burn_stack(frames: usize) -> u64 {
|
||||
fn resident_pages(lo: usize, len: usize) -> usize {
|
||||
let page = 4096;
|
||||
let mut vec = vec![0u8; len / page];
|
||||
let ret = unsafe {
|
||||
libc::mincore(lo as *mut libc::c_void, len, vec.as_mut_ptr())
|
||||
};
|
||||
assert_eq!(ret, 0, "mincore failed: {}", std::io::Error::last_os_error());
|
||||
let ret = unsafe { libc::mincore(lo as *mut libc::c_void, len, vec.as_mut_ptr()) };
|
||||
assert_eq!(
|
||||
ret,
|
||||
0,
|
||||
"mincore failed: {}",
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
vec.iter().filter(|&&b| b & 1 != 0).count()
|
||||
}
|
||||
|
||||
@@ -118,6 +125,9 @@ fn recycle_zaps_dead_stack_down_to_retain() {
|
||||
[{usable_base:#x}, +{zap_len:#x})"
|
||||
);
|
||||
// Pooled, not munmapped: the mapping must still be there.
|
||||
assert!(vma_exists(usable_base), "default-shaped stack was unmapped instead of pooled");
|
||||
assert!(
|
||||
vma_exists(usable_base),
|
||||
"default-shaped stack was unmapped instead of pooled"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
+12
-3
@@ -66,7 +66,10 @@ fn spike_then_parks_marks_lazyfree_and_keeps_live_data() {
|
||||
assert!(spike > SHRINK_THRESHOLD);
|
||||
|
||||
let worker = spawn_with(
|
||||
SpawnOpts { stack_reserve: Some(8 * 1024 * 1024), ..SpawnOpts::default() },
|
||||
SpawnOpts {
|
||||
stack_reserve: Some(8 * 1024 * 1024),
|
||||
..SpawnOpts::default()
|
||||
},
|
||||
move || {
|
||||
// Live data that must survive the shrink, and an anchor
|
||||
// address inside the stack for the smaps scan.
|
||||
@@ -111,7 +114,11 @@ fn spike_then_parks_marks_lazyfree_and_keeps_live_data() {
|
||||
"expected ≥ 2 MiB LazyFree in the stack range, got {} bytes",
|
||||
lazy
|
||||
);
|
||||
assert_eq!(checksum, 64 * 0xA5u64, "live stack data corrupted by shrink");
|
||||
assert_eq!(
|
||||
checksum,
|
||||
64 * 0xA5u64,
|
||||
"live stack data corrupted by shrink"
|
||||
);
|
||||
worker.join().unwrap();
|
||||
});
|
||||
}
|
||||
@@ -131,7 +138,9 @@ fn shallow_actor_never_shrinks() {
|
||||
for _ in 0..(SHRINK_COOLDOWN + 8) {
|
||||
park_rx.recv().unwrap();
|
||||
}
|
||||
done_tx.send(lazy_free_bytes_in(anchor - 64 * 1024, anchor + 4096)).unwrap();
|
||||
done_tx
|
||||
.send(lazy_free_bytes_in(anchor - 64 * 1024, anchor + 4096))
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
let wpid = worker.pid();
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
//! registry entry guarantees for every named server.
|
||||
|
||||
use smarm::{
|
||||
call, channel, init, request_stop, spawn, Config, GenServer, GenServerBuilder, GenServerName,
|
||||
CallError, Receiver, RecvTimeoutError,
|
||||
call, channel, init, request_stop, spawn, CallError, Config, GenServer, GenServerBuilder,
|
||||
GenServerName, Receiver, RecvTimeoutError,
|
||||
};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
@@ -73,10 +73,12 @@ fn named_server_request_stop_releases_queued_caller_with_server_down() {
|
||||
let (res_tx, res_rx) = channel::<Result<(), CallError>>();
|
||||
|
||||
// 1. Start the named server and keep its ref alive.
|
||||
let server = GenServerBuilder::new(Blocker { gate: Some(gate_rx) })
|
||||
.named(BLOCKER)
|
||||
.start()
|
||||
.expect("name should be free");
|
||||
let server = GenServerBuilder::new(Blocker {
|
||||
gate: Some(gate_rx),
|
||||
})
|
||||
.named(BLOCKER)
|
||||
.start()
|
||||
.expect("name should be free");
|
||||
let spid = server.pid();
|
||||
|
||||
// 2. Send the cast and let the server dequeue it and park on the gate.
|
||||
|
||||
+16
-8
@@ -10,7 +10,11 @@
|
||||
//! out rather than produce a false pass — run with `cargo test -- --timeout`
|
||||
//! or under a CI timeout.
|
||||
|
||||
use smarm::{channel, runtime::{Config, Runtime}, spawn, yield_now, JoinHandle};
|
||||
use smarm::{
|
||||
channel,
|
||||
runtime::{Config, Runtime},
|
||||
spawn, yield_now, JoinHandle,
|
||||
};
|
||||
use std::sync::{
|
||||
atomic::{AtomicU64, AtomicUsize, Ordering},
|
||||
Arc,
|
||||
@@ -199,7 +203,9 @@ fn thundering_herd_all_wake() {
|
||||
}
|
||||
|
||||
// Let all receivers park before we send.
|
||||
for _ in 0..4 { yield_now(); }
|
||||
for _ in 0..4 {
|
||||
yield_now();
|
||||
}
|
||||
|
||||
// Coordinator blasts all channels.
|
||||
handles.push(spawn(move || {
|
||||
@@ -240,8 +246,7 @@ fn concurrent_spawn_join_churn() {
|
||||
for _ in 0..PARENTS {
|
||||
let tc = t.clone();
|
||||
parent_handles.push(spawn(move || {
|
||||
let mut child_handles: Vec<JoinHandle> =
|
||||
Vec::with_capacity(CHILDREN_PER_PARENT);
|
||||
let mut child_handles: Vec<JoinHandle> = Vec::with_capacity(CHILDREN_PER_PARENT);
|
||||
|
||||
for _ in 0..CHILDREN_PER_PARENT {
|
||||
let tcc = tc.clone();
|
||||
@@ -292,7 +297,9 @@ fn join_race_child_finishes_first() {
|
||||
}
|
||||
|
||||
// Yield enough to let children run to completion before we join.
|
||||
for _ in 0..8 { yield_now(); }
|
||||
for _ in 0..8 {
|
||||
yield_now();
|
||||
}
|
||||
|
||||
for h in handles {
|
||||
// If child already finished, join must return immediately with Ok.
|
||||
@@ -374,8 +381,7 @@ fn panic_storm_does_not_corrupt_scheduler() {
|
||||
fn pid_generation_increments_on_reuse() {
|
||||
use smarm::self_pid;
|
||||
|
||||
let pids: Arc<smarm::Mutex<Vec<smarm::Pid>>> =
|
||||
Arc::new(smarm::Mutex::new(Vec::new()));
|
||||
let pids: Arc<smarm::Mutex<Vec<smarm::Pid>>> = Arc::new(smarm::Mutex::new(Vec::new()));
|
||||
|
||||
let p = pids.clone();
|
||||
rt(1).run(move || {
|
||||
@@ -392,7 +398,9 @@ fn pid_generation_increments_on_reuse() {
|
||||
}
|
||||
});
|
||||
|
||||
let g = pids.lock_timeout(std::time::Duration::from_secs(1)).unwrap();
|
||||
let g = pids
|
||||
.lock_timeout(std::time::Duration::from_secs(1))
|
||||
.unwrap();
|
||||
// Any two PIDs that share an index must have different generations.
|
||||
for i in 0..g.len() {
|
||||
for j in (i + 1)..g.len() {
|
||||
|
||||
+10
-2
@@ -51,7 +51,11 @@ fn transient_child_is_restarted_on_panic_then_settles() {
|
||||
});
|
||||
sup.join().unwrap();
|
||||
});
|
||||
assert_eq!(runs.load(Ordering::SeqCst), 3, "two restarts then a clean exit");
|
||||
assert_eq!(
|
||||
runs.load(Ordering::SeqCst),
|
||||
3,
|
||||
"two restarts then a clean exit"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -167,7 +171,11 @@ fn one_for_all_restarts_a_normally_exited_sibling() {
|
||||
sup.join().unwrap();
|
||||
});
|
||||
assert_eq!(a.load(Ordering::SeqCst), 2, "A: crash then clean run");
|
||||
assert_eq!(b.load(Ordering::SeqCst), 2, "B cycled with the group despite a clean exit");
|
||||
assert_eq!(
|
||||
b.load(Ordering::SeqCst),
|
||||
2,
|
||||
"B cycled with the group despite a clean exit"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
//! The terminal-record contract (bridge soak signature 4): a watch installed
|
||||
//! *after* its target's death — the async-install race the bridge's proxies
|
||||
//! live with — must be able to recover the real down reason instead of a
|
||||
//! blanket `NoProc`. Two primitives carry it:
|
||||
//!
|
||||
//! - `finalize_actor` stamps the slot with `(generation, DownReason)`; the
|
||||
//! record survives reclaim, registry pruning, and the next tenant's
|
||||
//! install, and is overwritten only by the slot's next death.
|
||||
//! [`terminal_reason`] reads it generation-matched.
|
||||
//! - [`resolve_name`] is `whereis` with the corpse kept: the dead-holder arm
|
||||
//! returns the stored pid it prunes ([`NameResolution::Corpse`]) instead
|
||||
//! of discarding the only evidence of *who* died. `Unbound` stays the
|
||||
//! Erlang-shaped `noproc` for names that were never (or are no longer)
|
||||
//! bound.
|
||||
//!
|
||||
//! `monitor()` of a stale pid still queues plain `NoProc` — the upgrade is a
|
||||
//! caller's deliberate act, not a semantics change.
|
||||
|
||||
use smarm::{
|
||||
init, mark_watchable, request_stop, resolve_name, terminal_reason, CallError, Config,
|
||||
DownReason, GenServer, GenServerBuilder, GenServerName, NameResolution,
|
||||
};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
const TARGET: GenServerName<Target> = GenServerName::new("terminal_target");
|
||||
|
||||
/// Named server that panics on cast — the sig-4 death.
|
||||
struct Target;
|
||||
|
||||
impl GenServer for Target {
|
||||
type Call = ();
|
||||
type Reply = ();
|
||||
type Cast = ();
|
||||
type Info = ();
|
||||
type Timer = ();
|
||||
|
||||
fn handle_call(&mut self, _req: ()) {}
|
||||
fn handle_cast(&mut self, _op: ()) {
|
||||
panic!("terminal_target: induced panic");
|
||||
}
|
||||
}
|
||||
|
||||
/// Slot filler for the re-tenancy phase (distinct type, held alive).
|
||||
struct Filler;
|
||||
|
||||
impl GenServer for Filler {
|
||||
type Call = ();
|
||||
type Reply = ();
|
||||
type Cast = ();
|
||||
type Info = ();
|
||||
type Timer = ();
|
||||
|
||||
fn handle_call(&mut self, _req: ()) {}
|
||||
fn handle_cast(&mut self, _op: ()) {}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Observed {
|
||||
exit_reason: Option<DownReason>,
|
||||
anon_reason: Option<DownReason>,
|
||||
/// Anonymous but export-marked while alive — must stamp (sig 5).
|
||||
marked_reason: Option<DownReason>,
|
||||
/// Marked only after death — must remain unknowable.
|
||||
marked_late_reason: Option<DownReason>,
|
||||
panic_reason: Option<DownReason>,
|
||||
stopped_reason: Option<DownReason>,
|
||||
live_reason: Option<DownReason>,
|
||||
live_resolution_is_live: bool,
|
||||
unknown_resolution: NameResolution,
|
||||
/// First resolve after the named target's panic — must be Corpse(old pid).
|
||||
corpse_resolution_matches: bool,
|
||||
/// Second resolve — the Corpse arm pruned, so the name has healed.
|
||||
resolution_after_prune: NameResolution,
|
||||
/// Read AFTER the prune above: the record is slot-side, not registry-side.
|
||||
corpse_reason_after_prune: Option<DownReason>,
|
||||
/// Record survives the slot being re-tenanted (new tenant still alive).
|
||||
corpse_reason_after_reuse: Option<DownReason>,
|
||||
/// ... and dies with the next tenancy's death (overwritten).
|
||||
corpse_reason_after_tenant_death: Option<DownReason>,
|
||||
tenant_reason: Option<DownReason>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_record_recovers_the_reason_a_raced_watch_lost() {
|
||||
let out: Arc<Mutex<Option<Observed>>> = Arc::new(Mutex::new(None));
|
||||
let out_w = out.clone();
|
||||
|
||||
// Tiny slab: prompt slot recycling for the re-tenancy phase.
|
||||
init(Config::exact(2).max_actors(32)).run(move || {
|
||||
// --- Registered plain actors: one record per way of dying. The
|
||||
// record is named-tenancy-only, so each actor self-registers a
|
||||
// throwaway channel before dying; the anonymous control below pins
|
||||
// the complement.
|
||||
let h = smarm::spawn(|| {
|
||||
let (tx, _rx) = smarm::channel::<()>();
|
||||
let _ = smarm::register(smarm::Name::<()>::new("terminal_probe_exit"), tx);
|
||||
});
|
||||
let pid_exit = h.pid();
|
||||
let _ = h.join();
|
||||
let exit_reason = terminal_reason(pid_exit);
|
||||
|
||||
let h = smarm::spawn(|| {
|
||||
let (tx, _rx) = smarm::channel::<()>();
|
||||
let _ = smarm::register(smarm::Name::<()>::new("terminal_probe_panic"), tx);
|
||||
panic!("induced");
|
||||
});
|
||||
let pid_panic = h.pid();
|
||||
let _ = h.join();
|
||||
let panic_reason = terminal_reason(pid_panic);
|
||||
|
||||
let h = smarm::spawn(|| {
|
||||
let (tx, _rx) = smarm::channel::<()>();
|
||||
let _ = smarm::register(smarm::Name::<()>::new("terminal_probe_stop"), tx);
|
||||
loop {
|
||||
smarm::sleep(Duration::from_millis(2));
|
||||
}
|
||||
});
|
||||
let pid_stop = h.pid();
|
||||
request_stop(pid_stop);
|
||||
let _ = h.join();
|
||||
let stopped_reason = terminal_reason(pid_stop);
|
||||
|
||||
// --- Anonymous control: an unregistered death must NOT stamp (nor
|
||||
// evict) — the free list is LIFO, so green-thread churn would
|
||||
// otherwise overwrite a watchable record faster than any race
|
||||
// window this exists to cover.
|
||||
let h = smarm::spawn(|| panic!("anonymous"));
|
||||
let pid_anon = h.pid();
|
||||
let _ = h.join();
|
||||
let anon_reason = terminal_reason(pid_anon);
|
||||
|
||||
// --- mark_watchable: the bridge's export-seam eligibility (sig 5).
|
||||
// An anonymous actor marked while alive stamps like a named one ...
|
||||
let h = smarm::spawn(|| loop {
|
||||
smarm::sleep(Duration::from_millis(2));
|
||||
});
|
||||
let pid_marked = h.pid();
|
||||
mark_watchable(pid_marked);
|
||||
request_stop(pid_marked);
|
||||
let _ = h.join();
|
||||
let marked_reason = terminal_reason(pid_marked);
|
||||
|
||||
// ... while marking a pid whose tenancy already ended is a no-op:
|
||||
// the history is honestly unknowable, not retroactively invented.
|
||||
mark_watchable(pid_anon);
|
||||
let marked_late_reason = terminal_reason(pid_anon);
|
||||
|
||||
// --- The named target: live readings first. -----------------------
|
||||
let target = GenServerBuilder::new(Target)
|
||||
.named(TARGET)
|
||||
.start()
|
||||
.expect("name free at test start");
|
||||
let old_pid = target.pid();
|
||||
let live_reason = terminal_reason(old_pid);
|
||||
let live_resolution_is_live =
|
||||
resolve_name(TARGET.as_str()) == NameResolution::Live(old_pid.erase());
|
||||
let unknown_resolution = resolve_name("terminal_never_bound");
|
||||
|
||||
// --- Kill it by panic; confirm death via the ref, NEVER the name
|
||||
// (any name reader would take the prune arm and destroy the corpse
|
||||
// precondition — the same trap stale_name_slot_reuse.rs documents).
|
||||
let _ = target.cast(());
|
||||
loop {
|
||||
match target.call(()) {
|
||||
Err(CallError::ServerDown) => break,
|
||||
Ok(()) => smarm::sleep(Duration::from_millis(2)),
|
||||
}
|
||||
}
|
||||
|
||||
let corpse_resolution_matches =
|
||||
resolve_name(TARGET.as_str()) == NameResolution::Corpse(old_pid.erase());
|
||||
let resolution_after_prune = resolve_name(TARGET.as_str());
|
||||
let corpse_reason_after_prune = terminal_reason(old_pid);
|
||||
|
||||
// --- Re-tenant the freed slot; the record must outlive the install
|
||||
// and die only with the next tenancy's death.
|
||||
let mut fillers = Vec::new();
|
||||
let mut tenant = None;
|
||||
for i in 0..24 {
|
||||
let name: &'static str = Box::leak(format!("terminal_filler_{i}").into_boxed_str());
|
||||
let f = GenServerBuilder::new(Filler)
|
||||
.named(GenServerName::<Filler>::new(name))
|
||||
.start()
|
||||
.expect("filler names are fresh");
|
||||
let fp = f.pid();
|
||||
let landed = fp.index() == old_pid.index();
|
||||
fillers.push(f);
|
||||
if landed {
|
||||
tenant = Some((fillers.len() - 1, fp));
|
||||
break;
|
||||
}
|
||||
}
|
||||
let (tenant_at, tenant_pid) = tenant.expect(
|
||||
"precondition: the freed slot must be re-tenanted within the tiny slab \
|
||||
(slots are recycled; every filler is held alive)",
|
||||
);
|
||||
let corpse_reason_after_reuse = terminal_reason(old_pid);
|
||||
|
||||
request_stop(tenant_pid);
|
||||
loop {
|
||||
match fillers[tenant_at].call(()) {
|
||||
Err(CallError::ServerDown) => break,
|
||||
Ok(()) => smarm::sleep(Duration::from_millis(2)),
|
||||
}
|
||||
}
|
||||
let corpse_reason_after_tenant_death = terminal_reason(old_pid);
|
||||
let tenant_reason = terminal_reason(tenant_pid);
|
||||
|
||||
*out_w.lock().unwrap() = Some(Observed {
|
||||
exit_reason,
|
||||
anon_reason,
|
||||
panic_reason,
|
||||
stopped_reason,
|
||||
live_reason,
|
||||
live_resolution_is_live,
|
||||
unknown_resolution,
|
||||
corpse_resolution_matches,
|
||||
resolution_after_prune,
|
||||
marked_reason,
|
||||
marked_late_reason,
|
||||
corpse_reason_after_prune,
|
||||
corpse_reason_after_reuse,
|
||||
corpse_reason_after_tenant_death,
|
||||
tenant_reason,
|
||||
});
|
||||
});
|
||||
|
||||
let o = out.lock().unwrap().take().expect("runtime body completed");
|
||||
assert_eq!(o.exit_reason, Some(DownReason::Exit), "{o:?}");
|
||||
assert_eq!(
|
||||
o.anon_reason, None,
|
||||
"anonymous deaths must not stamp: {o:?}"
|
||||
);
|
||||
assert_eq!(o.panic_reason, Some(DownReason::Panic), "{o:?}");
|
||||
assert_eq!(
|
||||
o.marked_reason,
|
||||
Some(DownReason::Stopped),
|
||||
"mark_watchable while alive must make the death stamp: {o:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
o.marked_late_reason, None,
|
||||
"marking a dead tenancy must not invent history: {o:?}"
|
||||
);
|
||||
assert_eq!(o.stopped_reason, Some(DownReason::Stopped), "{o:?}");
|
||||
assert_eq!(
|
||||
o.live_reason, None,
|
||||
"live tenancy must have no record: {o:?}"
|
||||
);
|
||||
assert!(o.live_resolution_is_live, "{o:?}");
|
||||
assert_eq!(o.unknown_resolution, NameResolution::Unbound, "{o:?}");
|
||||
assert!(
|
||||
o.corpse_resolution_matches,
|
||||
"first post-death resolve must carry the corpse: {o:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
o.resolution_after_prune,
|
||||
NameResolution::Unbound,
|
||||
"the Corpse arm prunes — the name heals: {o:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
o.corpse_reason_after_prune,
|
||||
Some(DownReason::Panic),
|
||||
"the record is slot-side; registry pruning must not touch it: {o:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
o.corpse_reason_after_reuse,
|
||||
Some(DownReason::Panic),
|
||||
"a new tenant's install must leave the previous tenancy's record: {o:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
o.corpse_reason_after_tenant_death, None,
|
||||
"the next death overwrites — the old generation no longer matches: {o:?}"
|
||||
);
|
||||
assert_eq!(o.tenant_reason, Some(DownReason::Stopped), "{o:?}");
|
||||
}
|
||||
@@ -35,7 +35,10 @@ impl PipePair {
|
||||
let mut fds: [libc::c_int; 2] = [0; 2];
|
||||
let r = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC | libc::O_NONBLOCK) };
|
||||
assert_eq!(r, 0, "pipe2 failed");
|
||||
PipePair { read: fds[0], write: fds[1] }
|
||||
PipePair {
|
||||
read: fds[0],
|
||||
write: fds[1],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,9 +70,9 @@ fn run_with_watchdog(limit: Duration, body: impl FnOnce() + Send + 'static) {
|
||||
rt.run(body);
|
||||
let _ = done_tx.send(());
|
||||
});
|
||||
done_rx
|
||||
.recv_timeout(limit)
|
||||
.expect("Runtime::run did not return: idle scheduler thread was never woken at termination");
|
||||
done_rx.recv_timeout(limit).expect(
|
||||
"Runtime::run did not return: idle scheduler thread was never woken at termination",
|
||||
);
|
||||
}
|
||||
|
||||
/// Permanent-hang variant: sibling blocked in `poll_wake(wake_fd, None)`
|
||||
|
||||
+30
-10
@@ -166,14 +166,19 @@ fn timers_only_pop_entries_whose_deadline_has_passed() {
|
||||
#[test]
|
||||
fn timers_mix_sleep_and_wait_timeout_reasons() {
|
||||
let mut t = Timers::new();
|
||||
let target = Arc::new(RecordingTarget { calls: Mutex::new(Vec::new()) });
|
||||
let target = Arc::new(RecordingTarget {
|
||||
calls: Mutex::new(Vec::new()),
|
||||
});
|
||||
let now = Instant::now();
|
||||
|
||||
t.insert_sleep(now + Duration::from_millis(5), Pid::new(0, 0), 1);
|
||||
t.insert(
|
||||
now + Duration::from_millis(10),
|
||||
Pid::new(1, 0),
|
||||
Reason::WaitTimeout { target: target.clone(), epoch: 42 },
|
||||
Reason::WaitTimeout {
|
||||
target: target.clone(),
|
||||
epoch: 42,
|
||||
},
|
||||
);
|
||||
|
||||
let due = t.pop_due(now + Duration::from_millis(20));
|
||||
@@ -238,7 +243,10 @@ fn armed_send_timer_is_returned_and_fires() {
|
||||
|
||||
let mut due = t.pop_due(now + Duration::from_millis(20));
|
||||
assert_eq!(due.len(), 1, "an armed send timer should pop when due");
|
||||
assert!(!fired.load(Ordering::SeqCst), "pop must not fire on its own");
|
||||
assert!(
|
||||
!fired.load(Ordering::SeqCst),
|
||||
"pop must not fire on its own"
|
||||
);
|
||||
run_fire(due.pop().unwrap());
|
||||
assert!(fired.load(Ordering::SeqCst), "running the thunk delivers");
|
||||
assert!(t.is_empty());
|
||||
@@ -282,7 +290,11 @@ fn cancel_after_fire_returns_false() {
|
||||
fn cancel_unknown_id_returns_false() {
|
||||
let mut t = Timers::new();
|
||||
let now = Instant::now();
|
||||
let id = t.insert_send(now + Duration::from_millis(5), Pid::new(0, 0), Box::new(|| {}));
|
||||
let id = t.insert_send(
|
||||
now + Duration::from_millis(5),
|
||||
Pid::new(0, 0),
|
||||
Box::new(|| {}),
|
||||
);
|
||||
assert!(t.cancel(id));
|
||||
// Second cancel of the same id: already gone.
|
||||
assert!(!t.cancel(id));
|
||||
@@ -293,7 +305,11 @@ fn send_timers_interleave_with_sleep_in_deadline_order() {
|
||||
let mut t = Timers::new();
|
||||
let now = Instant::now();
|
||||
t.insert_sleep(now + Duration::from_millis(30), Pid::new(0, 0), 1);
|
||||
let _id = t.insert_send(now + Duration::from_millis(10), Pid::new(1, 0), Box::new(|| {}));
|
||||
let _id = t.insert_send(
|
||||
now + Duration::from_millis(10),
|
||||
Pid::new(1, 0),
|
||||
Box::new(|| {}),
|
||||
);
|
||||
t.insert_sleep(now + Duration::from_millis(20), Pid::new(2, 0), 1);
|
||||
|
||||
let due = t.pop_due(now + Duration::from_millis(50));
|
||||
@@ -308,7 +324,11 @@ fn send_timers_interleave_with_sleep_in_deadline_order() {
|
||||
fn clear_drops_armed_send_timers() {
|
||||
let mut t = Timers::new();
|
||||
let now = Instant::now();
|
||||
let id = t.insert_send(now + Duration::from_millis(10), Pid::new(0, 0), Box::new(|| {}));
|
||||
let id = t.insert_send(
|
||||
now + Duration::from_millis(10),
|
||||
Pid::new(0, 0),
|
||||
Box::new(|| {}),
|
||||
);
|
||||
t.clear();
|
||||
assert!(t.is_empty());
|
||||
// The arm record is gone too: cancelling reports nothing to cancel.
|
||||
@@ -356,7 +376,7 @@ fn send_after_to_unresolved_name_is_silent() {
|
||||
// Nobody registered NOPE; firing resolves to nothing and is dropped.
|
||||
let _id = send_after_named(Duration::from_millis(10), NOPE, 1);
|
||||
sleep(Duration::from_millis(40)); // let it fire and no-op
|
||||
// Reaching here without a panic is the assertion.
|
||||
// Reaching here without a panic is the assertion.
|
||||
});
|
||||
}
|
||||
|
||||
@@ -401,9 +421,9 @@ fn send_after_to_dead_typed_pid_is_silent() {
|
||||
assert_eq!(report_rx.recv().unwrap(), 1); // sink has now exited
|
||||
let _id = send_after(Duration::from_millis(15), sink, 2);
|
||||
sleep(Duration::from_millis(45)); // let it fire against the dead pid
|
||||
// No panic; the sink is gone, so its report sender dropped with it —
|
||||
// closed+empty is Err (documented), which also proves nothing
|
||||
// further was delivered.
|
||||
// No panic; the sink is gone, so its report sender dropped with it —
|
||||
// closed+empty is Err (documented), which also proves nothing
|
||||
// further was delivered.
|
||||
assert!(report_rx.try_recv().is_err(), "nothing further delivered");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
//! Non-panicking spawn at slab capacity (`try_spawn`).
|
||||
//!
|
||||
//! Covers: parity with `spawn` when slots are free; `Err(AtCapacity)` instead
|
||||
//! of a panic on a full slab (the spawning actor survives — the crash-loop
|
||||
//! from the motivating slowloris incident cannot start); self-heal (a freed
|
||||
//! slot makes the next `try_spawn` succeed); and exact claim-or-report
|
||||
//! accounting under a multi-thread race for the last slots (no TOCTOU
|
||||
//! overshoot, no panic).
|
||||
|
||||
use smarm::runtime::Config;
|
||||
use smarm::{spawn, try_spawn, try_spawn_under_with, yield_now, SpawnError, SpawnOpts};
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A child that holds its slot until `release` flips, without parking
|
||||
/// machinery: busy-yield keeps the scheduler moving and the slot occupied.
|
||||
fn holder(release: Arc<AtomicBool>) -> impl FnOnce() + Send + 'static {
|
||||
move || {
|
||||
while !release.load(Ordering::Acquire) {
|
||||
yield_now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_spawn_is_spawn_when_slots_free() {
|
||||
smarm::runtime::init(Config::exact(1)).run(|| {
|
||||
let ran = Arc::new(AtomicBool::new(false));
|
||||
let flag = ran.clone();
|
||||
let h = try_spawn(move || flag.store(true, Ordering::Release))
|
||||
.expect("slots free — must behave exactly like spawn");
|
||||
h.join().unwrap();
|
||||
assert!(ran.load(Ordering::Acquire));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_capacity_is_err_not_panic_and_accounting_is_exact() {
|
||||
const MAX: usize = 8;
|
||||
smarm::runtime::init(Config::exact(1).max_actors(MAX)).run(|| {
|
||||
let release = Arc::new(AtomicBool::new(false));
|
||||
// Fill the slab from the initial actor: slots are claimed at spawn
|
||||
// time, so children need not have run yet. Count until refusal.
|
||||
let mut held = Vec::new();
|
||||
loop {
|
||||
match try_spawn(holder(release.clone())) {
|
||||
Ok(h) => held.push(h),
|
||||
Err(e) => {
|
||||
assert_eq!(e, SpawnError::AtCapacity);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Initial actor occupies one slot; the rest were spawnable.
|
||||
assert_eq!(held.len(), MAX - 1, "slab accounting must be exact");
|
||||
// Still refusing (and still not panicking) on repeat.
|
||||
assert!(matches!(try_spawn(|| ()), Err(SpawnError::AtCapacity)));
|
||||
// The `_with` surface refuses identically — a custom shape must not
|
||||
// reach stack allocation when there is no slot for it.
|
||||
let opts = SpawnOpts {
|
||||
stack_reserve: Some(1024 * 1024),
|
||||
..SpawnOpts::default()
|
||||
};
|
||||
assert!(matches!(
|
||||
try_spawn_under_with(smarm::self_pid(), opts, || ()),
|
||||
Err(SpawnError::AtCapacity)
|
||||
));
|
||||
|
||||
// Self-heal: free the slots, join, and the next try_spawn succeeds.
|
||||
release.store(true, Ordering::Release);
|
||||
for h in held {
|
||||
h.join().unwrap();
|
||||
}
|
||||
let h = try_spawn(|| ()).expect("slots freed — must succeed again");
|
||||
h.join().unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_spawn_still_panics_at_capacity() {
|
||||
// The existing invariant-check semantics of `spawn` are untouched: at a
|
||||
// full slab it panics, the panic is caught at the actor isolation
|
||||
// boundary, and it surfaces as a join error — exactly as before. The
|
||||
// bomb actor is spawned into the LAST slot (so the slab is full only
|
||||
// once the bomb itself is live) and the panic lands inside the bomb,
|
||||
// not the initial actor.
|
||||
const MAX: usize = 6;
|
||||
smarm::runtime::init(Config::exact(1).max_actors(MAX)).run(|| {
|
||||
let release = Arc::new(AtomicBool::new(false));
|
||||
let mut held = Vec::new();
|
||||
for _ in 0..MAX - 2 {
|
||||
held.push(spawn(holder(release.clone())));
|
||||
}
|
||||
let armed = Arc::new(AtomicBool::new(false));
|
||||
let armed2 = armed.clone();
|
||||
let bomb = spawn(move || {
|
||||
armed2.store(true, Ordering::Release);
|
||||
// Slab is now full (initial + MAX−2 holders + this actor); the
|
||||
// plain spawn must panic this actor.
|
||||
let _ = spawn(|| ());
|
||||
unreachable!("allocate_slot must have panicked");
|
||||
});
|
||||
let err = bomb
|
||||
.join()
|
||||
.expect_err("bomb must die by panic, not run through");
|
||||
assert!(armed.load(Ordering::Acquire), "bomb must have actually run");
|
||||
// The panic message is a formatted String (panic! with args).
|
||||
let msg = err
|
||||
.payload
|
||||
.downcast_ref::<String>()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "<non-string payload>".into());
|
||||
assert!(
|
||||
msg.contains("slot table exhausted"),
|
||||
"panic must be the slab-exhaustion invariant message, got: {msg}"
|
||||
);
|
||||
release.store(true, Ordering::Release);
|
||||
for h in held {
|
||||
h.join().unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn racing_try_spawns_claim_exactly_the_free_slots() {
|
||||
// 4 scheduler threads, 4 spawner actors hammering try_spawn for a small
|
||||
// pool of remaining slots. Claim-or-report must hand out exactly the
|
||||
// free slots across all racers — no overshoot (TOCTOU), no panic.
|
||||
const MAX: usize = 32;
|
||||
const SPAWNERS: usize = 4;
|
||||
smarm::runtime::init(Config::exact(4).max_actors(MAX)).run(|| {
|
||||
let release = Arc::new(AtomicBool::new(false));
|
||||
let won = Arc::new(AtomicUsize::new(0));
|
||||
let done = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Occupy some slots up front so the racers fight over a remainder.
|
||||
let mut pre = Vec::new();
|
||||
for _ in 0..8 {
|
||||
pre.push(spawn(holder(release.clone())));
|
||||
}
|
||||
// Free slots now: MAX − 1 (initial) − 8 (pre) − SPAWNERS.
|
||||
let up_for_grabs = MAX - 1 - 8 - SPAWNERS;
|
||||
|
||||
let mut spawners = Vec::new();
|
||||
for _ in 0..SPAWNERS {
|
||||
let release = release.clone();
|
||||
let won = won.clone();
|
||||
let done = done.clone();
|
||||
spawners.push(spawn(move || {
|
||||
loop {
|
||||
match try_spawn(holder(release.clone())) {
|
||||
Ok(h) => {
|
||||
won.fetch_add(1, Ordering::AcqRel);
|
||||
drop(h); // detached; slot held by the holder
|
||||
}
|
||||
Err(SpawnError::AtCapacity) => break,
|
||||
Err(_) => unreachable!("non_exhaustive future-proofing"),
|
||||
}
|
||||
}
|
||||
done.fetch_add(1, Ordering::AcqRel);
|
||||
}));
|
||||
}
|
||||
// Wait for every racer to hit AtCapacity.
|
||||
while done.load(Ordering::Acquire) < SPAWNERS {
|
||||
yield_now();
|
||||
}
|
||||
assert_eq!(won.load(Ordering::Acquire), up_for_grabs);
|
||||
|
||||
release.store(true, Ordering::Release);
|
||||
for h in pre.into_iter().chain(spawners) {
|
||||
h.join().unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_error_is_a_real_error() {
|
||||
let e = SpawnError::AtCapacity;
|
||||
let msg = format!("{e}");
|
||||
assert!(
|
||||
msg.contains("capacity"),
|
||||
"Display should name the condition: {msg}"
|
||||
);
|
||||
let _: &dyn std::error::Error = &e;
|
||||
}
|
||||
Reference in New Issue
Block a user