6 Commits
Author SHA1 Message Date
smarm 8f2d513940 README: rewrite overview, add limitations, roadmap, and contribution notes
Expands the intro into an overview/limitations structure, documents
preemption, tracing/causal profiling, URUS, and adds a 'Coming up' and
'A note on open source' section.
2026-08-18 00:25:25 +02:00
Claude (sandbox)andClaude (sandbox) ca1c98336e feat(scheduler,runtime): non-panicking try_spawn for at-capacity load shedding
allocate_slot() panics on a full slab; for a load-shedding caller (an
accept loop spawning one actor per connection) that panic lands in the
spawning actor, which then crash-loops under Restart::Transient into the
still-full slab until its restart budget is spent — and the service stops
accepting entirely. Observed live (urus slowloris scaling, 2026-08-10).
A full slab is a routine overload condition for such callers, not an
invariant violation.

- RuntimeInner::try_allocate_slot() -> Option<u32>: the non-panicking
  core; a single pop under the free-list lock, so the claim is atomic
  (claim-or-report — no check-then-spawn TOCTOU, no headroom margin).
  allocate_slot() is now a thin panicking wrapper over it.
- scheduler::try_spawn / try_spawn_under_with -> Result<JoinHandle,
  SpawnError>: parity with spawn/spawn_under_with except a full slab
  returns Err(SpawnError::AtCapacity) instead of panicking. Minimal
  surface per the agreed strategy; the remaining _with/_addr mirrors are
  trivial wrappers if ever needed.
- Slot-first ordering on the try path (reverse of spawn's stack-first):
  under overload Err is the hot path, and a rejection costs one mutex
  pop — no mmap/pool-pop + init + recycle per shed unit of work. A
  drop-guard returns the claimed slot if stack allocation panics in the
  claim-to-install window (would otherwise leak and trip run()'s
  teardown slot-leak debug_assert).
- SpawnError: non_exhaustive, Display + std::error::Error.
- spawn and every existing call site untouched: the panic remains the
  correct loud invariant check at internal/bounded spawn sites.

tests/try_spawn.rs: parity when slots free; exact slab accounting at
capacity (Err, no panic, repeatable); custom-shape try refuses before
stack allocation; self-heal after slots free; plain spawn still panics
(surfaced via JoinError payload); 4-thread race for the last slots
claims exactly the free count; SpawnError impl checks.

Design doc: smarm-suggestion-try-spawn.md. Downstream consumer change
(canned 503 on AtCapacity in urus's accept loop) is urus scope, not
smarm.

(cherry picked from commit 36de4b36aeaa72b2a5f9f3797b9854652656dcf6)
2026-08-13 15:03:16 +02:00
smarm-agent 95306c7f60 style: cargo fmt sweep under rustc 1.97.1 (toolchain reformat, no semantic change) 2026-08-13 05:56:49 +00:00
smarm-agent 1262cc30e3 monitor: widen stamp eligibility to watchable = named ∪ exported (soak sig 5)
The terminal record existed for watches that raced their target's death, but
e43c673 scoped its stamp to named tenancies — and the pid-identity watch
surface (§4 Slice 3) targets arbitrary actors, including anonymous ones whose
pids cross the boundary in contract replies. The first wild pid-face hit
(width-20 soak, pid_watch_test.exs:47, 1/600 full-suite: a monitor installed
while the child was alive delivered :noproc instead of {:smarm_exit, :panic})
is exactly the residual a0ba9be's commit body deferred.

ever_named becomes `watchable`, with a second set-site: mark_watchable(pid),
which the bridge calls wherever a smarm pid is encoded across the boundary —
BEAM can only watch pids it holds, and can only hold pids that crossed.
Anonymous never-exported churn (holder threads, egress tasks) stays
ineligible, preserving e43c673's LIFO-eviction protection unchanged.

mark_watchable takes the cold lock before the liveness screen: finalize
publishes Done and reads the bit under the same lock, so the mark either
lands before the death stamps or observes the tenancy dead and no-ops —
no lost-stamp window, and marking a corpse cannot invent history (pinned
in the test alongside the mark-while-alive stamp).
2026-08-13 05:56:19 +00:00
smarm-agent 461fe4b768 fix(runtime): only named tenancies stamp the terminal record — anonymous churn must not evict it
Discovered wiring the bridge consult: with an unconditional stamp, the record
for the very death being raced was the shortest-lived data in the runtime.
Every green thread is a slot tenant, the free list is LIFO — so the slot a
named server's death frees is the first one recycled, and the next throwaway
exit (monitor holders, chain-runner work, anything) overwrote the record
before a raced watch could consult it. Deterministic bridge repro: the
corpse resolved fine, terminal_reason read None every time.

register_with now flags the tenancy (ever_named, reset at reclaim) before
the binding lands — set outside the registry lock, so no successfully
registered actor can die unflagged and a failed register's overshoot is
harmless — and finalize stamps only flagged tenancies. Watchable identities
are exactly the named ones (the bridge's pid-identity path deliberately
keeps Erlang's raw :noproc), so nothing consultable is lost.

Contract test updated: the three death modes now self-register; a new
anonymous control pins that unregistered deaths neither stamp nor evict.
2026-08-13 05:56:19 +00:00
smarm-agent b937f1f50f monitor/registry: terminal-outcome record — a raced watch can recover the real down reason (soak sig 4)
A watch installed after its target's death has, until now, only NoProc to
report — but the bridge's proxies install their native watch asynchronously
after acquire returns, so a link established before a crash (from the BEAM's
view) could still lose the panic's translated reason to that blanket NoProc
(width-20 soak signature 4: link_test.exs:26, 1/600 full-suite, 3/2000
link-only, all whereis-miss; deterministic repro in the bridge suite).

Two primitives, no change to monitor()'s own Erlang-faithful stale-pid
semantics — the upgrade is the caller's deliberate act:

- finalize_actor stamps the slot with (generation, DownReason) under the same
  cold-lock block that publishes the outcome. The record survives reclaim,
  registry pruning, and the next tenant's install; only the slot's next death
  overwrites it. terminal_reason(pid) reads it generation-matched.
- resolve_name(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 — whereis itself prunes on the way
  out, so a whereis-then-lookup consumer would find the evidence already
  destroyed. Live/Unbound match whereis's Some/None; the name heals exactly
  as before.

Contract pinned in tests/terminal_outcome_after_death.rs: one record per way
of dying (Exit/Panic/Stopped), no record while live, corpse capture + heal on
resolve_name, record independence from registry pruning, survival across slot
re-tenancy, overwrite at the next tenancy's death.
2026-08-13 05:56:19 +00:00
74 changed files with 2730 additions and 809 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "smarm" name = "smarm"
version = "0.6.0" version = "0.6.1"
edition = "2021" edition = "2021"
rust-version = "1.95" rust-version = "1.95"
+46 -43
View File
@@ -1,35 +1,38 @@
# smarm # 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 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.
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.
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 ## Quick taste
@@ -67,12 +70,9 @@ benches/
## Building and running ## Building and running
Standard Cargo. Requires Rust 1.95 or newer (the `#[naked]` attribute went stable 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.
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.
```sh ```sh
cargo test # all tests cargo test # all tests
@@ -80,26 +80,29 @@ cargo test --test mutex # one module
cargo bench # primes benchmark vs tokio 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 ## Docs
| Document | What it covers | | Document | What it covers |
|---|---| |---|---|
| [`Architecture.md`](./docs/Architecture.md) | Design intent, runtime model, and deferred work | | [`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_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 | | [`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 ## 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
View File
@@ -26,7 +26,9 @@ use std::time::Instant;
const ITERS: u32 = 15; const ITERS: u32 = 15;
fn available_threads() -> usize { 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 { fn env_sets() -> u32 {
@@ -108,17 +110,15 @@ fn bench_chained_smarm(threads: usize) -> (u64, u128) {
fn bench_chained_tokio_current() -> (u64, u128) { fn bench_chained_tokio_current() -> (u64, u128) {
let counter = Arc::new(AtomicU64::new(0)); let counter = Arc::new(AtomicU64::new(0));
let c2 = counter.clone(); 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 start = Instant::now();
let local = tokio::task::LocalSet::new(); let local = tokio::task::LocalSet::new();
local.block_on(&rt, async move { local.block_on(&rt, async move {
// Use a oneshot done channel like tokio's own chained_spawn bench. // Use a oneshot done channel like tokio's own chained_spawn bench.
let (done_tx, done_rx) = tokio::sync::oneshot::channel(); let (done_tx, done_rx) = tokio::sync::oneshot::channel();
fn iter( fn iter(c: Arc<AtomicU64>, done: tokio::sync::oneshot::Sender<()>, n: u64) {
c: Arc<AtomicU64>,
done: tokio::sync::oneshot::Sender<()>,
n: u64,
) {
if n == 0 { if n == 0 {
let _ = done.send(()); let _ = done.send(());
} else { } else {
@@ -186,7 +186,9 @@ fn bench_yield_smarm(threads: usize) -> (u64, u128) {
} }
fn bench_yield_tokio_current() -> (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 start = Instant::now();
let local = tokio::task::LocalSet::new(); let local = tokio::task::LocalSet::new();
local.block_on(&rt, async move { local.block_on(&rt, async move {
@@ -235,11 +237,22 @@ const PRIME_N: u64 = 400_000;
const PRIME_WORKERS: u64 = 64; const PRIME_WORKERS: u64 = 64;
fn is_prime(n: u64) -> bool { fn is_prime(n: u64) -> bool {
if n < 2 { return false; } if n < 2 {
if n < 4 { return true; } return false;
if n % 2 == 0 { return false; } }
if n < 4 {
return true;
}
if n % 2 == 0 {
return false;
}
let mut i = 3u64; 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 true
} }
@@ -250,7 +263,11 @@ fn count_primes(lo: u64, hi: u64) -> u64 {
fn primes_slice(w: u64) -> (u64, u64) { fn primes_slice(w: u64) -> (u64, u64) {
let per = PRIME_N / PRIME_WORKERS; let per = PRIME_N / PRIME_WORKERS;
let lo = w * per; 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) (lo, hi)
} }
@@ -267,7 +284,9 @@ fn bench_primes_smarm(threads: usize) -> (u64, u128) {
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed); 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()) (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) { fn bench_primes_tokio_current() -> (u64, u128) {
let total = Arc::new(AtomicU64::new(0)); let total = Arc::new(AtomicU64::new(0));
let t2 = total.clone(); 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 start = Instant::now();
let local = tokio::task::LocalSet::new(); let local = tokio::task::LocalSet::new();
local.block_on(&rt, async move { 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); 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()) (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); 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()) (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) { 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 start = Instant::now();
let local = tokio::task::LocalSet::new(); let local = tokio::task::LocalSet::new();
local.block_on(&rt, async move { local.block_on(&rt, async move {
@@ -395,7 +422,6 @@ fn bench_pp_tokio_multi() -> (u64, u128) {
// main // main
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Knob helper — reads SMARM_ALLOC_INTERVAL / SMARM_TIMESLICE_CYCLES env vars // Knob helper — reads SMARM_ALLOC_INTERVAL / SMARM_TIMESLICE_CYCLES env vars
// so the sweep script can override the preemption knobs without recompiling. // 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 { fn bench_cfg(threads: usize) -> smarm::runtime::Config {
let mut cfg = smarm::runtime::Config::exact(threads); let mut cfg = smarm::runtime::Config::exact(threads);
if let Ok(v) = std::env::var("SMARM_ALLOC_INTERVAL") { 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(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 cfg
} }
@@ -417,7 +447,10 @@ fn main() {
println!("smarm general benchmarks"); println!("smarm general benchmarks");
println!("available parallelism: {n} threads"); println!("available parallelism: {n} threads");
let sets = env_sets(); 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!( println!(
"CHAIN_DEPTH={CHAIN_DEPTH}, YIELD_TASKS={YIELD_TASKS}×{YIELD_ROUNDS}, \ "CHAIN_DEPTH={CHAIN_DEPTH}, YIELD_TASKS={YIELD_TASKS}×{YIELD_ROUNDS}, \
PRIME_N={PRIME_N}/{PRIME_WORKERS} workers, PP_ROUNDS={PP_ROUNDS}" PRIME_N={PRIME_N}/{PRIME_WORKERS} workers, PP_ROUNDS={PP_ROUNDS}"
@@ -426,21 +459,29 @@ fn main() {
// ---- 1. chained_spawn ---- // ---- 1. chained_spawn ----
print_header(&format!("chained_spawn: depth {CHAIN_DEPTH}")); print_header(&format!("chained_spawn: depth {CHAIN_DEPTH}"));
run_n("smarm 1-thread", ITERS, || bench_chained_smarm(1)); 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 current_thread", ITERS, bench_chained_tokio_current);
run_n("tokio multi-thread", ITERS, bench_chained_tokio_multi); run_n("tokio multi-thread", ITERS, bench_chained_tokio_multi);
// ---- 2. yield_many ---- // ---- 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("smarm 1-thread", ITERS, || bench_yield_smarm(1));
run_n(&format!("smarm {n}-thread"), ITERS, || bench_yield_smarm(n)); run_n(&format!("smarm {n}-thread"), ITERS, || bench_yield_smarm(n));
run_n("tokio current_thread", ITERS, bench_yield_tokio_current); run_n("tokio current_thread", ITERS, bench_yield_tokio_current);
run_n("tokio multi-thread", ITERS, bench_yield_tokio_multi); run_n("tokio multi-thread", ITERS, bench_yield_tokio_multi);
// ---- 3. fan_out_compute ---- // ---- 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("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 current_thread", ITERS, bench_primes_tokio_current);
run_n("tokio multi-thread", ITERS, bench_primes_tokio_multi); run_n("tokio multi-thread", ITERS, bench_primes_tokio_multi);
+76 -27
View File
@@ -64,11 +64,22 @@ const PRIME_N: u64 = 400_000;
const WORKERS: u64 = 64; const WORKERS: u64 = 64;
fn is_prime(n: u64) -> bool { fn is_prime(n: u64) -> bool {
if n < 2 { return false; } if n < 2 {
if n < 4 { return true; } return false;
if n % 2 == 0 { return false; } }
if n < 4 {
return true;
}
if n % 2 == 0 {
return false;
}
let mut i = 3u64; 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 true
} }
@@ -96,7 +107,9 @@ fn bench_primes_smarm(threads: usize) -> (u64, u128) {
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed); 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()) (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) { fn bench_primes_tokio_current() -> (u64, u128) {
let total = Arc::new(AtomicU64::new(0)); let total = Arc::new(AtomicU64::new(0));
let t2 = total.clone(); 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 start = Instant::now();
let local = tokio::task::LocalSet::new(); let local = tokio::task::LocalSet::new();
local.block_on(&rt, async move { 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); 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()) (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); 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()) (total.load(Ordering::Relaxed), start.elapsed().as_micros())
} }
fn bench_primes_baseline() -> (u64, u128) { fn bench_primes_baseline() -> (u64, u128) {
let start = Instant::now(); let start = Instant::now();
let total: u64 = (0..WORKERS).map(|w| { let total: u64 = (0..WORKERS)
.map(|w| {
let (lo, hi) = primes_slice(w); let (lo, hi) = primes_slice(w);
count_primes(lo, hi) count_primes(lo, hi)
}).sum(); })
.sum();
(total, start.elapsed().as_micros()) (total, start.elapsed().as_micros())
} }
@@ -167,15 +188,17 @@ fn bench_pingpong_smarm(threads: usize) -> (u64, u128) {
tx_a.send(0).unwrap(); tx_a.send(0).unwrap();
loop { loop {
let v = rx_b.recv().unwrap(); let v = rx_b.recv().unwrap();
if v >= PING_ROUNDS { break; } if v >= PING_ROUNDS {
break;
}
tx_a.send(v + 1).unwrap(); tx_a.send(v + 1).unwrap();
} }
}); });
let hb = smarm::spawn(move || { let hb = smarm::spawn(move || loop {
loop {
let v = rx_a.recv().unwrap(); let v = rx_a.recv().unwrap();
tx_b.send(v + 1).unwrap(); tx_b.send(v + 1).unwrap();
if v + 1 >= PING_ROUNDS { break; } if v + 1 >= PING_ROUNDS {
break;
} }
}); });
ha.join().unwrap(); ha.join().unwrap();
@@ -198,7 +221,9 @@ fn bench_pingpong_tokio_current() -> (u64, u128) {
tx_a.send(0).unwrap(); tx_a.send(0).unwrap();
loop { loop {
let v = rx_b.recv().await.unwrap(); let v = rx_b.recv().await.unwrap();
if v >= PING_ROUNDS { break; } if v >= PING_ROUNDS {
break;
}
tx_a.send(v + 1).unwrap(); tx_a.send(v + 1).unwrap();
} }
}); });
@@ -206,7 +231,9 @@ fn bench_pingpong_tokio_current() -> (u64, u128) {
loop { loop {
let v = rx_a.recv().await.unwrap(); let v = rx_a.recv().await.unwrap();
tx_b.send(v + 1).unwrap(); tx_b.send(v + 1).unwrap();
if v + 1 >= PING_ROUNDS { break; } if v + 1 >= PING_ROUNDS {
break;
}
} }
}); });
let _ = ha.await; let _ = ha.await;
@@ -229,7 +256,9 @@ fn bench_pingpong_tokio_multi() -> (u64, u128) {
tx_a.send(0).unwrap(); tx_a.send(0).unwrap();
loop { loop {
let v = rx_b.recv().await.unwrap(); let v = rx_b.recv().await.unwrap();
if v >= PING_ROUNDS { break; } if v >= PING_ROUNDS {
break;
}
tx_a.send(v + 1).unwrap(); tx_a.send(v + 1).unwrap();
} }
}); });
@@ -237,7 +266,9 @@ fn bench_pingpong_tokio_multi() -> (u64, u128) {
loop { loop {
let v = rx_a.recv().await.unwrap(); let v = rx_a.recv().await.unwrap();
tx_b.send(v + 1).unwrap(); tx_b.send(v + 1).unwrap();
if v + 1 >= PING_ROUNDS { break; } if v + 1 >= PING_ROUNDS {
break;
}
} }
}); });
let _ = ha.await; let _ = ha.await;
@@ -264,7 +295,9 @@ fn bench_spawn_smarm(threads: usize) -> (u64, u128) {
cc.fetch_add(1, Ordering::Relaxed); 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()) (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) { fn bench_spawn_tokio_current() -> (u64, u128) {
let counter = Arc::new(AtomicU64::new(0)); let counter = Arc::new(AtomicU64::new(0));
let c = counter.clone(); 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 start = Instant::now();
let local = tokio::task::LocalSet::new(); let local = tokio::task::LocalSet::new();
local.block_on(&rt, async move { local.block_on(&rt, async move {
@@ -283,7 +318,9 @@ fn bench_spawn_tokio_current() -> (u64, u128) {
cc.fetch_add(1, Ordering::Relaxed); 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()) (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); 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()) (counter.load(Ordering::Relaxed), start.elapsed().as_micros())
} }
@@ -320,22 +359,32 @@ fn main() {
println!("PRIME_N={PRIME_N}, WORKERS={WORKERS}, PING_ROUNDS={PING_ROUNDS}, SPAWN_COUNT={SPAWN_COUNT}"); println!("PRIME_N={PRIME_N}, WORKERS={WORKERS}, PING_ROUNDS={PING_ROUNDS}, SPAWN_COUNT={SPAWN_COUNT}");
// ---- Primes ---- // ---- Primes ----
print_header(&format!("Fan-out/fan-in: count primes in [2, {PRIME_N}) across {WORKERS} workers")); 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("baseline (serial)", ITERS, bench_primes_baseline);
run_n("smarm single-thread", ITERS, || bench_primes_smarm(1)); run_n("smarm single-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 current_thread", ITERS, bench_primes_tokio_current);
run_n("tokio multi-thread", ITERS, bench_primes_tokio_multi); run_n("tokio multi-thread", ITERS, bench_primes_tokio_multi);
// ---- Ping-pong ---- // ---- Ping-pong ----
print_header(&format!("Ping-pong: {PING_ROUNDS} round-trips between two actors")); print_header(&format!(
"Ping-pong: {PING_ROUNDS} round-trips between two actors"
));
run_n("smarm single-thread", ITERS, || bench_pingpong_smarm(1)); run_n("smarm single-thread", ITERS, || bench_pingpong_smarm(1));
run_n(&format!("smarm {n}-thread"), ITERS, || bench_pingpong_smarm(n)); run_n(&format!("smarm {n}-thread"), ITERS, || {
bench_pingpong_smarm(n)
});
run_n("tokio current_thread", ITERS, bench_pingpong_tokio_current); run_n("tokio current_thread", ITERS, bench_pingpong_tokio_current);
run_n("tokio multi-thread", ITERS, bench_pingpong_tokio_multi); run_n("tokio multi-thread", ITERS, bench_pingpong_tokio_multi);
// ---- Spawn throughput ---- // ---- Spawn throughput ----
print_header(&format!("Spawn throughput: {SPAWN_COUNT} actors spawned and joined")); print_header(&format!(
"Spawn throughput: {SPAWN_COUNT} actors spawned and joined"
));
run_n("smarm single-thread", ITERS, || bench_spawn_smarm(1)); run_n("smarm single-thread", ITERS, || bench_spawn_smarm(1));
run_n(&format!("smarm {n}-thread"), ITERS, || bench_spawn_smarm(n)); run_n(&format!("smarm {n}-thread"), ITERS, || bench_spawn_smarm(n));
run_n("tokio current_thread", ITERS, bench_spawn_tokio_current); run_n("tokio current_thread", ITERS, bench_spawn_tokio_current);
+24 -7
View File
@@ -16,12 +16,20 @@ const WORKERS: u64 = 16;
const ITERATIONS: u32 = 5; const ITERATIONS: u32 = 5;
fn is_prime(n: u64) -> bool { fn is_prime(n: u64) -> bool {
if n < 2 { return false; } if n < 2 {
if n < 4 { return true; } return false;
if n % 2 == 0 { return false; } }
if n < 4 {
return true;
}
if n % 2 == 0 {
return false;
}
let mut i = 3u64; let mut i = 3u64;
while i * i <= n { while i * i <= n {
if n % i == 0 { return false; } if n % i == 0 {
return false;
}
i += 2; i += 2;
} }
true true
@@ -30,7 +38,9 @@ fn is_prime(n: u64) -> bool {
fn count_primes_in(lo: u64, hi: u64) -> u64 { fn count_primes_in(lo: u64, hi: u64) -> u64 {
let mut count = 0u64; let mut count = 0u64;
for n in lo..hi { for n in lo..hi {
if is_prime(n) { count += 1; } if is_prime(n) {
count += 1;
}
} }
count count
} }
@@ -38,7 +48,11 @@ fn count_primes_in(lo: u64, hi: u64) -> u64 {
fn slice(worker: u64) -> (u64, u64) { fn slice(worker: u64) -> (u64, u64) {
let per = N / WORKERS; let per = N / WORKERS;
let lo = worker * per; 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) (lo, hi)
} }
@@ -125,7 +139,10 @@ fn main() {
"Counting primes in [2, {}) across {} workers, {} iterations each\n", "Counting primes in [2, {}) across {} workers, {} iterations each\n",
N, WORKERS, ITERATIONS 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)); println!("{}", "-".repeat(80));
run_n("baseline", ITERATIONS, bench_baseline); run_n("baseline", ITERATIONS, bench_baseline);
+44 -7
View File
@@ -27,12 +27,19 @@ use std::sync::Arc;
use std::time::Instant; use std::time::Instant;
fn env_usize(key: &str, default: usize) -> usize { 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> { fn env_threads() -> Vec<usize> {
std::env::var("SMARM_BENCH_THREADS") 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]) .unwrap_or_else(|_| vec![1, 2, 4])
} }
@@ -53,7 +60,11 @@ fn drive<Q: Send + Sync + 'static>(
for p in 0..producers { for p in 0..producers {
let q = q.clone(); let q = q.clone();
// Give the last producer the remainder. // 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 || { hs.push(std::thread::spawn(move || {
let pid = Pid::new(p as u32, 0); let pid = Pid::new(p as u32, 0);
for _ in 0..n { for _ in 0..n {
@@ -132,7 +143,12 @@ fn main() {
for &t in &threads_sweep { for &t in &threads_sweep {
for (p, c) in ratios_for(t) { for (p, c) in ratios_for(t) {
for s in ["mutex", "mpmc", "striped"] { 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 { if case.threads < 2 {
drive_single(&*q, MutexQueue::push, MutexQueue::pop, items) drive_single(&*q, MutexQueue::push, MutexQueue::pop, items)
} else { } else {
drive(q, MutexQueue::push, MutexQueue::pop, case.producers, case.consumers, items) drive(
q,
MutexQueue::push,
MutexQueue::pop,
case.producers,
case.consumers,
items,
)
} }
} }
"mpmc" => { "mpmc" => {
@@ -155,7 +178,14 @@ fn main() {
if case.threads < 2 { if case.threads < 2 {
drive_single(&*q, MpmcRing::push, MpmcRing::pop, items) drive_single(&*q, MpmcRing::push, MpmcRing::pop, items)
} else { } else {
drive(q, MpmcRing::push, MpmcRing::pop, case.producers, case.consumers, items) drive(
q,
MpmcRing::push,
MpmcRing::pop,
case.producers,
case.consumers,
items,
)
} }
} }
"striped" => { "striped" => {
@@ -163,7 +193,14 @@ fn main() {
if case.threads < 2 { if case.threads < 2 {
drive_single(&*q, StripedRing::push, StripedRing::pop, items) drive_single(&*q, StripedRing::push, StripedRing::pop, items)
} else { } else {
drive(q, StripedRing::push, StripedRing::pop, case.producers, case.consumers, items) drive(
q,
StripedRing::push,
StripedRing::pop,
case.producers,
case.consumers,
items,
)
} }
} }
_ => unreachable!(), _ => unreachable!(),
+21 -4
View File
@@ -54,12 +54,19 @@ fn variant() -> &'static str {
} }
fn env_usize(key: &str, default: usize) -> usize { 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> { fn env_threads() -> Vec<usize> {
std::env::var("SMARM_BENCH_THREADS") 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]) .unwrap_or_else(|_| vec![1, 2, 4])
} }
@@ -238,12 +245,22 @@ fn main() {
); );
println!( println!(
"RQCSV,runtime,{},{},{},{},{},{},{}", "RQCSV,runtime,{},{},{},{},{},{},{}",
variant(), slot_str, name, t, work, mid.us, per_s variant(),
slot_str,
name,
t,
work,
mid.us,
per_s
); );
if slot { if slot {
println!( println!(
"RQSLOT,{},{},{},{},{}", "RQSLOT,{},{},{},{},{}",
variant(), name, t, mid.hits, mid.displacements variant(),
name,
t,
mid.hits,
mid.displacements
); );
} }
} }
+55 -19
View File
@@ -37,7 +37,9 @@ use std::time::Instant;
const ITERS: u32 = 15; const ITERS: u32 = 15;
fn available_threads() -> usize { 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 { fn env_sets() -> u32 {
@@ -116,7 +118,9 @@ fn bench_recurse_smarm(threads: usize) -> (u64, u128) {
fn bench_recurse_tokio_current() -> (u64, u128) { fn bench_recurse_tokio_current() -> (u64, u128) {
let counter = Arc::new(AtomicU64::new(0)); let counter = Arc::new(AtomicU64::new(0));
let c2 = counter.clone(); 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 start = Instant::now();
let local = tokio::task::LocalSet::new(); let local = tokio::task::LocalSet::new();
local.block_on(&rt, async move { local.block_on(&rt, async move {
@@ -199,7 +203,9 @@ fn bench_hot_smarm() -> (u64, u128) {
} }
fn bench_hot_tokio_current() -> (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 start = Instant::now();
let local = tokio::task::LocalSet::new(); let local = tokio::task::LocalSet::new();
local.block_on(&rt, async move { local.block_on(&rt, async move {
@@ -249,7 +255,9 @@ fn bench_unc_smarm() -> (u64, u128) {
} }
fn bench_unc_tokio_current() -> (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 start = Instant::now();
let local = tokio::task::LocalSet::new(); let local = tokio::task::LocalSet::new();
local.block_on(&rt, async move { local.block_on(&rt, async move {
@@ -297,8 +305,12 @@ fn bench_panic_smarm(threads: usize) -> (u64, u128) {
} }
for h in handles { for h in handles {
match h.join() { match h.join() {
Ok(()) => { ok2.fetch_add(1, Ordering::Relaxed); } Ok(()) => {
Err(_) => { err2.fetch_add(1, Ordering::Relaxed); } 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 err = Arc::new(AtomicU64::new(0));
let ok2 = ok.clone(); let ok2 = ok.clone();
let err2 = err.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(); let prev_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {})); std::panic::set_hook(Box::new(|_| {}));
let start = Instant::now(); let start = Instant::now();
@@ -328,8 +342,12 @@ fn bench_panic_tokio_current() -> (u64, u128) {
} }
for h in handles { for h in handles {
match h.await { match h.await {
Ok(()) => { ok2.fetch_add(1, Ordering::Relaxed); } Ok(()) => {
Err(_) => { err2.fetch_add(1, Ordering::Relaxed); } 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 { for h in handles {
match h.await { match h.await {
Ok(()) => { ok2.fetch_add(1, Ordering::Relaxed); } Ok(()) => {
Err(_) => { err2.fetch_add(1, Ordering::Relaxed); } 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 // main
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Knob helper — reads SMARM_ALLOC_INTERVAL / SMARM_TIMESLICE_CYCLES env vars // Knob helper — reads SMARM_ALLOC_INTERVAL / SMARM_TIMESLICE_CYCLES env vars
// so the sweep script can override the preemption knobs without recompiling. // 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 { fn bench_cfg(threads: usize) -> smarm::runtime::Config {
let mut cfg = smarm::runtime::Config::exact(threads); let mut cfg = smarm::runtime::Config::exact(threads);
if let Ok(v) = std::env::var("SMARM_ALLOC_INTERVAL") { 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(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 cfg
} }
@@ -397,7 +422,10 @@ fn main() {
println!("smarm smarm-favored benchmarks"); println!("smarm smarm-favored benchmarks");
println!("available parallelism: {n} threads"); println!("available parallelism: {n} threads");
let sets = env_sets(); 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!( println!(
"RECURSE_DEPTH={RECURSE_DEPTH}, HOT_YIELDS={HOT_YIELDS}×2, \ "RECURSE_DEPTH={RECURSE_DEPTH}, HOT_YIELDS={HOT_YIELDS}×2, \
UNCONT_MSGS={UNCONT_MSGS}, PANIC_TASKS={PANIC_TASKS}" UNCONT_MSGS={UNCONT_MSGS}, PANIC_TASKS={PANIC_TASKS}"
@@ -406,22 +434,30 @@ fn main() {
// ---- 9. deep_recursion ---- // ---- 9. deep_recursion ----
print_header(&format!("deep_recursion: depth {RECURSE_DEPTH}")); print_header(&format!("deep_recursion: depth {RECURSE_DEPTH}"));
run_n("smarm 1-thread", ITERS, || bench_recurse_smarm(1)); 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 current_thread", ITERS, bench_recurse_tokio_current);
run_n("tokio multi-thread", ITERS, bench_recurse_tokio_multi); run_n("tokio multi-thread", ITERS, bench_recurse_tokio_multi);
// ---- 10. yield_in_hot_loop ---- // ---- 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("smarm 1-thread", ITERS, bench_hot_smarm);
run_n("tokio current_thread", ITERS, bench_hot_tokio_current); run_n("tokio current_thread", ITERS, bench_hot_tokio_current);
// ---- 11. uncontended_channel ---- // ---- 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("smarm 1-thread", ITERS, bench_unc_smarm);
run_n("tokio current_thread", ITERS, bench_unc_tokio_current); run_n("tokio current_thread", ITERS, bench_unc_tokio_current);
// ---- 12. catch_unwind_panics ---- // ---- 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("smarm 1-thread", ITERS, || bench_panic_smarm(1));
run_n(&format!("smarm {n}-thread"), ITERS, || bench_panic_smarm(n)); run_n(&format!("smarm {n}-thread"), ITERS, || bench_panic_smarm(n));
run_n("tokio current_thread", ITERS, bench_panic_tokio_current); run_n("tokio current_thread", ITERS, bench_panic_tokio_current);
+30 -5
View File
@@ -73,7 +73,10 @@ fn variant() -> &'static str {
} }
fn env_usize(key: &str, default: usize) -> usize { 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; 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 // Derived effective frequency: cycles per ns = GHz. Cross-checks the two
// lenses against the box's known base clock. // 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 p50 = pct(&pooled_ns, 50.0);
let p90 = pct(&pooled_ns, 90.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={} (instrumentation floor: {} ns / {} cyc, subtracted)",
rounds, warmup, runs, floor_ns, floor_cyc rounds, warmup, runs, floor_ns, floor_cyc
); );
println!(" {:<10} {:<10} {:<10} {:<10} {:<10}", "p50 ns", "p90 ns", "p99 ns", "min ns", "max ns"); println!(
println!(" {:<10} {:<10} {:<10} {:<10} {:<10}", p50, p90, p99, lo, hi); " {:<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!( println!(
" mean {:.1} ns | mean {:.0} cyc | derived {:.3} GHz", " mean {:.1} ns | mean {:.0} cyc | derived {:.3} GHz",
mean_ns, mean_cyc, derived_ghz mean_ns, mean_cyc, derived_ghz
@@ -251,6 +264,18 @@ fn main() {
// Greppable line — same spirit as SPINCSV. // Greppable line — same spirit as SPINCSV.
println!( println!(
"SWITCHCSV,{},{},{},{},{},{},{},{},{},{},{:.1},{:.0},{:.3}", "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
); );
} }
+105 -33
View File
@@ -36,7 +36,9 @@ use std::time::{Duration, Instant};
const ITERS: u32 = 15; const ITERS: u32 = 15;
fn available_threads() -> usize { 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 { fn env_sets() -> u32 {
@@ -114,11 +116,15 @@ fn bench_storm_smarm(threads: usize) -> (u64, u128) {
cc.fetch_add(1, Ordering::Relaxed); cc.fetch_add(1, Ordering::Relaxed);
})); }));
} }
for h in handles { h.join().unwrap(); } for h in handles {
h.join().unwrap();
}
// Tear down background. // Tear down background.
s2.store(true, Ordering::Relaxed); 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()) (counter.load(Ordering::Relaxed), start.elapsed().as_micros())
} }
@@ -129,7 +135,9 @@ fn bench_storm_tokio_current() -> (u64, u128) {
let c2 = counter.clone(); let c2 = counter.clone();
let s2 = stop.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 start = Instant::now();
let local = tokio::task::LocalSet::new(); let local = tokio::task::LocalSet::new();
local.block_on(&rt, async move { local.block_on(&rt, async move {
@@ -149,9 +157,13 @@ fn bench_storm_tokio_current() -> (u64, u128) {
cc.fetch_add(1, Ordering::Relaxed); 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); 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()) (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); 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); 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()) (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 () 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(); 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) { 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 start = Instant::now();
let local = tokio::task::LocalSet::new(); let local = tokio::task::LocalSet::new();
local.block_on(&rt, async move { local.block_on(&rt, async move {
@@ -248,10 +271,15 @@ fn bench_mpsc_tokio_current() -> (u64, u128) {
} }
count count
}); });
for h in prod_handles { let _ = h.await; } for h in prod_handles {
let _ = h.await;
}
let _ = consumer.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) { fn bench_mpsc_tokio_multi() -> (u64, u128) {
@@ -279,10 +307,15 @@ fn bench_mpsc_tokio_multi() -> (u64, u128) {
} }
count count
}); });
for h in prod_handles { let _ = h.await; } for h in prod_handles {
let _ = h.await;
}
let _ = consumer.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)); 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()) (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; 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()) (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; 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()) (TIMER_ACTORS, start.elapsed().as_micros())
} }
@@ -361,11 +400,22 @@ const SCALING_N: u64 = 400_000;
const SCALING_WORKERS: u64 = 64; const SCALING_WORKERS: u64 = 64;
fn is_prime(n: u64) -> bool { fn is_prime(n: u64) -> bool {
if n < 2 { return false; } if n < 2 {
if n < 4 { return true; } return false;
if n % 2 == 0 { return false; } }
if n < 4 {
return true;
}
if n % 2 == 0 {
return false;
}
let mut i = 3u64; 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 true
} }
@@ -376,7 +426,11 @@ fn count_primes(lo: u64, hi: u64) -> u64 {
fn scaling_slice(w: u64) -> (u64, u64) { fn scaling_slice(w: u64) -> (u64, u64) {
let per = SCALING_N / SCALING_WORKERS; let per = SCALING_N / SCALING_WORKERS;
let lo = w * per; 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) (lo, hi)
} }
@@ -393,7 +447,9 @@ fn bench_scaling_smarm(threads: usize) -> (u64, u128) {
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed); 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()) (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); 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()) (total.load(Ordering::Relaxed), start.elapsed().as_micros())
} }
@@ -424,7 +482,6 @@ fn bench_scaling_tokio_multi(threads: usize) -> (u64, u128) {
// main // main
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Knob helper — reads SMARM_ALLOC_INTERVAL / SMARM_TIMESLICE_CYCLES env vars // Knob helper — reads SMARM_ALLOC_INTERVAL / SMARM_TIMESLICE_CYCLES env vars
// so the sweep script can override the preemption knobs without recompiling. // 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 { fn bench_cfg(threads: usize) -> smarm::runtime::Config {
let mut cfg = smarm::runtime::Config::exact(threads); let mut cfg = smarm::runtime::Config::exact(threads);
if let Ok(v) = std::env::var("SMARM_ALLOC_INTERVAL") { 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(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 cfg
} }
@@ -446,7 +507,10 @@ fn main() {
println!("smarm tokio-favored benchmarks"); println!("smarm tokio-favored benchmarks");
println!("available parallelism: {n} threads"); println!("available parallelism: {n} threads");
let sets = env_sets(); 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!( println!(
"STORM_BACKGROUND={STORM_BACKGROUND}, STORM_SPAWN={STORM_SPAWN}, \ "STORM_BACKGROUND={STORM_BACKGROUND}, STORM_SPAWN={STORM_SPAWN}, \
MPSC={MPSC_PRODUCERS}×{MPSC_PER_PRODUCER}, \ 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" "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("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 current_thread", ITERS, bench_timers_tokio_current);
run_n("tokio multi-thread", ITERS, bench_timers_tokio_multi); run_n("tokio multi-thread", ITERS, bench_timers_tokio_multi);
@@ -487,13 +553,19 @@ fn main() {
)); ));
let sweep: Vec<usize> = { let sweep: Vec<usize> = {
let mut v = vec![1usize, 2, 4]; 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() v.into_iter().filter(|t| *t <= n).collect()
}; };
for t in &sweep { 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 { 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)
});
} }
} }
+3 -1
View File
@@ -67,7 +67,9 @@ fn main() {
println!("calibration: {per_us} work iters/µs"); println!("calibration: {per_us} work iters/µs");
let work_us = move |us: u64| work_iters(us * per_us); 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}"); println!("cores: {cores}");
if cores < 4 { if cores < 4 {
println!("probe: SKIPPED (needs the stages in parallel)"); println!("probe: SKIPPED (needs the stages in parallel)");
+8 -8
View File
@@ -35,8 +35,8 @@
#![deny(dead_code, unreachable_patterns)] #![deny(dead_code, unreachable_patterns)]
use smarm::gen_statem::{spawn, Cx, GenStatemRef, Machine, Reply, Resolution, Step};
use smarm::run; use smarm::run;
use smarm::gen_statem::{spawn, Cx, Machine, Reply, Resolution, Step, GenStatemRef};
// === user types ============================================================ // === user types ============================================================
@@ -123,7 +123,11 @@ impl DoorSm {
fn start(init: Door) -> GenStatemRef<DoorSm> { fn start(init: Door) -> GenStatemRef<DoorSm> {
spawn(DoorSm { spawn(DoorSm {
state: init, 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, (Door::Closed, Ev::Cast(Cast::Push | Cast::Unlock(_))) => Resolution::Unhandled,
// --- Locked (branching row: handler picks within UnlockOutcome) - // --- Locked (branching row: handler picks within UnlockOutcome) -
(Door::Locked, Ev::Cast(Cast::Unlock(key))) => { (Door::Locked, Ev::Cast(Cast::Unlock(key))) => Resolution::To(on_unlock(key).into()),
Resolution::To(on_unlock(key).into())
}
// Routed out in phase 1; listed only to keep this match total. // Routed out in phase 1; listed only to keep this match total.
(Door::Locked, Ev::Cast(Cast::Knock)) => { (Door::Locked, Ev::Cast(Cast::Knock)) => {
unreachable!("postponed event is replayed, not dispatched here") unreachable!("postponed event is replayed, not dispatched here")
} }
(Door::Locked, Ev::Cast(Cast::Push | Cast::Pull | Cast::Lock)) => { (Door::Locked, Ev::Cast(Cast::Push | Cast::Pull | Cast::Lock)) => Resolution::Unhandled,
Resolution::Unhandled
}
// --- state-independent queries (reply, then stay) --------------- // --- state-independent queries (reply, then stay) ---------------
(_, Ev::Call(Call::GetState(r))) => { (_, Ev::Call(Call::GetState(r))) => {
+9 -2
View File
@@ -18,8 +18,8 @@
// dispatch's own unreachable_patterns internally. // dispatch's own unreachable_patterns internally.
use smarm::gen_statem; use smarm::gen_statem;
use smarm::run;
use smarm::gen_statem::Reply; use smarm::gen_statem::Reply;
use smarm::run;
// === user types (identical to gen_statem_expanded.rs) ========================= // === user types (identical to gen_statem_expanded.rs) =========================
@@ -135,7 +135,14 @@ gen_statem! {
fn main() { fn main() {
run(|| { 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::Lock)).unwrap(); // Closed -> Locked
door.send(Ev::Cast(Cast::Knock)).unwrap(); // Locked: postponed (not yet counted) door.send(Ev::Cast(Cast::Knock)).unwrap(); // Locked: postponed (not yet counted)
+3 -1
View File
@@ -6,7 +6,9 @@
//! every use — so the address keeps working across a supervised restart, with //! every use — so the address keeps working across a supervised restart, with
//! no stale [`GenServerRef`] to refresh. //! 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`. /// A counter server: synchronous `Get`, asynchronous `Inc` / `Add`.
struct Counter { struct Counter {
+13 -3
View File
@@ -15,7 +15,9 @@
//! `call`, nothing more. //! `call`, nothing more.
use smarm::observer::{self, ObserverReply, ObserverRequest}; 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"); 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. /// A `ps`-style table over the flat snapshot.
fn print_snapshot(snap: &RuntimeSnapshot) { 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!( println!(
" {:<10} {:<9} {:<10} {:>4} {:>4} {:>4} {:>4} {:>5} {}", " {:<10} {:<9} {:<10} {:>4} {:>4} {:>4} {:>4} {:>5} {}",
"pid", "state", "parent", "mon", "lnk", "joi", "mbox", "msgs", "names" "pid", "state", "parent", "mon", "lnk", "joi", "mbox", "msgs", "names"
@@ -52,7 +58,11 @@ fn print_snapshot(snap: &RuntimeSnapshot) {
a.joiners, a.joiners,
a.mailbox_depth, a.mailbox_depth,
a.messages_received, 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(",")
},
); );
} }
} }
+20 -10
View File
@@ -342,8 +342,7 @@ mod inner {
// Count the loss in would-be delta terms so the audit's columns // Count the loss in would-be delta terms so the audit's columns
// compare directly against `injected_cycles`. // compare directly against `injected_cycles`.
DISCARD_OVERMAX_N.fetch_add(1, Ordering::Relaxed); DISCARD_OVERMAX_N.fetch_add(1, Ordering::Relaxed);
DISCARD_OVERMAX_CYCLES DISCARD_OVERMAX_CYCLES.fetch_add(interval.saturating_mul(pct) / 100, Ordering::Relaxed);
.fetch_add(interval.saturating_mul(pct) / 100, Ordering::Relaxed);
return; return;
} }
let delta = interval.saturating_mul(pct) / 100; let delta = interval.saturating_mul(pct) / 100;
@@ -419,8 +418,7 @@ mod inner {
let gap = preempt::rdtsc() let gap = preempt::rdtsc()
.saturating_sub(desched_tsc) .saturating_sub(desched_tsc)
.min(MAX_SAMPLE_CYCLES); .min(MAX_SAMPLE_CYCLES);
OFFCPU_IN_SITE_CYCLES OFFCPU_IN_SITE_CYCLES.fetch_add(gap.saturating_mul(pct) / 100, Ordering::Relaxed);
.fetch_add(gap.saturating_mul(pct) / 100, Ordering::Relaxed);
OFFCPU_IN_SITE_N.fetch_add(1, Ordering::Relaxed); OFFCPU_IN_SITE_N.fetch_add(1, Ordering::Relaxed);
} }
} }
@@ -533,7 +531,9 @@ mod inner {
park_forgiven_cycles: self park_forgiven_cycles: self
.park_forgiven_cycles .park_forgiven_cycles
.saturating_sub(before.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_park_n: self.drop_park_n.saturating_sub(before.drop_park_n),
drop_yield_cycles: self drop_yield_cycles: self
.drop_yield_cycles .drop_yield_cycles
@@ -542,12 +542,18 @@ mod inner {
discard_overmax_cycles: self discard_overmax_cycles: self
.discard_overmax_cycles .discard_overmax_cycles
.saturating_sub(before.discard_overmax_cycles), .saturating_sub(before.discard_overmax_cycles),
discard_overmax_n: self.discard_overmax_n.saturating_sub(before.discard_overmax_n), discard_overmax_n: self
discard_unarmed_n: self.discard_unarmed_n.saturating_sub(before.discard_unarmed_n), .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: self
.offcpu_in_site_cycles .offcpu_in_site_cycles
.saturating_sub(before.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 let cell = results
.iter() .iter()
.find(|r| r.site == site && r.speedup_pct == speedup_pct)?; .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 rate = normalized_rate(cell, point)?;
let b = normalized_rate(base, point)?; let b = normalized_rate(base, point)?;
if b <= 0.0 { if b <= 0.0 {
@@ -946,7 +954,9 @@ macro_rules! progress {
macro_rules! causal_site { macro_rules! causal_site {
($name:literal) => {{ ($name:literal) => {{
static __SMARM_SITE: ::std::sync::OnceLock<u32> = ::std::sync::OnceLock::new(); 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)),
)
}}; }};
} }
+45 -21
View File
@@ -104,7 +104,12 @@ pub fn channel<T>() -> (Sender<T>, Receiver<T>) {
senders: 1, senders: 1,
receiver_alive: true, receiver_alive: true,
})); }));
(Sender { inner: inner.clone() }, Receiver { inner }) (
Sender {
inner: inner.clone(),
},
Receiver { inner },
)
} }
struct Inner<T> { struct Inner<T> {
@@ -178,7 +183,9 @@ impl std::error::Error for RecvTimeoutError {}
impl<T> Clone for Sender<T> { impl<T> Clone for Sender<T> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
self.inner.lock().senders += 1; 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() g.parked_receiver.take()
}; };
if let Some((pid, epoch)) = unpark { 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); crate::scheduler::unpark_at(pid, epoch);
} else { } 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(()) Ok(())
} }
@@ -290,10 +305,12 @@ impl<T> Receiver<T> {
// Release the lock before parking: the unparker will need it. // Release the lock before parking: the unparker will need it.
crate::scheduler::park_current(); crate::scheduler::park_current();
// Woken up. Record it before looping to check the queue. // Woken up. Record it before looping to check the queue.
crate::te!(crate::trace::Event::RecvWake(match crate::actor::current_pid() { crate::te!(crate::trace::Event::RecvWake(
match crate::actor::current_pid() {
Some(p) => p, Some(p) => p,
None => panic!("smarm: RecvWake outside an actor (core corrupt)"), 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::insert_wait_timer(deadline, me, target, epoch);
crate::scheduler::park_current(); crate::scheduler::park_current();
crate::te!(crate::trace::Event::RecvWake(match crate::actor::current_pid() { crate::te!(crate::trace::Event::RecvWake(
match crate::actor::current_pid() {
Some(p) => p, Some(p) => p,
None => panic!("smarm: RecvWake outside an actor (core corrupt)"), None => panic!("smarm: RecvWake outside an actor (core corrupt)"),
})); }
));
let mut g = self.inner.lock(); let mut g = self.inner.lock();
if let Some(v) = g.queue.pop_front() { if let Some(v) = g.queue.pop_front() {
crate::preempt::note_message_received(); crate::preempt::note_message_received();
@@ -412,10 +431,12 @@ impl<T> Receiver<T> {
} }
// Release the lock before parking: the unparker will need it. // Release the lock before parking: the unparker will need it.
crate::scheduler::park_current(); crate::scheduler::park_current();
crate::te!(crate::trace::Event::RecvWake(match crate::actor::current_pid() { crate::te!(crate::trace::Event::RecvWake(
match crate::actor::current_pid() {
Some(p) => p, Some(p) => p,
None => panic!("smarm: RecvWake outside an actor (core corrupt)"), 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 // Channel-only selects skip all of it: `eager` is false, the guard
// is disarmed, and the loser-arm self-cleaning story is unchanged. // is disarmed, and the loser-arm self-cleaning story is unchanged.
let eager = arms.iter().any(|a| a.sel_eager_cleanup()); 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(); crate::scheduler::park_current();
@@ -687,11 +713,7 @@ impl Drop for UnregisterGuard<'_> {
// unregistered eagerly so none are left dangling. `Err` = an arm failed to // unregistered eagerly so none are left dangling. `Err` = an arm failed to
// register; same unwind (earlier fd arms unregistered, wait retired). // register; same unwind (earlier fd arms unregistered, wait retired).
// `Ok(None)` = every arm registered successfully; the caller parks. // `Ok(None)` = every arm registered successfully; the caller parks.
fn register_arms( fn register_arms(me: Pid, epoch: u32, arms: &[&dyn Selectable]) -> std::io::Result<Option<usize>> {
me: Pid,
epoch: u32,
arms: &[&dyn Selectable],
) -> std::io::Result<Option<usize>> {
for (i, arm) in arms.iter().enumerate() { for (i, arm) in arms.iter().enumerate() {
let registered = match arm.sel_register(me, epoch) { let registered = match arm.sel_register(me, epoch) {
Ok(r) => r, 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 /// 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 /// fails to register (see [`try_select_timeout`] for the fallible form; a
/// channel-only select can never fail). /// channel-only select can never fail).
pub fn select_timeout( pub fn select_timeout(arms: &[&dyn Selectable], timeout: std::time::Duration) -> Option<usize> {
arms: &[&dyn Selectable],
timeout: std::time::Duration,
) -> Option<usize> {
match try_select_timeout(arms, timeout) { match try_select_timeout(arms, timeout) {
Ok(r) => r, Ok(r) => r,
Err(e) => panic!( Err(e) => panic!(
@@ -776,7 +795,12 @@ pub fn try_select_timeout(
// would leave those fds unusable until a kernel event happened to // would leave those fds unusable until a kernel event happened to
// clear them. // clear them.
let eager = arms.iter().any(|a| a.sel_eager_cleanup()); 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(); crate::scheduler::park_current();
+26 -11
View File
@@ -16,10 +16,18 @@ thread_local! {
static ACTOR_SP: Cell<usize> = const { Cell::new(0) }; static ACTOR_SP: Cell<usize> = const { Cell::new(0) };
} }
fn get_scheduler_sp() -> usize { SCHEDULER_SP.with(|c| c.get()) } fn get_scheduler_sp() -> usize {
fn set_scheduler_sp(v: usize) { SCHEDULER_SP.with(|c| c.set(v)) } SCHEDULER_SP.with(|c| c.get())
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 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 // 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 { pub fn init_actor_stack(top: *mut u8, entry: extern "C-unwind" fn()) -> usize {
unsafe { unsafe {
let mut sp = (top as usize & !15) - 8; let mut sp = (top as usize & !15) - 8;
sp -= 8; (sp as *mut usize).write(entry as usize); // ret target sp -= 8;
sp -= 8; (sp as *mut usize).write(0); // rbx (sp as *mut usize).write(entry as usize); // ret target
sp -= 8; (sp as *mut usize).write(0); // rbp sp -= 8;
sp -= 8; (sp as *mut usize).write(0); // r12 (sp as *mut usize).write(0); // rbx
sp -= 8; (sp as *mut usize).write(0); // r13 sp -= 8;
sp -= 8; (sp as *mut usize).write(0); // r14 (sp as *mut usize).write(0); // rbp
sp -= 8; (sp as *mut usize).write(0); // r15 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 sp
} }
} }
+74 -30
View File
@@ -178,7 +178,9 @@
//! from any handler via [`Watcher::watch`]) because monitors are inherently //! from any handler via [`Watcher::watch`]) because monitors are inherently
//! created at runtime. The idle window is set once, in `init`. //! 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::monitor::{demonitor, monitor, Down, Monitor};
use crate::pid::Pid; use crate::pid::Pid;
use crate::registry::{register_with, resolve_named_sender, RegisterError}; 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> { impl<G: GenServer> Clone for GenServerRef<G> {
fn clone(&self) -> Self { 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 /// A clonable handle to the loop's monitor intake. Store it in the state
/// during `init` to watch monitors from later handlers. /// during `init` to watch monitors from later handlers.
pub fn watcher(&self) -> Watcher<G> { 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`. /// Shorthand for `ctx.watcher().watch(m)` when watching during `init`.
@@ -426,7 +433,10 @@ impl<G: GenServer> GenServerCtx<G> {
/// [`tick_every`](TimerHandle::tick_every) / /// [`tick_every`](TimerHandle::tick_every) /
/// [`cancel`](TimerHandle::cancel) from any later handler. /// [`cancel`](TimerHandle::cancel) from any later handler.
pub fn timer(&self) -> TimerHandle<G> { 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 /// 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. // Manual Clone for the same reason as `Watcher`: no `G: Clone` needed.
impl<G: GenServer> Clone for TimerHandle<G> { impl<G: GenServer> Clone for TimerHandle<G> {
fn clone(&self) -> Self { 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 // First instance fires after `every`; the payload is produced loop-side
// from `make` on fire, so the tick carries only the stable id. // 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)); let sub = send_after_to(every, self.sys_tx.clone(), Sys::Tick(local));
reg.periodics.insert(local, Periodic { every, live: sub, make }); reg.periodics.insert(
debug_assert!(reg.rearm_tx.is_some(), "rearm_tx must be Some while periodics is non-empty"); local,
Periodic {
every,
live: sub,
make,
},
);
debug_assert!(
reg.rearm_tx.is_some(),
"rearm_tx must be Some while periodics is non-empty"
);
local local
} }
@@ -617,7 +640,9 @@ pub struct Watcher<G: GenServer> {
// regardless of the server type (it clones only the inner sender). // regardless of the server type (it clones only the inner sender).
impl<G: GenServer> Clone for Watcher<G> { impl<G: GenServer> Clone for Watcher<G> {
fn clone(&self) -> Self { 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` /// live server). Consumes the builder, carrying its `with_info` / `under`
/// configuration through. /// configuration through.
pub fn named(self, name: GenServerName<G>) -> NamedGenServerBuilder<G> { 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 /// Private shared body behind [`start`](Self::start) and
@@ -700,18 +728,24 @@ impl<G: GenServer> GenServerBuilder<G> {
/// under the name before returning. /// under the name before returning.
fn spawn_server(self) -> GenServerRef<G> { fn spawn_server(self) -> GenServerRef<G> {
let (tx, rx) = channel::<Envelope<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 { let handle = match supervisor {
Some(sup) => { Some(sup) => crate::scheduler::spawn_under_with(sup, stack_opts, move || {
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) 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. /// associated constants at call sites.
#[inline] #[inline]
pub const fn new(name: &'static str) -> Self { pub const fn new(name: &'static str) -> Self {
Self { name, _marker: PhantomData } Self {
name,
_marker: PhantomData,
}
} }
/// The underlying registry key. /// 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 // 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 // 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). // 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); guard.0.init(&ctx);
let idle = ctx.idle.get(); let idle = ctx.idle.get();
drop(ctx); drop(ctx);
@@ -988,8 +1029,7 @@ fn server_loop<G: GenServer>(
// info band: [nd+nw, nd+nw+ni) // info band: [nd+nw, nd+nw+ni)
// inbox arm: [nd+nw+ni] // inbox arm: [nd+nw+ni]
let sel = { let sel = {
let mut arms: Vec<&dyn Selectable> = let mut arms: Vec<&dyn Selectable> = Vec::with_capacity(nd + nw + infos.len() + 1);
Vec::with_capacity(nd + nw + infos.len() + 1);
for m in &monitors { for m in &monitors {
arms.push(&m.rx); arms.push(&m.rx);
} }
@@ -1001,9 +1041,7 @@ fn server_loop<G: GenServer>(
} }
arms.push(&rx); arms.push(&rx);
match idle_deadline { match idle_deadline {
Some(dl) => { Some(dl) => select_timeout(&arms, dl.saturating_duration_since(Instant::now())),
select_timeout(&arms, dl.saturating_duration_since(Instant::now()))
}
None => Some(select(&arms)), None => Some(select(&arms)),
} }
}; };
@@ -1034,8 +1072,12 @@ fn server_loop<G: GenServer>(
// live set tracks only still-pending timers, then // live set tracks only still-pending timers, then
// dispatch. // dispatch.
match reg.lock() { match reg.lock() {
Ok(mut g) => { g.oneshots.remove(&id); } Ok(mut g) => {
Err(e) => panic!("smarm: gen_server reg lock poisoned (core corrupt): {e}"), g.oneshots.remove(&id);
}
Err(e) => {
panic!("smarm: gen_server reg lock poisoned (core corrupt): {e}")
}
} }
guard.0.handle_timer(msg); guard.0.handle_timer(msg);
reset_idle(&mut idle_deadline); reset_idle(&mut idle_deadline);
@@ -1049,7 +1091,9 @@ fn server_loop<G: GenServer>(
let msg = { let msg = {
let mut g = match reg.lock() { let mut g = match reg.lock() {
Ok(g) => g, 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; let r = &mut *g;
if let Some(p) = r.periodics.get_mut(&id) { if let Some(p) = r.periodics.get_mut(&id) {
@@ -1057,9 +1101,9 @@ fn server_loop<G: GenServer>(
let msg = (p.make)(); let msg = (p.make)();
let tx = match r.rearm_tx.as_ref() { let tx = match r.rearm_tx.as_ref() {
Some(tx) => tx.clone(), Some(tx) => tx.clone(),
None => panic!( None => {
"smarm: live periodic without rearm_tx (logic bug)" panic!("smarm: live periodic without rearm_tx (logic bug)")
), }
}; };
p.live = send_after_to(every, tx, Sys::Tick(id)); p.live = send_after_to(every, tx, Sys::Tick(id));
Some(msg) Some(msg)
+25 -10
View File
@@ -219,7 +219,11 @@ struct Timers {
impl Timers { impl Timers {
fn new() -> Self { 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 { fn mint(&mut self) -> u64 {
@@ -248,7 +252,11 @@ pub struct Cx<Ev> {
impl<Ev> Cx<Ev> { impl<Ev> Cx<Ev> {
fn new(sys_tx: Sender<Sys>, reg: Arc<Mutex<Timers>>) -> Self { 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 /// 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> { impl<M: Machine> Clone for GenStatemRef<M> {
fn clone(&self) -> Self { 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. /// a `_with` variant like the scheduler's own spawns.
/// ///
/// Panics if called outside `Runtime::run()`. /// Panics if called outside `Runtime::run()`.
pub fn spawn_with<M: Machine>( pub fn spawn_with<M: Machine>(opts: crate::scheduler::SpawnOpts, machine: M) -> GenStatemRef<M> {
opts: crate::scheduler::SpawnOpts,
machine: M,
) -> GenStatemRef<M> {
let (tx, rx) = channel::<M::Ev>(); let (tx, rx) = channel::<M::Ev>();
let handle = crate::scheduler::spawn_with(opts, move || statem_loop(rx, machine)); 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 /// 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) => { Sys::StateTimeout(local) => {
let mut t = match reg.lock() { let mut t = match reg.lock() {
Ok(g) => g, 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 { match t.state {
Some((live, _)) if live == local => { 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) => { Sys::Timeout(name, local) => {
let mut t = match reg.lock() { let mut t = match reg.lock() {
Ok(g) => g, 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) { match t.named.get(name) {
Some(&(live, _)) if live == local => { Some(&(live, _)) if live == local => {
+17 -4
View File
@@ -239,7 +239,10 @@ pub fn snapshot() -> RuntimeSnapshot {
actors.push(info); 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 /// want to inspect again) and want the tree view of it without re-reading
/// the runtime. /// the runtime.
pub fn tree_from(snap: RuntimeSnapshot) -> RuntimeTree { 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()); let mut index_of: HashMap<Pid, usize> = HashMap::with_capacity(actors.len());
for (i, a) in actors.iter().enumerate() { for (i, a) in actors.iter().enumerate() {
@@ -418,7 +424,10 @@ pub fn tree_from(snap: RuntimeSnapshot) -> RuntimeTree {
.into_iter() .into_iter()
.filter_map(|i| build_node(i, &children_of, &orphaned, &mut slots)) .filter_map(|i| build_node(i, &children_of, &orphaned, &mut slots))
.collect(); .collect();
RuntimeTree { format_version, roots: root_nodes } RuntimeTree {
format_version,
roots: root_nodes,
}
} }
fn build_node( fn build_node(
@@ -436,5 +445,9 @@ fn build_node(
.collect() .collect()
}) })
.unwrap_or_default(); .unwrap_or_default();
Some(TreeNode { info, orphaned: orphaned[i], children }) Some(TreeNode {
info,
orphaned: orphaned[i],
children,
})
} }
+4 -17
View File
@@ -136,7 +136,6 @@ pub struct IoThread {
waiters: Waiters, waiters: Waiters,
// ----- Epoll machinery ----- // ----- Epoll machinery -----
/// The epollfd, owned by `IoThread`. Callable cross-thread via /// The epollfd, owned by `IoThread`. Callable cross-thread via
/// `epoll_ctl` per the man page. /// `epoll_ctl` per the man page.
epollfd: RawFd, epollfd: RawFd,
@@ -147,7 +146,6 @@ pub struct IoThread {
shutdown_write: RawFd, shutdown_write: RawFd,
// ----- Threads ----- // ----- Threads -----
pool_thread: Option<OsJoinHandle<()>>, pool_thread: Option<OsJoinHandle<()>>,
epoll_thread: Option<OsJoinHandle<()>>, epoll_thread: Option<OsJoinHandle<()>>,
} }
@@ -284,9 +282,8 @@ impl IoThread {
events, events,
u64: fd as u64, u64: fd as u64,
}; };
let r = unsafe { let r =
libc::epoll_ctl(self.epollfd, libc::EPOLL_CTL_ADD, fd, &mut ev as *mut _) unsafe { libc::epoll_ctl(self.epollfd, libc::EPOLL_CTL_ADD, fd, &mut ev as *mut _) };
};
if r < 0 { if r < 0 {
return Err(io::Error::last_os_error()); return Err(io::Error::last_os_error());
} }
@@ -398,12 +395,7 @@ fn epoll_loop(epollfd: RawFd, waiters: Waiters, rt: Weak<RuntimeInner>) {
loop { loop {
let n = unsafe { let n = unsafe {
libc::epoll_wait( libc::epoll_wait(epollfd, events.as_mut_ptr(), MAX_EVENTS as libc::c_int, -1)
epollfd,
events.as_mut_ptr(),
MAX_EVENTS as libc::c_int,
-1,
)
}; };
if n < 0 { if n < 0 {
@@ -438,12 +430,7 @@ fn epoll_loop(epollfd: RawFd, waiters: Waiters, rt: Weak<RuntimeInner>) {
let entry = w.remove(&fd); let entry = w.remove(&fd);
if entry.is_some() { if entry.is_some() {
unsafe { unsafe {
libc::epoll_ctl( libc::epoll_ctl(epollfd, libc::EPOLL_CTL_DEL, fd, std::ptr::null_mut());
epollfd,
libc::EPOLL_CTL_DEL,
fd,
std::ptr::null_mut(),
);
} }
} }
entry entry
+38 -33
View File
@@ -11,36 +11,36 @@
//! //!
//! See `LOOM.md` for the design intent and the deferred-for-later list. //! 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 actor;
pub mod causal;
pub mod channel; pub mod channel;
pub mod scheduler; pub mod context;
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 gen_server; pub mod gen_server;
pub mod gen_statem; pub mod gen_statem;
pub mod introspect; pub mod introspect;
pub mod io;
pub mod link;
pub mod monitor;
pub mod mutex;
#[cfg(feature = "observer")] #[cfg(feature = "observer")]
pub mod observer; pub mod observer;
pub mod runtime;
pub(crate) mod park; pub(crate) mod park;
pub mod pg;
pub mod pid;
pub mod preempt;
pub(crate) mod raw_mutex; pub(crate) mod raw_mutex;
pub(crate) mod slot_state; pub mod registry;
pub(crate) mod sync_shim;
#[doc(hidden)] // pub only so benches/rq_micro.rs can drive the raw structures #[doc(hidden)] // pub only so benches/rq_micro.rs can drive the raw structures
pub mod run_queue; 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 trace;
pub mod causal;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Global allocator // Global allocator
@@ -59,34 +59,39 @@ pub use channel::{
}; };
pub use gen_server::{ pub use gen_server::{
call, cast, shutdown, whereis_server, CallError, CallTimeoutError, CastError, GenServer, 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::{ pub use gen_statem::{
CallError as GenStatemCallError, Cx, Machine, Reply, Resolution, SendError as GenStatemSendError, CallError as GenStatemCallError, Cx, GenStatemRef, Machine, Reply, Resolution,
GenStatemRef, SendError as GenStatemSendError,
}; };
pub use introspect::{StackInfo, pub use introspect::{
actor_info, snapshot, tree, tree_from, ActorInfo, ActorState, RuntimeSnapshot, RuntimeTree, 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")] #[cfg(feature = "observer")]
pub use observer::{ObserverReply, ObserverRequest}; pub use observer::{ObserverReply, ObserverRequest};
pub use link::{link, trap_exit, unlink, ExitSignal}; pub use pg::{
pub use monitor::{demonitor, monitor, Down, DownReason, Monitor, MonitorId}; dispatch, join, leave, members, members_as, pick, pick_as, Incarnation, Member, NodeId,
pub use mutex::{LockTimeout, Mutex, MutexGuard}; };
pub use pid::{Addressable, Erased, Name, Pid, RawPid}; 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::{ pub use registry::{
install, lookup_as, register, send, send_dyn, send_to, unregister, whereis, RegisterError, install, lookup_as, register, resolve_name, send, send_dyn, send_to, unregister, whereis,
SendError, NameResolution, RegisterError, SendError,
}; };
pub use runtime::{init, Config, Runtime}; pub use runtime::{init, Config, Runtime};
pub use scheduler::{ pub use scheduler::{
block_on_io, cancel_timer, request_stop, run, self_pid, send_after, send_after_named, 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, send_after_named_wall, send_after_wall, sleep, sleep_wall, spawn, spawn_addr, spawn_addr_with,
spawn, spawn_addr, spawn_addr_with, spawn_under, spawn_under_with, spawn_with, spawn_under, spawn_under_with, spawn_with, try_spawn, try_spawn_under_with, wait_readable,
wait_readable, wait_readable_timeout, wait_writable, wait_readable_timeout, wait_writable, wait_writable_timeout, yield_now, FdArm, JoinError,
wait_writable_timeout, yield_now, FdArm, JoinError, JoinHandle, SpawnOpts, JoinHandle, SpawnError, SpawnOpts,
}; };
pub use supervisor::{ChildSpec, OneForOne, Restart, Signal, Strategy}; pub use supervisor::{ChildSpec, OneForOne, Restart, Signal, Strategy};
pub use timer::TimerId; pub use timer::TimerId;
+4 -1
View File
@@ -157,7 +157,10 @@ pub fn link<A>(target: Pid<A>) {
}); });
match my_trap { match my_trap {
Some(tx) => { Some(tx) => {
let _ = tx.send(ExitSignal { from: target, reason: DownReason::NoProc }); let _ = tx.send(ExitSignal {
from: target,
reason: DownReason::NoProc,
});
} }
None => request_stop(me), None => request_stop(me),
} }
+62 -1
View File
@@ -171,12 +171,73 @@ pub fn monitor<A>(target: Pid<A>) -> Monitor {
}); });
if !registered { if !registered {
let _ = tx.send(Down { pid: target, reason: DownReason::NoProc }); let _ = tx.send(Down {
pid: target,
reason: DownReason::NoProc,
});
} }
Monitor { id, target, rx } 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 /// 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 /// 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 /// `None` if there was nothing left to remove: the target had already gone
+29 -10
View File
@@ -158,7 +158,11 @@ impl TimerTarget for MutexCore {
if st.holder == Some(pid) { if st.holder == Some(pid) {
return; 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) => { Some(pos) => {
st.waiters.remove(pos); st.waiters.remove(pos);
true true
@@ -246,7 +250,10 @@ impl<T> Mutex<T> {
Some(v) => v, Some(v) => v,
None => panic!("smarm: Mutex value missing on free fast path (core corrupt)"), 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, Some(v) => v,
None => panic!("smarm: Mutex value missing after grant (core corrupt)"), 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 { } else {
Err(LockTimeout) Err(LockTimeout)
} }
@@ -315,7 +325,10 @@ impl<T> Mutex<T> {
Some(v) => v, Some(v) => v,
None => panic!("smarm: Mutex value missing on try_lock free path (core corrupt)"), 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. /// Blocking fallback used when called outside the smarm runtime.
@@ -329,10 +342,15 @@ impl<T> Mutex<T> {
Ok(mut g) => g.take(), Ok(mut g) => g.take(),
Err(e) => panic!("smarm: mutex value lock poisoned (core corrupt): {e}"), 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(); 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 /// lock and one protected value; locking through any clone excludes
/// every other clone. /// every other clone.
fn clone(&self) -> Self { 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, Some(v) => v,
None => panic!("smarm: MutexGuard value missing (core corrupt)"), None => panic!("smarm: MutexGuard value missing (core corrupt)"),
}; };
f.debug_tuple("MutexGuard") f.debug_tuple("MutexGuard").field(value).finish()
.field(value)
.finish()
} }
} }
+38 -15
View File
@@ -105,7 +105,9 @@ mod parker {
impl Parker { impl Parker {
pub(super) fn new() -> Self { pub(super) fn new() -> Self {
Self { state: AtomicU32::new(EMPTY) } Self {
state: AtomicU32::new(EMPTY),
}
} }
/// Returns `true` = woken (permit consumed), `false` = timed out. /// Returns `true` = woken (permit consumed), `false` = timed out.
@@ -211,7 +213,10 @@ mod parker {
impl Parker { impl Parker {
pub(super) fn new() -> Self { 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. /// 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 // The CAS is the exactly-one guarantee: whoever clears the bit
// owns the wake; a racing wake_one retries on the observed value // owns the wake; a racing wake_one retries on the observed value
// (coherence: a failed CAS can never read older than `mask`). // (coherence: a failed CAS can never read older than `mask`).
match self.idle.compare_exchange( match self
mask, .idle
mask & !bit, .compare_exchange(mask, mask & !bit, Ordering::AcqRel, Ordering::Acquire)
Ordering::AcqRel, {
Ordering::Acquire,
) {
Ok(_) => { Ok(_) => {
self.parkers[id].unpark(); self.parkers[id].unpark();
return true; return true;
@@ -467,7 +470,8 @@ impl Coordinator {
// with the deadline still NO_DEADLINE compares `new < MAX` = true // with the deadline still NO_DEADLINE compares `new < MAX` = true
// and over-wakes — the benign direction. (Under the mandated timer // and over-wakes — the benign direction. (Under the mandated timer
// serialization this interleaving cannot occur anyway.) // 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 true
} }
@@ -578,7 +582,10 @@ mod tests {
let t0 = Instant::now(); let t0 = Instant::now();
let r = c.park(0, None, || false); let r = c.park(0, None, || false);
assert_eq!(r, ParkResult::Woken); 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] #[test]
@@ -632,14 +639,20 @@ mod tests {
// Wait until all four are published idle. // Wait until all four are published idle.
let t0 = Instant::now(); let t0 = Instant::now();
while c.idle_mask().count_ones() != N as u32 { 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(); std::thread::yield_now();
} }
assert!(c.wake_one()); assert!(c.wake_one());
// Exactly one wakes; give the others a beat to (incorrectly) wake. // Exactly one wakes; give the others a beat to (incorrectly) wake.
let t0 = Instant::now(); let t0 = Instant::now();
while woken.load(O::SeqCst) == 0 { 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::yield_now();
} }
std::thread::sleep(Duration::from_millis(100)); std::thread::sleep(Duration::from_millis(100));
@@ -706,7 +719,10 @@ mod tests {
assert_eq!(c.armed_deadline_nanos(), c.deadline_nanos(d2)); assert_eq!(c.armed_deadline_nanos(), c.deadline_nanos(d2));
c.disarm_timer(0); c.disarm_timer(0);
assert_eq!(c.armed_deadline_nanos(), NO_DEADLINE); 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); c.disarm_timer(1);
} }
@@ -721,7 +737,10 @@ mod tests {
let t0 = Instant::now(); let t0 = Instant::now();
let r = c.park(0, Some(far), || false); let r = c.park(0, Some(far), || false);
assert_eq!(r, ParkResult::Woken, "re-arm wake lost"); 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); c.disarm_timer(0);
} }
@@ -796,7 +815,11 @@ mod tests {
assert!(c.try_arm_timer(0, far)); assert!(c.try_arm_timer(0, far));
c.note_deadline(near); c.note_deadline(near);
let r = c.park(0, Some(far), || false); 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); c.disarm_timer(0);
} }
} }
+80 -17
View File
@@ -221,7 +221,9 @@ pub(crate) struct ProcessGroups {
impl ProcessGroups { impl ProcessGroups {
pub(crate) fn new() -> Self { 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 /// 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> { fn members_where(&self, group: &str, mut is_live: impl FnMut(Pid) -> bool) -> Vec<Pid> {
self.groups self.groups
.get(group) .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() .unwrap_or_default()
} }
/// The first live member of `group` in insertion order — stateless /// The first live member of `group` in insertion order — stateless
/// first-live `pick`, with the same read-path backstop as `members_where`. /// 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> { 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. /// Build the full member identity for `pid` from runtime identity.
fn member_for(inner: &crate::runtime::RuntimeInner, pid: Pid) -> Member { 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, /// 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 mon = monitor(pid);
let (rejected, reaped) = with_runtime(|inner| { 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 mut pg = inner.process_groups.lock();
let reaped = pg.reap_group(&group); let reaped = pg.reap_group(&group);
let rejected = pg.join(&group, ms); let rejected = pg.join(&group, ms);
@@ -507,7 +525,11 @@ mod tests {
let (tx, rx) = channel::<Down>(); let (tx, rx) = channel::<Down>();
let ms = Membership { let ms = Membership {
member: member(index, generation), member: member(index, generation),
monitor: Monitor { id: MonitorId(0), target: pid, rx }, monitor: Monitor {
id: MonitorId(0),
target: pid,
rx,
},
}; };
(ms, tx) (ms, tx)
} }
@@ -518,7 +540,10 @@ mod tests {
let (a, _ta) = synth(1, 0); let (a, _ta) = synth(1, 0);
let (b, _tb) = synth(1, 0); let (b, _tb) = synth(1, 0);
assert!(pg.join("workers", a).is_none(), "first join inserts"); 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)]); assert_eq!(pg.members_of("workers"), vec![member(1, 0)]);
} }
@@ -542,7 +567,10 @@ mod tests {
let (a, _ta) = synth(1, 0); let (a, _ta) = synth(1, 0);
let (b, _tb) = synth(1, 1); let (b, _tb) = synth(1, 1);
assert!(pg.join("g", a).is_none()); 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)]); assert_eq!(pg.members_of("g"), vec![member(1, 0), member(1, 1)]);
} }
@@ -555,16 +583,27 @@ mod tests {
pg.join("g", b); pg.join("g", b);
assert!(pg.leave("g", member(1, 0)).is_some()); assert!(pg.leave("g", member(1, 0)).is_some());
assert_eq!(pg.members_of("g"), vec![member(2, 0)]); 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.leave("g", member(2, 0)).is_some());
assert!(pg.members_of("g").is_empty(), "group is now empty"); 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] #[test]
fn remove_where_sweeps_every_group() { fn remove_where_sweeps_every_group() {
let mut pg = ProcessGroups::new(); 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); pg.join(g, m);
} }
// Death of pid index 1 (any generation) evicts it everywhere. // Death of pid index 1 (any generation) evicts it everywhere.
@@ -582,8 +621,16 @@ mod tests {
let pid = Pid::new(1, 0); let pid = Pid::new(1, 0);
let (tx, rx) = channel::<Down>(); let (tx, rx) = channel::<Down>();
let dead = Membership { let dead = Membership {
member: Member { node: DEFAULT_NODE_ID, incarnation: Incarnation::new(7), pid }, member: Member {
monitor: Monitor { id: MonitorId(0), target: pid, rx }, node: DEFAULT_NODE_ID,
incarnation: Incarnation::new(7),
pid,
},
monitor: Monitor {
id: MonitorId(0),
target: pid,
rx,
},
}; };
let _keep = tx; let _keep = tx;
let (live, _tl) = synth(2, 0); let (live, _tl) = synth(2, 0);
@@ -614,9 +661,17 @@ mod tests {
pg.join("b", b1); pg.join("b", b1);
// pid 1 dies: its group-a monitor receives a Down. Its group-b monitor // 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. // 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"); 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_eq!(pg.members_of("a"), vec![member(2, 0)]);
assert!(pg.members_of("b").is_empty(), "swept from b too; pruned"); 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 dead = Pid::new(1, 0);
let oracle = |pid: Pid| pid != dead; 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!(
assert_eq!(pg.first_member_where("g", oracle), Some(Pid::new(2, 0)), "pick skips the dead first member"); 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 // Backstop does not evict — that stays the monitor's job; raw storage
// still holds both until reap runs. // still holds both until reap runs.
+12 -3
View File
@@ -79,7 +79,10 @@ impl Pid<Erased> {
/// here; typing happens at typed-actor boundaries via [`Pid::from_raw`]. /// here; typing happens at typed-actor boundaries via [`Pid::from_raw`].
#[inline] #[inline]
pub const fn new(index: u32, generation: u32) -> Self { 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. /// resolution paths.
#[inline] #[inline]
pub(crate) const fn from_raw(raw: RawPid) -> Self { 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 /// 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. /// associated constants at call sites.
#[inline] #[inline]
pub const fn new(name: &'static str) -> Self { pub const fn new(name: &'static str) -> Self {
Self { name, _marker: PhantomData } Self {
name,
_marker: PhantomData,
}
} }
/// The underlying registry key. /// The underlying registry key.
+4 -1
View File
@@ -166,7 +166,10 @@ impl<T> RawMutex<T> {
{ {
self.lock_slow(); self.lock_slow();
} }
RawMutexGuard { m: self, prev_preempt } RawMutexGuard {
m: self,
prev_preempt,
}
} }
#[cold] #[cold]
+107 -15
View File
@@ -1,4 +1,3 @@
//! Give an actor a name so other actors can find it and message it. //! 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 //! 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 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
SendError::Unresolved(_) => write!(f, "no live actor registered under that name"), 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::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::Closed(_) => write!(f, "the actor's channel for this type is closed"),
SendError::NoMember(_) => write!(f, "no live member in the process group"), SendError::NoMember(_) => write!(f, "no live member in the process group"),
@@ -243,7 +244,10 @@ struct Mailbox {
impl Mailbox { impl Mailbox {
fn new(pid: Pid) -> Self { 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 /// Clone the `Sender<M>` for this actor, if it has one. Called **under the
@@ -294,7 +298,10 @@ pub(crate) struct Registry {
impl Registry { impl Registry {
pub(crate) fn new() -> Self { 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 /// 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. /// wholesale on pid mismatch) and is left untouched.
fn prune_holder(&mut self, holder: Pid) { fn prune_holder(&mut self, holder: Pid) {
self.by_name.retain(|_, p| *p != holder); 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()); self.by_index.remove(&holder.index());
} }
} }
@@ -351,7 +362,11 @@ impl Registry {
.iter() .iter()
.filter_map(|(&n, &p)| (p == mb.pid).then_some(n)) .filter_map(|(&n, &p)| (p == mb.pid).then_some(n))
.collect(); .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>, tx: Sender<M>,
) -> Result<(), RegisterError> { ) -> Result<(), RegisterError> {
with_runtime(|inner| { 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(); let mut reg = inner.registry.lock();
if !live(inner, me) { if !live(inner, me) {
return Err(RegisterError::NoProc); 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. /// index from a dead prior incarnation (pid mismatch) is replaced wholesale.
/// Caller holds the registry lock and has established that `me` is live. /// 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>) { 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 { if mb.pid != me {
*mb = Mailbox::new(me); *mb = Mailbox::new(me);
} }
mb.channels.insert( mb.channels.insert(
TypeId::of::<M>(), 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>) { pub(crate) fn install_for<M: Send + 'static>(pid: Pid, tx: Sender<M>) {
with_runtime(|inner| { with_runtime(|inner| {
let mut reg = inner.registry.lock(); 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); 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 /// 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 /// [`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 /// 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 // 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. // 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)) Some((pid, tx))
}) })
} }
@@ -535,7 +613,11 @@ pub fn unregister(name: &str) -> Option<Pid> {
with_runtime(|inner| { with_runtime(|inner| {
let mut reg = inner.registry.lock(); let mut reg = inner.registry.lock();
let pid = reg.by_name.remove(name)?; 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); reg.prune_holder(pid);
return Err(SendError::Unresolved(msg)); 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, Some(tx) => tx,
None => return Err(SendError::NoChannel(msg)), 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) { match reg.by_index.get(&pid.index()).map(|m| m.pid) {
// Exact incarnation, still alive: its `M` channel, or NoChannel. // Exact incarnation, still alive: its `M` channel, or NoChannel.
Some(stored) if stored == pid && live(inner, pid) => { 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, Some(tx) => tx,
None => return Err(SendError::NoChannel(msg)), None => return Err(SendError::NoChannel(msg)),
} }
@@ -611,7 +702,8 @@ fn send_to_pid<M: Send + 'static>(
_ => return Err(SendError::Dead(msg)), _ => 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 /// Deliver `msg` directly to the exact actor identified by `pid`. Unlike
+28 -5
View File
@@ -222,7 +222,10 @@ impl MpmcRing {
if diff == 0 { if diff == 0 {
// Our turn: claim the position. // Our turn: claim the position.
match self.enqueue_pos.0.compare_exchange_weak( match self.enqueue_pos.0.compare_exchange_weak(
pos, pos + 1, Ordering::Relaxed, Ordering::Relaxed, pos,
pos + 1,
Ordering::Relaxed,
Ordering::Relaxed,
) { ) {
Ok(_) => { Ok(_) => {
// SAFETY: the claim gives us exclusive write access // SAFETY: the claim gives us exclusive write access
@@ -250,7 +253,10 @@ impl MpmcRing {
let diff = seq as isize - (pos + 1) as isize; let diff = seq as isize - (pos + 1) as isize;
if diff == 0 { if diff == 0 {
match self.dequeue_pos.0.compare_exchange_weak( match self.dequeue_pos.0.compare_exchange_weak(
pos, pos + 1, Ordering::Relaxed, Ordering::Relaxed, pos,
pos + 1,
Ordering::Relaxed,
Ordering::Relaxed,
) { ) {
Ok(_) => { Ok(_) => {
// SAFETY: the claim gives us exclusive read access; // SAFETY: the claim gives us exclusive read access;
@@ -464,19 +470,36 @@ mod tests {
let popped = popped.lock().unwrap(); let popped = popped.lock().unwrap();
assert_eq!(popped.len(), total, "count mismatch"); 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!(set.len(), total, "duplicate or lost element");
assert_eq!(pop(&q), None); assert_eq!(pop(&q), None);
} }
#[test] #[test]
fn mpmc_exactly_once_contended() { 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] #[test]
fn striped_exactly_once_contended() { 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] #[test]
+136 -32
View File
@@ -112,10 +112,11 @@
//! deadline, so an expiry wakes one scheduler, not a herd. //! deadline, so an expiry wakes one scheduler, not a herd.
use crate::actor::{ use crate::actor::{
clear_current_pid, is_actor_done, reset_actor_done, set_current_actor_box, clear_current_pid, is_actor_done, reset_actor_done, set_current_actor_box, set_current_pid,
set_current_pid, take_last_outcome, Actor, Outcome, take_last_outcome, Actor, Outcome,
}; };
use crate::channel::Sender; use crate::channel::Sender;
use crate::context::{get_actor_sp, set_actor_sp, switch_to_actor};
use crate::io::IoThread; use crate::io::IoThread;
use crate::monitor::{Down, DownReason, MonitorId}; use crate::monitor::{Down, DownReason, MonitorId};
use crate::pid::Pid; use crate::pid::Pid;
@@ -124,11 +125,8 @@ use crate::raw_mutex::RawMutex;
use crate::slot_state::{StateWord, Status, Unpark}; use crate::slot_state::{StateWord, Status, Unpark};
use crate::supervisor::Signal; use crate::supervisor::Signal;
use crate::timer::Timers; use crate::timer::Timers;
use crate::context::{get_actor_sp, set_actor_sp, switch_to_actor};
use std::sync::atomic::{ use std::sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, AtomicU64, AtomicUsize, Ordering};
AtomicBool, AtomicPtr, AtomicU32, AtomicU64, AtomicUsize, Ordering,
};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::thread; use std::thread;
@@ -175,7 +173,9 @@ impl Config {
pub fn exact(n: usize) -> Self { pub fn exact(n: usize) -> Self {
assert!(n >= 1, "scheduler thread count must be ≥ 1"); assert!(n >= 1, "scheduler thread count must be ≥ 1");
Self { Self {
min: n, max: n, exact: Some(n), min: n,
max: n,
exact: Some(n),
alloc_interval: crate::preempt::DEFAULT_ALLOC_INTERVAL, alloc_interval: crate::preempt::DEFAULT_ALLOC_INTERVAL,
timeslice_cycles: crate::preempt::DEFAULT_TIMESLICE_CYCLES, timeslice_cycles: crate::preempt::DEFAULT_TIMESLICE_CYCLES,
stack_pool_cap: n * 4, stack_pool_cap: n * 4,
@@ -196,7 +196,9 @@ impl Config {
assert!(e >= 1, "exact must be ≥ 1"); assert!(e >= 1, "exact must be ≥ 1");
} }
Self { Self {
min, max, exact, min,
max,
exact,
alloc_interval: crate::preempt::DEFAULT_ALLOC_INTERVAL, alloc_interval: crate::preempt::DEFAULT_ALLOC_INTERVAL,
timeslice_cycles: crate::preempt::DEFAULT_TIMESLICE_CYCLES, timeslice_cycles: crate::preempt::DEFAULT_TIMESLICE_CYCLES,
stack_pool_cap: max * 4, stack_pool_cap: max * 4,
@@ -322,7 +324,9 @@ impl Default for Config {
.map(|n| n.get()) .map(|n| n.get())
.unwrap_or(1); .unwrap_or(1);
Self { Self {
min: 1, max: avail, exact: None, min: 1,
max: avail,
exact: None,
alloc_interval: crate::preempt::DEFAULT_ALLOC_INTERVAL, alloc_interval: crate::preempt::DEFAULT_ALLOC_INTERVAL,
timeslice_cycles: crate::preempt::DEFAULT_TIMESLICE_CYCLES, timeslice_cycles: crate::preempt::DEFAULT_TIMESLICE_CYCLES,
stack_pool_cap: avail * 4, stack_pool_cap: avail * 4,
@@ -376,7 +380,9 @@ pub struct RuntimeStats {
impl RuntimeStats { impl RuntimeStats {
/// Sum of run queue lengths across all scheduler threads. /// Sum of run queue lengths across all scheduler threads.
pub fn total_run_queue_len(&self) -> u64 { 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)) .map(|s| s.run_queue_len.load(Ordering::Relaxed))
.sum() .sum()
} }
@@ -400,7 +406,9 @@ impl RuntimeStats {
/// scheduler threads. Counters are reset at the start of each `run()`, /// scheduler threads. Counters are reset at the start of each `run()`,
/// so after a run this reads that run's total. /// so after a run this reads that run's total.
pub fn slot_hits(&self) -> u64 { pub fn slot_hits(&self) -> u64 {
self.inner.stats.iter() self.inner
.stats
.iter()
.map(|s| s.slot_hits.load(Ordering::Relaxed)) .map(|s| s.slot_hits.load(Ordering::Relaxed))
.sum() .sum()
} }
@@ -408,7 +416,9 @@ impl RuntimeStats {
/// RFC 005: total slot occupants displaced to the shared queue, summed /// RFC 005: total slot occupants displaced to the shared queue, summed
/// across scheduler threads. Reset at the start of each `run()`. /// across scheduler threads. Reset at the start of each `run()`.
pub fn slot_displacements(&self) -> u64 { pub fn slot_displacements(&self) -> u64 {
self.inner.stats.iter() self.inner
.stats
.iter()
.map(|s| s.slot_displacements.load(Ordering::Relaxed)) .map(|s| s.slot_displacements.load(Ordering::Relaxed))
.sum() .sum()
} }
@@ -472,6 +482,28 @@ pub(crate) struct SlotCold {
/// epoch-matched unpark. /// epoch-matched unpark.
pub(crate) waiters: Vec<(Pid, u32)>, pub(crate) waiters: Vec<(Pid, u32)>,
pub(crate) outcome: Option<Outcome>, 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>>, pub(crate) supervisor_channel: Option<Sender<Signal>>,
/// Watchers registered via `monitor()`, each tagged with its /// Watchers registered via `monitor()`, each tagged with its
/// `MonitorId` so `demonitor` can remove exactly one. Each receives one /// `MonitorId` so `demonitor` can remove exactly one. Each receives one
@@ -626,6 +658,8 @@ impl Slot {
actor: None, actor: None,
waiters: Vec::new(), waiters: Vec::new(),
outcome: None, outcome: None,
terminal: None,
watchable: false,
supervisor_channel: None, supervisor_channel: None,
monitors: Vec::new(), monitors: Vec::new(),
links: Vec::new(), links: Vec::new(),
@@ -689,7 +723,8 @@ impl Slot {
#[inline] #[inline]
pub(crate) fn record_message(&self) { pub(crate) fn record_message(&self) {
let v = self.messages_received.load(Ordering::Relaxed); 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). /// Read the received-message tally (Relaxed; cross-thread snapshot read).
@@ -708,7 +743,8 @@ impl Slot {
#[inline] #[inline]
pub(crate) fn add_budget(&self, cycles: u64) { pub(crate) fn add_budget(&self, cycles: u64) {
let v = self.budget_cycles.load(Ordering::Relaxed); 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 /// Read the accumulated budget cycles (Relaxed). Always 0 unless the
@@ -847,7 +883,6 @@ impl Slot {
Some(*unsafe { Box::from_raw(raw) }) Some(*unsafe { Box::from_raw(raw) })
} }
} }
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -1153,10 +1188,29 @@ impl RuntimeInner {
MonitorId(self.next_monitor_id.fetch_add(1, Ordering::Relaxed) + 1) 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 /// Pop a vacant slot index, or die loudly. The fixed slab is a deliberate
/// v0.5 simplification (ROADMAP: "Deferred"); the panic names the fix. /// 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 { pub(crate) fn allocate_slot(&self) -> u32 {
match self.free.lock().pop() { match self.try_allocate_slot() {
Some(idx) => idx, Some(idx) => idx,
None => panic!( None => panic!(
"smarm: actor slot table exhausted — {} actors are live \ "smarm: actor slot table exhausted — {} actors are live \
@@ -1246,11 +1300,13 @@ impl Runtime {
// Re-initialise shared state for this run. // Re-initialise shared state for this run.
assert_eq!( assert_eq!(
self.inner.run_queue.len(), 0, self.inner.run_queue.len(),
0,
"run() called while previous run still active" "run() called while previous run still active"
); );
debug_assert_eq!( 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" "run() called while previous run still active"
); );
// RFC 018: the IO producers reach the runtime (slot table + unpark) // RFC 018: the IO producers reach the runtime (slot table + unpark)
@@ -1397,7 +1453,9 @@ impl Runtime {
/// Snapshot of runtime statistics for introspection / tests. /// Snapshot of runtime statistics for introspection / tests.
pub fn stats(&self) -> RuntimeStats { pub fn stats(&self) -> RuntimeStats {
RuntimeStats { inner: self.inner.clone() } RuntimeStats {
inner: self.inner.clone(),
}
} }
} }
@@ -1432,7 +1490,10 @@ thread_local! {
} }
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub(crate) enum YieldIntent { Yield, Park } pub(crate) enum YieldIntent {
Yield,
Park,
}
pub(crate) fn set_yield_intent(i: YieldIntent) { pub(crate) fn set_yield_intent(i: YieldIntent) {
YIELD_INTENT.with(|c| c.set(i)); 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, /// for writes after reclaim — so an over-eager mark costs a cancel-write,
/// never data. /// never data.
fn maybe_shrink_stack(slot: &Slot) { 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); slot.parks_since_shrink.store(parks, Ordering::Relaxed);
let sp = slot.sp.load(Ordering::Relaxed); let sp = slot.sp.load(Ordering::Relaxed);
@@ -1566,12 +1630,19 @@ pub(crate) fn install_actor(
// publish below. // publish below.
let (diag_reserve, diag_guard) = stack.shape(); let (diag_reserve, diag_guard) = stack.shape();
let diag_top = stack.top() as usize; 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(); let mut cold = slot.cold.lock();
debug_assert!(cold.actor.is_none(), "install over live actor"); debug_assert!(cold.actor.is_none(), "install over live actor");
debug_assert!(cold.waiters.is_empty() && cold.monitors.is_empty() && cold.links.is_empty()); 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.outstanding_handles = 1;
cold.outcome = None; cold.outcome = None;
cold.pending_io_result = None; cold.pending_io_result = None;
@@ -1583,9 +1654,11 @@ pub(crate) fn install_actor(
slot.parks_since_shrink.store(0, Ordering::Relaxed); slot.parks_since_shrink.store(0, Ordering::Relaxed);
slot.shrink_count.store(0, Ordering::Relaxed); slot.shrink_count.store(0, Ordering::Relaxed);
slot.diag_stack_top.store(diag_top, 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_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.store_closure(closure);
slot.reset_counters(); slot.reset_counters();
inner.live_actors.fetch_add(1, Ordering::Relaxed); 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. // Release store orders everything above before any Acquire reader.
slot.word.publish_queued(gen); slot.word.publish_queued(gen);
inner.enqueue(pid); inner.enqueue(pid);
crate::te!(crate::trace::Event::Spawn { parent: supervisor, child: pid }); crate::te!(crate::trace::Event::Spawn {
parent: supervisor,
child: pid
});
pid pid
} }
@@ -1611,14 +1687,19 @@ pub(crate) fn install_actor(
/// is released — a last-sender drop can unpark a receiver, which takes the /// 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. /// run-queue mutex; legal under a cold lock, but pointless to nest.
pub(crate) fn reclaim_slot(inner: &RuntimeInner, pid: Pid) { 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 dropped_outside;
{ {
let mut cold = slot.cold.lock(); let mut cold = slot.cold.lock();
if slot.status_for(pid) != Status::Done || cold.outstanding_handles != 0 { if slot.status_for(pid) != Status::Done || cold.outstanding_handles != 0 {
return; // already reclaimed, or not yet eligible 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 = ( dropped_outside = (
cold.outcome.take(), cold.outcome.take(),
cold.supervisor_channel.take(), cold.supervisor_channel.take(),
@@ -1628,6 +1709,7 @@ pub(crate) fn reclaim_slot(inner: &RuntimeInner, pid: Pid) {
cold.waiters.clear(); cold.waiters.clear();
cold.monitors.clear(); cold.monitors.clear();
cold.links.clear(); cold.links.clear();
cold.watchable = false;
slot.reset_counters(); slot.reset_counters();
slot.stop_ptr.store(std::ptr::null_mut(), Ordering::Release); slot.stop_ptr.store(std::ptr::null_mut(), Ordering::Release);
// The generation bump IS the reclaim: every stale pid is dead from // 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"), None => panic!("finalize_actor: actor vanished"),
}; };
cold.outcome = Some(joiner_outcome); 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); slot.stop_ptr.store(std::ptr::null_mut(), Ordering::Release);
// Done is published under the cold lock, so join's // Done is published under the cold lock, so join's
// check-Done-or-register-waiter (also under it) can never miss: it // 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 // Notify monitors. Sent outside any slot lock: `send` may unpark a parked
// receiver, which takes the run-queue mutex. // receiver, which takes the run-queue mutex.
for (_, m) in monitors { 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 // 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 { match trap {
Some(Some(tx)) => { 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), Some(None) => crate::scheduler::request_stop(peer),
None => {} None => {}
@@ -1892,7 +1992,9 @@ fn schedule_loop(inner: &Arc<RuntimeInner>, slot_idx: usize) {
let io_out = inner.io_outstanding.load(Ordering::Acquire) let io_out = inner.io_outstanding.load(Ordering::Acquire)
+ inner.io_fd_waiters.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() { let pop = match inner.run_queue.pop() {
Some(pid) => Pop::Got(pid), Some(pid) => Pop::Got(pid),
None => { None => {
@@ -2044,7 +2146,9 @@ fn schedule_loop(inner: &Arc<RuntimeInner>, slot_idx: usize) {
} }
// Update per-thread stats: record who's on-CPU. // 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_actor_sp(sp);
set_current_pid(pid); set_current_pid(pid);
+157 -54
View File
@@ -68,9 +68,7 @@
use crate::actor::current_pid; use crate::actor::current_pid;
use crate::channel::Sender; use crate::channel::Sender;
use crate::pid::{Name, Pid}; use crate::pid::{Name, Pid};
use crate::runtime::{ use crate::runtime::{self, RuntimeInner, YieldIntent, RUNTIME};
self, RuntimeInner, YieldIntent, RUNTIME,
};
use crate::supervisor::Signal; use crate::supervisor::Signal;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::sync::Arc; use std::sync::Arc;
@@ -152,7 +150,9 @@ pub struct JoinHandle {
impl JoinHandle { impl JoinHandle {
/// The identity of the actor this handle refers to. /// 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 /// Block the calling actor until the spawned actor finishes, then
/// report how it finished: `Ok(())` if it returned normally or stopped /// report how it finished: `Ok(())` if it returned normally or stopped
@@ -182,12 +182,10 @@ impl JoinHandle {
crate::slot_state::Status::Stale => { crate::slot_state::Status::Stale => {
panic!("join: target slot has been reused") panic!("join: target slot has been reused")
} }
crate::slot_state::Status::Done => { crate::slot_state::Status::Done => Some(match cold.outcome.take() {
Some(match cold.outcome.take() {
Some(outcome) => outcome, Some(outcome) => outcome,
None => panic!("Done slot must have outcome"), None => panic!("Done slot must have outcome"),
}) }),
}
crate::slot_state::Status::Live => { crate::slot_state::Status::Live => {
// begin_wait is lock-free, legal under the cold lock; // begin_wait is lock-free, legal under the cold lock;
// registering under it makes the epoch atomic with // registering under it makes the epoch atomic with
@@ -227,8 +225,7 @@ impl JoinHandle {
match slot.status_for(self.pid) { match slot.status_for(self.pid) {
crate::slot_state::Status::Stale => false, crate::slot_state::Status::Stale => false,
status => { status => {
cold.outstanding_handles = cold.outstanding_handles = cold.outstanding_handles.saturating_sub(1);
cold.outstanding_handles.saturating_sub(1);
cold.outstanding_handles == 0 cold.outstanding_handles == 0
&& status == crate::slot_state::Status::Done && status == crate::slot_state::Status::Done
} }
@@ -281,6 +278,37 @@ pub struct SpawnOpts {
pub guard_size: Option<usize>, 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. /// Start a new actor running `f`, and return a [`JoinHandle`] for it.
/// ///
/// The new actor runs concurrently with its caller and with every other /// 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). /// [`spawn`] with per-actor stack shape overrides (RFC 019).
pub fn spawn_with(opts: SpawnOpts, f: impl FnOnce() + Send + 'static) -> JoinHandle { pub fn spawn_with(opts: SpawnOpts, f: impl FnOnce() + Send + 'static) -> JoinHandle {
let parent = current_pid().unwrap_or_else(|| { let parent = current_pid().unwrap_or_else(|| with_runtime(|_| crate::runtime::ROOT_PID));
with_runtime(|_| crate::runtime::ROOT_PID)
});
spawn_under_with(parent, opts, f) 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) 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>`], /// 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 _np = NoPreempt::enter();
let epoch = begin_wait(); let epoch = begin_wait();
let deadline = crate::timer::deadline_from_now(duration); let deadline = crate::timer::deadline_from_now(duration);
with_runtime(|inner| { with_runtime(|inner| match inner.timers.lock() {
match inner.timers.lock() {
Ok(mut timers) => timers.insert_sleep(deadline, me, epoch), Ok(mut timers) => timers.insert_sleep(deadline, me, epoch),
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"), Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
}
}); });
park_current(); park_current();
} }
@@ -588,11 +682,9 @@ pub fn sleep_wall(duration: std::time::Duration) {
let _np = NoPreempt::enter(); let _np = NoPreempt::enter();
let epoch = begin_wait(); let epoch = begin_wait();
let deadline = crate::timer::deadline_from_now(duration); let deadline = crate::timer::deadline_from_now(duration);
with_runtime(|inner| { with_runtime(|inner| match inner.timers.lock() {
match inner.timers.lock() {
Ok(mut timers) => timers.insert_sleep_wall(deadline, me, epoch), Ok(mut timers) => timers.insert_sleep_wall(deadline, me, epoch),
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"), Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
}
}); });
park_current(); park_current();
} }
@@ -608,15 +700,13 @@ pub fn insert_wait_timer(
target: std::sync::Arc<dyn crate::timer::TimerTarget>, target: std::sync::Arc<dyn crate::timer::TimerTarget>,
epoch: u32, epoch: u32,
) { ) {
with_runtime(|inner| { with_runtime(|inner| match inner.timers.lock() {
match inner.timers.lock() {
Ok(mut timers) => timers.insert( Ok(mut timers) => timers.insert(
deadline, deadline,
pid, pid,
crate::timer::Reason::WaitTimeout { target, epoch }, crate::timer::Reason::WaitTimeout { target, epoch },
), ),
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"), 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 fire = Box::new(move || {
let _ = crate::registry::send_to(dest, msg); let _ = crate::registry::send_to(dest, msg);
}); });
with_runtime(|inner| { with_runtime(|inner| match inner.timers.lock() {
match inner.timers.lock() {
Ok(mut timers) => timers.insert_send(deadline, dest.erase(), fire), Ok(mut timers) => timers.insert_send(deadline, dest.erase(), fire),
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"), 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 fire = Box::new(move || {
let _ = crate::registry::send(dest, msg); let _ = crate::registry::send(dest, msg);
}); });
with_runtime(|inner| { with_runtime(|inner| match inner.timers.lock() {
match inner.timers.lock() {
Ok(mut timers) => timers.insert_send(deadline, armed_by, fire), Ok(mut timers) => timers.insert_send(deadline, armed_by, fire),
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"), 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 fire = Box::new(move || {
let _ = crate::registry::send_to(dest, msg); let _ = crate::registry::send_to(dest, msg);
}); });
with_runtime(|inner| { with_runtime(|inner| match inner.timers.lock() {
match inner.timers.lock() {
Ok(mut timers) => timers.insert_send_wall(deadline, dest.erase(), fire), Ok(mut timers) => timers.insert_send_wall(deadline, dest.erase(), fire),
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"), 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 fire = Box::new(move || {
let _ = crate::registry::send(dest, msg); let _ = crate::registry::send(dest, msg);
}); });
with_runtime(|inner| { with_runtime(|inner| match inner.timers.lock() {
match inner.timers.lock() {
Ok(mut timers) => timers.insert_send_wall(deadline, armed_by, fire), Ok(mut timers) => timers.insert_send_wall(deadline, armed_by, fire),
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"), 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 fire = Box::new(move || {
let _ = tx.send(msg); let _ = tx.send(msg);
}); });
with_runtime(|inner| { with_runtime(|inner| match inner.timers.lock() {
match inner.timers.lock() {
Ok(mut timers) => timers.insert_send(deadline, armed_by, fire), Ok(mut timers) => timers.insert_send(deadline, armed_by, fire),
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"), 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 /// 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. /// now prevented, `false` if it had already fired or was already cancelled.
pub fn cancel_timer(id: crate::timer::TimerId) -> bool { pub fn cancel_timer(id: crate::timer::TimerId) -> bool {
with_runtime(|inner| { with_runtime(|inner| match inner.timers.lock() {
match inner.timers.lock() {
Ok(mut timers) => timers.cancel(id), Ok(mut timers) => timers.cancel(id),
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"), Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
}
}) })
} }
@@ -820,7 +898,8 @@ where
}; };
let mut cold = slot.cold.lock(); let mut cold = slot.cold.lock();
debug_assert_eq!( debug_assert_eq!(
slot.generation(), me.generation(), slot.generation(),
me.generation(),
"block_on_io: own slot reused mid-park" "block_on_io: own slot reused mid-park"
); );
match cold.pending_io_result.take() { match cold.pending_io_result.take() {
@@ -946,12 +1025,20 @@ pub struct FdArm {
impl FdArm { impl FdArm {
/// An arm that becomes ready when `fd` is readable. /// An arm that becomes ready when `fd` is readable.
pub fn readable(fd: std::os::fd::RawFd) -> Self { 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. /// An arm that becomes ready when `fd` is writable.
pub fn writable(fd: std::os::fd::RawFd) -> Self { 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() { match io.as_mut() {
Some(io) => { Some(io) => {
inner.io_fd_waiters.fetch_add(1, Ordering::AcqRel); inner.io_fd_waiters.fetch_add(1, Ordering::AcqRel);
let r = io.epoll_register( let r = io.epoll_register(self.fd, pid, epoch, self.readable, self.writable);
self.fd, pid, epoch, self.readable, self.writable,
);
if r.is_err() { if r.is_err() {
inner.io_fd_waiters.fetch_sub(1, Ordering::AcqRel); 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 { if writable {
events |= libc::POLLOUT; events |= libc::POLLOUT;
} }
let mut pfd = libc::pollfd { fd, events, revents: 0 }; let mut pfd = libc::pollfd {
fd,
events,
revents: 0,
};
loop { loop {
let r = unsafe { libc::poll(&mut pfd, 1, 0) }; let r = unsafe { libc::poll(&mut pfd, 1, 0) };
if r < 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> { pub fn read(fd: std::os::fd::RawFd, buf: &mut [u8]) -> std::io::Result<usize> {
wait_readable(fd)?; wait_readable(fd)?;
let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, buf.len()) }; 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 /// 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> { pub fn write(fd: std::os::fd::RawFd, buf: &[u8]) -> std::io::Result<usize> {
wait_writable(fd)?; wait_writable(fd)?;
let n = unsafe { libc::write(fd, buf.as_ptr() as *const _, buf.len()) }; 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>) { pub fn register_supervisor_channel(pid: Pid, sender: Sender<Signal>) {
with_runtime(|inner| { 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)); .unwrap_or_else(|| panic!("register_supervisor_channel: pid {:?} not found", pid));
let mut cold = slot.cold.lock(); let mut cold = slot.cold.lock();
assert_eq!( assert_eq!(
slot.generation(), pid.generation(), slot.generation(),
"register_supervisor_channel: pid {:?} not found", pid pid.generation(),
"register_supervisor_channel: pid {:?} not found",
pid
); );
cold.supervisor_channel = Some(sender); cold.supervisor_channel = Some(sender);
}); });
@@ -1177,6 +1277,9 @@ mod send_after_to_tests {
crate::sleep(Duration::from_millis(30)); crate::sleep(Duration::from_millis(30));
r2.store(true, Ordering::SeqCst); 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
View File
@@ -214,7 +214,10 @@ struct Buf {
impl Buf { impl Buf {
fn new() -> Self { fn new() -> Self {
Buf { b: [0; 320], len: 0 } Buf {
b: [0; 320],
len: 0,
}
} }
fn s(&mut self, s: &str) { fn s(&mut self, s: &str) {
for &c in s.as_bytes() { for &c in s.as_bytes() {
@@ -274,8 +277,14 @@ mod tests {
#[test] #[test]
fn inside_guard_both_edges() { fn inside_guard_both_edges() {
assert_eq!(classify(GUARD_LO, TOP, RESERVE, GUARD), FaultClass::Guard); assert_eq!(classify(GUARD_LO, TOP, RESERVE, GUARD), FaultClass::Guard);
assert_eq!(classify(GUARD_HI - 1, TOP, RESERVE, GUARD), FaultClass::Guard); assert_eq!(
assert_eq!(classify(GUARD_LO + GUARD / 2, TOP, RESERVE, GUARD), FaultClass::Guard); classify(GUARD_HI - 1, TOP, RESERVE, GUARD),
FaultClass::Guard
);
assert_eq!(
classify(GUARD_LO + GUARD / 2, TOP, RESERVE, GUARD),
FaultClass::Guard
);
} }
#[test] #[test]
+9 -9
View File
@@ -188,8 +188,7 @@ impl StateWord {
loop { loop {
let w = self.load(); let w = self.load();
debug_assert!( debug_assert!(
matches!(word_state(w), ST_RUNNING | ST_RUNNING_NOTIFIED) matches!(word_state(w), ST_RUNNING | ST_RUNNING_NOTIFIED) && word_gen(w) == gen,
&& word_gen(w) == gen,
"yield return from invalid word {w:#x}" "yield return from invalid word {w:#x}"
); );
if self if self
@@ -247,8 +246,7 @@ impl StateWord {
loop { loop {
let w = self.load(); let w = self.load();
debug_assert!( debug_assert!(
matches!(word_state(w), ST_RUNNING | ST_RUNNING_NOTIFIED) matches!(word_state(w), ST_RUNNING | ST_RUNNING_NOTIFIED) && word_gen(w) == gen,
&& word_gen(w) == gen,
"begin_wait from invalid word {w:#x}" "begin_wait from invalid word {w:#x}"
); );
let next = word_epoch(w).wrapping_add(1) & EPOCH_MASK; let next = word_epoch(w).wrapping_add(1) & EPOCH_MASK;
@@ -342,8 +340,7 @@ impl StateWord {
loop { loop {
let w = self.load(); let w = self.load();
debug_assert!( debug_assert!(
matches!(word_state(w), ST_RUNNING | ST_RUNNING_NOTIFIED) matches!(word_state(w), ST_RUNNING | ST_RUNNING_NOTIFIED) && word_gen(w) == gen,
&& word_gen(w) == gen,
"clear_notify from invalid word {w:#x}" "clear_notify from invalid word {w:#x}"
); );
if word_state(w) != ST_RUNNING_NOTIFIED { if word_state(w) != ST_RUNNING_NOTIFIED {
@@ -372,8 +369,7 @@ impl StateWord {
pub(crate) fn set_done(&self, gen: u32) { pub(crate) fn set_done(&self, gen: u32) {
let prev = self.0.swap(pack(gen, 0, ST_DONE), Ordering::AcqRel); let prev = self.0.swap(pack(gen, 0, ST_DONE), Ordering::AcqRel);
debug_assert!( debug_assert!(
matches!(word_state(prev), ST_RUNNING | ST_RUNNING_NOTIFIED) matches!(word_state(prev), ST_RUNNING | ST_RUNNING_NOTIFIED) && word_gen(prev) == gen,
&& word_gen(prev) == gen,
"finalize from invalid word {prev:#x}" "finalize from invalid word {prev:#x}"
); );
} }
@@ -538,7 +534,11 @@ mod loom_tests {
// not a pending notification. // not a pending notification.
assert!(word.try_claim(0)); assert!(word.try_claim(0));
assert_eq!(word.unpark(0, Some(epoch)), Unpark::Noop); 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
View File
@@ -57,16 +57,19 @@ impl Stack {
} }
let base = base as *mut u8; let base = base as *mut u8;
let ret = unsafe { let ret = unsafe { libc::mprotect(base as *mut libc::c_void, guard_size, libc::PROT_NONE) };
libc::mprotect(base as *mut libc::c_void, guard_size, libc::PROT_NONE)
};
if ret != 0 { if ret != 0 {
let err = io::Error::last_os_error(); let err = io::Error::last_os_error();
unsafe { libc::munmap(base as *mut libc::c_void, total_size) }; unsafe { libc::munmap(base as *mut libc::c_void, total_size) };
return Err(err); 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. /// 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 /// with `stack_size` page-rounded by `Stack::new` the result is always
/// page-aligned. Checked math: `retain ≥ stack_size` (notably the default /// page-aligned. Checked math: `retain ≥ stack_size` (notably the default
/// 64 KiB reserve with the 64 KiB RETAIN) and overflow collapse to `None`. /// 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()); debug_assert!(page.is_power_of_two());
let retain = retain.checked_add(page - 1)? & !(page - 1); // page_up(retain) let retain = retain.checked_add(page - 1)? & !(page - 1); // page_up(retain)
let len = stack_size.checked_sub(retain)?; let len = stack_size.checked_sub(retain)?;
+4 -2
View File
@@ -169,7 +169,10 @@ pub struct ChildSpec {
impl ChildSpec { impl ChildSpec {
pub fn new(restart: Restart, start: impl Fn() + Send + Sync + 'static) -> Self { 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
View File
@@ -129,7 +129,9 @@ impl Ord for Entry {
// Earlier deadline first; ties broken by insertion order so the // Earlier deadline first; ties broken by insertion order so the
// ordering is total. `Reason` and `Pid` deliberately don't // ordering is total. `Reason` and `Pid` deliberately don't
// participate. // 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))
} }
} }
+28 -12
View File
@@ -16,13 +16,17 @@
#[cfg(feature = "smarm-trace")] #[cfg(feature = "smarm-trace")]
#[macro_export] #[macro_export]
macro_rules! te { macro_rules! te {
($kind:expr) => { $crate::trace::record($kind) }; ($kind:expr) => {
$crate::trace::record($kind)
};
} }
#[cfg(not(feature = "smarm-trace"))] #[cfg(not(feature = "smarm-trace"))]
#[macro_export] #[macro_export]
macro_rules! te { macro_rules! te {
($kind:expr) => { () }; ($kind:expr) => {
()
};
} }
#[cfg(feature = "smarm-trace")] #[cfg(feature = "smarm-trace")]
@@ -109,8 +113,8 @@ mod inner {
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
pub fn open() { pub fn open() {
let path = std::env::var("SMARM_TRACE_FILE") let path =
.unwrap_or_else(|_| "smarm_trace.json".to_owned()); std::env::var("SMARM_TRACE_FILE").unwrap_or_else(|_| "smarm_trace.json".to_owned());
let (tx, rx) = mpsc::channel::<Msg>(); let (tx, rx) = mpsc::channel::<Msg>();
let start = Instant::now(); let start = Instant::now();
@@ -164,8 +168,11 @@ mod inner {
// which would try to re-acquire inner.shared (already held at many // which would try to re-acquire inner.shared (already held at many
// te!() call sites) -> deadlock. Guard at the very top, before any // te!() call sites) -> deadlock. Guard at the very top, before any
// allocation-capable call. // allocation-capable call.
let was_enabled = crate::preempt::PREEMPTION_ENABLED let was_enabled = crate::preempt::PREEMPTION_ENABLED.with(|e| {
.with(|e| { let v = e.get(); e.set(false); v }); let v = e.get();
e.set(false);
v
});
LOCAL_STATE.with(|cell| { LOCAL_STATE.with(|cell| {
let mut opt = cell.borrow_mut(); let mut opt = cell.borrow_mut();
@@ -197,7 +204,10 @@ mod inner {
fn drain_thread(rx: mpsc::Receiver<Msg>, path: &str) { fn drain_thread(rx: mpsc::Receiver<Msg>, path: &str) {
let f = match std::fs::File::create(path) { let f = match std::fs::File::create(path) {
Ok(f) => f, 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 mut w = std::io::BufWriter::new(f);
let _ = writeln!(w, "{{\"traceEvents\":["); let _ = writeln!(w, "{{\"traceEvents\":[");
@@ -210,7 +220,9 @@ mod inner {
Ok(Msg::Event(r)) => { Ok(Msg::Event(r)) => {
let (name, actor_idx) = chrome_fields(&r.event); let (name, actor_idx) = chrome_fields(&r.event);
let ts_us = r.nanos as f64 / 1000.0; 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; first = false;
let _ = write!(w, let _ = write!(w,
"{{\"ph\":\"i\",\"ts\":{:.3},\"pid\":{},\"tid\":{},\"name\":{:?},\"s\":\"g\"}}", "{{\"ph\":\"i\",\"ts\":{:.3},\"pid\":{},\"tid\":{},\"name\":{:?},\"s\":\"g\"}}",
@@ -234,8 +246,9 @@ mod inner {
fn chrome_fields(ev: &Event) -> (String, u32) { fn chrome_fields(ev: &Event) -> (String, u32) {
match ev { match ev {
Event::Spawn { parent, child } => Event::Spawn { parent, child } => {
(format!("spawn c={}", child.index()), parent.index()), (format!("spawn c={}", child.index()), parent.index())
}
Event::Resume(p) => ("resume".into(), p.index()), Event::Resume(p) => ("resume".into(), p.index()),
Event::Yield(p) => ("yield".into(), p.index()), Event::Yield(p) => ("yield".into(), p.index()),
Event::Park(p) => ("park".into(), p.index()), Event::Park(p) => ("park".into(), p.index()),
@@ -244,9 +257,12 @@ mod inner {
Event::UnparkDeferred(p) => ("unpark_deferred".into(), p.index()), Event::UnparkDeferred(p) => ("unpark_deferred".into(), p.index()),
Event::UnparkFlagConsumed(p) => ("unpark_flag_consumed".into(), p.index()), Event::UnparkFlagConsumed(p) => ("unpark_flag_consumed".into(), p.index()),
Event::Send { sender, receiver } => ( Event::Send { sender, receiver } => (
format!("send rx={}", receiver format!(
"send rx={}",
receiver
.map(|p| p.index().to_string()) .map(|p| p.index().to_string())
.unwrap_or_else(|| "none".into())), .unwrap_or_else(|| "none".into())
),
sender.index(), sender.index(),
), ),
Event::RecvPark(p) => ("recv_park".into(), p.index()), Event::RecvPark(p) => ("recv_park".into(), p.index()),
+24 -6
View File
@@ -49,8 +49,14 @@ fn looping_actor_on_check_is_stopped() {
} }
let _ = h.join(); let _ = h.join();
}); });
assert!(saw_stopped.load(Ordering::SeqCst), "expected DownReason::Stopped"); assert!(
assert!(dropped.load(Ordering::SeqCst), "Drop guard must run during the cancellation unwind"); saw_stopped.load(Ordering::SeqCst),
"expected DownReason::Stopped"
);
assert!(
dropped.load(Ordering::SeqCst),
"Drop guard must run during the cancellation unwind"
);
} }
#[test] #[test]
@@ -79,8 +85,14 @@ fn parked_on_recv_actor_is_stopped() {
} }
let _ = h.join(); let _ = h.join();
}); });
assert!(saw_stopped.load(Ordering::SeqCst), "expected DownReason::Stopped"); assert!(
assert!(dropped.load(Ordering::SeqCst), "Drop guard must run on cancellation of a parked actor"); saw_stopped.load(Ordering::SeqCst),
"expected DownReason::Stopped"
);
assert!(
dropped.load(Ordering::SeqCst),
"Drop guard must run on cancellation of a parked actor"
);
} }
#[test] #[test]
@@ -185,6 +197,12 @@ fn stop_flagged_while_queued_lands_at_first_park() {
.recv_timeout(Duration::from_secs(10)) .recv_timeout(Duration::from_secs(10))
.expect("runtime deadlocked: stop against a QUEUED actor was lost at its first park"); .expect("runtime deadlocked: stop against a QUEUED actor was lost at its first park");
assert!(saw_stopped.load(Ordering::SeqCst), "expected DownReason::Stopped"); assert!(
assert!(dropped.load(Ordering::SeqCst), "Drop guard must run during the cancellation unwind"); saw_stopped.load(Ordering::SeqCst),
"expected DownReason::Stopped"
);
assert!(
dropped.load(Ordering::SeqCst),
"Drop guard must run during the cancellation unwind"
);
} }
+25 -26
View File
@@ -24,7 +24,11 @@ fn progress_point_counts() {
h.join().unwrap(); h.join().unwrap();
let after = smarm::causal::progress_snapshot(); let after = smarm::causal::progress_snapshot();
let delta = |name: &str| { 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 - before
.iter() .iter()
.find(|(n, _)| n == name) .find(|(n, _)| n == name)
@@ -45,21 +49,12 @@ fn site_guard_nesting_restores() {
assert_eq!(smarm::causal::current_site_name(), None); assert_eq!(smarm::causal::current_site_name(), None);
{ {
let _outer = smarm::causal_site!("outer"); let _outer = smarm::causal_site!("outer");
assert_eq!( assert_eq!(smarm::causal::current_site_name().as_deref(), Some("outer"));
smarm::causal::current_site_name().as_deref(),
Some("outer")
);
{ {
let _inner = smarm::causal_site!("inner"); let _inner = smarm::causal_site!("inner");
assert_eq!( assert_eq!(smarm::causal::current_site_name().as_deref(), Some("inner"));
smarm::causal::current_site_name().as_deref(),
Some("inner")
);
} }
assert_eq!( assert_eq!(smarm::causal::current_site_name().as_deref(), Some("outer"));
smarm::causal::current_site_name().as_deref(),
Some("outer")
);
} }
assert_eq!(smarm::causal::current_site_name(), None); assert_eq!(smarm::causal::current_site_name(), None);
}); });
@@ -88,10 +83,7 @@ fn virtual_speedup_ledger() {
let bystander = smarm::spawn(move || { let bystander = smarm::spawn(move || {
while !stop2.load(Ordering::Relaxed) { while !stop2.load(Ordering::Relaxed) {
smarm::check!(); smarm::check!();
out2.store( out2.store(smarm::causal::my_absorbed_delay_cycles(), Ordering::Relaxed);
smarm::causal::my_absorbed_delay_cycles(),
Ordering::Relaxed,
);
} }
}); });
@@ -166,10 +158,7 @@ fn runnable_bystander_pays_delay() {
while !stop_b.load(Ordering::Relaxed) { while !stop_b.load(Ordering::Relaxed) {
iters2.fetch_add(1, Ordering::Relaxed); iters2.fetch_add(1, Ordering::Relaxed);
smarm::check!(); smarm::check!();
absorbed2.store( absorbed2.store(smarm::causal::my_absorbed_delay_cycles(), Ordering::Relaxed);
smarm::causal::my_absorbed_delay_cycles(),
Ordering::Relaxed,
);
} }
}); });
@@ -177,8 +166,7 @@ fn runnable_bystander_pays_delay() {
let i0 = iters.load(Ordering::Relaxed); let i0 = iters.load(Ordering::Relaxed);
let t = std::time::Instant::now(); let t = std::time::Instant::now();
smarm::sleep(Duration::from_millis(150)); smarm::sleep(Duration::from_millis(150));
let rate = let rate = (iters.load(Ordering::Relaxed) - i0) as f64 / t.elapsed().as_secs_f64();
(iters.load(Ordering::Relaxed) - i0) as f64 / t.elapsed().as_secs_f64();
out.store(rate as u64, Ordering::Relaxed); 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. // Raw deadline passed, effective deadline not: nothing fires, entry kept.
assert!(t.pop_due(now + Duration::from_millis(60)).is_empty()); 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 // Past raw + injected (with margin) it must fire. Chase in case a
// parallel test injected more debt meanwhile. // 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 // Just past the raw deadline: the wall entry fires, the virtual one is
// re-queued at its shifted deadline. // re-queued at its shifted deadline.
let due = t.pop_due(now + Duration::from_millis(60)); 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_eq!(due[0].pid, Pid::new(0, 0));
assert!(!t.is_empty(), "virtual sibling must remain queued, shifted"); 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. // Just past the raw deadline: only the wall send pops; run its thunk.
let due = t.pop_due(now + Duration::from_millis(60)); 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 { for e in due {
if let smarm::timer::Reason::Send { fire } = e.reason { if let smarm::timer::Reason::Send { fire } = e.reason {
fire(); fire();
+11 -3
View File
@@ -154,7 +154,10 @@ fn channel_ops_interleaved_with_monitor_churn_multi_thread() {
} }
consumer.join().unwrap(); 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() { fn recv_timeout_zero_duration_is_a_bounded_poll() {
run(|| { run(|| {
let (_tx, rx) = channel::<i64>(); 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,7 +268,8 @@ fn recv_timeout_many_waiters_multi_thread() {
let (tx, rx) = channel::<i64>(); let (tx, rx) = channel::<i64>();
let got = got2.clone(); let got = got2.clone();
let timed_out = timed_out2.clone(); let timed_out = timed_out2.clone();
handles.push(spawn(move || match rx.recv_timeout(Duration::from_millis(100)) { handles.push(spawn(move || {
match rx.recv_timeout(Duration::from_millis(100)) {
Ok(v) => { Ok(v) => {
assert_eq!(v, i); assert_eq!(v, i);
got.fetch_add(1, Ordering::Relaxed); got.fetch_add(1, Ordering::Relaxed);
@@ -271,6 +278,7 @@ fn recv_timeout_many_waiters_multi_thread() {
timed_out.fetch_add(1, Ordering::Relaxed); timed_out.fetch_add(1, Ordering::Relaxed);
} }
Err(e) => panic!("unexpected: {e}"), Err(e) => panic!("unexpected: {e}"),
}
})); }));
if i % 2 == 0 { if i % 2 == 0 {
handles.push(spawn(move || { handles.push(spawn(move || {
+29 -10
View File
@@ -11,9 +11,15 @@ thread_local! {
static LOG: Cell<u64> = const { Cell::new(0) }; static LOG: Cell<u64> = const { Cell::new(0) };
} }
fn log(v: u64) { LOG.with(|c| c.set(c.get() | v)); } fn log(v: u64) {
fn get_log() -> u64 { LOG.with(|c| c.get()) } LOG.with(|c| c.set(c.get() | v));
fn reset_log() { LOG.with(|c| c.set(0)); } }
fn get_log() -> u64 {
LOG.with(|c| c.get())
}
fn reset_log() {
LOG.with(|c| c.set(0));
}
extern "C-unwind" fn actor_simple() { extern "C-unwind" fn actor_simple() {
log(0x1); log(0x1);
@@ -73,7 +79,10 @@ extern "C-unwind" fn actor_reg_check() {
REG_BEFORE.set([s0, s1, s2, s3]).ok(); REG_BEFORE.set([s0, s1, s2, s3]).ok();
switch_to_scheduler(); 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!( core::arch::asm!(
"mov {a0}, r12", "mov {a1}, r13", "mov {a2}, r14", "mov {a3}, r15", "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, 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 stack = Stack::new(64 * 1024, 4096).unwrap();
let sp = init_actor_stack(stack.top(), actor_reg_check); let sp = init_actor_stack(stack.top(), actor_reg_check);
set_actor_sp(sp); set_actor_sp(sp);
unsafe { switch_to_actor(); switch_to_actor(); } unsafe {
assert_eq!(REG_BEFORE.get().copied().unwrap(), REG_AFTER.get().copied().unwrap()); switch_to_actor();
switch_to_actor();
}
assert_eq!(
REG_BEFORE.get().copied().unwrap(),
REG_AFTER.get().copied().unwrap()
);
} }
// Two actors, independent stacks. // 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_a = init_actor_stack(stack_a.top(), actor_a);
let sp_b = init_actor_stack(stack_b.top(), actor_b); 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(); 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(); let sp_b = get_actor_sp();
set_actor_sp(sp_a); unsafe { switch_to_actor() }; set_actor_sp(sp_a);
set_actor_sp(sp_b); unsafe { switch_to_actor() }; unsafe { switch_to_actor() };
set_actor_sp(sp_b);
unsafe { switch_to_actor() };
assert_eq!(A_VAL.with(|c| c.get()), 0xA00D); assert_eq!(A_VAL.with(|c| c.get()), 0xA00D);
assert_eq!(B_VAL.with(|c| c.get()), 0xB00D); assert_eq!(B_VAL.with(|c| c.get()), 0xB00D);
+22 -7
View File
@@ -11,8 +11,8 @@
//! OUTSIDE `run` — an in-actor assertion alone passes vacuously. //! OUTSIDE `run` — an in-actor assertion alone passes vacuously.
use smarm::{ use smarm::{
channel, run, select, select_timeout, spawn, try_select, wait_readable, channel, run, select, select_timeout, spawn, try_select, wait_readable, wait_readable_timeout,
wait_readable_timeout, wait_writable_timeout, yield_now, FdArm, wait_writable_timeout, yield_now, FdArm,
}; };
use std::os::fd::RawFd; use std::os::fd::RawFd;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
@@ -33,7 +33,10 @@ impl Pipe {
let mut fds: [libc::c_int; 2] = [0; 2]; let mut fds: [libc::c_int; 2] = [0; 2];
let r = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC | libc::O_NONBLOCK) }; let r = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC | libc::O_NONBLOCK) };
assert_eq!(r, 0, "pipe2 failed"); 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 (rfd, wfd) = (p.read, p.write);
let start = Instant::now(); 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)); assert!(start.elapsed() >= Duration::from_millis(30));
// Timed-out wait must leave the fd clean; ready path returns true. // Timed-out wait must leave the fd clean; ready path returns true.
assert_eq!(raw_write(wfd, b"d"), 1); 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]; let mut buf = [0u8; 1];
assert_eq!(raw_read(rfd, &mut buf), 1); assert_eq!(raw_read(rfd, &mut buf), 1);
ok2.store(true, Ordering::SeqCst); ok2.store(true, Ordering::SeqCst);
@@ -274,7 +283,10 @@ fn wait_readable_timeout_wakes_on_late_data() {
let p = Pipe::new(); let p = Pipe::new();
let (rfd, wfd) = (p.read, p.write); let (rfd, wfd) = (p.read, p.write);
let h = spawn(move || { 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]; let mut buf = [0u8; 1];
assert_eq!(raw_read(rfd, &mut buf), 1); assert_eq!(raw_read(rfd, &mut buf), 1);
got2.store(buf[0] as u32, Ordering::SeqCst); got2.store(buf[0] as u32, Ordering::SeqCst);
@@ -292,7 +304,10 @@ fn wait_writable_timeout_ready_now_on_empty_pipe() {
run(move || { run(move || {
let p = Pipe::new(); let p = Pipe::new();
// An empty pipe's write end is writable: ready-now path, no park. // 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); ok2.store(true, Ordering::SeqCst);
}); });
assert!(ok.load(Ordering::SeqCst)); assert!(ok.load(Ordering::SeqCst));
+58 -14
View File
@@ -403,7 +403,10 @@ fn worker_pool_down_reaches_handle_down() {
let got = Arc::new(Mutex::new(Vec::new())); let got = Arc::new(Mutex::new(Vec::new()));
let got2 = got.clone(); let got2 = got.clone();
run(move || { 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(); server.cast(PoolCast::SpawnDoomedWorker).unwrap();
let _ = server.call(()).unwrap(); // sync point: cast handled, worker live let _ = server.call(()).unwrap(); // sync point: cast handled, worker live
*got2.lock().unwrap() = server.call(()).unwrap(); *got2.lock().unwrap() = server.call(()).unwrap();
@@ -421,7 +424,10 @@ fn watch_dead_pid_is_noproc_down() {
let h = spawn(|| {}); let h = spawn(|| {});
let dead = h.pid(); let dead = h.pid();
h.join().unwrap(); 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(); server.cast(PoolCast::Watch(dead)).unwrap();
*got2.lock().unwrap() = server.call(()).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 { 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. // 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(); let count = server.call(()).unwrap();
assert_eq!(count, 0, "cancelled timer must not fire"); 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()); assert!(fired.lock().unwrap().is_empty());
} }
@@ -549,11 +564,16 @@ fn tick_every_rearms_repeatedly() {
run(move || { run(move || {
let cw = Arc::new(Mutex::new(None)); let cw = Arc::new(Mutex::new(None));
let server = start(timed(f2, cw)); 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 let _ = server.call(()).unwrap(); // sync: periodic armed
smarm::sleep(Duration::from_millis(130)); // ~6 periods smarm::sleep(Duration::from_millis(130)); // ~6 periods
let count = server.call(()).unwrap(); 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. // Every tick delivered the same payload.
assert!(fired.lock().unwrap().iter().all(|&v| v == 9)); assert!(fired.lock().unwrap().iter().all(|&v| v == 9));
@@ -568,7 +588,9 @@ fn cancel_stops_a_periodic() {
let c2 = cancel_won.clone(); let c2 = cancel_won.clone();
run(move || { run(move || {
let server = start(timed(f2, c2)); 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(); let _ = server.call(()).unwrap();
smarm::sleep(Duration::from_millis(70)); // a few ticks smarm::sleep(Duration::from_millis(70)); // a few ticks
server.cast(TkCast::CancelLast).unwrap(); server.cast(TkCast::CancelLast).unwrap();
@@ -616,11 +638,17 @@ fn idle_fires_repeatedly_on_quiet() {
let idles = Arc::new(Mutex::new(0)); let idles = Arc::new(Mutex::new(0));
let i2 = idles.clone(); let i2 = idles.clone();
run(move || { 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 smarm::sleep(Duration::from_millis(130)); // quiet ⇒ ~5 windows
drop(server); // keep the server alive across the quiet span 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 // 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 before_quiet = Arc::new(Mutex::new(u32::MAX));
let bq = before_quiet.clone(); let bq = before_quiet.clone();
run(move || { 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 // Poke every 25ms (< 60ms window) for ~100ms: each cast resets the
// window before it can elapse. // window before it can elapse.
for _ in 0..4 { for _ in 0..4 {
@@ -643,8 +674,15 @@ fn traffic_resets_the_idle_window() {
smarm::sleep(Duration::from_millis(140)); // now genuinely quiet smarm::sleep(Duration::from_millis(140)); // now genuinely quiet
drop(server); drop(server);
}); });
assert_eq!(*before_quiet.lock().unwrap(), 0, "steady traffic must suppress idle"); assert_eq!(
assert!(*idles.lock().unwrap() >= 1, "idle fires once the inbox falls quiet"); *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 // RFC 015 §4.7 — no armed timer survives loop exit. A server with a live
@@ -658,7 +696,9 @@ fn no_timer_survives_exit() {
let f_read = fired.clone(); let f_read = fired.clone();
run(move || { run(move || {
let server = start(timed(f_server, Arc::new(Mutex::new(None)))); 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 let _ = server.call(()).unwrap(); // sync: periodic armed
smarm::sleep(Duration::from_millis(45)); // a couple of ticks smarm::sleep(Duration::from_millis(45)); // a couple of ticks
let mon = smarm::monitor(server.pid()); let mon = smarm::monitor(server.pid());
@@ -667,6 +707,10 @@ fn no_timer_survives_exit() {
assert!(mon.rx.recv().is_ok()); assert!(mon.rx.recv().is_ok());
let at_exit = f_read.lock().unwrap().len(); let at_exit = f_read.lock().unwrap().len();
smarm::sleep(Duration::from_millis(90)); // would be several more ticks 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"
);
}); });
} }
+88 -12
View File
@@ -100,7 +100,15 @@ fn state_timeout_fires() {
let got = Arc::new(Mutex::new(0u32)); let got = Arc::new(Mutex::new(0u32));
let got2 = got.clone(); let got2 = got.clone();
run(move || { 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 m.send(Ev2::Cast(TCast::Arm)).unwrap(); // -> Armed, arms 5ms state-timeout
smarm::sleep(Duration::from_millis(40)); // let it fire smarm::sleep(Duration::from_millis(40)); // let it fire
*got2.lock().unwrap() = m.call(|r| Ev2::Call(TCall::StFires(r))).unwrap(); *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(); let got2 = got.clone();
run(move || { run(move || {
// Long window so the explicit Disarm beats it comfortably. // 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::Arm)).unwrap(); // -> Armed, arms 50ms state-timeout
m.send(Ev2::Cast(TCast::Disarm)).unwrap(); // -> Idle, auto-resets it m.send(Ev2::Cast(TCast::Disarm)).unwrap(); // -> Idle, auto-resets it
smarm::sleep(Duration::from_millis(80)); // past the original window smarm::sleep(Duration::from_millis(80)); // past the original window
*got2.lock().unwrap() = m.call(|r| Ev2::Call(TCall::StFires(r))).unwrap(); *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 // 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(); let got2 = got.clone();
run(move || { run(move || {
// Armed's own state-timeout is long so it doesn't interfere. // 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::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::Arm)).unwrap(); // -> Armed (ping must survive this)
m.send(Ev2::Cast(TCast::Disarm)).unwrap(); // -> Idle (and this) m.send(Ev2::Cast(TCast::Disarm)).unwrap(); // -> Idle (and this)
smarm::sleep(Duration::from_millis(60)); // let "ping" fire smarm::sleep(Duration::from_millis(60)); // let "ping" fire
*got2.lock().unwrap() = m.call(|r| Ev2::Call(TCall::NamedFires(r))).unwrap(); *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. // 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 got = Arc::new(Mutex::new(99u32));
let got2 = got.clone(); let got2 = got.clone();
run(move || { 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::Ping(30))).unwrap(); // arm "ping" for 30ms
m.send(Ev2::Cast(TCast::CancelPing)).unwrap(); // cancel before it fires m.send(Ev2::Cast(TCast::CancelPing)).unwrap(); // cancel before it fires
smarm::sleep(Duration::from_millis(60)); // past the original window smarm::sleep(Duration::from_millis(60)); // past the original window
*got2.lock().unwrap() = m.call(|r| Ev2::Call(TCall::NamedFires(r))).unwrap(); *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,7 +214,15 @@ fn cast_then_call_roundtrip() {
let got2 = got.clone(); let got2 = got.clone();
run(move || { run(move || {
// Long state-timeout window so it never fires during the test. // 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 }); 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::Arm)).unwrap(); // Idle -> Armed (enter)
m.send(Ev2::Cast(TCast::Disarm)).unwrap(); // Armed -> Idle (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)
@@ -178,7 +230,11 @@ fn cast_then_call_roundtrip() {
// enters = 1 (start) + 4 transitions = 5. // enters = 1 (start) + 4 transitions = 5.
*got2.lock().unwrap() = m.call(|r| Ev2::Call(TCall::Enters(r))).unwrap(); *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 // `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 got = Arc::new(Mutex::new((0u32, 0u32, 0u32)));
let got2 = got.clone(); let got2 = got.clone();
run(move || { 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(); let after_start = m.call(|r| Ev2::Call(TCall::Enters(r))).unwrap();
// A stay (a counter read returns `prev`) must not bump enters. // A stay (a counter read returns `prev`) must not bump enters.
let _ = m.call(|r| Ev2::Call(TCall::StFires(r))).unwrap(); 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 got = Arc::new(Mutex::new(None::<Result<u32, CallError>>));
let got2 = got.clone(); let got2 = got.clone();
run(move || { 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))); let r = m.call(|rep| Ev2::Call(TCall::Boom(rep)));
*got2.lock().unwrap() = Some(r); *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 smarm::sleep(Duration::from_millis(20)); // let the child wake with its reply
*g2.lock().unwrap() = *taken.lock().unwrap(); *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)] #[derive(Clone, Copy, PartialEq, Eq, Debug)]
+40 -9
View File
@@ -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 root itself is on-CPU (it's running this code) and rooted under
// the forest sentinel. // 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.state, ActorState::Running);
assert_eq!(root.supervisor, smarm::Pid::new(u32::MAX, u32::MAX)); 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, // The root is parented at the forest sentinel, so it's a genuine root,
// and the worker it spawned hangs beneath it. // 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.orphaned);
assert!( assert!(
root.children.iter().any(|c| c.info.pid == h.pid()), 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); let t = tree_from(snap);
assert_eq!(t.roots.len(), 2); 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!(!root.orphaned);
assert_eq!(root.children.len(), 1); assert_eq!(root.children.len(), 1);
assert_eq!(root.children[0].info.pid, child); assert_eq!(root.children[0].info.pid, child);
assert!(!root.children[0].orphaned); assert!(!root.children[0].orphaned);
let o = t.roots.iter().find(|n| n.info.pid == orphan).expect("orphan re-rooted"); let o = t
assert!(o.orphaned, "an actor whose parent is absent must be flagged orphaned"); .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()); 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 info = spin_until(h.pid(), |a| a.state == ActorState::Parked);
let s = info.stack; let s = info.stack;
assert_eq!(s.reserve, 64 * 1024, "default reserve"); 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!( assert!(
s.depth_high_water >= 8 * 4096, s.depth_high_water >= 8 * 4096,
"hwm sampled at the deep yield: expected ≥ 32 KiB, got {}", "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; let spike = 768 * 4096;
assert!(spike > SHRINK_THRESHOLD); assert!(spike > SHRINK_THRESHOLD);
let worker = spawn_with( let worker = spawn_with(
SpawnOpts { stack_reserve: Some(8 * 1024 * 1024), ..SpawnOpts::default() }, SpawnOpts {
stack_reserve: Some(8 * 1024 * 1024),
..SpawnOpts::default()
},
move || { move || {
std::hint::black_box(burn_stack_yielding(768)); std::hint::black_box(burn_stack_yielding(768));
for _ in 0..(SHRINK_COOLDOWN + 8) { 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); spin_until(wpid, |a| a.state == ActorState::Parked);
park_tx.send(()).unwrap(); 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; 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!( assert!(
s.parks_since_shrink < SHRINK_COOLDOWN, s.parks_since_shrink < SHRINK_COOLDOWN,
"counter must reset at shrink: {}", "counter must reset at shrink: {}",
+13 -3
View File
@@ -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_2 = v.iter().position(|&x| x == 2).unwrap();
let pos_3 = v.iter().position(|&x| x == 3).unwrap(); let pos_3 = v.iter().position(|&x| x == 3).unwrap();
let pos_4 = v.iter().position(|&x| x == 4).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!(
assert!(pos_3 < pos_4, "B's second step ran after A resumed: {:?}", *v); 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] #[test]
@@ -76,7 +84,9 @@ fn many_concurrent_block_on_io_calls_all_complete() {
cc.fetch_add(n, Ordering::SeqCst); 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); assert_eq!(counter.load(Ordering::SeqCst), 10);
} }
+11 -4
View File
@@ -144,8 +144,7 @@ fn write_sugar_sends_bytes_to_pipe() {
// Pipe is empty + has buffer space, so this returns immediately // Pipe is empty + has buffer space, so this returns immediately
// after wait_writable wakes (which happens fast because the // after wait_writable wakes (which happens fast because the
// kernel marks an empty pipe as immediately writable). // kernel marks an empty pipe as immediately writable).
let n = smarm::scheduler::write(p_writer.write, b"smarm") let n = smarm::scheduler::write(p_writer.write, b"smarm").expect("write failed");
.expect("write failed");
assert_eq!(n, 5); assert_eq!(n, 5);
c.fetch_add(1, Ordering::SeqCst); 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 pos_lit_a = v.iter().position(|&c| c == b'a').unwrap();
let big_b_count = v.iter().filter(|&&c| c == b'B').count(); 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_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. // At least the last B step should be before A resumes.
let last_big_b = v.iter().rposition(|&c| c == b'B').unwrap(); 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
);
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
+8 -2
View File
@@ -57,7 +57,10 @@ fn linked_pair_one_panics_other_is_stopped() {
panic!("boom"); 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"); assert_eq!(dn.pid, b, "Down reported the wrong pid");
if matches!(dn.reason, DownReason::Stopped) { if matches!(dn.reason, DownReason::Stopped) {
s.store(true, Ordering::SeqCst); s.store(true, Ordering::SeqCst);
@@ -152,7 +155,10 @@ fn link_to_dead_pid_stops_a_nontrapping_caller() {
}); });
let b = hb.pid(); let b = hb.pid();
let down_b = monitor(b); 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) { if matches!(dn.reason, DownReason::Stopped) {
s.store(true, Ordering::SeqCst); s.store(true, Ordering::SeqCst);
} }
+27 -6
View File
@@ -67,7 +67,10 @@ fn monitor_already_dead_target_is_noproc() {
// and its generation bumped, so `pid` is now stale. // and its generation bumped, so `pid` is now stale.
h.join().unwrap(); h.join().unwrap();
let down = monitor(pid); 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); assert_eq!(d.pid, pid);
if matches!(d.reason, DownReason::NoProc) { if matches!(d.reason, DownReason::NoProc) {
o.store(true, Ordering::SeqCst); 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] #[test]
@@ -103,8 +110,15 @@ fn demonitor_stops_delivery() {
let h = spawn(|| {}); let h = spawn(|| {});
let pid = h.pid(); let pid = h.pid();
let m = monitor(pid); let m = monitor(pid);
assert_eq!(demonitor(&m), Some(m.id), "live registration should be removed"); assert_eq!(
assert!(m.rx.recv().is_err(), "no Down should arrive after demonitor"); 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(); let _ = h.join();
}); });
} }
@@ -122,7 +136,10 @@ fn demonitor_one_of_many() {
let _ = h.join(); let _ = h.join();
assert!(matches!(ms[0].rx.recv().unwrap().reason, DownReason::Exit)); assert!(matches!(ms[0].rx.recv().unwrap().reason, DownReason::Exit));
assert!(matches!(ms[2].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 m = monitor(pid);
let d = m.rx.recv().expect("Down before close"); let d = m.rx.recv().expect("Down before close");
assert!(matches!(d.reason, DownReason::Exit)); 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(); let _ = h.join();
}); });
} }
+16 -4
View File
@@ -3,9 +3,9 @@
//! needs to be able to park. //! needs to be able to park.
use smarm::{run, spawn, yield_now, LockTimeout, Mutex}; use smarm::{run, spawn, yield_now, LockTimeout, Mutex};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::sync::Mutex as StdMutex; use std::sync::Mutex as StdMutex;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::{Duration, Instant}; 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(); let pos_b_locked = v.iter().position(|s| *s == "B_locked").unwrap();
assert!(pos_a_locked < pos_b_try, "log: {:?}", *v); assert!(pos_a_locked < pos_b_try, "log: {:?}", *v);
assert!(pos_b_try < pos_a_dropped, "B should attempt before A drops: {:?}", *v); assert!(
assert!(pos_a_dropped < pos_b_locked, "B should lock only after A drops: {:?}", *v); 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(); 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
View File
@@ -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 // An index that is not in the slab at all — the verb relays the
// primitive's `None` faithfully. // primitive's `None` faithfully.
let forged = smarm::Pid::new(u32::MAX - 1, 0); let forged = smarm::Pid::new(u32::MAX - 1, 0);
let ObserverReply::ActorInfo(none) = let ObserverReply::ActorInfo(none) = obs.call(ObserverRequest::ActorInfo(forged)).unwrap()
obs.call(ObserverRequest::ActorInfo(forged)).unwrap()
else { else {
panic!("ActorInfo verb must reply ActorInfo"); panic!("ActorInfo verb must reply ActorInfo");
}; };
@@ -113,7 +112,10 @@ fn observer_sees_a_parked_actor_as_parked() {
} }
smarm::yield_now(); 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(); gate_tx.send(()).unwrap();
worker.join().unwrap(); worker.join().unwrap();
+13 -3
View File
@@ -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 // Drain-on-contact: touching g1 detects the death and sweeps the pid
// out of every group (g2 included), not just g1. // out of every group (g2 included), not just g1.
assert!(members("g1").is_empty(), "evicted from the touched group"); 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); assert_eq!(pick("g1"), None);
}); });
} }
@@ -83,7 +86,11 @@ fn live_members_survive_a_peers_death() {
tx_a.send(()).unwrap(); tx_a.send(()).unwrap();
a.join().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())); assert_eq!(pick("svc"), Some(b.pid()));
tx_b.send(()).unwrap(); 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 // monitor() on a gone pid queues a NoProc Down immediately, so the
// membership is reaped the next time the group is touched. // membership is reaped the next time the group is touched.
join("late", pid); 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); assert_eq!(pick("late"), None);
}); });
} }
+10 -2
View File
@@ -41,7 +41,11 @@ fn stop_storm_does_not_poison_runtime() {
} }
c.fetch_add(1, Ordering::SeqCst); 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 /// 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); 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
View File
@@ -43,10 +43,21 @@ fn check_yields_when_timeslice_expired() {
let pos_big_b = v.iter().position(|&c| c == b'B').unwrap(); 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_a = v.iter().position(|&c| c == b'a').unwrap();
let pos_lit_b = v.iter().position(|&c| c == b'b').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!(
assert!(pos_big_b < pos_lit_b, "B's tail ran before A's head: {:?}", *v); pos_big_a < pos_lit_a,
assert!(pos_big_a.max(pos_big_b) < pos_lit_a.min(pos_lit_b), "A's tail ran before B's head: {:?}",
"preemption didn't interleave: {:?}", *v); *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] #[test]
+12 -3
View File
@@ -65,7 +65,10 @@ fn name_held_by_live_actor_is_taken() {
ready_rx.recv().unwrap(); ready_rx.recv().unwrap();
// Root tries to claim a live actor's name for itself -> NameTaken. // Root tries to claim a live actor's name for itself -> NameTaken.
let (tx_b, _rx_b) = channel::<u64>(); 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) send(SVC, 0).unwrap(); // release a (delivers to the holder, a)
a.join().unwrap(); a.join().unwrap();
}); });
@@ -105,7 +108,10 @@ fn dead_holder_is_pruned_and_name_taken_over() {
fn send_errors_unresolved_and_no_channel() { fn send_errors_unresolved_and_no_channel() {
run(|| { run(|| {
// No actor at all. // 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 (ready_tx, ready_rx) = channel::<()>();
let (tx, rx) = channel::<u64>(); let (tx, rx) = channel::<u64>();
@@ -228,7 +234,10 @@ fn send_dyn_delivers_and_reports_wrong_type() {
let p = h.pid(); // a bare Pid<Erased>, as if recovered off a Down let p = h.pid(); // a bare Pid<Erased>, as if recovered off a Down
send_dyn::<u64>(p, 3u64).unwrap(); // right type: delivered send_dyn::<u64>(p, 3u64).unwrap(); // right type: delivered
// Live actor, but it has no channel for &str — the genuinely-fallible case. // Live actor, but it has no channel for &str — the genuinely-fallible case.
assert!(matches!(send_dyn::<&'static str>(p, "nope"), Err(SendError::NoChannel(_)))); assert!(matches!(
send_dyn::<&'static str>(p, "nope"),
Err(SendError::NoChannel(_))
));
done_tx.send(()).unwrap(); done_tx.send(()).unwrap();
h.join().unwrap(); h.join().unwrap();
}); });
+75 -24
View File
@@ -14,10 +14,17 @@
//! - No slot leaks under high spawn/join churn //! - No slot leaks under high spawn/join churn
//! - Panic on one scheduler thread doesn't kill others //! - Panic on one scheduler thread doesn't kill others
use smarm::{channel, runtime::{Config, Runtime}, spawn, yield_now, JoinHandle}; use smarm::{
use std::sync::{atomic::{AtomicBool, AtomicU64, Ordering}, Arc}; channel,
use std::time::Duration; runtime::{Config, Runtime},
spawn, yield_now, JoinHandle,
};
use std::collections::HashSet; use std::collections::HashSet;
use std::sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc,
};
use std::time::Duration;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Helpers // Helpers
@@ -29,7 +36,9 @@ fn rt(n: usize) -> Runtime {
} }
/// Convenient single-threaded runtime (regression guard). /// Convenient single-threaded runtime (regression guard).
fn rt1() -> Runtime { rt(1) } fn rt1() -> Runtime {
rt(1)
}
/// Multi-threaded runtime using all available parallelism. /// Multi-threaded runtime using all available parallelism.
fn rt_par() -> Runtime { fn rt_par() -> Runtime {
@@ -79,7 +88,9 @@ fn config_min_1_max_1_is_single_threaded() {
fn runtime_run_executes_closure() { fn runtime_run_executes_closure() {
let flag = Arc::new(AtomicBool::new(false)); let flag = Arc::new(AtomicBool::new(false));
let f = flag.clone(); 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)); 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 b = Arc::new(AtomicU64::new(0));
let ac = a.clone(); let ac = a.clone();
let bc = b.clone(); let bc = b.clone();
r.run(move || { ac.fetch_add(1, Ordering::SeqCst); }); r.run(move || {
r.run(move || { bc.fetch_add(1, Ordering::SeqCst); }); ac.fetch_add(1, Ordering::SeqCst);
});
r.run(move || {
bc.fetch_add(1, Ordering::SeqCst);
});
assert_eq!(a.load(Ordering::SeqCst), 1); assert_eq!(a.load(Ordering::SeqCst), 1);
assert_eq!(b.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 v = Arc::new(AtomicU64::new(0));
let vc = v.clone(); let vc = v.clone();
rt1().run(move || { rt1().run(move || {
let h = spawn(move || { vc.store(42, Ordering::SeqCst); }); let h = spawn(move || {
vc.store(42, Ordering::SeqCst);
});
h.join().unwrap(); h.join().unwrap();
}); });
assert_eq!(v.load(Ordering::SeqCst), 42); assert_eq!(v.load(Ordering::SeqCst), 42);
@@ -155,7 +172,9 @@ fn exact_1_panic_captured() {
let s = saw_err.clone(); let s = saw_err.clone();
rt1().run(move || { rt1().run(move || {
let h = spawn(|| panic!("oops")); 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)); assert!(saw_err.load(Ordering::SeqCst));
} }
@@ -176,7 +195,9 @@ fn multi_thread_all_actors_complete() {
cc.fetch_add(1, Ordering::SeqCst); 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); assert_eq!(counter.load(Ordering::SeqCst), 100);
} }
@@ -221,7 +242,9 @@ fn multi_thread_many_channels_no_lost_wakeups() {
tx.send(1).unwrap(); 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); 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(); let g = m.lock_timeout(Duration::from_secs(1)).unwrap();
t.store(*g, Ordering::SeqCst); t.store(*g, Ordering::SeqCst);
}); });
@@ -262,7 +287,9 @@ fn multi_thread_join_across_threads() {
rt_par().run(move || { rt_par().run(move || {
let h = spawn(move || { let h = spawn(move || {
// Do some work to make scheduling interesting. // Do some work to make scheduling interesting.
for _ in 0..10 { yield_now(); } for _ in 0..10 {
yield_now();
}
vc.store(1, Ordering::SeqCst); vc.store(1, Ordering::SeqCst);
}); });
h.join().unwrap(); h.join().unwrap();
@@ -279,8 +306,7 @@ fn multi_thread_join_across_threads() {
#[test] #[test]
fn actors_run_on_multiple_os_threads() { fn actors_run_on_multiple_os_threads() {
let thread_ids: Arc<smarm::Mutex<HashSet<u64>>> = let thread_ids: Arc<smarm::Mutex<HashSet<u64>>> = Arc::new(smarm::Mutex::new(HashSet::new()));
Arc::new(smarm::Mutex::new(HashSet::new()));
rt_par().run({ rt_par().run({
let ids = thread_ids.clone(); let ids = thread_ids.clone();
@@ -294,11 +320,15 @@ fn actors_run_on_multiple_os_threads() {
g.insert(tid); 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(); let ids = thread_ids.lock_timeout(Duration::from_secs(1)).unwrap();
// If we have >1 scheduler threads, we expect >1 OS thread IDs. // 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). // run() completes (queue len == 0 at quiescence).
let r = rt_par(); let r = rt_par();
r.run(|| { r.run(|| {
for _ in 0..10 { spawn(|| {}); } for _ in 0..10 {
spawn(|| {});
}
// Don't join — let them drain naturally. // Don't join — let them drain naturally.
}); });
let stats = r.stats(); 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] #[test]
@@ -359,7 +395,9 @@ fn panic_in_actor_does_not_kill_runtime() {
})); }));
} }
let _ = bad.join(); // expect Err 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); assert_eq!(completed.load(Ordering::SeqCst), 10);
} }
@@ -379,7 +417,9 @@ fn no_slot_leak_under_churn() {
rt_par().run(move || { rt_par().run(move || {
for _ in 0..500 { for _ in 0..500 {
let cc = c.clone(); let cc = c.clone();
spawn(move || { cc.fetch_add(1, Ordering::SeqCst); }) spawn(move || {
cc.fetch_add(1, Ordering::SeqCst);
})
.join() .join()
.unwrap(); .unwrap();
} }
@@ -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(); let elapsed = start.elapsed();
assert!( assert!(
@@ -515,7 +559,10 @@ fn runtime_reusable_after_root_panic() {
let ran = Arc::new(AtomicBool::new(false)); let ran = Arc::new(AtomicBool::new(false));
let ran_t = ran.clone(); let ran_t = ran.clone();
r.run(move || ran_t.store(true, Ordering::Relaxed)); 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 { fn burn_stack(frames: usize) -> u64 {
let mut local = [0u8; 4096]; let mut local = [0u8; 4096];
local[0] = frames as u8; 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); std::hint::black_box(&mut local);
below.wrapping_add(local[0] as u64) below.wrapping_add(local[0] as u64)
} }
+7 -4
View File
@@ -14,7 +14,9 @@ use std::sync::Arc;
fn root_actor_runs() { fn root_actor_runs() {
let captured = Arc::new(AtomicI64::new(0)); let captured = Arc::new(AtomicI64::new(0));
let c = captured.clone(); 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); 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 captured = Arc::new(AtomicI64::new(0));
let c = captured.clone(); let c = captured.clone();
run(move || { run(move || {
let h = spawn(move || { c.store(7, Ordering::SeqCst); }); let h = spawn(move || {
c.store(7, Ordering::SeqCst);
});
let res = h.join(); let res = h.join();
assert!(res.is_ok(), "join returned {:?}", res); assert!(res.is_ok(), "join returned {:?}", res);
}); });
@@ -68,8 +72,7 @@ fn yield_now_interleaves_actors() {
#[test] #[test]
fn self_pid_is_stable_within_an_actor() { fn self_pid_is_stable_within_an_actor() {
let pid_cell: Arc<std::sync::Mutex<Option<smarm::Pid>>> = let pid_cell: Arc<std::sync::Mutex<Option<smarm::Pid>>> = Arc::new(std::sync::Mutex::new(None));
Arc::new(std::sync::Mutex::new(None));
let p2 = pid_cell.clone(); let p2 = pid_cell.clone();
run(move || { run(move || {
let h = spawn(move || { let h = spawn(move || {
+14 -3
View File
@@ -19,7 +19,12 @@ fn ready_arm_returns_immediately_without_parking() {
txa.send(42).unwrap(); txa.send(42).unwrap();
let i = select(&[&rxb, &rxa]); let i = select(&[&rxb, &rxa]);
assert_eq!(i, 1); 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); 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 (txa, rxa) = channel::<i64>();
let (_keep_b, rxb) = channel::<i64>(); let (_keep_b, rxb) = channel::<i64>();
txa.send(5).unwrap(); 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)); 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 (_keep_a, rxa) = channel::<i64>();
let (txb, rxb) = channel::<i64>(); let (txb, rxb) = channel::<i64>();
drop(txb); 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()); assert!(rxb.try_recv().is_err());
}); });
} }
+34 -11
View File
@@ -6,9 +6,7 @@
//! big reserve behaviorally takes effect (deep recursion completes). //! big reserve behaviorally takes effect (deep recursion completes).
use smarm::runtime::{Config, DEFAULT_STACK_GUARD, DEFAULT_STACK_RESERVE}; use smarm::runtime::{Config, DEFAULT_STACK_GUARD, DEFAULT_STACK_RESERVE};
use smarm::{ use smarm::{self_pid, spawn, spawn_under_with, spawn_with, GenServerBuilder, SpawnOpts};
self_pid, spawn, spawn_under_with, spawn_with, GenServerBuilder, SpawnOpts,
};
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
@@ -45,7 +43,10 @@ fn spawn_with_overrides_reserve_and_guard() {
#[test] #[test]
fn spawn_with_partial_override_keeps_config_default_for_the_rest() { fn spawn_with_partial_override_keeps_config_default_for_the_rest() {
rt1().run(|| { 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 h = spawn_with(opts, || {
let shape = smarm::introspect::stack_shape(self_pid()).unwrap(); let shape = smarm::introspect::stack_shape(self_pid()).unwrap();
assert_eq!(shape, (1024 * 1024, DEFAULT_STACK_GUARD)); assert_eq!(shape, (1024 * 1024, DEFAULT_STACK_GUARD));
@@ -57,7 +58,10 @@ fn spawn_with_partial_override_keeps_config_default_for_the_rest() {
#[test] #[test]
fn spawn_with_rounds_to_pages() { fn spawn_with_rounds_to_pages() {
rt1().run(|| { 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 h = spawn_with(opts, || {
let (reserve, guard) = smarm::introspect::stack_shape(self_pid()).unwrap(); let (reserve, guard) = smarm::introspect::stack_shape(self_pid()).unwrap();
assert_eq!(reserve % 4096, 0); assert_eq!(reserve % 4096, 0);
@@ -73,7 +77,10 @@ fn spawn_with_rounds_to_pages() {
fn spawn_under_with_takes_opts() { fn spawn_under_with_takes_opts() {
rt1().run(|| { rt1().run(|| {
let me = self_pid(); 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 h = spawn_under_with(me, opts, || {
let (reserve, _) = smarm::introspect::stack_shape(self_pid()).unwrap(); let (reserve, _) = smarm::introspect::stack_shape(self_pid()).unwrap();
assert_eq!(reserve, 128 * 1024); assert_eq!(reserve, 128 * 1024);
@@ -90,7 +97,10 @@ fn spawn_under_with_takes_opts() {
fn custom_stack_never_enters_the_pool() { fn custom_stack_never_enters_the_pool() {
rt1().run(|| { rt1().run(|| {
spawn_with( 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() .join()
@@ -122,7 +132,11 @@ fn default_stack_is_recycled() {
fn burn_stack(frames: usize) -> u64 { fn burn_stack(frames: usize) -> u64 {
let mut local = [0u8; 4096]; let mut local = [0u8; 4096];
local[0] = frames as u8; 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); std::hint::black_box(&mut local);
below.wrapping_add(local[0] as u64) 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 done = Arc::new(AtomicBool::new(false));
let done2 = done.clone(); let done2 = done.clone();
spawn_with( spawn_with(
SpawnOpts { stack_reserve: Some(8 * 1024 * 1024), ..SpawnOpts::default() }, SpawnOpts {
stack_reserve: Some(8 * 1024 * 1024),
..SpawnOpts::default()
},
move || { move || {
std::hint::black_box(burn_stack(256)); std::hint::black_box(burn_stack(256));
done2.store(true, Ordering::SeqCst); done2.store(true, Ordering::SeqCst);
@@ -167,7 +184,10 @@ impl smarm::GenServer for Echo {
fn gen_server_builder_stack_opts() { fn gen_server_builder_stack_opts() {
rt1().run(|| { rt1().run(|| {
let server = GenServerBuilder::new(Echo) 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(); .start();
let (reserve, guard) = server.call(()).unwrap(); let (reserve, guard) = server.call(()).unwrap();
assert_eq!(reserve, 256 * 1024); assert_eq!(reserve, 256 * 1024);
@@ -200,7 +220,10 @@ impl smarm::Machine for Probe {
fn gen_statem_spawn_with_stack_opts() { fn gen_statem_spawn_with_stack_opts() {
rt1().run(|| { rt1().run(|| {
let m = smarm::gen_statem::spawn_with( let m = smarm::gen_statem::spawn_with(
SpawnOpts { stack_reserve: Some(256 * 1024), ..SpawnOpts::default() }, SpawnOpts {
stack_reserve: Some(256 * 1024),
..SpawnOpts::default()
},
Probe, Probe,
); );
let (tx, rx) = smarm::channel::channel(); let (tx, rx) = smarm::channel::channel();
+30 -6
View File
@@ -78,14 +78,18 @@ fn run_as_child_if_requested() {
Ok("wide_guard_top") => { Ok("wide_guard_top") => {
// One byte below the usable region, 64 KiB guard: must fault. // One byte below the usable region, 64 KiB guard: must fault.
let s = Stack::new(64 * 1024, 64 * 1024).unwrap(); 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); std::process::exit(0);
} }
Ok("wide_guard_bottom") => { Ok("wide_guard_bottom") => {
// The very bottom page of a 64 KiB guard: an unprobed C-style // The very bottom page of a 64 KiB guard: an unprobed C-style
// leap over a small guard lands here — must still fault. // leap over a small guard lands here — must still fault.
let s = Stack::new(64 * 1024, 64 * 1024).unwrap(); 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); std::process::exit(0);
} }
Ok("stack_overflow") => { Ok("stack_overflow") => {
@@ -120,7 +124,12 @@ fn guard_page_causes_sigsegv() {
#[cfg(unix)] #[cfg(unix)]
{ {
use std::os::unix::process::ExitStatusExt; 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)] #[cfg(unix)]
{ {
use std::os::unix::process::ExitStatusExt; 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)] #[cfg(unix)]
{ {
use std::os::unix::process::ExitStatusExt; 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)] #[cfg(unix)]
{ {
use std::os::unix::process::ExitStatusExt; 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
View File
@@ -42,9 +42,15 @@ fn run_as_child_if_requested() {
let opts = match mode.as_str() { let opts = match mode.as_str() {
"rust_overflow" | "ffi_tier1" => SpawnOpts::default(), "rust_overflow" | "ffi_tier1" => SpawnOpts::default(),
// Small guard: the canary's 96 KiB displacement clears it. // 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. // 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}"), other => panic!("unknown subtest {other}"),
}; };
let is_rust = mode == "rust_overflow"; 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"), err.contains("overflowed its stack") && err.contains("in the guard region"),
"missing tier-1 diagnostic; stderr:\n{err}" "missing tier-1 diagnostic; stderr:\n{err}"
); );
assert!(err.contains("reserve=65536"), "wrong reserve in message:\n{err}"); assert!(
assert!(err.contains("guard=1048576"), "wrong guard in message:\n{err}"); err.contains("reserve=65536"),
"wrong reserve in message:\n{err}"
);
assert!(
err.contains("guard=1048576"),
"wrong guard in message:\n{err}"
);
} }
#[test] #[test]
+16 -6
View File
@@ -24,7 +24,11 @@ const RESERVE: usize = 4 * 1024 * 1024;
fn burn_stack(frames: usize) -> u64 { fn burn_stack(frames: usize) -> u64 {
let mut local = [0u8; 4096]; let mut local = [0u8; 4096];
local[0] = frames as u8; 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); std::hint::black_box(&mut local);
below.wrapping_add(local[0] as u64) 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 { fn resident_pages(lo: usize, len: usize) -> usize {
let page = 4096; let page = 4096;
let mut vec = vec![0u8; len / page]; let mut vec = vec![0u8; len / page];
let ret = unsafe { let ret = unsafe { libc::mincore(lo as *mut libc::c_void, len, vec.as_mut_ptr()) };
libc::mincore(lo as *mut libc::c_void, len, vec.as_mut_ptr()) assert_eq!(
}; ret,
assert_eq!(ret, 0, "mincore failed: {}", std::io::Error::last_os_error()); 0,
"mincore failed: {}",
std::io::Error::last_os_error()
);
vec.iter().filter(|&&b| b & 1 != 0).count() 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})" [{usable_base:#x}, +{zap_len:#x})"
); );
// Pooled, not munmapped: the mapping must still be there. // 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
View File
@@ -66,7 +66,10 @@ fn spike_then_parks_marks_lazyfree_and_keeps_live_data() {
assert!(spike > SHRINK_THRESHOLD); assert!(spike > SHRINK_THRESHOLD);
let worker = spawn_with( let worker = spawn_with(
SpawnOpts { stack_reserve: Some(8 * 1024 * 1024), ..SpawnOpts::default() }, SpawnOpts {
stack_reserve: Some(8 * 1024 * 1024),
..SpawnOpts::default()
},
move || { move || {
// Live data that must survive the shrink, and an anchor // Live data that must survive the shrink, and an anchor
// address inside the stack for the smaps scan. // 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", "expected ≥ 2 MiB LazyFree in the stack range, got {} bytes",
lazy 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(); worker.join().unwrap();
}); });
} }
@@ -131,7 +138,9 @@ fn shallow_actor_never_shrinks() {
for _ in 0..(SHRINK_COOLDOWN + 8) { for _ in 0..(SHRINK_COOLDOWN + 8) {
park_rx.recv().unwrap(); 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(); let wpid = worker.pid();
+5 -3
View File
@@ -18,8 +18,8 @@
//! registry entry guarantees for every named server. //! registry entry guarantees for every named server.
use smarm::{ use smarm::{
call, channel, init, request_stop, spawn, Config, GenServer, GenServerBuilder, GenServerName, call, channel, init, request_stop, spawn, CallError, Config, GenServer, GenServerBuilder,
CallError, Receiver, RecvTimeoutError, GenServerName, Receiver, RecvTimeoutError,
}; };
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::Duration; use std::time::Duration;
@@ -73,7 +73,9 @@ fn named_server_request_stop_releases_queued_caller_with_server_down() {
let (res_tx, res_rx) = channel::<Result<(), CallError>>(); let (res_tx, res_rx) = channel::<Result<(), CallError>>();
// 1. Start the named server and keep its ref alive. // 1. Start the named server and keep its ref alive.
let server = GenServerBuilder::new(Blocker { gate: Some(gate_rx) }) let server = GenServerBuilder::new(Blocker {
gate: Some(gate_rx),
})
.named(BLOCKER) .named(BLOCKER)
.start() .start()
.expect("name should be free"); .expect("name should be free");
+16 -8
View File
@@ -10,7 +10,11 @@
//! out rather than produce a false pass — run with `cargo test -- --timeout` //! out rather than produce a false pass — run with `cargo test -- --timeout`
//! or under a CI 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::{ use std::sync::{
atomic::{AtomicU64, AtomicUsize, Ordering}, atomic::{AtomicU64, AtomicUsize, Ordering},
Arc, Arc,
@@ -199,7 +203,9 @@ fn thundering_herd_all_wake() {
} }
// Let all receivers park before we send. // Let all receivers park before we send.
for _ in 0..4 { yield_now(); } for _ in 0..4 {
yield_now();
}
// Coordinator blasts all channels. // Coordinator blasts all channels.
handles.push(spawn(move || { handles.push(spawn(move || {
@@ -240,8 +246,7 @@ fn concurrent_spawn_join_churn() {
for _ in 0..PARENTS { for _ in 0..PARENTS {
let tc = t.clone(); let tc = t.clone();
parent_handles.push(spawn(move || { parent_handles.push(spawn(move || {
let mut child_handles: Vec<JoinHandle> = let mut child_handles: Vec<JoinHandle> = Vec::with_capacity(CHILDREN_PER_PARENT);
Vec::with_capacity(CHILDREN_PER_PARENT);
for _ in 0..CHILDREN_PER_PARENT { for _ in 0..CHILDREN_PER_PARENT {
let tcc = tc.clone(); 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. // 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 { for h in handles {
// If child already finished, join must return immediately with Ok. // 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() { fn pid_generation_increments_on_reuse() {
use smarm::self_pid; use smarm::self_pid;
let pids: Arc<smarm::Mutex<Vec<smarm::Pid>>> = let pids: Arc<smarm::Mutex<Vec<smarm::Pid>>> = Arc::new(smarm::Mutex::new(Vec::new()));
Arc::new(smarm::Mutex::new(Vec::new()));
let p = pids.clone(); let p = pids.clone();
rt(1).run(move || { 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. // Any two PIDs that share an index must have different generations.
for i in 0..g.len() { for i in 0..g.len() {
for j in (i + 1)..g.len() { for j in (i + 1)..g.len() {
+10 -2
View File
@@ -51,7 +51,11 @@ fn transient_child_is_restarted_on_panic_then_settles() {
}); });
sup.join().unwrap(); 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] #[test]
@@ -167,7 +171,11 @@ fn one_for_all_restarts_a_normally_exited_sibling() {
sup.join().unwrap(); sup.join().unwrap();
}); });
assert_eq!(a.load(Ordering::SeqCst), 2, "A: crash then clean run"); 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] #[test]
+276
View File
@@ -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:?}");
}
+7 -4
View File
@@ -35,7 +35,10 @@ impl PipePair {
let mut fds: [libc::c_int; 2] = [0; 2]; let mut fds: [libc::c_int; 2] = [0; 2];
let r = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC | libc::O_NONBLOCK) }; let r = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC | libc::O_NONBLOCK) };
assert_eq!(r, 0, "pipe2 failed"); 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); rt.run(body);
let _ = done_tx.send(()); let _ = done_tx.send(());
}); });
done_rx done_rx.recv_timeout(limit).expect(
.recv_timeout(limit) "Runtime::run did not return: idle scheduler thread was never woken at termination",
.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)` /// Permanent-hang variant: sibling blocked in `poll_wake(wake_fd, None)`
+26 -6
View File
@@ -166,14 +166,19 @@ fn timers_only_pop_entries_whose_deadline_has_passed() {
#[test] #[test]
fn timers_mix_sleep_and_wait_timeout_reasons() { fn timers_mix_sleep_and_wait_timeout_reasons() {
let mut t = Timers::new(); 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(); let now = Instant::now();
t.insert_sleep(now + Duration::from_millis(5), Pid::new(0, 0), 1); t.insert_sleep(now + Duration::from_millis(5), Pid::new(0, 0), 1);
t.insert( t.insert(
now + Duration::from_millis(10), now + Duration::from_millis(10),
Pid::new(1, 0), 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)); 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)); 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_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()); run_fire(due.pop().unwrap());
assert!(fired.load(Ordering::SeqCst), "running the thunk delivers"); assert!(fired.load(Ordering::SeqCst), "running the thunk delivers");
assert!(t.is_empty()); assert!(t.is_empty());
@@ -282,7 +290,11 @@ fn cancel_after_fire_returns_false() {
fn cancel_unknown_id_returns_false() { fn cancel_unknown_id_returns_false() {
let mut t = Timers::new(); let mut t = Timers::new();
let now = Instant::now(); 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)); assert!(t.cancel(id));
// Second cancel of the same id: already gone. // Second cancel of the same id: already gone.
assert!(!t.cancel(id)); assert!(!t.cancel(id));
@@ -293,7 +305,11 @@ fn send_timers_interleave_with_sleep_in_deadline_order() {
let mut t = Timers::new(); let mut t = Timers::new();
let now = Instant::now(); let now = Instant::now();
t.insert_sleep(now + Duration::from_millis(30), Pid::new(0, 0), 1); 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); t.insert_sleep(now + Duration::from_millis(20), Pid::new(2, 0), 1);
let due = t.pop_due(now + Duration::from_millis(50)); 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() { fn clear_drops_armed_send_timers() {
let mut t = Timers::new(); let mut t = Timers::new();
let now = Instant::now(); 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(); t.clear();
assert!(t.is_empty()); assert!(t.is_empty());
// The arm record is gone too: cancelling reports nothing to cancel. // The arm record is gone too: cancelling reports nothing to cancel.
+185
View File
@@ -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;
}