Compare commits
42
Commits
3e9c33377c
..
v0.6.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca1c98336e | ||
|
|
95306c7f60 | ||
|
|
1262cc30e3 | ||
|
|
461fe4b768 | ||
|
|
b937f1f50f | ||
|
|
301e3463e3 | ||
|
|
410ba33d82 | ||
|
|
5fd8aecf55 | ||
|
|
7d8b9e0310 | ||
|
|
8225716b11 | ||
|
|
3cb64eefc2 | ||
|
|
0fe052bc7e | ||
|
|
a03a7ca01e | ||
|
|
d4839f1d81 | ||
|
|
2854b560d6 | ||
|
|
7b026cfe56 | ||
|
|
006a3283e7 | ||
|
|
8c764e9169 | ||
|
|
41b9d6d056 | ||
|
|
dd845f22fe | ||
|
|
36a0a9832d | ||
|
|
feda6517e5 | ||
|
|
8625ae4c35 | ||
|
|
d9addeba5e | ||
|
|
d5a3ba1934 | ||
|
|
7eae56a296 | ||
|
|
527f045e17 | ||
|
|
0ee3fe7330 | ||
|
|
9bfeb2c6a2 | ||
|
|
a3be8f0977 | ||
|
|
a2d0b7af18 | ||
|
|
a4647f368a | ||
|
|
fec760a3c0 | ||
|
|
d5b6a8f66f | ||
|
|
efbc254634 | ||
|
|
04dbac1f4b | ||
|
|
d496914d40 | ||
|
|
2668f4018f | ||
|
|
1c90a4ef5e | ||
|
|
f6641cd266 | ||
|
|
0017c5b9a1 | ||
|
|
6c2b7e91cf |
+17
-1
@@ -2,7 +2,23 @@
|
|||||||
# smarm pre-commit gate: clippy the library (src/) with warnings as errors.
|
# smarm pre-commit gate: clippy the library (src/) with warnings as errors.
|
||||||
# unwrap_used / expect_used are denied (Cargo.toml [lints.clippy]): library
|
# unwrap_used / expect_used are denied (Cargo.toml [lints.clippy]): library
|
||||||
# code must not hide a panic behind unwrap/expect. Tests/examples are not gated.
|
# code must not hide a panic behind unwrap/expect. Tests/examples are not gated.
|
||||||
|
#
|
||||||
|
# Toolchain resolution: prefer an installed cargo-clippy; on machines whose
|
||||||
|
# rust comes without the clippy component (e.g. NixOS home-manager), fall
|
||||||
|
# back to an ephemeral nix-shell toolchain. The fallback uses its own target
|
||||||
|
# dir (target/clippy) because the shell's rustc version may differ from the
|
||||||
|
# default toolchain's — mixed-compiler artifacts in one target dir are an
|
||||||
|
# E0514 hard error. MSRV (Cargo.toml rust-version) keeps the older shell
|
||||||
|
# toolchain a legitimate gate.
|
||||||
set -eu
|
set -eu
|
||||||
[ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"
|
[ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"
|
||||||
cd "$(git rev-parse --show-toplevel)"
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
cargo clippy --lib -- -D warnings
|
if cargo clippy --version >/dev/null 2>&1; then
|
||||||
|
cargo clippy --lib -- -D warnings
|
||||||
|
elif command -v nix-shell >/dev/null 2>&1; then
|
||||||
|
nix-shell -p clippy -p cargo -p rustc \
|
||||||
|
--run 'CARGO_TARGET_DIR=target/clippy cargo clippy --lib -- -D warnings'
|
||||||
|
else
|
||||||
|
echo "pre-commit: cargo clippy unavailable and no nix-shell fallback" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ smarm_trace.json
|
|||||||
/bench_results/
|
/bench_results/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
profile.coz
|
||||||
|
|||||||
+20
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "smarm"
|
name = "smarm"
|
||||||
version = "0.4.0"
|
version = "0.6.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.95"
|
rust-version = "1.95"
|
||||||
|
|
||||||
@@ -19,6 +19,10 @@ expect_used = "deny"
|
|||||||
[features]
|
[features]
|
||||||
default = ["rq-mutex"]
|
default = ["rq-mutex"]
|
||||||
smarm-trace = []
|
smarm-trace = []
|
||||||
|
# RFC 007: native causal profiling. Zero cost when off (cf. smarm-trace): the
|
||||||
|
# hook in `maybe_preempt` and the resume-path fast-forward compile away; the
|
||||||
|
# two Slot ledger fields exist regardless and stay 0 (budget_cycles precedent).
|
||||||
|
smarm-causal = []
|
||||||
# RFC 016 Chunk 2: cycle-accurate per-actor time-budget accounting. Off by
|
# RFC 016 Chunk 2: cycle-accurate per-actor time-budget accounting. Off by
|
||||||
# default — it costs two extra RDTSC reads per actor resume on the hot path
|
# default — it costs two extra RDTSC reads per actor resume on the hot path
|
||||||
# (D6). The `ActorInfo.budget_cycles` field exists regardless; it just stays 0
|
# (D6). The `ActorInfo.budget_cycles` field exists regardless; it just stays 0
|
||||||
@@ -35,6 +39,9 @@ rq-mutex = []
|
|||||||
rq-mpmc = []
|
rq-mpmc = []
|
||||||
rq-striped = []
|
rq-striped = []
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
cc = "1"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
|
||||||
@@ -89,3 +96,15 @@ harness = false
|
|||||||
[[example]]
|
[[example]]
|
||||||
name = "observer"
|
name = "observer"
|
||||||
required-features = ["observer"]
|
required-features = ["observer"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "causal_pipeline"
|
||||||
|
required-features = ["smarm-causal"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "causal_attrib_probe"
|
||||||
|
required-features = ["smarm-causal"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "causal_probe"
|
||||||
|
required-features = ["smarm-causal"]
|
||||||
|
|||||||
+67
-26
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fn main() {
|
||||||
|
// RFC 019 §7 test canary (agreed Q3): compiled without stack-clash
|
||||||
|
// protection so its 96 KiB local is a genuine one-displacement guard
|
||||||
|
// jumper; distro-hardened compilers would otherwise probe it page-wise
|
||||||
|
// and defeat the test's purpose.
|
||||||
|
cc::Build::new()
|
||||||
|
.file("canary/canary.c")
|
||||||
|
.flag_if_supported("-fno-stack-clash-protection")
|
||||||
|
.compile("smarm_canary");
|
||||||
|
println!("cargo:rerun-if-changed=canary/canary.c");
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/* RFC 019 §7 FFI canary: an honest unprobed C frame with a 96 KiB local,
|
||||||
|
* touched from its LOW end first — the exact "one sub rsp steps over a small
|
||||||
|
* guard" pattern the RFC's motivating incident hit (a cargo-vendored gz
|
||||||
|
* build; cc-invoked builds do not enable -fstack-clash-protection, and this
|
||||||
|
* file pins that off explicitly so the canary stays a canary even on
|
||||||
|
* hardened-default toolchains). */
|
||||||
|
void smarm_canary_burn(void) {
|
||||||
|
volatile char buf[96 * 1024];
|
||||||
|
buf[0] = 1; /* deepest address first */
|
||||||
|
for (unsigned i = 0; i < sizeof buf; i += 4096) {
|
||||||
|
buf[i] = (char)i;
|
||||||
|
}
|
||||||
|
buf[sizeof buf - 1] = 1;
|
||||||
|
}
|
||||||
@@ -75,7 +75,7 @@ genuine advantage over tokio's task abort model.
|
|||||||
|
|
||||||
### Spawn-heavy workloads (19–70×)
|
### Spawn-heavy workloads (19–70×)
|
||||||
|
|
||||||
Every smarm actor `mmap`s a 64 KiB stack with a guard page. This is
|
Every smarm actor `mmap`s a 64 KiB stack reserve with a 64 KiB PROT_NONE guard below (both per-actor configurable since RFC 019; the reserve is demand-paged). This is
|
||||||
a syscall. Tokio tasks are heap-allocated state machines — no stack,
|
a syscall. Tokio tasks are heap-allocated state machines — no stack,
|
||||||
no syscall, ~100 bytes each. For workloads that spawn thousands of
|
no syscall, ~100 bytes each. For workloads that spawn thousands of
|
||||||
short-lived actors per second, this is a structural disadvantage.
|
short-lived actors per second, this is a structural disadvantage.
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
//! Attribution-efficiency probe (RFC 007 follow-up).
|
||||||
|
//!
|
||||||
|
//! Original hypothesis: the ~4pt impact shortfall on the 24-core
|
||||||
|
//! validation (+29.3/+83.5 vs theoretical +33/+100) is a constant
|
||||||
|
//! attribution efficiency eff ≈ 0.91 from site-exit tail truncation.
|
||||||
|
//! The guard-drop flush closed that leak, yet eff held at ~0.93 —
|
||||||
|
//! RESOLVED (2026-07-13 sweep): the residual is runnable off-CPU time
|
||||||
|
//! inside the site (~4.9 slice-expiry yields/entry x ~5.6µs runqueue
|
||||||
|
//! wait), wall time the ground truth below counts but on-CPU
|
||||||
|
//! attribution correctly skips. The offcpu audit bucket now counts it;
|
||||||
|
//! `eff+offcpu` printed per window should sit at ~1.00 — the
|
||||||
|
//! closed-books check.
|
||||||
|
//!
|
||||||
|
//! Measurement: same pipeline as `causal_pipeline`, but the `reserve`
|
||||||
|
//! actor also measures its raw in-site time directly (rdtsc at guard
|
||||||
|
//! enter/exit) and counts site entries. For each experiment window at
|
||||||
|
//! pct%:
|
||||||
|
//!
|
||||||
|
//! eff = (Δglobal_delay / (pct/100)) / Δin_site_cycles
|
||||||
|
//!
|
||||||
|
//! and the missing time per site entry localizes the leak:
|
||||||
|
//!
|
||||||
|
//! tail_us/entry = (Δin_site − Δglobal_delay/(pct/100)) / Δentries
|
||||||
|
//!
|
||||||
|
//! A constant eff across 25/50% with tail/entry in the tens of µs
|
||||||
|
//! localizes a per-entry mechanism; `eff+offcpu` ≈ 1.00 confirms the
|
||||||
|
//! runnable-gap account and rules out any remaining silent loss.
|
||||||
|
//!
|
||||||
|
//! Run: cargo run --release --example causal_attrib_probe --features smarm-causal
|
||||||
|
|
||||||
|
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
fn rdtsc() -> u64 {
|
||||||
|
// x86_64 only — same clock the ledger uses.
|
||||||
|
unsafe { core::arch::x86_64::_rdtsc() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same fixed-work loop as causal_pipeline (dependent LCG, preemptible).
|
||||||
|
fn work_iters(iters: u64) {
|
||||||
|
let mut acc = 0x2545_f491_4f6c_dd1du64;
|
||||||
|
let mut i = 0u64;
|
||||||
|
while i < iters {
|
||||||
|
let chunk_end = (i + 256).min(iters);
|
||||||
|
while i < chunk_end {
|
||||||
|
acc = acc.wrapping_mul(6364136223846793005).wrapping_add(i);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
std::hint::black_box(acc);
|
||||||
|
smarm::check!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calibrate_iters_per_us() -> u64 {
|
||||||
|
let n = 8_000_000u64;
|
||||||
|
let t = Instant::now();
|
||||||
|
work_iters(n);
|
||||||
|
(n / (t.elapsed().as_micros().max(1) as u64)).max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
static IN_SITE_CYCLES: AtomicU64 = AtomicU64::new(0);
|
||||||
|
static SITE_ENTRIES: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let per_us = calibrate_iters_per_us();
|
||||||
|
println!("calibration: {per_us} work iters/µs");
|
||||||
|
let work_us = move |us: u64| work_iters(us * per_us);
|
||||||
|
|
||||||
|
let cores = std::thread::available_parallelism()
|
||||||
|
.map(|n| n.get())
|
||||||
|
.unwrap_or(1);
|
||||||
|
println!("cores: {cores}");
|
||||||
|
if cores < 4 {
|
||||||
|
println!("probe: SKIPPED (needs the stages in parallel)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
smarm::init(smarm::Config::default()).run(move || {
|
||||||
|
let stop = Arc::new(AtomicBool::new(false));
|
||||||
|
let (tx_ab, rx_ab) = smarm::channel::<u64>();
|
||||||
|
let (tx_bc, rx_bc) = smarm::channel::<u64>();
|
||||||
|
|
||||||
|
let stop_p = stop.clone();
|
||||||
|
let producer = smarm::spawn(move || {
|
||||||
|
let mut i = 0u64;
|
||||||
|
while !stop_p.load(Ordering::Relaxed) {
|
||||||
|
{
|
||||||
|
let _g = smarm::causal_site!("serialize");
|
||||||
|
work_us(200);
|
||||||
|
}
|
||||||
|
if tx_ab.send(i).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reserve: the target — instrumented with ground-truth in-site time.
|
||||||
|
let reserve = smarm::spawn(move || {
|
||||||
|
while let Ok(item) = rx_ab.recv() {
|
||||||
|
{
|
||||||
|
let t0 = rdtsc();
|
||||||
|
let _g = smarm::causal_site!("reserve");
|
||||||
|
work_us(400);
|
||||||
|
// Measured before guard drop: exactly the span the
|
||||||
|
// ledger should be attributing.
|
||||||
|
IN_SITE_CYCLES.fetch_add(rdtsc().saturating_sub(t0), Ordering::Relaxed);
|
||||||
|
SITE_ENTRIES.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
if tx_bc.send(item).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let notify = smarm::spawn(move || {
|
||||||
|
while rx_bc.recv().is_ok() {
|
||||||
|
{
|
||||||
|
let _g = smarm::causal_site!("notify");
|
||||||
|
work_us(50);
|
||||||
|
}
|
||||||
|
smarm::progress!("orders-processed");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let stop_bg = stop.clone();
|
||||||
|
let background = smarm::spawn(move || {
|
||||||
|
while !stop_bg.load(Ordering::Relaxed) {
|
||||||
|
let _g = smarm::causal_site!("background-compaction");
|
||||||
|
work_us(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
smarm::sleep(Duration::from_millis(300));
|
||||||
|
|
||||||
|
let hz = smarm::causal::tsc_hz();
|
||||||
|
println!("tsc_hz: {:.3} GHz", hz / 1e9);
|
||||||
|
|
||||||
|
// Manual windows so ledger/ground-truth snapshots align exactly.
|
||||||
|
for &pct in &[25u32, 50, 50, 25] {
|
||||||
|
let g0 = smarm::causal::global_delay_cycles();
|
||||||
|
let s0 = IN_SITE_CYCLES.load(Ordering::Relaxed);
|
||||||
|
let e0 = SITE_ENTRIES.load(Ordering::Relaxed);
|
||||||
|
let a0 = smarm::causal::ledger_counters();
|
||||||
|
smarm::causal::begin_experiment_for_test("reserve", pct);
|
||||||
|
smarm::sleep(Duration::from_millis(1000));
|
||||||
|
smarm::causal::end_experiment_for_test();
|
||||||
|
let audit = smarm::causal::ledger_counters().delta_since(&a0);
|
||||||
|
let injected = smarm::causal::global_delay_cycles() - g0;
|
||||||
|
let in_site = IN_SITE_CYCLES.load(Ordering::Relaxed) - s0;
|
||||||
|
let entries = SITE_ENTRIES.load(Ordering::Relaxed) - e0;
|
||||||
|
|
||||||
|
let attributed = injected as f64 / (pct as f64 / 100.0);
|
||||||
|
let eff = attributed / in_site as f64;
|
||||||
|
// Books-closure check: add back the runnable off-CPU gaps the
|
||||||
|
// audit counted (delta terms -> raw via /pct) — should be ~1.00.
|
||||||
|
let eff_closed = (injected as f64 + audit.offcpu_in_site_cycles as f64)
|
||||||
|
/ (pct as f64 / 100.0)
|
||||||
|
/ in_site as f64;
|
||||||
|
let missing = in_site as f64 - attributed;
|
||||||
|
let tail_us = if entries > 0 {
|
||||||
|
missing / entries as f64 / hz * 1e6
|
||||||
|
} else {
|
||||||
|
f64::NAN
|
||||||
|
};
|
||||||
|
println!(
|
||||||
|
"pct {pct:>2}% in_site {:>8.1}ms attributed {:>8.1}ms eff {eff:.3} eff+offcpu {eff_closed:.3} entries {entries} missing/entry {tail_us:.1}µs",
|
||||||
|
in_site as f64 / hz * 1e3,
|
||||||
|
attributed / hz * 1e3,
|
||||||
|
);
|
||||||
|
// RFC 007 deficit hunt: name the losses. Drop/discard columns are
|
||||||
|
// in would-be delta terms — divide by pct/100 to compare with the
|
||||||
|
// missing attribution above.
|
||||||
|
let ms = |c: u64| c as f64 / hz * 1e3;
|
||||||
|
println!(
|
||||||
|
" audit: absorbed {:>7.1}ms forgiven {:>6.1}ms drop park {:>5.2}ms/{:<5} yield {:>5.2}ms/{:<5} offcpu {:>6.2}ms/{:<5} discard >max {:>5.2}ms/{:<3} unarmed {}",
|
||||||
|
ms(audit.spin_absorbed_cycles),
|
||||||
|
ms(audit.park_forgiven_cycles),
|
||||||
|
ms(audit.drop_park_cycles),
|
||||||
|
audit.drop_park_n,
|
||||||
|
ms(audit.drop_yield_cycles),
|
||||||
|
audit.drop_yield_n,
|
||||||
|
ms(audit.offcpu_in_site_cycles),
|
||||||
|
audit.offcpu_in_site_n,
|
||||||
|
ms(audit.discard_overmax_cycles),
|
||||||
|
audit.discard_overmax_n,
|
||||||
|
audit.discard_unarmed_n
|
||||||
|
);
|
||||||
|
smarm::sleep(Duration::from_millis(150));
|
||||||
|
}
|
||||||
|
|
||||||
|
stop.store(true, Ordering::Relaxed);
|
||||||
|
producer.join().unwrap();
|
||||||
|
reserve.join().unwrap();
|
||||||
|
notify.join().unwrap();
|
||||||
|
background.join().unwrap();
|
||||||
|
println!("probe: DONE");
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
//! Causal-profiling demo (RFC 007): a pipeline where conventional profiling
|
||||||
|
//! lies and causal profiling doesn't.
|
||||||
|
//!
|
||||||
|
//! producer --(serialize ~200µs/item)--> reserve --(~400µs/item)--> notify
|
||||||
|
//! background: an actor burning CPU constantly, fully off the critical path
|
||||||
|
//!
|
||||||
|
//! `reserve` is the true bottleneck. `serialize` is hot but overlapped with
|
||||||
|
//! `reserve`'s backlog, and `background` is the hottest code in the process
|
||||||
|
//! while contributing nothing to throughput. A cycle profiler ranks them
|
||||||
|
//! background > reserve ≈ 2×serialize; the causal report instead shows
|
||||||
|
//! throughput responding to virtual speedups of `reserve` and (near-)ignoring
|
||||||
|
//! `serialize` and `background`.
|
||||||
|
//!
|
||||||
|
//! Stage cost is fixed *work* (a calibrated arithmetic loop), not fixed wall
|
||||||
|
//! time. This matters: a timed busy-wait absorbs injected causal delay into
|
||||||
|
//! its own budget and finishes on schedule regardless, making every
|
||||||
|
//! experiment read as a no-op (found live on a 24-core run: dead-flat
|
||||||
|
//! deltas). Real workloads are work-shaped, so the demo must be too.
|
||||||
|
//!
|
||||||
|
//! Run:
|
||||||
|
//! cargo run --release --example causal_pipeline --features smarm-causal
|
||||||
|
//!
|
||||||
|
//! Modes (`SMARM_CAUSAL_MODE`), for probing what the guard placement leaves
|
||||||
|
//! out of the measurement (a site speeds up only what it wraps; `recv`/`send`
|
||||||
|
//! on the serialized stage sit outside the canonical guard):
|
||||||
|
//! work (default) — guard wraps only the 400µs of work.
|
||||||
|
//! wide — guard widened over recv + work + send, the whole
|
||||||
|
//! serialized per-item path.
|
||||||
|
//! occupancy — no experiments; times each segment of reserve's loop
|
||||||
|
//! at baseline and reports the unguarded per-item
|
||||||
|
//! overhead δ plus the impact ceiling it implies.
|
||||||
|
//!
|
||||||
|
//! Result (24-core run, 2026-07-13, job f9305cbb): δ measured 0.3µs/item —
|
||||||
|
//! 0.1% of the serialized path — and `wide` does not move the @50% cell
|
||||||
|
//! (+83.5/+86.3 vs work's +81.5/+86.7). This demo's +84-vs-+100 @50%
|
||||||
|
//! shortfall is therefore NOT unguarded stage time; it is controller-side:
|
||||||
|
//! injected delay reaches ~327ms of the ideal 350ms over the 700ms window,
|
||||||
|
//! plus a ~3% real-throughput dip while experiments run. Contrast urus's
|
||||||
|
//! causal_bench, where the same arithmetic identified a real ~70µs/request
|
||||||
|
//! unguarded remainder (recv/reply outside the store guard). Sites measure
|
||||||
|
//! what they wrap — and the occupancy probe tells you which case you're in.
|
||||||
|
//!
|
||||||
|
//! Prints a summary, writes `profile.coz` (Coz plot-compatible), and — given
|
||||||
|
//! enough cores for the pipeline to actually run in parallel — checks the
|
||||||
|
//! expected separation and exits nonzero if it doesn't hold, so a CI box can
|
||||||
|
//! run this as a smoke test.
|
||||||
|
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
/// LCG-mix `iters` times in dependent sequence (unvectorizable, un-elidable),
|
||||||
|
/// staying preemptible — and causal-sampleable/delayable — via `check!()`.
|
||||||
|
fn work_iters(iters: u64) {
|
||||||
|
let mut acc = 0x2545_f491_4f6c_dd1du64;
|
||||||
|
let mut i = 0u64;
|
||||||
|
while i < iters {
|
||||||
|
let chunk_end = (i + 256).min(iters);
|
||||||
|
while i < chunk_end {
|
||||||
|
acc = acc.wrapping_mul(6364136223846793005).wrapping_add(i);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
std::hint::black_box(acc);
|
||||||
|
smarm::check!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Measure how many `work_iters` iterations fit in a microsecond on this
|
||||||
|
/// machine, so stage costs below are meaningful in time while staying
|
||||||
|
/// work-shaped.
|
||||||
|
fn calibrate_iters_per_us() -> u64 {
|
||||||
|
let n = 8_000_000u64;
|
||||||
|
let t = Instant::now();
|
||||||
|
work_iters(n);
|
||||||
|
(n / (t.elapsed().as_micros().max(1) as u64)).max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Guard placement for the `reserve` stage — see module doc.
|
||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
enum Mode {
|
||||||
|
Work,
|
||||||
|
Wide,
|
||||||
|
Occupancy,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mode = match std::env::var("SMARM_CAUSAL_MODE").as_deref() {
|
||||||
|
Err(_) | Ok("") | Ok("work") => Mode::Work,
|
||||||
|
Ok("wide") => Mode::Wide,
|
||||||
|
Ok("occupancy") => Mode::Occupancy,
|
||||||
|
Ok(other) => {
|
||||||
|
eprintln!("unknown SMARM_CAUSAL_MODE {other:?} (work|wide|occupancy)");
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
println!(
|
||||||
|
"mode: {}",
|
||||||
|
match mode {
|
||||||
|
Mode::Work => "work",
|
||||||
|
Mode::Wide => "wide",
|
||||||
|
Mode::Occupancy => "occupancy",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let per_us = calibrate_iters_per_us();
|
||||||
|
println!("calibration: {per_us} work iters/µs");
|
||||||
|
let work_us = move |us: u64| work_iters(us * per_us);
|
||||||
|
|
||||||
|
let mut failures: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
smarm::init(smarm::Config::default()).run(move || {
|
||||||
|
let stop = Arc::new(AtomicBool::new(false));
|
||||||
|
|
||||||
|
let (tx_ab, rx_ab) = smarm::channel::<u64>();
|
||||||
|
let (tx_bc, rx_bc) = smarm::channel::<u64>();
|
||||||
|
|
||||||
|
// Producer: hot serialization, but upstream of the bottleneck.
|
||||||
|
let stop_p = stop.clone();
|
||||||
|
let producer = smarm::spawn(move || {
|
||||||
|
let mut i = 0u64;
|
||||||
|
while !stop_p.load(Ordering::Relaxed) {
|
||||||
|
{
|
||||||
|
let _g = smarm::causal_site!("serialize");
|
||||||
|
work_us(200);
|
||||||
|
}
|
||||||
|
if tx_ab.send(i).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
// tx_ab drops here; downstream drains and exits.
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reserve: the true bottleneck (~400µs of work per item).
|
||||||
|
let reserve = smarm::spawn(move || match mode {
|
||||||
|
Mode::Work => {
|
||||||
|
while let Ok(item) = rx_ab.recv() {
|
||||||
|
{
|
||||||
|
let _g = smarm::causal_site!("reserve");
|
||||||
|
work_us(400);
|
||||||
|
}
|
||||||
|
if tx_bc.send(item).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Whole serialized per-item path under the guard: a virtual
|
||||||
|
// speedup now also compresses recv/send, so the @50% cell should
|
||||||
|
// recover the theoretical 2× that `work` mode's placement caps.
|
||||||
|
Mode::Wide => loop {
|
||||||
|
let _g = smarm::causal_site!("reserve");
|
||||||
|
let Ok(item) = rx_ab.recv() else { break };
|
||||||
|
work_us(400);
|
||||||
|
if tx_bc.send(item).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Time each segment at baseline; the recv+send remainder δ is
|
||||||
|
// the serialized time a `work`-placed guard cannot speed up.
|
||||||
|
Mode::Occupancy => {
|
||||||
|
let (mut recv_ns, mut work_ns, mut send_ns, mut n) = (0u64, 0u64, 0u64, 0u64);
|
||||||
|
loop {
|
||||||
|
let t0 = Instant::now();
|
||||||
|
let Ok(item) = rx_ab.recv() else { break };
|
||||||
|
let t1 = Instant::now();
|
||||||
|
{
|
||||||
|
let _g = smarm::causal_site!("reserve");
|
||||||
|
work_us(400);
|
||||||
|
}
|
||||||
|
let t2 = Instant::now();
|
||||||
|
if tx_bc.send(item).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
recv_ns += (t1 - t0).as_nanos() as u64;
|
||||||
|
work_ns += (t2 - t1).as_nanos() as u64;
|
||||||
|
send_ns += t2.elapsed().as_nanos() as u64;
|
||||||
|
n += 1;
|
||||||
|
}
|
||||||
|
let items = n.max(1) as f64;
|
||||||
|
let (r, w, s) = (
|
||||||
|
recv_ns as f64 / items / 1e3,
|
||||||
|
work_ns as f64 / items / 1e3,
|
||||||
|
send_ns as f64 / items / 1e3,
|
||||||
|
);
|
||||||
|
let delta = r + s;
|
||||||
|
let total = w + delta;
|
||||||
|
println!("occupancy: {n} items; per item recv {r:.1}µs + work(guarded) {w:.1}µs + send {s:.1}µs");
|
||||||
|
println!(
|
||||||
|
"occupancy: unguarded δ = {delta:.1}µs/item = {:.1}% of the serialized path",
|
||||||
|
100.0 * delta / total
|
||||||
|
);
|
||||||
|
for pct in [25u32, 50] {
|
||||||
|
let f = 1.0 - f64::from(pct) / 100.0;
|
||||||
|
println!(
|
||||||
|
"occupancy: predicted reserve impact @{pct}% -> {:+.1}% (ceiling if δ were guarded: {:+.1}%)",
|
||||||
|
100.0 * (total / (f * w + delta) - 1.0),
|
||||||
|
100.0 * (1.0 / f - 1.0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Notify: light tail stage; marks the unit of useful work.
|
||||||
|
let notify = smarm::spawn(move || {
|
||||||
|
while rx_bc.recv().is_ok() {
|
||||||
|
{
|
||||||
|
let _g = smarm::causal_site!("notify");
|
||||||
|
work_us(50);
|
||||||
|
}
|
||||||
|
smarm::progress!("orders-processed");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Background: hottest code in the process, zero throughput relevance.
|
||||||
|
let stop_bg = stop.clone();
|
||||||
|
let background = smarm::spawn(move || {
|
||||||
|
while !stop_bg.load(Ordering::Relaxed) {
|
||||||
|
let _g = smarm::causal_site!("background-compaction");
|
||||||
|
work_us(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Warm up so queues reach steady state before measuring.
|
||||||
|
smarm::sleep(Duration::from_millis(300));
|
||||||
|
|
||||||
|
if mode == Mode::Occupancy {
|
||||||
|
// No experiments: hold steady state for a window, then drain and
|
||||||
|
// let the reserve actor print its segment report.
|
||||||
|
smarm::sleep(Duration::from_millis(1500));
|
||||||
|
stop.store(true, Ordering::Relaxed);
|
||||||
|
producer.join().unwrap();
|
||||||
|
reserve.join().unwrap();
|
||||||
|
notify.join().unwrap();
|
||||||
|
background.join().unwrap();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let results = smarm::causal::run_experiments(&smarm::causal::ExperimentPlan {
|
||||||
|
speedups_pct: vec![0, 25, 50],
|
||||||
|
experiment: Duration::from_millis(700),
|
||||||
|
cooldown: Duration::from_millis(150),
|
||||||
|
});
|
||||||
|
|
||||||
|
stop.store(true, Ordering::Relaxed);
|
||||||
|
producer.join().unwrap();
|
||||||
|
reserve.join().unwrap();
|
||||||
|
notify.join().unwrap();
|
||||||
|
background.join().unwrap();
|
||||||
|
|
||||||
|
print!("{}", smarm::causal::render_summary(&results));
|
||||||
|
// RFC 007 deficit hunt: SMARM_CAUSAL_AUDIT=1 appends the per-cell
|
||||||
|
// ledger audit (injected/absorbed/forgiven + drop and discard
|
||||||
|
// buckets) without touching the pinned summary format.
|
||||||
|
if std::env::var_os("SMARM_CAUSAL_AUDIT").is_some() {
|
||||||
|
print!("{}", smarm::causal::render_ledger_audit(&results));
|
||||||
|
}
|
||||||
|
let coz = smarm::causal::render_coz(&results);
|
||||||
|
match std::fs::write("profile.coz", coz) {
|
||||||
|
Ok(()) => println!("\nwrote profile.coz"),
|
||||||
|
Err(e) => eprintln!("\nfailed to write profile.coz: {e}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verdict. The separation only exists when the four pipeline actors
|
||||||
|
// actually run in parallel; on a small box, report and skip.
|
||||||
|
let cores = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
|
||||||
|
if cores < 4 {
|
||||||
|
println!("verdict: SKIPPED ({cores} cores; separation needs the stages in parallel)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let impact = |site: &str| {
|
||||||
|
smarm::causal::impact_pct(&results, site, 25, "orders-processed")
|
||||||
|
};
|
||||||
|
let mut expect = |site: &str, ok: &dyn Fn(f64) -> bool, want: &str| match impact(site) {
|
||||||
|
Some(p) => {
|
||||||
|
let verdict = if ok(p) { "ok" } else { "FAIL" };
|
||||||
|
println!("verdict: {site} @25% -> {p:+.1}% (want {want}) {verdict}");
|
||||||
|
if !ok(p) {
|
||||||
|
failures.push(format!("{site}: {p:+.1}% (want {want})"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
println!("verdict: {site} @25% -> missing cell FAIL");
|
||||||
|
failures.push(format!("{site}: missing cell"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect("reserve", &|p| p > 15.0, "> +15%");
|
||||||
|
expect("serialize", &|p| p < 10.0, "< +10%");
|
||||||
|
expect("background-compaction", &|p| p < 10.0, "< +10%");
|
||||||
|
|
||||||
|
if failures.is_empty() {
|
||||||
|
println!("verdict: PASS — causal separation holds");
|
||||||
|
} else {
|
||||||
|
println!("verdict: FAIL — {}", failures.join("; "));
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
//! Diagnostic probe for RFC 007 on a target box. Measures, in order:
|
||||||
|
//! 1. TSC frequency against `Instant` (the crate assumes 3 GHz).
|
||||||
|
//! 2. TSC sanity under actor migration: distribution of wall time actually
|
||||||
|
//! spent in `burn_us(400)` across many runs — a bimodal/short tail means
|
||||||
|
//! cross-core TSC offsets are cutting burns short.
|
||||||
|
//! 3. Pipeline stage rates with no experiment running (who is the real
|
||||||
|
//! bottleneck?).
|
||||||
|
//! 4. The same rates during a 50% experiment on `background-compaction`
|
||||||
|
//! (a correct implementation must slow every stage; an off-critical-path
|
||||||
|
//! target must reduce end-to-end throughput proportionally).
|
||||||
|
//!
|
||||||
|
//! Run: cargo run --release --example causal_probe --features smarm-causal
|
||||||
|
|
||||||
|
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
fn burn_us(us: u64) {
|
||||||
|
let cycles = us * 3_000;
|
||||||
|
let start = smarm::preempt::rdtsc();
|
||||||
|
while smarm::preempt::rdtsc().saturating_sub(start) < cycles {
|
||||||
|
smarm::check!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// 1. TSC calibration (plain OS thread, before the runtime starts).
|
||||||
|
let c0 = smarm::preempt::rdtsc();
|
||||||
|
let t0 = Instant::now();
|
||||||
|
std::thread::sleep(Duration::from_millis(200));
|
||||||
|
let hz = (smarm::preempt::rdtsc() - c0) as f64 / t0.elapsed().as_secs_f64();
|
||||||
|
println!("tsc_hz: {:.3e} (crate assumes 3.0e9)", hz);
|
||||||
|
|
||||||
|
smarm::init(smarm::Config::default()).run(move || {
|
||||||
|
// 2. burn_us(400) wall-time distribution inside a migrating actor.
|
||||||
|
let h = smarm::spawn(|| {
|
||||||
|
let mut samples: Vec<u64> = (0..500)
|
||||||
|
.map(|_| {
|
||||||
|
let t = Instant::now();
|
||||||
|
burn_us(400);
|
||||||
|
t.elapsed().as_micros() as u64
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
samples.sort_unstable();
|
||||||
|
println!(
|
||||||
|
"burn_us(400) wall us: min {} p10 {} p50 {} p90 {} max {}",
|
||||||
|
samples[0], samples[50], samples[250], samples[450], samples[499]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
h.join().unwrap();
|
||||||
|
|
||||||
|
// 3+4. Pipeline with per-stage counters.
|
||||||
|
let stop = Arc::new(AtomicBool::new(false));
|
||||||
|
let produced = Arc::new(AtomicU64::new(0));
|
||||||
|
let reserved = Arc::new(AtomicU64::new(0));
|
||||||
|
let notified = Arc::new(AtomicU64::new(0));
|
||||||
|
|
||||||
|
let (tx_ab, rx_ab) = smarm::channel::<u64>();
|
||||||
|
let (tx_bc, rx_bc) = smarm::channel::<u64>();
|
||||||
|
|
||||||
|
let stop_p = stop.clone();
|
||||||
|
let produced2 = produced.clone();
|
||||||
|
let producer = smarm::spawn(move || {
|
||||||
|
let mut i = 0u64;
|
||||||
|
while !stop_p.load(Ordering::Relaxed) {
|
||||||
|
{
|
||||||
|
let _g = smarm::causal_site!("serialize");
|
||||||
|
burn_us(200);
|
||||||
|
}
|
||||||
|
if tx_ab.send(i).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
produced2.fetch_add(1, Ordering::Relaxed);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let reserved2 = reserved.clone();
|
||||||
|
let reserve = smarm::spawn(move || {
|
||||||
|
while let Ok(item) = rx_ab.recv() {
|
||||||
|
{
|
||||||
|
let _g = smarm::causal_site!("reserve");
|
||||||
|
burn_us(400);
|
||||||
|
}
|
||||||
|
reserved2.fetch_add(1, Ordering::Relaxed);
|
||||||
|
if tx_bc.send(item).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let notified2 = notified.clone();
|
||||||
|
let notify = smarm::spawn(move || {
|
||||||
|
while rx_bc.recv().is_ok() {
|
||||||
|
{
|
||||||
|
let _g = smarm::causal_site!("notify");
|
||||||
|
burn_us(50);
|
||||||
|
}
|
||||||
|
notified2.fetch_add(1, Ordering::Relaxed);
|
||||||
|
smarm::progress!("orders-processed");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let stop_bg = stop.clone();
|
||||||
|
let background = smarm::spawn(move || {
|
||||||
|
while !stop_bg.load(Ordering::Relaxed) {
|
||||||
|
let _g = smarm::causal_site!("background-compaction");
|
||||||
|
burn_us(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
smarm::sleep(Duration::from_millis(300));
|
||||||
|
|
||||||
|
let window = |label: &str| {
|
||||||
|
let (p0, r0, n0) = (
|
||||||
|
produced.load(Ordering::Relaxed),
|
||||||
|
reserved.load(Ordering::Relaxed),
|
||||||
|
notified.load(Ordering::Relaxed),
|
||||||
|
);
|
||||||
|
let d0 = smarm::causal::global_delay_cycles();
|
||||||
|
let t = Instant::now();
|
||||||
|
smarm::sleep(Duration::from_millis(700));
|
||||||
|
let secs = t.elapsed().as_secs_f64();
|
||||||
|
println!(
|
||||||
|
"{label}: produced {:.0}/s reserved {:.0}/s notified {:.0}/s injected {:.0}ms(assumed-3GHz)",
|
||||||
|
(produced.load(Ordering::Relaxed) - p0) as f64 / secs,
|
||||||
|
(reserved.load(Ordering::Relaxed) - r0) as f64 / secs,
|
||||||
|
(notified.load(Ordering::Relaxed) - n0) as f64 / secs,
|
||||||
|
(smarm::causal::global_delay_cycles() - d0) as f64 / 3.0e9 * 1e3,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
window("no-experiment ");
|
||||||
|
smarm::causal::begin_experiment_for_test("background-compaction", 50);
|
||||||
|
window("bg-comp @ 50% ");
|
||||||
|
smarm::causal::end_experiment_for_test();
|
||||||
|
window("post-experiment");
|
||||||
|
|
||||||
|
stop.store(true, Ordering::Relaxed);
|
||||||
|
producer.join().unwrap();
|
||||||
|
reserve.join().unwrap();
|
||||||
|
notify.join().unwrap();
|
||||||
|
background.join().unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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))) => {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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(",")
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+969
@@ -0,0 +1,969 @@
|
|||||||
|
//! Native causal profiling (RFC 007). Enabled by `--features smarm-causal`;
|
||||||
|
//! zero cost without it (same discipline as `smarm-trace`).
|
||||||
|
//!
|
||||||
|
//! The Coz algorithm, transposed onto actors: to estimate what speeding up
|
||||||
|
//! code site S by p% would do to throughput, we instead *slow everything
|
||||||
|
//! else down* by p% of the time spent in S, and watch the progress-point
|
||||||
|
//! rates respond. Where Coz must inject real `usleep`s into OS threads from
|
||||||
|
//! the outside, smarm owns every clock that matters:
|
||||||
|
//!
|
||||||
|
//! - Sampling and delay injection happen at `maybe_preempt`'s amortised
|
||||||
|
//! cadence — an existing, safe hook (never inside a prep-to-park region).
|
||||||
|
//! - Injected delay is subtracted from the actor's timeslice
|
||||||
|
//! (`preempt::extend_timeslice`), so experiments don't perturb scheduling.
|
||||||
|
//! - Delay bookkeeping is *actor*-granular: each `Slot` carries an absorbed-
|
||||||
|
//! delay ledger, compared against a global ledger. Parked actors absorb
|
||||||
|
//! accrued delay for free on resume (Coz's blocked-thread rule) — waiting
|
||||||
|
//! is never penalised.
|
||||||
|
//!
|
||||||
|
//! v1 scope (per RFC discussion): explicit scoped sites (`causal_site!`)
|
||||||
|
//! rather than PC sampling (jar Q1 stays open); throughput progress points
|
||||||
|
//! only; timer-heap deadlines are *not* shifted (documented gap — long
|
||||||
|
//! experiments can make real-time timeouts fire early in virtual terms);
|
||||||
|
//! multi-scheduler coherence is best-effort via global atomics.
|
||||||
|
//!
|
||||||
|
//! Usage:
|
||||||
|
//! ```ignore
|
||||||
|
//! let _g = smarm::causal_site!("inventory-reserve"); // in suspect code
|
||||||
|
//! smarm::progress!("orders-processed"); // per unit of work
|
||||||
|
//! let results = smarm::causal::run_experiments(&Default::default());
|
||||||
|
//! print!("{}", smarm::causal::render_summary(&results));
|
||||||
|
//! std::fs::write("profile.coz", smarm::causal::render_coz(&results))?;
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
#[cfg(feature = "smarm-causal")]
|
||||||
|
mod inner {
|
||||||
|
use crate::preempt;
|
||||||
|
use std::cell::Cell;
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Global state
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Active experiment, packed `(site_id << 32) | speedup_pct`. 0 = idle.
|
||||||
|
/// A single word so the hot path reads one atomic; experiments are global
|
||||||
|
/// across scheduler threads (jar Q7, v1: plain Relaxed atomics).
|
||||||
|
static EXPERIMENT: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
|
/// Monotone experiment-window counter, bumped by every `begin()`. Lets
|
||||||
|
/// the offcpu-gap stash (RFC 007) tell apart two windows with an
|
||||||
|
/// identical site+pct word — live in the attrib probe's 50,50 schedule
|
||||||
|
/// — so a gap straddling `end()`/`begin()` never counts a cooldown.
|
||||||
|
static EXPERIMENT_EPOCH: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
|
/// Global virtual-delay ledger, in TSC cycles: the total delay every
|
||||||
|
/// actor *should* have experienced since startup. Grows while a sample
|
||||||
|
/// lands in the experiment's target site; each actor's `Slot` ledger
|
||||||
|
/// chases it by spin-absorbing at preemption checks.
|
||||||
|
static GLOBAL_DELAY: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
|
/// Registered site names; site id = index + 1 (0 = "no site").
|
||||||
|
static SITES: OnceLock<Mutex<Vec<&'static str>>> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Registered progress points (leaked for `'static`, like trace's drain
|
||||||
|
/// state — the set is small and lives for the process).
|
||||||
|
static PROGRESS: OnceLock<Mutex<Vec<&'static ProgressPoint>>> = OnceLock::new();
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
/// TSC at this thread's previous causal check, the sample "period"
|
||||||
|
/// denominator. Re-armed on every actor resume so scheduler time and
|
||||||
|
/// a previous actor's tail never count toward a sample. 0 = unarmed.
|
||||||
|
static LAST_SAMPLE_TSC: Cell<u64> = const { Cell::new(0) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Guard against TSC weirdness (migration between unsynced sockets,
|
||||||
|
/// virtualisation steps): a single sample interval larger than this is
|
||||||
|
/// discarded rather than believed. ~33ms at 3 GHz — far beyond any real
|
||||||
|
/// gap between preemption checks inside a slice.
|
||||||
|
const MAX_SAMPLE_CYCLES: u64 = 100_000_000;
|
||||||
|
|
||||||
|
/// Cap on delay spun in one visit, so one check can never wedge an actor
|
||||||
|
/// for a human-visible pause; the remainder is absorbed on later visits.
|
||||||
|
/// ~3ms at 3 GHz.
|
||||||
|
const MAX_SPIN_PER_VISIT: u64 = 10_000_000;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Ledger audit (RFC 007 deficit hunt): where injected delay is born,
|
||||||
|
// paid, and forgiven — and where would-be attribution is silently lost
|
||||||
|
// (deschedule tails, clamp discards). Monotone Relaxed totals, read via
|
||||||
|
// `ledger_counters()`; `run_experiments` windows them into
|
||||||
|
// `ExperimentResult`. Measure-only: nothing here changes injection or
|
||||||
|
// absorption behaviour.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Cycles bystanders actually spun to pay down the global ledger.
|
||||||
|
static SPIN_ABSORBED_CYCLES: AtomicU64 = AtomicU64::new(0);
|
||||||
|
/// Cycles waived at wake after a real park (the blocked-thread rule).
|
||||||
|
static PARK_FORGIVEN_CYCLES: AtomicU64 = AtomicU64::new(0);
|
||||||
|
/// Would-be attribution lost when the target-site actor parks mid-site.
|
||||||
|
static DROP_PARK_CYCLES: AtomicU64 = AtomicU64::new(0);
|
||||||
|
static DROP_PARK_N: AtomicU64 = AtomicU64::new(0);
|
||||||
|
/// Same loss at yields (explicit, slice-expiry, or a park that requeued).
|
||||||
|
static DROP_YIELD_CYCLES: AtomicU64 = AtomicU64::new(0);
|
||||||
|
static DROP_YIELD_N: AtomicU64 = AtomicU64::new(0);
|
||||||
|
/// Samples discarded by the TSC-weirdness clamp, in would-be delta terms.
|
||||||
|
static DISCARD_OVERMAX_CYCLES: AtomicU64 = AtomicU64::new(0);
|
||||||
|
static DISCARD_OVERMAX_N: AtomicU64 = AtomicU64::new(0);
|
||||||
|
/// In-site samples dropped because the thread's clock was unarmed.
|
||||||
|
static DISCARD_UNARMED_N: AtomicU64 = AtomicU64::new(0);
|
||||||
|
/// Would-be attribution over runnable off-CPU gaps inside the target
|
||||||
|
/// site (yield-descheduled -> resumed within the same window). Not a
|
||||||
|
/// loss: on-CPU-only attribution is the Coz model — queue-wait is not
|
||||||
|
/// shrunk by speeding the site's code — but counted so the audit books
|
||||||
|
/// close against wall in-site time (the located @50 "deficit").
|
||||||
|
static OFFCPU_IN_SITE_CYCLES: AtomicU64 = AtomicU64::new(0);
|
||||||
|
static OFFCPU_IN_SITE_N: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
|
fn sites() -> &'static Mutex<Vec<&'static str>> {
|
||||||
|
SITES.get_or_init(|| Mutex::new(Vec::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn progress_points() -> &'static Mutex<Vec<&'static ProgressPoint>> {
|
||||||
|
PROGRESS.get_or_init(|| Mutex::new(Vec::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recover from lock poisoning: all these registries hold plain data that
|
||||||
|
/// is valid at every instruction boundary, so a panicked registrant can't
|
||||||
|
/// leave them torn.
|
||||||
|
fn lock_unpoisoned<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
||||||
|
match m.lock() {
|
||||||
|
Ok(g) => g,
|
||||||
|
Err(poisoned) => poisoned.into_inner(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Sites
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Register (or look up) a causal site by name; returns its nonzero id.
|
||||||
|
/// Called once per `causal_site!` expansion via a `OnceLock`, so the
|
||||||
|
/// mutex is off every hot path.
|
||||||
|
pub fn site_id(name: &'static str) -> u32 {
|
||||||
|
let mut v = lock_unpoisoned(sites());
|
||||||
|
if let Some(pos) = v.iter().position(|n| *n == name) {
|
||||||
|
return (pos + 1) as u32;
|
||||||
|
}
|
||||||
|
v.push(name);
|
||||||
|
v.len() as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
fn site_name(id: u32) -> Option<String> {
|
||||||
|
if id == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let v = lock_unpoisoned(sites());
|
||||||
|
v.get((id - 1) as usize).map(|s| (*s).to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RAII marker: while alive, the current *actor* (not thread — the id
|
||||||
|
/// lives in its `Slot` and survives preemption/migration) is "inside"
|
||||||
|
/// the site. Nesting restores the outer site on drop. Inert outside an
|
||||||
|
/// actor (scheduler/OS-thread stacks).
|
||||||
|
pub struct SiteGuard {
|
||||||
|
/// Slot of the actor that entered, null if entered outside an actor.
|
||||||
|
/// Valid for the guard's whole life: the guard lives on the actor's
|
||||||
|
/// stack, and a slot is never reclaimed while its actor is alive —
|
||||||
|
/// the same argument as `preempt::check_cancelled`.
|
||||||
|
slot: *const crate::runtime::Slot,
|
||||||
|
prev: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SiteGuard {
|
||||||
|
/// Enter `site` for the on-CPU actor.
|
||||||
|
pub fn enter(site: u32) -> Self {
|
||||||
|
let slot = preempt::current_slot_ptr();
|
||||||
|
if slot.is_null() {
|
||||||
|
return SiteGuard { slot, prev: 0 };
|
||||||
|
}
|
||||||
|
// SAFETY: non-null ⇒ points at the on-CPU actor's slot; see the
|
||||||
|
// field docs for the lifetime argument.
|
||||||
|
let prev = unsafe { (*slot).causal_site() };
|
||||||
|
unsafe { (*slot).set_causal_site(site) };
|
||||||
|
site_transition(slot, prev, site);
|
||||||
|
SiteGuard { slot, prev }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for SiteGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if !self.slot.is_null() {
|
||||||
|
// SAFETY (both): as in `enter` — the actor (and thus its
|
||||||
|
// slot) is alive for as long as this guard is on its stack.
|
||||||
|
let site = unsafe { (*self.slot).causal_site() };
|
||||||
|
unsafe { (*self.slot).set_causal_site(self.prev) };
|
||||||
|
site_transition(self.slot, site, self.prev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Name of the site the on-CPU actor is currently inside, if any.
|
||||||
|
/// (Introspection/testing; not a hot path.)
|
||||||
|
pub fn current_site_name() -> Option<String> {
|
||||||
|
let slot = preempt::current_slot_ptr();
|
||||||
|
if slot.is_null() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// SAFETY: on-CPU actor's slot, valid for the whole resume.
|
||||||
|
site_name(unsafe { (*slot).causal_site() })
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Progress points
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// A named throughput counter. One per distinct name; `progress!` call
|
||||||
|
/// sites sharing a name share the counter.
|
||||||
|
pub struct ProgressPoint {
|
||||||
|
name: &'static str,
|
||||||
|
count: AtomicU64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProgressPoint {
|
||||||
|
/// The hot path: one Relaxed RMW. (Contended across actors by design
|
||||||
|
/// — a progress point is a global rate meter.)
|
||||||
|
#[inline]
|
||||||
|
pub fn bump(&self) {
|
||||||
|
self.count.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register (or look up) a progress point. Called once per `progress!`
|
||||||
|
/// expansion via a `OnceLock`; the mutex is off the hot path.
|
||||||
|
pub fn register_progress(name: &'static str) -> &'static ProgressPoint {
|
||||||
|
let mut v = lock_unpoisoned(progress_points());
|
||||||
|
if let Some(p) = v.iter().find(|p| p.name == name) {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
let p: &'static ProgressPoint = Box::leak(Box::new(ProgressPoint {
|
||||||
|
name,
|
||||||
|
count: AtomicU64::new(0),
|
||||||
|
}));
|
||||||
|
v.push(p);
|
||||||
|
p
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Snapshot of all progress points as `(name, count)`.
|
||||||
|
pub fn progress_snapshot() -> Vec<(String, u64)> {
|
||||||
|
lock_unpoisoned(progress_points())
|
||||||
|
.iter()
|
||||||
|
.map(|p| (p.name.to_string(), p.count.load(Ordering::Relaxed)))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// The hot hook: sample + absorb
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Called from `maybe_preempt` at the amortised timeslice-check cadence,
|
||||||
|
/// under the `PREEMPTION_ENABLED` gate (so never in a prep-to-park or
|
||||||
|
/// no-preempt region — spinning here is as safe as yielding is).
|
||||||
|
///
|
||||||
|
/// One Relaxed load and out when no experiment is running.
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn check() {
|
||||||
|
let exp = EXPERIMENT.load(Ordering::Relaxed);
|
||||||
|
if exp == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cold_check(exp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The experiment-active path, kept out of the inlined fast path.
|
||||||
|
#[cold]
|
||||||
|
fn cold_check(exp: u64) {
|
||||||
|
let slot = preempt::current_slot_ptr();
|
||||||
|
if slot.is_null() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let now = preempt::rdtsc();
|
||||||
|
let last = LAST_SAMPLE_TSC.with(|c| c.replace(now));
|
||||||
|
let target_site = (exp >> 32) as u32;
|
||||||
|
let pct = exp & 0xffff_ffff;
|
||||||
|
|
||||||
|
// SAFETY (both derefs below): non-null ⇒ the on-CPU actor's slot,
|
||||||
|
// never reclaimed while the actor runs — see `check_cancelled`.
|
||||||
|
let my_site = unsafe { (*slot).causal_site() };
|
||||||
|
|
||||||
|
if my_site == target_site && pct > 0 {
|
||||||
|
// A sample landed in the target site: everyone else must fall
|
||||||
|
// behind by pct% of the sampled interval. Grow the global ledger
|
||||||
|
// and credit ourselves the same amount — the credited gap *is*
|
||||||
|
// the virtual speedup.
|
||||||
|
if last == 0 {
|
||||||
|
// Unarmed clock: no interval to attribute — count the loss.
|
||||||
|
DISCARD_UNARMED_N.fetch_add(1, Ordering::Relaxed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// SAFETY: `slot` is the on-CPU actor's slot (checked non-null
|
||||||
|
// above); see `check_cancelled` for the lifetime argument.
|
||||||
|
unsafe { attribute(slot, now.saturating_sub(last), pct) };
|
||||||
|
} else {
|
||||||
|
// Not the winner: chase the global ledger by spinning off the
|
||||||
|
// difference, then push the slice start forward so injected
|
||||||
|
// delay never counts as compute (the clock correction that Coz
|
||||||
|
// cannot do from outside).
|
||||||
|
let global = GLOBAL_DELAY.load(Ordering::Relaxed);
|
||||||
|
let mine = unsafe { (*slot).causal_delay() };
|
||||||
|
if mine >= global {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let spin = (global - mine).min(MAX_SPIN_PER_VISIT);
|
||||||
|
let start = preempt::rdtsc();
|
||||||
|
while preempt::rdtsc().saturating_sub(start) < spin {
|
||||||
|
core::hint::spin_loop();
|
||||||
|
}
|
||||||
|
SPIN_ABSORBED_CYCLES.fetch_add(spin, Ordering::Relaxed);
|
||||||
|
unsafe { (*slot).set_causal_delay(mine.wrapping_add(spin)) };
|
||||||
|
preempt::extend_timeslice(spin);
|
||||||
|
// The spin is not part of the next sample interval either.
|
||||||
|
LAST_SAMPLE_TSC.with(|c| c.set(preempt::rdtsc()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attribute one target-site sample of `interval` cycles at `pct`%:
|
||||||
|
/// grow the global ledger and credit the sampling actor's own ledger by
|
||||||
|
/// the same amount — the credited gap *is* the virtual speedup. Shared
|
||||||
|
/// by the cold check and the guard-boundary flush. Applies the same
|
||||||
|
/// clamps as sampling always has: zero intervals and clock hiccups are
|
||||||
|
/// discarded, not the run.
|
||||||
|
///
|
||||||
|
/// SAFETY: `slot` must point at the on-CPU actor's slot (the
|
||||||
|
/// `check_cancelled` lifetime argument).
|
||||||
|
unsafe fn attribute(slot: *const crate::runtime::Slot, interval: u64, pct: u64) {
|
||||||
|
if interval == 0 {
|
||||||
|
return; // now == last: nothing to attribute, nothing lost
|
||||||
|
}
|
||||||
|
if interval > MAX_SAMPLE_CYCLES {
|
||||||
|
// TSC-weirdness clamp: the sample is discarded, not the run.
|
||||||
|
// Count the loss in would-be delta terms so the audit's columns
|
||||||
|
// compare directly against `injected_cycles`.
|
||||||
|
DISCARD_OVERMAX_N.fetch_add(1, Ordering::Relaxed);
|
||||||
|
DISCARD_OVERMAX_CYCLES.fetch_add(interval.saturating_mul(pct) / 100, Ordering::Relaxed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let delta = interval.saturating_mul(pct) / 100;
|
||||||
|
GLOBAL_DELAY.fetch_add(delta, Ordering::Relaxed);
|
||||||
|
let mine = (*slot).causal_delay();
|
||||||
|
(*slot).set_causal_delay(mine.wrapping_add(delta));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Site-boundary hook, called by `SiteGuard` enter/drop when the
|
||||||
|
/// actor's current site changes from `old` to `new`. Sample-only —
|
||||||
|
/// never spins — so it is safe anywhere, including no-preempt regions
|
||||||
|
/// where `check()` cannot run.
|
||||||
|
///
|
||||||
|
/// - Leaving the experiment's target site: flush the pending interval.
|
||||||
|
/// Cold checks only sample when they happen to fire in-site, so the
|
||||||
|
/// tail between the last check and the guard drop was otherwise
|
||||||
|
/// discarded on every site entry — measured live at ~22-29µs/entry,
|
||||||
|
/// ~6-7% of all target time (eff 0.93), which under-reported every
|
||||||
|
/// impact (+83.5% where theory says +100%).
|
||||||
|
/// - Entering the target site: re-arm the sample clock, so time spent
|
||||||
|
/// *before* the site can never be attributed to it by the first
|
||||||
|
/// in-site check (the symmetric over-attribution).
|
||||||
|
#[inline]
|
||||||
|
fn site_transition(slot: *const crate::runtime::Slot, old: u32, new: u32) {
|
||||||
|
let exp = EXPERIMENT.load(Ordering::Relaxed);
|
||||||
|
if exp == 0 || old == new {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let target = (exp >> 32) as u32;
|
||||||
|
let pct = exp & 0xffff_ffff;
|
||||||
|
if old == target && new != target {
|
||||||
|
let now = preempt::rdtsc();
|
||||||
|
let last = LAST_SAMPLE_TSC.with(|c| c.replace(now));
|
||||||
|
if pct > 0 {
|
||||||
|
if last != 0 {
|
||||||
|
// SAFETY: forwarded from the guard, which holds the on-CPU
|
||||||
|
// actor's slot for its whole life (see `SiteGuard::slot`).
|
||||||
|
unsafe { attribute(slot, now.saturating_sub(last), pct) };
|
||||||
|
} else {
|
||||||
|
DISCARD_UNARMED_N.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if new == target && old != target {
|
||||||
|
LAST_SAMPLE_TSC.with(|c| c.set(preempt::rdtsc()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resume-path hook (scheduler thread, actor off-CPU). Two duties:
|
||||||
|
///
|
||||||
|
/// - If the last deschedule was a *real park*, time blocked absorbs any
|
||||||
|
/// delay accrued meanwhile for free — Coz's blocked-thread rule, which
|
||||||
|
/// keeps experiments from punishing actors for waiting. An actor that
|
||||||
|
/// merely yielded (slice expiry) was runnable the whole time and keeps
|
||||||
|
/// its debt: it must pay by spinning at its next check. Forgiving on
|
||||||
|
/// every resume would make any yield-cadence actor delay-immune and
|
||||||
|
/// experiments inert (found live on a 24-core run: nothing slowed).
|
||||||
|
/// - If the deschedule was a *yield* in the live experiment's target
|
||||||
|
/// site, count the off-CPU gap it opened into the offcpu audit bucket
|
||||||
|
/// (RFC 007: the located @50 deficit — runnable queue-wait is wall
|
||||||
|
/// time in-site that on-CPU attribution correctly skips). Same-window
|
||||||
|
/// only, enforced by the experiment epoch; measure-only.
|
||||||
|
/// - Arm this thread's sample clock so the first interval of the resume
|
||||||
|
/// excludes scheduler time.
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn on_resume(slot: &crate::runtime::Slot) {
|
||||||
|
let (desched_tsc, desched_epoch) = slot.take_causal_desched();
|
||||||
|
if desched_tsc != 0 && desched_epoch == EXPERIMENT_EPOCH.load(Ordering::Relaxed) {
|
||||||
|
// Same epoch ⇒ no `begin()` since the stash; a nonzero word ⇒
|
||||||
|
// no `end()` either — the gap closed inside its own window.
|
||||||
|
let exp = EXPERIMENT.load(Ordering::Relaxed);
|
||||||
|
if exp != 0 {
|
||||||
|
let pct = exp & 0xffff_ffff;
|
||||||
|
let gap = preempt::rdtsc()
|
||||||
|
.saturating_sub(desched_tsc)
|
||||||
|
.min(MAX_SAMPLE_CYCLES);
|
||||||
|
OFFCPU_IN_SITE_CYCLES.fetch_add(gap.saturating_mul(pct) / 100, Ordering::Relaxed);
|
||||||
|
OFFCPU_IN_SITE_N.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if slot.take_causal_parked() {
|
||||||
|
let global = GLOBAL_DELAY.load(Ordering::Relaxed);
|
||||||
|
let mine = slot.causal_delay();
|
||||||
|
if mine < global {
|
||||||
|
PARK_FORGIVEN_CYCLES.fetch_add(global - mine, Ordering::Relaxed);
|
||||||
|
slot.set_causal_delay(global);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LAST_SAMPLE_TSC.with(|c| c.set(preempt::rdtsc()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deschedule-path hook (scheduler side, same OS thread the actor just
|
||||||
|
/// ran on). If an experiment is live and the departing actor sits in the
|
||||||
|
/// target site, the sample tail `[last sample -> now]` is about to be
|
||||||
|
/// lost: nothing flushes it here, and `on_resume` re-arms the clock
|
||||||
|
/// before the actor runs again. Measure-only (RFC 007 deficit hunt) —
|
||||||
|
/// tally the would-be attribution into the park/yield drop buckets and
|
||||||
|
/// leave behaviour untouched. The interval is capped at
|
||||||
|
/// MAX_SAMPLE_CYCLES: past that the flush would have discarded it anyway
|
||||||
|
/// (counted separately). `now` includes the few hundred ns of scheduler
|
||||||
|
/// bookkeeping since the actor actually stopped — an acceptable
|
||||||
|
/// overcount for a diagnostic.
|
||||||
|
///
|
||||||
|
/// Slice-expiry yields sample at the same checkpoint that deschedules
|
||||||
|
/// them, so their tails are ~zero by construction; a fat yield bucket
|
||||||
|
/// therefore points at explicit `yield_now` calls or requeued parks.
|
||||||
|
///
|
||||||
|
/// Yields additionally stash the deschedule instant on the slot so
|
||||||
|
/// `on_resume` can count the runnable off-CPU gap (offcpu bucket).
|
||||||
|
pub(crate) fn on_deschedule(slot: &crate::runtime::Slot, real_park: bool) {
|
||||||
|
let exp = EXPERIMENT.load(Ordering::Relaxed);
|
||||||
|
if exp == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let target = (exp >> 32) as u32;
|
||||||
|
let pct = exp & 0xffff_ffff;
|
||||||
|
if pct == 0 || slot.causal_site() != target {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let now = preempt::rdtsc();
|
||||||
|
if !real_park {
|
||||||
|
// Runnable gap opens here; `on_resume` closes and counts it
|
||||||
|
// (offcpu bucket). Parks are excluded: blocked time is already
|
||||||
|
// represented by forgiveness, and blocked wall time is not
|
||||||
|
// queue-wait.
|
||||||
|
slot.set_causal_desched(now, EXPERIMENT_EPOCH.load(Ordering::Relaxed));
|
||||||
|
}
|
||||||
|
let last = LAST_SAMPLE_TSC.with(|c| c.get());
|
||||||
|
if last == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let interval = now.saturating_sub(last).min(MAX_SAMPLE_CYCLES);
|
||||||
|
let would_be = interval.saturating_mul(pct) / 100;
|
||||||
|
if real_park {
|
||||||
|
DROP_PARK_N.fetch_add(1, Ordering::Relaxed);
|
||||||
|
DROP_PARK_CYCLES.fetch_add(would_be, Ordering::Relaxed);
|
||||||
|
} else {
|
||||||
|
DROP_YIELD_N.fetch_add(1, Ordering::Relaxed);
|
||||||
|
DROP_YIELD_CYCLES.fetch_add(would_be, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Experiments
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn begin(site: u32, pct: u32) {
|
||||||
|
EXPERIMENT_EPOCH.fetch_add(1, Ordering::Relaxed);
|
||||||
|
EXPERIMENT.store(((site as u64) << 32) | pct as u64, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end() {
|
||||||
|
EXPERIMENT.store(0, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Total virtual delay injected so far, in TSC cycles.
|
||||||
|
pub fn global_delay_cycles() -> u64 {
|
||||||
|
GLOBAL_DELAY.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cumulative ledger-audit totals since startup (RFC 007 deficit hunt).
|
||||||
|
/// All monotone; window a span by snapshotting before/after and taking
|
||||||
|
/// `delta_since`. Cycle fields are in would-be-injected delta terms so
|
||||||
|
/// they compare directly against `injected_cycles`.
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
pub struct LedgerCounters {
|
||||||
|
pub spin_absorbed_cycles: u64,
|
||||||
|
pub park_forgiven_cycles: u64,
|
||||||
|
pub drop_park_cycles: u64,
|
||||||
|
pub drop_park_n: u64,
|
||||||
|
pub drop_yield_cycles: u64,
|
||||||
|
pub drop_yield_n: u64,
|
||||||
|
pub discard_overmax_cycles: u64,
|
||||||
|
pub discard_overmax_n: u64,
|
||||||
|
pub discard_unarmed_n: u64,
|
||||||
|
pub offcpu_in_site_cycles: u64,
|
||||||
|
pub offcpu_in_site_n: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LedgerCounters {
|
||||||
|
/// Field-wise difference against an earlier snapshot.
|
||||||
|
pub fn delta_since(&self, before: &LedgerCounters) -> LedgerCounters {
|
||||||
|
LedgerCounters {
|
||||||
|
spin_absorbed_cycles: self
|
||||||
|
.spin_absorbed_cycles
|
||||||
|
.saturating_sub(before.spin_absorbed_cycles),
|
||||||
|
park_forgiven_cycles: self
|
||||||
|
.park_forgiven_cycles
|
||||||
|
.saturating_sub(before.park_forgiven_cycles),
|
||||||
|
drop_park_cycles: self
|
||||||
|
.drop_park_cycles
|
||||||
|
.saturating_sub(before.drop_park_cycles),
|
||||||
|
drop_park_n: self.drop_park_n.saturating_sub(before.drop_park_n),
|
||||||
|
drop_yield_cycles: self
|
||||||
|
.drop_yield_cycles
|
||||||
|
.saturating_sub(before.drop_yield_cycles),
|
||||||
|
drop_yield_n: self.drop_yield_n.saturating_sub(before.drop_yield_n),
|
||||||
|
discard_overmax_cycles: self
|
||||||
|
.discard_overmax_cycles
|
||||||
|
.saturating_sub(before.discard_overmax_cycles),
|
||||||
|
discard_overmax_n: self
|
||||||
|
.discard_overmax_n
|
||||||
|
.saturating_sub(before.discard_overmax_n),
|
||||||
|
discard_unarmed_n: self
|
||||||
|
.discard_unarmed_n
|
||||||
|
.saturating_sub(before.discard_unarmed_n),
|
||||||
|
offcpu_in_site_cycles: self
|
||||||
|
.offcpu_in_site_cycles
|
||||||
|
.saturating_sub(before.offcpu_in_site_cycles),
|
||||||
|
offcpu_in_site_n: self
|
||||||
|
.offcpu_in_site_n
|
||||||
|
.saturating_sub(before.offcpu_in_site_n),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Snapshot the cumulative audit counters.
|
||||||
|
pub fn ledger_counters() -> LedgerCounters {
|
||||||
|
LedgerCounters {
|
||||||
|
spin_absorbed_cycles: SPIN_ABSORBED_CYCLES.load(Ordering::Relaxed),
|
||||||
|
park_forgiven_cycles: PARK_FORGIVEN_CYCLES.load(Ordering::Relaxed),
|
||||||
|
drop_park_cycles: DROP_PARK_CYCLES.load(Ordering::Relaxed),
|
||||||
|
drop_park_n: DROP_PARK_N.load(Ordering::Relaxed),
|
||||||
|
drop_yield_cycles: DROP_YIELD_CYCLES.load(Ordering::Relaxed),
|
||||||
|
drop_yield_n: DROP_YIELD_N.load(Ordering::Relaxed),
|
||||||
|
discard_overmax_cycles: DISCARD_OVERMAX_CYCLES.load(Ordering::Relaxed),
|
||||||
|
discard_overmax_n: DISCARD_OVERMAX_N.load(Ordering::Relaxed),
|
||||||
|
discard_unarmed_n: DISCARD_UNARMED_N.load(Ordering::Relaxed),
|
||||||
|
offcpu_in_site_cycles: OFFCPU_IN_SITE_CYCLES.load(Ordering::Relaxed),
|
||||||
|
offcpu_in_site_n: OFFCPU_IN_SITE_N.load(Ordering::Relaxed),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Absorbed-delay ledger of the on-CPU actor (testing/introspection).
|
||||||
|
pub fn my_absorbed_delay_cycles() -> u64 {
|
||||||
|
let slot = preempt::current_slot_ptr();
|
||||||
|
if slot.is_null() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// SAFETY: on-CPU actor's slot; see `check_cancelled`.
|
||||||
|
unsafe { (*slot).causal_delay() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test support: start an experiment targeting `site_name` at `pct`%
|
||||||
|
/// virtual speedup. Registers the site if needed.
|
||||||
|
pub fn begin_experiment_for_test(name: &'static str, pct: u32) {
|
||||||
|
begin(site_id(name), pct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test support: stop the running experiment.
|
||||||
|
pub fn end_experiment_for_test() {
|
||||||
|
end();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test support: grow the global delay ledger directly, as if target-site
|
||||||
|
/// samples had attributed `cycles` — deterministic driver for the timer
|
||||||
|
/// virtual-time tests. Calibrates the TSC eagerly so conversion later
|
||||||
|
/// never stalls a scheduler loop.
|
||||||
|
pub fn inject_delay_cycles_for_test(cycles: u64) {
|
||||||
|
let _ = tsc_hz();
|
||||||
|
GLOBAL_DELAY.fetch_add(cycles, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert ledger cycles to wall time at the measured TSC rate.
|
||||||
|
pub fn cycles_to_duration(cycles: u64) -> Duration {
|
||||||
|
Duration::from_secs_f64(cycles as f64 / tsc_hz())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Controller parameters: which speedups to try per site, and the
|
||||||
|
/// experiment/cooldown windows.
|
||||||
|
pub struct ExperimentPlan {
|
||||||
|
pub speedups_pct: Vec<u32>,
|
||||||
|
pub experiment: Duration,
|
||||||
|
pub cooldown: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ExperimentPlan {
|
||||||
|
fn default() -> Self {
|
||||||
|
ExperimentPlan {
|
||||||
|
speedups_pct: vec![0, 25, 50],
|
||||||
|
experiment: Duration::from_millis(500),
|
||||||
|
cooldown: Duration::from_millis(100),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One completed experiment cell.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ExperimentResult {
|
||||||
|
pub site: String,
|
||||||
|
pub speedup_pct: u32,
|
||||||
|
pub duration: Duration,
|
||||||
|
/// Progress-point deltas over the window, `(name, count)`.
|
||||||
|
pub deltas: Vec<(String, u64)>,
|
||||||
|
/// Virtual delay injected during the window (cycles).
|
||||||
|
pub injected_cycles: u64,
|
||||||
|
// Ledger-audit deltas over the window (RFC 007 deficit hunt); see
|
||||||
|
// `LedgerCounters` for field semantics. `spin_absorbed_cycles > 0`
|
||||||
|
// in a 0% cell means the window paid debt left over from an earlier
|
||||||
|
// one — the baseline-contamination signature.
|
||||||
|
pub spin_absorbed_cycles: u64,
|
||||||
|
pub park_forgiven_cycles: u64,
|
||||||
|
pub drop_park_cycles: u64,
|
||||||
|
pub drop_park_n: u64,
|
||||||
|
pub drop_yield_cycles: u64,
|
||||||
|
pub drop_yield_n: u64,
|
||||||
|
pub discard_overmax_cycles: u64,
|
||||||
|
pub discard_overmax_n: u64,
|
||||||
|
pub discard_unarmed_n: u64,
|
||||||
|
pub offcpu_in_site_cycles: u64,
|
||||||
|
pub offcpu_in_site_n: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the plan synchronously on the calling (OS) thread: for every
|
||||||
|
/// registered site × speedup, run one experiment window and record
|
||||||
|
/// progress-point deltas, with a cooldown between cells. Sites and
|
||||||
|
/// progress points must already be registered (the workload has to be
|
||||||
|
/// running); the caller owns workload start/stop.
|
||||||
|
///
|
||||||
|
/// v1 controller: exhaustive sweep, fixed windows, no adaptive site
|
||||||
|
/// selection or confidence stopping (jar Q5).
|
||||||
|
///
|
||||||
|
/// Callable from a plain OS thread *or* from inside an actor: sleeping
|
||||||
|
/// parks the green thread when we're on one (so no scheduler thread is
|
||||||
|
/// blocked), and falls back to `thread::sleep` otherwise.
|
||||||
|
pub fn run_experiments(plan: &ExperimentPlan) -> Vec<ExperimentResult> {
|
||||||
|
fn controller_sleep(d: Duration) {
|
||||||
|
if preempt::current_slot_ptr().is_null() {
|
||||||
|
std::thread::sleep(d);
|
||||||
|
} else {
|
||||||
|
// Wall-anchored: the controller's window/cooldown sleeps
|
||||||
|
// *define* the experiment's wall length; letting them chase
|
||||||
|
// the delay it is itself injecting would stretch every window
|
||||||
|
// (observed ~2x at 50% speedup). Deltas are rate-normalized
|
||||||
|
// either way — this fixes cost, not bias.
|
||||||
|
crate::scheduler::sleep_wall(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Calibrate before any window so report rendering never has to sleep.
|
||||||
|
let _ = tsc_hz();
|
||||||
|
let site_list: Vec<(u32, String)> = {
|
||||||
|
let v = lock_unpoisoned(sites());
|
||||||
|
v.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, n)| ((i + 1) as u32, (*n).to_string()))
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for (sid, sname) in &site_list {
|
||||||
|
for &pct in &plan.speedups_pct {
|
||||||
|
let before = progress_snapshot();
|
||||||
|
let injected_before = global_delay_cycles();
|
||||||
|
let audit_before = ledger_counters();
|
||||||
|
let t0 = Instant::now();
|
||||||
|
begin(*sid, pct);
|
||||||
|
controller_sleep(plan.experiment);
|
||||||
|
end();
|
||||||
|
// Snapshot immediately: injection and spin freeze at `end()`
|
||||||
|
// (checks gate on the experiment word), but forgiveness does
|
||||||
|
// not — a later snapshot would leak cooldown wakes into the
|
||||||
|
// window.
|
||||||
|
let audit = ledger_counters().delta_since(&audit_before);
|
||||||
|
let elapsed = t0.elapsed();
|
||||||
|
let after = progress_snapshot();
|
||||||
|
let deltas = after
|
||||||
|
.iter()
|
||||||
|
.map(|(n, c)| {
|
||||||
|
let b = before
|
||||||
|
.iter()
|
||||||
|
.find(|(bn, _)| bn == n)
|
||||||
|
.map(|(_, bc)| *bc)
|
||||||
|
.unwrap_or(0);
|
||||||
|
(n.clone(), c.saturating_sub(b))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
out.push(ExperimentResult {
|
||||||
|
site: sname.clone(),
|
||||||
|
speedup_pct: pct,
|
||||||
|
duration: elapsed,
|
||||||
|
deltas,
|
||||||
|
injected_cycles: global_delay_cycles() - injected_before,
|
||||||
|
spin_absorbed_cycles: audit.spin_absorbed_cycles,
|
||||||
|
park_forgiven_cycles: audit.park_forgiven_cycles,
|
||||||
|
drop_park_cycles: audit.drop_park_cycles,
|
||||||
|
drop_park_n: audit.drop_park_n,
|
||||||
|
drop_yield_cycles: audit.drop_yield_cycles,
|
||||||
|
drop_yield_n: audit.drop_yield_n,
|
||||||
|
discard_overmax_cycles: audit.discard_overmax_cycles,
|
||||||
|
discard_overmax_n: audit.discard_overmax_n,
|
||||||
|
discard_unarmed_n: audit.discard_unarmed_n,
|
||||||
|
offcpu_in_site_cycles: audit.offcpu_in_site_cycles,
|
||||||
|
offcpu_in_site_n: audit.offcpu_in_site_n,
|
||||||
|
});
|
||||||
|
controller_sleep(plan.cooldown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Reports
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Measured TSC frequency (Hz), calibrated once. The crate-wide 3 GHz
|
||||||
|
/// constant is fine for the *relative* timeslice check, but report
|
||||||
|
/// normalisation divides wall time by injected time, so a 20% Hz error
|
||||||
|
/// skews every impact number — measured live: a 3.7 GHz box inflated all
|
||||||
|
/// baselines uniformly. Calibrated against `Instant` over ~50ms on first
|
||||||
|
/// use; `run_experiments` triggers it before its first window (using the
|
||||||
|
/// park-aware sleep, so no scheduler thread is blocked when called from
|
||||||
|
/// an actor).
|
||||||
|
static TSC_HZ_MEASURED: OnceLock<f64> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Measured TSC frequency in Hz. Calibrates on first call (~50ms).
|
||||||
|
pub fn tsc_hz() -> f64 {
|
||||||
|
*TSC_HZ_MEASURED.get_or_init(|| {
|
||||||
|
let c0 = preempt::rdtsc();
|
||||||
|
let t0 = Instant::now();
|
||||||
|
let d = Duration::from_millis(50);
|
||||||
|
if preempt::current_slot_ptr().is_null() {
|
||||||
|
std::thread::sleep(d);
|
||||||
|
} else {
|
||||||
|
// Wall-anchored: calibration divides TSC delta by *wall*
|
||||||
|
// elapsed; a virtual sleep dilated by concurrent injection
|
||||||
|
// would still measure correctly (elapsed() is wall) but
|
||||||
|
// waste window time — and must never depend on the ledger
|
||||||
|
// it exists to convert.
|
||||||
|
crate::scheduler::sleep_wall(d);
|
||||||
|
}
|
||||||
|
(preempt::rdtsc().wrapping_sub(c0)) as f64 / t0.elapsed().as_secs_f64()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Normalized rate for one cell: count over the *virtual* window
|
||||||
|
/// (wall − injected) — Coz's normalization: injected delay does not
|
||||||
|
/// exist in the virtual timeline. A bottleneck site keeps its raw count
|
||||||
|
/// while shrinking the divisor → positive impact; a fully overlapped
|
||||||
|
/// site loses count proportionally → ~zero.
|
||||||
|
fn normalized_rate(r: &ExperimentResult, point: &str) -> Option<f64> {
|
||||||
|
let count = r.deltas.iter().find(|(n, _)| n == point).map(|(_, c)| *c)?;
|
||||||
|
let injected_secs = r.injected_cycles as f64 / tsc_hz();
|
||||||
|
let virtual_secs = (r.duration.as_secs_f64() - injected_secs).max(1e-9);
|
||||||
|
Some(count as f64 / virtual_secs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Impact of virtually speeding up `site` by `speedup_pct` on progress
|
||||||
|
/// point `point`, in percent relative to that site's own 0% baseline
|
||||||
|
/// cell. `None` if either cell or the point is missing, or the baseline
|
||||||
|
/// rate is zero. This is the machine-readable form of the summary's
|
||||||
|
/// "vs baseline" column, for programmatic checks (CI, examples).
|
||||||
|
pub fn impact_pct(
|
||||||
|
results: &[ExperimentResult],
|
||||||
|
site: &str,
|
||||||
|
speedup_pct: u32,
|
||||||
|
point: &str,
|
||||||
|
) -> Option<f64> {
|
||||||
|
let cell = results
|
||||||
|
.iter()
|
||||||
|
.find(|r| r.site == site && r.speedup_pct == speedup_pct)?;
|
||||||
|
let base = results
|
||||||
|
.iter()
|
||||||
|
.find(|r| r.site == site && r.speedup_pct == 0)?;
|
||||||
|
let rate = normalized_rate(cell, point)?;
|
||||||
|
let b = normalized_rate(base, point)?;
|
||||||
|
if b <= 0.0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some((rate / b - 1.0) * 100.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Human-readable summary: per (site, progress point), the throughput at
|
||||||
|
/// each virtual speedup and the change relative to that site's own 0%
|
||||||
|
/// baseline. A near-zero column across speedups means: optimising this
|
||||||
|
/// site buys you nothing — the RFC's headline answer.
|
||||||
|
///
|
||||||
|
/// Ends with a one-line fidelity note (RFC 007 Validation): reported
|
||||||
|
/// impacts are conservative — attribution counts on-CPU site time only,
|
||||||
|
/// so runnable queue-wait inside the site (the located @50 "deficit",
|
||||||
|
/// eff ≈ 0.93 live) is never injected and gains are lower bounds; site
|
||||||
|
/// *rankings* are unaffected.
|
||||||
|
pub fn render_summary(results: &[ExperimentResult]) -> String {
|
||||||
|
use std::fmt::Write;
|
||||||
|
let mut s = String::new();
|
||||||
|
let _ = writeln!(s, "== smarm causal profile ==");
|
||||||
|
let mut sites_seen: Vec<&str> = Vec::new();
|
||||||
|
for r in results {
|
||||||
|
if !sites_seen.contains(&r.site.as_str()) {
|
||||||
|
sites_seen.push(&r.site);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for site in sites_seen {
|
||||||
|
let _ = writeln!(s, "site {site}");
|
||||||
|
for r in results.iter().filter(|r| r.site == site) {
|
||||||
|
for (name, _) in &r.deltas {
|
||||||
|
let rate = match normalized_rate(r, name) {
|
||||||
|
Some(x) => x,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
let rel = impact_pct(results, site, r.speedup_pct, name)
|
||||||
|
.map(|p| format!("{p:+.1}%"))
|
||||||
|
.unwrap_or_else(|| "n/a".to_string());
|
||||||
|
let _ = writeln!(
|
||||||
|
s,
|
||||||
|
" speedup {:>3}% {name:<24} {rate:>12.1}/s vs baseline {rel} (injected {:.1}ms)",
|
||||||
|
r.speedup_pct,
|
||||||
|
r.injected_cycles as f64 / tsc_hz() * 1e3
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !results.is_empty() {
|
||||||
|
let _ = writeln!(
|
||||||
|
s,
|
||||||
|
"note: impacts are lower bounds — site time counts on-CPU only (runnable queue-wait is not attributed); rankings unaffected"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ledger-audit companion to `render_summary` (RFC 007 deficit hunt):
|
||||||
|
/// per cell, where the window's virtual delay went — born (injected),
|
||||||
|
/// paid (absorbed), waived (forgiven at wake) — and the attribution the
|
||||||
|
/// sampler lost: tails dropped at parks/yields inside the target site,
|
||||||
|
/// plus clamp discards. Cycle columns in ms at the calibrated TSC rate.
|
||||||
|
/// `absorbed` above `injected` in a cell (0% especially) means it paid
|
||||||
|
/// debt left over from earlier windows.
|
||||||
|
pub fn render_ledger_audit(results: &[ExperimentResult]) -> String {
|
||||||
|
use std::fmt::Write;
|
||||||
|
let hz = tsc_hz();
|
||||||
|
let ms = |c: u64| c as f64 / hz * 1e3;
|
||||||
|
let mut s = String::new();
|
||||||
|
let _ = writeln!(s, "== smarm causal ledger audit ==");
|
||||||
|
for r in results {
|
||||||
|
let _ = writeln!(
|
||||||
|
s,
|
||||||
|
"site {:<22} @{:>2}% injected {:>7.1}ms absorbed {:>7.1}ms forgiven {:>7.1}ms \
|
||||||
|
drop park {:>6.2}ms/{:<4} yield {:>6.2}ms/{:<4} offcpu {:>6.2}ms/{:<5} \
|
||||||
|
discard >max {:>6.2}ms/{:<3} unarmed {}",
|
||||||
|
r.site,
|
||||||
|
r.speedup_pct,
|
||||||
|
ms(r.injected_cycles),
|
||||||
|
ms(r.spin_absorbed_cycles),
|
||||||
|
ms(r.park_forgiven_cycles),
|
||||||
|
ms(r.drop_park_cycles),
|
||||||
|
r.drop_park_n,
|
||||||
|
ms(r.drop_yield_cycles),
|
||||||
|
r.drop_yield_n,
|
||||||
|
ms(r.offcpu_in_site_cycles),
|
||||||
|
r.offcpu_in_site_n,
|
||||||
|
ms(r.discard_overmax_cycles),
|
||||||
|
r.discard_overmax_n,
|
||||||
|
r.discard_unarmed_n
|
||||||
|
);
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Coz-compatible profile text (`profile.coz`), so Coz's existing plot
|
||||||
|
/// tooling renders our experiments — the RFC's "don't build a UI" call.
|
||||||
|
pub fn render_coz(results: &[ExperimentResult]) -> String {
|
||||||
|
use std::fmt::Write;
|
||||||
|
let mut s = String::new();
|
||||||
|
let _ = writeln!(s, "startup\ttime=0");
|
||||||
|
for r in results {
|
||||||
|
let _ = writeln!(
|
||||||
|
s,
|
||||||
|
"experiment\tselected={}\tspeedup={:.2}\tduration={}\tselected-samples=1",
|
||||||
|
r.site,
|
||||||
|
r.speedup_pct as f64 / 100.0,
|
||||||
|
r.duration.as_nanos()
|
||||||
|
);
|
||||||
|
for (name, count) in &r.deltas {
|
||||||
|
let _ = writeln!(s, "throughput-point\tname={name}\tdelta={count}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "smarm-causal")]
|
||||||
|
pub use inner::*;
|
||||||
|
|
||||||
|
/// Mark one unit of useful work complete at a named throughput progress
|
||||||
|
/// point (RFC 007). One Relaxed increment when `smarm-causal` is on; nothing
|
||||||
|
/// at all when it's off.
|
||||||
|
#[cfg(feature = "smarm-causal")]
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! progress {
|
||||||
|
($name:literal) => {{
|
||||||
|
static __SMARM_PP: ::std::sync::OnceLock<&'static $crate::causal::ProgressPoint> =
|
||||||
|
::std::sync::OnceLock::new();
|
||||||
|
__SMARM_PP
|
||||||
|
.get_or_init(|| $crate::causal::register_progress($name))
|
||||||
|
.bump();
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "smarm-causal"))]
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! progress {
|
||||||
|
($name:literal) => {{}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enter a named causal-profiling site for the current actor; the returned
|
||||||
|
/// guard exits it (restoring any enclosing site) on drop. Site identity is
|
||||||
|
/// stored in the actor's slot, so it survives preemption and migration.
|
||||||
|
/// Expands to a unit no-op without `smarm-causal`.
|
||||||
|
#[cfg(feature = "smarm-causal")]
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! causal_site {
|
||||||
|
($name:literal) => {{
|
||||||
|
static __SMARM_SITE: ::std::sync::OnceLock<u32> = ::std::sync::OnceLock::new();
|
||||||
|
$crate::causal::SiteGuard::enter(
|
||||||
|
*__SMARM_SITE.get_or_init(|| $crate::causal::site_id($name)),
|
||||||
|
)
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "smarm-causal"))]
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! causal_site {
|
||||||
|
($name:literal) => {
|
||||||
|
()
|
||||||
|
};
|
||||||
|
}
|
||||||
+327
-219
@@ -1,38 +1,102 @@
|
|||||||
//! Unbounded MPSC channels.
|
//! Unbounded multi-producer, single-consumer channels: how actors talk to
|
||||||
|
//! each other.
|
||||||
//!
|
//!
|
||||||
//! Inner state is `Arc<RawMutex<Inner<T>>>` so channels can be sent across OS
|
//! A channel is a queue with a typed [`Sender`] on one end and a typed
|
||||||
//! threads (required for the multi-scheduler runtime where a sender and
|
//! [`Receiver`] on the other. Any number of actors can hold a clone of the
|
||||||
//! receiver may run on different scheduler threads simultaneously).
|
//! `Sender` and push messages onto the same queue; exactly one [`Receiver`]
|
||||||
|
//! reads them back out, in the order they arrived. This is the basic wiring
|
||||||
|
//! smarm's other actor primitives (`gen_server`, `pg`, the registry) are all
|
||||||
|
//! built out of, and it is directly usable on its own for a worker that just
|
||||||
|
//! needs an inbox.
|
||||||
//!
|
//!
|
||||||
//! ## Why `RawMutex` (Channel class), not `std::sync::Mutex`
|
//! ## A first channel
|
||||||
//!
|
//!
|
||||||
//! An actor holding a guard with preemption *enabled* can be timesliced
|
//! ```
|
||||||
//! inside the critical section and resume on a different OS thread — the
|
//! use smarm::{channel, run, spawn};
|
||||||
//! pthread mutex would then be released from a thread that didn't lock it,
|
|
||||||
//! which is UB (Linux futexes happen to tolerate it, but it's not
|
|
||||||
//! guaranteed). `RawMutex` disables preemption for the guard's span and is
|
|
||||||
//! cross-thread-release sound by construction, closing the hole. It also
|
|
||||||
//! cannot poison. Channel locks form their own [`LockClass::Channel`]
|
|
||||||
//! (raw_mutex.rs): they may be taken under a cold (Leaf) lock — finalize and
|
|
||||||
//! `monitor()` clone senders that live in slots — but nothing may be locked
|
|
||||||
//! under them, which the debug build enforces. `recv_match` runs its user
|
|
||||||
//! predicate under this lock: keep it cheap, pure, and channel-free.
|
|
||||||
//!
|
//!
|
||||||
//! Semantics:
|
//! run(|| {
|
||||||
//! - Senders are clonable; the last sender drop closes the channel.
|
//! let (tx, rx) = channel::<u64>();
|
||||||
//! - `Receiver::recv` on an empty open channel parks the receiver.
|
//!
|
||||||
//! - `Receiver::recv` on an empty closed channel returns `Err(RecvError)`.
|
//! let worker = spawn(move || {
|
||||||
//! - `Sender::send` on an open channel always succeeds.
|
//! // Blocks until a message arrives.
|
||||||
//! - `Sender::send` on a closed channel (receiver dropped) returns
|
//! let n = rx.recv().unwrap();
|
||||||
//! `Err(SendError(value))`.
|
//! assert_eq!(n, 42);
|
||||||
//! - When a send pushes to a previously empty queue and a receiver is
|
//!
|
||||||
//! parked, the receiver is unparked.
|
//! // Once every Sender is dropped, recv() reports the channel closed
|
||||||
|
//! // instead of blocking forever.
|
||||||
|
//! assert!(rx.recv().is_err());
|
||||||
|
//! });
|
||||||
|
//!
|
||||||
|
//! tx.send(42).unwrap();
|
||||||
|
//! drop(tx); // last sender gone: the channel is now closed
|
||||||
|
//! worker.join().unwrap();
|
||||||
|
//! });
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! ## Sending
|
||||||
|
//!
|
||||||
|
//! [`Sender`] is cheaply clonable: hand a clone to every actor that needs to
|
||||||
|
//! push messages into this queue. The channel stays open as long as at least
|
||||||
|
//! one clone exists; [`Sender::send`] never blocks and always succeeds while
|
||||||
|
//! the channel is open, since the queue is unbounded. Once the [`Receiver`]
|
||||||
|
//! has been dropped, `send` returns the message back to you in
|
||||||
|
//! [`SendError`] instead of delivering it.
|
||||||
|
//!
|
||||||
|
//! ## Receiving
|
||||||
|
//!
|
||||||
|
//! There is exactly one [`Receiver`] per channel (it is not clonable).
|
||||||
|
//! [`Receiver::recv`] returns the next message in arrival order, parking the
|
||||||
|
//! calling actor if the queue is currently empty. Once every `Sender` has
|
||||||
|
//! been dropped and the queue has been drained, `recv` stops parking and
|
||||||
|
//! returns [`RecvError`] instead, so a receiver never blocks forever waiting
|
||||||
|
//! on senders that are never coming back.
|
||||||
|
//!
|
||||||
|
//! Beyond plain `recv`, three variants cover the common needs:
|
||||||
|
//!
|
||||||
|
//! - [`Receiver::try_recv`]: never parks: reports an empty-but-open channel
|
||||||
|
//! as `Ok(None)` instead of waiting.
|
||||||
|
//! - [`Receiver::recv_timeout`]: parks, but gives up and returns
|
||||||
|
//! [`RecvTimeoutError::Timeout`] if no message arrives before a deadline.
|
||||||
|
//! - [`Receiver::recv_match`] / [`Receiver::try_recv_match`]: selective
|
||||||
|
//! receive. Instead of taking whatever is at the front of the queue, pick
|
||||||
|
//! out the first message matching a predicate, leaving the rest queued in
|
||||||
|
//! order. Handy for an actor that wants to prioritise one kind of message
|
||||||
|
//! over others already waiting.
|
||||||
|
//!
|
||||||
|
//! ## Waiting on several channels: `select`
|
||||||
|
//!
|
||||||
|
//! [`select`] parks an actor across several receivers at once and reports
|
||||||
|
//! the index of the first one that is ready (has a message queued, or has
|
||||||
|
//! been closed). [`select_timeout`] adds a deadline, the way `recv_timeout`
|
||||||
|
//! does for a single channel. See their docs for the full contract,
|
||||||
|
//! including the priority-order and no-fairness guarantee.
|
||||||
|
//!
|
||||||
|
//! ## Implementation notes
|
||||||
|
//!
|
||||||
|
//! The queue and its bookkeeping live behind `Arc<RawMutex<Inner<T>>>`
|
||||||
|
//! rather than a `std::sync::Mutex`, so that a channel can be freely shared
|
||||||
|
//! and sent across the OS threads backing the multi-scheduler runtime.
|
||||||
|
//! `RawMutex` matters here for a subtler reason too: an ordinary pthread
|
||||||
|
//! mutex can be released from a different OS thread than the one that took
|
||||||
|
//! it (smarm's preemption can migrate a timesliced actor between scheduler
|
||||||
|
//! threads mid-critical-section), and doing that to a `std::sync::Mutex` is
|
||||||
|
//! undefined behavior. `RawMutex` disables preemption for the guard's short
|
||||||
|
//! lifetime instead, so the release always happens on the thread that
|
||||||
|
//! acquired it, and it has no poisoning to worry about besides. Channel
|
||||||
|
//! locks are cheap and are never held across another lock acquisition or a
|
||||||
|
//! blocking call; the predicate passed to `recv_match` runs under this lock,
|
||||||
|
//! which is why it needs to stay cheap, pure, and must not call back into
|
||||||
|
//! the same channel.
|
||||||
|
|
||||||
use crate::pid::Pid;
|
use crate::pid::Pid;
|
||||||
use crate::raw_mutex::RawMutex;
|
use crate::raw_mutex::RawMutex;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// Create a new channel and return its `(Sender, Receiver)` halves.
|
||||||
|
///
|
||||||
|
/// The channel is unbounded (no capacity limit) and open until every
|
||||||
|
/// `Sender` has been dropped.
|
||||||
pub fn channel<T>() -> (Sender<T>, Receiver<T>) {
|
pub fn channel<T>() -> (Sender<T>, Receiver<T>) {
|
||||||
let inner = Arc::new(RawMutex::new_channel(Inner {
|
let inner = Arc::new(RawMutex::new_channel(Inner {
|
||||||
queue: VecDeque::new(),
|
queue: VecDeque::new(),
|
||||||
@@ -40,34 +104,50 @@ 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> {
|
||||||
queue: VecDeque<T>,
|
queue: VecDeque<T>,
|
||||||
/// The parked receiver's `(pid, park-epoch)`. The epoch is the slot
|
/// The parked receiver's `(pid, park-epoch)`, if one is currently
|
||||||
/// word's runtime-wide wait identity (see slot_state.rs): wakers call
|
/// waiting. The epoch identifies exactly which wait this is, so a waker
|
||||||
/// `unpark_at(pid, epoch)`, so an entry left over from an already-woken
|
/// left over from a wait that already ended (a losing `select` arm, a
|
||||||
/// wait — a `select` loser arm, a satisfied `recv_timeout`'s timer — is
|
/// `recv_timeout` whose timer fired after it was already satisfied) is
|
||||||
/// inert: the wake fails the word's epoch CAS and no-ops. This replaces
|
/// inert and does nothing when it fires.
|
||||||
/// the old per-channel `cur_wait`/`next_wait_seq`/`timed_out` trio: wait
|
|
||||||
/// identity now exists exactly once, in the slot word.
|
|
||||||
parked_receiver: Option<(Pid, u32)>,
|
parked_receiver: Option<(Pid, u32)>,
|
||||||
senders: usize,
|
senders: usize,
|
||||||
receiver_alive: bool,
|
receiver_alive: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The sending half of a channel, created by [`channel`]. Clonable: every
|
||||||
|
/// clone pushes onto the same queue, and the channel stays open as long as
|
||||||
|
/// any clone is alive. Dropping the last `Sender` closes the channel, which
|
||||||
|
/// wakes a parked [`Receiver`] so it can observe the closure.
|
||||||
pub struct Sender<T> {
|
pub struct Sender<T> {
|
||||||
inner: Arc<RawMutex<Inner<T>>>,
|
inner: Arc<RawMutex<Inner<T>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The receiving half of a channel, created by [`channel`]. Not clonable:
|
||||||
|
/// a channel has exactly one receiver. Reads messages in the order they
|
||||||
|
/// were sent, via [`recv`](Receiver::recv) and its variants.
|
||||||
pub struct Receiver<T> {
|
pub struct Receiver<T> {
|
||||||
inner: Arc<RawMutex<Inner<T>>>,
|
inner: Arc<RawMutex<Inner<T>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returned by [`Sender::send`] when the channel's [`Receiver`] has already
|
||||||
|
/// been dropped. Carries the message back so it is never silently lost;
|
||||||
|
/// recover it with `.0` or by matching.
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct SendError<T>(pub T);
|
pub struct SendError<T>(pub T);
|
||||||
|
|
||||||
|
/// Returned by [`Receiver::recv`] (and the other receive methods, in their
|
||||||
|
/// own error types) when the channel is closed: every `Sender` has been
|
||||||
|
/// dropped and no message is left queued.
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
pub struct RecvError;
|
pub struct RecvError;
|
||||||
|
|
||||||
@@ -84,8 +164,8 @@ impl std::error::Error for RecvError {}
|
|||||||
pub enum RecvTimeoutError {
|
pub enum RecvTimeoutError {
|
||||||
/// The deadline passed with no message available.
|
/// The deadline passed with no message available.
|
||||||
Timeout,
|
Timeout,
|
||||||
/// All senders dropped with no message available — the bounded analogue
|
/// Every sender was dropped with no message available. The
|
||||||
/// of [`RecvError`].
|
/// timeout-aware counterpart of plain [`RecvError`].
|
||||||
Disconnected,
|
Disconnected,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,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(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,8 +197,8 @@ impl<T> Drop for Sender<T> {
|
|||||||
// Wake the parked receiver on the last sender drop regardless of
|
// Wake the parked receiver on the last sender drop regardless of
|
||||||
// whether the queue is empty. A plain `recv` only ever parks on an
|
// whether the queue is empty. A plain `recv` only ever parks on an
|
||||||
// empty queue (so this is unchanged for it), but a selective
|
// empty queue (so this is unchanged for it), but a selective
|
||||||
// `recv_match` may be parked on a *non-empty* queue holding only
|
// `recv_match` may be parked on a non-empty queue holding only
|
||||||
// non-matching messages — it must wake to observe closure and
|
// non-matching messages. It must wake to observe closure and
|
||||||
// return Err rather than sleep forever.
|
// return Err rather than sleep forever.
|
||||||
if g.senders == 0 {
|
if g.senders == 0 {
|
||||||
g.parked_receiver.take()
|
g.parked_receiver.take()
|
||||||
@@ -132,19 +214,37 @@ impl<T> Drop for Sender<T> {
|
|||||||
|
|
||||||
impl<T> Drop for Receiver<T> {
|
impl<T> Drop for Receiver<T> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.inner.lock().receiver_alive = false;
|
// The only consumer is gone: queued messages can never be delivered.
|
||||||
|
// Drop them now instead of leaving them queued until the last Sender
|
||||||
|
// happens to go away, which can be long after this receiver's owner
|
||||||
|
// has exited if some other part of the runtime is still holding a
|
||||||
|
// clone of the Sender. Draining runs each queued message's own drop
|
||||||
|
// glue, which matters for a gen_server call: dropping a queued call
|
||||||
|
// envelope drops its reply channel too, which wakes the caller with
|
||||||
|
// an error instead of leaving it parked forever. Drain under the
|
||||||
|
// lock, then run the drops after releasing it, since a message's
|
||||||
|
// drop glue may itself touch a different channel or the scheduler.
|
||||||
|
let drained = {
|
||||||
|
let mut g = self.inner.lock();
|
||||||
|
g.receiver_alive = false;
|
||||||
|
std::mem::take(&mut g.queue)
|
||||||
|
};
|
||||||
|
drop(drained);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Sender<T> {
|
impl<T> Sender<T> {
|
||||||
/// Number of messages currently queued behind this channel. Introspection
|
/// Number of messages currently queued and not yet received. For
|
||||||
/// only (RFC 016 mailbox depth); takes the channel lock, so callers reach
|
/// introspection and monitoring; takes the channel's internal lock, so
|
||||||
/// it under the registry Leaf (Leaf → Channel) via the erased probe in
|
/// avoid calling it from a hot path.
|
||||||
/// `registry.rs`, never on a hot path.
|
|
||||||
pub(crate) fn queued_len(&self) -> usize {
|
pub(crate) fn queued_len(&self) -> usize {
|
||||||
self.inner.lock().queue.len()
|
self.inner.lock().queue.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Push `value` onto the channel. Succeeds unconditionally as long as
|
||||||
|
/// the [`Receiver`] is still alive: the queue has no capacity limit, so
|
||||||
|
/// this never blocks and never fails except when the channel is closed,
|
||||||
|
/// in which case `value` comes back in [`SendError`].
|
||||||
pub fn send(&self, value: T) -> Result<(), SendError<T>> {
|
pub fn send(&self, value: T) -> Result<(), SendError<T>> {
|
||||||
let unpark = {
|
let unpark = {
|
||||||
let mut g = self.inner.lock();
|
let mut g = self.inner.lock();
|
||||||
@@ -155,16 +255,28 @@ 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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Receiver<T> {
|
impl<T> Receiver<T> {
|
||||||
|
/// Block until a message is available and return it. Messages come back
|
||||||
|
/// in the order they were sent. If the queue is empty and every
|
||||||
|
/// [`Sender`] has already been dropped, returns [`RecvError`] instead of
|
||||||
|
/// blocking forever.
|
||||||
pub fn recv(&self) -> Result<T, RecvError> {
|
pub fn recv(&self) -> Result<T, RecvError> {
|
||||||
loop {
|
loop {
|
||||||
{
|
{
|
||||||
@@ -184,45 +296,36 @@ impl<T> Receiver<T> {
|
|||||||
g.parked_receiver.is_none_or(|(p, _)| p == me),
|
g.parked_receiver.is_none_or(|(p, _)| p == me),
|
||||||
"channel has more than one receiver"
|
"channel has more than one receiver"
|
||||||
);
|
);
|
||||||
// begin_wait is lock-free — legal under the Channel lock;
|
// begin_wait is lock-free, so it's legal under the Channel lock;
|
||||||
// registering in the same critical section makes the epoch
|
// registering in the same critical section makes the epoch
|
||||||
// atomic with the senders' view of the registration.
|
// atomic with the senders' view of the registration.
|
||||||
g.parked_receiver = Some((me, crate::scheduler::begin_wait()));
|
g.parked_receiver = Some((me, crate::scheduler::begin_wait()));
|
||||||
crate::te!(crate::trace::Event::RecvPark(me));
|
crate::te!(crate::trace::Event::RecvPark(me));
|
||||||
}
|
}
|
||||||
// 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)"),
|
||||||
}));
|
}
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bounded receive: like [`recv`](Self::recv), but gives up once
|
/// Like [`recv`](Self::recv), but gives up and returns
|
||||||
/// `timeout` has elapsed, returning [`RecvTimeoutError::Timeout`].
|
/// [`RecvTimeoutError::Timeout`] if no message has arrived by the time
|
||||||
|
/// `timeout` elapses.
|
||||||
///
|
///
|
||||||
/// Built on the same timer machinery as `Mutex::lock_timeout`: the wait
|
/// If a message arrives at essentially the same moment the deadline
|
||||||
/// registers a `WaitTimeout` entry stamped with the wait's park-epoch;
|
/// passes, the message wins: you get `Ok` rather than `Timeout`. If
|
||||||
/// on expiry the channel (as the
|
/// every sender is dropped before a message arrives or the deadline
|
||||||
/// [`TimerTarget`](crate::timer::TimerTarget)) checks whether *this*
|
/// passes, you get [`RecvTimeoutError::Disconnected`].
|
||||||
/// wait is still parked and, only then, cancels it. A wake that races
|
|
||||||
/// the deadline resolves message-first: if a message is available when
|
|
||||||
/// the receiver runs, it is delivered even if the timer had already
|
|
||||||
/// fired. A satisfied or abandoned wait leaves its timer entry to expire
|
|
||||||
/// as a no-op (registration gone; epoch consumed), per the
|
|
||||||
/// no-cancellation convention in `timer.rs`.
|
|
||||||
///
|
///
|
||||||
/// The wake is classified from state alone — wakes are precise (the only
|
/// `Duration::ZERO` is a valid timeout: it still gives any
|
||||||
/// stamped wakers of this wait are a send, the last-sender drop, and the
|
/// already-queued message a chance to be returned, and only then
|
||||||
/// timer; a stop wake unwinds out of `park_current` and never reaches
|
/// reports `Timeout`.
|
||||||
/// the classification), so: message queued → `Ok`; `senders == 0` →
|
|
||||||
/// `Disconnected`; neither → it was the timer → `Timeout`.
|
|
||||||
///
|
|
||||||
/// `Duration::ZERO` is a valid timeout: it parks until the immediately-
|
|
||||||
/// due timer is drained, then reports `Timeout` unless a message was
|
|
||||||
/// already queued.
|
|
||||||
pub fn recv_timeout(&self, timeout: std::time::Duration) -> Result<T, RecvTimeoutError>
|
pub fn recv_timeout(&self, timeout: std::time::Duration) -> Result<T, RecvTimeoutError>
|
||||||
where
|
where
|
||||||
T: Send + 'static,
|
T: Send + 'static,
|
||||||
@@ -254,17 +357,19 @@ impl<T> Receiver<T> {
|
|||||||
|
|
||||||
// Arm the timer after releasing the channel lock (insert takes the
|
// Arm the timer after releasing the channel lock (insert takes the
|
||||||
// timers lock; never nest under a Channel lock). A send or even the
|
// timers lock; never nest under a Channel lock). A send or even the
|
||||||
// timer itself may unpark us before we park — the RunningNotified
|
// timer itself may unpark us before we park; the runtime's wake
|
||||||
// protocol makes the park below return immediately in that case.
|
// protocol makes the park below return immediately in that case.
|
||||||
let deadline = crate::timer::deadline_from_now(timeout);
|
let deadline = crate::timer::deadline_from_now(timeout);
|
||||||
let target: std::sync::Arc<dyn crate::timer::TimerTarget> = self.inner.clone();
|
let target: std::sync::Arc<dyn crate::timer::TimerTarget> = self.inner.clone();
|
||||||
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();
|
||||||
@@ -276,16 +381,23 @@ impl<T> Receiver<T> {
|
|||||||
Err(RecvTimeoutError::Timeout)
|
Err(RecvTimeoutError::Timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Selective receive: remove and return the first queued message for which
|
/// Selective receive: find and return the first queued message for
|
||||||
/// `pred` holds, leaving the rest in arrival order. If no queued message
|
/// which `pred` returns `true`, leaving every other message in the
|
||||||
/// matches, parks and re-scans on every send (a selective receiver may park
|
/// queue untouched and in order. Useful when an actor's inbox mixes
|
||||||
/// on a *non-empty* queue). Returns `Err(RecvError)` only once the channel
|
/// message kinds and it wants to handle one kind out of turn, without
|
||||||
/// is closed and no queued message matches.
|
/// discarding the rest.
|
||||||
///
|
///
|
||||||
/// `pred` is run while the channel lock is held: keep it cheap and pure,
|
/// If nothing queued matches, this blocks and re-checks every time a new
|
||||||
/// and do not call back into this channel from inside it. It is modelled as
|
/// message arrives, the same way [`recv`](Self::recv) blocks on an empty
|
||||||
/// `Fn` (not `FnMut`) deliberately — it is re-run from scratch on every
|
/// queue: a selective receiver can be waiting even while the queue holds
|
||||||
/// scan, so a stateful predicate would observe surprising re-counting.
|
/// messages, just none that match yet. Returns [`RecvError`] only once
|
||||||
|
/// the channel is closed and still nothing matches.
|
||||||
|
///
|
||||||
|
/// `pred` runs while the channel is locked, so keep it cheap, side
|
||||||
|
/// effect free, and make sure it never calls back into this same
|
||||||
|
/// channel. It takes `&T` and is called fresh on every scan (not `FnMut`
|
||||||
|
/// with running state), so it should judge each message purely on its
|
||||||
|
/// own content.
|
||||||
pub fn recv_match<F>(&self, pred: F) -> Result<T, RecvError>
|
pub fn recv_match<F>(&self, pred: F) -> Result<T, RecvError>
|
||||||
where
|
where
|
||||||
F: Fn(&T) -> bool,
|
F: Fn(&T) -> bool,
|
||||||
@@ -317,19 +429,23 @@ impl<T> Receiver<T> {
|
|||||||
g.parked_receiver = Some((me, crate::scheduler::begin_wait()));
|
g.parked_receiver = Some((me, crate::scheduler::begin_wait()));
|
||||||
crate::te!(crate::trace::Event::RecvPark(me));
|
crate::te!(crate::trace::Event::RecvPark(me));
|
||||||
}
|
}
|
||||||
// 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)"),
|
||||||
}));
|
}
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Non-blocking selective receive. `Ok(Some(v))` if a queued message
|
/// The non-blocking counterpart of [`recv_match`](Self::recv_match):
|
||||||
/// matched `pred` (removed, rest left in order), `Ok(None)` if the channel
|
/// returns immediately either way. `Ok(Some(v))` if a queued message
|
||||||
/// is open but nothing matched, `Err(RecvError)` if closed and nothing
|
/// matched `pred` (removed; the rest stay queued in order), `Ok(None)`
|
||||||
/// matched. Same predicate contract as [`recv_match`](Self::recv_match).
|
/// if the channel is open but nothing currently matches, `Err(RecvError)`
|
||||||
|
/// if the channel is closed and nothing matches. Same predicate contract
|
||||||
|
/// as `recv_match`.
|
||||||
pub fn try_recv_match<F>(&self, pred: F) -> Result<Option<T>, RecvError>
|
pub fn try_recv_match<F>(&self, pred: F) -> Result<Option<T>, RecvError>
|
||||||
where
|
where
|
||||||
F: Fn(&T) -> bool,
|
F: Fn(&T) -> bool,
|
||||||
@@ -349,8 +465,10 @@ impl<T> Receiver<T> {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Non-blocking. `Ok(Some(v))` if a message was available, `Ok(None)` if
|
/// The non-blocking counterpart of [`recv`](Self::recv): returns
|
||||||
/// the channel is empty but open, `Err(RecvError)` if closed and drained.
|
/// immediately either way. `Ok(Some(v))` if a message was queued,
|
||||||
|
/// `Ok(None)` if the channel is open but currently empty, `Err(RecvError)`
|
||||||
|
/// if the channel is closed and the queue is drained.
|
||||||
pub fn try_recv(&self) -> Result<Option<T>, RecvError> {
|
pub fn try_recv(&self) -> Result<Option<T>, RecvError> {
|
||||||
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() {
|
||||||
@@ -365,18 +483,18 @@ impl<T> Receiver<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// TimerTarget — the expiry half of recv_timeout
|
// TimerTarget: the expiry half of recv_timeout
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
impl<T: Send + 'static> crate::timer::TimerTarget for RawMutex<Inner<T>> {
|
impl<T: Send + 'static> crate::timer::TimerTarget for RawMutex<Inner<T>> {
|
||||||
fn on_timeout(&self, pid: Pid, epoch: u32) {
|
fn on_timeout(&self, pid: Pid, epoch: u32) {
|
||||||
// Cancel the wait only if THIS wait (epoch match) is still
|
// Cancel the wait only if THIS wait (epoch match) is still
|
||||||
// registered. If a sender already took `parked_receiver`, the
|
// registered. If a sender already took `parked_receiver`, the
|
||||||
// receiver is waking with a message — message wins, the timer
|
// receiver is waking with a message: message wins, the timer
|
||||||
// no-ops. If a later wait by the same receiver is registered, the
|
// no-ops. If a later wait by the same receiver is registered, the
|
||||||
// epoch mismatches — stale entry, no-op. (The unpark_at would fail
|
// epoch mismatches: stale entry, no-op. (unpark_at would fail its
|
||||||
// its word CAS in either case anyway; checking under the lock keeps
|
// internal check in either case anyway; checking under the lock
|
||||||
// the registration bookkeeping exact.)
|
// keeps the registration bookkeeping exact.)
|
||||||
let unpark = {
|
let unpark = {
|
||||||
let mut g = self.lock();
|
let mut g = self.lock();
|
||||||
if g.parked_receiver == Some((pid, epoch)) {
|
if g.parked_receiver == Some((pid, epoch)) {
|
||||||
@@ -386,7 +504,7 @@ impl<T: Send + 'static> crate::timer::TimerTarget for RawMutex<Inner<T>> {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Unpark outside the channel lock — it may take the run-queue lock;
|
// Unpark outside the channel lock: it may take the run-queue lock;
|
||||||
// legal under a Channel lock, but pointless to nest.
|
// legal under a Channel lock, but pointless to nest.
|
||||||
if unpark {
|
if unpark {
|
||||||
crate::scheduler::unpark_at(pid, epoch);
|
crate::scheduler::unpark_at(pid, epoch);
|
||||||
@@ -395,7 +513,7 @@ impl<T: Send + 'static> crate::timer::TimerTarget for RawMutex<Inner<T>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// select — ready-index wait over multiple receivers
|
// select: ready-index wait over multiple receivers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
pub(crate) mod sealed {
|
pub(crate) mod sealed {
|
||||||
@@ -403,33 +521,34 @@ pub(crate) mod sealed {
|
|||||||
}
|
}
|
||||||
impl<T> sealed::Sealed for Receiver<T> {}
|
impl<T> sealed::Sealed for Receiver<T> {}
|
||||||
|
|
||||||
/// An arm of a [`select`]. Implemented by [`Receiver`]; sealed, because the
|
/// An arm of a [`select`]: something you can wait on alongside other arms
|
||||||
/// registration contract below is part of the runtime's wake protocol.
|
/// and be told when it becomes ready. Implemented by [`Receiver`]; sealed
|
||||||
|
/// (cannot be implemented outside this crate), since the registration
|
||||||
|
/// contract below is part of the runtime's internal wake protocol.
|
||||||
///
|
///
|
||||||
/// Contract (all under the arm's own lock): `sel_register` checks-or-
|
/// Contract (all under the arm's own lock): `sel_register` checks-or-
|
||||||
/// registers atomically — if the arm is ready it does NOT register and
|
/// registers atomically. If the arm is ready it does not register and
|
||||||
/// returns `Ok(false)`; otherwise it publishes `(pid, epoch)` where its
|
/// returns `Ok(false)`; otherwise it publishes `(pid, epoch)` where its
|
||||||
/// wakers will find it and returns `Ok(true)`. "Ready" means a receive
|
/// wakers will find it and returns `Ok(true)`. "Ready" means a receive
|
||||||
/// would not park: a message is queued, or the arm is closed. `Err` means
|
/// would not block: a message is queued, or the arm is closed. `Err` means
|
||||||
/// the arm could not register at all (only fd arms can fail; channel
|
/// the arm could not register at all (only fd arms can fail; channel
|
||||||
/// registration is infallible) — the wait must be retired and earlier
|
/// registration always succeeds), and the wait must be retired and earlier
|
||||||
/// eager-cleanup arms unregistered.
|
/// eager-cleanup arms unregistered.
|
||||||
pub trait Selectable: sealed::Sealed {
|
pub trait Selectable: sealed::Sealed {
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
fn sel_register(&self, pid: Pid, epoch: u32) -> std::io::Result<bool>;
|
fn sel_register(&self, pid: Pid, epoch: u32) -> std::io::Result<bool>;
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
fn sel_ready(&self) -> bool;
|
fn sel_ready(&self) -> bool;
|
||||||
/// Remove this arm's `(pid, epoch)` registration if — and only if — it
|
/// Remove this arm's `(pid, epoch)` registration if, and only if, it is
|
||||||
/// is still in place. Default no-op: a losing channel arm's stale
|
/// still in place. Default no-op: a losing channel arm's stale
|
||||||
/// registration is inert (its wakers die at the epoch CAS; the next
|
/// registration is harmless and self-cleans. Fd arms override this:
|
||||||
/// wait overwrites the slot). Fd arms override this: their staleness
|
/// their staleness would otherwise leave the fd unusable for future
|
||||||
/// poisons the fd (waiters entry + kernel-side ONESHOT registration)
|
/// selects, so they need an eager cleanup pass.
|
||||||
/// and needs an eager cleanup pass.
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
fn sel_unregister(&self, _pid: Pid, _epoch: u32) {}
|
fn sel_unregister(&self, _pid: Pid, _epoch: u32) {}
|
||||||
/// Whether this arm requires the eager cleanup pass at all. Gates the
|
/// Whether this arm requires the eager cleanup pass at all. Gates the
|
||||||
/// post-wake `sel_unregister` sweep so channel-only selects keep
|
/// post-wake `sel_unregister` sweep so channel-only selects keep their
|
||||||
/// today's zero-cancellation hot path.
|
/// cheap, cleanup-free path.
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
fn sel_eager_cleanup(&self) -> bool {
|
fn sel_eager_cleanup(&self) -> bool {
|
||||||
false
|
false
|
||||||
@@ -456,38 +575,35 @@ impl<T> Selectable for Receiver<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Park on every arm at once; return the index of the first ready one.
|
/// Wait on several channels at once and return the index of the first one
|
||||||
|
/// that is ready, instead of blocking on just one with [`Receiver::recv`].
|
||||||
///
|
///
|
||||||
/// "Ready" means a receive on that arm would not park: a message is queued,
|
/// "Ready" means a receive on that arm would not block: a message is
|
||||||
/// or the arm is **closed** (so the caller's `try_recv` observes the
|
/// queued, or the arm is closed (so the caller's own `try_recv` observes
|
||||||
/// disconnect — a dead arm is an event, not a hang). The caller consumes the
|
/// the disconnect: a dead arm is something to react to, not something to
|
||||||
/// arm itself, typically via [`Receiver::try_recv`]; single-receiver
|
/// hang on). `select` only tells you which arm is ready; read the actual
|
||||||
/// channels guarantee nothing can steal the message in between.
|
/// message yourself, typically with [`Receiver::try_recv`] on that arm.
|
||||||
///
|
///
|
||||||
/// A closed arm stays ready *forever*: once its disconnect has been
|
/// A closed arm stays ready forever. Once you have observed its disconnect,
|
||||||
/// observed, drop it from the arm set — under priority order it would
|
/// drop it from the arm set you pass in next time: otherwise, under the
|
||||||
/// otherwise win every subsequent call and starve every higher-indexed arm.
|
/// priority order below, it would win every subsequent call and starve
|
||||||
|
/// every arm listed after it.
|
||||||
///
|
///
|
||||||
/// Arms are scanned **in order**: index 0 is the highest priority, both on
|
/// Arms are checked **in order**: index 0 is the highest priority, both
|
||||||
/// the immediate-ready path and after a wake. This is a documented
|
/// when checking immediately and after being woken. This is a deliberate,
|
||||||
/// guarantee (compose like BEAM receive clauses: put control channels
|
/// documented guarantee, not an accident of implementation: put a control
|
||||||
/// first), not an accident — and therefore there is NO fairness promise; a
|
/// or shutdown channel first so it is always noticed promptly. The
|
||||||
/// saturated arm 0 starves arm 1 by design.
|
/// flip side is that there is **no fairness guarantee**: a busy arm 0 can
|
||||||
|
/// starve arm 1 indefinitely by design.
|
||||||
///
|
///
|
||||||
/// One actor may select on a channel and later `recv` on it (or select on
|
/// One actor can `select` on a channel and later plain `recv` on it (or
|
||||||
/// overlapping sets) freely. What stays illegal is what was always illegal:
|
/// `select` again on an overlapping set of arms) with no restriction. What
|
||||||
/// two *different* actors receiving on one channel.
|
/// stays illegal is what was always illegal for a channel: two *different*
|
||||||
|
/// actors receiving on the same one.
|
||||||
///
|
///
|
||||||
/// Built on the consuming-wake protocol (see slot_state.rs): all arms are
|
/// Panics if `arms` is empty, if called outside an actor, or if an fd arm
|
||||||
/// registered under one wait epoch; the winning wake consumes it, so losing
|
/// fails to register (see [`try_select`] for the fallible form; a
|
||||||
/// arms' registrations are inert and need no cancellation pass — they
|
/// channel-only `select` can never fail).
|
||||||
/// self-clean at their wakers' failed CAS, or get overwritten by this
|
|
||||||
/// receiver's next wait on that channel.
|
|
||||||
///
|
|
||||||
/// Panics if `arms` is empty, when called outside an actor, or if an fd
|
|
||||||
/// arm fails to register (EBADF, EMFILE, a second waiter on one fd —
|
|
||||||
/// see [`try_select`] for the fallible form; channel-only selects cannot
|
|
||||||
/// fail).
|
|
||||||
pub fn select(arms: &[&dyn Selectable]) -> usize {
|
pub fn select(arms: &[&dyn Selectable]) -> usize {
|
||||||
match try_select(arms) {
|
match try_select(arms) {
|
||||||
Ok(i) => i,
|
Ok(i) => i,
|
||||||
@@ -495,9 +611,10 @@ pub fn select(arms: &[&dyn Selectable]) -> usize {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [`select`], fallible: `Err` when an arm fails to register (only fd
|
/// The fallible form of [`select`]: `Err` when an arm fails to register.
|
||||||
/// arms can — EBADF, EMFILE on the epoll set, or a second waiter on an
|
/// Only fd arms can fail this way (for example, the file descriptor is
|
||||||
/// fd that already has one). On `Err` the wait is fully retired and no
|
/// invalid, or something else is already waiting on it); a channel-only
|
||||||
|
/// select can never fail. On `Err` the wait is fully retired and no
|
||||||
/// registration is left behind: every arm registered before the failing
|
/// registration is left behind: every arm registered before the failing
|
||||||
/// one has been unregistered.
|
/// one has been unregistered.
|
||||||
pub fn try_select(arms: &[&dyn Selectable]) -> std::io::Result<usize> {
|
pub fn try_select(arms: &[&dyn Selectable]) -> std::io::Result<usize> {
|
||||||
@@ -513,15 +630,19 @@ pub fn try_select(arms: &[&dyn Selectable]) -> std::io::Result<usize> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stale fd registrations are not harmless (a losing fd arm's
|
// Stale fd registrations are not harmless (a losing fd arm's
|
||||||
// waiters entry poisons the fd with AlreadyExists and its
|
// leftover registration can make the fd unusable for the next
|
||||||
// kernel-side ONESHOT registration can fire arbitrarily late), so
|
// select until a kernel event happens to clear it), so selects
|
||||||
// selects containing fd arms run an eager cleanup pass after the
|
// containing fd arms run an eager cleanup pass after the park,
|
||||||
// park — including when a terminal stop unwinds out of it, via
|
// including when a terminal stop unwinds out of it, via the guard.
|
||||||
// the guard. Channel-only selects skip all of it: `eager` is
|
// Channel-only selects skip all of it: `eager` is false, the guard
|
||||||
// false, the guard is disarmed, and the loser-arm self-cleaning
|
// is disarmed, and the loser-arm self-cleaning story is unchanged.
|
||||||
// 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();
|
||||||
|
|
||||||
@@ -532,22 +653,22 @@ pub fn try_select(arms: &[&dyn Selectable]) -> std::io::Result<usize> {
|
|||||||
drop(guard);
|
drop(guard);
|
||||||
|
|
||||||
// Woken precisely: an arm's send (message) or last-sender drop
|
// Woken precisely: an arm's send (message) or last-sender drop
|
||||||
// (closure) consumed our epoch, and both leave their arm ready —
|
// (closure) is what woke us, and both leave their arm ready.
|
||||||
// return the first one, in priority order (which may be a
|
// Return the first ready one, in priority order (which may be a
|
||||||
// different, higher-priority arm than the one that woke us; its
|
// different, higher-priority arm than the one that woke us; its
|
||||||
// message stays queued and re-reports ready on the next call).
|
// message stays queued and re-reports ready on the next call).
|
||||||
// Fd arms classify by a fresh zero-timeout poll, so they too are
|
// Fd arms classify by a fresh zero-timeout poll, so they too are
|
||||||
// a pure function of state — independent of the registration the
|
// a pure function of current state, independent of the
|
||||||
// cleanup pass just removed.
|
// registration the cleanup pass just removed.
|
||||||
for (i, arm) in arms.iter().enumerate() {
|
for (i, arm) in arms.iter().enumerate() {
|
||||||
if arm.sel_ready() {
|
if arm.sel_ready() {
|
||||||
return Ok(i);
|
return Ok(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Unreachable by protocol (a stop wake unwinds out of
|
// Unreachable in practice (a stop wake unwinds out of
|
||||||
// park_current). Defensive: re-open the wait and re-register —
|
// park_current before we get here). Defensive: re-open the wait
|
||||||
// stale own-registrations are overwritten (channels) or were
|
// and re-register; stale own-registrations are overwritten
|
||||||
// removed by the cleanup pass above (fds).
|
// (channels) or were removed by the cleanup pass above (fds).
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,11 +683,11 @@ fn unregister_arms(arms: &[&dyn Selectable], me: Pid, epoch: u32) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stop-unwind twin of the explicit cleanup pass: a terminal stop unwinds
|
// Stop-unwind twin of the explicit cleanup pass: a terminal stop unwinds
|
||||||
/// out of `park_current`, and a registered fd arm must not outlive its
|
// out of `park_current`, and a registered fd arm must not outlive its
|
||||||
/// actor (the generalization of `wait_fd`'s `Dereg`). Disarmed on the
|
// actor. Disarmed on the normal path after the explicit pass runs; never
|
||||||
/// normal path after the explicit pass runs; never armed when no fd arm
|
// armed when no fd arm is registered, keeping the channel-only path
|
||||||
/// registered, keeping the channel-only path guard-free in effect.
|
// guard-free in effect.
|
||||||
struct UnregisterGuard<'a> {
|
struct UnregisterGuard<'a> {
|
||||||
arms: &'a [&'a dyn Selectable],
|
arms: &'a [&'a dyn Selectable],
|
||||||
me: Pid,
|
me: Pid,
|
||||||
@@ -582,25 +703,17 @@ impl Drop for UnregisterGuard<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The registration pass shared by [`select`] and [`select_timeout`]:
|
// The registration pass shared by `select` and `select_timeout`: check-or-
|
||||||
/// check-or-register each arm, in priority order, each atomically under its
|
// register each arm, in priority order, each atomically under its own lock.
|
||||||
/// own lock. Cross-arm atomicity is unnecessary: an arm becoming ready
|
// Cross-arm atomicity is unnecessary: an arm becoming ready right after its
|
||||||
/// right after its registration wakes the caller through the protocol (the
|
// registration still wakes the caller through the normal wake path.
|
||||||
/// prep-to-park window is closed by RunningNotified).
|
//
|
||||||
///
|
// `Ok(Some(i))` = arm `i` was already ready, the pass stopped, and the wait
|
||||||
/// `Ok(Some(i))` = arm `i` was ready, the pass stopped, and the wait has
|
// has been fully retired (no park may follow): earlier fd arms are
|
||||||
/// been RETIRED (no park may follow): earlier arms hold live-epoch
|
// unregistered eagerly so none are left dangling. `Err` = an arm failed to
|
||||||
/// registrations, so earlier *fd* arms are unregistered eagerly, then the
|
// register; same unwind (earlier fd arms unregistered, wait retired).
|
||||||
/// epoch is bumped, a landed notification eaten, and a pending stop
|
// `Ok(None)` = every arm registered successfully; the caller parks.
|
||||||
/// re-observed — without which a stale arm wake could fault a later
|
fn register_arms(me: Pid, epoch: u32, arms: &[&dyn Selectable]) -> std::io::Result<Option<usize>> {
|
||||||
/// one-shot park. `Err` = an arm failed to register; identical unwind
|
|
||||||
/// (earlier fd arms unregistered, wait retired). `Ok(None)` = every arm
|
|
||||||
/// registered; the caller parks.
|
|
||||||
fn register_arms(
|
|
||||||
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,
|
||||||
@@ -619,10 +732,10 @@ fn register_arms(
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The [`select_timeout`] timer target: stateless, because precise wakes
|
// The `select_timeout` timer target: stateless, because a wake's cause can
|
||||||
/// make classification a pure function of channel state. The entry is
|
// always be read back off plain channel state (an arm ready, or not). If
|
||||||
/// stamped with the select's epoch; if an arm already won, this unpark dies
|
// an arm already won before the deadline, this timer's fire is simply
|
||||||
/// at the word's epoch CAS (the no-cancellation convention in `timer.rs`).
|
// ignored, the way any other stale wakeup is.
|
||||||
struct SelectTimeout;
|
struct SelectTimeout;
|
||||||
impl crate::timer::TimerTarget for SelectTimeout {
|
impl crate::timer::TimerTarget for SelectTimeout {
|
||||||
fn on_timeout(&self, pid: Pid, epoch: u32) {
|
fn on_timeout(&self, pid: Pid, epoch: u32) {
|
||||||
@@ -630,31 +743,22 @@ impl crate::timer::TimerTarget for SelectTimeout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [`select`] with a deadline: returns `Some(index)` like `select`, or
|
/// Like [`select`], but gives up and returns `None` if no arm becomes
|
||||||
/// `None` once `timeout` elapses with no arm ready.
|
/// ready before `timeout` elapses.
|
||||||
///
|
///
|
||||||
/// All of `select`'s semantics carry over (priority order, closed arms
|
/// All of `select`'s semantics carry over: arms are still checked in
|
||||||
/// permanently ready, no fairness promise). The timeout is one more stamped
|
/// priority order, a closed arm is still permanently ready, and there is
|
||||||
/// waker on the same wait epoch — nothing is registered in any arm for it,
|
/// still no fairness guarantee across arms. A message that arrives at
|
||||||
/// so there is nothing to cancel or leak: an arm winning leaves the timer
|
/// essentially the same moment the deadline passes still wins, the same
|
||||||
/// entry to expire as a stale-epoch no-op; the timer winning leaves the
|
/// way [`Receiver::recv_timeout`] resolves that race.
|
||||||
/// arms' registrations to self-clean exactly as a `select` loser's would.
|
|
||||||
///
|
///
|
||||||
/// The wake is classified from state alone (wakes are precise): some arm
|
/// `Duration::ZERO` is a valid timeout: it still gives an already-ready arm
|
||||||
/// ready → `Some` of the first, in priority order; none ready → the timer
|
/// a chance to be reported before falling through to `None`.
|
||||||
/// was the only remaining stamped waker → `None`. A message that races the
|
|
||||||
/// deadline resolves message-first, as `recv_timeout` does.
|
|
||||||
///
|
///
|
||||||
/// `Duration::ZERO` is a valid timeout: it parks until the immediately-due
|
/// Panics if `arms` is empty, if called outside an actor, or if an fd arm
|
||||||
/// timer is drained, then reports `None` unless an arm was already ready.
|
/// fails to register (see [`try_select_timeout`] for the fallible form; a
|
||||||
///
|
/// channel-only select can never fail).
|
||||||
/// Panics if `arms` is empty, when called outside an actor, or if an fd
|
pub fn select_timeout(arms: &[&dyn Selectable], timeout: std::time::Duration) -> Option<usize> {
|
||||||
/// arm fails to register (see [`try_select_timeout`] for the fallible
|
|
||||||
/// form; channel-only selects cannot fail).
|
|
||||||
pub fn select_timeout(
|
|
||||||
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!(
|
||||||
@@ -663,9 +767,9 @@ pub fn select_timeout(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [`select_timeout`], fallible: `Err` when an arm fails to register
|
/// The fallible form of [`select_timeout`]: `Err` when an arm fails to
|
||||||
/// (only fd arms can). On `Err` the wait is fully retired and no
|
/// register (only fd arms can). On `Err` the wait is fully retired and no
|
||||||
/// registration — arm-side or kernel-side — is left behind.
|
/// registration is left behind on any arm.
|
||||||
pub fn try_select_timeout(
|
pub fn try_select_timeout(
|
||||||
arms: &[&dyn Selectable],
|
arms: &[&dyn Selectable],
|
||||||
timeout: std::time::Duration,
|
timeout: std::time::Duration,
|
||||||
@@ -680,19 +784,23 @@ pub fn try_select_timeout(
|
|||||||
return Ok(Some(i)); // ready now: the timer was never armed
|
return Ok(Some(i)); // ready now: the timer was never armed
|
||||||
}
|
}
|
||||||
|
|
||||||
// Arm the timer after the registration pass, outside every Channel
|
// Arm the timer after the registration pass, outside every channel
|
||||||
// lock (insert takes the timers lock).
|
// lock (inserting a timer takes the timers lock).
|
||||||
let deadline = crate::timer::deadline_from_now(timeout);
|
let deadline = crate::timer::deadline_from_now(timeout);
|
||||||
let target: std::sync::Arc<dyn crate::timer::TimerTarget> = std::sync::Arc::new(SelectTimeout);
|
let target: std::sync::Arc<dyn crate::timer::TimerTarget> = std::sync::Arc::new(SelectTimeout);
|
||||||
crate::scheduler::insert_wait_timer(deadline, me, target, epoch);
|
crate::scheduler::insert_wait_timer(deadline, me, target, epoch);
|
||||||
|
|
||||||
// Same eager-cleanup story as `try_select`: the timer arm needs none
|
// Same eager-cleanup story as `try_select`: a timer win in particular
|
||||||
// (stateless, stale entries die at the epoch CAS), channel arms need
|
// leaves every fd arm's registration behind, which without this pass
|
||||||
// none, fd arms do — and a timer win in particular leaves every fd
|
// would leave those fds unusable until a kernel event happened to
|
||||||
// arm's registration behind, which without this pass would poison
|
// clear them.
|
||||||
// those fds until a kernel event happened to fire.
|
|
||||||
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
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+98
-28
@@ -178,11 +178,13 @@
|
|||||||
//! 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};
|
||||||
use crate::scheduler::{cancel_timer, request_stop, send_after_to, spawn, spawn_under};
|
use crate::scheduler::{cancel_timer, request_stop, send_after_to};
|
||||||
use crate::timer::TimerId;
|
use crate::timer::TimerId;
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -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(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -643,11 +668,17 @@ pub struct GenServerBuilder<G: GenServer> {
|
|||||||
state: G,
|
state: G,
|
||||||
infos: Vec<Receiver<G::Info>>,
|
infos: Vec<Receiver<G::Info>>,
|
||||||
supervisor: Option<Pid>,
|
supervisor: Option<Pid>,
|
||||||
|
stack_opts: crate::scheduler::SpawnOpts,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<G: GenServer> GenServerBuilder<G> {
|
impl<G: GenServer> GenServerBuilder<G> {
|
||||||
pub fn new(state: G) -> Self {
|
pub fn new(state: G) -> Self {
|
||||||
GenServerBuilder { state, infos: Vec::new(), supervisor: None }
|
GenServerBuilder {
|
||||||
|
state,
|
||||||
|
infos: Vec::new(),
|
||||||
|
supervisor: None,
|
||||||
|
stack_opts: crate::scheduler::SpawnOpts::default(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add an out-of-band channel; messages arriving on it are dispatched to
|
/// Add an out-of-band channel; messages arriving on it are dispatched to
|
||||||
@@ -665,6 +696,14 @@ impl<G: GenServer> GenServerBuilder<G> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stack shape for the server actor (RFC 019) — see
|
||||||
|
/// [`SpawnOpts`](crate::SpawnOpts). Useful for servers that recurse
|
||||||
|
/// deeply or call into FFI with large C frames.
|
||||||
|
pub fn stack_opts(mut self, opts: crate::scheduler::SpawnOpts) -> Self {
|
||||||
|
self.stack_opts = opts;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Spawn the server actor and hand back its [`GenServerRef`]. The server's
|
/// Spawn the server actor and hand back its [`GenServerRef`]. The server's
|
||||||
/// lifetime is governed by its refs, not by joining, so the backing join
|
/// lifetime is governed by its refs, not by joining, so the backing join
|
||||||
/// handle is dropped.
|
/// handle is dropped.
|
||||||
@@ -677,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
|
||||||
@@ -686,12 +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 } = self;
|
let GenServerBuilder {
|
||||||
|
state,
|
||||||
|
infos,
|
||||||
|
supervisor,
|
||||||
|
stack_opts,
|
||||||
|
} = self;
|
||||||
let handle = match supervisor {
|
let handle = match supervisor {
|
||||||
Some(sup) => spawn_under(sup, move || server_loop::<G>(rx, state, infos)),
|
Some(sup) => crate::scheduler::spawn_under_with(sup, stack_opts, move || {
|
||||||
None => spawn(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(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -719,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.
|
||||||
@@ -758,6 +815,12 @@ impl<G: GenServer> NamedGenServerBuilder<G> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stack shape for the server actor (see [`GenServerBuilder::stack_opts`]).
|
||||||
|
pub fn stack_opts(mut self, opts: crate::scheduler::SpawnOpts) -> Self {
|
||||||
|
self.builder = self.builder.stack_opts(opts);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Spawn the server and bind its name in one step. Fallible: returns
|
/// Spawn the server and bind its name in one step. Fallible: returns
|
||||||
/// [`RegisterError::NameTaken`] if the name is already held by a different
|
/// [`RegisterError::NameTaken`] if the name is already held by a different
|
||||||
/// live server.
|
/// live server.
|
||||||
@@ -907,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);
|
||||||
@@ -962,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);
|
||||||
}
|
}
|
||||||
@@ -975,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)),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1008,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);
|
||||||
@@ -1023,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) {
|
||||||
@@ -1031,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)
|
||||||
|
|||||||
+36
-8
@@ -71,7 +71,7 @@
|
|||||||
|
|
||||||
use crate::channel::{channel, select, Receiver, Sender};
|
use crate::channel::{channel, select, Receiver, Sender};
|
||||||
use crate::pid::Pid;
|
use crate::pid::Pid;
|
||||||
use crate::scheduler::{cancel_timer, send_after_to, spawn as spawn_actor};
|
use crate::scheduler::{cancel_timer, send_after_to};
|
||||||
use crate::timer::TimerId;
|
use crate::timer::TimerId;
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,9 +445,22 @@ impl<M: Machine> GenStatemRef<M> {
|
|||||||
///
|
///
|
||||||
/// Panics if called outside `Runtime::run()`.
|
/// Panics if called outside `Runtime::run()`.
|
||||||
pub fn spawn<M: Machine>(machine: M) -> GenStatemRef<M> {
|
pub fn spawn<M: Machine>(machine: M) -> GenStatemRef<M> {
|
||||||
|
spawn_with(crate::scheduler::SpawnOpts::default(), machine)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`spawn`] with per-actor stack shape overrides (RFC 019) for the machine's
|
||||||
|
/// actor — see [`SpawnOpts`](crate::SpawnOpts). gen_statem has no builder
|
||||||
|
/// (its one-shot `spawn(machine)` shape predates RFC 019), so the opts ride
|
||||||
|
/// a `_with` variant like the scheduler's own spawns.
|
||||||
|
///
|
||||||
|
/// Panics if called outside `Runtime::run()`.
|
||||||
|
pub fn spawn_with<M: Machine>(opts: crate::scheduler::SpawnOpts, machine: M) -> GenStatemRef<M> {
|
||||||
let (tx, rx) = channel::<M::Ev>();
|
let (tx, rx) = channel::<M::Ev>();
|
||||||
let handle = spawn_actor(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
|
||||||
@@ -475,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 => {
|
||||||
@@ -488,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 => {
|
||||||
|
|||||||
+251
-87
@@ -1,26 +1,85 @@
|
|||||||
//! RFC 016 — runtime introspection (Chunk 1: the read primitive).
|
//! Inspect what is running right now: which actors exist, what state each one
|
||||||
|
//! is in, and how they are related.
|
||||||
//!
|
//!
|
||||||
//! A synchronous, internal read of the slab that returns *owned* data. This is
|
//! This is the tool for questions like "is my server still alive", "how many
|
||||||
//! the mechanism the whole RFC hangs off: tests, the future observer
|
//! actors are currently parked waiting on something", or "what does the spawn
|
||||||
//! gen_server (Chunk 4), and a later control plane (RFC 003) are all consumers
|
//! tree look like". It is meant for debugging, test assertions, a health check
|
||||||
//! of [`snapshot`] / [`actor_info`], never of the runtime internals directly.
|
//! endpoint, or a monitoring dashboard: anywhere you want to look at the
|
||||||
|
//! runtime from the outside without stopping it or coupling your code to its
|
||||||
|
//! internals.
|
||||||
//!
|
//!
|
||||||
//! ## Consistency (DECISION D2 — per-slot tearing, `ps` semantics)
|
//! Three entry points, in order of scope:
|
||||||
//!
|
//!
|
||||||
//! [`snapshot`] is point-in-time and mildly racy *across* actors: each slot's
|
//! - [`snapshot`] returns every actor that currently exists, as a plain
|
||||||
//! scheduling state is a lock-free word load, so an actor reported `Running`
|
//! owned `Vec`, so you can filter, count, or search it however you like.
|
||||||
//! may already be `Parked`, and an actor can die mid-scan. This is the cheap,
|
//! - [`actor_info`] returns a coherent view of exactly one actor, by pid.
|
||||||
//! useful model (a coherent stop-the-world cut is expensive and rarely wanted).
|
//! Cheaper than filtering a whole snapshot down to one entry, and more
|
||||||
//! [`actor_info`] is coherent for the single actor it names.
|
//! precise (see "Consistency" below).
|
||||||
|
//! - [`tree`] returns the same actors as [`snapshot`], folded into a
|
||||||
|
//! parent/child forest that mirrors who spawned whom.
|
||||||
//!
|
//!
|
||||||
//! ## Locking
|
//! ```
|
||||||
|
//! use smarm::{actor_info, channel, run, snapshot, spawn, ActorState};
|
||||||
//!
|
//!
|
||||||
//! The lock order is **Leaf → Channel, at most one of each** (`raw_mutex.rs`);
|
//! run(|| {
|
||||||
//! cold locks, the registry, and the free list are all Leaves, so we may never
|
//! let (ready_tx, ready_rx) = channel::<()>();
|
||||||
//! hold two at once. The read is therefore phased: first a single registry-leaf
|
//! let (gate_tx, gate_rx) = channel::<()>();
|
||||||
//! pass for names and mailbox depth (the per-channel length read is a Channel
|
//!
|
||||||
//! lock taken under that Leaf — legal), released before the slab scan takes any
|
//! let worker = spawn(move || {
|
||||||
//! per-slot cold Leaf.
|
//! ready_tx.send(()).unwrap();
|
||||||
|
//! gate_rx.recv().unwrap(); // blocks here until released
|
||||||
|
//! });
|
||||||
|
//! ready_rx.recv().unwrap();
|
||||||
|
//!
|
||||||
|
//! // `snapshot` sees every actor, including this one and the worker.
|
||||||
|
//! let snap = snapshot();
|
||||||
|
//! assert!(snap.actors.len() >= 2);
|
||||||
|
//!
|
||||||
|
//! // `actor_info` gives a coherent view of just the worker. It is
|
||||||
|
//! // blocked on the gate channel, so it must be Parked.
|
||||||
|
//! let pid = worker.pid();
|
||||||
|
//! let info = actor_info(pid).expect("worker is still alive");
|
||||||
|
//! assert_eq!(info.state, ActorState::Parked);
|
||||||
|
//!
|
||||||
|
//! gate_tx.send(()).unwrap();
|
||||||
|
//! worker.join().unwrap();
|
||||||
|
//!
|
||||||
|
//! // Once joined, the pid no longer names a live actor.
|
||||||
|
//! assert!(actor_info(pid).is_none());
|
||||||
|
//! });
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! ## Consistency
|
||||||
|
//!
|
||||||
|
//! [`snapshot`] is not a single atomic pause-the-world freeze: it walks every
|
||||||
|
//! actor's state one after another, so it is a series of independent,
|
||||||
|
//! cheap, lock-free reads rather than one coherent moment in time. Between
|
||||||
|
//! reading actor A and actor B, either one can change state, and an actor can
|
||||||
|
//! even finish and disappear mid-scan. In practice this is exactly what you
|
||||||
|
//! want: a coherent stop-the-world snapshot would mean pausing every actor in
|
||||||
|
//! the runtime just to look at it, which is expensive and rarely necessary
|
||||||
|
//! for a dashboard, a test assertion, or a debugging session.
|
||||||
|
//!
|
||||||
|
//! [`actor_info`], in contrast, is coherent for the one actor it names: all of
|
||||||
|
//! its fields describe the same instant for that actor, because a single
|
||||||
|
//! actor's data cannot tear the way a scan across many actors can.
|
||||||
|
//!
|
||||||
|
//! ## Implementation notes
|
||||||
|
//!
|
||||||
|
//! These details matter if you are working on smarm itself; they are not part
|
||||||
|
//! of the public contract.
|
||||||
|
//!
|
||||||
|
//! The read never stops the scheduler and never holds a lock across the whole
|
||||||
|
//! scan. Each actor's scheduling state is a single lock-free word load
|
||||||
|
//! (hence the possible tearing described above). Reading the rest of an
|
||||||
|
//! actor's cold data (its supervisor, monitors, links, and so on) takes a
|
||||||
|
//! brief per-actor lock, just long enough to copy those fields out; nothing
|
||||||
|
//! is held across actors. Locking follows the crate-wide rule that at most
|
||||||
|
//! one "leaf" lock (a per-actor lock, the registry lock, or the free list
|
||||||
|
//! lock) is held at a time, with no leaf lock held while acquiring another.
|
||||||
|
//! The read is phased accordingly: first one pass over the registry to
|
||||||
|
//! collect every actor's registered names and mailbox depth, released before
|
||||||
|
//! the per-actor scan begins.
|
||||||
|
|
||||||
use crate::pid::Pid;
|
use crate::pid::Pid;
|
||||||
use crate::registry::MailboxInfo;
|
use crate::registry::MailboxInfo;
|
||||||
@@ -31,15 +90,28 @@ use crate::slot_state::{
|
|||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// Snapshot wire-format version (DECISION D1). [`RuntimeSnapshot`] is treated as
|
/// The format version carried by every [`RuntimeSnapshot`] and
|
||||||
/// a stable type from day one: it becomes the observer protocol (Chunk 4) and
|
/// [`RuntimeTree`], as [`RuntimeSnapshot::format_version`] /
|
||||||
/// crosses a version boundary the moment a remote observer attaches (RFC 011),
|
/// [`RuntimeTree::format_version`]. If you serialize a snapshot (for example
|
||||||
/// so the version travels with the data from the start.
|
/// to send it somewhere else, or to compare snapshots taken with different
|
||||||
|
/// versions of smarm) check this field: a change in its value means the shape
|
||||||
|
/// of [`ActorInfo`] or its neighbors has changed and old and new snapshots
|
||||||
|
/// should not be assumed compatible. If you only ever read a snapshot
|
||||||
|
/// in-process in the same version of smarm that produced it, you can ignore
|
||||||
|
/// this field.
|
||||||
pub const SNAPSHOT_FORMAT_VERSION: u16 = 1;
|
pub const SNAPSHOT_FORMAT_VERSION: u16 = 1;
|
||||||
|
|
||||||
/// Fine-grained scheduling state, mapped from the packed slot word with no new
|
/// What an actor is doing right now, from the scheduler's point of view.
|
||||||
/// storage. `RunningNotified` collapses into `Notified` — a wake landed while
|
///
|
||||||
/// the actor was on-CPU and it will re-queue when it yields.
|
/// - `Queued`: runnable, waiting for a scheduler thread to pick it up.
|
||||||
|
/// - `Running`: currently executing on a scheduler thread.
|
||||||
|
/// - `Notified`: was running and got woken up (for example, a message
|
||||||
|
/// arrived) before it had a chance to yield or park; it will be re-queued
|
||||||
|
/// as soon as it does.
|
||||||
|
/// - `Parked`: blocked, waiting on something such as a channel receive, a
|
||||||
|
/// mutex, a timer, or an IO event.
|
||||||
|
/// - `Done`: has finished (returned or panicked) but its slot has not been
|
||||||
|
/// reclaimed for reuse yet, so it is still visible to introspection.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum ActorState {
|
pub enum ActorState {
|
||||||
Queued,
|
Queued,
|
||||||
@@ -49,8 +121,8 @@ pub enum ActorState {
|
|||||||
Done,
|
Done,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Classify a packed state word. `None` for a Vacant slot (skipped by the scan)
|
/// Classify a packed state word. `None` for a Vacant slot (skipped by the
|
||||||
/// — the only state that is not an actor.
|
/// scan): a vacant slot holds no actor at all, live or done.
|
||||||
fn classify(w: u64) -> Option<ActorState> {
|
fn classify(w: u64) -> Option<ActorState> {
|
||||||
Some(match word_state(w) {
|
Some(match word_state(w) {
|
||||||
ST_QUEUED => ActorState::Queued,
|
ST_QUEUED => ActorState::Queued,
|
||||||
@@ -62,61 +134,104 @@ fn classify(w: u64) -> Option<ActorState> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Owned, point-in-time view of one actor — no borrows of runtime internals, so
|
/// An owned, self-contained view of one actor at (approximately) one moment.
|
||||||
/// it is safe to hand to any consumer.
|
/// It borrows nothing from the runtime, so you can keep it, send it
|
||||||
|
/// elsewhere, or print it long after the actor it describes has changed
|
||||||
|
/// state or even exited.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ActorInfo {
|
pub struct ActorInfo {
|
||||||
pub pid: Pid,
|
pub pid: Pid,
|
||||||
/// Registered names, inverted from the registry (usually 0 or 1).
|
/// Names this actor is currently registered under (see the
|
||||||
|
/// [`registry`](crate::registry) module). Usually empty or one name;
|
||||||
|
/// an actor can have more if it registered several.
|
||||||
pub names: Vec<&'static str>,
|
pub names: Vec<&'static str>,
|
||||||
pub state: ActorState,
|
pub state: ActorState,
|
||||||
/// Spawn-time parent edge (DECISION D9): `spawn_under` sets it to the
|
/// The actor that spawned this one: whoever called `spawn` or
|
||||||
/// supervisor, plain `spawn` to the spawning actor — so it is parentage,
|
/// `spawn_under` to create it. This is a parentage record, not
|
||||||
/// not necessarily a supervision relationship. `ROOT_PID` for the run's
|
/// necessarily a supervision relationship: `spawn_under` records the
|
||||||
/// root actor and for `Done` tombstones (whose `Actor` is already gone).
|
/// supervisor you asked for, while plain `spawn` records the spawning
|
||||||
|
/// actor itself, whether or not it supervises anything. It is the
|
||||||
|
/// runtime's root pid for the run's own root actor, and for a `Done`
|
||||||
|
/// actor whose bookkeeping has already been cleared.
|
||||||
pub supervisor: Pid,
|
pub supervisor: Pid,
|
||||||
pub trap_exit: bool,
|
pub trap_exit: bool,
|
||||||
pub monitors: u32,
|
pub monitors: u32,
|
||||||
pub links: u32,
|
pub links: u32,
|
||||||
pub joiners: u32,
|
pub joiners: u32,
|
||||||
/// Queued messages summed over the actor's *published* channels (register /
|
/// Messages currently queued and not yet delivered, summed across every
|
||||||
/// install / spawn_addr / gen_server). 0 for an actor that holds only a
|
/// channel this actor has published (via `register`, `install`,
|
||||||
/// private `channel()` receiver — those are invisible to the registry.
|
/// `spawn_addr`, or starting a gen_server). This is 0 for an actor that
|
||||||
|
/// only holds a private, unpublished `channel()` receiver, since nothing
|
||||||
|
/// outside the actor can see that channel exists.
|
||||||
pub mailbox_depth: u32,
|
pub mailbox_depth: u32,
|
||||||
/// Timeslice overruns tallied for this incarnation (RFC 016 Chunk 2): how
|
/// How many times this actor has been preempted for running past its
|
||||||
/// many times the actor was preempted for exceeding its slice. Resets on
|
/// scheduling timeslice. Counts only since the actor's current start (a
|
||||||
/// restart (per-incarnation, D7).
|
/// supervisor restart begins a fresh count).
|
||||||
pub overruns: u64,
|
pub overruns: u64,
|
||||||
/// Messages this actor has received (dequeued) this incarnation (RFC 016
|
/// How many messages this actor has received (taken off its inbox), since
|
||||||
/// Chunk 2) — answers "is this actor a hotspot / draining slower than its
|
/// its current start. Useful for spotting an actor whose mailbox is
|
||||||
/// mailbox fills." Counts received, not sent (D4). Per-incarnation (D7).
|
/// filling up faster than it can drain it: compare this against
|
||||||
|
/// `mailbox_depth` over time.
|
||||||
pub messages_received: u64,
|
pub messages_received: u64,
|
||||||
/// Approximate on-CPU cycles this incarnation has consumed (RFC 016 Chunk 2)
|
/// Approximate CPU cycles this actor has spent running, since its current
|
||||||
/// — a reductions-like work metric for relative comparison. Always 0 unless
|
/// start. A relative measure for comparing actors against each other, not
|
||||||
/// the `budget-accounting` feature is enabled (it costs an RDTSC per resume,
|
/// an absolute or wall-clock figure. Always 0 unless the crate's
|
||||||
/// D6). Per-incarnation (D7).
|
/// `budget-accounting` feature is enabled, since measuring it costs a
|
||||||
|
/// timestamp read on every resume.
|
||||||
pub budget_cycles: u64,
|
pub budget_cycles: u64,
|
||||||
|
/// RFC 019 §8 — this actor's stack, as the runtime sees it. All fields
|
||||||
|
/// are lock-free atomic reads, coherent for this incarnation via the
|
||||||
|
/// same generation check as the counters above. Exact RSS is
|
||||||
|
/// deliberately absent: `mincore` is debug tooling, never a runtime
|
||||||
|
/// path.
|
||||||
|
pub stack: StackInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A whole-runtime snapshot. See the module docs for the D2 tearing model.
|
/// RFC 019 §8 — per-actor stack introspection. Sizes are page-rounded, as
|
||||||
|
/// [`Stack::new`](crate::stack::Stack::new) rounds them.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct StackInfo {
|
||||||
|
/// Usable stack size ([`SpawnOpts::stack_reserve`]
|
||||||
|
/// (crate::SpawnOpts::stack_reserve) or the Config/default).
|
||||||
|
pub reserve: usize,
|
||||||
|
/// PROT_NONE guard below the usable region.
|
||||||
|
pub guard: usize,
|
||||||
|
/// Sampled high-water depth in bytes: `top − lowest saved sp`. Sampled,
|
||||||
|
/// not exact — the context save at yields/parks/preemptions is the
|
||||||
|
/// sampler (RFC 019 §2), so a spike the actor never yielded inside is
|
||||||
|
/// invisible. 0 depth means "never descheduled at any depth", not
|
||||||
|
/// "never ran".
|
||||||
|
pub depth_high_water: usize,
|
||||||
|
/// Parks on this incarnation since its last shrink (or since install if
|
||||||
|
/// it has never shrunk) — the §3 cooldown counter, live.
|
||||||
|
pub parks_since_shrink: u32,
|
||||||
|
/// §3 shrinks performed on this incarnation.
|
||||||
|
pub shrinks: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A snapshot of every actor in the runtime at (approximately) one moment.
|
||||||
|
/// See the module docs' "Consistency" section for what "approximately" means
|
||||||
|
/// here.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RuntimeSnapshot {
|
pub struct RuntimeSnapshot {
|
||||||
pub format_version: u16,
|
pub format_version: u16,
|
||||||
pub actors: Vec<ActorInfo>,
|
pub actors: Vec<ActorInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Snapshot every live (and `Done`-but-not-yet-reclaimed) actor on the slab.
|
/// Every actor that currently exists: running, queued, parked, or finished
|
||||||
/// O(n) over the slot table, running with preemption disabled (like every
|
/// but not yet cleaned up. Cheap and lock-free per actor; see the module
|
||||||
/// runtime primitive) but holding no lock across the scan. Panics outside
|
/// docs for what "approximately one moment" means for the result as a whole.
|
||||||
/// `Runtime::run()`; callable from actor code and the run thread.
|
/// Panics if called outside [`run`](crate::run).
|
||||||
pub fn snapshot() -> RuntimeSnapshot {
|
pub fn snapshot() -> RuntimeSnapshot {
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| {
|
||||||
// Phase A: one registry-leaf pass for names + mailbox depth, released
|
// First pass: one registry lock to collect names + mailbox depth for
|
||||||
// before any cold leaf (no two Leaves at once).
|
// every actor, released before touching any per-actor lock below.
|
||||||
let mail = inner.registry.lock().introspect_map();
|
let mail = inner.registry.lock().introspect_map();
|
||||||
|
|
||||||
// Phase B: lock-free slab scan; per-slot cold leaf only to copy cold
|
// Second pass: walk the actor table. Each actor's scheduling state is
|
||||||
// fields. Tearing across slots is intentional (D2).
|
// a lock-free word load; only copying its other fields takes a brief
|
||||||
|
// per-actor lock. Tearing across actors is expected here (see the
|
||||||
|
// module docs' "Consistency" section).
|
||||||
let mut actors = Vec::new();
|
let mut actors = Vec::new();
|
||||||
for (idx, slot) in inner.slots.iter().enumerate() {
|
for (idx, slot) in inner.slots.iter().enumerate() {
|
||||||
let idx = idx as u32;
|
let idx = idx as u32;
|
||||||
@@ -124,12 +239,34 @@ pub fn snapshot() -> RuntimeSnapshot {
|
|||||||
actors.push(info);
|
actors.push(info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RuntimeSnapshot { format_version: SNAPSHOT_FORMAT_VERSION, actors }
|
RuntimeSnapshot {
|
||||||
|
format_version: SNAPSHOT_FORMAT_VERSION,
|
||||||
|
actors,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A coherent view of exactly one actor, or `None` if `pid` does not name a
|
||||||
|
/// currently-live entry: it is stale (that actor has already exited and its
|
||||||
|
/// slot was reused by another), out of range, or was never a real pid at
|
||||||
|
/// all. Unlike [`snapshot`], every field of the result describes the same
|
||||||
|
/// instant, since there is only one actor to read.
|
||||||
|
/// The stack shape `(reserve, guard)` of a live actor, page-rounded — the
|
||||||
|
/// RFC 019 introspection surface's first field (depth sampling and shrink
|
||||||
|
/// counters land with the shrink machinery). `None` if `pid` no longer names
|
||||||
|
/// a live actor. Takes the actor's cold lock briefly; debugging/assertion
|
||||||
|
/// use, not a hot-path call.
|
||||||
|
pub fn stack_shape(pid: Pid) -> Option<(usize, usize)> {
|
||||||
|
with_runtime(|inner| {
|
||||||
|
let slot = inner.slot_at(pid)?;
|
||||||
|
let cold = slot.cold.lock();
|
||||||
|
if slot.generation() != pid.generation() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
cold.actor.as_ref().map(|a| a.stack.shape())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Coherent view of a single actor, or `None` if the pid is stale, out of
|
|
||||||
/// range, or names a Vacant slot.
|
|
||||||
pub fn actor_info(pid: Pid) -> Option<ActorInfo> {
|
pub fn actor_info(pid: Pid) -> Option<ActorInfo> {
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| {
|
||||||
let slot = inner.slot_at(pid)?;
|
let slot = inner.slot_at(pid)?;
|
||||||
@@ -141,11 +278,12 @@ pub fn actor_info(pid: Pid) -> Option<ActorInfo> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build one `ActorInfo` for slot `idx`, or `None` if Vacant or
|
/// Build one `ActorInfo` for slot `idx`, or `None` if the slot is empty or
|
||||||
/// racing-reclaimed. State is classified from a lock-free word load (the torn
|
/// was reclaimed while this read was in progress. The scheduling state comes
|
||||||
/// read); the cold lock then pins the generation (reclaim bumps it under that
|
/// from a lock-free word load (the source of the tearing described in the
|
||||||
/// same lock) so the cold fields are coherent for this incarnation. `mail` is
|
/// module docs); the per-actor lock then confirms the actor has not since
|
||||||
/// this slot's registry entry, if any.
|
/// exited and been replaced, so the rest of the fields are coherent for this
|
||||||
|
/// exact actor. `mail` is this slot's registry entry, if any.
|
||||||
fn read_slot(slot: &Slot, idx: u32, mail: Option<&MailboxInfo>) -> Option<ActorInfo> {
|
fn read_slot(slot: &Slot, idx: u32, mail: Option<&MailboxInfo>) -> Option<ActorInfo> {
|
||||||
let w = slot.state_word();
|
let w = slot.state_word();
|
||||||
let state = classify(w)?;
|
let state = classify(w)?;
|
||||||
@@ -153,10 +291,11 @@ fn read_slot(slot: &Slot, idx: u32, mail: Option<&MailboxInfo>) -> Option<ActorI
|
|||||||
let pid = Pid::new(idx, gen);
|
let pid = Pid::new(idx, gen);
|
||||||
|
|
||||||
let cold = slot.cold.lock();
|
let cold = slot.cold.lock();
|
||||||
// If the generation moved between the lock-free load and acquiring the cold
|
// If the generation moved between the lock-free load and acquiring the
|
||||||
// lock, the slot was reclaimed (and maybe reused) — drop it rather than mix
|
// per-actor lock, this actor exited (and the slot may already hold a new
|
||||||
// one incarnation's state with another's cold data. (ps semantics: a racing
|
// one). Drop it rather than mix one actor's state with another's data; a
|
||||||
// actor may simply be missed mid-scan.)
|
// racing actor may simply be missed by this scan, which is expected (see
|
||||||
|
// the module docs' "Consistency" section).
|
||||||
if word_gen(slot.state_word()) != gen {
|
if word_gen(slot.state_word()) != gen {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -172,7 +311,15 @@ fn read_slot(slot: &Slot, idx: u32, mail: Option<&MailboxInfo>) -> Option<ActorI
|
|||||||
let joiners = cold.waiters.len() as u32;
|
let joiners = cold.waiters.len() as u32;
|
||||||
drop(cold);
|
drop(cold);
|
||||||
|
|
||||||
// Counters are hot-region atomics, read lock-free (RFC 016 Chunk 2).
|
// Counters are plain atomics, read lock-free.
|
||||||
|
let (reserve, guard, top, hwm, parks_since_shrink, shrinks) = slot.stack_introspect();
|
||||||
|
let stack = StackInfo {
|
||||||
|
reserve,
|
||||||
|
guard,
|
||||||
|
depth_high_water: top.saturating_sub(hwm),
|
||||||
|
parks_since_shrink,
|
||||||
|
shrinks,
|
||||||
|
};
|
||||||
let overruns = slot.overruns();
|
let overruns = slot.overruns();
|
||||||
let messages_received = slot.messages_received();
|
let messages_received = slot.messages_received();
|
||||||
let budget_cycles = slot.budget_cycles();
|
let budget_cycles = slot.budget_cycles();
|
||||||
@@ -198,45 +345,55 @@ fn read_slot(slot: &Slot, idx: u32, mail: Option<&MailboxInfo>) -> Option<ActorI
|
|||||||
overruns,
|
overruns,
|
||||||
messages_received,
|
messages_received,
|
||||||
budget_cycles,
|
budget_cycles,
|
||||||
|
stack,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Chunk 3 — tree view (pure derivation over a Chunk-1 snapshot)
|
// Tree view: a pure derivation over a snapshot
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// One node in the parentage forest. `children` are the actors whose recorded
|
/// One node in the parentage forest returned by [`tree`]. `children` are the
|
||||||
/// parent edge points at this node's pid.
|
/// actors whose recorded parent (see [`ActorInfo::supervisor`]) points at
|
||||||
|
/// this node's actor.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TreeNode {
|
pub struct TreeNode {
|
||||||
pub info: ActorInfo,
|
pub info: ActorInfo,
|
||||||
/// The actor's recorded parent was absent from the snapshot (already
|
/// True if this actor's recorded parent was not found in the snapshot
|
||||||
/// Done/Vacant, or itself a tombstone), so it was re-rooted under the forest
|
/// (it had already exited, or was itself missing), so this node was
|
||||||
/// sentinel rather than dropped — the tree stays total (DECISION D8).
|
/// placed at the top of the forest instead of being dropped. This keeps
|
||||||
|
/// every actor in the snapshot visible somewhere in the tree, even one
|
||||||
|
/// whose parent is gone.
|
||||||
pub orphaned: bool,
|
pub orphaned: bool,
|
||||||
pub children: Vec<TreeNode>,
|
pub children: Vec<TreeNode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The parentage forest. Roots are actors parented at `ROOT_PID` (genuine
|
/// The parentage forest: every actor from a snapshot, arranged by who spawned
|
||||||
/// roots) plus re-rooted orphans. The edge is *spawned-by / parent*, not
|
/// whom. Roots are actors with no parent in the snapshot (including the
|
||||||
/// necessarily supervision (DECISION D9) — see [`ActorInfo::supervisor`].
|
/// run's own root actor) plus any orphaned actors (see [`TreeNode::orphaned`]).
|
||||||
|
/// This mirrors spawn parentage, not necessarily a supervision tree; see
|
||||||
|
/// [`ActorInfo::supervisor`].
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RuntimeTree {
|
pub struct RuntimeTree {
|
||||||
pub format_version: u16,
|
pub format_version: u16,
|
||||||
pub roots: Vec<TreeNode>,
|
pub roots: Vec<TreeNode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Take a live [`snapshot`] and fold it into the parentage forest.
|
/// Take a fresh [`snapshot`] and fold it into the parentage forest.
|
||||||
pub fn tree() -> RuntimeTree {
|
pub fn tree() -> RuntimeTree {
|
||||||
tree_from(snapshot())
|
tree_from(snapshot())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fold an existing snapshot into a forest by grouping each actor under its
|
/// Fold an existing snapshot into a parentage forest by grouping each actor
|
||||||
/// parent pid — a single O(n) pass, no new reads. Exposed separately so a
|
/// under its parent, without taking a new snapshot. Useful if you already
|
||||||
/// consumer that already holds a snapshot (or a synthetic one, in tests) can
|
/// have one (for example, one built in a test, or one you took earlier and
|
||||||
/// derive the tree without a second scan.
|
/// want to inspect again) and want the tree view of it without re-reading
|
||||||
|
/// 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() {
|
||||||
@@ -254,7 +411,7 @@ pub fn tree_from(snap: RuntimeSnapshot) -> RuntimeTree {
|
|||||||
children_of.entry(parent).or_default().push(i);
|
children_of.entry(parent).or_default().push(i);
|
||||||
} else {
|
} else {
|
||||||
// Parent is the forest sentinel (genuine root) or absent from the
|
// Parent is the forest sentinel (genuine root) or absent from the
|
||||||
// snapshot (orphan, D8) — either way a root of the forest.
|
// snapshot (orphan): either way, a root of the forest.
|
||||||
orphaned[i] = parent != ROOT_PID;
|
orphaned[i] = parent != ROOT_PID;
|
||||||
roots.push(i);
|
roots.push(i);
|
||||||
}
|
}
|
||||||
@@ -267,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(
|
||||||
@@ -285,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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,44 +13,68 @@
|
|||||||
//! leaves the actor, no copying through an intermediary thread. Built on
|
//! leaves the actor, no copying through an intermediary thread. Built on
|
||||||
//! these are the conveniences `read(fd, &mut buf)` and `write(fd, &buf)`.
|
//! these are the conveniences `read(fd, &mut buf)` and `write(fd, &buf)`.
|
||||||
//!
|
//!
|
||||||
//! Architecture
|
//! Architecture (RFC 018: driver-enqueues)
|
||||||
//! ============
|
//! =======================================
|
||||||
//! Per `run()`, two OS threads:
|
//! Per `run()`, two OS threads, each a *producer* behind the runtime's
|
||||||
//! - **epoll thread**: owns the epollfd. Loops in `epoll_wait`. On a
|
//! two-call contract — make the actor runnable (`unpark_at`, whose enqueue
|
||||||
//! ready fd, pushes `Completion::FdReady { pid, fd, events }` to the
|
//! tail wakes a parked scheduler), nothing else:
|
||||||
//! shared completion queue and writes the scheduler-wake pipe. On the
|
|
||||||
//! shutdown pipe (also registered in epollfd), exits.
|
|
||||||
//! - **pool thread**: blocks on the request mpsc. Runs the closure
|
|
||||||
//! inside `catch_unwind`, pushes `Completion::Blocking { pid, result }`,
|
|
||||||
//! writes the scheduler-wake pipe.
|
|
||||||
//!
|
//!
|
||||||
//! Both threads share a single `completions: Arc<Mutex<VecDeque<Completion>>>`
|
//! - **epoll thread**: owns `epoll_wait` on the epollfd. On a ready fd it
|
||||||
//! and the same scheduler-wake pipe.
|
//! removes the parked waiter from the shared `waiters` map and DELs the
|
||||||
|
//! fd (both under the waiters lock — see below), then unparks the
|
||||||
|
//! actor directly. On the shutdown pipe (also registered in the
|
||||||
|
//! epollfd), exits.
|
||||||
|
//! - **pool thread**: blocks on the request mpsc. Runs the closure inside
|
||||||
|
//! `catch_unwind`, stashes the result in the actor's slot
|
||||||
|
//! (`pending_io_result`, under the cold lock, generation-checked),
|
||||||
|
//! decrements the runtime's `io_outstanding`, and unparks the actor.
|
||||||
//!
|
//!
|
||||||
//! `epoll_ctl` (register/unregister fd interest) is called by the
|
//! There is no shared completion queue and no wake pipe: each producer
|
||||||
//! scheduler thread *directly* on the epollfd. That's well-defined per
|
//! routes its own completion, so the whole byte-vs-completion visibility
|
||||||
//! `epoll_ctl(2)`: a thread may be calling `epoll_wait` on the epollfd
|
//! discipline of the drain era — and the stranded-completion hazards it
|
||||||
//! while another thread calls `epoll_ctl`. Avoids needing a second mpsc
|
//! defended against — is unrepresentable. Producers reach the runtime
|
||||||
//! and a second wake mechanism.
|
//! through a `Weak<RuntimeInner>`: upgraded per completion (the path is
|
||||||
|
//! syscall-bound; the refcount op is noise) and avoiding an Arc cycle
|
||||||
|
//! through `RuntimeInner::io`.
|
||||||
|
//!
|
||||||
|
//! `epoll_ctl` (register fd interest) is called by the scheduler thread
|
||||||
|
//! directly on the epollfd. That's well-defined per `epoll_ctl(2)`: a
|
||||||
|
//! thread may be calling `epoll_wait` on the epollfd while another thread
|
||||||
|
//! calls `epoll_ctl`.
|
||||||
//!
|
//!
|
||||||
//! Epoll mode
|
//! Epoll mode
|
||||||
//! ==========
|
//! ==========
|
||||||
//! Level-triggered with EPOLLONESHOT. After a wakeup the kernel
|
//! Level-triggered with EPOLLONESHOT. After a wakeup the kernel
|
||||||
//! auto-disarms the fd, so we never get two wakeups for one
|
//! auto-disarms the fd, so we never get two wakeups for one
|
||||||
//! `wait_readable` call. The scheduler explicitly `EPOLL_CTL_DEL`s the fd
|
//! `wait_readable` call. The epoll thread explicitly `EPOLL_CTL_DEL`s the
|
||||||
//! on completion to free the slot for re-registration. Net effect: each
|
//! fd on readiness to free the slot for re-registration. Net effect: each
|
||||||
//! `wait_readable(fd)` is one ADD, one wakeup, one DEL — symmetric and
|
//! `wait_readable(fd)` is one ADD, one wakeup, one DEL — symmetric and
|
||||||
//! stateless between calls.
|
//! stateless between calls.
|
||||||
//!
|
//!
|
||||||
|
//! ## The waiters lock is the ADD/DEL serialization
|
||||||
|
//!
|
||||||
|
//! Registration (scheduler thread: check-vacant, defensive DEL, ADD,
|
||||||
|
//! insert) and readiness consumption (epoll thread: remove, DEL) each run
|
||||||
|
//! entirely under the `waiters` mutex. This is what makes the
|
||||||
|
//! oneshot-rearm race unrepresentable: a woken actor re-registering the
|
||||||
|
//! same fd cannot interleave with the epoll thread's DEL for the *previous*
|
||||||
|
//! registration — whichever takes the lock second sees a consistent
|
||||||
|
//! kernel-side state. Lock order: `io` (the runtime's outer mutex, held by
|
||||||
|
//! scheduler-side callers) → `waiters` → slot/queue leaves via `unpark_at`.
|
||||||
|
//! The epoll thread takes `waiters` without `io` — it must never take
|
||||||
|
//! `io`, both for lock-order hygiene and because teardown holds `io` while
|
||||||
|
//! joining it.
|
||||||
|
//!
|
||||||
//! Fd hygiene
|
//! Fd hygiene
|
||||||
//! ==========
|
//! ==========
|
||||||
//! An actor stopped while waiting on an fd unwinds out of `wait_fd`'s park;
|
//! An actor stopped while waiting on an fd unwinds out of `wait_fd`'s park;
|
||||||
//! a drop guard there (armed after a successful register, forgotten on a
|
//! a drop guard there (armed after a successful register, forgotten on a
|
||||||
//! normal wake) removes the `waiters` entry iff it is still that wait's
|
//! normal wake) calls [`IoThread::cancel_waiter`], which removes the
|
||||||
//! `(pid, epoch)` and only then `EPOLL_CTL_DEL`s the fd — an entry already
|
//! `waiters` entry iff it is still that wait's `(pid, epoch)` and only then
|
||||||
//! consumed by a racing `FdReady` means the fd may carry someone else's
|
//! `EPOLL_CTL_DEL`s the fd — an entry already consumed by the epoll thread
|
||||||
//! fresh registration, which must be left alone. `epoll_register` keeps a
|
//! means the fd may carry someone else's fresh registration, which must be
|
||||||
//! defensive bare DEL before ADD as belt-and-braces.
|
//! left alone. `epoll_register` keeps a defensive bare DEL before ADD as
|
||||||
|
//! belt-and-braces.
|
||||||
//!
|
//!
|
||||||
//! Buffers used with `read`/`write` should be on fds opened with
|
//! Buffers used with `read`/`write` should be on fds opened with
|
||||||
//! `O_NONBLOCK`. If they aren't, the syscall may block the scheduler
|
//! `O_NONBLOCK`. If they aren't, the syscall may block the scheduler
|
||||||
@@ -68,13 +92,14 @@
|
|||||||
//! they have no equivalent panic-propagation path.
|
//! they have no equivalent panic-propagation path.
|
||||||
|
|
||||||
use crate::pid::Pid;
|
use crate::pid::Pid;
|
||||||
|
use crate::runtime::RuntimeInner;
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::HashMap;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::os::fd::RawFd;
|
use std::os::fd::RawFd;
|
||||||
use std::panic;
|
use std::panic;
|
||||||
use std::sync::mpsc;
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{mpsc, Arc, Mutex, Weak};
|
||||||
use std::thread::JoinHandle as OsJoinHandle;
|
use std::thread::JoinHandle as OsJoinHandle;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -86,45 +111,31 @@ use std::thread::JoinHandle as OsJoinHandle;
|
|||||||
pub type IoResult = Result<Box<dyn Any + Send>, Box<dyn Any + Send>>;
|
pub type IoResult = Result<Box<dyn Any + Send>, Box<dyn Any + Send>>;
|
||||||
|
|
||||||
struct Request {
|
struct Request {
|
||||||
/// The submitter's park-epoch — carried through to the `Blocking`
|
/// The submitter's park-epoch — the eventual wake is epoch-matched.
|
||||||
/// completion so the wake is epoch-matched.
|
|
||||||
epoch: u32,
|
epoch: u32,
|
||||||
pid: Pid,
|
pid: Pid,
|
||||||
/// The work to perform. Returns the wire-form result directly.
|
/// The work to perform. Returns the wire-form result directly.
|
||||||
work: Box<dyn FnOnce() -> IoResult + Send>,
|
work: Box<dyn FnOnce() -> IoResult + Send>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Completion message from either IO thread back to the scheduler.
|
/// The parked-waiter map, shared between scheduler-side registration and
|
||||||
pub enum Completion {
|
/// the epoll thread's readiness consumption. See the module docs on why
|
||||||
/// A `block_on_io` closure has finished (Ok = return value, Err = panic
|
/// this single lock is the ADD/DEL serialization.
|
||||||
/// payload).
|
type Waiters = Arc<Mutex<HashMap<RawFd, (Pid, u32)>>>;
|
||||||
Blocking { pid: Pid, epoch: u32, result: IoResult },
|
|
||||||
/// An fd registered via `wait_readable`/`wait_writable` is ready. The
|
|
||||||
/// scheduler looks up the parked pid in `waiters`, unparks it, and
|
|
||||||
/// removes the entry. `pid` isn't in this variant because the epoll
|
|
||||||
/// thread doesn't have access to the `waiters` map; the scheduler
|
|
||||||
/// thread owns that.
|
|
||||||
FdReady { fd: RawFd, events: u32 },
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// IoThread — created per `run()`, owned by `SchedulerState`.
|
// IoThread — created per `run()`, owned by `RuntimeInner::io`.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
pub struct IoThread {
|
pub struct IoThread {
|
||||||
// ----- Channels & queues -----
|
|
||||||
|
|
||||||
/// Submission queue into the blocking-work pool.
|
/// Submission queue into the blocking-work pool.
|
||||||
tx: mpsc::Sender<Request>,
|
tx: mpsc::Sender<Request>,
|
||||||
/// Shared completion queue, fed by both the pool and the epoll thread.
|
/// One parked actor per registered fd. Populated by `epoll_register`,
|
||||||
completions: Arc<Mutex<VecDeque<Completion>>>,
|
/// consumed by the epoll thread on readiness or `cancel_waiter` on an
|
||||||
/// Pipe the scheduler polls in its idle path. Both IO threads write to
|
/// unwound wait.
|
||||||
/// `wake_write` after pushing a completion.
|
waiters: Waiters,
|
||||||
wake_read: RawFd,
|
|
||||||
wake_write: RawFd,
|
|
||||||
|
|
||||||
// ----- 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,
|
||||||
@@ -133,39 +144,24 @@ pub struct IoThread {
|
|||||||
/// shutdown.
|
/// shutdown.
|
||||||
shutdown_read: RawFd,
|
shutdown_read: RawFd,
|
||||||
shutdown_write: RawFd,
|
shutdown_write: RawFd,
|
||||||
/// One parked actor per registered fd. Populated by `wait_readable` /
|
|
||||||
/// `wait_writable` and drained by the scheduler when a `FdReady`
|
|
||||||
/// completion is processed.
|
|
||||||
pub waiters: HashMap<RawFd, (Pid, u32)>,
|
|
||||||
|
|
||||||
// ----- Threads -----
|
// ----- Threads -----
|
||||||
|
|
||||||
pool_thread: Option<OsJoinHandle<()>>,
|
pool_thread: Option<OsJoinHandle<()>>,
|
||||||
epoll_thread: Option<OsJoinHandle<()>>,
|
epoll_thread: Option<OsJoinHandle<()>>,
|
||||||
|
|
||||||
/// Number of `block_on_io` requests in-flight. Used by the scheduler's
|
|
||||||
/// idle path to decide whether to wait on the pipe or exit. Fd waits
|
|
||||||
/// are not counted here; they're counted by `waiters.len()`.
|
|
||||||
pub outstanding: u32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IoThread {
|
impl IoThread {
|
||||||
pub fn start() -> io::Result<Self> {
|
/// Start the pool and epoll threads. `rt` is the producers' route back
|
||||||
// Scheduler-facing wake pipe.
|
/// into the runtime (slot table + unpark protocol); a `Weak` so the
|
||||||
let (wake_read, wake_write) = make_pipe()?;
|
/// `RuntimeInner → IoThread → RuntimeInner` cycle never forms.
|
||||||
// Pool submission channel + shared completion queue.
|
pub(crate) fn start(rt: Weak<RuntimeInner>) -> io::Result<Self> {
|
||||||
|
// Pool submission channel.
|
||||||
let (tx, rx) = mpsc::channel::<Request>();
|
let (tx, rx) = mpsc::channel::<Request>();
|
||||||
let completions: Arc<Mutex<VecDeque<Completion>>> =
|
let waiters: Waiters = Arc::new(Mutex::new(HashMap::new()));
|
||||||
Arc::new(Mutex::new(VecDeque::new()));
|
|
||||||
|
|
||||||
// Epoll machinery.
|
// Epoll machinery.
|
||||||
let epollfd = unsafe { libc::epoll_create1(libc::EPOLL_CLOEXEC) };
|
let epollfd = unsafe { libc::epoll_create1(libc::EPOLL_CLOEXEC) };
|
||||||
if epollfd < 0 {
|
if epollfd < 0 {
|
||||||
// Best-effort fd cleanup before bailing.
|
|
||||||
unsafe {
|
|
||||||
libc::close(wake_read);
|
|
||||||
libc::close(wake_write);
|
|
||||||
}
|
|
||||||
return Err(io::Error::last_os_error());
|
return Err(io::Error::last_os_error());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,8 +170,6 @@ impl IoThread {
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
unsafe {
|
unsafe {
|
||||||
libc::close(epollfd);
|
libc::close(epollfd);
|
||||||
libc::close(wake_read);
|
|
||||||
libc::close(wake_write);
|
|
||||||
}
|
}
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
@@ -202,42 +196,37 @@ impl IoThread {
|
|||||||
libc::close(epollfd);
|
libc::close(epollfd);
|
||||||
libc::close(shutdown_read);
|
libc::close(shutdown_read);
|
||||||
libc::close(shutdown_write);
|
libc::close(shutdown_write);
|
||||||
libc::close(wake_read);
|
|
||||||
libc::close(wake_write);
|
|
||||||
}
|
}
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spawn pool thread.
|
// Spawn pool thread.
|
||||||
let pool_comps = completions.clone();
|
let pool_rt = rt.clone();
|
||||||
let pool_thread = std::thread::Builder::new()
|
let pool_thread = std::thread::Builder::new()
|
||||||
.name("smarm-io-pool".into())
|
.name("smarm-io-pool".into())
|
||||||
.spawn(move || pool_loop(rx, pool_comps, wake_write))?;
|
.spawn(move || pool_loop(rx, pool_rt))?;
|
||||||
|
|
||||||
// Spawn epoll thread.
|
// Spawn epoll thread.
|
||||||
let epoll_comps = completions.clone();
|
let epoll_waiters = waiters.clone();
|
||||||
let epoll_thread = std::thread::Builder::new()
|
let epoll_thread = std::thread::Builder::new()
|
||||||
.name("smarm-io-epoll".into())
|
.name("smarm-io-epoll".into())
|
||||||
.spawn(move || epoll_loop(epollfd, epoll_comps, wake_write))?;
|
.spawn(move || epoll_loop(epollfd, epoll_waiters, rt))?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
tx,
|
tx,
|
||||||
completions,
|
waiters,
|
||||||
wake_read,
|
|
||||||
wake_write,
|
|
||||||
epollfd,
|
epollfd,
|
||||||
shutdown_read,
|
shutdown_read,
|
||||||
shutdown_write,
|
shutdown_write,
|
||||||
waiters: HashMap::new(),
|
|
||||||
pool_thread: Some(pool_thread),
|
pool_thread: Some(pool_thread),
|
||||||
epoll_thread: Some(epoll_thread),
|
epoll_thread: Some(epoll_thread),
|
||||||
outstanding: 0,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hand a request to the pool. Increments `outstanding`.
|
/// Hand a request to the pool. The caller (scheduler.rs) increments
|
||||||
|
/// `io_outstanding` BEFORE calling — the pool decrements on completion,
|
||||||
|
/// and an increment that trailed the completion would underflow.
|
||||||
pub fn submit(&mut self, pid: Pid, epoch: u32, work: Box<dyn FnOnce() -> IoResult + Send>) {
|
pub fn submit(&mut self, pid: Pid, epoch: u32, work: Box<dyn FnOnce() -> IoResult + Send>) {
|
||||||
self.outstanding += 1;
|
|
||||||
// Send can only fail if the pool has hung up, which only happens
|
// Send can only fail if the pool has hung up, which only happens
|
||||||
// on shutdown. submit during shutdown is a bug.
|
// on shutdown. submit during shutdown is a bug.
|
||||||
if self.tx.send(Request { pid, epoch, work }).is_err() {
|
if self.tx.send(Request { pid, epoch, work }).is_err() {
|
||||||
@@ -245,39 +234,13 @@ impl IoThread {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drain every available completion. Caller (the scheduler) routes the
|
|
||||||
/// results and updates `outstanding` / `waiters` accordingly.
|
|
||||||
pub fn drain_completions(&mut self) -> Vec<Completion> {
|
|
||||||
let mut q = match self.completions.lock() {
|
|
||||||
Ok(g) => g,
|
|
||||||
Err(e) => panic!("smarm: io completions lock poisoned (core corrupt): {e}"),
|
|
||||||
};
|
|
||||||
let mut out = Vec::with_capacity(q.len());
|
|
||||||
while let Some(c) = q.pop_front() {
|
|
||||||
out.push(c);
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn wake_fd(&self) -> RawFd {
|
|
||||||
self.wake_read
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write the wake pipe directly: rouse every scheduler thread blocked in
|
|
||||||
/// its idle `poll_wake`. Used by the terminal (AllDone) path — an idle
|
|
||||||
/// sibling may be blocked on a snapshot that nothing will ever refresh
|
|
||||||
/// (an orphaned timer deadline, or `io_outstanding` from a waiter that
|
|
||||||
/// was stop-cancelled and so never produces a completion).
|
|
||||||
pub fn wake(&self) {
|
|
||||||
wake_scheduler(self.wake_write);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register interest in `fd` becoming readable/writable; record `pid`
|
/// Register interest in `fd` becoming readable/writable; record `pid`
|
||||||
/// as the parked waiter. The epoll thread will push a `FdReady`
|
/// as the parked waiter. The epoll thread unparks it on readiness.
|
||||||
/// completion when the kernel signals.
|
/// The caller increments `io_fd_waiters` BEFORE calling (mirror of
|
||||||
|
/// `submit`'s contract) and decrements it again if this errors.
|
||||||
///
|
///
|
||||||
/// EPOLLONESHOT: one wakeup per registration. The scheduler must
|
/// EPOLLONESHOT: one wakeup per registration; the epoll thread DELs on
|
||||||
/// `epoll_del` on completion to free the slot for re-registration.
|
/// readiness, `cancel_waiter` DELs on an unwound wait.
|
||||||
pub fn epoll_register(
|
pub fn epoll_register(
|
||||||
&mut self,
|
&mut self,
|
||||||
fd: RawFd,
|
fd: RawFd,
|
||||||
@@ -286,20 +249,24 @@ impl IoThread {
|
|||||||
readable: bool,
|
readable: bool,
|
||||||
writable: bool,
|
writable: bool,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
|
let mut waiters = match self.waiters.lock() {
|
||||||
|
Ok(g) => g,
|
||||||
|
Err(e) => panic!("smarm: io waiters lock poisoned (core corrupt): {e}"),
|
||||||
|
};
|
||||||
// Two actors waiting on the same fd would be a misuse: the kernel
|
// Two actors waiting on the same fd would be a misuse: the kernel
|
||||||
// delivers exactly one EPOLLONESHOT wakeup, so the second waiter
|
// delivers exactly one EPOLLONESHOT wakeup, so the second waiter
|
||||||
// would hang. Reject up front.
|
// would hang. Reject up front.
|
||||||
if self.waiters.contains_key(&fd) {
|
if waiters.contains_key(&fd) {
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
io::ErrorKind::AlreadyExists,
|
io::ErrorKind::AlreadyExists,
|
||||||
"fd already has a parked waiter",
|
"fd already has a parked waiter",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Belt-and-braces: the unwind guard in `wait_fd` is responsible for
|
// Belt-and-braces: `cancel_waiter` is responsible for cleaning up a
|
||||||
// cleaning up a stopped waiter's registration, but a bare DEL is
|
// stopped waiter's registration, but a bare DEL is harmless if the
|
||||||
// harmless if the fd isn't registered (ENOENT) and removes any leak
|
// fd isn't registered (ENOENT) and removes any leak a path we
|
||||||
// a path we haven't thought of might leave behind.
|
// haven't thought of might leave behind.
|
||||||
unsafe {
|
unsafe {
|
||||||
libc::epoll_ctl(self.epollfd, libc::EPOLL_CTL_DEL, fd, std::ptr::null_mut());
|
libc::epoll_ctl(self.epollfd, libc::EPOLL_CTL_DEL, fd, std::ptr::null_mut());
|
||||||
}
|
}
|
||||||
@@ -315,26 +282,35 @@ 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());
|
||||||
}
|
}
|
||||||
self.waiters.insert(fd, (pid, epoch));
|
waiters.insert(fd, (pid, epoch));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove `fd` from the epollfd. Called by the scheduler after a
|
/// Remove `fd`'s waiter iff it is still `(pid, epoch)`, DELing the fd
|
||||||
/// `FdReady` completion, so the next `wait_readable(fd)` can ADD again.
|
/// from the epollfd in the same critical section. Returns whether the
|
||||||
///
|
/// entry was removed (the caller then decrements `io_fd_waiters`).
|
||||||
/// Does NOT touch `waiters` — that's the scheduler's bookkeeping; this
|
/// `false` means the epoll thread consumed the registration first —
|
||||||
/// is purely the kernel-side cleanup.
|
/// the fd may already carry someone else's fresh ADD; hands off.
|
||||||
pub fn epoll_deregister(&mut self, fd: RawFd) {
|
pub fn cancel_waiter(&mut self, fd: RawFd, pid: Pid, epoch: u32) -> bool {
|
||||||
|
let mut waiters = match self.waiters.lock() {
|
||||||
|
Ok(g) => g,
|
||||||
|
Err(e) => panic!("smarm: io waiters lock poisoned (core corrupt): {e}"),
|
||||||
|
};
|
||||||
|
if waiters.get(&fd) == Some(&(pid, epoch)) {
|
||||||
|
waiters.remove(&fd);
|
||||||
// EPOLL_CTL_DEL of an already-removed fd returns ENOENT; ignore.
|
// EPOLL_CTL_DEL of an already-removed fd returns ENOENT; ignore.
|
||||||
unsafe {
|
unsafe {
|
||||||
libc::epoll_ctl(self.epollfd, libc::EPOLL_CTL_DEL, fd, std::ptr::null_mut());
|
libc::epoll_ctl(self.epollfd, libc::EPOLL_CTL_DEL, fd, std::ptr::null_mut());
|
||||||
}
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,7 +330,10 @@ impl Drop for IoThread {
|
|||||||
let real_tx = std::mem::replace(&mut self.tx, dead_tx);
|
let real_tx = std::mem::replace(&mut self.tx, dead_tx);
|
||||||
drop(real_tx);
|
drop(real_tx);
|
||||||
|
|
||||||
// 3. Join both threads.
|
// 3. Join both threads. Safe even while the caller holds the
|
||||||
|
// runtime's `io` mutex: neither thread ever takes it (they reach
|
||||||
|
// the runtime through a Weak they upgrade per completion, and
|
||||||
|
// the epoll thread's only lock is `waiters`).
|
||||||
if let Some(h) = self.epoll_thread.take() {
|
if let Some(h) = self.epoll_thread.take() {
|
||||||
let _ = h.join();
|
let _ = h.join();
|
||||||
}
|
}
|
||||||
@@ -367,8 +346,6 @@ impl Drop for IoThread {
|
|||||||
libc::close(self.epollfd);
|
libc::close(self.epollfd);
|
||||||
libc::close(self.shutdown_read);
|
libc::close(self.shutdown_read);
|
||||||
libc::close(self.shutdown_write);
|
libc::close(self.shutdown_write);
|
||||||
libc::close(self.wake_read);
|
|
||||||
libc::close(self.wake_write);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -379,36 +356,38 @@ impl Drop for IoThread {
|
|||||||
const SHUTDOWN_EPOLL_TOKEN: u64 = u64::MAX;
|
const SHUTDOWN_EPOLL_TOKEN: u64 = u64::MAX;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Pool loop
|
// Pool loop (producer: Blocking completions)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
fn pool_loop(
|
fn pool_loop(rx: mpsc::Receiver<Request>, rt: Weak<RuntimeInner>) {
|
||||||
rx: mpsc::Receiver<Request>,
|
|
||||||
completions: Arc<Mutex<VecDeque<Completion>>>,
|
|
||||||
wake_write: RawFd,
|
|
||||||
) {
|
|
||||||
while let Ok(Request { pid, epoch, work }) = rx.recv() {
|
while let Ok(Request { pid, epoch, work }) = rx.recv() {
|
||||||
let result: IoResult = match panic::catch_unwind(panic::AssertUnwindSafe(work)) {
|
let result: IoResult = match panic::catch_unwind(panic::AssertUnwindSafe(work)) {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
Err(payload) => Err(payload),
|
Err(payload) => Err(payload),
|
||||||
};
|
};
|
||||||
match completions.lock() {
|
let Some(inner) = rt.upgrade() else { return };
|
||||||
Ok(mut g) => g.push_back(Completion::Blocking { pid, epoch, result }),
|
// Stash the result under the cold lock (generation-checked: an
|
||||||
Err(e) => panic!("smarm: io completions lock poisoned (core corrupt): {e}"),
|
// actor stopped with the op in flight discards it), decrement the
|
||||||
|
// in-flight count, then wake through the epoch-matched unpark. The
|
||||||
|
// unpark's enqueue tail wakes a parked scheduler; the actor stays
|
||||||
|
// `live` until it resumes and finalizes, so the decrement's
|
||||||
|
// ordering against the termination verdict is not load-bearing.
|
||||||
|
if let Some(slot) = inner.slot_at(pid) {
|
||||||
|
let mut cold = slot.cold.lock();
|
||||||
|
if slot.generation() == pid.generation() {
|
||||||
|
cold.pending_io_result = Some(result);
|
||||||
}
|
}
|
||||||
wake_scheduler(wake_write);
|
}
|
||||||
|
inner.io_outstanding.fetch_sub(1, Ordering::AcqRel);
|
||||||
|
inner.unpark_at(pid, epoch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Epoll loop
|
// Epoll loop (producer: FdReady completions)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
fn epoll_loop(
|
fn epoll_loop(epollfd: RawFd, waiters: Waiters, rt: Weak<RuntimeInner>) {
|
||||||
epollfd: RawFd,
|
|
||||||
completions: Arc<Mutex<VecDeque<Completion>>>,
|
|
||||||
wake_write: RawFd,
|
|
||||||
) {
|
|
||||||
// Buffer for epoll_wait. 64 is plenty for our scale; if a real load
|
// Buffer for epoll_wait. 64 is plenty for our scale; if a real load
|
||||||
// appears that needs more, this is a one-line change.
|
// appears that needs more, this is a one-line change.
|
||||||
const MAX_EVENTS: usize = 64;
|
const MAX_EVENTS: usize = 64;
|
||||||
@@ -416,12 +395,7 @@ fn epoll_loop(
|
|||||||
|
|
||||||
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 {
|
||||||
@@ -436,29 +410,36 @@ fn epoll_loop(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut shutdown_requested = false;
|
let mut shutdown_requested = false;
|
||||||
let mut pushed_any = false;
|
|
||||||
{
|
|
||||||
let mut q = match completions.lock() {
|
|
||||||
Ok(g) => g,
|
|
||||||
Err(e) => panic!("smarm: io completions lock poisoned (core corrupt): {e}"),
|
|
||||||
};
|
|
||||||
for ev in events.iter().take(n as usize) {
|
for ev in events.iter().take(n as usize) {
|
||||||
if ev.u64 == SHUTDOWN_EPOLL_TOKEN {
|
if ev.u64 == SHUTDOWN_EPOLL_TOKEN {
|
||||||
shutdown_requested = true;
|
shutdown_requested = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let fd = ev.u64 as RawFd;
|
let fd = ev.u64 as RawFd;
|
||||||
let evs = ev.events;
|
// Consume the registration: remove + DEL under the waiters
|
||||||
q.push_back(Completion::FdReady {
|
// lock (the ADD/DEL serialization — see module docs). A
|
||||||
fd,
|
// vanished entry means `cancel_waiter` beat us: the wake is
|
||||||
events: evs,
|
// already moot.
|
||||||
});
|
let entry = {
|
||||||
pushed_any = true;
|
let mut w = match waiters.lock() {
|
||||||
|
Ok(g) => g,
|
||||||
|
Err(e) => {
|
||||||
|
panic!("smarm: io waiters lock poisoned (core corrupt): {e}")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let entry = w.remove(&fd);
|
||||||
|
if entry.is_some() {
|
||||||
|
unsafe {
|
||||||
|
libc::epoll_ctl(epollfd, libc::EPOLL_CTL_DEL, fd, std::ptr::null_mut());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
entry
|
||||||
if pushed_any {
|
};
|
||||||
wake_scheduler(wake_write);
|
if let Some((pid, epoch)) = entry {
|
||||||
|
let Some(inner) = rt.upgrade() else { return };
|
||||||
|
inner.io_fd_waiters.fetch_sub(1, Ordering::AcqRel);
|
||||||
|
inner.unpark_at(pid, epoch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if shutdown_requested {
|
if shutdown_requested {
|
||||||
return;
|
return;
|
||||||
@@ -466,27 +447,8 @@ fn epoll_loop(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write one byte to the scheduler's wake pipe. Retries on EINTR; ignores
|
|
||||||
/// EAGAIN (pipe full means there's already an outstanding wake we haven't
|
|
||||||
/// consumed yet, which is sufficient).
|
|
||||||
fn wake_scheduler(wake_write: RawFd) {
|
|
||||||
let buf: [u8; 1] = [0];
|
|
||||||
unsafe {
|
|
||||||
loop {
|
|
||||||
let n = libc::write(wake_write, buf.as_ptr() as *const _, 1);
|
|
||||||
if n < 0 {
|
|
||||||
let e = *libc::__errno_location();
|
|
||||||
if e == libc::EINTR {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Pipe helpers (unchanged from v0.2)
|
// Pipe helper
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
fn make_pipe() -> io::Result<(RawFd, RawFd)> {
|
fn make_pipe() -> io::Result<(RawFd, RawFd)> {
|
||||||
@@ -497,46 +459,3 @@ fn make_pipe() -> io::Result<(RawFd, RawFd)> {
|
|||||||
}
|
}
|
||||||
Ok((fds[0], fds[1]))
|
Ok((fds[0], fds[1]))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drain pending bytes from the wake pipe. The scheduler calls this after
|
|
||||||
/// a `poll` wakeup so the next idle call sees an empty pipe.
|
|
||||||
pub fn drain_wake_pipe(fd: RawFd) {
|
|
||||||
let mut buf = [0u8; 64];
|
|
||||||
loop {
|
|
||||||
let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, buf.len()) };
|
|
||||||
if n <= 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Block on `fd` for up to `timeout`, returning when either there's data
|
|
||||||
/// to read or the timeout elapses. `None` for `timeout` means wait forever.
|
|
||||||
pub fn poll_wake(fd: RawFd, timeout: Option<std::time::Duration>) {
|
|
||||||
let timeout_ms: libc::c_int = match timeout {
|
|
||||||
None => -1,
|
|
||||||
Some(d) => {
|
|
||||||
let ms = d.as_millis();
|
|
||||||
if ms > i32::MAX as u128 {
|
|
||||||
i32::MAX
|
|
||||||
} else {
|
|
||||||
ms as i32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let mut pfd = libc::pollfd {
|
|
||||||
fd,
|
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: 0,
|
|
||||||
};
|
|
||||||
loop {
|
|
||||||
let r = unsafe { libc::poll(&mut pfd as *mut _, 1, timeout_ms) };
|
|
||||||
if r < 0 {
|
|
||||||
let e = unsafe { *libc::__errno_location() };
|
|
||||||
if e == libc::EINTR {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+39
-29
@@ -11,32 +11,35 @@
|
|||||||
//!
|
//!
|
||||||
//! 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 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 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;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -56,32 +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::{
|
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, sleep,
|
block_on_io, cancel_timer, request_stop, run, self_pid, send_after, send_after_named,
|
||||||
spawn, spawn_addr, spawn_under, wait_readable, wait_readable_timeout, wait_writable,
|
send_after_named_wall, send_after_wall, sleep, sleep_wall, spawn, spawn_addr, spawn_addr_with,
|
||||||
wait_writable_timeout, yield_now, FdArm, JoinError, JoinHandle,
|
spawn_under, spawn_under_with, spawn_with, try_spawn, try_spawn_under_with, wait_readable,
|
||||||
|
wait_readable_timeout, wait_writable, wait_writable_timeout, yield_now, FdArm, JoinError,
|
||||||
|
JoinHandle, SpawnError, SpawnOpts,
|
||||||
};
|
};
|
||||||
pub use supervisor::{ChildSpec, OneForOne, Restart, Signal, Strategy};
|
pub use supervisor::{ChildSpec, OneForOne, Restart, Signal, Strategy};
|
||||||
pub use timer::TimerId;
|
pub use timer::TimerId;
|
||||||
|
|||||||
+4
-1
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
+165
-64
@@ -1,49 +1,85 @@
|
|||||||
//! Process monitors.
|
//! Find out when another actor dies, without it knowing or caring that you're
|
||||||
|
//! watching.
|
||||||
//!
|
//!
|
||||||
//! `monitor(target)` asks the runtime to deliver a single [`Down`] when
|
//! Say one actor manages a pool of workers and needs to know when a worker
|
||||||
//! `target` terminates, and hands back a [`Monitor`] — the [`Receiver`] to read
|
//! exits, so it can replace it. The worker does not need to know it is being
|
||||||
//! it from, plus the identity (`id`, `target`) needed to take the registration
|
//! watched, and nothing about the worker's own behavior should change because
|
||||||
//! back down with [`demonitor`]. A monitor is:
|
//! someone is watching it. That is what [`monitor`] is for: call
|
||||||
|
//! `monitor(target)` to get a [`Monitor`], and read exactly one [`Down`]
|
||||||
|
//! message off `monitor.rx` whenever `target` terminates, however it
|
||||||
|
//! terminates.
|
||||||
//!
|
//!
|
||||||
//! - **unidirectional** — the watcher learns of the target's death, but the
|
//! ```
|
||||||
//! target learns nothing of the watcher, and the watcher is unaffected by
|
//! use smarm::{monitor, run, spawn, DownReason};
|
||||||
//! the death beyond the notification (contrast a *link*, which propagates
|
|
||||||
//! failure);
|
|
||||||
//! - **one-shot** — exactly one `Down` is ever sent for a given monitor.
|
|
||||||
//! The returned channel closes afterwards, so a second `recv()` yields
|
|
||||||
//! `Err(RecvError)`.
|
|
||||||
//!
|
//!
|
||||||
//! This generalizes the older single-`supervisor_channel` mechanism: a
|
//! run(|| {
|
||||||
//! supervisor is just a hard-wired monitor that the parent installs at spawn
|
//! let worker = spawn(|| {
|
||||||
//! time. Here any actor may monitor any pid, any number of times.
|
//! // does some work, then returns
|
||||||
|
//! });
|
||||||
|
//! let pid = worker.pid();
|
||||||
//!
|
//!
|
||||||
//! ## Reasons
|
//! let m = monitor(pid);
|
||||||
|
//! let _ = worker.join();
|
||||||
//!
|
//!
|
||||||
//! [`DownReason`] is deliberately payload-free. A panicking actor's payload
|
//! let down = m.rx.recv().expect("monitor channel closed before Down");
|
||||||
//! has a single owner and is delivered to whoever `join()`s the actor (as
|
//! assert_eq!(down.pid, pid);
|
||||||
//! `JoinError`); a monitor only learns *that* it panicked, not the value.
|
//! assert_eq!(down.reason, DownReason::Exit);
|
||||||
//! Monitoring a pid that is already gone (reclaimed, or never alive) yields
|
//! });
|
||||||
//! [`DownReason::NoProc`] immediately, mirroring Erlang's `noproc`.
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! ## Demonitoring
|
//! A monitor is one-directional and one-shot:
|
||||||
//!
|
//!
|
||||||
//! Each `monitor()` registration is tagged with a process-unique [`MonitorId`].
|
//! - **One-directional**: the watcher learns that the target died, but the
|
||||||
//! [`demonitor`] removes the registration named by a [`Monitor`] from its
|
//! target is completely unaffected. It never learns it was being watched,
|
||||||
//! target's slot, returning `Some(id)` if a live registration was found or
|
//! and its own behavior and lifetime do not change because of the monitor.
|
||||||
//! `None` if it had already fired (or the target is gone). Dropping the
|
//! This is the opposite of a [`link`](mod@crate::link), which is bidirectional:
|
||||||
//! [`Monitor`] afterwards discards any `Down` that the target had *already*
|
//! linking two actors means an abnormal death on either side can bring the
|
||||||
//! queued — the equivalent of Erlang's `demonitor(Ref, [flush])`.
|
//! other down too. Reach for a monitor when you just want to *know*; reach
|
||||||
|
//! for a link when a peer's crash should actually stop you.
|
||||||
|
//! - **One-shot**: you get exactly one [`Down`] per `monitor()` call, then the
|
||||||
|
//! channel closes. Calling `monitor` again on the same target (or a
|
||||||
|
//! different one) gives you an independent registration with its own
|
||||||
|
//! [`Monitor`] and its own one-shot channel; nothing stops you from
|
||||||
|
//! monitoring the same actor many times over; each call is watched and
|
||||||
|
//! fires on its own.
|
||||||
//!
|
//!
|
||||||
//! ## Races
|
//! ## Why a monitor never hands you the panic value
|
||||||
//!
|
//!
|
||||||
//! Registration (below) and `finalize_actor` (in `runtime`) both run under the
|
//! If the target panicked, [`Down`] tells you *that* it panicked
|
||||||
//! shared-state mutex, so a target that is still alive when its monitor is
|
//! ([`DownReason::Panic`]), but not the panic's payload. The payload has a
|
||||||
//! registered is guaranteed to deliver a real `Down`; there is no window in
|
//! single owner: it is handed to whichever caller `join()`s the actor's
|
||||||
//! which the death slips between the liveness check and the registration.
|
//! [`JoinHandle`](crate::JoinHandle), as a `JoinError`. A monitor only needs
|
||||||
//! `demonitor` is protected by the generation half of the pid: if the target
|
//! to know that something went wrong, not reproduce the exact value that
|
||||||
//! has died and its slot index been recycled, `slot_mut(target)` fails the
|
//! caused it, so it gets the reason and nothing else.
|
||||||
//! generation check and `demonitor` is a clean no-op — it can never strip a
|
//!
|
||||||
//! *different* actor's monitor that happens to share the slot index.
|
//! Monitoring a target that is already gone (it finished and was cleaned up,
|
||||||
|
//! or the pid never pointed at a real actor) is not an error: you get a
|
||||||
|
//! [`Down`] with [`DownReason::NoProc`] right away, instead of waiting
|
||||||
|
//! forever for something that already happened.
|
||||||
|
//!
|
||||||
|
//! ## Stopping a monitor early
|
||||||
|
//!
|
||||||
|
//! [`demonitor`] cancels a monitor before it fires. If the registration was
|
||||||
|
//! still live, it removes it and returns `Some` of the monitor's id: no
|
||||||
|
//! `Down` will arrive on that channel from here on. If the target had already
|
||||||
|
//! died and its `Down` already sent, there is nothing left to cancel and
|
||||||
|
//! `demonitor` returns `None`; the `Down` you already have (or that is
|
||||||
|
//! already sitting in the channel) is unaffected.
|
||||||
|
//!
|
||||||
|
//! If you want to cancel *and* make sure a `Down` that already arrived is
|
||||||
|
//! discarded without reading it, just drop the [`Monitor`]: dropping it closes
|
||||||
|
//! its receiver, and any queued `Down` is dropped along with it.
|
||||||
|
//!
|
||||||
|
//! ## Correctness notes for implementers
|
||||||
|
//!
|
||||||
|
//! A target that is still alive at the moment `monitor()` registers is
|
||||||
|
//! guaranteed to eventually produce a real `Down`: registration and the
|
||||||
|
//! target's own termination bookkeeping run under the same lock, so there is
|
||||||
|
//! no window in which the target could die without the just-added
|
||||||
|
//! registration seeing it. `demonitor` is similarly race-free against a target
|
||||||
|
//! that has since died and had its slot reused by a new, unrelated actor: it
|
||||||
|
//! is checked against the exact monitored incarnation, so it can never remove
|
||||||
|
//! a different actor's registration by accident, it simply reports `None`.
|
||||||
|
|
||||||
use crate::channel::{channel, Receiver, Sender};
|
use crate::channel::{channel, Receiver, Sender};
|
||||||
use crate::pid::Pid;
|
use crate::pid::Pid;
|
||||||
@@ -51,8 +87,8 @@ use crate::scheduler::with_runtime;
|
|||||||
|
|
||||||
/// Why a monitored actor went down.
|
/// Why a monitored actor went down.
|
||||||
///
|
///
|
||||||
/// `Copy` because it carries no payload — see the module docs for why the
|
/// Carries no payload: see the module docs for why a monitor never receives
|
||||||
/// panic payload is *not* included here.
|
/// the panic value itself.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum DownReason {
|
pub enum DownReason {
|
||||||
/// The target returned normally.
|
/// The target returned normally.
|
||||||
@@ -76,21 +112,22 @@ pub struct Down {
|
|||||||
pub reason: DownReason,
|
pub reason: DownReason,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A process-unique identifier for one `monitor()` registration.
|
/// A unique identifier for one [`monitor`] registration.
|
||||||
///
|
///
|
||||||
/// Opaque and `Copy`. Allocated from a monotonic counter in shared state, so
|
/// Opaque and `Copy`. Never reused for the life of the runtime, so if you
|
||||||
/// it is never reused for the lifetime of the runtime — distinct `monitor()`
|
/// monitor the same target more than once, each call's id is distinct. This
|
||||||
/// calls on the same target get distinct ids, which is what lets [`demonitor`]
|
/// is what lets [`demonitor`] tear down exactly one of several monitors on
|
||||||
/// tear down exactly one of several monitors on a target.
|
/// the same target without disturbing the others.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct MonitorId(pub(crate) u64);
|
pub struct MonitorId(pub(crate) u64);
|
||||||
|
|
||||||
/// A live monitor: the receiving end of the one-shot [`Down`] channel, plus the
|
/// A live monitor: the receiving end of the one-shot [`Down`] channel, plus the
|
||||||
/// identity needed to [`demonitor`] it.
|
/// identity needed to [`demonitor`] it.
|
||||||
///
|
///
|
||||||
/// Read the notification from [`Monitor::rx`]. Not `Clone` (the receiver is a
|
/// Read the notification from [`Monitor::rx`]. Not `Clone`, since only one
|
||||||
/// single consumer). Dropping it closes the receiving end; if a `Down` was
|
/// side is meant to consume it. Dropping a `Monitor` closes the receiving
|
||||||
/// already queued it is discarded with the channel.
|
/// end; if a `Down` had already arrived but was never read, it is discarded
|
||||||
|
/// along with it.
|
||||||
pub struct Monitor {
|
pub struct Monitor {
|
||||||
/// This registration's process-unique id.
|
/// This registration's process-unique id.
|
||||||
pub id: MonitorId,
|
pub id: MonitorId,
|
||||||
@@ -110,10 +147,11 @@ pub fn monitor<A>(target: Pid<A>) -> Monitor {
|
|||||||
let target = target.erase();
|
let target = target.erase();
|
||||||
let (tx, rx) = channel::<Down>();
|
let (tx, rx) = channel::<Down>();
|
||||||
|
|
||||||
// Register under the target's cold lock. `tx.clone()` takes the channel's
|
// Implementation note: registration happens under the target's cold
|
||||||
// own lock — a Channel-class RawMutex, explicitly permitted *under* a Leaf
|
// lock. `tx.clone()` takes the channel's own lock, a Channel-class
|
||||||
// (cold) lock by the lock order (see raw_mutex.rs). We must still not
|
// RawMutex, which is explicitly permitted under a Leaf (cold) lock by
|
||||||
// *send* under the lock, as `Sender::send` can unpark a parked receiver,
|
// the lock order documented in raw_mutex.rs. We must still not *send*
|
||||||
|
// under the lock, since `Sender::send` can unpark a parked receiver,
|
||||||
// and there's no reason to nest that.
|
// and there's no reason to nest that.
|
||||||
let (id, registered) = with_runtime(|inner| {
|
let (id, registered) = with_runtime(|inner| {
|
||||||
let id = inner.alloc_monitor_id();
|
let id = inner.alloc_monitor_id();
|
||||||
@@ -133,26 +171,89 @@ 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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cancel the monitor `m`. Returns `Some(id)` if a live registration was found
|
/// Flag `target`'s tenancy as watchable: its death will stamp the slot's
|
||||||
/// on the target's slot and removed, or `None` if there was nothing to remove
|
/// terminal record (see [`terminal_reason`]), exactly as registering a name
|
||||||
/// — the target already fired its `Down` (the registration is drained on
|
/// does. The bridge calls this wherever a smarm pid is *encoded across the
|
||||||
/// finalize), was never alive (`NoProc`), or has been reclaimed.
|
/// 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.
|
||||||
///
|
///
|
||||||
/// This stops any *future* `Down`. To also discard a `Down` the target may have
|
/// Generation-checked and live-screened: marking a pid whose tenancy already
|
||||||
/// *already* queued (the finalize-races-demonitor case), drop `m` afterwards;
|
/// ended is a no-op — its record either exists (it was flagged before dying)
|
||||||
/// dropping the [`Monitor`] closes its receiver and the queued notice goes with
|
/// or is honestly unknowable. Same `Runtime::run()` context contract as
|
||||||
/// it — the analogue of Erlang's `demonitor(Ref, [flush])`.
|
/// [`monitor`].
|
||||||
|
pub fn mark_watchable<A>(target: Pid<A>) {
|
||||||
|
let target = target.erase();
|
||||||
|
with_runtime(|inner| {
|
||||||
|
if let Some(slot) = inner.slot_at(target) {
|
||||||
|
// Cold lock FIRST: finalize publishes Done and checks the
|
||||||
|
// watchable bit under this same lock, so the mark either lands
|
||||||
|
// before finalize reads it (the death stamps) or observes the
|
||||||
|
// tenancy already dead (no-op). No lost-stamp window between an
|
||||||
|
// unlocked liveness read and the flag set.
|
||||||
|
let mut cold = slot.cold.lock();
|
||||||
|
if slot.is_live_for(target) {
|
||||||
|
cold.watchable = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The terminal [`DownReason`] of the tenancy `target` names, if that tenancy
|
||||||
|
/// ever registered a name and is the *most recent named* death of its slot:
|
||||||
|
/// finalize stamps the slot with `(generation, reason)` for once-registered
|
||||||
|
/// tenancies (anonymous green-thread churn does not stamp — nor evict), and
|
||||||
|
/// the record survives reclaim and the next tenant's install, until the next
|
||||||
|
/// *named* tenant of the slot itself dies. `None` means the pid never lived,
|
||||||
|
/// is still alive, never held a name, or its record was overwritten by a
|
||||||
|
/// later named tenancy's death — callers fall back to `NoProc` semantics.
|
||||||
|
///
|
||||||
|
/// This exists for watch-installers that raced their target's death (bridge
|
||||||
|
/// soak signature 4): a `NoProc` observed at install time can be upgraded to
|
||||||
|
/// the real reason while the record still matches, which is exactly what an
|
||||||
|
/// install that had won the race would have delivered. It does NOT change
|
||||||
|
/// [`monitor`]'s own semantics — monitoring a stale pid still queues `NoProc`,
|
||||||
|
/// the same shape Erlang gives — the upgrade is the caller's deliberate act.
|
||||||
|
/// Same context contract as [`monitor`]: must run inside `Runtime::run()`.
|
||||||
|
pub fn terminal_reason<A>(target: Pid<A>) -> Option<DownReason> {
|
||||||
|
let target = target.erase();
|
||||||
|
with_runtime(|inner| {
|
||||||
|
let slot = inner.slot_at(target)?;
|
||||||
|
let cold = slot.cold.lock();
|
||||||
|
match cold.terminal {
|
||||||
|
Some((generation, reason)) if generation == target.generation() => Some(reason),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cancel the monitor `m`. Returns `Some(id)` if a live registration was found
|
||||||
|
/// and removed, so no `Down` will arrive on `m.rx` from here on. Returns
|
||||||
|
/// `None` if there was nothing left to remove: the target had already gone
|
||||||
|
/// down and its `Down` was already sent (or is already sitting in the
|
||||||
|
/// channel, unread).
|
||||||
|
///
|
||||||
|
/// This only stops a *future* `Down`. If you also want to discard a `Down`
|
||||||
|
/// that already arrived (or is about to, in a race with this call), drop `m`
|
||||||
|
/// instead of, or in addition to, calling this: dropping the [`Monitor`]
|
||||||
|
/// closes its receiver and any queued notice is discarded with it.
|
||||||
pub fn demonitor(m: &Monitor) -> Option<MonitorId> {
|
pub fn demonitor(m: &Monitor) -> Option<MonitorId> {
|
||||||
// Remove the registration under the target's cold lock, but move the
|
// Implementation note: the registration is removed under the target's
|
||||||
// `Sender` *out* and let it drop only after the lock is released:
|
// cold lock, but the `Sender` is moved *out* and dropped only after the
|
||||||
// dropping the last sender runs `Sender::drop`, which may unpark a parked
|
// lock is released. Dropping the last sender runs `Sender::drop`, which
|
||||||
// receiver — legal under a cold lock, but pointless to nest.
|
// may unpark a parked receiver; legal under a cold lock, but pointless
|
||||||
|
// to nest.
|
||||||
let removed: Option<(MonitorId, Sender<Down>)> = with_runtime(|inner| {
|
let removed: Option<(MonitorId, Sender<Down>)> = with_runtime(|inner| {
|
||||||
let slot = inner.slot_at(m.target)?;
|
let slot = inner.slot_at(m.target)?;
|
||||||
let mut cold = slot.cold.lock();
|
let mut cold = slot.cold.lock();
|
||||||
|
|||||||
+149
-19
@@ -1,12 +1,89 @@
|
|||||||
//! Actor-aware mutex with mandatory timeout.
|
//! Shared mutable state across actors, when a channel is overkill.
|
||||||
//!
|
//!
|
||||||
//! `Mutex<T>` parks the calling *green* thread on contention rather than
|
//! smarm actors normally coordinate by sending messages, and for a piece of
|
||||||
//! blocking the OS thread. Every lock attempt is bounded by a timeout.
|
//! owned state the right tool is usually a `gen_server`: one actor holds the
|
||||||
|
//! data and everyone else talks to it. Sometimes that is more machinery than
|
||||||
|
//! you need, and plain shared, lockable state is simpler: [`Mutex<T>`] is
|
||||||
|
//! that escape hatch. It behaves like `std::sync::Mutex<T>`, guarding a value
|
||||||
|
//! of type `T` behind a guard that gives you `&mut T` while held, but it is
|
||||||
|
//! built for smarm's actors rather than OS threads.
|
||||||
//!
|
//!
|
||||||
//! Internals use `Arc<std::sync::Mutex<...>>` so the type is genuinely
|
//! The key difference from `std::sync::Mutex` is what happens on contention.
|
||||||
//! `Send + Sync` and can be shared across scheduler threads.
|
//! [`Mutex::lock`] parks the calling actor (a cooperatively scheduled green
|
||||||
|
//! thread) rather than blocking the underlying OS thread, so other actors on
|
||||||
|
//! the same OS thread keep running while it waits. And every lock attempt is
|
||||||
|
//! bounded by a timeout: an actor that hangs on to the lock forever (stuck in
|
||||||
|
//! a bug, or just slow) would otherwise wedge every other actor waiting on
|
||||||
|
//! it, so smarm makes the wait bounded by default instead of leaving it up
|
||||||
|
//! to you to remember.
|
||||||
//!
|
//!
|
||||||
//! Fairness: FIFO. Poisoning: none. Reentrance: deadlock (caller bug).
|
//! ## A first lock
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! use smarm::{run, spawn, Mutex};
|
||||||
|
//!
|
||||||
|
//! run(|| {
|
||||||
|
//! let counter = Mutex::new(0u32);
|
||||||
|
//!
|
||||||
|
//! // Mutex::clone() is cheap and hands out another handle to the SAME
|
||||||
|
//! // underlying value, much like Arc::clone: every clone shares one lock
|
||||||
|
//! // and one value, so mutations through one are visible through all.
|
||||||
|
//! let a = counter.clone();
|
||||||
|
//! let b = counter.clone();
|
||||||
|
//!
|
||||||
|
//! let h1 = spawn(move || {
|
||||||
|
//! let mut guard = a.lock().unwrap();
|
||||||
|
//! *guard += 1;
|
||||||
|
//! });
|
||||||
|
//! let h2 = spawn(move || {
|
||||||
|
//! let mut guard = b.lock().unwrap();
|
||||||
|
//! *guard += 1;
|
||||||
|
//! });
|
||||||
|
//! h1.join().unwrap();
|
||||||
|
//! h2.join().unwrap();
|
||||||
|
//!
|
||||||
|
//! assert_eq!(*counter.lock().unwrap(), 2);
|
||||||
|
//! });
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! ## Choosing a timeout
|
||||||
|
//!
|
||||||
|
//! [`Mutex::lock`] waits up to [`DEFAULT_TIMEOUT`] (30 seconds) before giving
|
||||||
|
//! up with [`LockTimeout`]. To use a different bound for one call, use
|
||||||
|
//! [`Mutex::lock_timeout`] instead; to change the default for every future
|
||||||
|
//! `lock()` call on this mutex (including through its clones), use
|
||||||
|
//! [`Mutex::set_default_timeout`]. If you never want to wait at all, use
|
||||||
|
//! [`Mutex::try_lock`], which returns immediately whether or not the lock was
|
||||||
|
//! free.
|
||||||
|
//!
|
||||||
|
//! ## Fairness and panics
|
||||||
|
//!
|
||||||
|
//! Waiters are granted the lock in the order they started waiting (FIFO), so
|
||||||
|
//! no actor can be starved by later arrivals repeatedly cutting in line.
|
||||||
|
//!
|
||||||
|
//! This mutex never poisons. `std::sync::Mutex` marks itself poisoned if a
|
||||||
|
//! thread panics while holding the lock, because a partly mutated value might
|
||||||
|
//! be left behind for the next lock holder to see. smarm's actors already
|
||||||
|
//! rely on `Drop` running during unwinding to release the lock, so if a
|
||||||
|
//! holder panics, [`MutexGuard::drop`] still runs and the next waiter is
|
||||||
|
//! granted the lock normally. It is the same tradeoff `std::sync::Mutex`
|
||||||
|
//! offers you if you choose to ignore poisoning: you may see a value left
|
||||||
|
//! mid-update by the panicking actor, so a panic inside a critical section is
|
||||||
|
//! still a bug worth fixing, just not one that also wedges every future lock
|
||||||
|
//! attempt.
|
||||||
|
//!
|
||||||
|
//! Locking a mutex you already hold (on the same actor) does not queue
|
||||||
|
//! behind yourself: it deadlocks, the same way relocking a non-reentrant
|
||||||
|
//! `std::sync::Mutex` does. Don't call `lock` while already holding a guard
|
||||||
|
//! from the same `Mutex`.
|
||||||
|
//!
|
||||||
|
//! ## Outside the runtime
|
||||||
|
//!
|
||||||
|
//! `Mutex<T>` also works when called from plain code that is not running as
|
||||||
|
//! a smarm actor (for example, in a test's setup code before calling
|
||||||
|
//! [`run`](crate::run)). There, an actor's cooperative park has no meaning,
|
||||||
|
//! so a lock attempt instead blocks the calling OS thread directly until the
|
||||||
|
//! mutex is free; there is no timeout on this path.
|
||||||
|
|
||||||
use crate::pid::Pid;
|
use crate::pid::Pid;
|
||||||
use crate::scheduler;
|
use crate::scheduler;
|
||||||
@@ -15,8 +92,14 @@ use std::collections::VecDeque;
|
|||||||
use std::sync::{Arc, Mutex as StdMutex};
|
use std::sync::{Arc, Mutex as StdMutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// How long [`Mutex::lock`] waits for the lock before giving up, unless
|
||||||
|
/// overridden per-mutex with [`Mutex::set_default_timeout`] or per-call with
|
||||||
|
/// [`Mutex::lock_timeout`].
|
||||||
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
|
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
|
/// Returned by [`Mutex::lock`] / [`Mutex::lock_timeout`] when the timeout
|
||||||
|
/// elapses before the lock became available. The lock attempt is abandoned;
|
||||||
|
/// nothing was acquired, and the mutex's value is unaffected.
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
pub struct LockTimeout;
|
pub struct LockTimeout;
|
||||||
|
|
||||||
@@ -70,12 +153,16 @@ impl TimerTarget for MutexCore {
|
|||||||
};
|
};
|
||||||
// Remove from waiters only if still there with matching epoch.
|
// Remove from waiters only if still there with matching epoch.
|
||||||
// If the lock was already granted (holder == Some(pid)), the
|
// If the lock was already granted (holder == Some(pid)), the
|
||||||
// timer fired after the grant — treat as no-op; the actor
|
// timer fired after the grant: treat as no-op; the actor
|
||||||
// will see `is_holder == true` and return Ok.
|
// will see `is_holder == true` and return Ok.
|
||||||
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
|
||||||
@@ -100,6 +187,8 @@ pub struct Mutex<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Mutex<T> {
|
impl<T> Mutex<T> {
|
||||||
|
/// Wrap `value` in a new mutex, initially unlocked, with the default
|
||||||
|
/// lock timeout ([`DEFAULT_TIMEOUT`]).
|
||||||
pub fn new(value: T) -> Self {
|
pub fn new(value: T) -> Self {
|
||||||
Self {
|
Self {
|
||||||
core: Arc::new(MutexCore::new(DEFAULT_TIMEOUT)),
|
core: Arc::new(MutexCore::new(DEFAULT_TIMEOUT)),
|
||||||
@@ -107,6 +196,11 @@ impl<T> Mutex<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Change how long future [`lock`](Self::lock) calls on this mutex wait
|
||||||
|
/// before giving up. Applies to every clone of this `Mutex` (they share
|
||||||
|
/// one underlying lock), and to `lock` calls already in progress that
|
||||||
|
/// have not yet started waiting. Does not affect [`lock_timeout`](Self::lock_timeout)
|
||||||
|
/// calls, which always use the timeout passed in.
|
||||||
pub fn set_default_timeout(&self, timeout: Duration) {
|
pub fn set_default_timeout(&self, timeout: Duration) {
|
||||||
match self.core.state.lock() {
|
match self.core.state.lock() {
|
||||||
Ok(mut st) => st.default_timeout = timeout,
|
Ok(mut st) => st.default_timeout = timeout,
|
||||||
@@ -114,6 +208,12 @@ impl<T> Mutex<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Acquire the lock, waiting up to this mutex's default timeout
|
||||||
|
/// ([`DEFAULT_TIMEOUT`], or whatever [`set_default_timeout`](Self::set_default_timeout)
|
||||||
|
/// last set) if it is currently held elsewhere. Returns a [`MutexGuard`]
|
||||||
|
/// that releases the lock when dropped, or [`LockTimeout`] if the
|
||||||
|
/// deadline passes first. To use a one-off timeout instead of the
|
||||||
|
/// mutex's default, call [`lock_timeout`](Self::lock_timeout) directly.
|
||||||
pub fn lock(&self) -> Result<MutexGuard<'_, T>, LockTimeout> {
|
pub fn lock(&self) -> Result<MutexGuard<'_, T>, LockTimeout> {
|
||||||
let timeout = match self.core.state.lock() {
|
let timeout = match self.core.state.lock() {
|
||||||
Ok(st) => st.default_timeout,
|
Ok(st) => st.default_timeout,
|
||||||
@@ -122,6 +222,10 @@ impl<T> Mutex<T> {
|
|||||||
self.lock_timeout(timeout)
|
self.lock_timeout(timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Acquire the lock, waiting up to `timeout` (ignoring this mutex's
|
||||||
|
/// default) if it is currently held elsewhere. Returns a [`MutexGuard`]
|
||||||
|
/// that releases the lock when dropped, or [`LockTimeout`] if `timeout`
|
||||||
|
/// elapses first with the lock still unavailable.
|
||||||
pub fn lock_timeout(&self, timeout: Duration) -> Result<MutexGuard<'_, T>, LockTimeout> {
|
pub fn lock_timeout(&self, timeout: Duration) -> Result<MutexGuard<'_, T>, LockTimeout> {
|
||||||
// Outside the runtime (e.g. in tests, after run() returns) there is no
|
// Outside the runtime (e.g. in tests, after run() returns) there is no
|
||||||
// current actor PID. Fall back to a blocking std::sync::Mutex acquire.
|
// current actor PID. Fall back to a blocking std::sync::Mutex acquire.
|
||||||
@@ -146,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),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +264,7 @@ impl<T> Mutex<T> {
|
|||||||
Ok(g) => g,
|
Ok(g) => g,
|
||||||
Err(e) => panic!("smarm: mutex state lock poisoned (core corrupt): {e}"),
|
Err(e) => panic!("smarm: mutex state lock poisoned (core corrupt): {e}"),
|
||||||
};
|
};
|
||||||
// begin_wait is lock-free — legal under the state lock; this
|
// begin_wait is lock-free (legal under the state lock); this
|
||||||
// makes the epoch atomic with the registration's visibility to
|
// makes the epoch atomic with the registration's visibility to
|
||||||
// grants and timeouts.
|
// grants and timeouts.
|
||||||
let epoch = scheduler::begin_wait();
|
let epoch = scheduler::begin_wait();
|
||||||
@@ -170,7 +277,7 @@ impl<T> Mutex<T> {
|
|||||||
scheduler::insert_wait_timer(deadline, me, target, epoch);
|
scheduler::insert_wait_timer(deadline, me, target, epoch);
|
||||||
scheduler::park_current();
|
scheduler::park_current();
|
||||||
|
|
||||||
// Resumed — precisely: only our grant or our timer can wake this
|
// Resumed, precisely: only our grant or our timer can wake this
|
||||||
// wait (both epoch-stamped; a stop wake unwinds out of
|
// wait (both epoch-stamped; a stop wake unwinds out of
|
||||||
// park_current). The one-shot interpretation below is therefore
|
// park_current). The one-shot interpretation below is therefore
|
||||||
// exhaustive. Are we the holder?
|
// exhaustive. Are we the holder?
|
||||||
@@ -187,12 +294,18 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Acquire the lock only if it is immediately available: never parks and
|
||||||
|
/// never waits. Returns `Some` with a [`MutexGuard`] if the lock was
|
||||||
|
/// free, `None` if it is currently held elsewhere.
|
||||||
pub fn try_lock(&self) -> Option<MutexGuard<'_, T>> {
|
pub fn try_lock(&self) -> Option<MutexGuard<'_, T>> {
|
||||||
let me = crate::actor::current_pid()?;
|
let me = crate::actor::current_pid()?;
|
||||||
let mut st = match self.core.state.lock() {
|
let mut st = match self.core.state.lock() {
|
||||||
@@ -212,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.
|
||||||
@@ -226,16 +342,28 @@ 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),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Clone for Mutex<T> {
|
impl<T> Clone for Mutex<T> {
|
||||||
|
/// Cheap: hands back another handle to the same underlying lock and
|
||||||
|
/// value, the way `Arc::clone` does. All clones of a `Mutex` share one
|
||||||
|
/// lock and one protected value; locking through any clone excludes
|
||||||
|
/// 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(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,6 +375,10 @@ unsafe impl<T: Send> Sync for Mutex<T> {}
|
|||||||
// Guard
|
// Guard
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Grants access to the value inside a [`Mutex`] while the lock is held.
|
||||||
|
/// Dereferences to `&T` and `&mut T`. Dropping the guard releases the lock
|
||||||
|
/// and, if another actor is waiting, wakes the next one in arrival order.
|
||||||
|
/// Returned by [`Mutex::lock`], [`Mutex::lock_timeout`], and [`Mutex::try_lock`].
|
||||||
pub struct MutexGuard<'a, T> {
|
pub struct MutexGuard<'a, T> {
|
||||||
mutex: &'a Mutex<T>,
|
mutex: &'a Mutex<T>,
|
||||||
value: Option<T>,
|
value: Option<T>,
|
||||||
@@ -277,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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1017
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -98,6 +98,28 @@ pub(crate) fn clear_current_slot() {
|
|||||||
CURRENT_SLOT.with(|c| c.set(std::ptr::null()));
|
CURRENT_SLOT.with(|c| c.set(std::ptr::null()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Raw pointer to the on-CPU actor's slot, null on the scheduler's own
|
||||||
|
/// stack. Same lifetime argument as `note_overrun`: the slot is never
|
||||||
|
/// reclaimed while its actor is on-CPU. Consumers: the `smarm-causal`
|
||||||
|
/// profiler (RFC 007) and — unconditionally — the SIGSEGV classifier
|
||||||
|
/// (RFC 019 §7), which additionally relies on this being a plain load of a
|
||||||
|
/// const-initialized TLS Cell (no lazy init, no allocation, no dtor): safe
|
||||||
|
/// from a signal handler.
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn current_slot_ptr() -> *const crate::runtime::Slot {
|
||||||
|
CURRENT_SLOT.with(|c| c.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RFC 007 (`smarm-causal`) — push the slice start forward by `cycles`, so
|
||||||
|
/// virtually-injected delay spun inside `maybe_preempt` does not count against
|
||||||
|
/// the actor's timeslice (the clock-correction half of the RFC: the runtime
|
||||||
|
/// owns this clock, so it can subtract its own perturbation).
|
||||||
|
#[cfg(feature = "smarm-causal")]
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn extend_timeslice(cycles: u64) {
|
||||||
|
TIMESLICE_START.with(|c| c.set(c.get().wrapping_add(cycles)));
|
||||||
|
}
|
||||||
|
|
||||||
/// Tally a timeslice overrun against the on-CPU actor (RFC 016 Chunk 2). A
|
/// Tally a timeslice overrun against the on-CPU actor (RFC 016 Chunk 2). A
|
||||||
/// no-op if no actor is bound (the scheduler's own stack). Reached only from
|
/// no-op if no actor is bound (the scheduler's own stack). Reached only from
|
||||||
/// the slice-expiry branch, which is already the yield path, so its cost is
|
/// the slice-expiry branch, which is already the yield path, so its cost is
|
||||||
@@ -247,6 +269,11 @@ pub fn maybe_preempt() {
|
|||||||
// Observe a pending stop first: if we are being cancelled
|
// Observe a pending stop first: if we are being cancelled
|
||||||
// there is no point yielding, we unwind instead.
|
// there is no point yielding, we unwind instead.
|
||||||
check_cancelled();
|
check_cancelled();
|
||||||
|
// RFC 007: causal-profiling sample/absorb point. Shares the
|
||||||
|
// amortised cadence, and the PREEMPTION_ENABLED gate — so it
|
||||||
|
// can never spin inside a prep-to-park or no-preempt region.
|
||||||
|
#[cfg(feature = "smarm-causal")]
|
||||||
|
crate::causal::check();
|
||||||
let start = TIMESLICE_START.with(|s| s.get());
|
let start = TIMESLICE_START.with(|s| s.get());
|
||||||
if rdtsc().saturating_sub(start) > CONFIGURED_TIMESLICE_CYCLES.with(|t| t.get()) {
|
if rdtsc().saturating_sub(start) > CONFIGURED_TIMESLICE_CYCLES.with(|t| t.get()) {
|
||||||
// Tally the overrun (RFC 016 Chunk 2) before handing back —
|
// Tally the overrun (RFC 016 Chunk 2) before handing back —
|
||||||
|
|||||||
+4
-1
@@ -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]
|
||||||
|
|||||||
+389
-216
@@ -1,55 +1,107 @@
|
|||||||
//! Named mailbox registry — resolve a name (or pid) to a *messageable* actor.
|
//! Give an actor a name so other actors can find it and message it.
|
||||||
//!
|
//!
|
||||||
//! ## What changed (RFC 014)
|
//! Without the registry, the only way to reach an actor is to already be
|
||||||
|
//! holding its [`Pid`], usually because you spawned it yourself or someone
|
||||||
|
//! passed it to you. That is fine for a worker you just created, but it does
|
||||||
|
//! not work for a well-known service that arbitrary parts of your program
|
||||||
|
//! need to find independently, like a logger, a config store, or a
|
||||||
|
//! connection pool. The registry solves this: an actor claims a name once,
|
||||||
|
//! and from then on any other actor can look that name up, or send to it
|
||||||
|
//! directly, without ever having been handed a `Pid`.
|
||||||
//!
|
//!
|
||||||
//! The old registry was a `name <-> pid` bimap: `whereis` handed back a `Pid`
|
//! ```
|
||||||
//! you could not send to, because a pid is just `(index, generation)` with no
|
//! use smarm::{channel, register, run, send, spawn, unregister, whereis, Name};
|
||||||
//! delivery endpoint. This rework makes resolution yield something messageable.
|
|
||||||
//!
|
//!
|
||||||
//! Two facts shape the structure:
|
//! const COUNTER: Name<u64> = Name::new("counter");
|
||||||
//!
|
//!
|
||||||
//! 1. **A name resolves to a single actor.** Many actors under one label is
|
//! run(|| {
|
||||||
//! what *process groups* (`pg`) are for; the registry is one-name-one-actor
|
//! let (ready_tx, ready_rx) = channel::<()>();
|
||||||
//! (several names *may* point at the same actor).
|
//! let (tx, rx) = channel::<u64>();
|
||||||
//! 2. **Channels are typed**, so an actor has no single untyped mailbox. An
|
|
||||||
//! actor instead owns a *set* of typed channels — one [`Sender`] per message
|
|
||||||
//! type it accepts. So the registry maps name/pid to a [`Mailbox`]: a small
|
|
||||||
//! structure holding that actor's pid plus all of its typed channels, keyed
|
|
||||||
//! by message [`TypeId`].
|
|
||||||
//!
|
//!
|
||||||
//! Resolution is therefore: `name -> pid` (single actor) `-> Mailbox -> the
|
//! let worker = spawn(move || {
|
||||||
//! channel for message type M`. A `Name<Cmd>` and a `Name<Admin>` on the *same*
|
//! // Claim the name for this actor's inbox. Any actor holding
|
||||||
//! actor select *different* channels purely by their type parameter, so
|
//! // `COUNTER` can now reach this one by name.
|
||||||
//! capability separation (RFC 014 §4.7) needs no extra machinery.
|
//! register(COUNTER, tx).unwrap();
|
||||||
|
//! ready_tx.send(()).unwrap();
|
||||||
|
//! assert_eq!(rx.recv().unwrap(), 42);
|
||||||
|
//! });
|
||||||
//!
|
//!
|
||||||
//! ## Type erasure is contained
|
//! ready_rx.recv().unwrap(); // wait for the worker to register
|
||||||
//!
|
//!
|
||||||
//! Each stored channel is a `Box<dyn Any + Send>` that is concretely a
|
//! // Look the name up, or just send to it directly.
|
||||||
//! `Sender<M>`, filed under `TypeId::of::<M>()`. A resolve for `M` looks up
|
//! assert_eq!(whereis("counter"), Some(worker.pid()));
|
||||||
//! that exact `TypeId` and downcasts to `Sender<M>` — keyed by the very type we
|
//! send(COUNTER, 42).unwrap();
|
||||||
//! downcast to, so the downcast cannot fail on correct data; a failure is a
|
|
||||||
//! smarm bug, asserted in debug. The phantom `M` on [`Name`] re-imposes the
|
|
||||||
//! type at the call site, so callers never touch the erasure.
|
|
||||||
//!
|
//!
|
||||||
//! ## Cleanup is lazy (prune-on-contact)
|
//! worker.join().unwrap();
|
||||||
//!
|
//!
|
||||||
//! As before, there is no `finalize` hook and no name field on the slot. Every
|
//! // The name dies with the actor: nobody holds it anymore.
|
||||||
//! operation that touches a binding checks the target pid's liveness via the
|
//! assert_eq!(whereis("counter"), None);
|
||||||
//! generation-checked slot word; a binding to a dead actor behaves as absent
|
//! });
|
||||||
//! and is pruned on contact (its [`Mailbox`] and every name pointing at it are
|
//! ```
|
||||||
//! dropped). The cost is a dead binding lingering until something looks at it;
|
|
||||||
//! the payoff is zero coupling to the actor lifecycle.
|
|
||||||
//!
|
//!
|
||||||
//! ## Locking
|
//! ## Names carry a message type
|
||||||
//!
|
//!
|
||||||
//! One `RawMutex` (Leaf class) in `RuntimeInner`, exactly like the old
|
//! A [`Name<M>`] is a plain string plus a type parameter `M`: the message
|
||||||
//! registry. The fold (name index *and* handles under the one lock) is what
|
//! type that name expects to receive. [`Name::new`] is `const`, so the usual
|
||||||
//! keeps a name-addressed `send` on a single Leaf — `raw_mutex` panics on a
|
//! pattern is a module-level constant like `COUNTER` above, shared by every
|
||||||
//! second Leaf acquired while one is held. The send path clones the `Sender`
|
//! caller. The type parameter means a name is only ever sent the kind of
|
||||||
//! **under** the Leaf lock (a `Sender::clone` takes a Channel lock, permitted
|
//! message it was declared for. If two different constants share the same
|
||||||
//! under a Leaf), then **releases** the Leaf and only *then* sends — a send can
|
//! string but have different message types, they still address two
|
||||||
//! unpark a receiver, and wakeup-bearing work runs outside the Leaf. Order is
|
//! independent channels on the same actor: registering both just gives that
|
||||||
//! **Leaf -> Channel**, as `pg`/`finalize`.
|
//! actor two ways to be reached, one per message type. This is how you give
|
||||||
|
//! one actor a "public" channel and a separate, differently-typed "admin"
|
||||||
|
//! channel under related names, without inventing an enum to merge them.
|
||||||
|
//!
|
||||||
|
//! ## One actor per name, looked up fresh every time
|
||||||
|
//!
|
||||||
|
//! A name always points at exactly one actor at a time (contrast a *process
|
||||||
|
//! group*, from the [`pg`](crate::pg) module, which is one name mapping to
|
||||||
|
//! many actors). Unlike a plain [`Pid`], which names one specific actor
|
||||||
|
//! forever and stops working the moment that actor dies, a name is
|
||||||
|
//! re-resolved on every [`send`]: if the actor holding it dies and a new one
|
||||||
|
//! registers under the same name, the next `send` reaches the new holder
|
||||||
|
//! automatically. Use a name for a long-lived service whose exact identity
|
||||||
|
//! you do not want to track by hand; use a `Pid` when you already have one
|
||||||
|
//! and want to talk to that exact actor.
|
||||||
|
//!
|
||||||
|
//! ## Registration ends when the actor does
|
||||||
|
//!
|
||||||
|
//! There is no separate step to clean up a name when its actor exits: dying
|
||||||
|
//! is enough. The next operation that touches a dead binding (a [`whereis`],
|
||||||
|
//! a [`send`], or another actor's [`register`] of the same name) notices the
|
||||||
|
//! actor is gone and clears the stale entry as a side effect, so the name
|
||||||
|
//! becomes free again. [`unregister`] is only for a live actor voluntarily
|
||||||
|
//! giving up a name it no longer wants; nothing has to call it on the way
|
||||||
|
//! out.
|
||||||
|
//!
|
||||||
|
//! ## Implementation notes
|
||||||
|
//!
|
||||||
|
//! These details matter if you are working on smarm itself; they are not
|
||||||
|
//! part of the public contract.
|
||||||
|
//!
|
||||||
|
//! Internally, each live actor that has published at least one channel owns
|
||||||
|
//! a `Mailbox`: its pid plus a set of typed channels, keyed by the message
|
||||||
|
//! type's `TypeId`. A stored channel is a `Box<dyn Any + Send>` that
|
||||||
|
//! is concretely a `Sender<M>`; resolving for `M` looks up that exact
|
||||||
|
//! `TypeId` and downcasts, so the downcast cannot fail on correct data (a
|
||||||
|
//! failure would be a bug in the registry itself, checked in debug builds).
|
||||||
|
//! Registering a name therefore means: find or create the actor's mailbox,
|
||||||
|
//! insert the channel under its type, and point the name at the actor's pid.
|
||||||
|
//!
|
||||||
|
//! There is no callback when an actor exits. Every operation that touches a
|
||||||
|
//! binding checks the target pid's liveness directly against the scheduler's
|
||||||
|
//! slot table (which also tracks a generation counter, so a dead actor's
|
||||||
|
//! reused slot index is never mistaken for the same actor). A binding to a
|
||||||
|
//! dead actor is treated as absent and dropped right there. This keeps the
|
||||||
|
//! registry decoupled from actor teardown, at the cost of a dead binding
|
||||||
|
//! lingering until something happens to look at it.
|
||||||
|
//!
|
||||||
|
//! The whole registry (both the name index and the per-actor mailboxes) sits
|
||||||
|
//! behind one lock, which is what lets a name-addressed [`send`] resolve and
|
||||||
|
//! clone the target's sender in a single critical section. The sender is
|
||||||
|
//! cloned while that lock is held, then the lock is released before the
|
||||||
|
//! actual send, since delivering a message can wake a parked receiver and
|
||||||
|
//! that wakeup work should not run while the registry is locked.
|
||||||
|
|
||||||
use crate::channel::Sender;
|
use crate::channel::Sender;
|
||||||
use crate::pid::{Addressable, Name, Pid};
|
use crate::pid::{Addressable, Name, Pid};
|
||||||
@@ -80,28 +132,33 @@ impl std::fmt::Display for RegisterError {
|
|||||||
|
|
||||||
impl std::error::Error for RegisterError {}
|
impl std::error::Error for RegisterError {}
|
||||||
|
|
||||||
/// Why a name-addressed [`send`] did not deliver. Carries the message back so
|
/// Why a send did not deliver. Every variant carries the undelivered message
|
||||||
/// the caller never loses it (mirrors [`crate::channel::SendError`]).
|
/// back, mirroring [`crate::channel::SendError`], so a failed send never
|
||||||
|
/// silently drops what you tried to send.
|
||||||
///
|
///
|
||||||
/// `Debug`/`Display` are hand-written so neither demands `M: Debug` — the
|
/// `Debug` and `Display` are hand-written so neither requires `M: Debug`,
|
||||||
/// payload is returned, not printed.
|
/// since the payload is handed back to you, not printed.
|
||||||
pub enum SendError<M> {
|
pub enum SendError<M> {
|
||||||
/// No live actor is currently registered under this name. Name-addressed
|
/// No live actor is currently registered under this name. Returned only
|
||||||
/// [`send`] only; the pid-addressed counterpart is [`SendError::Dead`].
|
/// by name-addressed [`send`]; the pid-addressed counterpart of "nothing
|
||||||
|
/// there" is [`SendError::Dead`].
|
||||||
Unresolved(M),
|
Unresolved(M),
|
||||||
/// The pid-addressed actor is no longer the live incarnation this pid names
|
/// The actor this pid identifies has died, even if its slot has since
|
||||||
/// — it has died, even if its slot now holds a *different* actor (a direct
|
/// been taken over by a different, live actor. A direct `Pid<A>` send
|
||||||
/// `Pid<A>` send never redirects; contrast name-addressed [`send`]). Pid
|
/// never redirects to that new occupant; contrast name-addressed
|
||||||
/// paths ([`send_to`] / [`send_dyn`]) only.
|
/// [`send`], which would reach it. Returned by the pid-addressed sends,
|
||||||
|
/// [`send_to`] and [`send_dyn`].
|
||||||
Dead(M),
|
Dead(M),
|
||||||
/// The actor is live but exposes no channel for this message type.
|
/// The actor is live but has not published a channel for this message
|
||||||
|
/// type.
|
||||||
NoChannel(M),
|
NoChannel(M),
|
||||||
/// The actor's channel for this message type is closed (its receiver is gone).
|
/// The actor's channel for this message type is closed (its receiver has
|
||||||
|
/// been dropped).
|
||||||
Closed(M),
|
Closed(M),
|
||||||
/// No live member to deliver to — a [`dispatch`](crate::dispatch) over an
|
/// No live member was available to deliver to: returned by
|
||||||
/// empty (or all-dead) process group. Group-addressed dispatch only; the
|
/// [`dispatch`](crate::dispatch) when the target process group is empty
|
||||||
/// name-addressed counterpart is [`SendError::Unresolved`]. The message is
|
/// or every member in it has died. The name-addressed counterpart of
|
||||||
/// handed back undelivered.
|
/// this case is [`SendError::Unresolved`].
|
||||||
NoMember(M),
|
NoMember(M),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,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"),
|
||||||
@@ -150,9 +209,9 @@ impl<M> std::error::Error for SendError<M> {}
|
|||||||
|
|
||||||
/// A registry-stored channel, type-erased over its message type. The stored
|
/// A registry-stored channel, type-erased over its message type. The stored
|
||||||
/// object must serve two readers: `clone_sender` (downcast back to the concrete
|
/// object must serve two readers: `clone_sender` (downcast back to the concrete
|
||||||
/// `Sender<M>`) and the RFC 016 snapshot (queued length without knowing `M`).
|
/// `Sender<M>`) and the runtime introspection snapshot (queued length without
|
||||||
/// A bare `Box<dyn Any>` gives the first but not the second, so we erase behind
|
/// knowing `M`). A bare `Box<dyn Any>` gives the first but not the second, so
|
||||||
/// this small trait instead.
|
/// we erase behind this small trait instead.
|
||||||
trait ErasedSender: Send {
|
trait ErasedSender: Send {
|
||||||
fn as_any(&self) -> &dyn Any;
|
fn as_any(&self) -> &dyn Any;
|
||||||
fn queued_len(&self) -> usize;
|
fn queued_len(&self) -> usize;
|
||||||
@@ -169,7 +228,7 @@ impl<M: Send + 'static> ErasedSender for Sender<M> {
|
|||||||
|
|
||||||
/// One typed channel of an actor, type-erased. Concretely a `Sender<M>` filed
|
/// One typed channel of an actor, type-erased. Concretely a `Sender<M>` filed
|
||||||
/// under `TypeId::of::<M>()`; `msg_type` is `type_name::<M>()`, kept for
|
/// under `TypeId::of::<M>()`; `msg_type` is `type_name::<M>()`, kept for
|
||||||
/// observers (RFC 014 §4.5) and as the debug cross-check on the downcast.
|
/// observability tooling and as the debug cross-check on the downcast.
|
||||||
struct Channel {
|
struct Channel {
|
||||||
sender: Box<dyn ErasedSender>,
|
sender: Box<dyn ErasedSender>,
|
||||||
msg_type: &'static str,
|
msg_type: &'static str,
|
||||||
@@ -185,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
|
||||||
@@ -204,7 +266,7 @@ impl Mailbox {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-actor registry view handed to RFC 016 introspection: registered names
|
/// Per-actor registry view handed to runtime introspection: registered names
|
||||||
/// and summed mailbox depth, tagged with the mailbox's `pid` so a stale
|
/// and summed mailbox depth, tagged with the mailbox's `pid` so a stale
|
||||||
/// incarnation can be filtered against the slab. Covers only *published*
|
/// incarnation can be filtered against the slab. Covers only *published*
|
||||||
/// channels (`register` / `install` / `spawn_addr` / gen_server start); an
|
/// channels (`register` / `install` / `spawn_addr` / gen_server start); an
|
||||||
@@ -217,39 +279,60 @@ pub(crate) struct MailboxInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The directory. Invariant (held under the registry lock): every value in
|
/// The directory. Invariant (held under the registry lock): every value in
|
||||||
/// `by_name` is the index of a [`Mailbox`] present in `by_index`, and that
|
/// `by_name` is the full [`Pid`] (index *and* generation) of an actor that
|
||||||
/// mailbox's `pid.index()` equals the key. Stale entries (dead actors) violate
|
/// published a [`Mailbox`] into `by_index` at registration time. Stale entries
|
||||||
/// nothing — they are simply pruned on contact.
|
/// (dead holders, including holders whose slot has since been re-tenanted by
|
||||||
|
/// a different actor) violate nothing: they are pruned on contact, and the
|
||||||
|
/// generation makes "dead" decidable even after slot reuse.
|
||||||
pub(crate) struct Registry {
|
pub(crate) struct Registry {
|
||||||
/// `pid.index() -> the actor's mailbox`. The handle store.
|
/// `pid.index() -> the actor's mailbox`. The handle store.
|
||||||
by_index: HashMap<u32, Mailbox>,
|
by_index: HashMap<u32, Mailbox>,
|
||||||
/// `name -> pid.index()`. Several names may map to one actor.
|
/// `name -> holder pid`. Several names may map to one actor. The full pid
|
||||||
by_name: HashMap<&'static str, u32>,
|
/// (not just the index) is load-bearing: an index alone cannot tell a dead
|
||||||
|
/// holder from the live actor now tenanting its recycled slot. Comparing
|
||||||
|
/// only the index would make such a name read as live-held (unresolvable
|
||||||
|
/// and unregisterable at once) and could misdeliver to whatever new,
|
||||||
|
/// same-typed actor now sits in that slot.
|
||||||
|
by_name: HashMap<&'static str, Pid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
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 actor's mailbox and every name that pointed at it.
|
/// Drop a dead holder's artifacts: every name bound to it, and its
|
||||||
fn prune(&mut self, index: u32) {
|
/// mailbox, but only while the mailbox is still *its own*. A recycled
|
||||||
self.by_index.remove(&index);
|
/// slot's mailbox belongs to the live tenant (publish replaces it
|
||||||
self.by_name.retain(|_, idx| *idx != index);
|
/// wholesale on pid mismatch) and is left untouched.
|
||||||
|
fn prune_holder(&mut self, holder: Pid) {
|
||||||
|
self.by_name.retain(|_, p| *p != holder);
|
||||||
|
if self
|
||||||
|
.by_index
|
||||||
|
.get(&holder.index())
|
||||||
|
.is_some_and(|mb| mb.pid == holder)
|
||||||
|
{
|
||||||
|
self.by_index.remove(&holder.index());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RFC 016 snapshot input: per-slot-index registry view — the actor's
|
/// Runtime introspection input: per-slot-index registry view, giving the
|
||||||
/// registered names (inverted from `by_name`) and its mailbox depth (queued
|
/// actor's registered names (inverted from `by_name`) and its mailbox
|
||||||
/// messages summed across every published typed channel). Built in one pass
|
/// depth (queued messages summed across every published typed channel).
|
||||||
/// under the registry Leaf; the per-channel `queued_len` takes a Channel
|
/// Carries each mailbox's full `pid` so the caller can discard a stale
|
||||||
/// lock, legal under the Leaf (Leaf → Channel). Carries each mailbox's full
|
/// incarnation's entry against the slab's live generation. Names are
|
||||||
/// `pid` so the caller can discard a stale incarnation's entry against the
|
/// matched to mailboxes by *full pid*, so a stale name (dead holder)
|
||||||
/// slab's live generation. Names that dangle (point at no mailbox) are
|
/// still annotates the corpse's own mailbox if that survives, but never a
|
||||||
/// dropped — they violate no invariant and get pruned on next contact.
|
/// recycled slot's new tenant; names that attach to no mailbox are
|
||||||
|
/// dropped, since that violates no invariant and they get pruned on next
|
||||||
|
/// contact.
|
||||||
pub(crate) fn introspect_map(&self) -> HashMap<u32, MailboxInfo> {
|
pub(crate) fn introspect_map(&self) -> HashMap<u32, MailboxInfo> {
|
||||||
let mut names: HashMap<u32, Vec<&'static str>> = HashMap::new();
|
let mut names: HashMap<Pid, Vec<&'static str>> = HashMap::new();
|
||||||
for (&name, &idx) in &self.by_name {
|
for (&name, &pid) in &self.by_name {
|
||||||
names.entry(idx).or_default().push(name);
|
names.entry(pid).or_default().push(name);
|
||||||
}
|
}
|
||||||
let mut out: HashMap<u32, MailboxInfo> = HashMap::with_capacity(self.by_index.len());
|
let mut out: HashMap<u32, MailboxInfo> = HashMap::with_capacity(self.by_index.len());
|
||||||
for (&idx, mb) in &self.by_index {
|
for (&idx, mb) in &self.by_index {
|
||||||
@@ -258,7 +341,7 @@ impl Registry {
|
|||||||
idx,
|
idx,
|
||||||
MailboxInfo {
|
MailboxInfo {
|
||||||
pid: mb.pid,
|
pid: mb.pid,
|
||||||
names: names.remove(&idx).unwrap_or_default(),
|
names: names.remove(&mb.pid).unwrap_or_default(),
|
||||||
depth: depth.min(u32::MAX as usize) as u32,
|
depth: depth.min(u32::MAX as usize) as u32,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -268,17 +351,22 @@ impl Registry {
|
|||||||
|
|
||||||
/// Single-actor form of [`introspect_map`](Self::introspect_map): the
|
/// Single-actor form of [`introspect_map`](Self::introspect_map): the
|
||||||
/// registry view for one slot index, or `None` if no mailbox is published
|
/// registry view for one slot index, or `None` if no mailbox is published
|
||||||
/// there. Used by `actor_info` so its cost stays proportional to the one
|
/// there. Used by the runtime's per-actor introspection so its cost stays
|
||||||
/// actor rather than locking every channel in the runtime.
|
/// proportional to the one actor rather than locking every channel in the
|
||||||
|
/// runtime.
|
||||||
pub(crate) fn introspect_one(&self, idx: u32) -> Option<MailboxInfo> {
|
pub(crate) fn introspect_one(&self, idx: u32) -> Option<MailboxInfo> {
|
||||||
let mb = self.by_index.get(&idx)?;
|
let mb = self.by_index.get(&idx)?;
|
||||||
let depth: usize = mb.channels.values().map(|c| c.sender.queued_len()).sum();
|
let depth: usize = mb.channels.values().map(|c| c.sender.queued_len()).sum();
|
||||||
let names = self
|
let names = self
|
||||||
.by_name
|
.by_name
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(&n, &i)| (i == idx).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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,14 +375,18 @@ fn live(inner: &crate::runtime::RuntimeInner, pid: Pid) -> bool {
|
|||||||
inner.slot_at(pid).is_some_and(|s| s.is_live_for(pid))
|
inner.slot_at(pid).is_some_and(|s| s.is_live_for(pid))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Publish the current actor's `Sender<M>` under `name`, capturing the channel
|
/// Give the current actor's channel a name, so other actors can find and
|
||||||
/// so the name becomes messageable. Idempotent for the same `(name, type)`;
|
/// message it by that name instead of needing its [`Pid`].
|
||||||
/// registering a *second* type under the same (or another) name on the same
|
|
||||||
/// actor just adds another channel to the actor's mailbox.
|
|
||||||
///
|
///
|
||||||
/// Fails with [`RegisterError::NameTaken`] if the name is held by a *different*
|
/// Calling this again with the same `(name, type)` from the same actor is
|
||||||
/// live actor (a binding to a dead actor is pruned and the name treated as
|
/// harmless. Registering a *second* message type under the same (or a
|
||||||
/// free). Panics if called outside `Runtime::run()`.
|
/// different) name from the same actor just adds another typed channel to
|
||||||
|
/// that actor's mailbox; it does not replace the first.
|
||||||
|
///
|
||||||
|
/// Fails with [`RegisterError::NameTaken`] if the name is currently held by a
|
||||||
|
/// *different* live actor. A name held by an actor that has since died is not
|
||||||
|
/// considered taken: it is quietly reclaimed and handed to you. Panics if
|
||||||
|
/// called outside [`run`](crate::run).
|
||||||
pub fn register<M: Send + 'static>(name: Name<M>, tx: Sender<M>) -> Result<(), RegisterError> {
|
pub fn register<M: Send + 'static>(name: Name<M>, tx: Sender<M>) -> Result<(), RegisterError> {
|
||||||
register_with(self_pid(), name.as_str(), tx)
|
register_with(self_pid(), name.as_str(), tx)
|
||||||
}
|
}
|
||||||
@@ -302,8 +394,8 @@ pub fn register<M: Send + 'static>(name: Name<M>, tx: Sender<M>) -> Result<(), R
|
|||||||
/// Bind `name` to `pid`'s mailbox and publish `tx` under `M`'s [`TypeId`], for
|
/// Bind `name` to `pid`'s mailbox and publish `tx` under `M`'s [`TypeId`], for
|
||||||
/// an explicit (already-live) actor rather than `self`. The shared core of
|
/// an explicit (already-live) actor rather than `self`. The shared core of
|
||||||
/// [`register`] (which passes `self_pid()`) and the parent-side server-name
|
/// [`register`] (which passes `self_pid()`) and the parent-side server-name
|
||||||
/// bind in `gen_server` (which names a freshly spawned server before its body
|
/// bind in `gen_server`, which names a freshly spawned server before its body
|
||||||
/// has run, so the name resolves the instant `start()` returns). Same collision
|
/// has run, so the name resolves the instant `start()` returns. Same collision
|
||||||
/// rules and lock discipline as `register`.
|
/// rules and lock discipline as `register`.
|
||||||
pub(crate) fn register_with<M: Send + 'static>(
|
pub(crate) fn register_with<M: Send + 'static>(
|
||||||
me: Pid,
|
me: Pid,
|
||||||
@@ -311,25 +403,36 @@ 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);
|
||||||
}
|
}
|
||||||
if let Some(&holder_idx) = reg.by_name.get(key) {
|
if let Some(&holder) = reg.by_name.get(key) {
|
||||||
match reg.by_index.get(&holder_idx).map(|m| m.pid) {
|
if holder == me {
|
||||||
Some(holder) if holder == me => {} // same actor: just add the channel below
|
// Same actor: just add the channel below.
|
||||||
Some(holder) if live(inner, holder) => {
|
} else if live(inner, holder) {
|
||||||
return Err(RegisterError::NameTaken { holder });
|
return Err(RegisterError::NameTaken { holder });
|
||||||
}
|
} else {
|
||||||
Some(_) => reg.prune(holder_idx), // dead holder: free the name
|
// Dead holder: free the name (and its other stale artifacts).
|
||||||
None => {
|
// Liveness is judged against the *stored* pid, generation
|
||||||
reg.by_name.remove(key); // dangling name: free it
|
// included, so a recycled slot's live tenant no longer makes a
|
||||||
}
|
// dead name read as taken.
|
||||||
|
reg.prune_holder(holder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Publish (or extend) the mailbox with this channel, then bind the name.
|
// Publish (or extend) the mailbox with this channel, then bind the name.
|
||||||
publish_channel::<M>(&mut reg, me, tx);
|
publish_channel::<M>(&mut reg, me, tx);
|
||||||
reg.by_name.insert(key, me.index());
|
reg.by_name.insert(key, me);
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -340,26 +443,32 @@ 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>(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Publish the current actor's `Sender<A::Msg>` into its mailbox **without**
|
/// Publish the current actor's `Sender<A::Msg>` into its mailbox **without**
|
||||||
/// binding a name, and hand back the typed [`Pid<A>`] that addresses this
|
/// binding a name, and hand back the typed [`Pid<A>`] that addresses this
|
||||||
/// actor directly. This is the opt-in, lazy install of RFC 014 §5: an actor
|
/// actor directly.
|
||||||
/// that wants to be reachable by a direct, identity-bound [`Pid<A>`] (rather
|
|
||||||
/// than only via a re-resolving [`Name`]) calls this once with its inbox
|
|
||||||
/// sender, then hands the returned pid out.
|
|
||||||
///
|
///
|
||||||
/// Unlike [`register`] there is no name to collide on, and `self` is always a
|
/// This is for an actor that wants to be reachable directly by its pid,
|
||||||
/// live actor inside `run()`, so this is infallible. Panics if called outside
|
/// rather than only through a re-resolving [`Name`]: call this once with your
|
||||||
/// `Runtime::run()`.
|
/// inbox sender, then hand the returned `Pid<A>` to whoever should be able to
|
||||||
|
/// message you. Unlike [`register`] there is no name to collide on, and the
|
||||||
|
/// current actor is always live while inside `run()`, so this cannot fail.
|
||||||
|
/// Panics if called outside [`run`](crate::run).
|
||||||
pub fn install<A: Addressable>(tx: Sender<A::Msg>) -> Pid<A> {
|
pub fn install<A: Addressable>(tx: Sender<A::Msg>) -> Pid<A> {
|
||||||
let me = self_pid();
|
let me = self_pid();
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| {
|
||||||
@@ -375,158 +484,217 @@ pub fn install<A: Addressable>(tx: Sender<A::Msg>) -> Pid<A> {
|
|||||||
/// Publish `tx` into `pid`'s mailbox under `M`'s [`TypeId`], for an explicit
|
/// Publish `tx` into `pid`'s mailbox under `M`'s [`TypeId`], for an explicit
|
||||||
/// (freshly minted, already-live) actor rather than `self`. The parent-side
|
/// (freshly minted, already-live) actor rather than `self`. The parent-side
|
||||||
/// half of [`spawn_addr`](crate::spawn_addr): the spawner makes the inbox and
|
/// half of [`spawn_addr`](crate::spawn_addr): the spawner makes the inbox and
|
||||||
/// publishes the sender here *before* handing back the `Pid<A>`, so an immediate
|
/// publishes the sender here *before* handing back the `Pid<A>`, so an
|
||||||
/// `send_to` on the returned pid always resolves — the address is live the
|
/// immediate `send_to` on the returned pid always resolves. The address is
|
||||||
/// instant the caller holds it, with no dependence on the body having run yet.
|
/// live the instant the caller holds it, with no dependence on the spawned
|
||||||
|
/// actor's body having run yet.
|
||||||
///
|
///
|
||||||
/// Caller guarantees `pid` is the just-installed actor (Queued, this exact
|
/// Caller guarantees `pid` is the just-installed actor (queued, this exact
|
||||||
/// incarnation); `publish_channel` replaces any stale leftover at the slot.
|
/// incarnation); `publish_channel` replaces any stale leftover at the slot.
|
||||||
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The single actor currently registered under `name`, or `None` if unbound or
|
/// Look up which actor currently holds `name`, if any. Returns `None` if the
|
||||||
/// no longer live (the stale binding is pruned on the way out).
|
/// name is unbound, or if it was bound to an actor that has since died (the
|
||||||
|
/// stale binding is cleared as a side effect of this call).
|
||||||
pub fn whereis(name: &str) -> Option<Pid> {
|
pub fn whereis(name: &str) -> Option<Pid> {
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| {
|
||||||
let mut reg = inner.registry.lock();
|
let mut reg = inner.registry.lock();
|
||||||
let idx = *reg.by_name.get(name)?;
|
let pid = *reg.by_name.get(name)?;
|
||||||
match reg.by_index.get(&idx).map(|m| m.pid) {
|
if live(inner, pid) {
|
||||||
Some(pid) if live(inner, pid) => Some(pid),
|
Some(pid)
|
||||||
Some(_) => {
|
} else {
|
||||||
reg.prune(idx);
|
// Generation-checked against the stored holder: a recycled slot's
|
||||||
|
// live tenant reads dead here, and the stale name heals.
|
||||||
|
reg.prune_holder(pid);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
None => {
|
|
||||||
reg.by_name.remove(name);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve `name` to a *typed* [`Pid<A>`] — the identity-bound counterpart of
|
/// What a name is bound to, three-valued (bridge soak signature 4).
|
||||||
/// [`whereis`] (RFC 014 §4.4). Recovers the compile-checked
|
|
||||||
/// [`send_to`] path from a durable name: looks the name up,
|
|
||||||
/// then re-types the erased pid as `Pid<A>` via the unchecked
|
|
||||||
/// [`assert_type`](crate::pid::assert_type) primitive. A wrong `A` is not
|
|
||||||
/// unsound — it degrades to [`SendError::NoChannel`] on the next send (routing
|
|
||||||
/// is by message `TypeId`), never a misdelivery. `None` if unbound or dead.
|
|
||||||
///
|
///
|
||||||
/// Panics if called outside `Runtime::run()`.
|
/// [`Live`](NameResolution::Live) is [`whereis`]'s `Some`.
|
||||||
|
/// [`Corpse`](NameResolution::Corpse) carries the *stored* holder pid of a
|
||||||
|
/// dead-but-unpruned binding — a state Erlang cannot represent (its name
|
||||||
|
/// death unregisters atomically; smarm's prune is lazy), captured here before
|
||||||
|
/// the prune that `whereis` performs discards it, so the caller can consult
|
||||||
|
/// [`terminal_reason`](crate::monitor::terminal_reason) for the tenancy's
|
||||||
|
/// real down reason. [`Unbound`](NameResolution::Unbound) matches Erlang's
|
||||||
|
/// unregistered name.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum NameResolution {
|
||||||
|
/// The stored holder is live (generation-checked); the binding stands.
|
||||||
|
Live(Pid),
|
||||||
|
/// The stored holder is dead. The binding was pruned on the way out —
|
||||||
|
/// the name heals exactly as `whereis` heals it; only the evidence is
|
||||||
|
/// returned instead of discarded. A second resolve is `Unbound`.
|
||||||
|
Corpse(Pid),
|
||||||
|
/// No binding stored (never registered, or already pruned by any reader).
|
||||||
|
Unbound,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve `name` like [`whereis`], but keep the corpse: the dead-holder arm
|
||||||
|
/// returns the stored pid it pruned instead of a bare `None`. Same lock
|
||||||
|
/// discipline and pruning behavior as `whereis`; same `Runtime::run()`
|
||||||
|
/// context contract.
|
||||||
|
pub fn resolve_name(name: &str) -> NameResolution {
|
||||||
|
with_runtime(|inner| {
|
||||||
|
let mut reg = inner.registry.lock();
|
||||||
|
let Some(&pid) = reg.by_name.get(name) else {
|
||||||
|
return NameResolution::Unbound;
|
||||||
|
};
|
||||||
|
if live(inner, pid) {
|
||||||
|
NameResolution::Live(pid)
|
||||||
|
} else {
|
||||||
|
reg.prune_holder(pid);
|
||||||
|
NameResolution::Corpse(pid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like [`whereis`], but returns a *typed* [`Pid<A>`] instead of a bare
|
||||||
|
/// [`Pid`], so a follow-up [`send_to`] is compile-checked instead of needing
|
||||||
|
/// the untyped [`send_dyn`] escape hatch. `None` if the name is unbound or its
|
||||||
|
/// holder has died.
|
||||||
|
///
|
||||||
|
/// The type `A` is not checked against what the name's holder actually
|
||||||
|
/// published: if you pick the wrong `A`, this still succeeds, but the next
|
||||||
|
/// send against the returned pid degrades to [`SendError::NoChannel`] rather
|
||||||
|
/// than reaching the wrong actor or the wrong channel.
|
||||||
|
///
|
||||||
|
/// Panics if called outside [`run`](crate::run).
|
||||||
pub fn lookup_as<A: Addressable>(name: &str) -> Option<Pid<A>> {
|
pub fn lookup_as<A: Addressable>(name: &str) -> Option<Pid<A>> {
|
||||||
whereis(name).map(crate::pid::assert_type::<A>)
|
whereis(name).map(crate::pid::assert_type::<A>)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve `name` to its actor's pid and a cloned `Sender<M>`, under the Leaf
|
/// Resolve `name` to its actor's pid and a cloned `Sender<M>`, all under one
|
||||||
/// lock (clone-under-lock, then release). The crate-internal building block for
|
/// lock acquisition. The crate-internal building block for `gen_server`'s
|
||||||
/// `gen_server`'s by-name addressing: a named server publishes its inbox as a
|
/// by-name addressing: a named server publishes its inbox as a
|
||||||
/// `Sender<Envelope<G>>` (via [`register_with`]), and `whereis_server` / `call`
|
/// `Sender<Envelope<G>>` (via [`register_with`]), and the server's `call` /
|
||||||
/// / `cast` recover that exact typed sender here to rebuild a `GenServerRef<G>`.
|
/// `cast` / `whereis_server` recover that exact typed sender here to rebuild a
|
||||||
/// `None` if unbound, dead (pruned on the way out), or holding no `M` channel.
|
/// `GenServerRef<G>`. `None` if unbound, dead (pruned on the way out), or
|
||||||
|
/// holding no `M` channel.
|
||||||
pub(crate) fn resolve_named_sender<M: Send + 'static>(name: &str) -> Option<(Pid, Sender<M>)> {
|
pub(crate) fn resolve_named_sender<M: Send + 'static>(name: &str) -> Option<(Pid, Sender<M>)> {
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| {
|
||||||
let mut reg = inner.registry.lock();
|
let mut reg = inner.registry.lock();
|
||||||
let idx = *reg.by_name.get(name)?;
|
let pid = *reg.by_name.get(name)?;
|
||||||
let pid = match reg.by_index.get(&idx).map(|m| m.pid) {
|
if !live(inner, pid) {
|
||||||
Some(pid) if live(inner, pid) => pid,
|
// Stored-pid liveness, generation included: a name whose holder
|
||||||
Some(_) => {
|
// died is pruned (heals) even if the slot has a new tenant.
|
||||||
reg.prune(idx);
|
// Otherwise the tenant's mailbox would make the name unresolvable
|
||||||
|
// without pruning, wedging it for the tenant's lifetime.
|
||||||
|
reg.prune_holder(pid);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
None => {
|
// A live holder's mailbox is its own (publish replaces wholesale on
|
||||||
reg.by_name.remove(name);
|
// pid mismatch, and one live actor per slot), so index lookup is safe.
|
||||||
return None;
|
let tx = reg
|
||||||
}
|
.by_index
|
||||||
};
|
.get(&pid.index())
|
||||||
let tx = reg.by_index.get(&idx).and_then(Mailbox::clone_sender::<M>)?;
|
.and_then(Mailbox::clone_sender::<M>)?;
|
||||||
Some((pid, tx))
|
Some((pid, tx))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove the binding for `name`, returning the actor it pointed at if still
|
/// Give up a name. Returns the actor it pointed at, if that actor was still
|
||||||
/// live. Only the *name* is freed; the actor's mailbox (and any other names for
|
/// live. Only the *name* is freed; the actor's mailbox (and any other names
|
||||||
/// it) remain. A binding to a dead actor is reported as `None`.
|
/// bound to it) are unaffected. A binding to an already-dead actor reports
|
||||||
|
/// `None`, since there was nothing live to release.
|
||||||
pub fn unregister(name: &str) -> Option<Pid> {
|
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 idx = reg.by_name.remove(name)?;
|
let pid = reg.by_name.remove(name)?;
|
||||||
match reg.by_index.get(&idx).map(|m| m.pid) {
|
if live(inner, pid) {
|
||||||
Some(pid) if live(inner, pid) => Some(pid),
|
Some(pid)
|
||||||
_ => None,
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve `name` to its actor's `Sender<M>` and deliver `msg`. The whole point
|
/// Look `name` up and deliver `msg` to whichever actor currently holds it.
|
||||||
/// of the rework: a name you can *send* to.
|
/// This is the point of naming an actor: a name you can send a message to
|
||||||
|
/// directly, without a separate lookup step.
|
||||||
///
|
///
|
||||||
/// Errors (message returned in every case): [`SendError::Unresolved`] if no
|
/// On failure the message comes back to you, wrapped in the [`SendError`]
|
||||||
/// live actor holds the name, [`SendError::NoChannel`] if that actor has no
|
/// variant that explains why: [`SendError::Unresolved`] if no live actor
|
||||||
/// channel for `M`, [`SendError::Closed`] if its `M` channel's receiver is
|
/// currently holds the name, [`SendError::NoChannel`] if the actor that holds
|
||||||
/// gone. Panics if called outside `Runtime::run()`.
|
/// it never published a channel for `M`, or [`SendError::Closed`] if it
|
||||||
|
/// published one but has since dropped the receiving end. Panics if called
|
||||||
|
/// outside [`run`](crate::run).
|
||||||
pub fn send<M: Send + 'static>(name: Name<M>, msg: M) -> Result<(), SendError<M>> {
|
pub fn send<M: Send + 'static>(name: Name<M>, msg: M) -> Result<(), SendError<M>> {
|
||||||
let key = name.as_str();
|
let key = name.as_str();
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| {
|
||||||
// Resolve + clone the sender under the Leaf lock, then drop the lock
|
// Resolve + clone the sender under the registry lock, then drop the
|
||||||
// before sending (a send can unpark a receiver).
|
// lock before sending (a send can unpark a receiver).
|
||||||
let tx = {
|
let tx = {
|
||||||
let mut reg = inner.registry.lock();
|
let mut reg = inner.registry.lock();
|
||||||
let idx = match reg.by_name.get(key) {
|
let pid = match reg.by_name.get(key) {
|
||||||
Some(&i) => i,
|
Some(&p) => p,
|
||||||
None => return Err(SendError::Unresolved(msg)),
|
None => return Err(SendError::Unresolved(msg)),
|
||||||
};
|
};
|
||||||
let pid = match reg.by_index.get(&idx).map(|m| m.pid) {
|
|
||||||
Some(pid) => pid,
|
|
||||||
None => {
|
|
||||||
reg.by_name.remove(key);
|
|
||||||
return Err(SendError::Unresolved(msg));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if !live(inner, pid) {
|
if !live(inner, pid) {
|
||||||
reg.prune(idx);
|
// Stored-pid liveness (generation included), so a recycled
|
||||||
|
// slot's new live tenant is never mistaken for the name's
|
||||||
|
// original (now-dead) holder.
|
||||||
|
reg.prune_holder(pid);
|
||||||
return Err(SendError::Unresolved(msg));
|
return Err(SendError::Unresolved(msg));
|
||||||
}
|
}
|
||||||
match reg.by_index.get(&idx).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))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve a *raw* pid to its mailbox and deliver `msg` on the channel for `M`,
|
/// Resolve a *raw* pid to its mailbox and deliver `msg` on the channel for `M`,
|
||||||
/// with **no redirect**. The stored mailbox must be this exact incarnation
|
/// with **no redirect**. The stored mailbox must be this exact incarnation
|
||||||
/// (generation included) and still live; otherwise the actor this pid named is
|
/// (generation included) and still live; otherwise the actor this pid named
|
||||||
/// gone and the result is [`SendError::Dead`] — even when the slot now holds a
|
/// is gone and the result is [`SendError::Dead`], even when the slot now
|
||||||
/// different, live actor (which we leave untouched). Shared by [`send_to`]
|
/// holds a different, live actor (which is left untouched). Shared by
|
||||||
/// (typed, `M = A::Msg`, channel guaranteed on an installed actor) and
|
/// [`send_to`] (typed, `M = A::Msg`, channel guaranteed on an installed
|
||||||
/// [`send_dyn`] (explicit `M`, where `NoChannel` is a real outcome).
|
/// actor) and [`send_dyn`] (explicit `M`, where `NoChannel` is a real
|
||||||
|
/// outcome).
|
||||||
fn send_to_pid<M: Send + 'static>(
|
fn send_to_pid<M: Send + 'static>(
|
||||||
inner: &crate::runtime::RuntimeInner,
|
inner: &crate::runtime::RuntimeInner,
|
||||||
pid: Pid,
|
pid: Pid,
|
||||||
msg: M,
|
msg: M,
|
||||||
) -> Result<(), SendError<M>> {
|
) -> Result<(), SendError<M>> {
|
||||||
// Resolve + clone the sender under the Leaf lock, then drop the lock before
|
// Resolve + clone the sender under the registry lock, then drop the lock
|
||||||
// sending (a send can unpark a receiver) — Leaf -> Channel, as name `send`.
|
// before sending (a send can unpark a receiver), same order as `send`.
|
||||||
let tx = {
|
let tx = {
|
||||||
let mut reg = inner.registry.lock();
|
let mut reg = inner.registry.lock();
|
||||||
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)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Our incarnation's mailbox, but the actor has died: prune + Dead.
|
// Our incarnation's mailbox, but the actor has died: prune + Dead.
|
||||||
Some(stored) if stored == pid => {
|
Some(stored) if stored == pid => {
|
||||||
reg.prune(pid.index());
|
reg.prune_holder(pid);
|
||||||
return Err(SendError::Dead(msg));
|
return Err(SendError::Dead(msg));
|
||||||
}
|
}
|
||||||
// A different incarnation (or nothing) occupies the slot: the actor
|
// A different incarnation (or nothing) occupies the slot: the actor
|
||||||
@@ -534,35 +702,40 @@ 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` to the exact actor named by `pid` — RFC 014 §4.2's direct,
|
/// Deliver `msg` directly to the exact actor identified by `pid`. Unlike
|
||||||
/// identity-bound addressing mode. Unlike name-addressed [`send`] there is **no
|
/// name-addressed [`send`], there is **no redirect**: if that specific actor
|
||||||
/// redirect**: if that incarnation has died the message comes back as
|
/// has died, the message comes back as [`SendError::Dead`], even if its slot
|
||||||
/// [`SendError::Dead`], even if its slot now holds a different actor.
|
/// has since been taken over by a different, live actor. Use this when you
|
||||||
|
/// already hold a `Pid<A>` and want to talk to that one actor specifically;
|
||||||
|
/// use [`send`] with a [`Name`] when you want whichever actor currently holds
|
||||||
|
/// a name.
|
||||||
///
|
///
|
||||||
/// The message type is the actor's `A::Msg`, so on a live actor that has
|
/// The message type is the actor's `A::Msg`, so on a live actor that has
|
||||||
/// installed its inbox (via [`install`] or [`register`]) the channel is always
|
/// installed its inbox (via [`install`] or [`register`]) the channel is
|
||||||
/// present; [`SendError::NoChannel`] therefore means the actor is live but
|
/// always present; [`SendError::NoChannel`] therefore means the actor is live
|
||||||
/// never published a `Pid<A>`-reachable inbox. Panics if called outside
|
/// but never published a `Pid<A>`-reachable inbox. Panics if called outside
|
||||||
/// `Runtime::run()`.
|
/// [`run`](crate::run).
|
||||||
pub fn send_to<A: Addressable>(pid: Pid<A>, msg: A::Msg) -> Result<(), SendError<A::Msg>> {
|
pub fn send_to<A: Addressable>(pid: Pid<A>, msg: A::Msg) -> Result<(), SendError<A::Msg>> {
|
||||||
with_runtime(|inner| send_to_pid::<A::Msg>(inner, pid.erase(), msg))
|
with_runtime(|inner| send_to_pid::<A::Msg>(inner, pid.erase(), msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The explicit bare-pid escape hatch (RFC 014 §4.6): deliver `msg` of type `M`
|
/// The escape hatch for sending to a bare, untyped [`Pid`] when the typed
|
||||||
/// to `pid` when all you hold is an untyped [`Pid`] — a pid off a [`Down`], or
|
/// [`send_to`] is unavailable, for example a pid recovered from a [`Down`]
|
||||||
/// out of a future `members()` — so the typed [`send_to`] is unavailable.
|
/// notification or a group's `members()` list, where you no longer know the
|
||||||
|
/// actor's message type at compile time.
|
||||||
///
|
///
|
||||||
/// This is the one send whose message type can genuinely be wrong: the actor
|
/// Because the message type is not checked at compile time here, this is the
|
||||||
/// may be live yet expose no channel for `M`, returning [`SendError::NoChannel`]
|
/// one send that can genuinely be live-but-wrong: the actor may be alive yet
|
||||||
/// (on the typed paths that downcast collapses to a `debug_assert`). It is
|
/// expose no channel for `M`, in which case you get [`SendError::NoChannel`]
|
||||||
/// named and documented as the fallible fallback so the typed `Pid<A>` /
|
/// back instead of a misdelivery. Liveness and redirect behavior are
|
||||||
/// `Name<M>` paths stay the obvious default and an agentic caller reaches for a
|
/// otherwise identical to [`send_to`]: identity-bound, no redirect,
|
||||||
/// present primitive instead of inventing a workaround. Liveness is identical
|
/// [`SendError::Dead`] once the addressed incarnation is gone. Prefer
|
||||||
/// to [`send_to`]: identity-bound, no redirect, [`SendError::Dead`] once the
|
/// `send_to` with a typed `Pid<A>` whenever you have one; reach for this only
|
||||||
/// addressed incarnation is gone. Panics if called outside `Runtime::run()`.
|
/// when you don't. Panics if called outside [`run`](crate::run).
|
||||||
///
|
///
|
||||||
/// [`Down`]: crate::Down
|
/// [`Down`]: crate::Down
|
||||||
pub fn send_dyn<M: Send + 'static>(pid: Pid, msg: M) -> Result<(), SendError<M>> {
|
pub fn send_dyn<M: Send + 'static>(pid: Pid, msg: M) -> Result<(), SendError<M>> {
|
||||||
|
|||||||
+28
-5
@@ -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]
|
||||||
|
|||||||
+801
-200
File diff suppressed because it is too large
Load Diff
+638
-221
File diff suppressed because it is too large
Load Diff
+330
@@ -0,0 +1,330 @@
|
|||||||
|
//! RFC 019 §7 — overflow diagnostics.
|
||||||
|
//!
|
||||||
|
//! One process-global SIGSEGV handler, installed once at [`crate::runtime::init`]
|
||||||
|
//! (before any scheduler thread exists, so the PRIOR save is unracing), plus a
|
||||||
|
//! per-scheduler-thread `sigaltstack` registered at `schedule_loop` entry — a
|
||||||
|
//! guard hit means the faulting stack has no room to run anything, so the
|
||||||
|
//! altstack is not optional.
|
||||||
|
//!
|
||||||
|
//! The handler classifies `si_addr` against the *current* actor only, reached
|
||||||
|
//! through `preempt::CURRENT_SLOT` — a const-initialized `Cell<*const Slot>`
|
||||||
|
//! whose access is a plain TLS load (no lazy init, no allocation, no dtor
|
||||||
|
//! registration), and which every scheduler thread has materialized before an
|
||||||
|
//! actor can run on it. The slot's diag atomics (`diag_stack_top` & co) are
|
||||||
|
//! written in `install_actor` before the Release publish and are only consulted
|
||||||
|
//! here while the actor is on-CPU, so they cannot be stale.
|
||||||
|
//!
|
||||||
|
//! Two classification tiers:
|
||||||
|
//! - **In-guard**: definitive. Rust frames probe pages in order
|
||||||
|
//! (`__rust_probestack`), so Rust overflow always lands here; so does any C
|
||||||
|
//! built with `-fstack-clash-protection` (distro-packaged libraries), and —
|
||||||
|
//! with the 1 MiB default guard — nearly every unprobed frame too.
|
||||||
|
//! - **Overshoot**: within [`OVERSHOOT_SLOP`] *below* the guard. An unprobed
|
||||||
|
//! frame (cargo-built C via `cc` almost never enables clash protection)
|
||||||
|
//! large enough to step over the guard in one `sub rsp`. Attribution is
|
||||||
|
//! "probable": the address is in unmapped VA that nothing else owns, an
|
||||||
|
//! actor was on-CPU, and the distance fits a frame — the diagnostic says so.
|
||||||
|
//!
|
||||||
|
//! Classified faults print one line (async-signal-safe: stack buffer +
|
||||||
|
//! `write(2)`, no fmt, no alloc, no locks) and re-raise with default
|
||||||
|
//! disposition — no unwind, no resume, no fail-soft (jarred; UB-adjacent from
|
||||||
|
//! a handler). Unclassified faults reinstate the PRIOR handler and refault, so
|
||||||
|
//! std's own "thread ... has overflowed its stack" diagnostics for OS-thread
|
||||||
|
//! stacks survive our presence. Reinstating deregisters us for good, which is
|
||||||
|
//! fine: the process is dying either way.
|
||||||
|
|
||||||
|
use std::cell::Cell;
|
||||||
|
use std::mem::MaybeUninit;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
use std::sync::Once;
|
||||||
|
|
||||||
|
/// Tier-2 window below the guard. Matches the guard default (and the kernel's
|
||||||
|
/// `stack_guard_gap`): a frame that out-jumps both the guard and this window
|
||||||
|
/// in one displacement is past what a diagnostic can honestly attribute.
|
||||||
|
pub(crate) const OVERSHOOT_SLOP: usize = 1024 * 1024;
|
||||||
|
|
||||||
|
/// Per-scheduler-thread signal stack. MINSIGSTKSZ is ~11 KiB on AVX-512
|
||||||
|
/// hardware; 64 KiB leaves the formatter room without mattering to anyone.
|
||||||
|
/// One per OS thread, never freed: scheduler threads live for the process in
|
||||||
|
/// practice, and repeated `run()`s on reused threads re-use the registration
|
||||||
|
/// (the TLS flag), so the leak is bounded by the OS thread count.
|
||||||
|
const ALTSTACK_SIZE: usize = 64 * 1024;
|
||||||
|
|
||||||
|
static INSTALL: Once = Once::new();
|
||||||
|
/// The handler that was installed before ours (std's, typically). Written
|
||||||
|
/// exactly once inside INSTALL — which completes in `runtime::init` before
|
||||||
|
/// any scheduler thread (and thus any classifiable fault) can exist — and
|
||||||
|
/// only read from the handler afterwards.
|
||||||
|
static mut PRIOR: MaybeUninit<libc::sigaction> = MaybeUninit::uninit();
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
/// Whether this OS thread has registered its altstack.
|
||||||
|
static ALTSTACK_SET: Cell<bool> = const { Cell::new(false) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where a fault landed relative to the current actor's stack.
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) enum FaultClass {
|
||||||
|
/// Inside `[top − reserve − guard, top − reserve)`: the guard region.
|
||||||
|
Guard,
|
||||||
|
/// Within `OVERSHOOT_SLOP` below the guard: stepped over it. Payload is
|
||||||
|
/// the distance below `guard_lo`.
|
||||||
|
Overshoot(usize),
|
||||||
|
/// Not ours to explain.
|
||||||
|
Foreign,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pure classifier — all edges unit-tested below. `top` is the stack's usable
|
||||||
|
/// top, `reserve`/`guard` its shape; both page-rounded by `Stack::new`.
|
||||||
|
pub(crate) fn classify(addr: usize, top: usize, reserve: usize, guard: usize) -> FaultClass {
|
||||||
|
let guard_hi = top.wrapping_sub(reserve);
|
||||||
|
let guard_lo = guard_hi.wrapping_sub(guard);
|
||||||
|
if addr >= guard_lo && addr < guard_hi {
|
||||||
|
FaultClass::Guard
|
||||||
|
} else if addr < guard_lo && addr >= guard_lo.saturating_sub(OVERSHOOT_SLOP) {
|
||||||
|
FaultClass::Overshoot(guard_lo - addr)
|
||||||
|
} else {
|
||||||
|
FaultClass::Foreign
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Install the process-global handler. Idempotent; called from
|
||||||
|
/// `runtime::init`.
|
||||||
|
pub(crate) fn install_once() {
|
||||||
|
INSTALL.call_once(|| unsafe {
|
||||||
|
let mut sa: libc::sigaction = std::mem::zeroed();
|
||||||
|
sa.sa_sigaction = handler as *const () as usize;
|
||||||
|
sa.sa_flags = libc::SA_SIGINFO | libc::SA_ONSTACK;
|
||||||
|
libc::sigemptyset(&mut sa.sa_mask);
|
||||||
|
let prior = &mut *std::ptr::addr_of_mut!(PRIOR);
|
||||||
|
libc::sigaction(libc::SIGSEGV, &sa, prior.as_mut_ptr());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register this OS thread's altstack (idempotent per thread). Called at
|
||||||
|
/// `schedule_loop` entry, so every thread that can run an actor has one.
|
||||||
|
pub(crate) fn register_altstack() {
|
||||||
|
ALTSTACK_SET.with(|set| {
|
||||||
|
if set.get() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
let sp = libc::mmap(
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
ALTSTACK_SIZE,
|
||||||
|
libc::PROT_READ | libc::PROT_WRITE,
|
||||||
|
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
|
||||||
|
-1,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
if sp == libc::MAP_FAILED {
|
||||||
|
// Degrade: no altstack means a guard hit dies without the
|
||||||
|
// message (handler can't run) — the pre-RFC behavior, never
|
||||||
|
// incorrectness.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let ss = libc::stack_t {
|
||||||
|
ss_sp: sp,
|
||||||
|
ss_flags: 0,
|
||||||
|
ss_size: ALTSTACK_SIZE,
|
||||||
|
};
|
||||||
|
libc::sigaltstack(&ss, std::ptr::null_mut());
|
||||||
|
}
|
||||||
|
set.set(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// The handler
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
unsafe extern "C" fn handler(
|
||||||
|
_sig: libc::c_int,
|
||||||
|
info: *mut libc::siginfo_t,
|
||||||
|
_ctx: *mut libc::c_void,
|
||||||
|
) {
|
||||||
|
let slot_ptr = crate::preempt::current_slot_ptr();
|
||||||
|
if !slot_ptr.is_null() {
|
||||||
|
let slot = &*slot_ptr;
|
||||||
|
let top = slot.diag_stack_top.load(Ordering::Relaxed);
|
||||||
|
if top != 0 {
|
||||||
|
let reserve = slot.diag_stack_reserve.load(Ordering::Relaxed);
|
||||||
|
let guard = slot.diag_stack_guard.load(Ordering::Relaxed);
|
||||||
|
let pid = slot.diag_pid.load(Ordering::Relaxed);
|
||||||
|
let addr = (*info).si_addr() as usize;
|
||||||
|
match classify(addr, top, reserve, guard) {
|
||||||
|
FaultClass::Guard => {
|
||||||
|
let mut b = Buf::new();
|
||||||
|
b.s("smarm: actor ");
|
||||||
|
b.pid(pid);
|
||||||
|
b.s(" overflowed its stack: fault in the guard region, depth-at-fault=");
|
||||||
|
b.u(top - addr);
|
||||||
|
b.s(" bytes (reserve=");
|
||||||
|
b.u(reserve);
|
||||||
|
b.s(", guard=");
|
||||||
|
b.u(guard);
|
||||||
|
b.s("). Raise stack_reserve (SpawnOpts or Config).\n");
|
||||||
|
b.emit();
|
||||||
|
die_by_default();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FaultClass::Overshoot(below) => {
|
||||||
|
let mut b = Buf::new();
|
||||||
|
b.s("smarm: actor ");
|
||||||
|
b.pid(pid);
|
||||||
|
b.s(" probably overflowed its stack: fault ");
|
||||||
|
b.u(below);
|
||||||
|
b.s(" bytes below the guard - an unprobed (FFI?) frame stepped over it (reserve=");
|
||||||
|
b.u(reserve);
|
||||||
|
b.s(", guard=");
|
||||||
|
b.u(guard);
|
||||||
|
b.s("). Raise stack_guard or stack_reserve.\n");
|
||||||
|
b.emit();
|
||||||
|
die_by_default();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FaultClass::Foreign => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Not ours: put back whoever was there before us and refault into them.
|
||||||
|
let prior = &*std::ptr::addr_of!(PRIOR);
|
||||||
|
libc::sigaction(libc::SIGSEGV, prior.as_ptr(), std::ptr::null_mut());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset SIGSEGV to default disposition; returning from the handler then
|
||||||
|
/// refaults at the same instruction and the process dies the normal death
|
||||||
|
/// (core-dumpable, correct wait status), exactly as if we were never here —
|
||||||
|
/// but with the message already on stderr.
|
||||||
|
unsafe fn die_by_default() {
|
||||||
|
let mut dfl: libc::sigaction = std::mem::zeroed();
|
||||||
|
dfl.sa_sigaction = libc::SIG_DFL;
|
||||||
|
libc::sigemptyset(&mut dfl.sa_mask);
|
||||||
|
libc::sigaction(libc::SIGSEGV, &dfl, std::ptr::null_mut());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Async-signal-safe formatting: fixed buffer, decimal itoa, one write(2).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
struct Buf {
|
||||||
|
b: [u8; 320],
|
||||||
|
len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Buf {
|
||||||
|
fn new() -> Self {
|
||||||
|
Buf {
|
||||||
|
b: [0; 320],
|
||||||
|
len: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn s(&mut self, s: &str) {
|
||||||
|
for &c in s.as_bytes() {
|
||||||
|
if self.len < self.b.len() {
|
||||||
|
self.b[self.len] = c;
|
||||||
|
self.len += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn u(&mut self, mut n: usize) {
|
||||||
|
let mut tmp = [0u8; 20];
|
||||||
|
let mut i = tmp.len();
|
||||||
|
loop {
|
||||||
|
i -= 1;
|
||||||
|
tmp[i] = b'0' + (n % 10) as u8;
|
||||||
|
n /= 10;
|
||||||
|
if n == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for &c in &tmp[i..] {
|
||||||
|
if self.len < self.b.len() {
|
||||||
|
self.b[self.len] = c;
|
||||||
|
self.len += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// `idx.gen`, unpacked from the install-time packing.
|
||||||
|
fn pid(&mut self, packed: u64) {
|
||||||
|
self.u((packed >> 32) as usize);
|
||||||
|
self.s(".");
|
||||||
|
self.u((packed & 0xffff_ffff) as usize);
|
||||||
|
}
|
||||||
|
fn emit(&self) {
|
||||||
|
unsafe {
|
||||||
|
libc::write(2, self.b.as_ptr() as *const libc::c_void, self.len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Classifier units — the arithmetic edges, before anything integrates.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{classify, FaultClass, OVERSHOOT_SLOP};
|
||||||
|
|
||||||
|
const PG: usize = 4096;
|
||||||
|
// A synthetic stack far from address-space edges: top at 1 GiB.
|
||||||
|
const TOP: usize = 1 << 30;
|
||||||
|
const RESERVE: usize = 16 * PG;
|
||||||
|
const GUARD: usize = 4 * PG;
|
||||||
|
const GUARD_HI: usize = TOP - RESERVE;
|
||||||
|
const GUARD_LO: usize = GUARD_HI - GUARD;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inside_guard_both_edges() {
|
||||||
|
assert_eq!(classify(GUARD_LO, TOP, RESERVE, GUARD), FaultClass::Guard);
|
||||||
|
assert_eq!(
|
||||||
|
classify(GUARD_HI - 1, TOP, RESERVE, GUARD),
|
||||||
|
FaultClass::Guard
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
classify(GUARD_LO + GUARD / 2, TOP, RESERVE, GUARD),
|
||||||
|
FaultClass::Guard
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn usable_region_is_foreign() {
|
||||||
|
// A fault inside the RW stack itself isn't a guard hit and must not
|
||||||
|
// be explained as one.
|
||||||
|
assert_eq!(classify(GUARD_HI, TOP, RESERVE, GUARD), FaultClass::Foreign);
|
||||||
|
assert_eq!(classify(TOP - 1, TOP, RESERVE, GUARD), FaultClass::Foreign);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn above_top_is_foreign() {
|
||||||
|
assert_eq!(classify(TOP, TOP, RESERVE, GUARD), FaultClass::Foreign);
|
||||||
|
assert_eq!(classify(TOP + PG, TOP, RESERVE, GUARD), FaultClass::Foreign);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn overshoot_window_edges() {
|
||||||
|
assert_eq!(
|
||||||
|
classify(GUARD_LO - 1, TOP, RESERVE, GUARD),
|
||||||
|
FaultClass::Overshoot(1)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
classify(GUARD_LO - OVERSHOOT_SLOP, TOP, RESERVE, GUARD),
|
||||||
|
FaultClass::Overshoot(OVERSHOOT_SLOP)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
classify(GUARD_LO - OVERSHOOT_SLOP - 1, TOP, RESERVE, GUARD),
|
||||||
|
FaultClass::Foreign
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn low_address_stack_saturates_not_wraps() {
|
||||||
|
// A stack mapped so low that the slop window would underflow: the
|
||||||
|
// window clips to 0 instead of wrapping around the address space.
|
||||||
|
let top = RESERVE + GUARD + PG; // guard_lo == PG
|
||||||
|
assert_eq!(classify(0, top, RESERVE, GUARD), FaultClass::Overshoot(PG));
|
||||||
|
// Null-page fault still classified only because it IS within slop
|
||||||
|
// here; with a normal-height stack it is Foreign (covered above by
|
||||||
|
// the window-edge test at realistic addresses).
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-9
@@ -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"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+236
-16
@@ -1,32 +1,45 @@
|
|||||||
//! mmap-based growable stack with a guard page below.
|
//! mmap-based actor stack with a PROT_NONE guard region below (RFC 019).
|
||||||
//!
|
//!
|
||||||
//! Layout (low → high address):
|
//! Layout (low → high address):
|
||||||
//! [ guard page (PROT_NONE) | stack region ]
|
//! [ guard region (PROT_NONE) | stack region ]
|
||||||
//! ^ top() — initial stack pointer
|
//! ^ top() — initial stack pointer
|
||||||
//!
|
//!
|
||||||
//! Stacks grow downward. Overflow lands in the guard page → SIGSEGV.
|
//! Stacks grow downward. Overflow lands in the guard region → SIGSEGV.
|
||||||
|
//!
|
||||||
|
//! Both the usable reserve and the guard are caller-chosen (page-rounded).
|
||||||
|
//! The reserve is a *virtual* reservation: anonymous mmap is demand-paged,
|
||||||
|
//! so RSS is touched-pages, not reserve × actors. The guard costs address
|
||||||
|
//! space only. A wide guard (the runtime defaults to 64 KiB) exists for
|
||||||
|
//! unprobed FFI frames: Rust frames touch pages in order (probestack), so
|
||||||
|
//! one page catches Rust overflow, but a C frame with a large local can
|
||||||
|
//! step over a single page in one `sub rsp`.
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
pub struct Stack {
|
pub struct Stack {
|
||||||
/// Bottom of the entire mmap'd region (start of guard page).
|
/// Bottom of the entire mmap'd region (start of the guard).
|
||||||
base: *mut u8,
|
base: *mut u8,
|
||||||
/// Total mmap'd size: guard_size + stack_size.
|
/// Total mmap'd size: guard_size + stack_size.
|
||||||
total_size: usize,
|
total_size: usize,
|
||||||
/// Usable stack size (excluding guard page).
|
/// Usable stack size (excluding the guard).
|
||||||
stack_size: usize,
|
stack_size: usize,
|
||||||
|
/// PROT_NONE region below the usable stack.
|
||||||
|
guard_size: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stack owns its memory; safe to send across threads.
|
// Stack owns its memory; safe to send across threads.
|
||||||
unsafe impl Send for Stack {}
|
unsafe impl Send for Stack {}
|
||||||
|
|
||||||
impl Stack {
|
impl Stack {
|
||||||
/// Allocate a new stack. `stack_size` is the usable region; one page is
|
/// Allocate a new stack. `stack_size` is the usable region; `guard_size`
|
||||||
/// added below as a guard page. Both are rounded up to the page size.
|
/// is mapped PROT_NONE below it. Both are rounded up to the page size
|
||||||
pub fn new(stack_size: usize) -> io::Result<Self> {
|
/// and must be non-zero.
|
||||||
|
pub fn new(stack_size: usize, guard_size: usize) -> io::Result<Self> {
|
||||||
|
assert!(stack_size > 0, "stack_size must be non-zero");
|
||||||
|
assert!(guard_size > 0, "guard_size must be non-zero");
|
||||||
let page = page_size();
|
let page = page_size();
|
||||||
let stack_size = round_up(stack_size, page);
|
let stack_size = round_up(stack_size, page);
|
||||||
let guard_size = page;
|
let guard_size = round_up(guard_size, page);
|
||||||
let total_size = guard_size + stack_size;
|
let total_size = guard_size + stack_size;
|
||||||
|
|
||||||
let base = unsafe {
|
let base = unsafe {
|
||||||
@@ -44,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 })
|
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.
|
||||||
@@ -62,14 +78,54 @@ impl Stack {
|
|||||||
(raw_top & !15) as *mut u8
|
(raw_top & !15) as *mut u8
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pointer to the bottom of the usable region (just above the guard page).
|
/// Pointer to the bottom of the usable region (just above the guard).
|
||||||
pub fn usable_base(&self) -> *mut u8 {
|
pub fn usable_base(&self) -> *mut u8 {
|
||||||
unsafe { self.base.add(page_size()) }
|
unsafe { self.base.add(self.guard_size) }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stack_size(&self) -> usize {
|
pub fn stack_size(&self) -> usize {
|
||||||
self.stack_size
|
self.stack_size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn guard_size(&self) -> usize {
|
||||||
|
self.guard_size
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `(stack_size, guard_size)` after page rounding. The pool rule
|
||||||
|
/// (RFC 019 §1) compares this against the runtime defaults: only
|
||||||
|
/// default-shaped stacks are pooled.
|
||||||
|
pub fn shape(&self) -> (usize, usize) {
|
||||||
|
(self.stack_size, self.guard_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pool-recycle zap (RFC 019 §6): `MADV_DONTNEED` everything below the
|
||||||
|
/// retained entry end `[top − retain, top)` — the span the next actor's
|
||||||
|
/// shallow frames land in stays resident, the dead spike below it is
|
||||||
|
/// released. The stack is unowned at the call site (its actor is dead),
|
||||||
|
/// so a synchronous eager zap races nothing and the RSS drop is
|
||||||
|
/// immediate — a museum of worst-case spikes is exactly what a pool must
|
||||||
|
/// not be; DONTNEED's ~8× per-page cost vs FREE is irrelevant off the
|
||||||
|
/// hot path. Advisory like the park-path shrink: a failure degrades to
|
||||||
|
/// "the pool keeps RSS", never to incorrectness. No-op (no syscall) when
|
||||||
|
/// `retain` covers the whole usable region — i.e. always, at the 64 KiB
|
||||||
|
/// default reserve.
|
||||||
|
pub(crate) fn recycle_zap(&self, retain: usize) {
|
||||||
|
if let Some((off, len)) = retain_range(self.stack_size, retain, page_size()) {
|
||||||
|
unsafe {
|
||||||
|
libc::madvise(
|
||||||
|
self.usable_base().add(off) as *mut libc::c_void,
|
||||||
|
len,
|
||||||
|
libc::MADV_DONTNEED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Round `n` up to whole pages — the same rounding `Stack::new` applies, so
|
||||||
|
/// runtime defaults stored pre-rounded compare exactly against [`Stack::shape`].
|
||||||
|
pub(crate) fn round_to_pages(n: usize) -> usize {
|
||||||
|
round_up(n, page_size())
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for Stack {
|
impl Drop for Stack {
|
||||||
@@ -80,10 +136,174 @@ impl Drop for Stack {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn page_size() -> usize {
|
pub(crate) fn page_size() -> usize {
|
||||||
unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize }
|
unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn round_up(n: usize, align: usize) -> usize {
|
fn round_up(n: usize, align: usize) -> usize {
|
||||||
(n + align - 1) & !(align - 1)
|
(n + align - 1) & !(align - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The whole-page span the park-path shrink may `MADV_FREE` (RFC 019 §3):
|
||||||
|
/// `[page_up(hwm), page_down(sp − redzone))`, or `None` if no full page fits.
|
||||||
|
///
|
||||||
|
/// `hwm` is the sampled high-water (deepest observed `sp`); everything in
|
||||||
|
/// `[hwm, sp)` is below the live frame and dead by definition. One page of
|
||||||
|
/// redzone stays resident under live `sp` — it covers the SysV 128-byte red
|
||||||
|
/// zone plus spill margin with room to spare. Rounding is inward on both
|
||||||
|
/// ends so the result can never touch the redzone, cross `sp`, or dip below
|
||||||
|
/// `hwm`; all arithmetic is checked so adversarial inputs (`sp < redzone`,
|
||||||
|
/// `hwm ≥ sp`, values near the address-space edges) collapse to `None`
|
||||||
|
/// rather than a wild or negative-length range.
|
||||||
|
pub(crate) fn shrink_range(hwm: usize, sp: usize, page: usize) -> Option<(usize, usize)> {
|
||||||
|
debug_assert!(page.is_power_of_two());
|
||||||
|
if hwm >= sp {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let redzone = page;
|
||||||
|
let end = sp.checked_sub(redzone)? & !(page - 1); // page_down(sp − redzone)
|
||||||
|
let start = hwm.checked_add(page - 1)? & !(page - 1); // page_up(hwm)
|
||||||
|
if end > start {
|
||||||
|
Some((start, end - start))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `(offset_from_usable_base, len)` span the pool recycle DONTNEEDs
|
||||||
|
/// (RFC 019 §6): everything below the retained entry end. "Bottom RETAIN of
|
||||||
|
/// the stack" is read stack-wise (entry frames = highest addresses of a
|
||||||
|
/// downward stack): the retained span is `[top − page_up(retain), top)`, the
|
||||||
|
/// zapped span is the rest — retaining the low-address deep end instead
|
||||||
|
/// would keep the coldest pages and release the ones the next actor faults
|
||||||
|
/// first. `retain` rounds *up* to whole pages (retain more, zap less), so
|
||||||
|
/// with `stack_size` page-rounded by `Stack::new` the result is always
|
||||||
|
/// page-aligned. Checked math: `retain ≥ stack_size` (notably the default
|
||||||
|
/// 64 KiB reserve with the 64 KiB RETAIN) and overflow collapse to `None`.
|
||||||
|
pub(crate) fn retain_range(
|
||||||
|
stack_size: usize,
|
||||||
|
retain: usize,
|
||||||
|
page: usize,
|
||||||
|
) -> Option<(usize, usize)> {
|
||||||
|
debug_assert!(page.is_power_of_two());
|
||||||
|
let retain = retain.checked_add(page - 1)? & !(page - 1); // page_up(retain)
|
||||||
|
let len = stack_size.checked_sub(retain)?;
|
||||||
|
if len == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some((0, len))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{retain_range, shrink_range};
|
||||||
|
|
||||||
|
const PG: usize = 4096;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retain_covers_whole_stack_is_a_noop() {
|
||||||
|
// The default config: reserve == RETAIN == 64 KiB. No zap, no syscall.
|
||||||
|
assert_eq!(retain_range(16 * PG, 16 * PG, PG), None);
|
||||||
|
assert_eq!(retain_range(PG, PG, PG), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retain_larger_than_stack_is_a_noop() {
|
||||||
|
assert_eq!(retain_range(16 * PG, 17 * PG, PG), None);
|
||||||
|
assert_eq!(retain_range(PG, usize::MAX, PG), None); // page_up overflows
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retain_zero_zaps_everything() {
|
||||||
|
assert_eq!(retain_range(16 * PG, 0, PG), Some((0, 16 * PG)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retain_rounds_up_zapping_less() {
|
||||||
|
// 1 byte of retain keeps a whole page.
|
||||||
|
assert_eq!(retain_range(16 * PG, 1, PG), Some((0, 15 * PG)));
|
||||||
|
assert_eq!(retain_range(16 * PG, PG + 1, PG), Some((0, 14 * PG)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retain_one_page_short_of_stack() {
|
||||||
|
assert_eq!(retain_range(2 * PG, PG, PG), Some((0, PG)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retain_range_is_page_aligned() {
|
||||||
|
for size_pg in [1usize, 2, 3, 16, 1024] {
|
||||||
|
for retain in [0usize, 1, PG - 1, PG, PG + 1, 4 * PG, size_pg * PG] {
|
||||||
|
if let Some((off, len)) = retain_range(size_pg * PG, retain, PG) {
|
||||||
|
assert_eq!(off, 0);
|
||||||
|
assert_eq!(len % PG, 0);
|
||||||
|
assert!(len <= size_pg * PG);
|
||||||
|
assert!(len > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_and_inverted_spans_are_none() {
|
||||||
|
assert_eq!(shrink_range(0x8000_0000, 0x8000_0000, PG), None); // hwm == sp
|
||||||
|
assert_eq!(shrink_range(0x8000_1000, 0x8000_0000, PG), None); // hwm > sp
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn span_smaller_than_redzone_plus_page_is_none() {
|
||||||
|
let sp = 0x8000_0000;
|
||||||
|
// Everything within redzone+1 page of sp: no full page clears both
|
||||||
|
// the redzone and the page_up(hwm) rounding.
|
||||||
|
assert_eq!(shrink_range(sp - PG, sp, PG), None);
|
||||||
|
assert_eq!(shrink_range(sp - 2 * PG + 1, sp, PG), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exact_two_pages_frees_one() {
|
||||||
|
let sp = 0x8000_0000;
|
||||||
|
let hwm = sp - 2 * PG;
|
||||||
|
// [hwm, hwm+PG) frees; [sp−PG, sp) is redzone.
|
||||||
|
assert_eq!(shrink_range(hwm, sp, PG), Some((hwm, PG)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unaligned_ends_round_inward() {
|
||||||
|
let sp = 0x8000_0123; // live sp mid-page
|
||||||
|
let hwm = 0x7f00_0abc; // high-water mid-page
|
||||||
|
let (start, len) = shrink_range(hwm, sp, PG).unwrap();
|
||||||
|
assert_eq!(start % PG, 0);
|
||||||
|
assert_eq!(len % PG, 0);
|
||||||
|
assert!(start >= hwm); // never below the sampled high-water
|
||||||
|
assert!(start + len <= (sp - PG) & !(PG - 1)); // never into the redzone
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn result_never_crosses_sp() {
|
||||||
|
// Sweep hwm across every offset of the page straddling the boundary.
|
||||||
|
let sp = 0x8000_0000 + 137;
|
||||||
|
for hwm in (sp - 4 * PG)..(sp) {
|
||||||
|
if let Some((start, len)) = shrink_range(hwm, sp, PG) {
|
||||||
|
assert!(start >= hwm);
|
||||||
|
assert!(start + len + PG <= sp + PG); // end ≤ page_down(sp − PG) < sp
|
||||||
|
assert!(len > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn underflow_near_zero_is_none() {
|
||||||
|
assert_eq!(shrink_range(0, PG - 1, PG), None); // sp < redzone
|
||||||
|
assert_eq!(shrink_range(0, 0, PG), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn big_span_frees_interior() {
|
||||||
|
let sp = 0x8000_0000;
|
||||||
|
let spike = 4 * 1024 * 1024;
|
||||||
|
let hwm = sp - spike;
|
||||||
|
let (start, len) = shrink_range(hwm, sp, PG).unwrap();
|
||||||
|
assert_eq!(start, hwm); // aligned input: starts exactly at hwm
|
||||||
|
assert_eq!(len, spike - PG); // everything but the redzone page
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+4
-2
@@ -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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-2
@@ -6,10 +6,19 @@
|
|||||||
//! Build the loom models with: `RUSTFLAGS="--cfg loom" cargo test --lib --release`
|
//! Build the loom models with: `RUSTFLAGS="--cfg loom" cargo test --lib --release`
|
||||||
|
|
||||||
#[cfg(loom)]
|
#[cfg(loom)]
|
||||||
pub(crate) use loom::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
pub(crate) use loom::sync::atomic::{fence, AtomicU64, AtomicUsize, Ordering};
|
||||||
|
|
||||||
#[cfg(not(loom))]
|
#[cfg(not(loom))]
|
||||||
pub(crate) use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
pub(crate) use std::sync::atomic::{fence, AtomicU64, AtomicUsize, Ordering};
|
||||||
|
|
||||||
|
// park.rs condvar-parker (loom + non-Linux builds only; the Linux non-loom
|
||||||
|
// build parks on a futex and never touches these — gating them identically
|
||||||
|
// keeps the default build free of unused imports).
|
||||||
|
#[cfg(loom)]
|
||||||
|
pub(crate) use loom::sync::{Condvar, Mutex};
|
||||||
|
|
||||||
|
#[cfg(all(not(loom), not(target_os = "linux")))]
|
||||||
|
pub(crate) use std::sync::{Condvar, Mutex};
|
||||||
|
|
||||||
/// `UnsafeCell` with loom's `with`/`with_mut` access API; pass-through cost
|
/// `UnsafeCell` with loom's `with`/`with_mut` access API; pass-through cost
|
||||||
/// is zero in normal builds (`#[inline]`, newtype over std's cell).
|
/// is zero in normal builds (`#[inline]`, newtype over std's cell).
|
||||||
|
|||||||
+150
-15
@@ -102,6 +102,19 @@ pub struct Entry {
|
|||||||
seq: u64,
|
seq: u64,
|
||||||
pub pid: Pid,
|
pub pid: Pid,
|
||||||
pub reason: Reason,
|
pub reason: Reason,
|
||||||
|
/// RFC 007 virtual time: the global delay ledger reading when this entry
|
||||||
|
/// was (re-)queued. `pop_due` shifts the effective deadline by any delay
|
||||||
|
/// injected since, so timers dilate together with the causally-delayed
|
||||||
|
/// workload instead of firing early in virtual terms.
|
||||||
|
#[cfg(feature = "smarm-causal")]
|
||||||
|
delay_stamp: u64,
|
||||||
|
/// RFC 007: a wall-anchored entry opts out of the virtual-time shift —
|
||||||
|
/// its deadline is honoured in wall time regardless of injected delay.
|
||||||
|
/// Used by the causal controller's own measurement/cooldown sleeps so
|
||||||
|
/// experiment windows keep a fixed wall length; ordinary workload timers
|
||||||
|
/// stay virtual (`false`).
|
||||||
|
#[cfg(feature = "smarm-causal")]
|
||||||
|
wall: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for Entry {
|
impl PartialEq for Entry {
|
||||||
@@ -116,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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,6 +143,14 @@ impl PartialOrd for Entry {
|
|||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Timers {
|
pub struct Timers {
|
||||||
|
/// RFC 018: the scheduler coordination layer. Attached once at
|
||||||
|
/// `RuntimeInner::new`; every insert notes its deadline (min-maintained
|
||||||
|
/// snapshot for the busy-path due-check + the timekeeper re-arm wake)
|
||||||
|
/// and every pop/clear re-anchors the snapshot to the heap minimum.
|
||||||
|
/// All calls happen under the timers mutex — the serialization the
|
||||||
|
/// coordinator's timer protocol mandates. `None` only in unit tests
|
||||||
|
/// that construct a bare `Timers`.
|
||||||
|
coord: Option<std::sync::Arc<crate::park::Coordinator>>,
|
||||||
/// Reverse-wrapped so the smallest deadline is at the top.
|
/// Reverse-wrapped so the smallest deadline is at the top.
|
||||||
heap: BinaryHeap<Reverse<Entry>>,
|
heap: BinaryHeap<Reverse<Entry>>,
|
||||||
/// Monotonic counter for the tiebreaker `seq` field (and the `TimerId` of a
|
/// Monotonic counter for the tiebreaker `seq` field (and the `TimerId` of a
|
||||||
@@ -144,7 +167,18 @@ pub struct Timers {
|
|||||||
|
|
||||||
impl Timers {
|
impl Timers {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self { heap: BinaryHeap::new(), next_seq: 0, armed: std::collections::HashSet::new() }
|
Self {
|
||||||
|
coord: None,
|
||||||
|
heap: BinaryHeap::new(),
|
||||||
|
next_seq: 0,
|
||||||
|
armed: std::collections::HashSet::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attach the scheduler coordination layer (RFC 018). Called once, at
|
||||||
|
/// runtime construction, before any scheduler thread exists.
|
||||||
|
pub(crate) fn attach_coordinator(&mut self, c: std::sync::Arc<crate::park::Coordinator>) {
|
||||||
|
self.coord = Some(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a `Sleep` timer. Convenience for the common case.
|
/// Insert a `Sleep` timer. Convenience for the common case.
|
||||||
@@ -152,6 +186,19 @@ impl Timers {
|
|||||||
self.insert(deadline, pid, Reason::Sleep { epoch });
|
self.insert(deadline, pid, Reason::Sleep { epoch });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Insert a *wall-anchored* `Sleep` timer: fires at `deadline` in wall
|
||||||
|
/// time even while causal profiling (feature `smarm-causal`) is injecting
|
||||||
|
/// virtual delay — it never chases the delay ledger. Without the feature
|
||||||
|
/// this is identical to [`insert_sleep`](Self::insert_sleep).
|
||||||
|
///
|
||||||
|
/// Intended for measurement machinery (the causal controller's window and
|
||||||
|
/// cooldown sleeps, TSC calibration) whose durations *define* wall time
|
||||||
|
/// rather than participate in the workload. Workload code should use the
|
||||||
|
/// ordinary virtual-anchored timers.
|
||||||
|
pub fn insert_sleep_wall(&mut self, deadline: Instant, pid: Pid, epoch: u32) {
|
||||||
|
self.push(deadline, pid, Reason::Sleep { epoch }, true);
|
||||||
|
}
|
||||||
|
|
||||||
/// Arm a cancellable `send_after` timer: run `fire` at `deadline` unless
|
/// Arm a cancellable `send_after` timer: run `fire` at `deadline` unless
|
||||||
/// [`cancel`](Self::cancel)led first. `pid` is informational only (the
|
/// [`cancel`](Self::cancel)led first. `pid` is informational only (the
|
||||||
/// destination, or who armed it — useful for introspection); it is *not*
|
/// destination, or who armed it — useful for introspection); it is *not*
|
||||||
@@ -163,11 +210,24 @@ impl Timers {
|
|||||||
pid: Pid,
|
pid: Pid,
|
||||||
fire: Box<dyn FnOnce() + Send>,
|
fire: Box<dyn FnOnce() + Send>,
|
||||||
) -> TimerId {
|
) -> TimerId {
|
||||||
let seq = self.next_seq;
|
self.armed.insert(self.next_seq);
|
||||||
self.next_seq = self.next_seq.wrapping_add(1);
|
TimerId(self.push(deadline, pid, Reason::Send { fire }, false))
|
||||||
self.armed.insert(seq);
|
}
|
||||||
self.heap.push(Reverse(Entry { deadline, seq, pid, reason: Reason::Send { fire } }));
|
|
||||||
TimerId(seq)
|
/// Arm a *wall-anchored* cancellable `send_after` timer (RFC 007): the
|
||||||
|
/// same contract as [`insert_send`](Self::insert_send), but the entry
|
||||||
|
/// opts out of the virtual-time shift and fires at its raw deadline
|
||||||
|
/// regardless of injected delay — the `Send`-reason sibling of
|
||||||
|
/// [`insert_sleep_wall`](Self::insert_sleep_wall). Without the
|
||||||
|
/// `smarm-causal` feature this is identical to `insert_send`.
|
||||||
|
pub fn insert_send_wall(
|
||||||
|
&mut self,
|
||||||
|
deadline: Instant,
|
||||||
|
pid: Pid,
|
||||||
|
fire: Box<dyn FnOnce() + Send>,
|
||||||
|
) -> TimerId {
|
||||||
|
self.armed.insert(self.next_seq);
|
||||||
|
TimerId(self.push(deadline, pid, Reason::Send { fire }, true))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cancel an armed `send_after` timer. Returns `true` if the timer was
|
/// Cancel an armed `send_after` timer. Returns `true` if the timer was
|
||||||
@@ -179,11 +239,38 @@ impl Timers {
|
|||||||
self.armed.remove(&id.0)
|
self.armed.remove(&id.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert an arbitrary timer entry.
|
/// Insert an arbitrary (virtual-anchored) timer entry.
|
||||||
pub fn insert(&mut self, deadline: Instant, pid: Pid, reason: Reason) {
|
pub fn insert(&mut self, deadline: Instant, pid: Pid, reason: Reason) {
|
||||||
|
self.push(deadline, pid, reason, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Common insertion path. `wall` selects the RFC 007 anchor (see
|
||||||
|
/// [`insert_sleep_wall`](Self::insert_sleep_wall)); it is accepted — and
|
||||||
|
/// ignored — without the `smarm-causal` feature so callers don't fork.
|
||||||
|
/// Returns the entry's `seq`.
|
||||||
|
fn push(&mut self, deadline: Instant, pid: Pid, reason: Reason, wall: bool) -> u64 {
|
||||||
|
#[cfg(not(feature = "smarm-causal"))]
|
||||||
|
let _ = wall;
|
||||||
let seq = self.next_seq;
|
let seq = self.next_seq;
|
||||||
self.next_seq = self.next_seq.wrapping_add(1);
|
self.next_seq = self.next_seq.wrapping_add(1);
|
||||||
self.heap.push(Reverse(Entry { deadline, seq, pid, reason }));
|
self.heap.push(Reverse(Entry {
|
||||||
|
deadline,
|
||||||
|
seq,
|
||||||
|
pid,
|
||||||
|
reason,
|
||||||
|
#[cfg(feature = "smarm-causal")]
|
||||||
|
delay_stamp: crate::causal::global_delay_cycles(),
|
||||||
|
#[cfg(feature = "smarm-causal")]
|
||||||
|
wall,
|
||||||
|
}));
|
||||||
|
// RFC 018: publish the (possibly new-minimum) deadline to the
|
||||||
|
// busy-path snapshot and wake the timekeeper if it is parked
|
||||||
|
// toward a later one. We hold the timers mutex — the mandated
|
||||||
|
// serialization for both.
|
||||||
|
if let Some(c) = &self.coord {
|
||||||
|
c.note_deadline(deadline);
|
||||||
|
}
|
||||||
|
seq
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
@@ -196,6 +283,9 @@ impl Timers {
|
|||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
self.heap.clear();
|
self.heap.clear();
|
||||||
self.armed.clear();
|
self.armed.clear();
|
||||||
|
if let Some(c) = &self.coord {
|
||||||
|
c.refresh_deadline(None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Soonest pending deadline, or `None` if the heap is empty.
|
/// Soonest pending deadline, or `None` if the heap is empty.
|
||||||
@@ -210,23 +300,68 @@ impl Timers {
|
|||||||
/// one is silently dropped here (its `seq` was already removed from
|
/// one is silently dropped here (its `seq` was already removed from
|
||||||
/// `armed` by [`cancel`](Self::cancel)). Returning it removes it from
|
/// `armed` by [`cancel`](Self::cancel)). Returning it removes it from
|
||||||
/// `armed`, so a later `cancel` of a fired timer reports `false`.
|
/// `armed`, so a later `cancel` of a fired timer reports `false`.
|
||||||
|
///
|
||||||
|
/// RFC 007 virtual time (feature `smarm-causal`): before an entry fires,
|
||||||
|
/// any global delay injected since it was (re-)queued is added to its
|
||||||
|
/// deadline; an entry whose *effective* deadline hasn't passed is pushed
|
||||||
|
/// back with the shifted deadline and a fresh stamp, so it keeps chasing
|
||||||
|
/// delay injected while it waits. Consequences, both benign:
|
||||||
|
/// [`peek_deadline`](Self::peek_deadline) may under-report (raw deadline
|
||||||
|
/// earlier than effective), costing at most one spurious scheduler wake
|
||||||
|
/// per injected chunk; and a shift never converts wall time — with zero
|
||||||
|
/// debt the path is byte-identical to the featureless one. Wall-anchored
|
||||||
|
/// entries ([`insert_sleep_wall`](Self::insert_sleep_wall)) are exempt
|
||||||
|
/// from the shift and always fire at their raw deadline.
|
||||||
pub fn pop_due(&mut self, now: Instant) -> Vec<Entry> {
|
pub fn pop_due(&mut self, now: Instant) -> Vec<Entry> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
|
#[cfg(feature = "smarm-causal")]
|
||||||
|
let global = crate::causal::global_delay_cycles();
|
||||||
while let Some(r) = self.heap.peek() {
|
while let Some(r) = self.heap.peek() {
|
||||||
if r.0.deadline <= now {
|
if r.0.deadline > now {
|
||||||
let entry = match self.heap.pop() {
|
break;
|
||||||
|
}
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
let mut entry = match self.heap.pop() {
|
||||||
Some(e) => e.0,
|
Some(e) => e.0,
|
||||||
None => panic!("smarm: timer heap pop after peek returned None (core corrupt)"),
|
None => panic!("smarm: timer heap pop after peek returned None (core corrupt)"),
|
||||||
};
|
};
|
||||||
if matches!(entry.reason, Reason::Send { .. }) && !self.armed.remove(&entry.seq) {
|
if matches!(entry.reason, Reason::Send { .. }) && !self.armed.contains(&entry.seq) {
|
||||||
// Cancelled before it came due: discard, do not deliver.
|
// Cancelled before it came due: discard, do not deliver.
|
||||||
|
// (Checked before any shift so a cancelled entry is never
|
||||||
|
// re-queued just to be discarded later.)
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
out.push(entry);
|
#[cfg(feature = "smarm-causal")]
|
||||||
} else {
|
if !entry.wall {
|
||||||
break;
|
let debt = global.saturating_sub(entry.delay_stamp);
|
||||||
|
if debt > 0 {
|
||||||
|
let shifted = entry
|
||||||
|
.deadline
|
||||||
|
.checked_add(crate::causal::cycles_to_duration(debt))
|
||||||
|
.unwrap_or(entry.deadline);
|
||||||
|
if shifted > now {
|
||||||
|
// Not due in virtual time: re-queue at the shifted
|
||||||
|
// deadline, stamped, keeping `seq` (and thus `Send`
|
||||||
|
// cancellation identity) intact.
|
||||||
|
entry.deadline = shifted;
|
||||||
|
entry.delay_stamp = global;
|
||||||
|
self.heap.push(Reverse(entry));
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if matches!(entry.reason, Reason::Send { .. }) {
|
||||||
|
self.armed.remove(&entry.seq);
|
||||||
|
}
|
||||||
|
out.push(entry);
|
||||||
|
}
|
||||||
|
// RFC 018: re-anchor the busy-path snapshot to the new heap minimum
|
||||||
|
// (still under the timers mutex). A causal-shift re-queue above went
|
||||||
|
// through `heap.push` directly, so this peek is the one place the
|
||||||
|
// snapshot is guaranteed to catch up.
|
||||||
|
if let Some(c) = &self.coord {
|
||||||
|
c.refresh_deadline(self.peek_deadline());
|
||||||
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-12
@@ -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
@@ -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"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+1039
File diff suppressed because it is too large
Load Diff
+11
-3
@@ -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 || {
|
||||||
|
|||||||
+34
-15
@@ -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);
|
||||||
@@ -23,7 +29,7 @@ extern "C-unwind" fn actor_simple() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn actor_runs_and_returns_to_scheduler() {
|
fn actor_runs_and_returns_to_scheduler() {
|
||||||
reset_log();
|
reset_log();
|
||||||
let stack = Stack::new(64 * 1024).unwrap();
|
let stack = Stack::new(64 * 1024, 4096).unwrap();
|
||||||
let sp = init_actor_stack(stack.top(), actor_simple);
|
let sp = init_actor_stack(stack.top(), actor_simple);
|
||||||
set_actor_sp(sp);
|
set_actor_sp(sp);
|
||||||
unsafe { switch_to_actor() };
|
unsafe { switch_to_actor() };
|
||||||
@@ -40,7 +46,7 @@ extern "C-unwind" fn actor_two_steps() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn actor_yields_and_resumes() {
|
fn actor_yields_and_resumes() {
|
||||||
reset_log();
|
reset_log();
|
||||||
let stack = Stack::new(64 * 1024).unwrap();
|
let stack = Stack::new(64 * 1024, 4096).unwrap();
|
||||||
let sp = init_actor_stack(stack.top(), actor_two_steps);
|
let sp = init_actor_stack(stack.top(), actor_two_steps);
|
||||||
set_actor_sp(sp);
|
set_actor_sp(sp);
|
||||||
|
|
||||||
@@ -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,
|
||||||
@@ -85,11 +94,17 @@ extern "C-unwind" fn actor_reg_check() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn callee_saved_registers_survive_yield() {
|
fn callee_saved_registers_survive_yield() {
|
||||||
let stack = Stack::new(64 * 1024).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.
|
||||||
@@ -117,20 +132,24 @@ extern "C-unwind" fn actor_b() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn two_actors_dont_corrupt_each_other() {
|
fn two_actors_dont_corrupt_each_other() {
|
||||||
let stack_a = Stack::new(64 * 1024).unwrap();
|
let stack_a = Stack::new(64 * 1024, 4096).unwrap();
|
||||||
let stack_b = Stack::new(64 * 1024).unwrap();
|
let stack_b = Stack::new(64 * 1024, 4096).unwrap();
|
||||||
|
|
||||||
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
@@ -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
@@ -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
@@ -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)]
|
||||||
|
|||||||
+162
-5
@@ -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()),
|
||||||
@@ -237,6 +245,13 @@ fn tree_from_nests_children_and_reroots_orphans() {
|
|||||||
overruns: 0,
|
overruns: 0,
|
||||||
messages_received: 0,
|
messages_received: 0,
|
||||||
budget_cycles: 0,
|
budget_cycles: 0,
|
||||||
|
stack: smarm::StackInfo {
|
||||||
|
reserve: 0,
|
||||||
|
guard: 0,
|
||||||
|
depth_high_water: 0,
|
||||||
|
parks_since_shrink: 0,
|
||||||
|
shrinks: 0,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let snap = RuntimeSnapshot {
|
let snap = RuntimeSnapshot {
|
||||||
@@ -251,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());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,3 +378,134 @@ fn budget_cycles_accumulate_when_enabled() {
|
|||||||
h.join().unwrap();
|
h.join().unwrap();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// RFC 019 §8 — the stack introspection surface.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Burn ~`frames` × 4 KiB of stack with a yield at max depth, so the context
|
||||||
|
/// save samples the high-water there (RFC 019 §2: hwm is SAMPLED at
|
||||||
|
/// deschedule, not tracked continuously).
|
||||||
|
#[inline(never)]
|
||||||
|
fn burn_stack_yielding(frames: usize) -> u64 {
|
||||||
|
let mut local = [0u8; 4096];
|
||||||
|
local[0] = frames as u8;
|
||||||
|
let below = if frames == 0 {
|
||||||
|
smarm::yield_now();
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
burn_stack_yielding(frames - 1)
|
||||||
|
};
|
||||||
|
std::hint::black_box(&mut local);
|
||||||
|
below.wrapping_add(local[0] as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stack_info_reports_defaults_and_sampled_depth() {
|
||||||
|
run(|| {
|
||||||
|
let (ready_tx, ready_rx) = channel::<()>();
|
||||||
|
let (gate_tx, gate_rx) = channel::<()>();
|
||||||
|
|
||||||
|
let h = spawn(move || {
|
||||||
|
// ~32 KiB deep with a yield at the bottom: the sample point.
|
||||||
|
std::hint::black_box(burn_stack_yielding(8));
|
||||||
|
ready_tx.send(()).unwrap();
|
||||||
|
gate_rx.recv().unwrap();
|
||||||
|
});
|
||||||
|
ready_rx.recv().unwrap();
|
||||||
|
|
||||||
|
let info = spin_until(h.pid(), |a| a.state == ActorState::Parked);
|
||||||
|
let s = info.stack;
|
||||||
|
assert_eq!(s.reserve, 64 * 1024, "default reserve");
|
||||||
|
assert_eq!(
|
||||||
|
s.guard,
|
||||||
|
1024 * 1024,
|
||||||
|
"default guard (kernel stack_guard_gap convention)"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
s.depth_high_water >= 8 * 4096,
|
||||||
|
"hwm sampled at the deep yield: expected ≥ 32 KiB, got {}",
|
||||||
|
s.depth_high_water
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
s.depth_high_water < s.reserve,
|
||||||
|
"depth {} cannot exceed the reserve {}",
|
||||||
|
s.depth_high_water,
|
||||||
|
s.reserve
|
||||||
|
);
|
||||||
|
// Parked at the gate right now, never shrunk (64 KiB reserve cannot
|
||||||
|
// cross the shrink threshold).
|
||||||
|
assert!(s.parks_since_shrink >= 1, "the gate park must be counted");
|
||||||
|
assert_eq!(s.shrinks, 0);
|
||||||
|
|
||||||
|
gate_tx.send(()).unwrap();
|
||||||
|
h.join().unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stack_info_shrink_counters_are_live() {
|
||||||
|
use smarm::runtime::{Config, SHRINK_COOLDOWN, SHRINK_THRESHOLD};
|
||||||
|
use smarm::{spawn_with, SpawnOpts};
|
||||||
|
|
||||||
|
let rt = smarm::runtime::init(Config::exact(1));
|
||||||
|
rt.run(|| {
|
||||||
|
let (park_tx, park_rx) = channel::<()>();
|
||||||
|
|
||||||
|
let spike = 768 * 4096;
|
||||||
|
assert!(spike > SHRINK_THRESHOLD);
|
||||||
|
let worker = spawn_with(
|
||||||
|
SpawnOpts {
|
||||||
|
stack_reserve: Some(8 * 1024 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
std::hint::black_box(burn_stack_yielding(768));
|
||||||
|
for _ in 0..(SHRINK_COOLDOWN + 8) {
|
||||||
|
park_rx.recv().unwrap();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let wpid = worker.pid();
|
||||||
|
// Before any parks complete: the spike depth is visible.
|
||||||
|
let info = spin_until(wpid, |a| a.state == ActorState::Parked);
|
||||||
|
assert!(
|
||||||
|
info.stack.depth_high_water >= spike,
|
||||||
|
"spike should be sampled: {} < {spike}",
|
||||||
|
info.stack.depth_high_water
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cross the cooldown, then read the counters live while the worker
|
||||||
|
// is parked waiting for the remaining rounds (post-join the slot is
|
||||||
|
// reclaimed and the generation check correctly hides it).
|
||||||
|
for _ in 0..(SHRINK_COOLDOWN + 2) {
|
||||||
|
spin_until(wpid, |a| a.state == ActorState::Parked);
|
||||||
|
park_tx.send(()).unwrap();
|
||||||
|
}
|
||||||
|
let info = spin_until(wpid, |a| {
|
||||||
|
a.state == ActorState::Parked && a.stack.shrinks >= 1
|
||||||
|
});
|
||||||
|
let s = info.stack;
|
||||||
|
assert!(
|
||||||
|
s.shrinks >= 1,
|
||||||
|
"cooldown was crossed with a spike above threshold"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
s.parks_since_shrink < SHRINK_COOLDOWN,
|
||||||
|
"counter must reset at shrink: {}",
|
||||||
|
s.parks_since_shrink
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
s.depth_high_water < spike,
|
||||||
|
"hwm resets to the shallow park sp at shrink; got {}",
|
||||||
|
s.depth_high_water
|
||||||
|
);
|
||||||
|
|
||||||
|
for _ in 0..6 {
|
||||||
|
spin_until(wpid, |a| a.state == ActorState::Parked);
|
||||||
|
park_tx.send(()).unwrap();
|
||||||
|
}
|
||||||
|
worker.join().unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
+13
-3
@@ -56,8 +56,16 @@ fn other_actors_run_while_block_on_io_is_in_flight() {
|
|||||||
let pos_2 = v.iter().position(|&x| x == 2).unwrap();
|
let pos_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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
//! RFC 018 scheduler park/wake — observable-behavior guards.
|
||||||
|
//!
|
||||||
|
//! These pin the two timer-latency properties the park/wake swap must
|
||||||
|
//! preserve or introduce:
|
||||||
|
//!
|
||||||
|
//! - `sleep_fires_under_saturation`: due timers fire even when every
|
||||||
|
//! scheduler is busy (nobody parked ⇒ no timekeeper) — the busy-path
|
||||||
|
//! due-check, ratified design point (a). The old drain phase gave this
|
||||||
|
//! for free (timers drained every loop iteration); the new design must
|
||||||
|
//! not lose it.
|
||||||
|
//! - `submillisecond_sleep_is_prompt`: a sub-ms sleep completes promptly.
|
||||||
|
//! Under the old wake pipe, `poll_wake`'s `as_millis` truncation turned
|
||||||
|
//! sub-ms deadlines into 0ms busy-polls (correct wall time, pathological
|
||||||
|
//! CPU); under park/wake the futex timespec carries full nanosecond
|
||||||
|
//! precision.
|
||||||
|
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sleep_fires_under_saturation() {
|
||||||
|
let rt = smarm::runtime::init(smarm::runtime::Config::exact(4));
|
||||||
|
rt.run(|| {
|
||||||
|
let stop = Arc::new(AtomicBool::new(false));
|
||||||
|
let mut spinners = Vec::new();
|
||||||
|
// 8 spinners over 4 schedulers: the run queue never empties, so no
|
||||||
|
// scheduler ever parks and no timekeeper exists. Only the busy-path
|
||||||
|
// due-check can fire the sleeper's timer before the spinners quit.
|
||||||
|
for _ in 0..8 {
|
||||||
|
let stop = stop.clone();
|
||||||
|
spinners.push(smarm::spawn(move || {
|
||||||
|
let t0 = Instant::now();
|
||||||
|
while !stop.load(Ordering::Relaxed) && t0.elapsed() < Duration::from_secs(5) {
|
||||||
|
smarm::yield_now();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
let t0 = Instant::now();
|
||||||
|
smarm::sleep(Duration::from_millis(10));
|
||||||
|
let dt = t0.elapsed();
|
||||||
|
stop.store(true, Ordering::Relaxed);
|
||||||
|
for s in spinners {
|
||||||
|
let _ = s.join();
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
dt < Duration::from_millis(500),
|
||||||
|
"10ms sleep took {dt:?} under scheduler saturation — busy-path \
|
||||||
|
timer firing is broken (timekeeper-only firing stalls under load)"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn submillisecond_sleep_is_prompt() {
|
||||||
|
let rt = smarm::runtime::init(smarm::runtime::Config::exact(2));
|
||||||
|
rt.run(|| {
|
||||||
|
// Warm one iteration, then measure.
|
||||||
|
smarm::sleep(Duration::from_micros(500));
|
||||||
|
let t0 = Instant::now();
|
||||||
|
smarm::sleep(Duration::from_micros(500));
|
||||||
|
let dt = t0.elapsed();
|
||||||
|
assert!(dt >= Duration::from_micros(400), "woke early: {dt:?}");
|
||||||
|
assert!(
|
||||||
|
dt < Duration::from_millis(100),
|
||||||
|
"500µs sleep took {dt:?} — sub-ms deadline handling is broken"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
+13
-3
@@ -44,7 +44,10 @@ fn a_dead_actor_vanishes_from_every_group_it_joined() {
|
|||||||
// Drain-on-contact: touching g1 detects the death and sweeps the pid
|
// 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
@@ -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
@@ -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
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
+139
-22
@@ -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!(
|
||||||
@@ -485,3 +529,76 @@ fn multi_thread_timer_only_no_pipe_contention() {
|
|||||||
SLEEP_MS * 2,
|
SLEEP_MS * 2,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Root panic propagation
|
||||||
|
|
||||||
|
/// A panic in the root actor escapes `run()` to the caller. Anything else
|
||||||
|
/// makes every assert inside `run` silently vacuous — found live when a
|
||||||
|
/// failing-first test passed: the tripped assert was caught by the
|
||||||
|
/// trampoline, recorded as `Outcome::Panic` on the root slot, and dropped
|
||||||
|
/// unread with the initial handle.
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "root actor panic escapes")]
|
||||||
|
fn root_panic_escapes_run() {
|
||||||
|
rt1().run(|| {
|
||||||
|
panic!("root actor panic escapes");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Teardown completes before the root panic propagates: a caller that
|
||||||
|
/// catches it can immediately `run()` again on the same `Runtime` (the
|
||||||
|
/// documented sequential-reuse contract).
|
||||||
|
#[test]
|
||||||
|
fn runtime_reusable_after_root_panic() {
|
||||||
|
let r = rt1();
|
||||||
|
let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
|
r.run(|| panic!("boom"));
|
||||||
|
}));
|
||||||
|
assert!(caught.is_err(), "root panic must escape run()");
|
||||||
|
let ran = Arc::new(AtomicBool::new(false));
|
||||||
|
let ran_t = ran.clone();
|
||||||
|
r.run(move || ran_t.store(true, Ordering::Relaxed));
|
||||||
|
assert!(
|
||||||
|
ran.load(Ordering::Relaxed),
|
||||||
|
"runtime unusable after root panic"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// RFC 019 — Config stack knobs
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Burn ~`frames` × 4 KiB of stack; probestack touches pages in order so
|
||||||
|
/// exceeding the reserve would hit the guard and SIGSEGV the process.
|
||||||
|
#[inline(never)]
|
||||||
|
fn burn_stack(frames: usize) -> u64 {
|
||||||
|
let mut local = [0u8; 4096];
|
||||||
|
local[0] = frames as u8;
|
||||||
|
let below = if frames == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
burn_stack(frames - 1)
|
||||||
|
};
|
||||||
|
std::hint::black_box(&mut local);
|
||||||
|
below.wrapping_add(local[0] as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_stack_reserve_permits_deep_recursion() {
|
||||||
|
// ~256 KiB of frames: four times the old fixed 64 KiB reserve. With
|
||||||
|
// Config::stack_reserve raised this must complete; before RFC 019 it
|
||||||
|
// could only segfault.
|
||||||
|
let rt = smarm::runtime::init(Config::exact(1).stack_reserve(1024 * 1024));
|
||||||
|
let done = Arc::new(AtomicBool::new(false));
|
||||||
|
let done2 = done.clone();
|
||||||
|
rt.run(move || {
|
||||||
|
spawn(move || {
|
||||||
|
std::hint::black_box(burn_stack(64));
|
||||||
|
done2.store(true, Ordering::SeqCst);
|
||||||
|
})
|
||||||
|
.join()
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
|
assert!(done.load(Ordering::SeqCst));
|
||||||
|
}
|
||||||
|
|||||||
+7
-4
@@ -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 || {
|
||||||
|
|||||||
+21
-4
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -108,8 +113,14 @@ fn loser_arm_wake_after_parked_select_stays_precise() {
|
|||||||
t0.elapsed() >= Duration::from_millis(40),
|
t0.elapsed() >= Duration::from_millis(40),
|
||||||
"one-shot park returned early: a stale loser-arm wake landed"
|
"one-shot park returned early: a stale loser-arm wake landed"
|
||||||
);
|
);
|
||||||
|
// Arm 0 is now closed (the sender actor exited after its sends) and
|
||||||
|
// a closed arm reports ready forever under priority order — observe
|
||||||
|
// the disconnect and drop it from the set, per the documented
|
||||||
|
// closed-arm rule.
|
||||||
|
assert_eq!(select(&[&rxa, &rxb]), 0);
|
||||||
|
assert!(rxa.try_recv().is_err(), "arm 0 must report disconnect");
|
||||||
// The loser's message was never lost.
|
// The loser's message was never lost.
|
||||||
assert_eq!(select(&[&rxa, &rxb]), 1);
|
assert_eq!(select(&[&rxb]), 0);
|
||||||
assert_eq!(rxb.try_recv().unwrap(), Some(2));
|
assert_eq!(rxb.try_recv().unwrap(), Some(2));
|
||||||
h.join().unwrap();
|
h.join().unwrap();
|
||||||
});
|
});
|
||||||
@@ -270,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));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -336,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());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
//! RFC 019 commit 2 — the `SpawnOpts` surface.
|
||||||
|
//!
|
||||||
|
//! Covers: per-spawn stack shape overrides on every spawn surface, the
|
||||||
|
//! `None ⇒ Config default` resolution, the pool rule from the outside
|
||||||
|
//! (obligation 4: a custom-shaped stack never enters the pool), and that a
|
||||||
|
//! big reserve behaviorally takes effect (deep recursion completes).
|
||||||
|
|
||||||
|
use smarm::runtime::{Config, DEFAULT_STACK_GUARD, DEFAULT_STACK_RESERVE};
|
||||||
|
use smarm::{self_pid, spawn, spawn_under_with, spawn_with, GenServerBuilder, SpawnOpts};
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
fn rt1() -> smarm::runtime::Runtime {
|
||||||
|
smarm::runtime::init(Config::exact(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_spawn_has_default_shape() {
|
||||||
|
rt1().run(|| {
|
||||||
|
let h = spawn(|| {
|
||||||
|
let shape = smarm::introspect::stack_shape(self_pid()).unwrap();
|
||||||
|
assert_eq!(shape, (DEFAULT_STACK_RESERVE, DEFAULT_STACK_GUARD));
|
||||||
|
});
|
||||||
|
h.join().unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_with_overrides_reserve_and_guard() {
|
||||||
|
rt1().run(|| {
|
||||||
|
let opts = SpawnOpts {
|
||||||
|
stack_reserve: Some(1024 * 1024),
|
||||||
|
guard_size: Some(256 * 1024),
|
||||||
|
};
|
||||||
|
let h = spawn_with(opts, || {
|
||||||
|
let shape = smarm::introspect::stack_shape(self_pid()).unwrap();
|
||||||
|
assert_eq!(shape, (1024 * 1024, 256 * 1024));
|
||||||
|
});
|
||||||
|
h.join().unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_with_partial_override_keeps_config_default_for_the_rest() {
|
||||||
|
rt1().run(|| {
|
||||||
|
let opts = SpawnOpts {
|
||||||
|
stack_reserve: Some(1024 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
};
|
||||||
|
let h = spawn_with(opts, || {
|
||||||
|
let shape = smarm::introspect::stack_shape(self_pid()).unwrap();
|
||||||
|
assert_eq!(shape, (1024 * 1024, DEFAULT_STACK_GUARD));
|
||||||
|
});
|
||||||
|
h.join().unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_with_rounds_to_pages() {
|
||||||
|
rt1().run(|| {
|
||||||
|
let opts = SpawnOpts {
|
||||||
|
stack_reserve: Some(64 * 1024 + 1),
|
||||||
|
guard_size: Some(4097),
|
||||||
|
};
|
||||||
|
let h = spawn_with(opts, || {
|
||||||
|
let (reserve, guard) = smarm::introspect::stack_shape(self_pid()).unwrap();
|
||||||
|
assert_eq!(reserve % 4096, 0);
|
||||||
|
assert_eq!(guard % 4096, 0);
|
||||||
|
assert!(reserve >= 64 * 1024 + 1);
|
||||||
|
assert!(guard >= 4097);
|
||||||
|
});
|
||||||
|
h.join().unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_under_with_takes_opts() {
|
||||||
|
rt1().run(|| {
|
||||||
|
let me = self_pid();
|
||||||
|
let opts = SpawnOpts {
|
||||||
|
stack_reserve: Some(128 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
};
|
||||||
|
let h = spawn_under_with(me, opts, || {
|
||||||
|
let (reserve, _) = smarm::introspect::stack_shape(self_pid()).unwrap();
|
||||||
|
assert_eq!(reserve, 128 * 1024);
|
||||||
|
});
|
||||||
|
h.join().unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obligation 4, from the outside: a dead custom stack must not be handed to
|
||||||
|
/// the next default spawn. The pool is LIFO, so if the custom stack had been
|
||||||
|
/// (wrongly) pushed at death, the very next default-shaped spawn on this
|
||||||
|
/// single-threaded runtime would pop it and report a custom shape.
|
||||||
|
#[test]
|
||||||
|
fn custom_stack_never_enters_the_pool() {
|
||||||
|
rt1().run(|| {
|
||||||
|
spawn_with(
|
||||||
|
SpawnOpts {
|
||||||
|
stack_reserve: Some(512 * 1024),
|
||||||
|
guard_size: Some(128 * 1024),
|
||||||
|
},
|
||||||
|
|| {},
|
||||||
|
)
|
||||||
|
.join()
|
||||||
|
.unwrap();
|
||||||
|
let h = spawn(|| {
|
||||||
|
let shape = smarm::introspect::stack_shape(self_pid()).unwrap();
|
||||||
|
assert_eq!(shape, (DEFAULT_STACK_RESERVE, DEFAULT_STACK_GUARD));
|
||||||
|
});
|
||||||
|
h.join().unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The reverse direction of the pool rule: a default-shaped stack IS pooled
|
||||||
|
/// and reused (cap = threads × 4 ≥ 1 here, pool empty at start).
|
||||||
|
#[test]
|
||||||
|
fn default_stack_is_recycled() {
|
||||||
|
rt1().run(|| {
|
||||||
|
spawn(|| {}).join().unwrap();
|
||||||
|
let h = spawn(|| {
|
||||||
|
let shape = smarm::introspect::stack_shape(self_pid()).unwrap();
|
||||||
|
assert_eq!(shape, (DEFAULT_STACK_RESERVE, DEFAULT_STACK_GUARD));
|
||||||
|
});
|
||||||
|
h.join().unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Burn ~`frames` × 4 KiB of stack (see tests/runtime.rs twin).
|
||||||
|
#[inline(never)]
|
||||||
|
fn burn_stack(frames: usize) -> u64 {
|
||||||
|
let mut local = [0u8; 4096];
|
||||||
|
local[0] = frames as u8;
|
||||||
|
let below = if frames == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
burn_stack(frames - 1)
|
||||||
|
};
|
||||||
|
std::hint::black_box(&mut local);
|
||||||
|
below.wrapping_add(local[0] as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn big_reserve_behaviorally_takes_effect() {
|
||||||
|
// ~1 MiB deep on an 8 MiB per-spawn reserve, runtime default untouched.
|
||||||
|
rt1().run(|| {
|
||||||
|
let done = Arc::new(AtomicBool::new(false));
|
||||||
|
let done2 = done.clone();
|
||||||
|
spawn_with(
|
||||||
|
SpawnOpts {
|
||||||
|
stack_reserve: Some(8 * 1024 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
std::hint::black_box(burn_stack(256));
|
||||||
|
done2.store(true, Ordering::SeqCst);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.join()
|
||||||
|
.unwrap();
|
||||||
|
assert!(done.load(Ordering::SeqCst));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Builder surfaces
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
struct Echo;
|
||||||
|
impl smarm::GenServer for Echo {
|
||||||
|
type Call = ();
|
||||||
|
type Reply = (usize, usize);
|
||||||
|
type Cast = ();
|
||||||
|
type Info = ();
|
||||||
|
type Timer = ();
|
||||||
|
fn handle_call(&mut self, _c: ()) -> (usize, usize) {
|
||||||
|
smarm::introspect::stack_shape(self_pid()).unwrap()
|
||||||
|
}
|
||||||
|
fn handle_cast(&mut self, _c: ()) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gen_server_builder_stack_opts() {
|
||||||
|
rt1().run(|| {
|
||||||
|
let server = GenServerBuilder::new(Echo)
|
||||||
|
.stack_opts(SpawnOpts {
|
||||||
|
stack_reserve: Some(256 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
})
|
||||||
|
.start();
|
||||||
|
let (reserve, guard) = server.call(()).unwrap();
|
||||||
|
assert_eq!(reserve, 256 * 1024);
|
||||||
|
assert_eq!(guard, DEFAULT_STACK_GUARD);
|
||||||
|
server.shutdown();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Probe;
|
||||||
|
impl smarm::Machine for Probe {
|
||||||
|
type Ev = smarm::channel::Sender<(usize, usize)>;
|
||||||
|
fn state_timeout_ev() -> Self::Ev {
|
||||||
|
unreachable!("no timers in this test")
|
||||||
|
}
|
||||||
|
fn timeout_ev(_name: &'static str) -> Self::Ev {
|
||||||
|
unreachable!("no timers in this test")
|
||||||
|
}
|
||||||
|
fn on_start(&mut self, _cx: &mut smarm::Cx<Self::Ev>) {}
|
||||||
|
fn handle(
|
||||||
|
&mut self,
|
||||||
|
ev: Self::Ev,
|
||||||
|
_cx: &mut smarm::Cx<Self::Ev>,
|
||||||
|
) -> smarm::gen_statem::Step<Self::Ev> {
|
||||||
|
let _ = ev.send(smarm::introspect::stack_shape(self_pid()).unwrap());
|
||||||
|
smarm::gen_statem::Step::Stayed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gen_statem_spawn_with_stack_opts() {
|
||||||
|
rt1().run(|| {
|
||||||
|
let m = smarm::gen_statem::spawn_with(
|
||||||
|
SpawnOpts {
|
||||||
|
stack_reserve: Some(256 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
},
|
||||||
|
Probe,
|
||||||
|
);
|
||||||
|
let (tx, rx) = smarm::channel::channel();
|
||||||
|
m.send(tx).unwrap();
|
||||||
|
let (reserve, guard) = rx.recv().unwrap();
|
||||||
|
assert_eq!(reserve, 256 * 1024);
|
||||||
|
assert_eq!(guard, DEFAULT_STACK_GUARD);
|
||||||
|
});
|
||||||
|
}
|
||||||
+103
-11
@@ -7,13 +7,13 @@ use smarm::stack::Stack;
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn top_is_16_byte_aligned() {
|
fn top_is_16_byte_aligned() {
|
||||||
let s = Stack::new(64 * 1024).unwrap();
|
let s = Stack::new(64 * 1024, 4096).unwrap();
|
||||||
assert_eq!(s.top() as usize % 16, 0);
|
assert_eq!(s.top() as usize % 16, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn top_is_within_allocation() {
|
fn top_is_within_allocation() {
|
||||||
let s = Stack::new(64 * 1024).unwrap();
|
let s = Stack::new(64 * 1024, 4096).unwrap();
|
||||||
let top = s.top() as usize;
|
let top = s.top() as usize;
|
||||||
let base = s.usable_base() as usize;
|
let base = s.usable_base() as usize;
|
||||||
assert!(top > base);
|
assert!(top > base);
|
||||||
@@ -22,7 +22,7 @@ fn top_is_within_allocation() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn write_and_read_top_of_stack() {
|
fn write_and_read_top_of_stack() {
|
||||||
let s = Stack::new(64 * 1024).unwrap();
|
let s = Stack::new(64 * 1024, 4096).unwrap();
|
||||||
let sentinel: u64 = 0xDEAD_BEEF_CAFE_1234;
|
let sentinel: u64 = 0xDEAD_BEEF_CAFE_1234;
|
||||||
unsafe {
|
unsafe {
|
||||||
let ptr = s.top().sub(8) as *mut u64;
|
let ptr = s.top().sub(8) as *mut u64;
|
||||||
@@ -33,7 +33,7 @@ fn write_and_read_top_of_stack() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn write_and_read_bottom_of_usable_region() {
|
fn write_and_read_bottom_of_usable_region() {
|
||||||
let s = Stack::new(64 * 1024).unwrap();
|
let s = Stack::new(64 * 1024, 4096).unwrap();
|
||||||
let sentinel: u64 = 0x0102_0304_0506_0708;
|
let sentinel: u64 = 0x0102_0304_0506_0708;
|
||||||
unsafe {
|
unsafe {
|
||||||
let ptr = s.usable_base() as *mut u64;
|
let ptr = s.usable_base() as *mut u64;
|
||||||
@@ -44,17 +44,17 @@ fn write_and_read_bottom_of_usable_region() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn small_stack_allocates() {
|
fn small_stack_allocates() {
|
||||||
assert!(Stack::new(4096).is_ok());
|
assert!(Stack::new(4096, 4096).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn large_stack_allocates() {
|
fn large_stack_allocates() {
|
||||||
assert!(Stack::new(8 * 1024 * 1024).is_ok());
|
assert!(Stack::new(8 * 1024 * 1024, 4096).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stack_size_at_least_requested() {
|
fn stack_size_at_least_requested() {
|
||||||
let s = Stack::new(64 * 1024).unwrap();
|
let s = Stack::new(64 * 1024, 4096).unwrap();
|
||||||
assert!(s.stack_size() >= 64 * 1024);
|
assert!(s.stack_size() >= 64 * 1024);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,15 +68,32 @@ use std::process::Command;
|
|||||||
fn run_as_child_if_requested() {
|
fn run_as_child_if_requested() {
|
||||||
match env::var("SMARM_SUBTEST").as_deref() {
|
match env::var("SMARM_SUBTEST").as_deref() {
|
||||||
Ok("guard_page_direct") => {
|
Ok("guard_page_direct") => {
|
||||||
let s = Stack::new(64 * 1024).unwrap();
|
let s = Stack::new(64 * 1024, 4096).unwrap();
|
||||||
unsafe {
|
unsafe {
|
||||||
let guard_ptr = s.usable_base().sub(1);
|
let guard_ptr = s.usable_base().sub(1);
|
||||||
guard_ptr.write_volatile(0xAB);
|
guard_ptr.write_volatile(0xAB);
|
||||||
}
|
}
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
|
Ok("wide_guard_top") => {
|
||||||
|
// One byte below the usable region, 64 KiB guard: must fault.
|
||||||
|
let s = Stack::new(64 * 1024, 64 * 1024).unwrap();
|
||||||
|
unsafe {
|
||||||
|
s.usable_base().sub(1).write_volatile(0xAB);
|
||||||
|
}
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
Ok("wide_guard_bottom") => {
|
||||||
|
// The very bottom page of a 64 KiB guard: an unprobed C-style
|
||||||
|
// leap over a small guard lands here — must still fault.
|
||||||
|
let s = Stack::new(64 * 1024, 64 * 1024).unwrap();
|
||||||
|
unsafe {
|
||||||
|
s.usable_base().sub(64 * 1024).write_volatile(0xAB);
|
||||||
|
}
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
Ok("stack_overflow") => {
|
Ok("stack_overflow") => {
|
||||||
let s = Stack::new(64 * 1024).unwrap();
|
let s = Stack::new(64 * 1024, 4096).unwrap();
|
||||||
unsafe {
|
unsafe {
|
||||||
let mut ptr = s.top().sub(1);
|
let mut ptr = s.top().sub(1);
|
||||||
let stop = s.usable_base().sub(1);
|
let stop = s.usable_base().sub(1);
|
||||||
@@ -107,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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +140,76 @@ 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// RFC 019 — explicit shape: rounding, guard accessor, wide-guard coverage.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sizes_round_up_to_page() {
|
||||||
|
let s = Stack::new(64 * 1024 + 1, 4096 + 1).unwrap();
|
||||||
|
assert_eq!(s.stack_size() % 4096, 0);
|
||||||
|
assert_eq!(s.guard_size() % 4096, 0);
|
||||||
|
assert!(s.stack_size() >= 64 * 1024 + 1);
|
||||||
|
assert!(s.guard_size() >= 4096 + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shape_reports_rounded_sizes() {
|
||||||
|
let s = Stack::new(64 * 1024, 64 * 1024).unwrap();
|
||||||
|
assert_eq!(s.shape(), (64 * 1024, 64 * 1024));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn usable_base_sits_above_guard() {
|
||||||
|
let s = Stack::new(64 * 1024, 64 * 1024).unwrap();
|
||||||
|
// The usable region must start exactly guard_size above the mapping
|
||||||
|
// base: a write at usable_base is legal, one byte below is not (the
|
||||||
|
// subprocess tests below prove the "not").
|
||||||
|
let sentinel: u64 = 0x1111_2222_3333_4444;
|
||||||
|
unsafe {
|
||||||
|
let ptr = s.usable_base() as *mut u64;
|
||||||
|
ptr.write_volatile(sentinel);
|
||||||
|
assert_eq!(ptr.read_volatile(), sentinel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wide_guard_faults_at_top() {
|
||||||
|
run_as_child_if_requested();
|
||||||
|
let status = spawn_subtest("wide_guard_top");
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::process::ExitStatusExt;
|
||||||
|
assert_eq!(
|
||||||
|
status.signal(),
|
||||||
|
Some(11),
|
||||||
|
"expected SIGSEGV, got: {:?}",
|
||||||
|
status
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wide_guard_faults_at_bottom() {
|
||||||
|
run_as_child_if_requested();
|
||||||
|
let status = spawn_subtest("wide_guard_bottom");
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::process::ExitStatusExt;
|
||||||
|
assert_eq!(
|
||||||
|
status.signal(),
|
||||||
|
Some(11),
|
||||||
|
"expected SIGSEGV, got: {:?}",
|
||||||
|
status
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
//! RFC 019 §7 — overflow diagnostics, observed from outside via subprocess
|
||||||
|
//! (mirrors tests/stack.rs's harness, plus stderr capture).
|
||||||
|
//!
|
||||||
|
//! Four cases:
|
||||||
|
//! - Rust recursion at defaults: probed frames walk into the guard →
|
||||||
|
//! tier-1 definitive message, death by SIGSEGV.
|
||||||
|
//! - FFI canary (96 KiB unprobed C local) at defaults: first touch lands
|
||||||
|
//! inside the 1 MiB guard → tier-1 message.
|
||||||
|
//! - FFI canary with the guard shrunk to 4 KiB: the frame steps over it
|
||||||
|
//! into unmapped VA below → tier-2 "stepped over" message. This is the
|
||||||
|
//! RFC's motivating incident (cargo-vendored gz build) reproduced.
|
||||||
|
//! - FFI canary with reserve raised to 256 KiB: fits, runs clean, exits 0 —
|
||||||
|
//! the §1 knob is the fix, proven by the same frame.
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
unsafe extern "C" {
|
||||||
|
fn smarm_canary_burn();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unbounded probed recursion; each frame dirties 4 KiB. black_box defeats
|
||||||
|
/// tail-call elision so the walk is real.
|
||||||
|
#[inline(never)]
|
||||||
|
#[allow(unconditional_recursion)]
|
||||||
|
fn recurse_forever(depth: u64) -> u64 {
|
||||||
|
let mut local = [0u8; 4096];
|
||||||
|
local[0] = depth as u8;
|
||||||
|
std::hint::black_box(&mut local);
|
||||||
|
recurse_forever(depth + 1).wrapping_add(local[0] as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_as_child_if_requested() {
|
||||||
|
let mode = match env::var("SMARM_DIAG_SUBTEST") {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
use smarm::runtime::Config;
|
||||||
|
use smarm::{spawn_with, SpawnOpts};
|
||||||
|
let rt = smarm::runtime::init(Config::exact(1));
|
||||||
|
rt.run(move || {
|
||||||
|
let opts = match mode.as_str() {
|
||||||
|
"rust_overflow" | "ffi_tier1" => SpawnOpts::default(),
|
||||||
|
// Small guard: the canary's 96 KiB displacement clears it.
|
||||||
|
"ffi_tier2" => SpawnOpts {
|
||||||
|
guard_size: Some(4096),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
},
|
||||||
|
// Enough reserve: the same frame simply fits.
|
||||||
|
"ffi_clean" => SpawnOpts {
|
||||||
|
stack_reserve: Some(256 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
},
|
||||||
|
other => panic!("unknown subtest {other}"),
|
||||||
|
};
|
||||||
|
let is_rust = mode == "rust_overflow";
|
||||||
|
spawn_with(opts, move || {
|
||||||
|
if is_rust {
|
||||||
|
std::hint::black_box(recurse_forever(0));
|
||||||
|
} else {
|
||||||
|
unsafe { smarm_canary_burn() };
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join()
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_subtest(name: &str) -> std::process::Output {
|
||||||
|
let exe = env::current_exe().unwrap();
|
||||||
|
Command::new(exe)
|
||||||
|
.env("SMARM_DIAG_SUBTEST", name)
|
||||||
|
.args(["--test-threads=1", "--quiet"])
|
||||||
|
.output()
|
||||||
|
.expect("failed to spawn subprocess")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn assert_died_sigsegv(out: &std::process::Output) {
|
||||||
|
use std::os::unix::process::ExitStatusExt;
|
||||||
|
assert_eq!(
|
||||||
|
out.status.signal(),
|
||||||
|
Some(11),
|
||||||
|
"expected death by SIGSEGV, got {:?}; stderr:\n{}",
|
||||||
|
out.status,
|
||||||
|
String::from_utf8_lossy(&out.stderr)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rust_overflow_dies_with_tier1_message() {
|
||||||
|
run_as_child_if_requested();
|
||||||
|
let out = spawn_subtest("rust_overflow");
|
||||||
|
assert_died_sigsegv(&out);
|
||||||
|
let err = String::from_utf8_lossy(&out.stderr);
|
||||||
|
assert!(
|
||||||
|
err.contains("overflowed its stack") && err.contains("in the guard region"),
|
||||||
|
"missing tier-1 diagnostic; stderr:\n{err}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
err.contains("reserve=65536"),
|
||||||
|
"wrong reserve in message:\n{err}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
err.contains("guard=1048576"),
|
||||||
|
"wrong guard in message:\n{err}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ffi_canary_at_defaults_dies_with_tier1_message() {
|
||||||
|
run_as_child_if_requested();
|
||||||
|
let out = spawn_subtest("ffi_tier1");
|
||||||
|
assert_died_sigsegv(&out);
|
||||||
|
let err = String::from_utf8_lossy(&out.stderr);
|
||||||
|
// 96 KiB displacement from a 64 KiB reserve lands ~32 KiB into the
|
||||||
|
// 1 MiB guard: definitively classified.
|
||||||
|
assert!(
|
||||||
|
err.contains("in the guard region"),
|
||||||
|
"wide guard should catch the unprobed frame in tier 1; stderr:\n{err}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ffi_canary_over_small_guard_dies_with_tier2_message() {
|
||||||
|
run_as_child_if_requested();
|
||||||
|
let out = spawn_subtest("ffi_tier2");
|
||||||
|
assert_died_sigsegv(&out);
|
||||||
|
let err = String::from_utf8_lossy(&out.stderr);
|
||||||
|
assert!(
|
||||||
|
err.contains("stepped over it") && err.contains("below the guard"),
|
||||||
|
"expected tier-2 overshoot attribution; stderr:\n{err}"
|
||||||
|
);
|
||||||
|
assert!(err.contains("guard=4096"), "wrong guard in message:\n{err}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ffi_canary_with_enough_reserve_runs_clean() {
|
||||||
|
run_as_child_if_requested();
|
||||||
|
let out = spawn_subtest("ffi_clean");
|
||||||
|
assert!(
|
||||||
|
out.status.success(),
|
||||||
|
"canary should fit in 256 KiB reserve, got {:?}; stderr:\n{}",
|
||||||
|
out.status,
|
||||||
|
String::from_utf8_lossy(&out.stderr)
|
||||||
|
);
|
||||||
|
let err = String::from_utf8_lossy(&out.stderr);
|
||||||
|
assert!(
|
||||||
|
!err.contains("smarm: actor"),
|
||||||
|
"no diagnostic expected on the clean path; stderr:\n{err}"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
//! RFC 019 commit 5 — pool recycle zaps a dead stack down to its retained
|
||||||
|
//! entry end, observed from the outside.
|
||||||
|
//!
|
||||||
|
//! A default-shaped stack that spiked deep and then died must not carry its
|
||||||
|
//! spike into the pool as resident RSS: `recycle_stack` DONTNEEDs everything
|
||||||
|
//! below the top `RECYCLE_RETAIN` bytes before pushing. The zap is
|
||||||
|
//! synchronous on the death path, so the drop is immediate — but the death
|
||||||
|
//! path itself races the observer's `join` return, hence the brief poll.
|
||||||
|
//!
|
||||||
|
//! Residency is measured with `mincore`, not smaps: a neighboring rw anon
|
||||||
|
//! mapping can land flush against the stack top and the kernel merges the
|
||||||
|
//! VMAs (observed under the full test run), so per-mapping smaps fields
|
||||||
|
//! over-count. The PROT_NONE guard below can never merge, so the usable
|
||||||
|
//! base is exactly the anchor VMA's start, and `mincore` counts pages
|
||||||
|
//! within [usable_base, usable_base + reserve) regardless of merging.
|
||||||
|
|
||||||
|
use smarm::runtime::{Config, RECYCLE_RETAIN};
|
||||||
|
use smarm::{channel, spawn, yield_now};
|
||||||
|
|
||||||
|
const RESERVE: usize = 4 * 1024 * 1024;
|
||||||
|
|
||||||
|
/// Burn ~`frames` × 4 KiB of stack, dirtying every frame.
|
||||||
|
#[inline(never)]
|
||||||
|
fn burn_stack(frames: usize) -> u64 {
|
||||||
|
let mut local = [0u8; 4096];
|
||||||
|
local[0] = frames as u8;
|
||||||
|
let below = if frames == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
burn_stack(frames - 1)
|
||||||
|
};
|
||||||
|
std::hint::black_box(&mut local);
|
||||||
|
below.wrapping_add(local[0] as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resident-page count over [lo, lo + len) via mincore (len page-aligned).
|
||||||
|
fn resident_pages(lo: usize, len: usize) -> usize {
|
||||||
|
let page = 4096;
|
||||||
|
let mut vec = vec![0u8; len / page];
|
||||||
|
let ret = unsafe { libc::mincore(lo as *mut libc::c_void, len, vec.as_mut_ptr()) };
|
||||||
|
assert_eq!(
|
||||||
|
ret,
|
||||||
|
0,
|
||||||
|
"mincore failed: {}",
|
||||||
|
std::io::Error::last_os_error()
|
||||||
|
);
|
||||||
|
vec.iter().filter(|&&b| b & 1 != 0).count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The [start, end) of the VMA containing `addr`.
|
||||||
|
fn vma_containing(addr: usize) -> (usize, usize) {
|
||||||
|
let maps = std::fs::read_to_string("/proc/self/maps").unwrap();
|
||||||
|
for line in maps.lines() {
|
||||||
|
if let Some((range, _)) = line.split_once(' ') {
|
||||||
|
if let Some((a, b)) = range.split_once('-') {
|
||||||
|
if let (Ok(start), Ok(end)) =
|
||||||
|
(usize::from_str_radix(a, 16), usize::from_str_radix(b, 16))
|
||||||
|
{
|
||||||
|
if start <= addr && addr < end {
|
||||||
|
return (start, end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic!("no VMA contains {addr:#x}");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vma_exists(addr: usize) -> bool {
|
||||||
|
let maps = std::fs::read_to_string("/proc/self/maps").unwrap();
|
||||||
|
for line in maps.lines() {
|
||||||
|
if let Some((range, _)) = line.split_once(' ') {
|
||||||
|
if let Some((a, b)) = range.split_once('-') {
|
||||||
|
if let (Ok(start), Ok(end)) =
|
||||||
|
(usize::from_str_radix(a, 16), usize::from_str_radix(b, 16))
|
||||||
|
{
|
||||||
|
if start <= addr && addr < end {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recycle_zaps_dead_stack_down_to_retain() {
|
||||||
|
// Default reserve raised so the pool holds big stacks (default-shaped ⇒
|
||||||
|
// pooled) and the zap has something to bite; single scheduler.
|
||||||
|
let rt = smarm::runtime::init(Config::exact(1).stack_reserve(RESERVE));
|
||||||
|
rt.run(|| {
|
||||||
|
let (tx, rx) = channel::<usize>();
|
||||||
|
|
||||||
|
let h = spawn(move || {
|
||||||
|
let probe = 0u8;
|
||||||
|
let anchor = &probe as *const u8 as usize;
|
||||||
|
// The guard below is PROT_NONE and can never merge with the
|
||||||
|
// usable region, so the anchor VMA's start IS the usable base.
|
||||||
|
let (vlo, _) = vma_containing(anchor);
|
||||||
|
// Dirty ~3 MiB of the 4 MiB reserve, then die.
|
||||||
|
std::hint::black_box(burn_stack(768));
|
||||||
|
tx.send(vlo).unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
let usable_base = rx.recv().unwrap();
|
||||||
|
h.join().unwrap();
|
||||||
|
|
||||||
|
// The zap span is everything below the retained entry end. DONTNEED
|
||||||
|
// on private anon discards synchronously and unconditionally, so
|
||||||
|
// this must go to exactly zero resident pages; the poll only covers
|
||||||
|
// the death path racing join's return.
|
||||||
|
let zap_len = RESERVE - RECYCLE_RETAIN;
|
||||||
|
let mut resident = usize::MAX;
|
||||||
|
for _ in 0..10_000 {
|
||||||
|
resident = resident_pages(usable_base, zap_len);
|
||||||
|
if resident == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
yield_now();
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
resident, 0,
|
||||||
|
"recycled stack's zap span still resident: {resident} pages in \
|
||||||
|
[{usable_base:#x}, +{zap_len:#x})"
|
||||||
|
);
|
||||||
|
// Pooled, not munmapped: the mapping must still be there.
|
||||||
|
assert!(
|
||||||
|
vma_exists(usable_base),
|
||||||
|
"default-shaped stack was unmapped instead of pooled"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
//! RFC 019 commit 3 — park-path stack shrink, observed from the outside.
|
||||||
|
//!
|
||||||
|
//! The one integration-level claim of the shrink machinery: an actor that
|
||||||
|
//! spikes deep, returns shallow, and then parks past the cooldown gets its
|
||||||
|
//! dead span MADV_FREE'd — visible as `LazyFree` in `/proc/self/smaps`
|
||||||
|
//! within the stack's address range — while everything live survives.
|
||||||
|
//!
|
||||||
|
//! The high-water mark is *sampled* at context-save, so the spike yields
|
||||||
|
//! once at max depth to guarantee a sample there (in production, preemption
|
||||||
|
//! provides the quasi-random samples; a test must not rely on luck).
|
||||||
|
|
||||||
|
use smarm::runtime::{Config, SHRINK_COOLDOWN, SHRINK_THRESHOLD};
|
||||||
|
use smarm::{actor_info, channel, spawn, spawn_with, yield_now, ActorState, SpawnOpts};
|
||||||
|
|
||||||
|
/// Burn ~`frames` × 4 KiB of stack, yielding once at the bottom so the
|
||||||
|
/// context-save samples `sp` at max depth.
|
||||||
|
#[inline(never)]
|
||||||
|
fn burn_stack_yielding(frames: usize) -> u64 {
|
||||||
|
let mut local = [0u8; 4096];
|
||||||
|
local[0] = frames as u8;
|
||||||
|
let below = if frames == 0 {
|
||||||
|
yield_now();
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
burn_stack_yielding(frames - 1)
|
||||||
|
};
|
||||||
|
std::hint::black_box(&mut local);
|
||||||
|
below.wrapping_add(local[0] as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sum the `LazyFree:` kB of every smaps mapping intersecting [lo, hi).
|
||||||
|
fn lazy_free_bytes_in(lo: usize, hi: usize) -> usize {
|
||||||
|
let smaps = std::fs::read_to_string("/proc/self/smaps").unwrap();
|
||||||
|
let mut total_kb = 0usize;
|
||||||
|
let mut in_range = false;
|
||||||
|
for line in smaps.lines() {
|
||||||
|
if let Some((range, _)) = line.split_once(' ') {
|
||||||
|
if let Some((a, b)) = range.split_once('-') {
|
||||||
|
if let (Ok(start), Ok(end)) =
|
||||||
|
(usize::from_str_radix(a, 16), usize::from_str_radix(b, 16))
|
||||||
|
{
|
||||||
|
in_range = start < hi && end > lo;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if in_range {
|
||||||
|
if let Some(rest) = line.strip_prefix("LazyFree:") {
|
||||||
|
let kb: usize = rest.trim().trim_end_matches(" kB").trim().parse().unwrap();
|
||||||
|
total_kb += kb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
total_kb * 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spike_then_parks_marks_lazyfree_and_keeps_live_data() {
|
||||||
|
// Single scheduler: the controller can gate on the worker being Parked.
|
||||||
|
let rt = smarm::runtime::init(Config::exact(1));
|
||||||
|
rt.run(|| {
|
||||||
|
let (park_tx, park_rx) = channel::<()>();
|
||||||
|
let (done_tx, done_rx) = channel::<(usize, u64)>();
|
||||||
|
|
||||||
|
let spike = 768 * 4096; // ~3 MiB, well past SHRINK_THRESHOLD
|
||||||
|
assert!(spike > SHRINK_THRESHOLD);
|
||||||
|
|
||||||
|
let worker = spawn_with(
|
||||||
|
SpawnOpts {
|
||||||
|
stack_reserve: Some(8 * 1024 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
// Live data that must survive the shrink, and an anchor
|
||||||
|
// address inside the stack for the smaps scan.
|
||||||
|
let live = [0xA5u8; 64];
|
||||||
|
let anchor = live.as_ptr() as usize;
|
||||||
|
|
||||||
|
// Spike: ~3 MiB deep, sampled at the bottom, unwound.
|
||||||
|
std::hint::black_box(burn_stack_yielding(768));
|
||||||
|
|
||||||
|
// Park past the cooldown. Each recv on the drained inbox is
|
||||||
|
// one park; the controller sends only when it sees us Parked.
|
||||||
|
for _ in 0..(SHRINK_COOLDOWN + 8) {
|
||||||
|
park_rx.recv().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measure from inside: the stack spans ≤ 8 MiB below anchor.
|
||||||
|
let lazy = lazy_free_bytes_in(anchor - 8 * 1024 * 1024, anchor + 4096);
|
||||||
|
let checksum = live.iter().map(|&b| b as u64).sum();
|
||||||
|
done_tx.send((lazy, checksum)).unwrap();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let wpid = worker.pid();
|
||||||
|
for _ in 0..(SHRINK_COOLDOWN + 8) {
|
||||||
|
// Gate: send only once the worker is genuinely parked so every
|
||||||
|
// round is a real park-on-empty-mailbox.
|
||||||
|
loop {
|
||||||
|
match actor_info(wpid) {
|
||||||
|
Some(info) if info.state == ActorState::Parked => break,
|
||||||
|
Some(_) => yield_now(),
|
||||||
|
None => panic!("worker died early"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
park_tx.send(()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let (lazy, checksum) = done_rx.recv().unwrap();
|
||||||
|
// The spike was ~3 MiB; demand at least 2 MiB marked to leave slack
|
||||||
|
// for the redzone, rounding, and pages the unwind re-dirtied.
|
||||||
|
assert!(
|
||||||
|
lazy >= 2 * 1024 * 1024,
|
||||||
|
"expected ≥ 2 MiB LazyFree in the stack range, got {} bytes",
|
||||||
|
lazy
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
checksum,
|
||||||
|
64 * 0xA5u64,
|
||||||
|
"live stack data corrupted by shrink"
|
||||||
|
);
|
||||||
|
worker.join().unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Steady-state actors must never pay the syscall: an actor that parks a lot
|
||||||
|
/// but never spikes past the threshold ends with zero LazyFree in its stack.
|
||||||
|
#[test]
|
||||||
|
fn shallow_actor_never_shrinks() {
|
||||||
|
let rt = smarm::runtime::init(Config::exact(1));
|
||||||
|
rt.run(|| {
|
||||||
|
let (park_tx, park_rx) = channel::<()>();
|
||||||
|
let (done_tx, done_rx) = channel::<usize>();
|
||||||
|
|
||||||
|
let worker = spawn(move || {
|
||||||
|
let probe = 0u8;
|
||||||
|
let anchor = &probe as *const u8 as usize;
|
||||||
|
for _ in 0..(SHRINK_COOLDOWN + 8) {
|
||||||
|
park_rx.recv().unwrap();
|
||||||
|
}
|
||||||
|
done_tx
|
||||||
|
.send(lazy_free_bytes_in(anchor - 64 * 1024, anchor + 4096))
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
let wpid = worker.pid();
|
||||||
|
for _ in 0..(SHRINK_COOLDOWN + 8) {
|
||||||
|
loop {
|
||||||
|
match actor_info(wpid) {
|
||||||
|
Some(info) if info.state == ActorState::Parked => break,
|
||||||
|
Some(_) => yield_now(),
|
||||||
|
None => panic!("worker died early"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
park_tx.send(()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(done_rx.recv().unwrap(), 0, "steady-state actor was shrunk");
|
||||||
|
worker.join().unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
//! Reproducer (soak20 signature 2, refcount_test.exs "watcher crash"):
|
||||||
|
//! `by_name` stores only the slot *index*, so a name whose holder died — never
|
||||||
|
//! unregistered, since no smarm stop path unregisters (prune is lazy) — and
|
||||||
|
//! whose slot was then re-tenanted by an unrelated actor reads as *live-held*:
|
||||||
|
//!
|
||||||
|
//! - `register` of the name fails `NameTaken { holder: <unrelated tenant> }`,
|
||||||
|
//! so the bridge's generated `start()` (a `let _ =`) silently no-ops and
|
||||||
|
//! `start_server/1` reports `:ok` for a server that never came up;
|
||||||
|
//! - a by-name `call` resolves the tenant's mailbox, misses on the message
|
||||||
|
//! `TypeId`, and fails `ServerDown` fast — and does NOT prune (only the
|
||||||
|
//! dead-holder and dangling-name arms prune), so the name never heals
|
||||||
|
//! while the tenant lives. The wedge is self-sustaining.
|
||||||
|
//!
|
||||||
|
//! Wild signature: 110235x fast `{:error, :server_down}` probes over the full
|
||||||
|
//! 5 s await window after a swallowed restart (200-run width-20 soak, run 59).
|
||||||
|
//!
|
||||||
|
//! The test asserts the *contract*: after its holder dies, a name must be
|
||||||
|
//! re-registrable regardless of what happened to the slot. Red pre-fix.
|
||||||
|
|
||||||
|
use smarm::{
|
||||||
|
call, init, request_stop, whereis, CallError, Config, GenServer, GenServerBuilder,
|
||||||
|
GenServerName, RegisterError,
|
||||||
|
};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const TARGET: GenServerName<Target> = GenServerName::new("stale_reuse_target");
|
||||||
|
|
||||||
|
/// The named server whose death opens the window. Trivial on purpose.
|
||||||
|
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: ()) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The unrelated tenant. A *different* server type, so its mailbox holds a
|
||||||
|
/// different `Envelope` `TypeId` — a same-typed tenant would make the by-name
|
||||||
|
/// `call` *deliver to the wrong server* instead of failing, which is the same
|
||||||
|
/// root hole wearing a worse hat.
|
||||||
|
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 {
|
||||||
|
old_slot: (u32, u32),
|
||||||
|
tenant_slot: (u32, u32),
|
||||||
|
/// `whereis` of the dead name after re-tenanting — `Some` is the misread.
|
||||||
|
whereis_after_reuse: Option<(u32, u32)>,
|
||||||
|
/// By-name call after re-tenanting — the wild `server_down` fast-fail.
|
||||||
|
call_after_reuse: Result<(), CallError>,
|
||||||
|
/// The contract under test: re-registering the dead name.
|
||||||
|
restart: Result<(), RegisterError>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dead_name_with_reused_slot_must_be_re_registrable() {
|
||||||
|
let out: Arc<Mutex<Option<Observed>>> = Arc::new(Mutex::new(None));
|
||||||
|
let out_w = out.clone();
|
||||||
|
|
||||||
|
// A deliberately tiny slab forces prompt slot recycling: with every filler
|
||||||
|
// held alive, the freed slot is the only *recycled* one, so a filler lands
|
||||||
|
// on it deterministically well before the slab (a loud panic) runs out.
|
||||||
|
init(Config::exact(2).max_actors(32)).run(move || {
|
||||||
|
// 1. Named server up; record its slot.
|
||||||
|
let target = GenServerBuilder::new(Target)
|
||||||
|
.named(TARGET)
|
||||||
|
.start()
|
||||||
|
.expect("name should be free at test start");
|
||||||
|
let old_pid = target.pid();
|
||||||
|
|
||||||
|
// 2. Kill it WITHOUT unregistering (no stop path does). Death is
|
||||||
|
// confirmed via the *ref*, never the name — a by-name resolve of a
|
||||||
|
// dead-but-not-yet-reused holder takes the prune arm and heals the
|
||||||
|
// name, destroying the precondition.
|
||||||
|
request_stop(old_pid);
|
||||||
|
loop {
|
||||||
|
match target.call(()) {
|
||||||
|
Err(CallError::ServerDown) => break,
|
||||||
|
Ok(()) => smarm::sleep(Duration::from_millis(5)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drop(target);
|
||||||
|
|
||||||
|
// 3. Re-tenant the slot: spawn fillers (all kept alive) until one
|
||||||
|
// lands on the old index.
|
||||||
|
let mut fillers = Vec::new();
|
||||||
|
let mut tenant = None;
|
||||||
|
for i in 0..24 {
|
||||||
|
let name: &'static str = Box::leak(format!("stale_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();
|
||||||
|
fillers.push(f);
|
||||||
|
if fp.index() == old_pid.index() {
|
||||||
|
tenant = Some(fp);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let tenant = tenant.expect(
|
||||||
|
"precondition: the freed slot must be re-tenanted within the tiny slab \
|
||||||
|
(slots are recycled; every filler is held alive)",
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Observe the poisoned state through the same paths the bridge uses.
|
||||||
|
let whereis_after_reuse = whereis(TARGET.as_str()).map(|p| (p.index(), p.generation()));
|
||||||
|
let call_after_reuse = call(TARGET, ());
|
||||||
|
let restart = GenServerBuilder::new(Target)
|
||||||
|
.named(TARGET)
|
||||||
|
.start()
|
||||||
|
.map(|_fresh_ref| ());
|
||||||
|
|
||||||
|
*out_w.lock().unwrap() = Some(Observed {
|
||||||
|
old_slot: (old_pid.index(), old_pid.generation()),
|
||||||
|
tenant_slot: (tenant.index(), tenant.generation()),
|
||||||
|
whereis_after_reuse,
|
||||||
|
call_after_reuse,
|
||||||
|
restart,
|
||||||
|
});
|
||||||
|
|
||||||
|
drop(fillers);
|
||||||
|
});
|
||||||
|
|
||||||
|
let o = out.lock().unwrap().take().expect("run body completed");
|
||||||
|
eprintln!("observed: {o:?}");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
o.restart.is_ok(),
|
||||||
|
"re-registering '{}' after its holder died failed with {:?}: the dead name \
|
||||||
|
reads as held by the live, unrelated tenant {:?} because by_name kept only \
|
||||||
|
the slot index (old slot {:?}). This is the silent-no-op start_server path \
|
||||||
|
of soak20 signature 2.",
|
||||||
|
TARGET.as_str(),
|
||||||
|
o.restart,
|
||||||
|
o.tenant_slot,
|
||||||
|
o.old_slot,
|
||||||
|
);
|
||||||
|
|
||||||
|
// The healed semantics around the re-register: the stale name reads
|
||||||
|
// *unbound* (never the tenant), and a by-name call fails ServerDown rather
|
||||||
|
// than resolving anything of the tenant's.
|
||||||
|
assert_eq!(
|
||||||
|
o.whereis_after_reuse, None,
|
||||||
|
"whereis of a dead name must prune and report unbound, not the slot's new tenant",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
o.call_after_reuse,
|
||||||
|
Err(CallError::ServerDown),
|
||||||
|
"a by-name call to a dead name must fail ServerDown",
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
//! Reproducer: a *named* gen_server stopped with `request_stop` while a `call`
|
||||||
|
//! sits **un-dequeued** in its inbox does NOT release the parked caller with
|
||||||
|
//! `CallError::ServerDown`. The caller parks forever, contradicting the
|
||||||
|
//! documented gen_server guarantee ("Any caller currently waiting in `call`
|
||||||
|
//! sees `Err(ServerDown)`").
|
||||||
|
//!
|
||||||
|
//! Root cause (channel.rs): `Receiver::Drop` only flips `receiver_alive = false`
|
||||||
|
//! and never drains `queue`. The queued `Envelope::Call(_, reply_tx)` therefore
|
||||||
|
//! survives as long as the channel `Arc<Inner>` does — and for a *named* server
|
||||||
|
//! the registry holds a `Sender` clone (lazy prune) that keeps the `Arc` alive
|
||||||
|
//! after the server is gone. So the queued `reply_tx` is never dropped, the
|
||||||
|
//! caller's `reply_rx` never closes, and `reply_rx.recv()` parks forever.
|
||||||
|
//!
|
||||||
|
//! Anonymous servers happen to dodge this: when their last `GenServerRef`
|
||||||
|
//! drops, every `Sender` drops, the `Arc` refcount hits zero, `Inner` (and its
|
||||||
|
//! queue) is dropped, and the queued `reply_tx` goes with it — waking the
|
||||||
|
//! caller. The bug is specific to "a `Sender` outlives the `Receiver`", which a
|
||||||
|
//! registry entry guarantees for every named server.
|
||||||
|
|
||||||
|
use smarm::{
|
||||||
|
call, channel, init, request_stop, spawn, CallError, Config, GenServer, GenServerBuilder,
|
||||||
|
GenServerName, Receiver, RecvTimeoutError,
|
||||||
|
};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const BLOCKER: GenServerName<Blocker> = GenServerName::new("repro_blocker");
|
||||||
|
|
||||||
|
/// A server that, on its single cast, parks forever on a gate channel the test
|
||||||
|
/// never feeds. This deterministically holds the server *inside a handler* (not
|
||||||
|
/// at the inbox recv), so any subsequent `call` queues behind it and stays
|
||||||
|
/// un-dequeued — exactly the state `request_stop` then has to clean up.
|
||||||
|
struct Blocker {
|
||||||
|
gate: Option<Receiver<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GenServer for Blocker {
|
||||||
|
type Call = ();
|
||||||
|
type Reply = ();
|
||||||
|
type Cast = ();
|
||||||
|
type Info = ();
|
||||||
|
type Timer = ();
|
||||||
|
|
||||||
|
// Trivial + instant: if this ever ran for the queued call, the caller would
|
||||||
|
// get Ok(()) immediately. It must NOT run — the server is parked on the gate
|
||||||
|
// when the stop arrives.
|
||||||
|
fn handle_call(&mut self, _req: ()) {}
|
||||||
|
|
||||||
|
// Park forever (until cancelled). recv() on an open channel with no message
|
||||||
|
// parks the actor; the gate sender is held by the test and never fires.
|
||||||
|
fn handle_cast(&mut self, _op: ()) {
|
||||||
|
if let Some(gate) = self.gate.take() {
|
||||||
|
let _ = gate.recv();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn named_server_request_stop_releases_queued_caller_with_server_down() {
|
||||||
|
// Final observation, asserted after the run.
|
||||||
|
// Some(Err(ServerDown)) -> contract honored (fixed)
|
||||||
|
// None -> caller never released; parked past the 3s
|
||||||
|
// bound (bug reproduced)
|
||||||
|
let outcome: Arc<Mutex<Option<Result<(), CallError>>>> = Arc::new(Mutex::new(None));
|
||||||
|
let outcome_w = outcome.clone();
|
||||||
|
|
||||||
|
init(Config::exact(2)).run(move || {
|
||||||
|
// Gate the server will park on. Held for the whole run so the server's
|
||||||
|
// gate.recv() parks (rather than seeing Disconnected and returning).
|
||||||
|
let (gate_tx, gate_rx) = channel::<()>();
|
||||||
|
|
||||||
|
// Channel the queued caller reports its result back on.
|
||||||
|
let (res_tx, res_rx) = channel::<Result<(), CallError>>();
|
||||||
|
|
||||||
|
// 1. Start the named server and keep its ref alive.
|
||||||
|
let server = GenServerBuilder::new(Blocker {
|
||||||
|
gate: Some(gate_rx),
|
||||||
|
})
|
||||||
|
.named(BLOCKER)
|
||||||
|
.start()
|
||||||
|
.expect("name should be free");
|
||||||
|
let spid = server.pid();
|
||||||
|
|
||||||
|
// 2. Send the cast and let the server dequeue it and park on the gate.
|
||||||
|
server.cast(()).expect("server is live");
|
||||||
|
smarm::sleep(Duration::from_millis(100));
|
||||||
|
|
||||||
|
// 3. A separate caller issues a by-name `call`. The server is parked on
|
||||||
|
// the gate, so this Call envelope queues un-dequeued; the caller then
|
||||||
|
// parks on its reply channel.
|
||||||
|
spawn(move || {
|
||||||
|
let r = call(BLOCKER, ());
|
||||||
|
let _ = res_tx.send(r);
|
||||||
|
});
|
||||||
|
smarm::sleep(Duration::from_millis(100));
|
||||||
|
|
||||||
|
// 4. Stop the server. Its loop unwinds out of the gate.recv() and drops
|
||||||
|
// the inbox Receiver — at which point the queued caller is *supposed*
|
||||||
|
// to be released with ServerDown.
|
||||||
|
request_stop(spid);
|
||||||
|
|
||||||
|
// 5. Bounded wait. A correct runtime releases the caller in well under
|
||||||
|
// 3s; the bug leaves it parked, so we time out.
|
||||||
|
let observed = match res_rx.recv_timeout(Duration::from_secs(3)) {
|
||||||
|
Ok(r) => Some(r),
|
||||||
|
Err(RecvTimeoutError::Timeout) => None,
|
||||||
|
Err(RecvTimeoutError::Disconnected) => None,
|
||||||
|
};
|
||||||
|
*outcome_w.lock().unwrap() = observed;
|
||||||
|
|
||||||
|
// Keep the gate sender alive until the very end.
|
||||||
|
drop(gate_tx);
|
||||||
|
});
|
||||||
|
|
||||||
|
let observed = outcome.lock().unwrap().take();
|
||||||
|
assert_eq!(
|
||||||
|
observed,
|
||||||
|
Some(Err(CallError::ServerDown)),
|
||||||
|
"queued caller was not released with ServerDown after the named server \
|
||||||
|
was request_stop'd (None = parked forever => bug reproduced)"
|
||||||
|
);
|
||||||
|
}
|
||||||
+16
-8
@@ -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
@@ -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]
|
||||||
|
|||||||
@@ -0,0 +1,276 @@
|
|||||||
|
//! The terminal-record contract (bridge soak signature 4): a watch installed
|
||||||
|
//! *after* its target's death — the async-install race the bridge's proxies
|
||||||
|
//! live with — must be able to recover the real down reason instead of a
|
||||||
|
//! blanket `NoProc`. Two primitives carry it:
|
||||||
|
//!
|
||||||
|
//! - `finalize_actor` stamps the slot with `(generation, DownReason)`; the
|
||||||
|
//! record survives reclaim, registry pruning, and the next tenant's
|
||||||
|
//! install, and is overwritten only by the slot's next death.
|
||||||
|
//! [`terminal_reason`] reads it generation-matched.
|
||||||
|
//! - [`resolve_name`] is `whereis` with the corpse kept: the dead-holder arm
|
||||||
|
//! returns the stored pid it prunes ([`NameResolution::Corpse`]) instead
|
||||||
|
//! of discarding the only evidence of *who* died. `Unbound` stays the
|
||||||
|
//! Erlang-shaped `noproc` for names that were never (or are no longer)
|
||||||
|
//! bound.
|
||||||
|
//!
|
||||||
|
//! `monitor()` of a stale pid still queues plain `NoProc` — the upgrade is a
|
||||||
|
//! caller's deliberate act, not a semantics change.
|
||||||
|
|
||||||
|
use smarm::{
|
||||||
|
init, mark_watchable, request_stop, resolve_name, terminal_reason, CallError, Config,
|
||||||
|
DownReason, GenServer, GenServerBuilder, GenServerName, NameResolution,
|
||||||
|
};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const TARGET: GenServerName<Target> = GenServerName::new("terminal_target");
|
||||||
|
|
||||||
|
/// Named server that panics on cast — the sig-4 death.
|
||||||
|
struct Target;
|
||||||
|
|
||||||
|
impl GenServer for Target {
|
||||||
|
type Call = ();
|
||||||
|
type Reply = ();
|
||||||
|
type Cast = ();
|
||||||
|
type Info = ();
|
||||||
|
type Timer = ();
|
||||||
|
|
||||||
|
fn handle_call(&mut self, _req: ()) {}
|
||||||
|
fn handle_cast(&mut self, _op: ()) {
|
||||||
|
panic!("terminal_target: induced panic");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Slot filler for the re-tenancy phase (distinct type, held alive).
|
||||||
|
struct Filler;
|
||||||
|
|
||||||
|
impl GenServer for Filler {
|
||||||
|
type Call = ();
|
||||||
|
type Reply = ();
|
||||||
|
type Cast = ();
|
||||||
|
type Info = ();
|
||||||
|
type Timer = ();
|
||||||
|
|
||||||
|
fn handle_call(&mut self, _req: ()) {}
|
||||||
|
fn handle_cast(&mut self, _op: ()) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Observed {
|
||||||
|
exit_reason: Option<DownReason>,
|
||||||
|
anon_reason: Option<DownReason>,
|
||||||
|
/// Anonymous but export-marked while alive — must stamp (sig 5).
|
||||||
|
marked_reason: Option<DownReason>,
|
||||||
|
/// Marked only after death — must remain unknowable.
|
||||||
|
marked_late_reason: Option<DownReason>,
|
||||||
|
panic_reason: Option<DownReason>,
|
||||||
|
stopped_reason: Option<DownReason>,
|
||||||
|
live_reason: Option<DownReason>,
|
||||||
|
live_resolution_is_live: bool,
|
||||||
|
unknown_resolution: NameResolution,
|
||||||
|
/// First resolve after the named target's panic — must be Corpse(old pid).
|
||||||
|
corpse_resolution_matches: bool,
|
||||||
|
/// Second resolve — the Corpse arm pruned, so the name has healed.
|
||||||
|
resolution_after_prune: NameResolution,
|
||||||
|
/// Read AFTER the prune above: the record is slot-side, not registry-side.
|
||||||
|
corpse_reason_after_prune: Option<DownReason>,
|
||||||
|
/// Record survives the slot being re-tenanted (new tenant still alive).
|
||||||
|
corpse_reason_after_reuse: Option<DownReason>,
|
||||||
|
/// ... and dies with the next tenancy's death (overwritten).
|
||||||
|
corpse_reason_after_tenant_death: Option<DownReason>,
|
||||||
|
tenant_reason: Option<DownReason>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn terminal_record_recovers_the_reason_a_raced_watch_lost() {
|
||||||
|
let out: Arc<Mutex<Option<Observed>>> = Arc::new(Mutex::new(None));
|
||||||
|
let out_w = out.clone();
|
||||||
|
|
||||||
|
// Tiny slab: prompt slot recycling for the re-tenancy phase.
|
||||||
|
init(Config::exact(2).max_actors(32)).run(move || {
|
||||||
|
// --- Registered plain actors: one record per way of dying. The
|
||||||
|
// record is named-tenancy-only, so each actor self-registers a
|
||||||
|
// throwaway channel before dying; the anonymous control below pins
|
||||||
|
// the complement.
|
||||||
|
let h = smarm::spawn(|| {
|
||||||
|
let (tx, _rx) = smarm::channel::<()>();
|
||||||
|
let _ = smarm::register(smarm::Name::<()>::new("terminal_probe_exit"), tx);
|
||||||
|
});
|
||||||
|
let pid_exit = h.pid();
|
||||||
|
let _ = h.join();
|
||||||
|
let exit_reason = terminal_reason(pid_exit);
|
||||||
|
|
||||||
|
let h = smarm::spawn(|| {
|
||||||
|
let (tx, _rx) = smarm::channel::<()>();
|
||||||
|
let _ = smarm::register(smarm::Name::<()>::new("terminal_probe_panic"), tx);
|
||||||
|
panic!("induced");
|
||||||
|
});
|
||||||
|
let pid_panic = h.pid();
|
||||||
|
let _ = h.join();
|
||||||
|
let panic_reason = terminal_reason(pid_panic);
|
||||||
|
|
||||||
|
let h = smarm::spawn(|| {
|
||||||
|
let (tx, _rx) = smarm::channel::<()>();
|
||||||
|
let _ = smarm::register(smarm::Name::<()>::new("terminal_probe_stop"), tx);
|
||||||
|
loop {
|
||||||
|
smarm::sleep(Duration::from_millis(2));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let pid_stop = h.pid();
|
||||||
|
request_stop(pid_stop);
|
||||||
|
let _ = h.join();
|
||||||
|
let stopped_reason = terminal_reason(pid_stop);
|
||||||
|
|
||||||
|
// --- Anonymous control: an unregistered death must NOT stamp (nor
|
||||||
|
// evict) — the free list is LIFO, so green-thread churn would
|
||||||
|
// otherwise overwrite a watchable record faster than any race
|
||||||
|
// window this exists to cover.
|
||||||
|
let h = smarm::spawn(|| panic!("anonymous"));
|
||||||
|
let pid_anon = h.pid();
|
||||||
|
let _ = h.join();
|
||||||
|
let anon_reason = terminal_reason(pid_anon);
|
||||||
|
|
||||||
|
// --- mark_watchable: the bridge's export-seam eligibility (sig 5).
|
||||||
|
// An anonymous actor marked while alive stamps like a named one ...
|
||||||
|
let h = smarm::spawn(|| loop {
|
||||||
|
smarm::sleep(Duration::from_millis(2));
|
||||||
|
});
|
||||||
|
let pid_marked = h.pid();
|
||||||
|
mark_watchable(pid_marked);
|
||||||
|
request_stop(pid_marked);
|
||||||
|
let _ = h.join();
|
||||||
|
let marked_reason = terminal_reason(pid_marked);
|
||||||
|
|
||||||
|
// ... while marking a pid whose tenancy already ended is a no-op:
|
||||||
|
// the history is honestly unknowable, not retroactively invented.
|
||||||
|
mark_watchable(pid_anon);
|
||||||
|
let marked_late_reason = terminal_reason(pid_anon);
|
||||||
|
|
||||||
|
// --- The named target: live readings first. -----------------------
|
||||||
|
let target = GenServerBuilder::new(Target)
|
||||||
|
.named(TARGET)
|
||||||
|
.start()
|
||||||
|
.expect("name free at test start");
|
||||||
|
let old_pid = target.pid();
|
||||||
|
let live_reason = terminal_reason(old_pid);
|
||||||
|
let live_resolution_is_live =
|
||||||
|
resolve_name(TARGET.as_str()) == NameResolution::Live(old_pid.erase());
|
||||||
|
let unknown_resolution = resolve_name("terminal_never_bound");
|
||||||
|
|
||||||
|
// --- Kill it by panic; confirm death via the ref, NEVER the name
|
||||||
|
// (any name reader would take the prune arm and destroy the corpse
|
||||||
|
// precondition — the same trap stale_name_slot_reuse.rs documents).
|
||||||
|
let _ = target.cast(());
|
||||||
|
loop {
|
||||||
|
match target.call(()) {
|
||||||
|
Err(CallError::ServerDown) => break,
|
||||||
|
Ok(()) => smarm::sleep(Duration::from_millis(2)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let corpse_resolution_matches =
|
||||||
|
resolve_name(TARGET.as_str()) == NameResolution::Corpse(old_pid.erase());
|
||||||
|
let resolution_after_prune = resolve_name(TARGET.as_str());
|
||||||
|
let corpse_reason_after_prune = terminal_reason(old_pid);
|
||||||
|
|
||||||
|
// --- Re-tenant the freed slot; the record must outlive the install
|
||||||
|
// and die only with the next tenancy's death.
|
||||||
|
let mut fillers = Vec::new();
|
||||||
|
let mut tenant = None;
|
||||||
|
for i in 0..24 {
|
||||||
|
let name: &'static str = Box::leak(format!("terminal_filler_{i}").into_boxed_str());
|
||||||
|
let f = GenServerBuilder::new(Filler)
|
||||||
|
.named(GenServerName::<Filler>::new(name))
|
||||||
|
.start()
|
||||||
|
.expect("filler names are fresh");
|
||||||
|
let fp = f.pid();
|
||||||
|
let landed = fp.index() == old_pid.index();
|
||||||
|
fillers.push(f);
|
||||||
|
if landed {
|
||||||
|
tenant = Some((fillers.len() - 1, fp));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let (tenant_at, tenant_pid) = tenant.expect(
|
||||||
|
"precondition: the freed slot must be re-tenanted within the tiny slab \
|
||||||
|
(slots are recycled; every filler is held alive)",
|
||||||
|
);
|
||||||
|
let corpse_reason_after_reuse = terminal_reason(old_pid);
|
||||||
|
|
||||||
|
request_stop(tenant_pid);
|
||||||
|
loop {
|
||||||
|
match fillers[tenant_at].call(()) {
|
||||||
|
Err(CallError::ServerDown) => break,
|
||||||
|
Ok(()) => smarm::sleep(Duration::from_millis(2)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let corpse_reason_after_tenant_death = terminal_reason(old_pid);
|
||||||
|
let tenant_reason = terminal_reason(tenant_pid);
|
||||||
|
|
||||||
|
*out_w.lock().unwrap() = Some(Observed {
|
||||||
|
exit_reason,
|
||||||
|
anon_reason,
|
||||||
|
panic_reason,
|
||||||
|
stopped_reason,
|
||||||
|
live_reason,
|
||||||
|
live_resolution_is_live,
|
||||||
|
unknown_resolution,
|
||||||
|
corpse_resolution_matches,
|
||||||
|
resolution_after_prune,
|
||||||
|
marked_reason,
|
||||||
|
marked_late_reason,
|
||||||
|
corpse_reason_after_prune,
|
||||||
|
corpse_reason_after_reuse,
|
||||||
|
corpse_reason_after_tenant_death,
|
||||||
|
tenant_reason,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let o = out.lock().unwrap().take().expect("runtime body completed");
|
||||||
|
assert_eq!(o.exit_reason, Some(DownReason::Exit), "{o:?}");
|
||||||
|
assert_eq!(
|
||||||
|
o.anon_reason, None,
|
||||||
|
"anonymous deaths must not stamp: {o:?}"
|
||||||
|
);
|
||||||
|
assert_eq!(o.panic_reason, Some(DownReason::Panic), "{o:?}");
|
||||||
|
assert_eq!(
|
||||||
|
o.marked_reason,
|
||||||
|
Some(DownReason::Stopped),
|
||||||
|
"mark_watchable while alive must make the death stamp: {o:?}"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
o.marked_late_reason, None,
|
||||||
|
"marking a dead tenancy must not invent history: {o:?}"
|
||||||
|
);
|
||||||
|
assert_eq!(o.stopped_reason, Some(DownReason::Stopped), "{o:?}");
|
||||||
|
assert_eq!(
|
||||||
|
o.live_reason, None,
|
||||||
|
"live tenancy must have no record: {o:?}"
|
||||||
|
);
|
||||||
|
assert!(o.live_resolution_is_live, "{o:?}");
|
||||||
|
assert_eq!(o.unknown_resolution, NameResolution::Unbound, "{o:?}");
|
||||||
|
assert!(
|
||||||
|
o.corpse_resolution_matches,
|
||||||
|
"first post-death resolve must carry the corpse: {o:?}"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
o.resolution_after_prune,
|
||||||
|
NameResolution::Unbound,
|
||||||
|
"the Corpse arm prunes — the name heals: {o:?}"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
o.corpse_reason_after_prune,
|
||||||
|
Some(DownReason::Panic),
|
||||||
|
"the record is slot-side; registry pruning must not touch it: {o:?}"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
o.corpse_reason_after_reuse,
|
||||||
|
Some(DownReason::Panic),
|
||||||
|
"a new tenant's install must leave the previous tenancy's record: {o:?}"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
o.corpse_reason_after_tenant_death, None,
|
||||||
|
"the next death overwrites — the old generation no longer matches: {o:?}"
|
||||||
|
);
|
||||||
|
assert_eq!(o.tenant_reason, Some(DownReason::Stopped), "{o:?}");
|
||||||
|
}
|
||||||
@@ -35,7 +35,10 @@ impl PipePair {
|
|||||||
let mut fds: [libc::c_int; 2] = [0; 2];
|
let 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)`
|
||||||
|
|||||||
+87
-8
@@ -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.
|
||||||
@@ -401,7 +421,66 @@ fn send_after_to_dead_typed_pid_is_silent() {
|
|||||||
assert_eq!(report_rx.recv().unwrap(), 1); // sink has now exited
|
assert_eq!(report_rx.recv().unwrap(), 1); // sink has now exited
|
||||||
let _id = send_after(Duration::from_millis(15), sink, 2);
|
let _id = send_after(Duration::from_millis(15), sink, 2);
|
||||||
sleep(Duration::from_millis(45)); // let it fire against the dead pid
|
sleep(Duration::from_millis(45)); // let it fire against the dead pid
|
||||||
// No panic, and nothing further delivered.
|
// No panic; the sink is gone, so its report sender dropped with it —
|
||||||
assert_eq!(report_rx.try_recv(), Ok(None));
|
// closed+empty is Err (documented), which also proves nothing
|
||||||
|
// further was delivered.
|
||||||
|
assert!(report_rx.try_recv().is_err(), "nothing further delivered");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Wall-anchored send_after (RFC 007 user-facing opt-out). The API exists in
|
||||||
|
// both feature configs; featureless it is behaviourally identical to
|
||||||
|
// `send_after` — these tests pin exactly that.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn armed_wall_send_timer_is_returned_and_fires() {
|
||||||
|
let mut t = Timers::new();
|
||||||
|
let now = Instant::now();
|
||||||
|
let fired = Arc::new(AtomicBool::new(false));
|
||||||
|
let f = fired.clone();
|
||||||
|
let _id = t.insert_send_wall(
|
||||||
|
now + Duration::from_millis(10),
|
||||||
|
Pid::new(0, 0),
|
||||||
|
Box::new(move || f.store(true, Ordering::SeqCst)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut due = t.pop_due(now + Duration::from_millis(20));
|
||||||
|
assert_eq!(due.len(), 1, "an armed wall send timer should pop when due");
|
||||||
|
run_fire(due.pop().unwrap());
|
||||||
|
assert!(fired.load(Ordering::SeqCst), "running the thunk delivers");
|
||||||
|
assert!(t.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
use smarm::send_after_named_wall;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn send_after_named_wall_delivers_after_the_delay() {
|
||||||
|
const WPING: Name<u64> = Name::new("send_after_wall_ping");
|
||||||
|
run(|| {
|
||||||
|
let (tx, rx) = channel::<u64>();
|
||||||
|
register(WPING, tx).unwrap();
|
||||||
|
let t0 = Instant::now();
|
||||||
|
let _id = send_after_named_wall(Duration::from_millis(30), WPING, 99);
|
||||||
|
assert_eq!(rx.recv().unwrap(), 99);
|
||||||
|
assert!(
|
||||||
|
t0.elapsed() >= Duration::from_millis(25),
|
||||||
|
"delivered too early: {:?}",
|
||||||
|
t0.elapsed()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn send_after_named_wall_cancels() {
|
||||||
|
const WC: Name<u64> = Name::new("send_after_wall_cancel");
|
||||||
|
run(|| {
|
||||||
|
let (tx, rx) = channel::<u64>();
|
||||||
|
register(WC, tx).unwrap();
|
||||||
|
let id = send_after_named_wall(Duration::from_millis(50), WC, 7);
|
||||||
|
assert!(cancel_timer(id), "cancel before fire returns true");
|
||||||
|
sleep(Duration::from_millis(90));
|
||||||
|
assert_eq!(rx.try_recv(), Ok(None), "cancelled wall timer delivered");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
//! Non-panicking spawn at slab capacity (`try_spawn`).
|
||||||
|
//!
|
||||||
|
//! Covers: parity with `spawn` when slots are free; `Err(AtCapacity)` instead
|
||||||
|
//! of a panic on a full slab (the spawning actor survives — the crash-loop
|
||||||
|
//! from the motivating slowloris incident cannot start); self-heal (a freed
|
||||||
|
//! slot makes the next `try_spawn` succeed); and exact claim-or-report
|
||||||
|
//! accounting under a multi-thread race for the last slots (no TOCTOU
|
||||||
|
//! overshoot, no panic).
|
||||||
|
|
||||||
|
use smarm::runtime::Config;
|
||||||
|
use smarm::{spawn, try_spawn, try_spawn_under_with, yield_now, SpawnError, SpawnOpts};
|
||||||
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// A child that holds its slot until `release` flips, without parking
|
||||||
|
/// machinery: busy-yield keeps the scheduler moving and the slot occupied.
|
||||||
|
fn holder(release: Arc<AtomicBool>) -> impl FnOnce() + Send + 'static {
|
||||||
|
move || {
|
||||||
|
while !release.load(Ordering::Acquire) {
|
||||||
|
yield_now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn try_spawn_is_spawn_when_slots_free() {
|
||||||
|
smarm::runtime::init(Config::exact(1)).run(|| {
|
||||||
|
let ran = Arc::new(AtomicBool::new(false));
|
||||||
|
let flag = ran.clone();
|
||||||
|
let h = try_spawn(move || flag.store(true, Ordering::Release))
|
||||||
|
.expect("slots free — must behave exactly like spawn");
|
||||||
|
h.join().unwrap();
|
||||||
|
assert!(ran.load(Ordering::Acquire));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn at_capacity_is_err_not_panic_and_accounting_is_exact() {
|
||||||
|
const MAX: usize = 8;
|
||||||
|
smarm::runtime::init(Config::exact(1).max_actors(MAX)).run(|| {
|
||||||
|
let release = Arc::new(AtomicBool::new(false));
|
||||||
|
// Fill the slab from the initial actor: slots are claimed at spawn
|
||||||
|
// time, so children need not have run yet. Count until refusal.
|
||||||
|
let mut held = Vec::new();
|
||||||
|
loop {
|
||||||
|
match try_spawn(holder(release.clone())) {
|
||||||
|
Ok(h) => held.push(h),
|
||||||
|
Err(e) => {
|
||||||
|
assert_eq!(e, SpawnError::AtCapacity);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Initial actor occupies one slot; the rest were spawnable.
|
||||||
|
assert_eq!(held.len(), MAX - 1, "slab accounting must be exact");
|
||||||
|
// Still refusing (and still not panicking) on repeat.
|
||||||
|
assert!(matches!(try_spawn(|| ()), Err(SpawnError::AtCapacity)));
|
||||||
|
// The `_with` surface refuses identically — a custom shape must not
|
||||||
|
// reach stack allocation when there is no slot for it.
|
||||||
|
let opts = SpawnOpts {
|
||||||
|
stack_reserve: Some(1024 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
};
|
||||||
|
assert!(matches!(
|
||||||
|
try_spawn_under_with(smarm::self_pid(), opts, || ()),
|
||||||
|
Err(SpawnError::AtCapacity)
|
||||||
|
));
|
||||||
|
|
||||||
|
// Self-heal: free the slots, join, and the next try_spawn succeeds.
|
||||||
|
release.store(true, Ordering::Release);
|
||||||
|
for h in held {
|
||||||
|
h.join().unwrap();
|
||||||
|
}
|
||||||
|
let h = try_spawn(|| ()).expect("slots freed — must succeed again");
|
||||||
|
h.join().unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plain_spawn_still_panics_at_capacity() {
|
||||||
|
// The existing invariant-check semantics of `spawn` are untouched: at a
|
||||||
|
// full slab it panics, the panic is caught at the actor isolation
|
||||||
|
// boundary, and it surfaces as a join error — exactly as before. The
|
||||||
|
// bomb actor is spawned into the LAST slot (so the slab is full only
|
||||||
|
// once the bomb itself is live) and the panic lands inside the bomb,
|
||||||
|
// not the initial actor.
|
||||||
|
const MAX: usize = 6;
|
||||||
|
smarm::runtime::init(Config::exact(1).max_actors(MAX)).run(|| {
|
||||||
|
let release = Arc::new(AtomicBool::new(false));
|
||||||
|
let mut held = Vec::new();
|
||||||
|
for _ in 0..MAX - 2 {
|
||||||
|
held.push(spawn(holder(release.clone())));
|
||||||
|
}
|
||||||
|
let armed = Arc::new(AtomicBool::new(false));
|
||||||
|
let armed2 = armed.clone();
|
||||||
|
let bomb = spawn(move || {
|
||||||
|
armed2.store(true, Ordering::Release);
|
||||||
|
// Slab is now full (initial + MAX−2 holders + this actor); the
|
||||||
|
// plain spawn must panic this actor.
|
||||||
|
let _ = spawn(|| ());
|
||||||
|
unreachable!("allocate_slot must have panicked");
|
||||||
|
});
|
||||||
|
let err = bomb
|
||||||
|
.join()
|
||||||
|
.expect_err("bomb must die by panic, not run through");
|
||||||
|
assert!(armed.load(Ordering::Acquire), "bomb must have actually run");
|
||||||
|
// The panic message is a formatted String (panic! with args).
|
||||||
|
let msg = err
|
||||||
|
.payload
|
||||||
|
.downcast_ref::<String>()
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| "<non-string payload>".into());
|
||||||
|
assert!(
|
||||||
|
msg.contains("slot table exhausted"),
|
||||||
|
"panic must be the slab-exhaustion invariant message, got: {msg}"
|
||||||
|
);
|
||||||
|
release.store(true, Ordering::Release);
|
||||||
|
for h in held {
|
||||||
|
h.join().unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn racing_try_spawns_claim_exactly_the_free_slots() {
|
||||||
|
// 4 scheduler threads, 4 spawner actors hammering try_spawn for a small
|
||||||
|
// pool of remaining slots. Claim-or-report must hand out exactly the
|
||||||
|
// free slots across all racers — no overshoot (TOCTOU), no panic.
|
||||||
|
const MAX: usize = 32;
|
||||||
|
const SPAWNERS: usize = 4;
|
||||||
|
smarm::runtime::init(Config::exact(4).max_actors(MAX)).run(|| {
|
||||||
|
let release = Arc::new(AtomicBool::new(false));
|
||||||
|
let won = Arc::new(AtomicUsize::new(0));
|
||||||
|
let done = Arc::new(AtomicUsize::new(0));
|
||||||
|
|
||||||
|
// Occupy some slots up front so the racers fight over a remainder.
|
||||||
|
let mut pre = Vec::new();
|
||||||
|
for _ in 0..8 {
|
||||||
|
pre.push(spawn(holder(release.clone())));
|
||||||
|
}
|
||||||
|
// Free slots now: MAX − 1 (initial) − 8 (pre) − SPAWNERS.
|
||||||
|
let up_for_grabs = MAX - 1 - 8 - SPAWNERS;
|
||||||
|
|
||||||
|
let mut spawners = Vec::new();
|
||||||
|
for _ in 0..SPAWNERS {
|
||||||
|
let release = release.clone();
|
||||||
|
let won = won.clone();
|
||||||
|
let done = done.clone();
|
||||||
|
spawners.push(spawn(move || {
|
||||||
|
loop {
|
||||||
|
match try_spawn(holder(release.clone())) {
|
||||||
|
Ok(h) => {
|
||||||
|
won.fetch_add(1, Ordering::AcqRel);
|
||||||
|
drop(h); // detached; slot held by the holder
|
||||||
|
}
|
||||||
|
Err(SpawnError::AtCapacity) => break,
|
||||||
|
Err(_) => unreachable!("non_exhaustive future-proofing"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
done.fetch_add(1, Ordering::AcqRel);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// Wait for every racer to hit AtCapacity.
|
||||||
|
while done.load(Ordering::Acquire) < SPAWNERS {
|
||||||
|
yield_now();
|
||||||
|
}
|
||||||
|
assert_eq!(won.load(Ordering::Acquire), up_for_grabs);
|
||||||
|
|
||||||
|
release.store(true, Ordering::Release);
|
||||||
|
for h in pre.into_iter().chain(spawners) {
|
||||||
|
h.join().unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_error_is_a_real_error() {
|
||||||
|
let e = SpawnError::AtCapacity;
|
||||||
|
let msg = format!("{e}");
|
||||||
|
assert!(
|
||||||
|
msg.contains("capacity"),
|
||||||
|
"Display should name the condition: {msg}"
|
||||||
|
);
|
||||||
|
let _: &dyn std::error::Error = &e;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user