Compare commits
5
Commits
301e3463e3
..
v0.6.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca1c98336e | ||
|
|
95306c7f60 | ||
|
|
1262cc30e3 | ||
|
|
461fe4b768 | ||
|
|
b937f1f50f |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "smarm"
|
name = "smarm"
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.95"
|
rust-version = "1.95"
|
||||||
|
|
||||||
|
|||||||
+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);
|
||||||
|
|
||||||
|
|||||||
+90
-41
@@ -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)
|
||||||
let (lo, hi) = primes_slice(w);
|
.map(|w| {
|
||||||
count_primes(lo, hi)
|
let (lo, hi) = primes_slice(w);
|
||||||
}).sum();
|
count_primes(lo, hi)
|
||||||
|
})
|
||||||
|
.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 {
|
||||||
if v + 1 >= PING_ROUNDS { break; }
|
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,24 +359,34 @@ 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!(
|
||||||
run_n("baseline (serial)", ITERS, bench_primes_baseline);
|
"Fan-out/fan-in: count primes in [2, {PRIME_N}) across {WORKERS} workers"
|
||||||
run_n("smarm single-thread", ITERS, || bench_primes_smarm(1));
|
));
|
||||||
run_n(&format!("smarm {n}-thread"), ITERS, || bench_primes_smarm(n));
|
run_n("baseline (serial)", ITERS, bench_primes_baseline);
|
||||||
run_n("tokio current_thread", ITERS, bench_primes_tokio_current);
|
run_n("smarm single-thread", ITERS, || bench_primes_smarm(1));
|
||||||
run_n("tokio multi-thread", ITERS, bench_primes_tokio_multi);
|
run_n(&format!("smarm {n}-thread"), ITERS, || {
|
||||||
|
bench_primes_smarm(n)
|
||||||
|
});
|
||||||
|
run_n("tokio current_thread", ITERS, bench_primes_tokio_current);
|
||||||
|
run_n("tokio multi-thread", ITERS, bench_primes_tokio_multi);
|
||||||
|
|
||||||
// ---- Ping-pong ----
|
// ---- Ping-pong ----
|
||||||
print_header(&format!("Ping-pong: {PING_ROUNDS} round-trips between two actors"));
|
print_header(&format!(
|
||||||
run_n("smarm single-thread", ITERS, || bench_pingpong_smarm(1));
|
"Ping-pong: {PING_ROUNDS} round-trips between two actors"
|
||||||
run_n(&format!("smarm {n}-thread"), ITERS, || bench_pingpong_smarm(n));
|
));
|
||||||
run_n("tokio current_thread", ITERS, bench_pingpong_tokio_current);
|
run_n("smarm single-thread", ITERS, || bench_pingpong_smarm(1));
|
||||||
run_n("tokio multi-thread", ITERS, bench_pingpong_tokio_multi);
|
run_n(&format!("smarm {n}-thread"), ITERS, || {
|
||||||
|
bench_pingpong_smarm(n)
|
||||||
|
});
|
||||||
|
run_n("tokio current_thread", ITERS, bench_pingpong_tokio_current);
|
||||||
|
run_n("tokio multi-thread", ITERS, bench_pingpong_tokio_multi);
|
||||||
|
|
||||||
// ---- Spawn throughput ----
|
// ---- Spawn throughput ----
|
||||||
print_header(&format!("Spawn throughput: {SPAWN_COUNT} actors spawned and joined"));
|
print_header(&format!(
|
||||||
run_n("smarm single-thread", ITERS, || bench_spawn_smarm(1));
|
"Spawn throughput: {SPAWN_COUNT} actors spawned and joined"
|
||||||
|
));
|
||||||
|
run_n("smarm single-thread", ITERS, || bench_spawn_smarm(1));
|
||||||
run_n(&format!("smarm {n}-thread"), ITERS, || bench_spawn_smarm(n));
|
run_n(&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);
|
||||||
run_n("tokio multi-thread", ITERS, bench_spawn_tokio_multi);
|
run_n("tokio multi-thread", ITERS, bench_spawn_tokio_multi);
|
||||||
}
|
}
|
||||||
|
|||||||
+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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+107
-35
@@ -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 {
|
||||||
@@ -84,8 +86,8 @@ fn run_n<F: FnMut() -> (u64, u128)>(name: &str, n: u32, mut f: F) {
|
|||||||
// 5. spawn_storm_busy — workers loaded, then storm of zero-work spawns
|
// 5. spawn_storm_busy — workers loaded, then storm of zero-work spawns
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const STORM_BACKGROUND: u64 = 8; // number of background "busy" actors
|
const STORM_BACKGROUND: u64 = 8; // number of background "busy" actors
|
||||||
const STORM_SPAWN: u64 = 10_000; // zero-work spawns to time
|
const STORM_SPAWN: u64 = 10_000; // zero-work spawns to time
|
||||||
|
|
||||||
fn bench_storm_smarm(threads: usize) -> (u64, u128) {
|
fn bench_storm_smarm(threads: usize) -> (u64, u128) {
|
||||||
let counter = Arc::new(AtomicU64::new(0));
|
let counter = Arc::new(AtomicU64::new(0));
|
||||||
@@ -114,11 +116,15 @@ fn bench_storm_smarm(threads: usize) -> (u64, u128) {
|
|||||||
cc.fetch_add(1, Ordering::Relaxed);
|
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)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,9 @@ fn main() {
|
|||||||
println!("calibration: {per_us} work iters/µs");
|
println!("calibration: {per_us} work iters/µs");
|
||||||
let work_us = move |us: u64| work_iters(us * per_us);
|
let work_us = move |us: u64| work_iters(us * per_us);
|
||||||
|
|
||||||
let cores = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
|
let cores = std::thread::available_parallelism()
|
||||||
|
.map(|n| n.get())
|
||||||
|
.unwrap_or(1);
|
||||||
println!("cores: {cores}");
|
println!("cores: {cores}");
|
||||||
if cores < 4 {
|
if cores < 4 {
|
||||||
println!("probe: SKIPPED (needs the stages in parallel)");
|
println!("probe: SKIPPED (needs the stages in parallel)");
|
||||||
|
|||||||
@@ -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(",")
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -99,7 +99,7 @@ pub extern "C-unwind" fn trampoline() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let outcome = match panic::catch_unwind(panic::AssertUnwindSafe(b)) {
|
let outcome = match panic::catch_unwind(panic::AssertUnwindSafe(b)) {
|
||||||
Ok(()) => Outcome::Exit,
|
Ok(()) => Outcome::Exit,
|
||||||
Err(payload) => {
|
Err(payload) => {
|
||||||
if payload.is::<StopSentinel>() {
|
if payload.is::<StopSentinel>() {
|
||||||
Outcome::Stopped
|
Outcome::Stopped
|
||||||
|
|||||||
+20
-10
@@ -342,8 +342,7 @@ mod inner {
|
|||||||
// Count the loss in would-be delta terms so the audit's columns
|
// Count the loss in would-be delta terms so the audit's columns
|
||||||
// compare directly against `injected_cycles`.
|
// compare directly against `injected_cycles`.
|
||||||
DISCARD_OVERMAX_N.fetch_add(1, Ordering::Relaxed);
|
DISCARD_OVERMAX_N.fetch_add(1, Ordering::Relaxed);
|
||||||
DISCARD_OVERMAX_CYCLES
|
DISCARD_OVERMAX_CYCLES.fetch_add(interval.saturating_mul(pct) / 100, Ordering::Relaxed);
|
||||||
.fetch_add(interval.saturating_mul(pct) / 100, Ordering::Relaxed);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let delta = interval.saturating_mul(pct) / 100;
|
let delta = interval.saturating_mul(pct) / 100;
|
||||||
@@ -419,8 +418,7 @@ mod inner {
|
|||||||
let gap = preempt::rdtsc()
|
let gap = preempt::rdtsc()
|
||||||
.saturating_sub(desched_tsc)
|
.saturating_sub(desched_tsc)
|
||||||
.min(MAX_SAMPLE_CYCLES);
|
.min(MAX_SAMPLE_CYCLES);
|
||||||
OFFCPU_IN_SITE_CYCLES
|
OFFCPU_IN_SITE_CYCLES.fetch_add(gap.saturating_mul(pct) / 100, Ordering::Relaxed);
|
||||||
.fetch_add(gap.saturating_mul(pct) / 100, Ordering::Relaxed);
|
|
||||||
OFFCPU_IN_SITE_N.fetch_add(1, Ordering::Relaxed);
|
OFFCPU_IN_SITE_N.fetch_add(1, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -533,7 +531,9 @@ mod inner {
|
|||||||
park_forgiven_cycles: self
|
park_forgiven_cycles: self
|
||||||
.park_forgiven_cycles
|
.park_forgiven_cycles
|
||||||
.saturating_sub(before.park_forgiven_cycles),
|
.saturating_sub(before.park_forgiven_cycles),
|
||||||
drop_park_cycles: self.drop_park_cycles.saturating_sub(before.drop_park_cycles),
|
drop_park_cycles: self
|
||||||
|
.drop_park_cycles
|
||||||
|
.saturating_sub(before.drop_park_cycles),
|
||||||
drop_park_n: self.drop_park_n.saturating_sub(before.drop_park_n),
|
drop_park_n: self.drop_park_n.saturating_sub(before.drop_park_n),
|
||||||
drop_yield_cycles: self
|
drop_yield_cycles: self
|
||||||
.drop_yield_cycles
|
.drop_yield_cycles
|
||||||
@@ -542,12 +542,18 @@ mod inner {
|
|||||||
discard_overmax_cycles: self
|
discard_overmax_cycles: self
|
||||||
.discard_overmax_cycles
|
.discard_overmax_cycles
|
||||||
.saturating_sub(before.discard_overmax_cycles),
|
.saturating_sub(before.discard_overmax_cycles),
|
||||||
discard_overmax_n: self.discard_overmax_n.saturating_sub(before.discard_overmax_n),
|
discard_overmax_n: self
|
||||||
discard_unarmed_n: self.discard_unarmed_n.saturating_sub(before.discard_unarmed_n),
|
.discard_overmax_n
|
||||||
|
.saturating_sub(before.discard_overmax_n),
|
||||||
|
discard_unarmed_n: self
|
||||||
|
.discard_unarmed_n
|
||||||
|
.saturating_sub(before.discard_unarmed_n),
|
||||||
offcpu_in_site_cycles: self
|
offcpu_in_site_cycles: self
|
||||||
.offcpu_in_site_cycles
|
.offcpu_in_site_cycles
|
||||||
.saturating_sub(before.offcpu_in_site_cycles),
|
.saturating_sub(before.offcpu_in_site_cycles),
|
||||||
offcpu_in_site_n: self.offcpu_in_site_n.saturating_sub(before.offcpu_in_site_n),
|
offcpu_in_site_n: self
|
||||||
|
.offcpu_in_site_n
|
||||||
|
.saturating_sub(before.offcpu_in_site_n),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -795,7 +801,9 @@ mod inner {
|
|||||||
let cell = results
|
let cell = results
|
||||||
.iter()
|
.iter()
|
||||||
.find(|r| r.site == site && r.speedup_pct == speedup_pct)?;
|
.find(|r| r.site == site && r.speedup_pct == speedup_pct)?;
|
||||||
let base = results.iter().find(|r| r.site == site && r.speedup_pct == 0)?;
|
let base = results
|
||||||
|
.iter()
|
||||||
|
.find(|r| r.site == site && r.speedup_pct == 0)?;
|
||||||
let rate = normalized_rate(cell, point)?;
|
let rate = normalized_rate(cell, point)?;
|
||||||
let b = normalized_rate(base, point)?;
|
let b = normalized_rate(base, point)?;
|
||||||
if b <= 0.0 {
|
if b <= 0.0 {
|
||||||
@@ -946,7 +954,9 @@ macro_rules! progress {
|
|||||||
macro_rules! causal_site {
|
macro_rules! causal_site {
|
||||||
($name:literal) => {{
|
($name:literal) => {{
|
||||||
static __SMARM_SITE: ::std::sync::OnceLock<u32> = ::std::sync::OnceLock::new();
|
static __SMARM_SITE: ::std::sync::OnceLock<u32> = ::std::sync::OnceLock::new();
|
||||||
$crate::causal::SiteGuard::enter(*__SMARM_SITE.get_or_init(|| $crate::causal::site_id($name)))
|
$crate::causal::SiteGuard::enter(
|
||||||
|
*__SMARM_SITE.get_or_init(|| $crate::causal::site_id($name)),
|
||||||
|
)
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+51
-27
@@ -104,7 +104,12 @@ pub fn channel<T>() -> (Sender<T>, Receiver<T>) {
|
|||||||
senders: 1,
|
senders: 1,
|
||||||
receiver_alive: true,
|
receiver_alive: true,
|
||||||
}));
|
}));
|
||||||
(Sender { inner: inner.clone() }, Receiver { inner })
|
(
|
||||||
|
Sender {
|
||||||
|
inner: inner.clone(),
|
||||||
|
},
|
||||||
|
Receiver { inner },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Inner<T> {
|
struct Inner<T> {
|
||||||
@@ -178,7 +183,9 @@ impl std::error::Error for RecvTimeoutError {}
|
|||||||
impl<T> Clone for Sender<T> {
|
impl<T> Clone for Sender<T> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
self.inner.lock().senders += 1;
|
self.inner.lock().senders += 1;
|
||||||
Sender { inner: self.inner.clone() }
|
Sender {
|
||||||
|
inner: self.inner.clone(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,10 +255,18 @@ impl<T> Sender<T> {
|
|||||||
g.parked_receiver.take()
|
g.parked_receiver.take()
|
||||||
};
|
};
|
||||||
if let Some((pid, epoch)) = unpark {
|
if let Some((pid, epoch)) = unpark {
|
||||||
crate::te!(crate::trace::Event::Send { sender: crate::actor::current_pid().unwrap_or(crate::pid::Pid::new(u32::MAX, u32::MAX)), receiver: Some(pid) });
|
crate::te!(crate::trace::Event::Send {
|
||||||
|
sender: crate::actor::current_pid()
|
||||||
|
.unwrap_or(crate::pid::Pid::new(u32::MAX, u32::MAX)),
|
||||||
|
receiver: Some(pid)
|
||||||
|
});
|
||||||
crate::scheduler::unpark_at(pid, epoch);
|
crate::scheduler::unpark_at(pid, epoch);
|
||||||
} else {
|
} else {
|
||||||
crate::te!(crate::trace::Event::Send { sender: crate::actor::current_pid().unwrap_or(crate::pid::Pid::new(u32::MAX, u32::MAX)), receiver: None });
|
crate::te!(crate::trace::Event::Send {
|
||||||
|
sender: crate::actor::current_pid()
|
||||||
|
.unwrap_or(crate::pid::Pid::new(u32::MAX, u32::MAX)),
|
||||||
|
receiver: None
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -290,10 +305,12 @@ impl<T> Receiver<T> {
|
|||||||
// Release the lock before parking: the unparker will need it.
|
// Release the lock before parking: the unparker will need it.
|
||||||
crate::scheduler::park_current();
|
crate::scheduler::park_current();
|
||||||
// Woken up. Record it before looping to check the queue.
|
// Woken up. Record it before looping to check the queue.
|
||||||
crate::te!(crate::trace::Event::RecvWake(match crate::actor::current_pid() {
|
crate::te!(crate::trace::Event::RecvWake(
|
||||||
Some(p) => p,
|
match crate::actor::current_pid() {
|
||||||
None => panic!("smarm: RecvWake outside an actor (core corrupt)"),
|
Some(p) => p,
|
||||||
}));
|
None => panic!("smarm: RecvWake outside an actor (core corrupt)"),
|
||||||
|
}
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,10 +364,12 @@ impl<T> Receiver<T> {
|
|||||||
crate::scheduler::insert_wait_timer(deadline, me, target, epoch);
|
crate::scheduler::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(
|
||||||
Some(p) => p,
|
match crate::actor::current_pid() {
|
||||||
None => panic!("smarm: RecvWake outside an actor (core corrupt)"),
|
Some(p) => p,
|
||||||
}));
|
None => panic!("smarm: RecvWake outside an actor (core corrupt)"),
|
||||||
|
}
|
||||||
|
));
|
||||||
let mut g = self.inner.lock();
|
let mut g = self.inner.lock();
|
||||||
if let Some(v) = g.queue.pop_front() {
|
if let Some(v) = g.queue.pop_front() {
|
||||||
crate::preempt::note_message_received();
|
crate::preempt::note_message_received();
|
||||||
@@ -412,10 +431,12 @@ impl<T> Receiver<T> {
|
|||||||
}
|
}
|
||||||
// Release the lock before parking: the unparker will need it.
|
// Release the lock before parking: the unparker will need it.
|
||||||
crate::scheduler::park_current();
|
crate::scheduler::park_current();
|
||||||
crate::te!(crate::trace::Event::RecvWake(match crate::actor::current_pid() {
|
crate::te!(crate::trace::Event::RecvWake(
|
||||||
Some(p) => p,
|
match crate::actor::current_pid() {
|
||||||
None => panic!("smarm: RecvWake outside an actor (core corrupt)"),
|
Some(p) => p,
|
||||||
}));
|
None => panic!("smarm: RecvWake outside an actor (core corrupt)"),
|
||||||
|
}
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,7 +637,12 @@ pub fn try_select(arms: &[&dyn Selectable]) -> std::io::Result<usize> {
|
|||||||
// Channel-only selects skip all of it: `eager` is false, the guard
|
// Channel-only selects skip all of it: `eager` is false, the guard
|
||||||
// is disarmed, and the loser-arm self-cleaning story is unchanged.
|
// is disarmed, and the loser-arm self-cleaning story is unchanged.
|
||||||
let eager = arms.iter().any(|a| a.sel_eager_cleanup());
|
let eager = arms.iter().any(|a| a.sel_eager_cleanup());
|
||||||
let mut guard = UnregisterGuard { arms, me, epoch, armed: eager };
|
let mut guard = UnregisterGuard {
|
||||||
|
arms,
|
||||||
|
me,
|
||||||
|
epoch,
|
||||||
|
armed: eager,
|
||||||
|
};
|
||||||
|
|
||||||
crate::scheduler::park_current();
|
crate::scheduler::park_current();
|
||||||
|
|
||||||
@@ -687,11 +713,7 @@ impl Drop for UnregisterGuard<'_> {
|
|||||||
// unregistered eagerly so none are left dangling. `Err` = an arm failed to
|
// unregistered eagerly so none are left dangling. `Err` = an arm failed to
|
||||||
// register; same unwind (earlier fd arms unregistered, wait retired).
|
// register; same unwind (earlier fd arms unregistered, wait retired).
|
||||||
// `Ok(None)` = every arm registered successfully; the caller parks.
|
// `Ok(None)` = every arm registered successfully; the caller parks.
|
||||||
fn register_arms(
|
fn register_arms(me: Pid, epoch: u32, arms: &[&dyn Selectable]) -> std::io::Result<Option<usize>> {
|
||||||
me: Pid,
|
|
||||||
epoch: u32,
|
|
||||||
arms: &[&dyn Selectable],
|
|
||||||
) -> std::io::Result<Option<usize>> {
|
|
||||||
for (i, arm) in arms.iter().enumerate() {
|
for (i, arm) in arms.iter().enumerate() {
|
||||||
let registered = match arm.sel_register(me, epoch) {
|
let registered = match arm.sel_register(me, epoch) {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
@@ -736,10 +758,7 @@ impl crate::timer::TimerTarget for SelectTimeout {
|
|||||||
/// Panics if `arms` is empty, if called outside an actor, or if an fd arm
|
/// Panics if `arms` is empty, if called outside an actor, or if an fd arm
|
||||||
/// fails to register (see [`try_select_timeout`] for the fallible form; a
|
/// fails to register (see [`try_select_timeout`] for the fallible form; a
|
||||||
/// channel-only select can never fail).
|
/// channel-only select can never fail).
|
||||||
pub fn select_timeout(
|
pub fn select_timeout(arms: &[&dyn Selectable], timeout: std::time::Duration) -> Option<usize> {
|
||||||
arms: &[&dyn Selectable],
|
|
||||||
timeout: std::time::Duration,
|
|
||||||
) -> Option<usize> {
|
|
||||||
match try_select_timeout(arms, timeout) {
|
match try_select_timeout(arms, timeout) {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
Err(e) => panic!(
|
Err(e) => panic!(
|
||||||
@@ -776,7 +795,12 @@ pub fn try_select_timeout(
|
|||||||
// would leave those fds unusable until a kernel event happened to
|
// would leave those fds unusable until a kernel event happened to
|
||||||
// clear them.
|
// clear them.
|
||||||
let eager = arms.iter().any(|a| a.sel_eager_cleanup());
|
let eager = arms.iter().any(|a| a.sel_eager_cleanup());
|
||||||
let mut guard = UnregisterGuard { arms, me, epoch, armed: eager };
|
let mut guard = UnregisterGuard {
|
||||||
|
arms,
|
||||||
|
me,
|
||||||
|
epoch,
|
||||||
|
armed: eager,
|
||||||
|
};
|
||||||
|
|
||||||
crate::scheduler::park_current();
|
crate::scheduler::park_current();
|
||||||
|
|
||||||
|
|||||||
+26
-11
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+78
-34
@@ -178,7 +178,9 @@
|
|||||||
//! from any handler via [`Watcher::watch`]) because monitors are inherently
|
//! from any handler via [`Watcher::watch`]) because monitors are inherently
|
||||||
//! created at runtime. The idle window is set once, in `init`.
|
//! created at runtime. The idle window is set once, in `init`.
|
||||||
|
|
||||||
use crate::channel::{channel, select, select_timeout, Receiver, RecvTimeoutError, Selectable, Sender};
|
use crate::channel::{
|
||||||
|
channel, select, select_timeout, Receiver, RecvTimeoutError, Selectable, Sender,
|
||||||
|
};
|
||||||
use crate::monitor::{demonitor, monitor, Down, Monitor};
|
use crate::monitor::{demonitor, monitor, Down, Monitor};
|
||||||
use crate::pid::Pid;
|
use crate::pid::Pid;
|
||||||
use crate::registry::{register_with, resolve_named_sender, RegisterError};
|
use crate::registry::{register_with, resolve_named_sender, RegisterError};
|
||||||
@@ -273,7 +275,10 @@ pub struct GenServerRef<G: GenServer> {
|
|||||||
|
|
||||||
impl<G: GenServer> Clone for GenServerRef<G> {
|
impl<G: GenServer> Clone for GenServerRef<G> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
GenServerRef { tx: self.tx.clone(), pid: self.pid }
|
GenServerRef {
|
||||||
|
tx: self.tx.clone(),
|
||||||
|
pid: self.pid,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,7 +417,9 @@ impl<G: GenServer> GenServerCtx<G> {
|
|||||||
/// A clonable handle to the loop's monitor intake. Store it in the state
|
/// A clonable handle to the loop's monitor intake. Store it in the state
|
||||||
/// during `init` to watch monitors from later handlers.
|
/// during `init` to watch monitors from later handlers.
|
||||||
pub fn watcher(&self) -> Watcher<G> {
|
pub fn watcher(&self) -> Watcher<G> {
|
||||||
Watcher { tx: self.sys_tx.clone() }
|
Watcher {
|
||||||
|
tx: self.sys_tx.clone(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shorthand for `ctx.watcher().watch(m)` when watching during `init`.
|
/// Shorthand for `ctx.watcher().watch(m)` when watching during `init`.
|
||||||
@@ -426,7 +433,10 @@ impl<G: GenServer> GenServerCtx<G> {
|
|||||||
/// [`tick_every`](TimerHandle::tick_every) /
|
/// [`tick_every`](TimerHandle::tick_every) /
|
||||||
/// [`cancel`](TimerHandle::cancel) from any later handler.
|
/// [`cancel`](TimerHandle::cancel) from any later handler.
|
||||||
pub fn timer(&self) -> TimerHandle<G> {
|
pub fn timer(&self) -> TimerHandle<G> {
|
||||||
TimerHandle { sys_tx: self.sys_tx.clone(), reg: self.reg.clone() }
|
TimerHandle {
|
||||||
|
sys_tx: self.sys_tx.clone(),
|
||||||
|
reg: self.reg.clone(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a quiet-period window: if the loop goes `after` without dispatching
|
/// Set a quiet-period window: if the loop goes `after` without dispatching
|
||||||
@@ -518,7 +528,10 @@ pub struct TimerHandle<G: GenServer> {
|
|||||||
// Manual Clone for the same reason as `Watcher`: no `G: Clone` needed.
|
// Manual Clone for the same reason as `Watcher`: no `G: Clone` needed.
|
||||||
impl<G: GenServer> Clone for TimerHandle<G> {
|
impl<G: GenServer> Clone for TimerHandle<G> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
TimerHandle { sys_tx: self.sys_tx.clone(), reg: self.reg.clone() }
|
TimerHandle {
|
||||||
|
sys_tx: self.sys_tx.clone(),
|
||||||
|
reg: self.reg.clone(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -569,8 +582,18 @@ impl<G: GenServer> TimerHandle<G> {
|
|||||||
// First instance fires after `every`; the payload is produced loop-side
|
// First instance fires after `every`; the payload is produced loop-side
|
||||||
// from `make` on fire, so the tick carries only the stable id.
|
// from `make` on fire, so the tick carries only the stable id.
|
||||||
let sub = send_after_to(every, self.sys_tx.clone(), Sys::Tick(local));
|
let sub = send_after_to(every, self.sys_tx.clone(), Sys::Tick(local));
|
||||||
reg.periodics.insert(local, Periodic { every, live: sub, make });
|
reg.periodics.insert(
|
||||||
debug_assert!(reg.rearm_tx.is_some(), "rearm_tx must be Some while periodics is non-empty");
|
local,
|
||||||
|
Periodic {
|
||||||
|
every,
|
||||||
|
live: sub,
|
||||||
|
make,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
debug_assert!(
|
||||||
|
reg.rearm_tx.is_some(),
|
||||||
|
"rearm_tx must be Some while periodics is non-empty"
|
||||||
|
);
|
||||||
local
|
local
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,7 +640,9 @@ pub struct Watcher<G: GenServer> {
|
|||||||
// regardless of the server type (it clones only the inner sender).
|
// regardless of the server type (it clones only the inner sender).
|
||||||
impl<G: GenServer> Clone for Watcher<G> {
|
impl<G: GenServer> Clone for Watcher<G> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Watcher { tx: self.tx.clone() }
|
Watcher {
|
||||||
|
tx: self.tx.clone(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -691,7 +716,10 @@ impl<G: GenServer> GenServerBuilder<G> {
|
|||||||
/// live server). Consumes the builder, carrying its `with_info` / `under`
|
/// live server). Consumes the builder, carrying its `with_info` / `under`
|
||||||
/// configuration through.
|
/// configuration through.
|
||||||
pub fn named(self, name: GenServerName<G>) -> NamedGenServerBuilder<G> {
|
pub fn named(self, name: GenServerName<G>) -> NamedGenServerBuilder<G> {
|
||||||
NamedGenServerBuilder { builder: self, name: name.as_str() }
|
NamedGenServerBuilder {
|
||||||
|
builder: self,
|
||||||
|
name: name.as_str(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Private shared body behind [`start`](Self::start) and
|
/// Private shared body behind [`start`](Self::start) and
|
||||||
@@ -700,18 +728,24 @@ impl<G: GenServer> GenServerBuilder<G> {
|
|||||||
/// under the name before returning.
|
/// under the name before returning.
|
||||||
fn spawn_server(self) -> GenServerRef<G> {
|
fn spawn_server(self) -> GenServerRef<G> {
|
||||||
let (tx, rx) = channel::<Envelope<G>>();
|
let (tx, rx) = channel::<Envelope<G>>();
|
||||||
let GenServerBuilder { state, infos, supervisor, stack_opts } = self;
|
let GenServerBuilder {
|
||||||
|
state,
|
||||||
|
infos,
|
||||||
|
supervisor,
|
||||||
|
stack_opts,
|
||||||
|
} = self;
|
||||||
let handle = match supervisor {
|
let handle = match supervisor {
|
||||||
Some(sup) => {
|
Some(sup) => crate::scheduler::spawn_under_with(sup, stack_opts, move || {
|
||||||
crate::scheduler::spawn_under_with(sup, stack_opts, move || {
|
|
||||||
server_loop::<G>(rx, state, infos)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
None => crate::scheduler::spawn_with(stack_opts, move || {
|
|
||||||
server_loop::<G>(rx, state, infos)
|
server_loop::<G>(rx, state, infos)
|
||||||
}),
|
}),
|
||||||
|
None => {
|
||||||
|
crate::scheduler::spawn_with(stack_opts, move || server_loop::<G>(rx, state, infos))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
GenServerRef { tx, pid: handle.pid() }
|
GenServerRef {
|
||||||
|
tx,
|
||||||
|
pid: handle.pid(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -739,7 +773,10 @@ impl<G> GenServerName<G> {
|
|||||||
/// associated constants at call sites.
|
/// associated constants at call sites.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub const fn new(name: &'static str) -> Self {
|
pub const fn new(name: &'static str) -> Self {
|
||||||
Self { name, _marker: PhantomData }
|
Self {
|
||||||
|
name,
|
||||||
|
_marker: PhantomData,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The underlying registry key.
|
/// The underlying registry key.
|
||||||
@@ -933,7 +970,11 @@ fn server_loop<G: GenServer>(
|
|||||||
// Bind the ctx so the idle window set during init can be read back, then
|
// Bind the ctx so the idle window set during init can be read back, then
|
||||||
// drop it — that drops the loop's own Sys sender, so a state that cloned no
|
// drop it — that drops the loop's own Sys sender, so a state that cloned no
|
||||||
// Watcher/TimerHandle lets the arm auto-close (the unused-ctx behaviour).
|
// Watcher/TimerHandle lets the arm auto-close (the unused-ctx behaviour).
|
||||||
let ctx = GenServerCtx { sys_tx, reg: reg.clone(), idle: Cell::new(None) };
|
let ctx = GenServerCtx {
|
||||||
|
sys_tx,
|
||||||
|
reg: reg.clone(),
|
||||||
|
idle: Cell::new(None),
|
||||||
|
};
|
||||||
guard.0.init(&ctx);
|
guard.0.init(&ctx);
|
||||||
let idle = ctx.idle.get();
|
let idle = ctx.idle.get();
|
||||||
drop(ctx);
|
drop(ctx);
|
||||||
@@ -983,13 +1024,12 @@ fn server_loop<G: GenServer>(
|
|||||||
// inbox. The slice is rebuilt each iteration because the monitor
|
// inbox. The slice is rebuilt each iteration because the monitor
|
||||||
// and info sets shrink/grow. Mirrors the fast-path inbox park
|
// and info sets shrink/grow. Mirrors the fast-path inbox park
|
||||||
// above; keep them in sync.
|
// above; keep them in sync.
|
||||||
let nd = monitors.len(); // monitor band: [0, nd)
|
let nd = monitors.len(); // monitor band: [0, nd)
|
||||||
let nw = sys_open as usize; // system arm: [nd, nd+nw)
|
let nw = sys_open as usize; // system arm: [nd, nd+nw)
|
||||||
// info band: [nd+nw, nd+nw+ni)
|
// info band: [nd+nw, nd+nw+ni)
|
||||||
// inbox arm: [nd+nw+ni]
|
// inbox arm: [nd+nw+ni]
|
||||||
let sel = {
|
let sel = {
|
||||||
let mut arms: Vec<&dyn Selectable> =
|
let mut arms: Vec<&dyn Selectable> = Vec::with_capacity(nd + nw + infos.len() + 1);
|
||||||
Vec::with_capacity(nd + nw + infos.len() + 1);
|
|
||||||
for m in &monitors {
|
for m in &monitors {
|
||||||
arms.push(&m.rx);
|
arms.push(&m.rx);
|
||||||
}
|
}
|
||||||
@@ -1001,9 +1041,7 @@ fn server_loop<G: GenServer>(
|
|||||||
}
|
}
|
||||||
arms.push(&rx);
|
arms.push(&rx);
|
||||||
match idle_deadline {
|
match idle_deadline {
|
||||||
Some(dl) => {
|
Some(dl) => select_timeout(&arms, dl.saturating_duration_since(Instant::now())),
|
||||||
select_timeout(&arms, dl.saturating_duration_since(Instant::now()))
|
|
||||||
}
|
|
||||||
None => Some(select(&arms)),
|
None => Some(select(&arms)),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1034,8 +1072,12 @@ fn server_loop<G: GenServer>(
|
|||||||
// live set tracks only still-pending timers, then
|
// live set tracks only still-pending timers, then
|
||||||
// dispatch.
|
// dispatch.
|
||||||
match reg.lock() {
|
match reg.lock() {
|
||||||
Ok(mut g) => { g.oneshots.remove(&id); }
|
Ok(mut g) => {
|
||||||
Err(e) => panic!("smarm: gen_server reg lock poisoned (core corrupt): {e}"),
|
g.oneshots.remove(&id);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
panic!("smarm: gen_server reg lock poisoned (core corrupt): {e}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
guard.0.handle_timer(msg);
|
guard.0.handle_timer(msg);
|
||||||
reset_idle(&mut idle_deadline);
|
reset_idle(&mut idle_deadline);
|
||||||
@@ -1049,7 +1091,9 @@ fn server_loop<G: GenServer>(
|
|||||||
let msg = {
|
let msg = {
|
||||||
let mut g = match reg.lock() {
|
let mut g = match reg.lock() {
|
||||||
Ok(g) => g,
|
Ok(g) => g,
|
||||||
Err(e) => panic!("smarm: gen_server reg lock poisoned (core corrupt): {e}"),
|
Err(e) => panic!(
|
||||||
|
"smarm: gen_server reg lock poisoned (core corrupt): {e}"
|
||||||
|
),
|
||||||
};
|
};
|
||||||
let r = &mut *g;
|
let r = &mut *g;
|
||||||
if let Some(p) = r.periodics.get_mut(&id) {
|
if let Some(p) = r.periodics.get_mut(&id) {
|
||||||
@@ -1057,9 +1101,9 @@ fn server_loop<G: GenServer>(
|
|||||||
let msg = (p.make)();
|
let msg = (p.make)();
|
||||||
let tx = match r.rearm_tx.as_ref() {
|
let tx = match r.rearm_tx.as_ref() {
|
||||||
Some(tx) => tx.clone(),
|
Some(tx) => tx.clone(),
|
||||||
None => panic!(
|
None => {
|
||||||
"smarm: live periodic without rearm_tx (logic bug)"
|
panic!("smarm: live periodic without rearm_tx (logic bug)")
|
||||||
),
|
}
|
||||||
};
|
};
|
||||||
p.live = send_after_to(every, tx, Sys::Tick(id));
|
p.live = send_after_to(every, tx, Sys::Tick(id));
|
||||||
Some(msg)
|
Some(msg)
|
||||||
|
|||||||
+25
-10
@@ -219,7 +219,11 @@ struct Timers {
|
|||||||
|
|
||||||
impl Timers {
|
impl Timers {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Timers { next_local: 0, state: None, named: HashMap::new() }
|
Timers {
|
||||||
|
next_local: 0,
|
||||||
|
state: None,
|
||||||
|
named: HashMap::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mint(&mut self) -> u64 {
|
fn mint(&mut self) -> u64 {
|
||||||
@@ -248,7 +252,11 @@ pub struct Cx<Ev> {
|
|||||||
|
|
||||||
impl<Ev> Cx<Ev> {
|
impl<Ev> Cx<Ev> {
|
||||||
fn new(sys_tx: Sender<Sys>, reg: Arc<Mutex<Timers>>) -> Self {
|
fn new(sys_tx: Sender<Sys>, reg: Arc<Mutex<Timers>>) -> Self {
|
||||||
Cx { sys_tx, reg, _ev: PhantomData }
|
Cx {
|
||||||
|
sys_tx,
|
||||||
|
reg,
|
||||||
|
_ev: PhantomData,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Arm the **state timeout**: fire a `state_timeout` event after `after` in
|
/// Arm the **state timeout**: fire a `state_timeout` event after `after` in
|
||||||
@@ -387,7 +395,10 @@ pub struct GenStatemRef<M: Machine> {
|
|||||||
|
|
||||||
impl<M: Machine> Clone for GenStatemRef<M> {
|
impl<M: Machine> Clone for GenStatemRef<M> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
GenStatemRef { tx: self.tx.clone(), pid: self.pid }
|
GenStatemRef {
|
||||||
|
tx: self.tx.clone(),
|
||||||
|
pid: self.pid,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,13 +454,13 @@ pub fn spawn<M: Machine>(machine: M) -> GenStatemRef<M> {
|
|||||||
/// a `_with` variant like the scheduler's own spawns.
|
/// a `_with` variant like the scheduler's own spawns.
|
||||||
///
|
///
|
||||||
/// Panics if called outside `Runtime::run()`.
|
/// Panics if called outside `Runtime::run()`.
|
||||||
pub fn spawn_with<M: Machine>(
|
pub fn spawn_with<M: Machine>(opts: crate::scheduler::SpawnOpts, machine: M) -> GenStatemRef<M> {
|
||||||
opts: crate::scheduler::SpawnOpts,
|
|
||||||
machine: M,
|
|
||||||
) -> GenStatemRef<M> {
|
|
||||||
let (tx, rx) = channel::<M::Ev>();
|
let (tx, rx) = channel::<M::Ev>();
|
||||||
let handle = crate::scheduler::spawn_with(opts, move || statem_loop(rx, machine));
|
let handle = crate::scheduler::spawn_with(opts, move || statem_loop(rx, machine));
|
||||||
GenStatemRef { tx, pid: handle.pid() }
|
GenStatemRef {
|
||||||
|
tx,
|
||||||
|
pid: handle.pid(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The machine actor body: `on_start`, then one `handle` per event until the
|
/// The machine actor body: `on_start`, then one `handle` per event until the
|
||||||
@@ -488,7 +499,9 @@ fn statem_loop<M: Machine>(rx: Receiver<M::Ev>, mut machine: M) {
|
|||||||
Sys::StateTimeout(local) => {
|
Sys::StateTimeout(local) => {
|
||||||
let mut t = match reg.lock() {
|
let mut t = match reg.lock() {
|
||||||
Ok(g) => g,
|
Ok(g) => g,
|
||||||
Err(e) => panic!("smarm: gen_statem reg lock poisoned (core corrupt): {e}"),
|
Err(e) => panic!(
|
||||||
|
"smarm: gen_statem reg lock poisoned (core corrupt): {e}"
|
||||||
|
),
|
||||||
};
|
};
|
||||||
match t.state {
|
match t.state {
|
||||||
Some((live, _)) if live == local => {
|
Some((live, _)) if live == local => {
|
||||||
@@ -501,7 +514,9 @@ fn statem_loop<M: Machine>(rx: Receiver<M::Ev>, mut machine: M) {
|
|||||||
Sys::Timeout(name, local) => {
|
Sys::Timeout(name, local) => {
|
||||||
let mut t = match reg.lock() {
|
let mut t = match reg.lock() {
|
||||||
Ok(g) => g,
|
Ok(g) => g,
|
||||||
Err(e) => panic!("smarm: gen_statem reg lock poisoned (core corrupt): {e}"),
|
Err(e) => panic!(
|
||||||
|
"smarm: gen_statem reg lock poisoned (core corrupt): {e}"
|
||||||
|
),
|
||||||
};
|
};
|
||||||
match t.named.get(name) {
|
match t.named.get(name) {
|
||||||
Some(&(live, _)) if live == local => {
|
Some(&(live, _)) if live == local => {
|
||||||
|
|||||||
+17
-4
@@ -239,7 +239,10 @@ pub fn snapshot() -> RuntimeSnapshot {
|
|||||||
actors.push(info);
|
actors.push(info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RuntimeSnapshot { format_version: SNAPSHOT_FORMAT_VERSION, actors }
|
RuntimeSnapshot {
|
||||||
|
format_version: SNAPSHOT_FORMAT_VERSION,
|
||||||
|
actors,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,7 +390,10 @@ pub fn tree() -> RuntimeTree {
|
|||||||
/// want to inspect again) and want the tree view of it without re-reading
|
/// want to inspect again) and want the tree view of it without re-reading
|
||||||
/// the runtime.
|
/// the runtime.
|
||||||
pub fn tree_from(snap: RuntimeSnapshot) -> RuntimeTree {
|
pub fn tree_from(snap: RuntimeSnapshot) -> RuntimeTree {
|
||||||
let RuntimeSnapshot { format_version, actors } = snap;
|
let RuntimeSnapshot {
|
||||||
|
format_version,
|
||||||
|
actors,
|
||||||
|
} = snap;
|
||||||
|
|
||||||
let mut index_of: HashMap<Pid, usize> = HashMap::with_capacity(actors.len());
|
let mut index_of: HashMap<Pid, usize> = HashMap::with_capacity(actors.len());
|
||||||
for (i, a) in actors.iter().enumerate() {
|
for (i, a) in actors.iter().enumerate() {
|
||||||
@@ -418,7 +424,10 @@ pub fn tree_from(snap: RuntimeSnapshot) -> RuntimeTree {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|i| build_node(i, &children_of, &orphaned, &mut slots))
|
.filter_map(|i| build_node(i, &children_of, &orphaned, &mut slots))
|
||||||
.collect();
|
.collect();
|
||||||
RuntimeTree { format_version, roots: root_nodes }
|
RuntimeTree {
|
||||||
|
format_version,
|
||||||
|
roots: root_nodes,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_node(
|
fn build_node(
|
||||||
@@ -436,5 +445,9 @@ fn build_node(
|
|||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
Some(TreeNode { info, orphaned: orphaned[i], children })
|
Some(TreeNode {
|
||||||
|
info,
|
||||||
|
orphaned: orphaned[i],
|
||||||
|
children,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,7 +136,6 @@ pub struct IoThread {
|
|||||||
waiters: Waiters,
|
waiters: Waiters,
|
||||||
|
|
||||||
// ----- Epoll machinery -----
|
// ----- Epoll machinery -----
|
||||||
|
|
||||||
/// The epollfd, owned by `IoThread`. Callable cross-thread via
|
/// The epollfd, owned by `IoThread`. Callable cross-thread via
|
||||||
/// `epoll_ctl` per the man page.
|
/// `epoll_ctl` per the man page.
|
||||||
epollfd: RawFd,
|
epollfd: RawFd,
|
||||||
@@ -147,7 +146,6 @@ pub struct IoThread {
|
|||||||
shutdown_write: RawFd,
|
shutdown_write: RawFd,
|
||||||
|
|
||||||
// ----- Threads -----
|
// ----- Threads -----
|
||||||
|
|
||||||
pool_thread: Option<OsJoinHandle<()>>,
|
pool_thread: Option<OsJoinHandle<()>>,
|
||||||
epoll_thread: Option<OsJoinHandle<()>>,
|
epoll_thread: Option<OsJoinHandle<()>>,
|
||||||
}
|
}
|
||||||
@@ -284,9 +282,8 @@ impl IoThread {
|
|||||||
events,
|
events,
|
||||||
u64: fd as u64,
|
u64: fd as u64,
|
||||||
};
|
};
|
||||||
let r = unsafe {
|
let r =
|
||||||
libc::epoll_ctl(self.epollfd, libc::EPOLL_CTL_ADD, fd, &mut ev as *mut _)
|
unsafe { libc::epoll_ctl(self.epollfd, libc::EPOLL_CTL_ADD, fd, &mut ev as *mut _) };
|
||||||
};
|
|
||||||
if r < 0 {
|
if r < 0 {
|
||||||
return Err(io::Error::last_os_error());
|
return Err(io::Error::last_os_error());
|
||||||
}
|
}
|
||||||
@@ -398,12 +395,7 @@ fn epoll_loop(epollfd: RawFd, waiters: Waiters, rt: Weak<RuntimeInner>) {
|
|||||||
|
|
||||||
loop {
|
loop {
|
||||||
let n = unsafe {
|
let n = unsafe {
|
||||||
libc::epoll_wait(
|
libc::epoll_wait(epollfd, events.as_mut_ptr(), MAX_EVENTS as libc::c_int, -1)
|
||||||
epollfd,
|
|
||||||
events.as_mut_ptr(),
|
|
||||||
MAX_EVENTS as libc::c_int,
|
|
||||||
-1,
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if n < 0 {
|
if n < 0 {
|
||||||
@@ -438,12 +430,7 @@ fn epoll_loop(epollfd: RawFd, waiters: Waiters, rt: Weak<RuntimeInner>) {
|
|||||||
let entry = w.remove(&fd);
|
let entry = w.remove(&fd);
|
||||||
if entry.is_some() {
|
if entry.is_some() {
|
||||||
unsafe {
|
unsafe {
|
||||||
libc::epoll_ctl(
|
libc::epoll_ctl(epollfd, libc::EPOLL_CTL_DEL, fd, std::ptr::null_mut());
|
||||||
epollfd,
|
|
||||||
libc::EPOLL_CTL_DEL,
|
|
||||||
fd,
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entry
|
entry
|
||||||
|
|||||||
+38
-33
@@ -11,36 +11,36 @@
|
|||||||
//!
|
//!
|
||||||
//! See `LOOM.md` for the design intent and the deferred-for-later list.
|
//! See `LOOM.md` for the design intent and the deferred-for-later list.
|
||||||
|
|
||||||
pub mod stack;
|
|
||||||
pub(crate) mod signal;
|
|
||||||
pub mod context;
|
|
||||||
pub mod preempt;
|
|
||||||
pub mod pid;
|
|
||||||
pub mod actor;
|
pub mod actor;
|
||||||
|
pub mod causal;
|
||||||
pub mod channel;
|
pub mod channel;
|
||||||
pub mod scheduler;
|
pub mod context;
|
||||||
pub mod supervisor;
|
|
||||||
pub mod timer;
|
|
||||||
pub mod io;
|
|
||||||
pub mod mutex;
|
|
||||||
pub mod monitor;
|
|
||||||
pub mod registry;
|
|
||||||
pub mod pg;
|
|
||||||
pub mod link;
|
|
||||||
pub mod gen_server;
|
pub mod gen_server;
|
||||||
pub mod gen_statem;
|
pub mod gen_statem;
|
||||||
pub mod introspect;
|
pub mod introspect;
|
||||||
|
pub mod io;
|
||||||
|
pub mod link;
|
||||||
|
pub mod monitor;
|
||||||
|
pub mod mutex;
|
||||||
#[cfg(feature = "observer")]
|
#[cfg(feature = "observer")]
|
||||||
pub mod observer;
|
pub mod observer;
|
||||||
pub mod runtime;
|
|
||||||
pub(crate) mod park;
|
pub(crate) mod park;
|
||||||
|
pub mod pg;
|
||||||
|
pub mod pid;
|
||||||
|
pub mod preempt;
|
||||||
pub(crate) mod raw_mutex;
|
pub(crate) mod raw_mutex;
|
||||||
pub(crate) mod slot_state;
|
pub mod registry;
|
||||||
pub(crate) mod sync_shim;
|
|
||||||
#[doc(hidden)] // pub only so benches/rq_micro.rs can drive the raw structures
|
#[doc(hidden)] // pub only so benches/rq_micro.rs can drive the raw structures
|
||||||
pub mod run_queue;
|
pub mod run_queue;
|
||||||
|
pub mod runtime;
|
||||||
|
pub mod scheduler;
|
||||||
|
pub(crate) mod signal;
|
||||||
|
pub(crate) mod slot_state;
|
||||||
|
pub mod stack;
|
||||||
|
pub mod supervisor;
|
||||||
|
pub(crate) mod sync_shim;
|
||||||
|
pub mod timer;
|
||||||
pub mod trace;
|
pub mod trace;
|
||||||
pub mod causal;
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Global allocator
|
// Global allocator
|
||||||
@@ -59,34 +59,39 @@ pub use channel::{
|
|||||||
};
|
};
|
||||||
pub use gen_server::{
|
pub use gen_server::{
|
||||||
call, cast, shutdown, whereis_server, CallError, CallTimeoutError, CastError, GenServer,
|
call, cast, shutdown, whereis_server, CallError, CallTimeoutError, CastError, GenServer,
|
||||||
NamedGenServerBuilder, GenServerBuilder, GenServerCtx, GenServerName, GenServerRef, TimerHandle, Watcher,
|
GenServerBuilder, GenServerCtx, GenServerName, GenServerRef, NamedGenServerBuilder,
|
||||||
|
TimerHandle, Watcher,
|
||||||
};
|
};
|
||||||
pub use gen_statem::{
|
pub use gen_statem::{
|
||||||
CallError as GenStatemCallError, Cx, Machine, Reply, Resolution, SendError as GenStatemSendError,
|
CallError as GenStatemCallError, Cx, GenStatemRef, Machine, Reply, Resolution,
|
||||||
GenStatemRef,
|
SendError as GenStatemSendError,
|
||||||
};
|
};
|
||||||
pub use introspect::{StackInfo,
|
pub use introspect::{
|
||||||
actor_info, snapshot, tree, tree_from, ActorInfo, ActorState, RuntimeSnapshot, RuntimeTree,
|
actor_info, snapshot, tree, tree_from, ActorInfo, ActorState, RuntimeSnapshot, RuntimeTree,
|
||||||
TreeNode, SNAPSHOT_FORMAT_VERSION,
|
StackInfo, TreeNode, SNAPSHOT_FORMAT_VERSION,
|
||||||
};
|
};
|
||||||
|
pub use link::{link, trap_exit, unlink, ExitSignal};
|
||||||
|
pub use monitor::{
|
||||||
|
demonitor, mark_watchable, monitor, terminal_reason, Down, DownReason, Monitor, MonitorId,
|
||||||
|
};
|
||||||
|
pub use mutex::{LockTimeout, Mutex, MutexGuard};
|
||||||
#[cfg(feature = "observer")]
|
#[cfg(feature = "observer")]
|
||||||
pub use observer::{ObserverReply, ObserverRequest};
|
pub use observer::{ObserverReply, ObserverRequest};
|
||||||
pub use link::{link, trap_exit, unlink, ExitSignal};
|
pub use pg::{
|
||||||
pub use monitor::{demonitor, monitor, Down, DownReason, Monitor, MonitorId};
|
dispatch, join, leave, members, members_as, pick, pick_as, Incarnation, Member, NodeId,
|
||||||
pub use mutex::{LockTimeout, Mutex, MutexGuard};
|
};
|
||||||
pub use pid::{Addressable, Erased, Name, Pid, RawPid};
|
pub use pid::{Addressable, Erased, Name, Pid, RawPid};
|
||||||
pub use pg::{dispatch, join, leave, members, members_as, pick, pick_as, Incarnation, Member, NodeId};
|
|
||||||
pub use registry::{
|
pub use registry::{
|
||||||
install, lookup_as, register, send, send_dyn, send_to, unregister, whereis, RegisterError,
|
install, lookup_as, register, resolve_name, send, send_dyn, send_to, unregister, whereis,
|
||||||
SendError,
|
NameResolution, RegisterError, SendError,
|
||||||
};
|
};
|
||||||
pub use runtime::{init, Config, Runtime};
|
pub use runtime::{init, Config, Runtime};
|
||||||
pub use scheduler::{
|
pub use scheduler::{
|
||||||
block_on_io, cancel_timer, request_stop, run, self_pid, send_after, send_after_named,
|
block_on_io, cancel_timer, request_stop, run, self_pid, send_after, send_after_named,
|
||||||
send_after_named_wall, send_after_wall, sleep, sleep_wall,
|
send_after_named_wall, send_after_wall, sleep, sleep_wall, spawn, spawn_addr, spawn_addr_with,
|
||||||
spawn, spawn_addr, spawn_addr_with, spawn_under, spawn_under_with, spawn_with,
|
spawn_under, spawn_under_with, spawn_with, try_spawn, try_spawn_under_with, wait_readable,
|
||||||
wait_readable, wait_readable_timeout, wait_writable,
|
wait_readable_timeout, wait_writable, wait_writable_timeout, yield_now, FdArm, JoinError,
|
||||||
wait_writable_timeout, yield_now, FdArm, JoinError, JoinHandle, SpawnOpts,
|
JoinHandle, SpawnError, SpawnOpts,
|
||||||
};
|
};
|
||||||
pub use supervisor::{ChildSpec, OneForOne, Restart, Signal, Strategy};
|
pub use supervisor::{ChildSpec, OneForOne, Restart, Signal, Strategy};
|
||||||
pub use timer::TimerId;
|
pub use timer::TimerId;
|
||||||
|
|||||||
+4
-1
@@ -157,7 +157,10 @@ pub fn link<A>(target: Pid<A>) {
|
|||||||
});
|
});
|
||||||
match my_trap {
|
match my_trap {
|
||||||
Some(tx) => {
|
Some(tx) => {
|
||||||
let _ = tx.send(ExitSignal { from: target, reason: DownReason::NoProc });
|
let _ = tx.send(ExitSignal {
|
||||||
|
from: target,
|
||||||
|
reason: DownReason::NoProc,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
None => request_stop(me),
|
None => request_stop(me),
|
||||||
}
|
}
|
||||||
|
|||||||
+62
-1
@@ -171,12 +171,73 @@ pub fn monitor<A>(target: Pid<A>) -> Monitor {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if !registered {
|
if !registered {
|
||||||
let _ = tx.send(Down { pid: target, reason: DownReason::NoProc });
|
let _ = tx.send(Down {
|
||||||
|
pid: target,
|
||||||
|
reason: DownReason::NoProc,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Monitor { id, target, rx }
|
Monitor { id, target, rx }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Flag `target`'s tenancy as watchable: its death will stamp the slot's
|
||||||
|
/// terminal record (see [`terminal_reason`]), exactly as registering a name
|
||||||
|
/// does. The bridge calls this wherever a smarm pid is *encoded across the
|
||||||
|
/// boundary* — a contract reply, an introspection listing — because BEAM can
|
||||||
|
/// only watch pids it holds, and can only hold pids that crossed. Keeping the
|
||||||
|
/// bit rare is what keeps the record alive: anonymous never-exported churn
|
||||||
|
/// (holder threads, egress tasks) stays ineligible and cannot evict a
|
||||||
|
/// watchable tenancy's record from a LIFO-recycled slot.
|
||||||
|
///
|
||||||
|
/// Generation-checked and live-screened: marking a pid whose tenancy already
|
||||||
|
/// ended is a no-op — its record either exists (it was flagged before dying)
|
||||||
|
/// or is honestly unknowable. Same `Runtime::run()` context contract as
|
||||||
|
/// [`monitor`].
|
||||||
|
pub fn mark_watchable<A>(target: Pid<A>) {
|
||||||
|
let target = target.erase();
|
||||||
|
with_runtime(|inner| {
|
||||||
|
if let Some(slot) = inner.slot_at(target) {
|
||||||
|
// Cold lock FIRST: finalize publishes Done and checks the
|
||||||
|
// watchable bit under this same lock, so the mark either lands
|
||||||
|
// before finalize reads it (the death stamps) or observes the
|
||||||
|
// tenancy already dead (no-op). No lost-stamp window between an
|
||||||
|
// unlocked liveness read and the flag set.
|
||||||
|
let mut cold = slot.cold.lock();
|
||||||
|
if slot.is_live_for(target) {
|
||||||
|
cold.watchable = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The terminal [`DownReason`] of the tenancy `target` names, if that tenancy
|
||||||
|
/// ever registered a name and is the *most recent named* death of its slot:
|
||||||
|
/// finalize stamps the slot with `(generation, reason)` for once-registered
|
||||||
|
/// tenancies (anonymous green-thread churn does not stamp — nor evict), and
|
||||||
|
/// the record survives reclaim and the next tenant's install, until the next
|
||||||
|
/// *named* tenant of the slot itself dies. `None` means the pid never lived,
|
||||||
|
/// is still alive, never held a name, or its record was overwritten by a
|
||||||
|
/// later named tenancy's death — callers fall back to `NoProc` semantics.
|
||||||
|
///
|
||||||
|
/// This exists for watch-installers that raced their target's death (bridge
|
||||||
|
/// soak signature 4): a `NoProc` observed at install time can be upgraded to
|
||||||
|
/// the real reason while the record still matches, which is exactly what an
|
||||||
|
/// install that had won the race would have delivered. It does NOT change
|
||||||
|
/// [`monitor`]'s own semantics — monitoring a stale pid still queues `NoProc`,
|
||||||
|
/// the same shape Erlang gives — the upgrade is the caller's deliberate act.
|
||||||
|
/// Same context contract as [`monitor`]: must run inside `Runtime::run()`.
|
||||||
|
pub fn terminal_reason<A>(target: Pid<A>) -> Option<DownReason> {
|
||||||
|
let target = target.erase();
|
||||||
|
with_runtime(|inner| {
|
||||||
|
let slot = inner.slot_at(target)?;
|
||||||
|
let cold = slot.cold.lock();
|
||||||
|
match cold.terminal {
|
||||||
|
Some((generation, reason)) if generation == target.generation() => Some(reason),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Cancel the monitor `m`. Returns `Some(id)` if a live registration was found
|
/// Cancel the monitor `m`. Returns `Some(id)` if a live registration was found
|
||||||
/// and removed, so no `Down` will arrive on `m.rx` from here on. Returns
|
/// and removed, so no `Down` will arrive on `m.rx` from here on. Returns
|
||||||
/// `None` if there was nothing left to remove: the target had already gone
|
/// `None` if there was nothing left to remove: the target had already gone
|
||||||
|
|||||||
+29
-10
@@ -158,7 +158,11 @@ impl TimerTarget for MutexCore {
|
|||||||
if st.holder == Some(pid) {
|
if st.holder == Some(pid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match st.waiters.iter().position(|w| w.pid == pid && w.epoch == epoch) {
|
match st
|
||||||
|
.waiters
|
||||||
|
.iter()
|
||||||
|
.position(|w| w.pid == pid && w.epoch == epoch)
|
||||||
|
{
|
||||||
Some(pos) => {
|
Some(pos) => {
|
||||||
st.waiters.remove(pos);
|
st.waiters.remove(pos);
|
||||||
true
|
true
|
||||||
@@ -246,7 +250,10 @@ impl<T> Mutex<T> {
|
|||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => panic!("smarm: Mutex value missing on free fast path (core corrupt)"),
|
None => panic!("smarm: Mutex value missing on free fast path (core corrupt)"),
|
||||||
};
|
};
|
||||||
return Ok(MutexGuard { mutex: self, value: Some(value) });
|
return Ok(MutexGuard {
|
||||||
|
mutex: self,
|
||||||
|
value: Some(value),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,7 +294,10 @@ impl<T> Mutex<T> {
|
|||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => panic!("smarm: Mutex value missing after grant (core corrupt)"),
|
None => panic!("smarm: Mutex value missing after grant (core corrupt)"),
|
||||||
};
|
};
|
||||||
Ok(MutexGuard { mutex: self, value: Some(value) })
|
Ok(MutexGuard {
|
||||||
|
mutex: self,
|
||||||
|
value: Some(value),
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(LockTimeout)
|
Err(LockTimeout)
|
||||||
}
|
}
|
||||||
@@ -315,7 +325,10 @@ impl<T> Mutex<T> {
|
|||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => panic!("smarm: Mutex value missing on try_lock free path (core corrupt)"),
|
None => panic!("smarm: Mutex value missing on try_lock free path (core corrupt)"),
|
||||||
};
|
};
|
||||||
Some(MutexGuard { mutex: self, value: Some(value) })
|
Some(MutexGuard {
|
||||||
|
mutex: self,
|
||||||
|
value: Some(value),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Blocking fallback used when called outside the smarm runtime.
|
/// Blocking fallback used when called outside the smarm runtime.
|
||||||
@@ -329,10 +342,15 @@ impl<T> Mutex<T> {
|
|||||||
Ok(mut g) => g.take(),
|
Ok(mut g) => g.take(),
|
||||||
Err(e) => panic!("smarm: mutex value lock poisoned (core corrupt): {e}"),
|
Err(e) => panic!("smarm: mutex value lock poisoned (core corrupt): {e}"),
|
||||||
};
|
};
|
||||||
if let Some(v) = v { break v; }
|
if let Some(v) = v {
|
||||||
|
break v;
|
||||||
|
}
|
||||||
std::thread::yield_now();
|
std::thread::yield_now();
|
||||||
};
|
};
|
||||||
Ok(MutexGuard { mutex: self, value: Some(value) })
|
Ok(MutexGuard {
|
||||||
|
mutex: self,
|
||||||
|
value: Some(value),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,7 +360,10 @@ impl<T> Clone for Mutex<T> {
|
|||||||
/// lock and one protected value; locking through any clone excludes
|
/// lock and one protected value; locking through any clone excludes
|
||||||
/// every other clone.
|
/// every other clone.
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self { core: self.core.clone(), value: self.value.clone() }
|
Self {
|
||||||
|
core: self.core.clone(),
|
||||||
|
value: self.value.clone(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,9 +409,7 @@ impl<T: std::fmt::Debug> std::fmt::Debug for MutexGuard<'_, T> {
|
|||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => panic!("smarm: MutexGuard value missing (core corrupt)"),
|
None => panic!("smarm: MutexGuard value missing (core corrupt)"),
|
||||||
};
|
};
|
||||||
f.debug_tuple("MutexGuard")
|
f.debug_tuple("MutexGuard").field(value).finish()
|
||||||
.field(value)
|
|
||||||
.finish()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+38
-15
@@ -105,7 +105,9 @@ mod parker {
|
|||||||
|
|
||||||
impl Parker {
|
impl Parker {
|
||||||
pub(super) fn new() -> Self {
|
pub(super) fn new() -> Self {
|
||||||
Self { state: AtomicU32::new(EMPTY) }
|
Self {
|
||||||
|
state: AtomicU32::new(EMPTY),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` = woken (permit consumed), `false` = timed out.
|
/// Returns `true` = woken (permit consumed), `false` = timed out.
|
||||||
@@ -211,7 +213,10 @@ mod parker {
|
|||||||
|
|
||||||
impl Parker {
|
impl Parker {
|
||||||
pub(super) fn new() -> Self {
|
pub(super) fn new() -> Self {
|
||||||
Self { permit: Mutex::new(false), cv: Condvar::new() }
|
Self {
|
||||||
|
permit: Mutex::new(false),
|
||||||
|
cv: Condvar::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` = woken (permit consumed), `false` = timed out.
|
/// Returns `true` = woken (permit consumed), `false` = timed out.
|
||||||
@@ -395,12 +400,10 @@ impl Coordinator {
|
|||||||
// The CAS is the exactly-one guarantee: whoever clears the bit
|
// The CAS is the exactly-one guarantee: whoever clears the bit
|
||||||
// owns the wake; a racing wake_one retries on the observed value
|
// owns the wake; a racing wake_one retries on the observed value
|
||||||
// (coherence: a failed CAS can never read older than `mask`).
|
// (coherence: a failed CAS can never read older than `mask`).
|
||||||
match self.idle.compare_exchange(
|
match self
|
||||||
mask,
|
.idle
|
||||||
mask & !bit,
|
.compare_exchange(mask, mask & !bit, Ordering::AcqRel, Ordering::Acquire)
|
||||||
Ordering::AcqRel,
|
{
|
||||||
Ordering::Acquire,
|
|
||||||
) {
|
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.parkers[id].unpark();
|
self.parkers[id].unpark();
|
||||||
return true;
|
return true;
|
||||||
@@ -467,7 +470,8 @@ impl Coordinator {
|
|||||||
// with the deadline still NO_DEADLINE compares `new < MAX` = true
|
// with the deadline still NO_DEADLINE compares `new < MAX` = true
|
||||||
// and over-wakes — the benign direction. (Under the mandated timer
|
// and over-wakes — the benign direction. (Under the mandated timer
|
||||||
// serialization this interleaving cannot occur anyway.)
|
// serialization this interleaving cannot occur anyway.)
|
||||||
self.tk_armed.store(self.deadline_nanos(deadline), Ordering::SeqCst);
|
self.tk_armed
|
||||||
|
.store(self.deadline_nanos(deadline), Ordering::SeqCst);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,7 +582,10 @@ mod tests {
|
|||||||
let t0 = Instant::now();
|
let t0 = Instant::now();
|
||||||
let r = c.park(0, None, || false);
|
let r = c.park(0, None, || false);
|
||||||
assert_eq!(r, ParkResult::Woken);
|
assert_eq!(r, ParkResult::Woken);
|
||||||
assert!(t0.elapsed() < Duration::from_millis(100), "park blocked despite permit");
|
assert!(
|
||||||
|
t0.elapsed() < Duration::from_millis(100),
|
||||||
|
"park blocked despite permit"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -632,14 +639,20 @@ mod tests {
|
|||||||
// Wait until all four are published idle.
|
// Wait until all four are published idle.
|
||||||
let t0 = Instant::now();
|
let t0 = Instant::now();
|
||||||
while c.idle_mask().count_ones() != N as u32 {
|
while c.idle_mask().count_ones() != N as u32 {
|
||||||
assert!(t0.elapsed() < Duration::from_secs(5), "threads never parked");
|
assert!(
|
||||||
|
t0.elapsed() < Duration::from_secs(5),
|
||||||
|
"threads never parked"
|
||||||
|
);
|
||||||
std::thread::yield_now();
|
std::thread::yield_now();
|
||||||
}
|
}
|
||||||
assert!(c.wake_one());
|
assert!(c.wake_one());
|
||||||
// Exactly one wakes; give the others a beat to (incorrectly) wake.
|
// Exactly one wakes; give the others a beat to (incorrectly) wake.
|
||||||
let t0 = Instant::now();
|
let t0 = Instant::now();
|
||||||
while woken.load(O::SeqCst) == 0 {
|
while woken.load(O::SeqCst) == 0 {
|
||||||
assert!(t0.elapsed() < Duration::from_secs(5), "wake_one woke nobody");
|
assert!(
|
||||||
|
t0.elapsed() < Duration::from_secs(5),
|
||||||
|
"wake_one woke nobody"
|
||||||
|
);
|
||||||
std::thread::yield_now();
|
std::thread::yield_now();
|
||||||
}
|
}
|
||||||
std::thread::sleep(Duration::from_millis(100));
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
@@ -706,7 +719,10 @@ mod tests {
|
|||||||
assert_eq!(c.armed_deadline_nanos(), c.deadline_nanos(d2));
|
assert_eq!(c.armed_deadline_nanos(), c.deadline_nanos(d2));
|
||||||
c.disarm_timer(0);
|
c.disarm_timer(0);
|
||||||
assert_eq!(c.armed_deadline_nanos(), NO_DEADLINE);
|
assert_eq!(c.armed_deadline_nanos(), NO_DEADLINE);
|
||||||
assert!(c.try_arm_timer(1, d1), "role must be re-takeable after disarm");
|
assert!(
|
||||||
|
c.try_arm_timer(1, d1),
|
||||||
|
"role must be re-takeable after disarm"
|
||||||
|
);
|
||||||
c.disarm_timer(1);
|
c.disarm_timer(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -721,7 +737,10 @@ mod tests {
|
|||||||
let t0 = Instant::now();
|
let t0 = Instant::now();
|
||||||
let r = c.park(0, Some(far), || false);
|
let r = c.park(0, Some(far), || false);
|
||||||
assert_eq!(r, ParkResult::Woken, "re-arm wake lost");
|
assert_eq!(r, ParkResult::Woken, "re-arm wake lost");
|
||||||
assert!(t0.elapsed() < Duration::from_secs(5), "slept toward the stale deadline");
|
assert!(
|
||||||
|
t0.elapsed() < Duration::from_secs(5),
|
||||||
|
"slept toward the stale deadline"
|
||||||
|
);
|
||||||
c.disarm_timer(0);
|
c.disarm_timer(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -796,7 +815,11 @@ mod tests {
|
|||||||
assert!(c.try_arm_timer(0, far));
|
assert!(c.try_arm_timer(0, far));
|
||||||
c.note_deadline(near);
|
c.note_deadline(near);
|
||||||
let r = c.park(0, Some(far), || false);
|
let r = c.park(0, Some(far), || false);
|
||||||
assert_eq!(r, ParkResult::Woken, "re-arm wake lost through note_deadline");
|
assert_eq!(
|
||||||
|
r,
|
||||||
|
ParkResult::Woken,
|
||||||
|
"re-arm wake lost through note_deadline"
|
||||||
|
);
|
||||||
c.disarm_timer(0);
|
c.disarm_timer(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
+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]
|
||||||
|
|||||||
+107
-15
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
//! Give an actor a name so other actors can find it and message it.
|
//! Give an actor a name so other actors can find it and message it.
|
||||||
//!
|
//!
|
||||||
//! Without the registry, the only way to reach an actor is to already be
|
//! Without the registry, the only way to reach an actor is to already be
|
||||||
@@ -196,7 +195,9 @@ impl<M> std::fmt::Display for SendError<M> {
|
|||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
SendError::Unresolved(_) => write!(f, "no live actor registered under that name"),
|
SendError::Unresolved(_) => write!(f, "no live actor registered under that name"),
|
||||||
SendError::Dead(_) => write!(f, "the addressed actor is no longer the live incarnation"),
|
SendError::Dead(_) => {
|
||||||
|
write!(f, "the addressed actor is no longer the live incarnation")
|
||||||
|
}
|
||||||
SendError::NoChannel(_) => write!(f, "actor has no channel for this message type"),
|
SendError::NoChannel(_) => write!(f, "actor has no channel for this message type"),
|
||||||
SendError::Closed(_) => write!(f, "the actor's channel for this type is closed"),
|
SendError::Closed(_) => write!(f, "the actor's channel for this type is closed"),
|
||||||
SendError::NoMember(_) => write!(f, "no live member in the process group"),
|
SendError::NoMember(_) => write!(f, "no live member in the process group"),
|
||||||
@@ -243,7 +244,10 @@ struct Mailbox {
|
|||||||
|
|
||||||
impl Mailbox {
|
impl Mailbox {
|
||||||
fn new(pid: Pid) -> Self {
|
fn new(pid: Pid) -> Self {
|
||||||
Self { pid, channels: HashMap::new() }
|
Self {
|
||||||
|
pid,
|
||||||
|
channels: HashMap::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clone the `Sender<M>` for this actor, if it has one. Called **under the
|
/// Clone the `Sender<M>` for this actor, if it has one. Called **under the
|
||||||
@@ -294,7 +298,10 @@ pub(crate) struct Registry {
|
|||||||
|
|
||||||
impl Registry {
|
impl Registry {
|
||||||
pub(crate) fn new() -> Self {
|
pub(crate) fn new() -> Self {
|
||||||
Self { by_index: HashMap::new(), by_name: HashMap::new() }
|
Self {
|
||||||
|
by_index: HashMap::new(),
|
||||||
|
by_name: HashMap::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drop a dead holder's artifacts: every name bound to it, and its
|
/// Drop a dead holder's artifacts: every name bound to it, and its
|
||||||
@@ -303,7 +310,11 @@ impl Registry {
|
|||||||
/// wholesale on pid mismatch) and is left untouched.
|
/// wholesale on pid mismatch) and is left untouched.
|
||||||
fn prune_holder(&mut self, holder: Pid) {
|
fn prune_holder(&mut self, holder: Pid) {
|
||||||
self.by_name.retain(|_, p| *p != holder);
|
self.by_name.retain(|_, p| *p != holder);
|
||||||
if self.by_index.get(&holder.index()).is_some_and(|mb| mb.pid == holder) {
|
if self
|
||||||
|
.by_index
|
||||||
|
.get(&holder.index())
|
||||||
|
.is_some_and(|mb| mb.pid == holder)
|
||||||
|
{
|
||||||
self.by_index.remove(&holder.index());
|
self.by_index.remove(&holder.index());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -351,7 +362,11 @@ impl Registry {
|
|||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(&n, &p)| (p == mb.pid).then_some(n))
|
.filter_map(|(&n, &p)| (p == mb.pid).then_some(n))
|
||||||
.collect();
|
.collect();
|
||||||
Some(MailboxInfo { pid: mb.pid, names, depth: depth.min(u32::MAX as usize) as u32 })
|
Some(MailboxInfo {
|
||||||
|
pid: mb.pid,
|
||||||
|
names,
|
||||||
|
depth: depth.min(u32::MAX as usize) as u32,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,6 +403,16 @@ pub(crate) fn register_with<M: Send + 'static>(
|
|||||||
tx: Sender<M>,
|
tx: Sender<M>,
|
||||||
) -> Result<(), RegisterError> {
|
) -> Result<(), RegisterError> {
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| {
|
||||||
|
// Stamp-eligibility for the terminal record (soak sig 4): flag the
|
||||||
|
// tenancy BEFORE the binding lands and outside the registry lock (no
|
||||||
|
// nesting), so no successfully-registered actor can die unflagged.
|
||||||
|
// A register that then fails leaves a harmless overshoot; a stale
|
||||||
|
// `me` is screened by the same live() the binding requires below.
|
||||||
|
if live(inner, me) {
|
||||||
|
if let Some(slot) = inner.slot_at(me) {
|
||||||
|
slot.cold.lock().watchable = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut reg = inner.registry.lock();
|
let mut reg = inner.registry.lock();
|
||||||
if !live(inner, me) {
|
if !live(inner, me) {
|
||||||
return Err(RegisterError::NoProc);
|
return Err(RegisterError::NoProc);
|
||||||
@@ -418,13 +443,19 @@ pub(crate) fn register_with<M: Send + 'static>(
|
|||||||
/// index from a dead prior incarnation (pid mismatch) is replaced wholesale.
|
/// index from a dead prior incarnation (pid mismatch) is replaced wholesale.
|
||||||
/// Caller holds the registry lock and has established that `me` is live.
|
/// Caller holds the registry lock and has established that `me` is live.
|
||||||
fn publish_channel<M: Send + 'static>(reg: &mut Registry, me: Pid, tx: Sender<M>) {
|
fn publish_channel<M: Send + 'static>(reg: &mut Registry, me: Pid, tx: Sender<M>) {
|
||||||
let mb = reg.by_index.entry(me.index()).or_insert_with(|| Mailbox::new(me));
|
let mb = reg
|
||||||
|
.by_index
|
||||||
|
.entry(me.index())
|
||||||
|
.or_insert_with(|| Mailbox::new(me));
|
||||||
if mb.pid != me {
|
if mb.pid != me {
|
||||||
*mb = Mailbox::new(me);
|
*mb = Mailbox::new(me);
|
||||||
}
|
}
|
||||||
mb.channels.insert(
|
mb.channels.insert(
|
||||||
TypeId::of::<M>(),
|
TypeId::of::<M>(),
|
||||||
Channel { sender: Box::new(tx), msg_type: type_name::<M>() },
|
Channel {
|
||||||
|
sender: Box::new(tx),
|
||||||
|
msg_type: type_name::<M>(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,7 +494,10 @@ pub fn install<A: Addressable>(tx: Sender<A::Msg>) -> Pid<A> {
|
|||||||
pub(crate) fn install_for<M: Send + 'static>(pid: Pid, tx: Sender<M>) {
|
pub(crate) fn install_for<M: Send + 'static>(pid: Pid, tx: Sender<M>) {
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| {
|
||||||
let mut reg = inner.registry.lock();
|
let mut reg = inner.registry.lock();
|
||||||
debug_assert!(live(inner, pid), "install_for: pid must be a freshly spawned, live actor");
|
debug_assert!(
|
||||||
|
live(inner, pid),
|
||||||
|
"install_for: pid must be a freshly spawned, live actor"
|
||||||
|
);
|
||||||
publish_channel::<M>(&mut reg, pid, tx);
|
publish_channel::<M>(&mut reg, pid, tx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -486,6 +520,47 @@ pub fn whereis(name: &str) -> Option<Pid> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What a name is bound to, three-valued (bridge soak signature 4).
|
||||||
|
///
|
||||||
|
/// [`Live`](NameResolution::Live) is [`whereis`]'s `Some`.
|
||||||
|
/// [`Corpse`](NameResolution::Corpse) carries the *stored* holder pid of a
|
||||||
|
/// dead-but-unpruned binding — a state Erlang cannot represent (its name
|
||||||
|
/// death unregisters atomically; smarm's prune is lazy), captured here before
|
||||||
|
/// the prune that `whereis` performs discards it, so the caller can consult
|
||||||
|
/// [`terminal_reason`](crate::monitor::terminal_reason) for the tenancy's
|
||||||
|
/// real down reason. [`Unbound`](NameResolution::Unbound) matches Erlang's
|
||||||
|
/// unregistered name.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum NameResolution {
|
||||||
|
/// The stored holder is live (generation-checked); the binding stands.
|
||||||
|
Live(Pid),
|
||||||
|
/// The stored holder is dead. The binding was pruned on the way out —
|
||||||
|
/// the name heals exactly as `whereis` heals it; only the evidence is
|
||||||
|
/// returned instead of discarded. A second resolve is `Unbound`.
|
||||||
|
Corpse(Pid),
|
||||||
|
/// No binding stored (never registered, or already pruned by any reader).
|
||||||
|
Unbound,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve `name` like [`whereis`], but keep the corpse: the dead-holder arm
|
||||||
|
/// returns the stored pid it pruned instead of a bare `None`. Same lock
|
||||||
|
/// discipline and pruning behavior as `whereis`; same `Runtime::run()`
|
||||||
|
/// context contract.
|
||||||
|
pub fn resolve_name(name: &str) -> NameResolution {
|
||||||
|
with_runtime(|inner| {
|
||||||
|
let mut reg = inner.registry.lock();
|
||||||
|
let Some(&pid) = reg.by_name.get(name) else {
|
||||||
|
return NameResolution::Unbound;
|
||||||
|
};
|
||||||
|
if live(inner, pid) {
|
||||||
|
NameResolution::Live(pid)
|
||||||
|
} else {
|
||||||
|
reg.prune_holder(pid);
|
||||||
|
NameResolution::Corpse(pid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Like [`whereis`], but returns a *typed* [`Pid<A>`] instead of a bare
|
/// Like [`whereis`], but returns a *typed* [`Pid<A>`] instead of a bare
|
||||||
/// [`Pid`], so a follow-up [`send_to`] is compile-checked instead of needing
|
/// [`Pid`], so a follow-up [`send_to`] is compile-checked instead of needing
|
||||||
/// the untyped [`send_dyn`] escape hatch. `None` if the name is unbound or its
|
/// the untyped [`send_dyn`] escape hatch. `None` if the name is unbound or its
|
||||||
@@ -522,7 +597,10 @@ pub(crate) fn resolve_named_sender<M: Send + 'static>(name: &str) -> Option<(Pid
|
|||||||
}
|
}
|
||||||
// A live holder's mailbox is its own (publish replaces wholesale on
|
// A live holder's mailbox is its own (publish replaces wholesale on
|
||||||
// pid mismatch, and one live actor per slot), so index lookup is safe.
|
// pid mismatch, and one live actor per slot), so index lookup is safe.
|
||||||
let tx = reg.by_index.get(&pid.index()).and_then(Mailbox::clone_sender::<M>)?;
|
let tx = reg
|
||||||
|
.by_index
|
||||||
|
.get(&pid.index())
|
||||||
|
.and_then(Mailbox::clone_sender::<M>)?;
|
||||||
Some((pid, tx))
|
Some((pid, tx))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -535,7 +613,11 @@ pub fn unregister(name: &str) -> Option<Pid> {
|
|||||||
with_runtime(|inner| {
|
with_runtime(|inner| {
|
||||||
let mut reg = inner.registry.lock();
|
let mut reg = inner.registry.lock();
|
||||||
let pid = reg.by_name.remove(name)?;
|
let pid = reg.by_name.remove(name)?;
|
||||||
if live(inner, pid) { Some(pid) } else { None }
|
if live(inner, pid) {
|
||||||
|
Some(pid)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,12 +649,17 @@ pub fn send<M: Send + 'static>(name: Name<M>, msg: M) -> Result<(), SendError<M>
|
|||||||
reg.prune_holder(pid);
|
reg.prune_holder(pid);
|
||||||
return Err(SendError::Unresolved(msg));
|
return Err(SendError::Unresolved(msg));
|
||||||
}
|
}
|
||||||
match reg.by_index.get(&pid.index()).and_then(Mailbox::clone_sender::<M>) {
|
match reg
|
||||||
|
.by_index
|
||||||
|
.get(&pid.index())
|
||||||
|
.and_then(Mailbox::clone_sender::<M>)
|
||||||
|
{
|
||||||
Some(tx) => tx,
|
Some(tx) => tx,
|
||||||
None => return Err(SendError::NoChannel(msg)),
|
None => return Err(SendError::NoChannel(msg)),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
tx.send(msg).map_err(|crate::channel::SendError(m)| SendError::Closed(m))
|
tx.send(msg)
|
||||||
|
.map_err(|crate::channel::SendError(m)| SendError::Closed(m))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,7 +683,11 @@ fn send_to_pid<M: Send + 'static>(
|
|||||||
match reg.by_index.get(&pid.index()).map(|m| m.pid) {
|
match reg.by_index.get(&pid.index()).map(|m| m.pid) {
|
||||||
// Exact incarnation, still alive: its `M` channel, or NoChannel.
|
// Exact incarnation, still alive: its `M` channel, or NoChannel.
|
||||||
Some(stored) if stored == pid && live(inner, pid) => {
|
Some(stored) if stored == pid && live(inner, pid) => {
|
||||||
match reg.by_index.get(&pid.index()).and_then(Mailbox::clone_sender::<M>) {
|
match reg
|
||||||
|
.by_index
|
||||||
|
.get(&pid.index())
|
||||||
|
.and_then(Mailbox::clone_sender::<M>)
|
||||||
|
{
|
||||||
Some(tx) => tx,
|
Some(tx) => tx,
|
||||||
None => return Err(SendError::NoChannel(msg)),
|
None => return Err(SendError::NoChannel(msg)),
|
||||||
}
|
}
|
||||||
@@ -611,7 +702,8 @@ fn send_to_pid<M: Send + 'static>(
|
|||||||
_ => return Err(SendError::Dead(msg)),
|
_ => return Err(SendError::Dead(msg)),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
tx.send(msg).map_err(|crate::channel::SendError(m)| SendError::Closed(m))
|
tx.send(msg)
|
||||||
|
.map_err(|crate::channel::SendError(m)| SendError::Closed(m))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deliver `msg` directly to the exact actor identified by `pid`. Unlike
|
/// Deliver `msg` directly to the exact actor identified by `pid`. Unlike
|
||||||
|
|||||||
+28
-5
@@ -222,7 +222,10 @@ impl MpmcRing {
|
|||||||
if diff == 0 {
|
if diff == 0 {
|
||||||
// Our turn: claim the position.
|
// Our turn: claim the position.
|
||||||
match self.enqueue_pos.0.compare_exchange_weak(
|
match self.enqueue_pos.0.compare_exchange_weak(
|
||||||
pos, pos + 1, Ordering::Relaxed, Ordering::Relaxed,
|
pos,
|
||||||
|
pos + 1,
|
||||||
|
Ordering::Relaxed,
|
||||||
|
Ordering::Relaxed,
|
||||||
) {
|
) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// SAFETY: the claim gives us exclusive write access
|
// SAFETY: the claim gives us exclusive write access
|
||||||
@@ -250,7 +253,10 @@ impl MpmcRing {
|
|||||||
let diff = seq as isize - (pos + 1) as isize;
|
let diff = seq as isize - (pos + 1) as isize;
|
||||||
if diff == 0 {
|
if diff == 0 {
|
||||||
match self.dequeue_pos.0.compare_exchange_weak(
|
match self.dequeue_pos.0.compare_exchange_weak(
|
||||||
pos, pos + 1, Ordering::Relaxed, Ordering::Relaxed,
|
pos,
|
||||||
|
pos + 1,
|
||||||
|
Ordering::Relaxed,
|
||||||
|
Ordering::Relaxed,
|
||||||
) {
|
) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// SAFETY: the claim gives us exclusive read access;
|
// SAFETY: the claim gives us exclusive read access;
|
||||||
@@ -464,19 +470,36 @@ mod tests {
|
|||||||
|
|
||||||
let popped = popped.lock().unwrap();
|
let popped = popped.lock().unwrap();
|
||||||
assert_eq!(popped.len(), total, "count mismatch");
|
assert_eq!(popped.len(), total, "count mismatch");
|
||||||
let set: HashSet<u64> = popped.iter().map(|p| ((p.index() as u64) << 32) | p.generation() as u64).collect();
|
let set: HashSet<u64> = popped
|
||||||
|
.iter()
|
||||||
|
.map(|p| ((p.index() as u64) << 32) | p.generation() as u64)
|
||||||
|
.collect();
|
||||||
assert_eq!(set.len(), total, "duplicate or lost element");
|
assert_eq!(set.len(), total, "duplicate or lost element");
|
||||||
assert_eq!(pop(&q), None);
|
assert_eq!(pop(&q), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mpmc_exactly_once_contended() {
|
fn mpmc_exactly_once_contended() {
|
||||||
exactly_once(MpmcRing::new(8, 4096), |q, p| q.push(p), |q| q.pop(), 4, 4, 1000);
|
exactly_once(
|
||||||
|
MpmcRing::new(8, 4096),
|
||||||
|
|q, p| q.push(p),
|
||||||
|
|q| q.pop(),
|
||||||
|
4,
|
||||||
|
4,
|
||||||
|
1000,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn striped_exactly_once_contended() {
|
fn striped_exactly_once_contended() {
|
||||||
exactly_once(StripedRing::new(8, 4096), |q, p| q.push(p), |q| q.pop(), 4, 4, 1000);
|
exactly_once(
|
||||||
|
StripedRing::new(8, 4096),
|
||||||
|
|q, p| q.push(p),
|
||||||
|
|q| q.pop(),
|
||||||
|
4,
|
||||||
|
4,
|
||||||
|
1000,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+136
-32
@@ -112,10 +112,11 @@
|
|||||||
//! deadline, so an expiry wakes one scheduler, not a herd.
|
//! deadline, so an expiry wakes one scheduler, not a herd.
|
||||||
|
|
||||||
use crate::actor::{
|
use crate::actor::{
|
||||||
clear_current_pid, is_actor_done, reset_actor_done, set_current_actor_box,
|
clear_current_pid, is_actor_done, reset_actor_done, set_current_actor_box, set_current_pid,
|
||||||
set_current_pid, take_last_outcome, Actor, Outcome,
|
take_last_outcome, Actor, Outcome,
|
||||||
};
|
};
|
||||||
use crate::channel::Sender;
|
use crate::channel::Sender;
|
||||||
|
use crate::context::{get_actor_sp, set_actor_sp, switch_to_actor};
|
||||||
use crate::io::IoThread;
|
use crate::io::IoThread;
|
||||||
use crate::monitor::{Down, DownReason, MonitorId};
|
use crate::monitor::{Down, DownReason, MonitorId};
|
||||||
use crate::pid::Pid;
|
use crate::pid::Pid;
|
||||||
@@ -124,11 +125,8 @@ use crate::raw_mutex::RawMutex;
|
|||||||
use crate::slot_state::{StateWord, Status, Unpark};
|
use crate::slot_state::{StateWord, Status, Unpark};
|
||||||
use crate::supervisor::Signal;
|
use crate::supervisor::Signal;
|
||||||
use crate::timer::Timers;
|
use crate::timer::Timers;
|
||||||
use crate::context::{get_actor_sp, set_actor_sp, switch_to_actor};
|
|
||||||
|
|
||||||
use std::sync::atomic::{
|
use std::sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, AtomicU64, AtomicUsize, Ordering};
|
||||||
AtomicBool, AtomicPtr, AtomicU32, AtomicU64, AtomicUsize, Ordering,
|
|
||||||
};
|
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
@@ -175,7 +173,9 @@ impl Config {
|
|||||||
pub fn exact(n: usize) -> Self {
|
pub fn exact(n: usize) -> Self {
|
||||||
assert!(n >= 1, "scheduler thread count must be ≥ 1");
|
assert!(n >= 1, "scheduler thread count must be ≥ 1");
|
||||||
Self {
|
Self {
|
||||||
min: n, max: n, exact: Some(n),
|
min: n,
|
||||||
|
max: n,
|
||||||
|
exact: Some(n),
|
||||||
alloc_interval: crate::preempt::DEFAULT_ALLOC_INTERVAL,
|
alloc_interval: crate::preempt::DEFAULT_ALLOC_INTERVAL,
|
||||||
timeslice_cycles: crate::preempt::DEFAULT_TIMESLICE_CYCLES,
|
timeslice_cycles: crate::preempt::DEFAULT_TIMESLICE_CYCLES,
|
||||||
stack_pool_cap: n * 4,
|
stack_pool_cap: n * 4,
|
||||||
@@ -196,7 +196,9 @@ impl Config {
|
|||||||
assert!(e >= 1, "exact must be ≥ 1");
|
assert!(e >= 1, "exact must be ≥ 1");
|
||||||
}
|
}
|
||||||
Self {
|
Self {
|
||||||
min, max, exact,
|
min,
|
||||||
|
max,
|
||||||
|
exact,
|
||||||
alloc_interval: crate::preempt::DEFAULT_ALLOC_INTERVAL,
|
alloc_interval: crate::preempt::DEFAULT_ALLOC_INTERVAL,
|
||||||
timeslice_cycles: crate::preempt::DEFAULT_TIMESLICE_CYCLES,
|
timeslice_cycles: crate::preempt::DEFAULT_TIMESLICE_CYCLES,
|
||||||
stack_pool_cap: max * 4,
|
stack_pool_cap: max * 4,
|
||||||
@@ -322,7 +324,9 @@ impl Default for Config {
|
|||||||
.map(|n| n.get())
|
.map(|n| n.get())
|
||||||
.unwrap_or(1);
|
.unwrap_or(1);
|
||||||
Self {
|
Self {
|
||||||
min: 1, max: avail, exact: None,
|
min: 1,
|
||||||
|
max: avail,
|
||||||
|
exact: None,
|
||||||
alloc_interval: crate::preempt::DEFAULT_ALLOC_INTERVAL,
|
alloc_interval: crate::preempt::DEFAULT_ALLOC_INTERVAL,
|
||||||
timeslice_cycles: crate::preempt::DEFAULT_TIMESLICE_CYCLES,
|
timeslice_cycles: crate::preempt::DEFAULT_TIMESLICE_CYCLES,
|
||||||
stack_pool_cap: avail * 4,
|
stack_pool_cap: avail * 4,
|
||||||
@@ -376,7 +380,9 @@ pub struct RuntimeStats {
|
|||||||
impl RuntimeStats {
|
impl RuntimeStats {
|
||||||
/// Sum of run queue lengths across all scheduler threads.
|
/// Sum of run queue lengths across all scheduler threads.
|
||||||
pub fn total_run_queue_len(&self) -> u64 {
|
pub fn total_run_queue_len(&self) -> u64 {
|
||||||
self.inner.stats.iter()
|
self.inner
|
||||||
|
.stats
|
||||||
|
.iter()
|
||||||
.map(|s| s.run_queue_len.load(Ordering::Relaxed))
|
.map(|s| s.run_queue_len.load(Ordering::Relaxed))
|
||||||
.sum()
|
.sum()
|
||||||
}
|
}
|
||||||
@@ -400,7 +406,9 @@ impl RuntimeStats {
|
|||||||
/// scheduler threads. Counters are reset at the start of each `run()`,
|
/// scheduler threads. Counters are reset at the start of each `run()`,
|
||||||
/// so after a run this reads that run's total.
|
/// so after a run this reads that run's total.
|
||||||
pub fn slot_hits(&self) -> u64 {
|
pub fn slot_hits(&self) -> u64 {
|
||||||
self.inner.stats.iter()
|
self.inner
|
||||||
|
.stats
|
||||||
|
.iter()
|
||||||
.map(|s| s.slot_hits.load(Ordering::Relaxed))
|
.map(|s| s.slot_hits.load(Ordering::Relaxed))
|
||||||
.sum()
|
.sum()
|
||||||
}
|
}
|
||||||
@@ -408,7 +416,9 @@ impl RuntimeStats {
|
|||||||
/// RFC 005: total slot occupants displaced to the shared queue, summed
|
/// RFC 005: total slot occupants displaced to the shared queue, summed
|
||||||
/// across scheduler threads. Reset at the start of each `run()`.
|
/// across scheduler threads. Reset at the start of each `run()`.
|
||||||
pub fn slot_displacements(&self) -> u64 {
|
pub fn slot_displacements(&self) -> u64 {
|
||||||
self.inner.stats.iter()
|
self.inner
|
||||||
|
.stats
|
||||||
|
.iter()
|
||||||
.map(|s| s.slot_displacements.load(Ordering::Relaxed))
|
.map(|s| s.slot_displacements.load(Ordering::Relaxed))
|
||||||
.sum()
|
.sum()
|
||||||
}
|
}
|
||||||
@@ -472,6 +482,28 @@ pub(crate) struct SlotCold {
|
|||||||
/// epoch-matched unpark.
|
/// epoch-matched unpark.
|
||||||
pub(crate) waiters: Vec<(Pid, u32)>,
|
pub(crate) waiters: Vec<(Pid, u32)>,
|
||||||
pub(crate) outcome: Option<Outcome>,
|
pub(crate) outcome: Option<Outcome>,
|
||||||
|
/// The slot's most recent *watchable-tenancy* death: `(generation,
|
||||||
|
/// reason)`, stamped by `finalize_actor` — but only for a tenancy whose
|
||||||
|
/// `watchable` bit was set — and deliberately never cleared: a new
|
||||||
|
/// tenant's install leaves it standing (it describes the previous
|
||||||
|
/// tenancy), and only the next *watchable* death overwrites it.
|
||||||
|
/// Anonymous green-thread churn must not evict it: the free list is
|
||||||
|
/// LIFO, so the just-freed slot is the first recycled, and an
|
||||||
|
/// unconditional stamp made a watchable tenancy's record the
|
||||||
|
/// shortest-lived data in the runtime. Read generation-matched via
|
||||||
|
/// [`terminal_reason`](crate::monitor::terminal_reason), so a watch that
|
||||||
|
/// raced its target's death can recover the real down reason instead of
|
||||||
|
/// a blanket `NoProc` (bridge soak signatures 4 and 5).
|
||||||
|
pub(crate) terminal: Option<(u32, DownReason)>,
|
||||||
|
/// Stamp eligibility for `terminal` above: someone could plausibly hold
|
||||||
|
/// a watch on this tenancy. Two set-sites, both while the tenancy is
|
||||||
|
/// live: `register_with` *before* the binding lands (no successfully
|
||||||
|
/// registered actor can die unflagged; a failed register's overshoot is
|
||||||
|
/// harmless), and [`mark_watchable`](crate::monitor::mark_watchable) —
|
||||||
|
/// the bridge calls it wherever a pid is encoded across the boundary,
|
||||||
|
/// because BEAM can only watch pids it holds and can only hold pids
|
||||||
|
/// that crossed. Reset at reclaim.
|
||||||
|
pub(crate) watchable: bool,
|
||||||
pub(crate) supervisor_channel: Option<Sender<Signal>>,
|
pub(crate) supervisor_channel: Option<Sender<Signal>>,
|
||||||
/// Watchers registered via `monitor()`, each tagged with its
|
/// Watchers registered via `monitor()`, each tagged with its
|
||||||
/// `MonitorId` so `demonitor` can remove exactly one. Each receives one
|
/// `MonitorId` so `demonitor` can remove exactly one. Each receives one
|
||||||
@@ -626,6 +658,8 @@ impl Slot {
|
|||||||
actor: None,
|
actor: None,
|
||||||
waiters: Vec::new(),
|
waiters: Vec::new(),
|
||||||
outcome: None,
|
outcome: None,
|
||||||
|
terminal: None,
|
||||||
|
watchable: false,
|
||||||
supervisor_channel: None,
|
supervisor_channel: None,
|
||||||
monitors: Vec::new(),
|
monitors: Vec::new(),
|
||||||
links: Vec::new(),
|
links: Vec::new(),
|
||||||
@@ -689,7 +723,8 @@ impl Slot {
|
|||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn record_message(&self) {
|
pub(crate) fn record_message(&self) {
|
||||||
let v = self.messages_received.load(Ordering::Relaxed);
|
let v = self.messages_received.load(Ordering::Relaxed);
|
||||||
self.messages_received.store(v.wrapping_add(1), Ordering::Relaxed);
|
self.messages_received
|
||||||
|
.store(v.wrapping_add(1), Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read the received-message tally (Relaxed; cross-thread snapshot read).
|
/// Read the received-message tally (Relaxed; cross-thread snapshot read).
|
||||||
@@ -708,7 +743,8 @@ impl Slot {
|
|||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn add_budget(&self, cycles: u64) {
|
pub(crate) fn add_budget(&self, cycles: u64) {
|
||||||
let v = self.budget_cycles.load(Ordering::Relaxed);
|
let v = self.budget_cycles.load(Ordering::Relaxed);
|
||||||
self.budget_cycles.store(v.wrapping_add(cycles), Ordering::Relaxed);
|
self.budget_cycles
|
||||||
|
.store(v.wrapping_add(cycles), Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read the accumulated budget cycles (Relaxed). Always 0 unless the
|
/// Read the accumulated budget cycles (Relaxed). Always 0 unless the
|
||||||
@@ -847,7 +883,6 @@ impl Slot {
|
|||||||
Some(*unsafe { Box::from_raw(raw) })
|
Some(*unsafe { Box::from_raw(raw) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -1153,10 +1188,29 @@ impl RuntimeInner {
|
|||||||
MonitorId(self.next_monitor_id.fetch_add(1, Ordering::Relaxed) + 1)
|
MonitorId(self.next_monitor_id.fetch_add(1, Ordering::Relaxed) + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pop a vacant slot index, or `None` when the slab is full. The claim
|
||||||
|
/// is atomic — a single pop under the free-list lock — so callers get
|
||||||
|
/// claim-or-report semantics with no check-then-spawn TOCTOU: whoever
|
||||||
|
/// gets `Some` owns that slot, full stop.
|
||||||
|
pub(crate) fn try_allocate_slot(&self) -> Option<u32> {
|
||||||
|
self.free.lock().pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a slot claimed by [`try_allocate_slot`](Self::try_allocate_slot)
|
||||||
|
/// that never had an actor installed into it (e.g. stack allocation
|
||||||
|
/// panicked between claim and install). NOT for dead actors — their
|
||||||
|
/// slots go back through `reclaim_slot`, which handles generation bump,
|
||||||
|
/// waiter/monitor/link teardown, and stack recycling.
|
||||||
|
pub(crate) fn return_vacant_slot(&self, idx: u32) {
|
||||||
|
self.free.lock().push(idx);
|
||||||
|
}
|
||||||
|
|
||||||
/// Pop a vacant slot index, or die loudly. The fixed slab is a deliberate
|
/// Pop a vacant slot index, or die loudly. The fixed slab is a deliberate
|
||||||
/// v0.5 simplification (ROADMAP: "Deferred"); the panic names the fix.
|
/// v0.5 simplification (ROADMAP: "Deferred"); the panic names the fix.
|
||||||
|
/// Callers that can shed load instead use [`try_allocate_slot`]
|
||||||
|
/// (Self::try_allocate_slot) via `scheduler::try_spawn`.
|
||||||
pub(crate) fn allocate_slot(&self) -> u32 {
|
pub(crate) fn allocate_slot(&self) -> u32 {
|
||||||
match self.free.lock().pop() {
|
match self.try_allocate_slot() {
|
||||||
Some(idx) => idx,
|
Some(idx) => idx,
|
||||||
None => panic!(
|
None => panic!(
|
||||||
"smarm: actor slot table exhausted — {} actors are live \
|
"smarm: actor slot table exhausted — {} actors are live \
|
||||||
@@ -1246,11 +1300,13 @@ impl Runtime {
|
|||||||
|
|
||||||
// Re-initialise shared state for this run.
|
// Re-initialise shared state for this run.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
self.inner.run_queue.len(), 0,
|
self.inner.run_queue.len(),
|
||||||
|
0,
|
||||||
"run() called while previous run still active"
|
"run() called while previous run still active"
|
||||||
);
|
);
|
||||||
debug_assert_eq!(
|
debug_assert_eq!(
|
||||||
self.inner.live_actors.load(Ordering::Acquire), 0,
|
self.inner.live_actors.load(Ordering::Acquire),
|
||||||
|
0,
|
||||||
"run() called while previous run still active"
|
"run() called while previous run still active"
|
||||||
);
|
);
|
||||||
// RFC 018: the IO producers reach the runtime (slot table + unpark)
|
// RFC 018: the IO producers reach the runtime (slot table + unpark)
|
||||||
@@ -1397,7 +1453,9 @@ impl Runtime {
|
|||||||
|
|
||||||
/// Snapshot of runtime statistics for introspection / tests.
|
/// Snapshot of runtime statistics for introspection / tests.
|
||||||
pub fn stats(&self) -> RuntimeStats {
|
pub fn stats(&self) -> RuntimeStats {
|
||||||
RuntimeStats { inner: self.inner.clone() }
|
RuntimeStats {
|
||||||
|
inner: self.inner.clone(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1432,7 +1490,10 @@ thread_local! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub(crate) enum YieldIntent { Yield, Park }
|
pub(crate) enum YieldIntent {
|
||||||
|
Yield,
|
||||||
|
Park,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn set_yield_intent(i: YieldIntent) {
|
pub(crate) fn set_yield_intent(i: YieldIntent) {
|
||||||
YIELD_INTENT.with(|c| c.set(i));
|
YIELD_INTENT.with(|c| c.set(i));
|
||||||
@@ -1467,7 +1528,10 @@ pub const ROOT_PID: Pid = Pid::new(u32::MAX, u32::MAX);
|
|||||||
/// for writes after reclaim — so an over-eager mark costs a cancel-write,
|
/// for writes after reclaim — so an over-eager mark costs a cancel-write,
|
||||||
/// never data.
|
/// never data.
|
||||||
fn maybe_shrink_stack(slot: &Slot) {
|
fn maybe_shrink_stack(slot: &Slot) {
|
||||||
let parks = slot.parks_since_shrink.load(Ordering::Relaxed).saturating_add(1);
|
let parks = slot
|
||||||
|
.parks_since_shrink
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
.saturating_add(1);
|
||||||
slot.parks_since_shrink.store(parks, Ordering::Relaxed);
|
slot.parks_since_shrink.store(parks, Ordering::Relaxed);
|
||||||
|
|
||||||
let sp = slot.sp.load(Ordering::Relaxed);
|
let sp = slot.sp.load(Ordering::Relaxed);
|
||||||
@@ -1566,12 +1630,19 @@ pub(crate) fn install_actor(
|
|||||||
// publish below.
|
// publish below.
|
||||||
let (diag_reserve, diag_guard) = stack.shape();
|
let (diag_reserve, diag_guard) = stack.shape();
|
||||||
let diag_top = stack.top() as usize;
|
let diag_top = stack.top() as usize;
|
||||||
slot.stop_ptr.store(Arc::as_ptr(&stop) as *mut _, Ordering::Release);
|
slot.stop_ptr
|
||||||
|
.store(Arc::as_ptr(&stop) as *mut _, Ordering::Release);
|
||||||
{
|
{
|
||||||
let mut cold = slot.cold.lock();
|
let mut cold = slot.cold.lock();
|
||||||
debug_assert!(cold.actor.is_none(), "install over live actor");
|
debug_assert!(cold.actor.is_none(), "install over live actor");
|
||||||
debug_assert!(cold.waiters.is_empty() && cold.monitors.is_empty() && cold.links.is_empty());
|
debug_assert!(cold.waiters.is_empty() && cold.monitors.is_empty() && cold.links.is_empty());
|
||||||
cold.actor = Some(Actor { pid, stack, supervisor, stop, trap: None });
|
cold.actor = Some(Actor {
|
||||||
|
pid,
|
||||||
|
stack,
|
||||||
|
supervisor,
|
||||||
|
stop,
|
||||||
|
trap: None,
|
||||||
|
});
|
||||||
cold.outstanding_handles = 1;
|
cold.outstanding_handles = 1;
|
||||||
cold.outcome = None;
|
cold.outcome = None;
|
||||||
cold.pending_io_result = None;
|
cold.pending_io_result = None;
|
||||||
@@ -1583,9 +1654,11 @@ pub(crate) fn install_actor(
|
|||||||
slot.parks_since_shrink.store(0, Ordering::Relaxed);
|
slot.parks_since_shrink.store(0, Ordering::Relaxed);
|
||||||
slot.shrink_count.store(0, Ordering::Relaxed);
|
slot.shrink_count.store(0, Ordering::Relaxed);
|
||||||
slot.diag_stack_top.store(diag_top, Ordering::Relaxed);
|
slot.diag_stack_top.store(diag_top, Ordering::Relaxed);
|
||||||
slot.diag_stack_reserve.store(diag_reserve, Ordering::Relaxed);
|
slot.diag_stack_reserve
|
||||||
|
.store(diag_reserve, Ordering::Relaxed);
|
||||||
slot.diag_stack_guard.store(diag_guard, Ordering::Relaxed);
|
slot.diag_stack_guard.store(diag_guard, Ordering::Relaxed);
|
||||||
slot.diag_pid.store(((idx as u64) << 32) | gen as u64, Ordering::Relaxed);
|
slot.diag_pid
|
||||||
|
.store(((idx as u64) << 32) | gen as u64, Ordering::Relaxed);
|
||||||
slot.store_closure(closure);
|
slot.store_closure(closure);
|
||||||
slot.reset_counters();
|
slot.reset_counters();
|
||||||
inner.live_actors.fetch_add(1, Ordering::Relaxed);
|
inner.live_actors.fetch_add(1, Ordering::Relaxed);
|
||||||
@@ -1594,7 +1667,10 @@ pub(crate) fn install_actor(
|
|||||||
// Release store orders everything above before any Acquire reader.
|
// Release store orders everything above before any Acquire reader.
|
||||||
slot.word.publish_queued(gen);
|
slot.word.publish_queued(gen);
|
||||||
inner.enqueue(pid);
|
inner.enqueue(pid);
|
||||||
crate::te!(crate::trace::Event::Spawn { parent: supervisor, child: pid });
|
crate::te!(crate::trace::Event::Spawn {
|
||||||
|
parent: supervisor,
|
||||||
|
child: pid
|
||||||
|
});
|
||||||
pid
|
pid
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1611,14 +1687,19 @@ pub(crate) fn install_actor(
|
|||||||
/// is released — a last-sender drop can unpark a receiver, which takes the
|
/// is released — a last-sender drop can unpark a receiver, which takes the
|
||||||
/// run-queue mutex; legal under a cold lock, but pointless to nest.
|
/// run-queue mutex; legal under a cold lock, but pointless to nest.
|
||||||
pub(crate) fn reclaim_slot(inner: &RuntimeInner, pid: Pid) {
|
pub(crate) fn reclaim_slot(inner: &RuntimeInner, pid: Pid) {
|
||||||
let Some(slot) = inner.slot_at(pid) else { return };
|
let Some(slot) = inner.slot_at(pid) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
let dropped_outside;
|
let dropped_outside;
|
||||||
{
|
{
|
||||||
let mut cold = slot.cold.lock();
|
let mut cold = slot.cold.lock();
|
||||||
if slot.status_for(pid) != Status::Done || cold.outstanding_handles != 0 {
|
if slot.status_for(pid) != Status::Done || cold.outstanding_handles != 0 {
|
||||||
return; // already reclaimed, or not yet eligible
|
return; // already reclaimed, or not yet eligible
|
||||||
}
|
}
|
||||||
debug_assert!(cold.actor.is_none(), "reclaiming a slot that still owns an actor");
|
debug_assert!(
|
||||||
|
cold.actor.is_none(),
|
||||||
|
"reclaiming a slot that still owns an actor"
|
||||||
|
);
|
||||||
dropped_outside = (
|
dropped_outside = (
|
||||||
cold.outcome.take(),
|
cold.outcome.take(),
|
||||||
cold.supervisor_channel.take(),
|
cold.supervisor_channel.take(),
|
||||||
@@ -1628,6 +1709,7 @@ pub(crate) fn reclaim_slot(inner: &RuntimeInner, pid: Pid) {
|
|||||||
cold.waiters.clear();
|
cold.waiters.clear();
|
||||||
cold.monitors.clear();
|
cold.monitors.clear();
|
||||||
cold.links.clear();
|
cold.links.clear();
|
||||||
|
cold.watchable = false;
|
||||||
slot.reset_counters();
|
slot.reset_counters();
|
||||||
slot.stop_ptr.store(std::ptr::null_mut(), Ordering::Release);
|
slot.stop_ptr.store(std::ptr::null_mut(), Ordering::Release);
|
||||||
// The generation bump IS the reclaim: every stale pid is dead from
|
// The generation bump IS the reclaim: every stale pid is dead from
|
||||||
@@ -1667,6 +1749,18 @@ fn finalize_actor(inner: &Arc<RuntimeInner>, pid: Pid, outcome: Outcome) {
|
|||||||
None => panic!("finalize_actor: actor vanished"),
|
None => panic!("finalize_actor: actor vanished"),
|
||||||
};
|
};
|
||||||
cold.outcome = Some(joiner_outcome);
|
cold.outcome = Some(joiner_outcome);
|
||||||
|
// Terminal record (soak sig 4): stamped before the generation ever
|
||||||
|
// bumps, under the cold lock, so a reader that resolved this pid can
|
||||||
|
// recover the reason after the slot moves on — but only for a
|
||||||
|
// tenancy that ever held a name. The free list is LIFO, so the slot
|
||||||
|
// this death frees is the very next one recycled; if every green
|
||||||
|
// thread's exit stamped too, the churn behind any real workload
|
||||||
|
// would evict a watchable tenancy's record in well under the race
|
||||||
|
// window this exists to cover. Overwritten only by the slot's next
|
||||||
|
// *watchable* death.
|
||||||
|
if cold.watchable {
|
||||||
|
cold.terminal = Some((pid.generation(), down_reason));
|
||||||
|
}
|
||||||
slot.stop_ptr.store(std::ptr::null_mut(), Ordering::Release);
|
slot.stop_ptr.store(std::ptr::null_mut(), Ordering::Release);
|
||||||
// Done is published under the cold lock, so join's
|
// Done is published under the cold lock, so join's
|
||||||
// check-Done-or-register-waiter (also under it) can never miss: it
|
// check-Done-or-register-waiter (also under it) can never miss: it
|
||||||
@@ -1704,7 +1798,10 @@ fn finalize_actor(inner: &Arc<RuntimeInner>, pid: Pid, outcome: Outcome) {
|
|||||||
// Notify monitors. Sent outside any slot lock: `send` may unpark a parked
|
// Notify monitors. Sent outside any slot lock: `send` may unpark a parked
|
||||||
// receiver, which takes the run-queue mutex.
|
// receiver, which takes the run-queue mutex.
|
||||||
for (_, m) in monitors {
|
for (_, m) in monitors {
|
||||||
let _ = m.send(Down { pid, reason: down_reason });
|
let _ = m.send(Down {
|
||||||
|
pid,
|
||||||
|
reason: down_reason,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Walk linked peers ONE AT A TIME (cold locks are leaves). For every
|
// Walk linked peers ONE AT A TIME (cold locks are leaves). For every
|
||||||
@@ -1736,7 +1833,10 @@ fn finalize_actor(inner: &Arc<RuntimeInner>, pid: Pid, outcome: Outcome) {
|
|||||||
};
|
};
|
||||||
match trap {
|
match trap {
|
||||||
Some(Some(tx)) => {
|
Some(Some(tx)) => {
|
||||||
let _ = tx.send(crate::link::ExitSignal { from: pid, reason: down_reason });
|
let _ = tx.send(crate::link::ExitSignal {
|
||||||
|
from: pid,
|
||||||
|
reason: down_reason,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Some(None) => crate::scheduler::request_stop(peer),
|
Some(None) => crate::scheduler::request_stop(peer),
|
||||||
None => {}
|
None => {}
|
||||||
@@ -1892,7 +1992,9 @@ fn schedule_loop(inner: &Arc<RuntimeInner>, slot_idx: usize) {
|
|||||||
let io_out = inner.io_outstanding.load(Ordering::Acquire)
|
let io_out = inner.io_outstanding.load(Ordering::Acquire)
|
||||||
+ inner.io_fd_waiters.load(Ordering::Acquire);
|
+ inner.io_fd_waiters.load(Ordering::Acquire);
|
||||||
|
|
||||||
stats.run_queue_len.store(inner.run_queue.len(), Ordering::Relaxed);
|
stats
|
||||||
|
.run_queue_len
|
||||||
|
.store(inner.run_queue.len(), Ordering::Relaxed);
|
||||||
let pop = match inner.run_queue.pop() {
|
let pop = match inner.run_queue.pop() {
|
||||||
Some(pid) => Pop::Got(pid),
|
Some(pid) => Pop::Got(pid),
|
||||||
None => {
|
None => {
|
||||||
@@ -2044,7 +2146,9 @@ fn schedule_loop(inner: &Arc<RuntimeInner>, slot_idx: usize) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update per-thread stats: record who's on-CPU.
|
// Update per-thread stats: record who's on-CPU.
|
||||||
stats.current_pid_index.store(pid.index(), Ordering::Relaxed);
|
stats
|
||||||
|
.current_pid_index
|
||||||
|
.store(pid.index(), Ordering::Relaxed);
|
||||||
|
|
||||||
set_actor_sp(sp);
|
set_actor_sp(sp);
|
||||||
set_current_pid(pid);
|
set_current_pid(pid);
|
||||||
|
|||||||
+181
-78
@@ -68,9 +68,7 @@
|
|||||||
use crate::actor::current_pid;
|
use crate::actor::current_pid;
|
||||||
use crate::channel::Sender;
|
use crate::channel::Sender;
|
||||||
use crate::pid::{Name, Pid};
|
use crate::pid::{Name, Pid};
|
||||||
use crate::runtime::{
|
use crate::runtime::{self, RuntimeInner, YieldIntent, RUNTIME};
|
||||||
self, RuntimeInner, YieldIntent, RUNTIME,
|
|
||||||
};
|
|
||||||
use crate::supervisor::Signal;
|
use crate::supervisor::Signal;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -152,7 +150,9 @@ pub struct JoinHandle {
|
|||||||
|
|
||||||
impl JoinHandle {
|
impl JoinHandle {
|
||||||
/// The identity of the actor this handle refers to.
|
/// The identity of the actor this handle refers to.
|
||||||
pub fn pid(&self) -> Pid { self.pid }
|
pub fn pid(&self) -> Pid {
|
||||||
|
self.pid
|
||||||
|
}
|
||||||
|
|
||||||
/// Block the calling actor until the spawned actor finishes, then
|
/// Block the calling actor until the spawned actor finishes, then
|
||||||
/// report how it finished: `Ok(())` if it returned normally or stopped
|
/// report how it finished: `Ok(())` if it returned normally or stopped
|
||||||
@@ -182,12 +182,10 @@ impl JoinHandle {
|
|||||||
crate::slot_state::Status::Stale => {
|
crate::slot_state::Status::Stale => {
|
||||||
panic!("join: target slot has been reused")
|
panic!("join: target slot has been reused")
|
||||||
}
|
}
|
||||||
crate::slot_state::Status::Done => {
|
crate::slot_state::Status::Done => Some(match cold.outcome.take() {
|
||||||
Some(match cold.outcome.take() {
|
Some(outcome) => outcome,
|
||||||
Some(outcome) => outcome,
|
None => panic!("Done slot must have outcome"),
|
||||||
None => panic!("Done slot must have outcome"),
|
}),
|
||||||
})
|
|
||||||
}
|
|
||||||
crate::slot_state::Status::Live => {
|
crate::slot_state::Status::Live => {
|
||||||
// begin_wait is lock-free, legal under the cold lock;
|
// begin_wait is lock-free, legal under the cold lock;
|
||||||
// registering under it makes the epoch atomic with
|
// registering under it makes the epoch atomic with
|
||||||
@@ -227,8 +225,7 @@ impl JoinHandle {
|
|||||||
match slot.status_for(self.pid) {
|
match slot.status_for(self.pid) {
|
||||||
crate::slot_state::Status::Stale => false,
|
crate::slot_state::Status::Stale => false,
|
||||||
status => {
|
status => {
|
||||||
cold.outstanding_handles =
|
cold.outstanding_handles = cold.outstanding_handles.saturating_sub(1);
|
||||||
cold.outstanding_handles.saturating_sub(1);
|
|
||||||
cold.outstanding_handles == 0
|
cold.outstanding_handles == 0
|
||||||
&& status == crate::slot_state::Status::Done
|
&& status == crate::slot_state::Status::Done
|
||||||
}
|
}
|
||||||
@@ -281,6 +278,37 @@ pub struct SpawnOpts {
|
|||||||
pub guard_size: Option<usize>,
|
pub guard_size: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Why [`try_spawn`] could not start an actor.
|
||||||
|
///
|
||||||
|
/// Marked `non_exhaustive`: today the only refusal is a full slab, but a
|
||||||
|
/// future variant (say, a shutdown-in-progress refusal) must not be a
|
||||||
|
/// breaking change for shed-path `match`es.
|
||||||
|
#[non_exhaustive]
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum SpawnError {
|
||||||
|
/// The fixed actor slab ([`Config::max_actors`]
|
||||||
|
/// (crate::runtime::Config::max_actors)) is full: every slot is claimed
|
||||||
|
/// by a live actor. This is a routine overload condition, not an
|
||||||
|
/// invariant violation — shed the unit of work (close the socket,
|
||||||
|
/// return a 503) and try again once actors have died.
|
||||||
|
AtCapacity,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Display for SpawnError {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
match self {
|
||||||
|
SpawnError::AtCapacity => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"actor slab at capacity (`Config::max_actors` live actors)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for SpawnError {}
|
||||||
|
|
||||||
/// Start a new actor running `f`, and return a [`JoinHandle`] for it.
|
/// Start a new actor running `f`, and return a [`JoinHandle`] for it.
|
||||||
///
|
///
|
||||||
/// The new actor runs concurrently with its caller and with every other
|
/// The new actor runs concurrently with its caller and with every other
|
||||||
@@ -305,9 +333,7 @@ pub fn spawn(f: impl FnOnce() + Send + 'static) -> JoinHandle {
|
|||||||
|
|
||||||
/// [`spawn`] with per-actor stack shape overrides (RFC 019).
|
/// [`spawn`] with per-actor stack shape overrides (RFC 019).
|
||||||
pub fn spawn_with(opts: SpawnOpts, f: impl FnOnce() + Send + 'static) -> JoinHandle {
|
pub fn spawn_with(opts: SpawnOpts, f: impl FnOnce() + Send + 'static) -> JoinHandle {
|
||||||
let parent = current_pid().unwrap_or_else(|| {
|
let parent = current_pid().unwrap_or_else(|| with_runtime(|_| crate::runtime::ROOT_PID));
|
||||||
with_runtime(|_| crate::runtime::ROOT_PID)
|
|
||||||
});
|
|
||||||
spawn_under_with(parent, opts, f)
|
spawn_under_with(parent, opts, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,7 +365,77 @@ pub fn spawn_under_with<A>(
|
|||||||
crate::runtime::install_actor(inner, idx, sp, stack, supervisor, closure)
|
crate::runtime::install_actor(inner, idx, sp, stack, supervisor, closure)
|
||||||
});
|
});
|
||||||
|
|
||||||
JoinHandle { pid, consumed: false }
|
JoinHandle {
|
||||||
|
pid,
|
||||||
|
consumed: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`spawn`] that reports a full actor slab instead of panicking.
|
||||||
|
///
|
||||||
|
/// Behaviour parity with [`spawn`] in every case except one: when the fixed
|
||||||
|
/// slab ([`Config::max_actors`](crate::runtime::Config::max_actors)) is
|
||||||
|
/// full, this returns [`Err(SpawnError::AtCapacity)`](SpawnError::AtCapacity)
|
||||||
|
/// where `spawn` panics the calling actor. Use it at load-shedding call
|
||||||
|
/// sites — an accept loop spawning one actor per connection, a request
|
||||||
|
/// admission point — where "at capacity" is a routine overload condition to
|
||||||
|
/// handle (reject the unit of work), not an invariant violation. Internal
|
||||||
|
/// and bounded spawn sites should keep [`spawn`]: there, the panic is a
|
||||||
|
/// correct loud invariant check.
|
||||||
|
///
|
||||||
|
/// The claim is atomic (claim-or-report): there is no
|
||||||
|
/// check-then-spawn race against other spawners for the last slot, so no
|
||||||
|
/// headroom margin is needed.
|
||||||
|
pub fn try_spawn(f: impl FnOnce() + Send + 'static) -> Result<JoinHandle, SpawnError> {
|
||||||
|
let parent = current_pid().unwrap_or_else(|| with_runtime(|_| crate::runtime::ROOT_PID));
|
||||||
|
try_spawn_under_with(parent, SpawnOpts::default(), f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`try_spawn`] with an explicit supervisor and per-actor stack shape
|
||||||
|
/// overrides — the full-control core the other `try_` surface is built on
|
||||||
|
/// (mirrors [`spawn_under_with`]).
|
||||||
|
pub fn try_spawn_under_with<A>(
|
||||||
|
supervisor: Pid<A>,
|
||||||
|
opts: SpawnOpts,
|
||||||
|
f: impl FnOnce() + Send + 'static,
|
||||||
|
) -> Result<JoinHandle, SpawnError> {
|
||||||
|
let supervisor = supervisor.erase();
|
||||||
|
// Slot FIRST — deliberately the reverse of `spawn`'s stack-first order:
|
||||||
|
// under overload the Err arm is the HOT path, and a rejection must cost
|
||||||
|
// one mutex pop, not an mmap/pool-pop + init + recycle per shed unit of
|
||||||
|
// work. The claim is a single atomic pop (no TOCTOU; see
|
||||||
|
// `try_allocate_slot`).
|
||||||
|
let idx = match with_runtime(|inner| inner.try_allocate_slot()) {
|
||||||
|
Some(idx) => idx,
|
||||||
|
None => return Err(SpawnError::AtCapacity),
|
||||||
|
};
|
||||||
|
// Between claim and install the slot is owned by this frame alone; if
|
||||||
|
// stack allocation panics in that window the slot must go back or it
|
||||||
|
// leaks for the life of the runtime (and would trip the run()-teardown
|
||||||
|
// slot-leak debug_assert).
|
||||||
|
struct ReturnOnUnwind(Option<u32>);
|
||||||
|
impl Drop for ReturnOnUnwind {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(idx) = self.0 {
|
||||||
|
with_runtime(|inner| inner.return_vacant_slot(idx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut claimed = ReturnOnUnwind(Some(idx));
|
||||||
|
|
||||||
|
let stack = with_runtime(|inner| crate::runtime::acquire_stack(inner, opts));
|
||||||
|
let sp = init_actor_stack(stack.top(), crate::actor::trampoline);
|
||||||
|
let closure: crate::runtime::Closure = Box::new(f);
|
||||||
|
|
||||||
|
claimed.0 = None; // install_actor takes ownership of the slot from here
|
||||||
|
let pid = with_runtime(|inner| {
|
||||||
|
crate::runtime::install_actor(inner, idx, sp, stack, supervisor, closure)
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(JoinHandle {
|
||||||
|
pid,
|
||||||
|
consumed: false,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawn an actor that other actors can message directly by its [`Pid<A>`],
|
/// Spawn an actor that other actors can message directly by its [`Pid<A>`],
|
||||||
@@ -566,11 +662,9 @@ pub fn sleep(duration: std::time::Duration) {
|
|||||||
let _np = NoPreempt::enter();
|
let _np = NoPreempt::enter();
|
||||||
let epoch = begin_wait();
|
let epoch = begin_wait();
|
||||||
let deadline = crate::timer::deadline_from_now(duration);
|
let deadline = crate::timer::deadline_from_now(duration);
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| match inner.timers.lock() {
|
||||||
match inner.timers.lock() {
|
Ok(mut timers) => timers.insert_sleep(deadline, me, epoch),
|
||||||
Ok(mut timers) => timers.insert_sleep(deadline, me, epoch),
|
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
park_current();
|
park_current();
|
||||||
}
|
}
|
||||||
@@ -588,11 +682,9 @@ pub fn sleep_wall(duration: std::time::Duration) {
|
|||||||
let _np = NoPreempt::enter();
|
let _np = NoPreempt::enter();
|
||||||
let epoch = begin_wait();
|
let epoch = begin_wait();
|
||||||
let deadline = crate::timer::deadline_from_now(duration);
|
let deadline = crate::timer::deadline_from_now(duration);
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| match inner.timers.lock() {
|
||||||
match inner.timers.lock() {
|
Ok(mut timers) => timers.insert_sleep_wall(deadline, me, epoch),
|
||||||
Ok(mut timers) => timers.insert_sleep_wall(deadline, me, epoch),
|
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
park_current();
|
park_current();
|
||||||
}
|
}
|
||||||
@@ -608,15 +700,13 @@ pub fn insert_wait_timer(
|
|||||||
target: std::sync::Arc<dyn crate::timer::TimerTarget>,
|
target: std::sync::Arc<dyn crate::timer::TimerTarget>,
|
||||||
epoch: u32,
|
epoch: u32,
|
||||||
) {
|
) {
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| match inner.timers.lock() {
|
||||||
match inner.timers.lock() {
|
Ok(mut timers) => timers.insert(
|
||||||
Ok(mut timers) => timers.insert(
|
deadline,
|
||||||
deadline,
|
pid,
|
||||||
pid,
|
crate::timer::Reason::WaitTimeout { target, epoch },
|
||||||
crate::timer::Reason::WaitTimeout { target, epoch },
|
),
|
||||||
),
|
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,11 +736,9 @@ pub fn send_after<A: crate::pid::Addressable>(
|
|||||||
let fire = Box::new(move || {
|
let fire = Box::new(move || {
|
||||||
let _ = crate::registry::send_to(dest, msg);
|
let _ = crate::registry::send_to(dest, msg);
|
||||||
});
|
});
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| match inner.timers.lock() {
|
||||||
match inner.timers.lock() {
|
Ok(mut timers) => timers.insert_send(deadline, dest.erase(), fire),
|
||||||
Ok(mut timers) => timers.insert_send(deadline, dest.erase(), fire),
|
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -670,11 +758,9 @@ pub fn send_after_named<M: Send + 'static>(
|
|||||||
let fire = Box::new(move || {
|
let fire = Box::new(move || {
|
||||||
let _ = crate::registry::send(dest, msg);
|
let _ = crate::registry::send(dest, msg);
|
||||||
});
|
});
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| match inner.timers.lock() {
|
||||||
match inner.timers.lock() {
|
Ok(mut timers) => timers.insert_send(deadline, armed_by, fire),
|
||||||
Ok(mut timers) => timers.insert_send(deadline, armed_by, fire),
|
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -693,11 +779,9 @@ pub fn send_after_wall<A: crate::pid::Addressable>(
|
|||||||
let fire = Box::new(move || {
|
let fire = Box::new(move || {
|
||||||
let _ = crate::registry::send_to(dest, msg);
|
let _ = crate::registry::send_to(dest, msg);
|
||||||
});
|
});
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| match inner.timers.lock() {
|
||||||
match inner.timers.lock() {
|
Ok(mut timers) => timers.insert_send_wall(deadline, dest.erase(), fire),
|
||||||
Ok(mut timers) => timers.insert_send_wall(deadline, dest.erase(), fire),
|
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -714,11 +798,9 @@ pub fn send_after_named_wall<M: Send + 'static>(
|
|||||||
let fire = Box::new(move || {
|
let fire = Box::new(move || {
|
||||||
let _ = crate::registry::send(dest, msg);
|
let _ = crate::registry::send(dest, msg);
|
||||||
});
|
});
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| match inner.timers.lock() {
|
||||||
match inner.timers.lock() {
|
Ok(mut timers) => timers.insert_send_wall(deadline, armed_by, fire),
|
||||||
Ok(mut timers) => timers.insert_send_wall(deadline, armed_by, fire),
|
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -738,11 +820,9 @@ pub(crate) fn send_after_to<T: Send + 'static>(
|
|||||||
let fire = Box::new(move || {
|
let fire = Box::new(move || {
|
||||||
let _ = tx.send(msg);
|
let _ = tx.send(msg);
|
||||||
});
|
});
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| match inner.timers.lock() {
|
||||||
match inner.timers.lock() {
|
Ok(mut timers) => timers.insert_send(deadline, armed_by, fire),
|
||||||
Ok(mut timers) => timers.insert_send(deadline, armed_by, fire),
|
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -750,11 +830,9 @@ pub(crate) fn send_after_to<T: Send + 'static>(
|
|||||||
/// it fires. Returns `true` if the timer was still pending and delivery is
|
/// it fires. Returns `true` if the timer was still pending and delivery is
|
||||||
/// now prevented, `false` if it had already fired or was already cancelled.
|
/// now prevented, `false` if it had already fired or was already cancelled.
|
||||||
pub fn cancel_timer(id: crate::timer::TimerId) -> bool {
|
pub fn cancel_timer(id: crate::timer::TimerId) -> bool {
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| match inner.timers.lock() {
|
||||||
match inner.timers.lock() {
|
Ok(mut timers) => timers.cancel(id),
|
||||||
Ok(mut timers) => timers.cancel(id),
|
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -820,7 +898,8 @@ where
|
|||||||
};
|
};
|
||||||
let mut cold = slot.cold.lock();
|
let mut cold = slot.cold.lock();
|
||||||
debug_assert_eq!(
|
debug_assert_eq!(
|
||||||
slot.generation(), me.generation(),
|
slot.generation(),
|
||||||
|
me.generation(),
|
||||||
"block_on_io: own slot reused mid-park"
|
"block_on_io: own slot reused mid-park"
|
||||||
);
|
);
|
||||||
match cold.pending_io_result.take() {
|
match cold.pending_io_result.take() {
|
||||||
@@ -946,12 +1025,20 @@ pub struct FdArm {
|
|||||||
impl FdArm {
|
impl FdArm {
|
||||||
/// An arm that becomes ready when `fd` is readable.
|
/// An arm that becomes ready when `fd` is readable.
|
||||||
pub fn readable(fd: std::os::fd::RawFd) -> Self {
|
pub fn readable(fd: std::os::fd::RawFd) -> Self {
|
||||||
FdArm { fd, readable: true, writable: false }
|
FdArm {
|
||||||
|
fd,
|
||||||
|
readable: true,
|
||||||
|
writable: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An arm that becomes ready when `fd` is writable.
|
/// An arm that becomes ready when `fd` is writable.
|
||||||
pub fn writable(fd: std::os::fd::RawFd) -> Self {
|
pub fn writable(fd: std::os::fd::RawFd) -> Self {
|
||||||
FdArm { fd, readable: false, writable: true }
|
FdArm {
|
||||||
|
fd,
|
||||||
|
readable: false,
|
||||||
|
writable: true,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -976,9 +1063,7 @@ impl crate::channel::Selectable for FdArm {
|
|||||||
match io.as_mut() {
|
match io.as_mut() {
|
||||||
Some(io) => {
|
Some(io) => {
|
||||||
inner.io_fd_waiters.fetch_add(1, Ordering::AcqRel);
|
inner.io_fd_waiters.fetch_add(1, Ordering::AcqRel);
|
||||||
let r = io.epoll_register(
|
let r = io.epoll_register(self.fd, pid, epoch, self.readable, self.writable);
|
||||||
self.fd, pid, epoch, self.readable, self.writable,
|
|
||||||
);
|
|
||||||
if r.is_err() {
|
if r.is_err() {
|
||||||
inner.io_fd_waiters.fetch_sub(1, Ordering::AcqRel);
|
inner.io_fd_waiters.fetch_sub(1, Ordering::AcqRel);
|
||||||
}
|
}
|
||||||
@@ -1033,7 +1118,11 @@ fn poll_events(fd: std::os::fd::RawFd, readable: bool, writable: bool) -> std::i
|
|||||||
if writable {
|
if writable {
|
||||||
events |= libc::POLLOUT;
|
events |= libc::POLLOUT;
|
||||||
}
|
}
|
||||||
let mut pfd = libc::pollfd { fd, events, revents: 0 };
|
let mut pfd = libc::pollfd {
|
||||||
|
fd,
|
||||||
|
events,
|
||||||
|
revents: 0,
|
||||||
|
};
|
||||||
loop {
|
loop {
|
||||||
let r = unsafe { libc::poll(&mut pfd, 1, 0) };
|
let r = unsafe { libc::poll(&mut pfd, 1, 0) };
|
||||||
if r < 0 {
|
if r < 0 {
|
||||||
@@ -1081,7 +1170,11 @@ pub fn wait_writable_timeout(
|
|||||||
pub fn read(fd: std::os::fd::RawFd, buf: &mut [u8]) -> std::io::Result<usize> {
|
pub fn read(fd: std::os::fd::RawFd, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||||
wait_readable(fd)?;
|
wait_readable(fd)?;
|
||||||
let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, buf.len()) };
|
let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, buf.len()) };
|
||||||
if n < 0 { Err(std::io::Error::last_os_error()) } else { Ok(n as usize) }
|
if n < 0 {
|
||||||
|
Err(std::io::Error::last_os_error())
|
||||||
|
} else {
|
||||||
|
Ok(n as usize)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience wrapper: park until `fd` is writable, then perform the
|
/// Convenience wrapper: park until `fd` is writable, then perform the
|
||||||
@@ -1090,7 +1183,11 @@ pub fn read(fd: std::os::fd::RawFd, buf: &mut [u8]) -> std::io::Result<usize> {
|
|||||||
pub fn write(fd: std::os::fd::RawFd, buf: &[u8]) -> std::io::Result<usize> {
|
pub fn write(fd: std::os::fd::RawFd, buf: &[u8]) -> std::io::Result<usize> {
|
||||||
wait_writable(fd)?;
|
wait_writable(fd)?;
|
||||||
let n = unsafe { libc::write(fd, buf.as_ptr() as *const _, buf.len()) };
|
let n = unsafe { libc::write(fd, buf.as_ptr() as *const _, buf.len()) };
|
||||||
if n < 0 { Err(std::io::Error::last_os_error()) } else { Ok(n as usize) }
|
if n < 0 {
|
||||||
|
Err(std::io::Error::last_os_error())
|
||||||
|
} else {
|
||||||
|
Ok(n as usize)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -1099,12 +1196,15 @@ pub fn write(fd: std::os::fd::RawFd, buf: &[u8]) -> std::io::Result<usize> {
|
|||||||
|
|
||||||
pub fn register_supervisor_channel(pid: Pid, sender: Sender<Signal>) {
|
pub fn register_supervisor_channel(pid: Pid, sender: Sender<Signal>) {
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| {
|
||||||
let slot = inner.slot_at(pid)
|
let slot = inner
|
||||||
|
.slot_at(pid)
|
||||||
.unwrap_or_else(|| panic!("register_supervisor_channel: pid {:?} not found", pid));
|
.unwrap_or_else(|| panic!("register_supervisor_channel: pid {:?} not found", pid));
|
||||||
let mut cold = slot.cold.lock();
|
let mut cold = slot.cold.lock();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
slot.generation(), pid.generation(),
|
slot.generation(),
|
||||||
"register_supervisor_channel: pid {:?} not found", pid
|
pid.generation(),
|
||||||
|
"register_supervisor_channel: pid {:?} not found",
|
||||||
|
pid
|
||||||
);
|
);
|
||||||
cold.supervisor_channel = Some(sender);
|
cold.supervisor_channel = Some(sender);
|
||||||
});
|
});
|
||||||
@@ -1177,6 +1277,9 @@ mod send_after_to_tests {
|
|||||||
crate::sleep(Duration::from_millis(30));
|
crate::sleep(Duration::from_millis(30));
|
||||||
r2.store(true, Ordering::SeqCst);
|
r2.store(true, Ordering::SeqCst);
|
||||||
});
|
});
|
||||||
assert!(reached.load(Ordering::SeqCst), "runtime survived the dead-channel fire");
|
assert!(
|
||||||
|
reached.load(Ordering::SeqCst),
|
||||||
|
"runtime survived the dead-channel fire"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-3
@@ -214,7 +214,10 @@ struct Buf {
|
|||||||
|
|
||||||
impl Buf {
|
impl Buf {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Buf { b: [0; 320], len: 0 }
|
Buf {
|
||||||
|
b: [0; 320],
|
||||||
|
len: 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fn s(&mut self, s: &str) {
|
fn s(&mut self, s: &str) {
|
||||||
for &c in s.as_bytes() {
|
for &c in s.as_bytes() {
|
||||||
@@ -274,8 +277,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn inside_guard_both_edges() {
|
fn inside_guard_both_edges() {
|
||||||
assert_eq!(classify(GUARD_LO, TOP, RESERVE, GUARD), FaultClass::Guard);
|
assert_eq!(classify(GUARD_LO, TOP, RESERVE, GUARD), FaultClass::Guard);
|
||||||
assert_eq!(classify(GUARD_HI - 1, TOP, RESERVE, GUARD), FaultClass::Guard);
|
assert_eq!(
|
||||||
assert_eq!(classify(GUARD_LO + GUARD / 2, TOP, RESERVE, GUARD), FaultClass::Guard);
|
classify(GUARD_HI - 1, TOP, RESERVE, GUARD),
|
||||||
|
FaultClass::Guard
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
classify(GUARD_LO + GUARD / 2, TOP, RESERVE, GUARD),
|
||||||
|
FaultClass::Guard
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+9
-9
@@ -188,8 +188,7 @@ impl StateWord {
|
|||||||
loop {
|
loop {
|
||||||
let w = self.load();
|
let w = self.load();
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
matches!(word_state(w), ST_RUNNING | ST_RUNNING_NOTIFIED)
|
matches!(word_state(w), ST_RUNNING | ST_RUNNING_NOTIFIED) && word_gen(w) == gen,
|
||||||
&& word_gen(w) == gen,
|
|
||||||
"yield return from invalid word {w:#x}"
|
"yield return from invalid word {w:#x}"
|
||||||
);
|
);
|
||||||
if self
|
if self
|
||||||
@@ -247,8 +246,7 @@ impl StateWord {
|
|||||||
loop {
|
loop {
|
||||||
let w = self.load();
|
let w = self.load();
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
matches!(word_state(w), ST_RUNNING | ST_RUNNING_NOTIFIED)
|
matches!(word_state(w), ST_RUNNING | ST_RUNNING_NOTIFIED) && word_gen(w) == gen,
|
||||||
&& word_gen(w) == gen,
|
|
||||||
"begin_wait from invalid word {w:#x}"
|
"begin_wait from invalid word {w:#x}"
|
||||||
);
|
);
|
||||||
let next = word_epoch(w).wrapping_add(1) & EPOCH_MASK;
|
let next = word_epoch(w).wrapping_add(1) & EPOCH_MASK;
|
||||||
@@ -342,8 +340,7 @@ impl StateWord {
|
|||||||
loop {
|
loop {
|
||||||
let w = self.load();
|
let w = self.load();
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
matches!(word_state(w), ST_RUNNING | ST_RUNNING_NOTIFIED)
|
matches!(word_state(w), ST_RUNNING | ST_RUNNING_NOTIFIED) && word_gen(w) == gen,
|
||||||
&& word_gen(w) == gen,
|
|
||||||
"clear_notify from invalid word {w:#x}"
|
"clear_notify from invalid word {w:#x}"
|
||||||
);
|
);
|
||||||
if word_state(w) != ST_RUNNING_NOTIFIED {
|
if word_state(w) != ST_RUNNING_NOTIFIED {
|
||||||
@@ -372,8 +369,7 @@ impl StateWord {
|
|||||||
pub(crate) fn set_done(&self, gen: u32) {
|
pub(crate) fn set_done(&self, gen: u32) {
|
||||||
let prev = self.0.swap(pack(gen, 0, ST_DONE), Ordering::AcqRel);
|
let prev = self.0.swap(pack(gen, 0, ST_DONE), Ordering::AcqRel);
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
matches!(word_state(prev), ST_RUNNING | ST_RUNNING_NOTIFIED)
|
matches!(word_state(prev), ST_RUNNING | ST_RUNNING_NOTIFIED) && word_gen(prev) == gen,
|
||||||
&& word_gen(prev) == gen,
|
|
||||||
"finalize from invalid word {prev:#x}"
|
"finalize from invalid word {prev:#x}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -538,7 +534,11 @@ mod loom_tests {
|
|||||||
// not a pending notification.
|
// not a pending notification.
|
||||||
assert!(word.try_claim(0));
|
assert!(word.try_claim(0));
|
||||||
assert_eq!(word.unpark(0, Some(epoch)), Unpark::Noop);
|
assert_eq!(word.unpark(0, Some(epoch)), Unpark::Noop);
|
||||||
assert_eq!(word_state(word.load()), ST_RUNNING, "stale epoch notified a live run");
|
assert_eq!(
|
||||||
|
word_state(word.load()),
|
||||||
|
ST_RUNNING,
|
||||||
|
"stale epoch notified a live run"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-5
@@ -57,16 +57,19 @@ impl Stack {
|
|||||||
}
|
}
|
||||||
let base = base as *mut u8;
|
let base = base as *mut u8;
|
||||||
|
|
||||||
let ret = unsafe {
|
let ret = unsafe { libc::mprotect(base as *mut libc::c_void, guard_size, libc::PROT_NONE) };
|
||||||
libc::mprotect(base as *mut libc::c_void, guard_size, libc::PROT_NONE)
|
|
||||||
};
|
|
||||||
if ret != 0 {
|
if ret != 0 {
|
||||||
let err = io::Error::last_os_error();
|
let err = io::Error::last_os_error();
|
||||||
unsafe { libc::munmap(base as *mut libc::c_void, total_size) };
|
unsafe { libc::munmap(base as *mut libc::c_void, total_size) };
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self { base, total_size, stack_size, guard_size })
|
Ok(Self {
|
||||||
|
base,
|
||||||
|
total_size,
|
||||||
|
stack_size,
|
||||||
|
guard_size,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 16-byte-aligned top of the usable region.
|
/// 16-byte-aligned top of the usable region.
|
||||||
@@ -177,7 +180,11 @@ pub(crate) fn shrink_range(hwm: usize, sp: usize, page: usize) -> Option<(usize,
|
|||||||
/// with `stack_size` page-rounded by `Stack::new` the result is always
|
/// with `stack_size` page-rounded by `Stack::new` the result is always
|
||||||
/// page-aligned. Checked math: `retain ≥ stack_size` (notably the default
|
/// page-aligned. Checked math: `retain ≥ stack_size` (notably the default
|
||||||
/// 64 KiB reserve with the 64 KiB RETAIN) and overflow collapse to `None`.
|
/// 64 KiB reserve with the 64 KiB RETAIN) and overflow collapse to `None`.
|
||||||
pub(crate) fn retain_range(stack_size: usize, retain: usize, page: usize) -> Option<(usize, usize)> {
|
pub(crate) fn retain_range(
|
||||||
|
stack_size: usize,
|
||||||
|
retain: usize,
|
||||||
|
page: usize,
|
||||||
|
) -> Option<(usize, usize)> {
|
||||||
debug_assert!(page.is_power_of_two());
|
debug_assert!(page.is_power_of_two());
|
||||||
let retain = retain.checked_add(page - 1)? & !(page - 1); // page_up(retain)
|
let retain = retain.checked_add(page - 1)? & !(page - 1); // page_up(retain)
|
||||||
let len = stack_size.checked_sub(retain)?;
|
let len = stack_size.checked_sub(retain)?;
|
||||||
|
|||||||
+8
-6
@@ -123,9 +123,9 @@ pub enum Signal {
|
|||||||
impl std::fmt::Debug for Signal {
|
impl std::fmt::Debug for Signal {
|
||||||
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 {
|
||||||
Signal::Exit(pid) => write!(f, "Signal::Exit({:?})", pid),
|
Signal::Exit(pid) => write!(f, "Signal::Exit({:?})", pid),
|
||||||
Signal::Panic(pid, _) => write!(f, "Signal::Panic({:?}, ..)", pid),
|
Signal::Panic(pid, _) => write!(f, "Signal::Panic({:?}, ..)", pid),
|
||||||
Signal::Stopped(pid) => write!(f, "Signal::Stopped({:?})", pid),
|
Signal::Stopped(pid) => write!(f, "Signal::Stopped({:?})", pid),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,9 +133,9 @@ impl std::fmt::Debug for Signal {
|
|||||||
impl Signal {
|
impl Signal {
|
||||||
pub fn pid(&self) -> Pid {
|
pub fn pid(&self) -> Pid {
|
||||||
match self {
|
match self {
|
||||||
Signal::Exit(p) => *p,
|
Signal::Exit(p) => *p,
|
||||||
Signal::Panic(p, _) => *p,
|
Signal::Panic(p, _) => *p,
|
||||||
Signal::Stopped(p) => *p,
|
Signal::Stopped(p) => *p,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,7 +169,10 @@ pub struct ChildSpec {
|
|||||||
|
|
||||||
impl ChildSpec {
|
impl ChildSpec {
|
||||||
pub fn new(restart: Restart, start: impl Fn() + Send + Sync + 'static) -> Self {
|
pub fn new(restart: Restart, start: impl Fn() + Send + Sync + 'static) -> Self {
|
||||||
Self { start: Arc::new(start), restart }
|
Self {
|
||||||
|
start: Arc::new(start),
|
||||||
|
restart,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,4 +395,3 @@ impl OneForOne {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -129,7 +129,9 @@ impl Ord for Entry {
|
|||||||
// Earlier deadline first; ties broken by insertion order so the
|
// Earlier deadline first; ties broken by insertion order so the
|
||||||
// ordering is total. `Reason` and `Pid` deliberately don't
|
// ordering is total. `Reason` and `Pid` deliberately don't
|
||||||
// participate.
|
// participate.
|
||||||
self.deadline.cmp(&other.deadline).then_with(|| self.seq.cmp(&other.seq))
|
self.deadline
|
||||||
|
.cmp(&other.deadline)
|
||||||
|
.then_with(|| self.seq.cmp(&other.seq))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+44
-28
@@ -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")]
|
||||||
@@ -68,8 +72,8 @@ mod inner {
|
|||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
struct Record {
|
struct Record {
|
||||||
nanos: u64, // ns since open()
|
nanos: u64, // ns since open()
|
||||||
tid: u64, // OS thread id
|
tid: u64, // OS thread id
|
||||||
event: Event,
|
event: Event,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,8 +88,8 @@ mod inner {
|
|||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
struct Global {
|
struct Global {
|
||||||
sender: mpsc::Sender<Msg>,
|
sender: mpsc::Sender<Msg>,
|
||||||
start: Instant,
|
start: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
static GLOBAL: Mutex<Option<Global>> = Mutex::new(None);
|
static GLOBAL: Mutex<Option<Global>> = Mutex::new(None);
|
||||||
@@ -95,7 +99,7 @@ mod inner {
|
|||||||
// The start Instant is copied alongside it — also one mutex hit per thread.
|
// The start Instant is copied alongside it — also one mutex hit per thread.
|
||||||
// record() never touches GLOBAL after that.
|
// record() never touches GLOBAL after that.
|
||||||
struct LocalState {
|
struct LocalState {
|
||||||
tx: mpsc::Sender<Msg>,
|
tx: mpsc::Sender<Msg>,
|
||||||
start: Instant,
|
start: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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();
|
||||||
@@ -182,7 +189,7 @@ mod inner {
|
|||||||
}
|
}
|
||||||
if let Some(ls) = opt.as_ref() {
|
if let Some(ls) = opt.as_ref() {
|
||||||
let nanos = ls.start.elapsed().as_nanos() as u64;
|
let nanos = ls.start.elapsed().as_nanos() as u64;
|
||||||
let tid = os_tid();
|
let tid = os_tid();
|
||||||
let _ = ls.tx.send(Msg::Event(Record { nanos, tid, event }));
|
let _ = ls.tx.send(Msg::Event(Record { nanos, tid, event }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -197,7 +204,10 @@ mod inner {
|
|||||||
fn drain_thread(rx: mpsc::Receiver<Msg>, path: &str) {
|
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,27 +246,31 @@ 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::Yield(p) => ("yield".into(), p.index()),
|
Event::Resume(p) => ("resume".into(), p.index()),
|
||||||
Event::Park(p) => ("park".into(), p.index()),
|
Event::Yield(p) => ("yield".into(), p.index()),
|
||||||
Event::Done(p) => ("done".into(), p.index()),
|
Event::Park(p) => ("park".into(), p.index()),
|
||||||
Event::UnparkDirect(p) => ("unpark_direct".into(), p.index()),
|
Event::Done(p) => ("done".into(), p.index()),
|
||||||
Event::UnparkDeferred(p) => ("unpark_deferred".into(), p.index()),
|
Event::UnparkDirect(p) => ("unpark_direct".into(), p.index()),
|
||||||
|
Event::UnparkDeferred(p) => ("unpark_deferred".into(), p.index()),
|
||||||
Event::UnparkFlagConsumed(p) => ("unpark_flag_consumed".into(), p.index()),
|
Event::UnparkFlagConsumed(p) => ("unpark_flag_consumed".into(), p.index()),
|
||||||
Event::Send { sender, receiver } => (
|
Event::Send { sender, receiver } => (
|
||||||
format!("send rx={}", receiver
|
format!(
|
||||||
.map(|p| p.index().to_string())
|
"send rx={}",
|
||||||
.unwrap_or_else(|| "none".into())),
|
receiver
|
||||||
|
.map(|p| p.index().to_string())
|
||||||
|
.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()),
|
||||||
Event::RecvWake(p) => ("recv_wake".into(), p.index()),
|
Event::RecvWake(p) => ("recv_wake".into(), p.index()),
|
||||||
Event::Enqueue(p) => ("enqueue".into(), p.index()),
|
Event::Enqueue(p) => ("enqueue".into(), p.index()),
|
||||||
Event::Dequeue(p) => ("dequeue".into(), p.index()),
|
Event::Dequeue(p) => ("dequeue".into(), p.index()),
|
||||||
Event::SlotPush(p) => ("slot_push".into(), p.index()),
|
Event::SlotPush(p) => ("slot_push".into(), p.index()),
|
||||||
Event::SlotPop(p) => ("slot_pop".into(), p.index()),
|
Event::SlotPop(p) => ("slot_pop".into(), p.index()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+24
-6
@@ -49,8 +49,14 @@ fn looping_actor_on_check_is_stopped() {
|
|||||||
}
|
}
|
||||||
let _ = h.join();
|
let _ = h.join();
|
||||||
});
|
});
|
||||||
assert!(saw_stopped.load(Ordering::SeqCst), "expected DownReason::Stopped");
|
assert!(
|
||||||
assert!(dropped.load(Ordering::SeqCst), "Drop guard must run during the cancellation unwind");
|
saw_stopped.load(Ordering::SeqCst),
|
||||||
|
"expected DownReason::Stopped"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
dropped.load(Ordering::SeqCst),
|
||||||
|
"Drop guard must run during the cancellation unwind"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -79,8 +85,14 @@ fn parked_on_recv_actor_is_stopped() {
|
|||||||
}
|
}
|
||||||
let _ = h.join();
|
let _ = h.join();
|
||||||
});
|
});
|
||||||
assert!(saw_stopped.load(Ordering::SeqCst), "expected DownReason::Stopped");
|
assert!(
|
||||||
assert!(dropped.load(Ordering::SeqCst), "Drop guard must run on cancellation of a parked actor");
|
saw_stopped.load(Ordering::SeqCst),
|
||||||
|
"expected DownReason::Stopped"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
dropped.load(Ordering::SeqCst),
|
||||||
|
"Drop guard must run on cancellation of a parked actor"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -185,6 +197,12 @@ fn stop_flagged_while_queued_lands_at_first_park() {
|
|||||||
.recv_timeout(Duration::from_secs(10))
|
.recv_timeout(Duration::from_secs(10))
|
||||||
.expect("runtime deadlocked: stop against a QUEUED actor was lost at its first park");
|
.expect("runtime deadlocked: stop against a QUEUED actor was lost at its first park");
|
||||||
|
|
||||||
assert!(saw_stopped.load(Ordering::SeqCst), "expected DownReason::Stopped");
|
assert!(
|
||||||
assert!(dropped.load(Ordering::SeqCst), "Drop guard must run during the cancellation unwind");
|
saw_stopped.load(Ordering::SeqCst),
|
||||||
|
"expected DownReason::Stopped"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
dropped.load(Ordering::SeqCst),
|
||||||
|
"Drop guard must run during the cancellation unwind"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-26
@@ -24,7 +24,11 @@ fn progress_point_counts() {
|
|||||||
h.join().unwrap();
|
h.join().unwrap();
|
||||||
let after = smarm::causal::progress_snapshot();
|
let after = smarm::causal::progress_snapshot();
|
||||||
let delta = |name: &str| {
|
let delta = |name: &str| {
|
||||||
after.iter().find(|(n, _)| n == name).map(|(_, c)| *c).unwrap()
|
after
|
||||||
|
.iter()
|
||||||
|
.find(|(n, _)| n == name)
|
||||||
|
.map(|(_, c)| *c)
|
||||||
|
.unwrap()
|
||||||
- before
|
- before
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(n, _)| n == name)
|
.find(|(n, _)| n == name)
|
||||||
@@ -45,21 +49,12 @@ fn site_guard_nesting_restores() {
|
|||||||
assert_eq!(smarm::causal::current_site_name(), None);
|
assert_eq!(smarm::causal::current_site_name(), None);
|
||||||
{
|
{
|
||||||
let _outer = smarm::causal_site!("outer");
|
let _outer = smarm::causal_site!("outer");
|
||||||
assert_eq!(
|
assert_eq!(smarm::causal::current_site_name().as_deref(), Some("outer"));
|
||||||
smarm::causal::current_site_name().as_deref(),
|
|
||||||
Some("outer")
|
|
||||||
);
|
|
||||||
{
|
{
|
||||||
let _inner = smarm::causal_site!("inner");
|
let _inner = smarm::causal_site!("inner");
|
||||||
assert_eq!(
|
assert_eq!(smarm::causal::current_site_name().as_deref(), Some("inner"));
|
||||||
smarm::causal::current_site_name().as_deref(),
|
|
||||||
Some("inner")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
assert_eq!(
|
assert_eq!(smarm::causal::current_site_name().as_deref(), Some("outer"));
|
||||||
smarm::causal::current_site_name().as_deref(),
|
|
||||||
Some("outer")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
assert_eq!(smarm::causal::current_site_name(), None);
|
assert_eq!(smarm::causal::current_site_name(), None);
|
||||||
});
|
});
|
||||||
@@ -88,10 +83,7 @@ fn virtual_speedup_ledger() {
|
|||||||
let bystander = smarm::spawn(move || {
|
let bystander = smarm::spawn(move || {
|
||||||
while !stop2.load(Ordering::Relaxed) {
|
while !stop2.load(Ordering::Relaxed) {
|
||||||
smarm::check!();
|
smarm::check!();
|
||||||
out2.store(
|
out2.store(smarm::causal::my_absorbed_delay_cycles(), Ordering::Relaxed);
|
||||||
smarm::causal::my_absorbed_delay_cycles(),
|
|
||||||
Ordering::Relaxed,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -166,10 +158,7 @@ fn runnable_bystander_pays_delay() {
|
|||||||
while !stop_b.load(Ordering::Relaxed) {
|
while !stop_b.load(Ordering::Relaxed) {
|
||||||
iters2.fetch_add(1, Ordering::Relaxed);
|
iters2.fetch_add(1, Ordering::Relaxed);
|
||||||
smarm::check!();
|
smarm::check!();
|
||||||
absorbed2.store(
|
absorbed2.store(smarm::causal::my_absorbed_delay_cycles(), Ordering::Relaxed);
|
||||||
smarm::causal::my_absorbed_delay_cycles(),
|
|
||||||
Ordering::Relaxed,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -177,8 +166,7 @@ fn runnable_bystander_pays_delay() {
|
|||||||
let i0 = iters.load(Ordering::Relaxed);
|
let i0 = iters.load(Ordering::Relaxed);
|
||||||
let t = std::time::Instant::now();
|
let t = std::time::Instant::now();
|
||||||
smarm::sleep(Duration::from_millis(150));
|
smarm::sleep(Duration::from_millis(150));
|
||||||
let rate =
|
let rate = (iters.load(Ordering::Relaxed) - i0) as f64 / t.elapsed().as_secs_f64();
|
||||||
(iters.load(Ordering::Relaxed) - i0) as f64 / t.elapsed().as_secs_f64();
|
|
||||||
out.store(rate as u64, Ordering::Relaxed);
|
out.store(rate as u64, Ordering::Relaxed);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -448,7 +436,10 @@ fn timer_deadline_shifts_with_injected_delay() {
|
|||||||
|
|
||||||
// Raw deadline passed, effective deadline not: nothing fires, entry kept.
|
// Raw deadline passed, effective deadline not: nothing fires, entry kept.
|
||||||
assert!(t.pop_due(now + Duration::from_millis(60)).is_empty());
|
assert!(t.pop_due(now + Duration::from_millis(60)).is_empty());
|
||||||
assert!(!t.is_empty(), "shifted entry must be re-queued, not dropped");
|
assert!(
|
||||||
|
!t.is_empty(),
|
||||||
|
"shifted entry must be re-queued, not dropped"
|
||||||
|
);
|
||||||
|
|
||||||
// Past raw + injected (with margin) it must fire. Chase in case a
|
// Past raw + injected (with margin) it must fire. Chase in case a
|
||||||
// parallel test injected more debt meanwhile.
|
// parallel test injected more debt meanwhile.
|
||||||
@@ -503,7 +494,11 @@ fn wall_timer_ignores_injected_delay() {
|
|||||||
// Just past the raw deadline: the wall entry fires, the virtual one is
|
// Just past the raw deadline: the wall entry fires, the virtual one is
|
||||||
// re-queued at its shifted deadline.
|
// re-queued at its shifted deadline.
|
||||||
let due = t.pop_due(now + Duration::from_millis(60));
|
let due = t.pop_due(now + Duration::from_millis(60));
|
||||||
assert_eq!(due.len(), 1, "exactly the wall entry must fire at raw deadline");
|
assert_eq!(
|
||||||
|
due.len(),
|
||||||
|
1,
|
||||||
|
"exactly the wall entry must fire at raw deadline"
|
||||||
|
);
|
||||||
assert_eq!(due[0].pid, Pid::new(0, 0));
|
assert_eq!(due[0].pid, Pid::new(0, 0));
|
||||||
assert!(!t.is_empty(), "virtual sibling must remain queued, shifted");
|
assert!(!t.is_empty(), "virtual sibling must remain queued, shifted");
|
||||||
}
|
}
|
||||||
@@ -623,7 +618,11 @@ fn wall_send_after_ignores_injected_delay() {
|
|||||||
|
|
||||||
// Just past the raw deadline: only the wall send pops; run its thunk.
|
// Just past the raw deadline: only the wall send pops; run its thunk.
|
||||||
let due = t.pop_due(now + Duration::from_millis(60));
|
let due = t.pop_due(now + Duration::from_millis(60));
|
||||||
assert_eq!(due.len(), 1, "exactly the wall send must fire at raw deadline");
|
assert_eq!(
|
||||||
|
due.len(),
|
||||||
|
1,
|
||||||
|
"exactly the wall send must fire at raw deadline"
|
||||||
|
);
|
||||||
for e in due {
|
for e in due {
|
||||||
if let smarm::timer::Reason::Send { fire } = e.reason {
|
if let smarm::timer::Reason::Send { fire } = e.reason {
|
||||||
fire();
|
fire();
|
||||||
|
|||||||
+18
-10
@@ -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,15 +268,17 @@ 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 || {
|
||||||
Ok(v) => {
|
match rx.recv_timeout(Duration::from_millis(100)) {
|
||||||
assert_eq!(v, i);
|
Ok(v) => {
|
||||||
got.fetch_add(1, Ordering::Relaxed);
|
assert_eq!(v, i);
|
||||||
|
got.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
Err(RecvTimeoutError::Timeout) => {
|
||||||
|
timed_out.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
Err(e) => panic!("unexpected: {e}"),
|
||||||
}
|
}
|
||||||
Err(RecvTimeoutError::Timeout) => {
|
|
||||||
timed_out.fetch_add(1, Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
Err(e) => panic!("unexpected: {e}"),
|
|
||||||
}));
|
}));
|
||||||
if i % 2 == 0 {
|
if i % 2 == 0 {
|
||||||
handles.push(spawn(move || {
|
handles.push(spawn(move || {
|
||||||
|
|||||||
+30
-11
@@ -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);
|
||||||
@@ -56,7 +62,7 @@ fn actor_yields_and_resumes() {
|
|||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
static REG_BEFORE: OnceLock<[u64; 4]> = OnceLock::new();
|
static REG_BEFORE: OnceLock<[u64; 4]> = OnceLock::new();
|
||||||
static REG_AFTER: OnceLock<[u64; 4]> = OnceLock::new();
|
static REG_AFTER: OnceLock<[u64; 4]> = OnceLock::new();
|
||||||
|
|
||||||
extern "C-unwind" fn actor_reg_check() {
|
extern "C-unwind" fn actor_reg_check() {
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -73,7 +79,10 @@ extern "C-unwind" fn actor_reg_check() {
|
|||||||
REG_BEFORE.set([s0, s1, s2, s3]).ok();
|
REG_BEFORE.set([s0, s1, s2, s3]).ok();
|
||||||
switch_to_scheduler();
|
switch_to_scheduler();
|
||||||
|
|
||||||
let a0: u64; let a1: u64; let a2: u64; let a3: u64;
|
let a0: u64;
|
||||||
|
let a1: u64;
|
||||||
|
let a2: u64;
|
||||||
|
let a3: u64;
|
||||||
core::arch::asm!(
|
core::arch::asm!(
|
||||||
"mov {a0}, r12", "mov {a1}, r13", "mov {a2}, r14", "mov {a3}, r15",
|
"mov {a0}, r12", "mov {a1}, r13", "mov {a2}, r14", "mov {a3}, r15",
|
||||||
a0 = out(reg) a0, a1 = out(reg) a1, a2 = out(reg) a2, a3 = out(reg) a3,
|
a0 = out(reg) a0, a1 = out(reg) a1, a2 = out(reg) a2, a3 = out(reg) a3,
|
||||||
@@ -88,8 +97,14 @@ fn callee_saved_registers_survive_yield() {
|
|||||||
let stack = Stack::new(64 * 1024, 4096).unwrap();
|
let stack = Stack::new(64 * 1024, 4096).unwrap();
|
||||||
let sp = init_actor_stack(stack.top(), actor_reg_check);
|
let sp = init_actor_stack(stack.top(), actor_reg_check);
|
||||||
set_actor_sp(sp);
|
set_actor_sp(sp);
|
||||||
unsafe { switch_to_actor(); switch_to_actor(); }
|
unsafe {
|
||||||
assert_eq!(REG_BEFORE.get().copied().unwrap(), REG_AFTER.get().copied().unwrap());
|
switch_to_actor();
|
||||||
|
switch_to_actor();
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
REG_BEFORE.get().copied().unwrap(),
|
||||||
|
REG_AFTER.get().copied().unwrap()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Two actors, independent stacks.
|
// Two actors, independent stacks.
|
||||||
@@ -123,14 +138,18 @@ fn two_actors_dont_corrupt_each_other() {
|
|||||||
let sp_a = init_actor_stack(stack_a.top(), actor_a);
|
let sp_a = init_actor_stack(stack_a.top(), actor_a);
|
||||||
let sp_b = init_actor_stack(stack_b.top(), actor_b);
|
let sp_b = init_actor_stack(stack_b.top(), actor_b);
|
||||||
|
|
||||||
set_actor_sp(sp_a); unsafe { switch_to_actor() };
|
set_actor_sp(sp_a);
|
||||||
|
unsafe { switch_to_actor() };
|
||||||
let sp_a = get_actor_sp();
|
let sp_a = get_actor_sp();
|
||||||
|
|
||||||
set_actor_sp(sp_b); unsafe { switch_to_actor() };
|
set_actor_sp(sp_b);
|
||||||
|
unsafe { switch_to_actor() };
|
||||||
let sp_b = get_actor_sp();
|
let sp_b = get_actor_sp();
|
||||||
|
|
||||||
set_actor_sp(sp_a); unsafe { switch_to_actor() };
|
set_actor_sp(sp_a);
|
||||||
set_actor_sp(sp_b); unsafe { switch_to_actor() };
|
unsafe { switch_to_actor() };
|
||||||
|
set_actor_sp(sp_b);
|
||||||
|
unsafe { switch_to_actor() };
|
||||||
|
|
||||||
assert_eq!(A_VAL.with(|c| c.get()), 0xA00D);
|
assert_eq!(A_VAL.with(|c| c.get()), 0xA00D);
|
||||||
assert_eq!(B_VAL.with(|c| c.get()), 0xB00D);
|
assert_eq!(B_VAL.with(|c| c.get()), 0xB00D);
|
||||||
|
|||||||
+22
-7
@@ -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));
|
||||||
|
|||||||
+59
-15
@@ -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,15 +696,21 @@ 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());
|
||||||
drop(server); // inbox closes → loop exits → guard drains timers
|
drop(server); // inbox closes → loop exits → guard drains timers
|
||||||
// Clean Down ⇒ the loop returned without the no-leak assert aborting.
|
// Clean Down ⇒ the loop returned without the no-leak assert aborting.
|
||||||
assert!(mon.rx.recv().is_ok());
|
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"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+91
-15
@@ -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,15 +214,27 @@ 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(
|
||||||
m.send(Ev2::Cast(TCast::Arm)).unwrap(); // Idle -> Armed (enter)
|
T::Idle,
|
||||||
|
TData {
|
||||||
|
enters: 0,
|
||||||
|
st_fires: 0,
|
||||||
|
named_fires: 0,
|
||||||
|
st_window: 10_000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
m.send(Ev2::Cast(TCast::Arm)).unwrap(); // Idle -> Armed (enter)
|
||||||
m.send(Ev2::Cast(TCast::Disarm)).unwrap(); // Armed -> Idle (enter)
|
m.send(Ev2::Cast(TCast::Disarm)).unwrap(); // Armed -> Idle (enter)
|
||||||
m.send(Ev2::Cast(TCast::Arm)).unwrap(); // Idle -> Armed (enter)
|
m.send(Ev2::Cast(TCast::Arm)).unwrap(); // Idle -> Armed (enter)
|
||||||
m.send(Ev2::Cast(TCast::Disarm)).unwrap(); // Armed -> Idle (enter)
|
m.send(Ev2::Cast(TCast::Disarm)).unwrap(); // Armed -> Idle (enter)
|
||||||
// enters = 1 (start) + 4 transitions = 5.
|
// enters = 1 (start) + 4 transitions = 5.
|
||||||
*got2.lock().unwrap() = m.call(|r| Ev2::Call(TCall::Enters(r))).unwrap();
|
*got2.lock().unwrap() = m.call(|r| Ev2::Call(TCall::Enters(r))).unwrap();
|
||||||
});
|
});
|
||||||
assert_eq!(*got.lock().unwrap(), 5, "one enter on start, one per real transition");
|
assert_eq!(
|
||||||
|
*got.lock().unwrap(),
|
||||||
|
5,
|
||||||
|
"one enter on start, one per real transition"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// `enter` fires once on start and once per *real* transition; a stay (a call
|
// `enter` fires once on start and once per *real* transition; a stay (a call
|
||||||
@@ -188,7 +244,15 @@ fn enter_on_start_and_each_transition_but_not_stay() {
|
|||||||
let got = Arc::new(Mutex::new((0u32, 0u32, 0u32)));
|
let got = Arc::new(Mutex::new((0u32, 0u32, 0u32)));
|
||||||
let got2 = got.clone();
|
let got2 = got.clone();
|
||||||
run(move || {
|
run(move || {
|
||||||
let m = TimerSm::start(T::Idle, TData { enters: 0, st_fires: 0, named_fires: 0, st_window: 10_000 }); // enter -> 1
|
let m = TimerSm::start(
|
||||||
|
T::Idle,
|
||||||
|
TData {
|
||||||
|
enters: 0,
|
||||||
|
st_fires: 0,
|
||||||
|
named_fires: 0,
|
||||||
|
st_window: 10_000,
|
||||||
|
},
|
||||||
|
); // enter -> 1
|
||||||
let after_start = m.call(|r| Ev2::Call(TCall::Enters(r))).unwrap();
|
let after_start = m.call(|r| Ev2::Call(TCall::Enters(r))).unwrap();
|
||||||
// A stay (a counter read returns `prev`) must not bump enters.
|
// A stay (a counter read returns `prev`) must not bump enters.
|
||||||
let _ = m.call(|r| Ev2::Call(TCall::StFires(r))).unwrap();
|
let _ = m.call(|r| Ev2::Call(TCall::StFires(r))).unwrap();
|
||||||
@@ -208,7 +272,15 @@ fn call_to_panicking_handler_is_down() {
|
|||||||
let got = Arc::new(Mutex::new(None::<Result<u32, CallError>>));
|
let got = Arc::new(Mutex::new(None::<Result<u32, CallError>>));
|
||||||
let got2 = got.clone();
|
let got2 = got.clone();
|
||||||
run(move || {
|
run(move || {
|
||||||
let m = TimerSm::start(T::Idle, TData { enters: 0, st_fires: 0, named_fires: 0, st_window: 10_000 });
|
let m = TimerSm::start(
|
||||||
|
T::Idle,
|
||||||
|
TData {
|
||||||
|
enters: 0,
|
||||||
|
st_fires: 0,
|
||||||
|
named_fires: 0,
|
||||||
|
st_window: 10_000,
|
||||||
|
},
|
||||||
|
);
|
||||||
let r = m.call(|rep| Ev2::Call(TCall::Boom(rep)));
|
let r = m.call(|rep| Ev2::Call(TCall::Boom(rep)));
|
||||||
*got2.lock().unwrap() = Some(r);
|
*got2.lock().unwrap() = Some(r);
|
||||||
});
|
});
|
||||||
@@ -299,7 +371,11 @@ fn postponed_call_answered_after_transition() {
|
|||||||
smarm::sleep(Duration::from_millis(20)); // let the child wake with its reply
|
smarm::sleep(Duration::from_millis(20)); // let the child wake with its reply
|
||||||
*g2.lock().unwrap() = *taken.lock().unwrap();
|
*g2.lock().unwrap() = *taken.lock().unwrap();
|
||||||
});
|
});
|
||||||
assert_eq!(*got.lock().unwrap(), Some(42), "postponed call answered by the Filled state");
|
assert_eq!(
|
||||||
|
*got.lock().unwrap(),
|
||||||
|
Some(42),
|
||||||
|
"postponed call answered by the Filled state"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
|||||||
+40
-9
@@ -56,7 +56,11 @@ fn snapshot_lists_actors_with_parent_edge() {
|
|||||||
|
|
||||||
// The root itself is on-CPU (it's running this code) and rooted under
|
// The root itself is on-CPU (it's running this code) and rooted under
|
||||||
// the forest sentinel.
|
// the forest sentinel.
|
||||||
let root = snap.actors.iter().find(|a| a.pid == me).expect("root present");
|
let root = snap
|
||||||
|
.actors
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.pid == me)
|
||||||
|
.expect("root present");
|
||||||
assert_eq!(root.state, ActorState::Running);
|
assert_eq!(root.state, ActorState::Running);
|
||||||
assert_eq!(root.supervisor, smarm::Pid::new(u32::MAX, u32::MAX));
|
assert_eq!(root.supervisor, smarm::Pid::new(u32::MAX, u32::MAX));
|
||||||
|
|
||||||
@@ -200,7 +204,11 @@ fn tree_places_child_under_its_spawner() {
|
|||||||
|
|
||||||
// The root is parented at the forest sentinel, so it's a genuine root,
|
// The root is parented at the forest sentinel, so it's a genuine root,
|
||||||
// and the worker it spawned hangs beneath it.
|
// and the worker it spawned hangs beneath it.
|
||||||
let root = t.roots.iter().find(|n| n.info.pid == me).expect("root in forest");
|
let root = t
|
||||||
|
.roots
|
||||||
|
.iter()
|
||||||
|
.find(|n| n.info.pid == me)
|
||||||
|
.expect("root in forest");
|
||||||
assert!(!root.orphaned);
|
assert!(!root.orphaned);
|
||||||
assert!(
|
assert!(
|
||||||
root.children.iter().any(|c| c.info.pid == h.pid()),
|
root.children.iter().any(|c| c.info.pid == h.pid()),
|
||||||
@@ -258,14 +266,25 @@ fn tree_from_nests_children_and_reroots_orphans() {
|
|||||||
let t = tree_from(snap);
|
let t = tree_from(snap);
|
||||||
assert_eq!(t.roots.len(), 2);
|
assert_eq!(t.roots.len(), 2);
|
||||||
|
|
||||||
let root = t.roots.iter().find(|n| n.info.pid == root_pid).expect("root present");
|
let root = t
|
||||||
|
.roots
|
||||||
|
.iter()
|
||||||
|
.find(|n| n.info.pid == root_pid)
|
||||||
|
.expect("root present");
|
||||||
assert!(!root.orphaned);
|
assert!(!root.orphaned);
|
||||||
assert_eq!(root.children.len(), 1);
|
assert_eq!(root.children.len(), 1);
|
||||||
assert_eq!(root.children[0].info.pid, child);
|
assert_eq!(root.children[0].info.pid, child);
|
||||||
assert!(!root.children[0].orphaned);
|
assert!(!root.children[0].orphaned);
|
||||||
|
|
||||||
let o = t.roots.iter().find(|n| n.info.pid == orphan).expect("orphan re-rooted");
|
let o = t
|
||||||
assert!(o.orphaned, "an actor whose parent is absent must be flagged orphaned");
|
.roots
|
||||||
|
.iter()
|
||||||
|
.find(|n| n.info.pid == orphan)
|
||||||
|
.expect("orphan re-rooted");
|
||||||
|
assert!(
|
||||||
|
o.orphaned,
|
||||||
|
"an actor whose parent is absent must be flagged orphaned"
|
||||||
|
);
|
||||||
assert!(o.children.is_empty());
|
assert!(o.children.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,7 +417,11 @@ fn stack_info_reports_defaults_and_sampled_depth() {
|
|||||||
let info = spin_until(h.pid(), |a| a.state == ActorState::Parked);
|
let info = spin_until(h.pid(), |a| a.state == ActorState::Parked);
|
||||||
let s = info.stack;
|
let s = info.stack;
|
||||||
assert_eq!(s.reserve, 64 * 1024, "default reserve");
|
assert_eq!(s.reserve, 64 * 1024, "default reserve");
|
||||||
assert_eq!(s.guard, 1024 * 1024, "default guard (kernel stack_guard_gap convention)");
|
assert_eq!(
|
||||||
|
s.guard,
|
||||||
|
1024 * 1024,
|
||||||
|
"default guard (kernel stack_guard_gap convention)"
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
s.depth_high_water >= 8 * 4096,
|
s.depth_high_water >= 8 * 4096,
|
||||||
"hwm sampled at the deep yield: expected ≥ 32 KiB, got {}",
|
"hwm sampled at the deep yield: expected ≥ 32 KiB, got {}",
|
||||||
@@ -432,7 +455,10 @@ fn stack_info_shrink_counters_are_live() {
|
|||||||
let spike = 768 * 4096;
|
let spike = 768 * 4096;
|
||||||
assert!(spike > SHRINK_THRESHOLD);
|
assert!(spike > SHRINK_THRESHOLD);
|
||||||
let worker = spawn_with(
|
let worker = spawn_with(
|
||||||
SpawnOpts { stack_reserve: Some(8 * 1024 * 1024), ..SpawnOpts::default() },
|
SpawnOpts {
|
||||||
|
stack_reserve: Some(8 * 1024 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
},
|
||||||
move || {
|
move || {
|
||||||
std::hint::black_box(burn_stack_yielding(768));
|
std::hint::black_box(burn_stack_yielding(768));
|
||||||
for _ in 0..(SHRINK_COOLDOWN + 8) {
|
for _ in 0..(SHRINK_COOLDOWN + 8) {
|
||||||
@@ -457,9 +483,14 @@ fn stack_info_shrink_counters_are_live() {
|
|||||||
spin_until(wpid, |a| a.state == ActorState::Parked);
|
spin_until(wpid, |a| a.state == ActorState::Parked);
|
||||||
park_tx.send(()).unwrap();
|
park_tx.send(()).unwrap();
|
||||||
}
|
}
|
||||||
let info = spin_until(wpid, |a| a.state == ActorState::Parked && a.stack.shrinks >= 1);
|
let info = spin_until(wpid, |a| {
|
||||||
|
a.state == ActorState::Parked && a.stack.shrinks >= 1
|
||||||
|
});
|
||||||
let s = info.stack;
|
let s = info.stack;
|
||||||
assert!(s.shrinks >= 1, "cooldown was crossed with a spike above threshold");
|
assert!(
|
||||||
|
s.shrinks >= 1,
|
||||||
|
"cooldown was crossed with a spike above threshold"
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
s.parks_since_shrink < SHRINK_COOLDOWN,
|
s.parks_since_shrink < SHRINK_COOLDOWN,
|
||||||
"counter must reset at shrink: {}",
|
"counter must reset at shrink: {}",
|
||||||
|
|||||||
+13
-3
@@ -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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
+10
-4
@@ -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);
|
||||||
@@ -117,7 +120,7 @@ fn normal_exit_does_not_propagate() {
|
|||||||
let a = ha.pid();
|
let a = ha.pid();
|
||||||
link(a);
|
link(a);
|
||||||
yield_now(); // let A run to completion and finalize
|
yield_now(); // let A run to completion and finalize
|
||||||
// A exited normally: nothing should have landed on the inbox.
|
// A exited normally: nothing should have landed on the inbox.
|
||||||
if let Ok(None) = inbox.try_recv() {
|
if let Ok(None) = inbox.try_recv() {
|
||||||
e.store(true, Ordering::SeqCst);
|
e.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);
|
||||||
}
|
}
|
||||||
@@ -208,7 +214,7 @@ fn unlink_prevents_propagation() {
|
|||||||
panic!("boom"); // abnormal, but the link is gone
|
panic!("boom"); // abnormal, but the link is gone
|
||||||
});
|
});
|
||||||
yield_now(); // let A link, unlink, and panic
|
yield_now(); // let A link, unlink, and panic
|
||||||
// Unlinked before death → no ExitSignal should have arrived.
|
// Unlinked before death → no ExitSignal should have arrived.
|
||||||
if let Ok(None) = inbox.try_recv() {
|
if let Ok(None) = inbox.try_recv() {
|
||||||
sv.store(true, Ordering::SeqCst);
|
sv.store(true, Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-6
@@ -67,7 +67,10 @@ fn monitor_already_dead_target_is_noproc() {
|
|||||||
// and its generation bumped, so `pid` is now stale.
|
// 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();
|
||||||
|
|||||||
+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]
|
||||||
|
|||||||
+13
-4
@@ -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>();
|
||||||
@@ -227,8 +233,11 @@ fn send_dyn_delivers_and_reports_wrong_type() {
|
|||||||
ready_rx.recv().unwrap();
|
ready_rx.recv().unwrap();
|
||||||
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();
|
||||||
});
|
});
|
||||||
|
|||||||
+77
-26
@@ -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,9 +417,11 @@ 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 || {
|
||||||
.join()
|
cc.fetch_add(1, Ordering::SeqCst);
|
||||||
.unwrap();
|
})
|
||||||
|
.join()
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
assert_eq!(counter.load(Ordering::SeqCst), 500);
|
assert_eq!(counter.load(Ordering::SeqCst), 500);
|
||||||
@@ -474,7 +514,11 @@ fn multi_thread_timer_only_no_pipe_contention() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
assert_eq!(count.load(Ordering::SeqCst), ACTORS as u64, "not all actors completed");
|
assert_eq!(
|
||||||
|
count.load(Ordering::SeqCst),
|
||||||
|
ACTORS as u64,
|
||||||
|
"not all actors completed"
|
||||||
|
);
|
||||||
|
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
assert!(
|
assert!(
|
||||||
@@ -515,7 +559,10 @@ fn runtime_reusable_after_root_panic() {
|
|||||||
let ran = Arc::new(AtomicBool::new(false));
|
let ran = Arc::new(AtomicBool::new(false));
|
||||||
let ran_t = ran.clone();
|
let ran_t = ran.clone();
|
||||||
r.run(move || ran_t.store(true, Ordering::Relaxed));
|
r.run(move || ran_t.store(true, Ordering::Relaxed));
|
||||||
assert!(ran.load(Ordering::Relaxed), "runtime unusable after root panic");
|
assert!(
|
||||||
|
ran.load(Ordering::Relaxed),
|
||||||
|
"runtime unusable after root panic"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -528,7 +575,11 @@ fn runtime_reusable_after_root_panic() {
|
|||||||
fn burn_stack(frames: usize) -> u64 {
|
fn burn_stack(frames: usize) -> u64 {
|
||||||
let mut local = [0u8; 4096];
|
let mut local = [0u8; 4096];
|
||||||
local[0] = frames as u8;
|
local[0] = frames as u8;
|
||||||
let below = if frames == 0 { 0 } else { burn_stack(frames - 1) };
|
let below = if frames == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
burn_stack(frames - 1)
|
||||||
|
};
|
||||||
std::hint::black_box(&mut local);
|
std::hint::black_box(&mut local);
|
||||||
below.wrapping_add(local[0] as u64)
|
below.wrapping_add(local[0] as u64)
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-4
@@ -14,7 +14,9 @@ use std::sync::Arc;
|
|||||||
fn root_actor_runs() {
|
fn root_actor_runs() {
|
||||||
let captured = Arc::new(AtomicI64::new(0));
|
let captured = Arc::new(AtomicI64::new(0));
|
||||||
let c = captured.clone();
|
let c = captured.clone();
|
||||||
run(move || { c.store(99, Ordering::SeqCst); });
|
run(move || {
|
||||||
|
c.store(99, Ordering::SeqCst);
|
||||||
|
});
|
||||||
assert_eq!(captured.load(Ordering::SeqCst), 99);
|
assert_eq!(captured.load(Ordering::SeqCst), 99);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +29,9 @@ fn spawn_and_join_returns_exit() {
|
|||||||
let captured = Arc::new(AtomicI64::new(0));
|
let captured = Arc::new(AtomicI64::new(0));
|
||||||
let c = captured.clone();
|
let c = captured.clone();
|
||||||
run(move || {
|
run(move || {
|
||||||
let h = spawn(move || { c.store(7, Ordering::SeqCst); });
|
let h = spawn(move || {
|
||||||
|
c.store(7, Ordering::SeqCst);
|
||||||
|
});
|
||||||
let res = h.join();
|
let res = h.join();
|
||||||
assert!(res.is_ok(), "join returned {:?}", res);
|
assert!(res.is_ok(), "join returned {:?}", res);
|
||||||
});
|
});
|
||||||
@@ -68,8 +72,7 @@ fn yield_now_interleaves_actors() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn self_pid_is_stable_within_an_actor() {
|
fn self_pid_is_stable_within_an_actor() {
|
||||||
let pid_cell: Arc<std::sync::Mutex<Option<smarm::Pid>>> =
|
let pid_cell: Arc<std::sync::Mutex<Option<smarm::Pid>>> = Arc::new(std::sync::Mutex::new(None));
|
||||||
Arc::new(std::sync::Mutex::new(None));
|
|
||||||
let p2 = pid_cell.clone();
|
let p2 = pid_cell.clone();
|
||||||
run(move || {
|
run(move || {
|
||||||
let h = spawn(move || {
|
let h = spawn(move || {
|
||||||
|
|||||||
+14
-3
@@ -19,7 +19,12 @@ fn ready_arm_returns_immediately_without_parking() {
|
|||||||
txa.send(42).unwrap();
|
txa.send(42).unwrap();
|
||||||
let i = select(&[&rxb, &rxa]);
|
let i = select(&[&rxb, &rxa]);
|
||||||
assert_eq!(i, 1);
|
assert_eq!(i, 1);
|
||||||
out2.store(rxa.try_recv().unwrap().expect("ready arm must hold a message"), Ordering::SeqCst);
|
out2.store(
|
||||||
|
rxa.try_recv()
|
||||||
|
.unwrap()
|
||||||
|
.expect("ready arm must hold a message"),
|
||||||
|
Ordering::SeqCst,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
assert_eq!(out.load(Ordering::SeqCst), 42);
|
assert_eq!(out.load(Ordering::SeqCst), 42);
|
||||||
}
|
}
|
||||||
@@ -276,7 +281,10 @@ fn select_timeout_ready_arm_wins_without_arming_a_timer() {
|
|||||||
let (txa, rxa) = channel::<i64>();
|
let (txa, rxa) = channel::<i64>();
|
||||||
let (_keep_b, rxb) = channel::<i64>();
|
let (_keep_b, rxb) = channel::<i64>();
|
||||||
txa.send(5).unwrap();
|
txa.send(5).unwrap();
|
||||||
assert_eq!(select_timeout(&[&rxb, &rxa], Duration::from_millis(500)), Some(1));
|
assert_eq!(
|
||||||
|
select_timeout(&[&rxb, &rxa], Duration::from_millis(500)),
|
||||||
|
Some(1)
|
||||||
|
);
|
||||||
assert_eq!(rxa.try_recv().unwrap(), Some(5));
|
assert_eq!(rxa.try_recv().unwrap(), Some(5));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -342,7 +350,10 @@ fn select_timeout_closed_arm_is_ready_not_a_timeout() {
|
|||||||
let (_keep_a, rxa) = channel::<i64>();
|
let (_keep_a, rxa) = channel::<i64>();
|
||||||
let (txb, rxb) = channel::<i64>();
|
let (txb, rxb) = channel::<i64>();
|
||||||
drop(txb);
|
drop(txb);
|
||||||
assert_eq!(select_timeout(&[&rxa, &rxb], Duration::from_millis(200)), Some(1));
|
assert_eq!(
|
||||||
|
select_timeout(&[&rxa, &rxb], Duration::from_millis(200)),
|
||||||
|
Some(1)
|
||||||
|
);
|
||||||
assert!(rxb.try_recv().is_err());
|
assert!(rxb.try_recv().is_err());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-11
@@ -6,9 +6,7 @@
|
|||||||
//! big reserve behaviorally takes effect (deep recursion completes).
|
//! big reserve behaviorally takes effect (deep recursion completes).
|
||||||
|
|
||||||
use smarm::runtime::{Config, DEFAULT_STACK_GUARD, DEFAULT_STACK_RESERVE};
|
use smarm::runtime::{Config, DEFAULT_STACK_GUARD, DEFAULT_STACK_RESERVE};
|
||||||
use smarm::{
|
use smarm::{self_pid, spawn, spawn_under_with, spawn_with, GenServerBuilder, SpawnOpts};
|
||||||
self_pid, spawn, spawn_under_with, spawn_with, GenServerBuilder, SpawnOpts,
|
|
||||||
};
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -45,7 +43,10 @@ fn spawn_with_overrides_reserve_and_guard() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn spawn_with_partial_override_keeps_config_default_for_the_rest() {
|
fn spawn_with_partial_override_keeps_config_default_for_the_rest() {
|
||||||
rt1().run(|| {
|
rt1().run(|| {
|
||||||
let opts = SpawnOpts { stack_reserve: Some(1024 * 1024), ..SpawnOpts::default() };
|
let opts = SpawnOpts {
|
||||||
|
stack_reserve: Some(1024 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
};
|
||||||
let h = spawn_with(opts, || {
|
let h = spawn_with(opts, || {
|
||||||
let shape = smarm::introspect::stack_shape(self_pid()).unwrap();
|
let shape = smarm::introspect::stack_shape(self_pid()).unwrap();
|
||||||
assert_eq!(shape, (1024 * 1024, DEFAULT_STACK_GUARD));
|
assert_eq!(shape, (1024 * 1024, DEFAULT_STACK_GUARD));
|
||||||
@@ -57,7 +58,10 @@ fn spawn_with_partial_override_keeps_config_default_for_the_rest() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn spawn_with_rounds_to_pages() {
|
fn spawn_with_rounds_to_pages() {
|
||||||
rt1().run(|| {
|
rt1().run(|| {
|
||||||
let opts = SpawnOpts { stack_reserve: Some(64 * 1024 + 1), guard_size: Some(4097) };
|
let opts = SpawnOpts {
|
||||||
|
stack_reserve: Some(64 * 1024 + 1),
|
||||||
|
guard_size: Some(4097),
|
||||||
|
};
|
||||||
let h = spawn_with(opts, || {
|
let h = spawn_with(opts, || {
|
||||||
let (reserve, guard) = smarm::introspect::stack_shape(self_pid()).unwrap();
|
let (reserve, guard) = smarm::introspect::stack_shape(self_pid()).unwrap();
|
||||||
assert_eq!(reserve % 4096, 0);
|
assert_eq!(reserve % 4096, 0);
|
||||||
@@ -73,7 +77,10 @@ fn spawn_with_rounds_to_pages() {
|
|||||||
fn spawn_under_with_takes_opts() {
|
fn spawn_under_with_takes_opts() {
|
||||||
rt1().run(|| {
|
rt1().run(|| {
|
||||||
let me = self_pid();
|
let me = self_pid();
|
||||||
let opts = SpawnOpts { stack_reserve: Some(128 * 1024), ..SpawnOpts::default() };
|
let opts = SpawnOpts {
|
||||||
|
stack_reserve: Some(128 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
};
|
||||||
let h = spawn_under_with(me, opts, || {
|
let h = spawn_under_with(me, opts, || {
|
||||||
let (reserve, _) = smarm::introspect::stack_shape(self_pid()).unwrap();
|
let (reserve, _) = smarm::introspect::stack_shape(self_pid()).unwrap();
|
||||||
assert_eq!(reserve, 128 * 1024);
|
assert_eq!(reserve, 128 * 1024);
|
||||||
@@ -90,7 +97,10 @@ fn spawn_under_with_takes_opts() {
|
|||||||
fn custom_stack_never_enters_the_pool() {
|
fn custom_stack_never_enters_the_pool() {
|
||||||
rt1().run(|| {
|
rt1().run(|| {
|
||||||
spawn_with(
|
spawn_with(
|
||||||
SpawnOpts { stack_reserve: Some(512 * 1024), guard_size: Some(128 * 1024) },
|
SpawnOpts {
|
||||||
|
stack_reserve: Some(512 * 1024),
|
||||||
|
guard_size: Some(128 * 1024),
|
||||||
|
},
|
||||||
|| {},
|
|| {},
|
||||||
)
|
)
|
||||||
.join()
|
.join()
|
||||||
@@ -122,7 +132,11 @@ fn default_stack_is_recycled() {
|
|||||||
fn burn_stack(frames: usize) -> u64 {
|
fn burn_stack(frames: usize) -> u64 {
|
||||||
let mut local = [0u8; 4096];
|
let mut local = [0u8; 4096];
|
||||||
local[0] = frames as u8;
|
local[0] = frames as u8;
|
||||||
let below = if frames == 0 { 0 } else { burn_stack(frames - 1) };
|
let below = if frames == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
burn_stack(frames - 1)
|
||||||
|
};
|
||||||
std::hint::black_box(&mut local);
|
std::hint::black_box(&mut local);
|
||||||
below.wrapping_add(local[0] as u64)
|
below.wrapping_add(local[0] as u64)
|
||||||
}
|
}
|
||||||
@@ -134,7 +148,10 @@ fn big_reserve_behaviorally_takes_effect() {
|
|||||||
let done = Arc::new(AtomicBool::new(false));
|
let done = Arc::new(AtomicBool::new(false));
|
||||||
let done2 = done.clone();
|
let done2 = done.clone();
|
||||||
spawn_with(
|
spawn_with(
|
||||||
SpawnOpts { stack_reserve: Some(8 * 1024 * 1024), ..SpawnOpts::default() },
|
SpawnOpts {
|
||||||
|
stack_reserve: Some(8 * 1024 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
},
|
||||||
move || {
|
move || {
|
||||||
std::hint::black_box(burn_stack(256));
|
std::hint::black_box(burn_stack(256));
|
||||||
done2.store(true, Ordering::SeqCst);
|
done2.store(true, Ordering::SeqCst);
|
||||||
@@ -167,7 +184,10 @@ impl smarm::GenServer for Echo {
|
|||||||
fn gen_server_builder_stack_opts() {
|
fn gen_server_builder_stack_opts() {
|
||||||
rt1().run(|| {
|
rt1().run(|| {
|
||||||
let server = GenServerBuilder::new(Echo)
|
let server = GenServerBuilder::new(Echo)
|
||||||
.stack_opts(SpawnOpts { stack_reserve: Some(256 * 1024), ..SpawnOpts::default() })
|
.stack_opts(SpawnOpts {
|
||||||
|
stack_reserve: Some(256 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
})
|
||||||
.start();
|
.start();
|
||||||
let (reserve, guard) = server.call(()).unwrap();
|
let (reserve, guard) = server.call(()).unwrap();
|
||||||
assert_eq!(reserve, 256 * 1024);
|
assert_eq!(reserve, 256 * 1024);
|
||||||
@@ -200,7 +220,10 @@ impl smarm::Machine for Probe {
|
|||||||
fn gen_statem_spawn_with_stack_opts() {
|
fn gen_statem_spawn_with_stack_opts() {
|
||||||
rt1().run(|| {
|
rt1().run(|| {
|
||||||
let m = smarm::gen_statem::spawn_with(
|
let m = smarm::gen_statem::spawn_with(
|
||||||
SpawnOpts { stack_reserve: Some(256 * 1024), ..SpawnOpts::default() },
|
SpawnOpts {
|
||||||
|
stack_reserve: Some(256 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
},
|
||||||
Probe,
|
Probe,
|
||||||
);
|
);
|
||||||
let (tx, rx) = smarm::channel::channel();
|
let (tx, rx) = smarm::channel::channel();
|
||||||
|
|||||||
+30
-6
@@ -78,14 +78,18 @@ fn run_as_child_if_requested() {
|
|||||||
Ok("wide_guard_top") => {
|
Ok("wide_guard_top") => {
|
||||||
// One byte below the usable region, 64 KiB guard: must fault.
|
// One byte below the usable region, 64 KiB guard: must fault.
|
||||||
let s = Stack::new(64 * 1024, 64 * 1024).unwrap();
|
let s = Stack::new(64 * 1024, 64 * 1024).unwrap();
|
||||||
unsafe { s.usable_base().sub(1).write_volatile(0xAB); }
|
unsafe {
|
||||||
|
s.usable_base().sub(1).write_volatile(0xAB);
|
||||||
|
}
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
Ok("wide_guard_bottom") => {
|
Ok("wide_guard_bottom") => {
|
||||||
// The very bottom page of a 64 KiB guard: an unprobed C-style
|
// The very bottom page of a 64 KiB guard: an unprobed C-style
|
||||||
// leap over a small guard lands here — must still fault.
|
// leap over a small guard lands here — must still fault.
|
||||||
let s = Stack::new(64 * 1024, 64 * 1024).unwrap();
|
let s = Stack::new(64 * 1024, 64 * 1024).unwrap();
|
||||||
unsafe { s.usable_base().sub(64 * 1024).write_volatile(0xAB); }
|
unsafe {
|
||||||
|
s.usable_base().sub(64 * 1024).write_volatile(0xAB);
|
||||||
|
}
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
Ok("stack_overflow") => {
|
Ok("stack_overflow") => {
|
||||||
@@ -120,7 +124,12 @@ fn guard_page_causes_sigsegv() {
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
use std::os::unix::process::ExitStatusExt;
|
use std::os::unix::process::ExitStatusExt;
|
||||||
assert_eq!(status.signal(), Some(11), "expected SIGSEGV, got: {:?}", status);
|
assert_eq!(
|
||||||
|
status.signal(),
|
||||||
|
Some(11),
|
||||||
|
"expected SIGSEGV, got: {:?}",
|
||||||
|
status
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +140,12 @@ fn stack_overflow_causes_sigsegv() {
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
use std::os::unix::process::ExitStatusExt;
|
use std::os::unix::process::ExitStatusExt;
|
||||||
assert_eq!(status.signal(), Some(11), "expected SIGSEGV, got: {:?}", status);
|
assert_eq!(
|
||||||
|
status.signal(),
|
||||||
|
Some(11),
|
||||||
|
"expected SIGSEGV, got: {:?}",
|
||||||
|
status
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +189,12 @@ fn wide_guard_faults_at_top() {
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
use std::os::unix::process::ExitStatusExt;
|
use std::os::unix::process::ExitStatusExt;
|
||||||
assert_eq!(status.signal(), Some(11), "expected SIGSEGV, got: {:?}", status);
|
assert_eq!(
|
||||||
|
status.signal(),
|
||||||
|
Some(11),
|
||||||
|
"expected SIGSEGV, got: {:?}",
|
||||||
|
status
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,6 +205,11 @@ fn wide_guard_faults_at_bottom() {
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
use std::os::unix::process::ExitStatusExt;
|
use std::os::unix::process::ExitStatusExt;
|
||||||
assert_eq!(status.signal(), Some(11), "expected SIGSEGV, got: {:?}", status);
|
assert_eq!(
|
||||||
|
status.signal(),
|
||||||
|
Some(11),
|
||||||
|
"expected SIGSEGV, got: {:?}",
|
||||||
|
status
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-4
@@ -42,9 +42,15 @@ fn run_as_child_if_requested() {
|
|||||||
let opts = match mode.as_str() {
|
let opts = match mode.as_str() {
|
||||||
"rust_overflow" | "ffi_tier1" => SpawnOpts::default(),
|
"rust_overflow" | "ffi_tier1" => SpawnOpts::default(),
|
||||||
// Small guard: the canary's 96 KiB displacement clears it.
|
// Small guard: the canary's 96 KiB displacement clears it.
|
||||||
"ffi_tier2" => SpawnOpts { guard_size: Some(4096), ..SpawnOpts::default() },
|
"ffi_tier2" => SpawnOpts {
|
||||||
|
guard_size: Some(4096),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
},
|
||||||
// Enough reserve: the same frame simply fits.
|
// Enough reserve: the same frame simply fits.
|
||||||
"ffi_clean" => SpawnOpts { stack_reserve: Some(256 * 1024), ..SpawnOpts::default() },
|
"ffi_clean" => SpawnOpts {
|
||||||
|
stack_reserve: Some(256 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
},
|
||||||
other => panic!("unknown subtest {other}"),
|
other => panic!("unknown subtest {other}"),
|
||||||
};
|
};
|
||||||
let is_rust = mode == "rust_overflow";
|
let is_rust = mode == "rust_overflow";
|
||||||
@@ -92,8 +98,14 @@ fn rust_overflow_dies_with_tier1_message() {
|
|||||||
err.contains("overflowed its stack") && err.contains("in the guard region"),
|
err.contains("overflowed its stack") && err.contains("in the guard region"),
|
||||||
"missing tier-1 diagnostic; stderr:\n{err}"
|
"missing tier-1 diagnostic; stderr:\n{err}"
|
||||||
);
|
);
|
||||||
assert!(err.contains("reserve=65536"), "wrong reserve in message:\n{err}");
|
assert!(
|
||||||
assert!(err.contains("guard=1048576"), "wrong guard in message:\n{err}");
|
err.contains("reserve=65536"),
|
||||||
|
"wrong reserve in message:\n{err}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
err.contains("guard=1048576"),
|
||||||
|
"wrong guard in message:\n{err}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+16
-6
@@ -24,7 +24,11 @@ const RESERVE: usize = 4 * 1024 * 1024;
|
|||||||
fn burn_stack(frames: usize) -> u64 {
|
fn burn_stack(frames: usize) -> u64 {
|
||||||
let mut local = [0u8; 4096];
|
let mut local = [0u8; 4096];
|
||||||
local[0] = frames as u8;
|
local[0] = frames as u8;
|
||||||
let below = if frames == 0 { 0 } else { burn_stack(frames - 1) };
|
let below = if frames == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
burn_stack(frames - 1)
|
||||||
|
};
|
||||||
std::hint::black_box(&mut local);
|
std::hint::black_box(&mut local);
|
||||||
below.wrapping_add(local[0] as u64)
|
below.wrapping_add(local[0] as u64)
|
||||||
}
|
}
|
||||||
@@ -33,10 +37,13 @@ fn burn_stack(frames: usize) -> u64 {
|
|||||||
fn resident_pages(lo: usize, len: usize) -> usize {
|
fn resident_pages(lo: usize, len: usize) -> usize {
|
||||||
let page = 4096;
|
let page = 4096;
|
||||||
let mut vec = vec![0u8; len / page];
|
let mut vec = vec![0u8; len / page];
|
||||||
let ret = unsafe {
|
let ret = unsafe { libc::mincore(lo as *mut libc::c_void, len, vec.as_mut_ptr()) };
|
||||||
libc::mincore(lo as *mut libc::c_void, len, vec.as_mut_ptr())
|
assert_eq!(
|
||||||
};
|
ret,
|
||||||
assert_eq!(ret, 0, "mincore failed: {}", std::io::Error::last_os_error());
|
0,
|
||||||
|
"mincore failed: {}",
|
||||||
|
std::io::Error::last_os_error()
|
||||||
|
);
|
||||||
vec.iter().filter(|&&b| b & 1 != 0).count()
|
vec.iter().filter(|&&b| b & 1 != 0).count()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +125,9 @@ fn recycle_zaps_dead_stack_down_to_retain() {
|
|||||||
[{usable_base:#x}, +{zap_len:#x})"
|
[{usable_base:#x}, +{zap_len:#x})"
|
||||||
);
|
);
|
||||||
// Pooled, not munmapped: the mapping must still be there.
|
// Pooled, not munmapped: the mapping must still be there.
|
||||||
assert!(vma_exists(usable_base), "default-shaped stack was unmapped instead of pooled");
|
assert!(
|
||||||
|
vma_exists(usable_base),
|
||||||
|
"default-shaped stack was unmapped instead of pooled"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-3
@@ -66,7 +66,10 @@ fn spike_then_parks_marks_lazyfree_and_keeps_live_data() {
|
|||||||
assert!(spike > SHRINK_THRESHOLD);
|
assert!(spike > SHRINK_THRESHOLD);
|
||||||
|
|
||||||
let worker = spawn_with(
|
let worker = spawn_with(
|
||||||
SpawnOpts { stack_reserve: Some(8 * 1024 * 1024), ..SpawnOpts::default() },
|
SpawnOpts {
|
||||||
|
stack_reserve: Some(8 * 1024 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
},
|
||||||
move || {
|
move || {
|
||||||
// Live data that must survive the shrink, and an anchor
|
// Live data that must survive the shrink, and an anchor
|
||||||
// address inside the stack for the smaps scan.
|
// address inside the stack for the smaps scan.
|
||||||
@@ -111,7 +114,11 @@ fn spike_then_parks_marks_lazyfree_and_keeps_live_data() {
|
|||||||
"expected ≥ 2 MiB LazyFree in the stack range, got {} bytes",
|
"expected ≥ 2 MiB LazyFree in the stack range, got {} bytes",
|
||||||
lazy
|
lazy
|
||||||
);
|
);
|
||||||
assert_eq!(checksum, 64 * 0xA5u64, "live stack data corrupted by shrink");
|
assert_eq!(
|
||||||
|
checksum,
|
||||||
|
64 * 0xA5u64,
|
||||||
|
"live stack data corrupted by shrink"
|
||||||
|
);
|
||||||
worker.join().unwrap();
|
worker.join().unwrap();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -131,7 +138,9 @@ fn shallow_actor_never_shrinks() {
|
|||||||
for _ in 0..(SHRINK_COOLDOWN + 8) {
|
for _ in 0..(SHRINK_COOLDOWN + 8) {
|
||||||
park_rx.recv().unwrap();
|
park_rx.recv().unwrap();
|
||||||
}
|
}
|
||||||
done_tx.send(lazy_free_bytes_in(anchor - 64 * 1024, anchor + 4096)).unwrap();
|
done_tx
|
||||||
|
.send(lazy_free_bytes_in(anchor - 64 * 1024, anchor + 4096))
|
||||||
|
.unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
let wpid = worker.pid();
|
let wpid = worker.pid();
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
//! registry entry guarantees for every named server.
|
//! registry entry guarantees for every named server.
|
||||||
|
|
||||||
use smarm::{
|
use smarm::{
|
||||||
call, channel, init, request_stop, spawn, Config, GenServer, GenServerBuilder, GenServerName,
|
call, channel, init, request_stop, spawn, CallError, Config, GenServer, GenServerBuilder,
|
||||||
CallError, Receiver, RecvTimeoutError,
|
GenServerName, Receiver, RecvTimeoutError,
|
||||||
};
|
};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -73,10 +73,12 @@ fn named_server_request_stop_releases_queued_caller_with_server_down() {
|
|||||||
let (res_tx, res_rx) = channel::<Result<(), CallError>>();
|
let (res_tx, res_rx) = channel::<Result<(), CallError>>();
|
||||||
|
|
||||||
// 1. Start the named server and keep its ref alive.
|
// 1. Start the named server and keep its ref alive.
|
||||||
let server = GenServerBuilder::new(Blocker { gate: Some(gate_rx) })
|
let server = GenServerBuilder::new(Blocker {
|
||||||
.named(BLOCKER)
|
gate: Some(gate_rx),
|
||||||
.start()
|
})
|
||||||
.expect("name should be free");
|
.named(BLOCKER)
|
||||||
|
.start()
|
||||||
|
.expect("name should be free");
|
||||||
let spid = server.pid();
|
let spid = server.pid();
|
||||||
|
|
||||||
// 2. Send the cast and let the server dequeue it and park on the gate.
|
// 2. Send the cast and let the server dequeue it and park on the gate.
|
||||||
|
|||||||
+16
-8
@@ -10,7 +10,11 @@
|
|||||||
//! out rather than produce a false pass — run with `cargo test -- --timeout`
|
//! 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)`
|
||||||
|
|||||||
+30
-10
@@ -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.
|
||||||
@@ -356,7 +376,7 @@ fn send_after_to_unresolved_name_is_silent() {
|
|||||||
// Nobody registered NOPE; firing resolves to nothing and is dropped.
|
// Nobody registered NOPE; firing resolves to nothing and is dropped.
|
||||||
let _id = send_after_named(Duration::from_millis(10), NOPE, 1);
|
let _id = send_after_named(Duration::from_millis(10), NOPE, 1);
|
||||||
sleep(Duration::from_millis(40)); // let it fire and no-op
|
sleep(Duration::from_millis(40)); // let it fire and no-op
|
||||||
// Reaching here without a panic is the assertion.
|
// Reaching here without a panic is the assertion.
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,9 +421,9 @@ fn send_after_to_dead_typed_pid_is_silent() {
|
|||||||
assert_eq!(report_rx.recv().unwrap(), 1); // sink has now exited
|
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; the sink is gone, so its report sender dropped with it —
|
// No panic; the sink is gone, so its report sender dropped with it —
|
||||||
// closed+empty is Err (documented), which also proves nothing
|
// closed+empty is Err (documented), which also proves nothing
|
||||||
// further was delivered.
|
// further was delivered.
|
||||||
assert!(report_rx.try_recv().is_err(), "nothing further delivered");
|
assert!(report_rx.try_recv().is_err(), "nothing further delivered");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
//! Non-panicking spawn at slab capacity (`try_spawn`).
|
||||||
|
//!
|
||||||
|
//! Covers: parity with `spawn` when slots are free; `Err(AtCapacity)` instead
|
||||||
|
//! of a panic on a full slab (the spawning actor survives — the crash-loop
|
||||||
|
//! from the motivating slowloris incident cannot start); self-heal (a freed
|
||||||
|
//! slot makes the next `try_spawn` succeed); and exact claim-or-report
|
||||||
|
//! accounting under a multi-thread race for the last slots (no TOCTOU
|
||||||
|
//! overshoot, no panic).
|
||||||
|
|
||||||
|
use smarm::runtime::Config;
|
||||||
|
use smarm::{spawn, try_spawn, try_spawn_under_with, yield_now, SpawnError, SpawnOpts};
|
||||||
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// A child that holds its slot until `release` flips, without parking
|
||||||
|
/// machinery: busy-yield keeps the scheduler moving and the slot occupied.
|
||||||
|
fn holder(release: Arc<AtomicBool>) -> impl FnOnce() + Send + 'static {
|
||||||
|
move || {
|
||||||
|
while !release.load(Ordering::Acquire) {
|
||||||
|
yield_now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn try_spawn_is_spawn_when_slots_free() {
|
||||||
|
smarm::runtime::init(Config::exact(1)).run(|| {
|
||||||
|
let ran = Arc::new(AtomicBool::new(false));
|
||||||
|
let flag = ran.clone();
|
||||||
|
let h = try_spawn(move || flag.store(true, Ordering::Release))
|
||||||
|
.expect("slots free — must behave exactly like spawn");
|
||||||
|
h.join().unwrap();
|
||||||
|
assert!(ran.load(Ordering::Acquire));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn at_capacity_is_err_not_panic_and_accounting_is_exact() {
|
||||||
|
const MAX: usize = 8;
|
||||||
|
smarm::runtime::init(Config::exact(1).max_actors(MAX)).run(|| {
|
||||||
|
let release = Arc::new(AtomicBool::new(false));
|
||||||
|
// Fill the slab from the initial actor: slots are claimed at spawn
|
||||||
|
// time, so children need not have run yet. Count until refusal.
|
||||||
|
let mut held = Vec::new();
|
||||||
|
loop {
|
||||||
|
match try_spawn(holder(release.clone())) {
|
||||||
|
Ok(h) => held.push(h),
|
||||||
|
Err(e) => {
|
||||||
|
assert_eq!(e, SpawnError::AtCapacity);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Initial actor occupies one slot; the rest were spawnable.
|
||||||
|
assert_eq!(held.len(), MAX - 1, "slab accounting must be exact");
|
||||||
|
// Still refusing (and still not panicking) on repeat.
|
||||||
|
assert!(matches!(try_spawn(|| ()), Err(SpawnError::AtCapacity)));
|
||||||
|
// The `_with` surface refuses identically — a custom shape must not
|
||||||
|
// reach stack allocation when there is no slot for it.
|
||||||
|
let opts = SpawnOpts {
|
||||||
|
stack_reserve: Some(1024 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
};
|
||||||
|
assert!(matches!(
|
||||||
|
try_spawn_under_with(smarm::self_pid(), opts, || ()),
|
||||||
|
Err(SpawnError::AtCapacity)
|
||||||
|
));
|
||||||
|
|
||||||
|
// Self-heal: free the slots, join, and the next try_spawn succeeds.
|
||||||
|
release.store(true, Ordering::Release);
|
||||||
|
for h in held {
|
||||||
|
h.join().unwrap();
|
||||||
|
}
|
||||||
|
let h = try_spawn(|| ()).expect("slots freed — must succeed again");
|
||||||
|
h.join().unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plain_spawn_still_panics_at_capacity() {
|
||||||
|
// The existing invariant-check semantics of `spawn` are untouched: at a
|
||||||
|
// full slab it panics, the panic is caught at the actor isolation
|
||||||
|
// boundary, and it surfaces as a join error — exactly as before. The
|
||||||
|
// bomb actor is spawned into the LAST slot (so the slab is full only
|
||||||
|
// once the bomb itself is live) and the panic lands inside the bomb,
|
||||||
|
// not the initial actor.
|
||||||
|
const MAX: usize = 6;
|
||||||
|
smarm::runtime::init(Config::exact(1).max_actors(MAX)).run(|| {
|
||||||
|
let release = Arc::new(AtomicBool::new(false));
|
||||||
|
let mut held = Vec::new();
|
||||||
|
for _ in 0..MAX - 2 {
|
||||||
|
held.push(spawn(holder(release.clone())));
|
||||||
|
}
|
||||||
|
let armed = Arc::new(AtomicBool::new(false));
|
||||||
|
let armed2 = armed.clone();
|
||||||
|
let bomb = spawn(move || {
|
||||||
|
armed2.store(true, Ordering::Release);
|
||||||
|
// Slab is now full (initial + MAX−2 holders + this actor); the
|
||||||
|
// plain spawn must panic this actor.
|
||||||
|
let _ = spawn(|| ());
|
||||||
|
unreachable!("allocate_slot must have panicked");
|
||||||
|
});
|
||||||
|
let err = bomb
|
||||||
|
.join()
|
||||||
|
.expect_err("bomb must die by panic, not run through");
|
||||||
|
assert!(armed.load(Ordering::Acquire), "bomb must have actually run");
|
||||||
|
// The panic message is a formatted String (panic! with args).
|
||||||
|
let msg = err
|
||||||
|
.payload
|
||||||
|
.downcast_ref::<String>()
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| "<non-string payload>".into());
|
||||||
|
assert!(
|
||||||
|
msg.contains("slot table exhausted"),
|
||||||
|
"panic must be the slab-exhaustion invariant message, got: {msg}"
|
||||||
|
);
|
||||||
|
release.store(true, Ordering::Release);
|
||||||
|
for h in held {
|
||||||
|
h.join().unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn racing_try_spawns_claim_exactly_the_free_slots() {
|
||||||
|
// 4 scheduler threads, 4 spawner actors hammering try_spawn for a small
|
||||||
|
// pool of remaining slots. Claim-or-report must hand out exactly the
|
||||||
|
// free slots across all racers — no overshoot (TOCTOU), no panic.
|
||||||
|
const MAX: usize = 32;
|
||||||
|
const SPAWNERS: usize = 4;
|
||||||
|
smarm::runtime::init(Config::exact(4).max_actors(MAX)).run(|| {
|
||||||
|
let release = Arc::new(AtomicBool::new(false));
|
||||||
|
let won = Arc::new(AtomicUsize::new(0));
|
||||||
|
let done = Arc::new(AtomicUsize::new(0));
|
||||||
|
|
||||||
|
// Occupy some slots up front so the racers fight over a remainder.
|
||||||
|
let mut pre = Vec::new();
|
||||||
|
for _ in 0..8 {
|
||||||
|
pre.push(spawn(holder(release.clone())));
|
||||||
|
}
|
||||||
|
// Free slots now: MAX − 1 (initial) − 8 (pre) − SPAWNERS.
|
||||||
|
let up_for_grabs = MAX - 1 - 8 - SPAWNERS;
|
||||||
|
|
||||||
|
let mut spawners = Vec::new();
|
||||||
|
for _ in 0..SPAWNERS {
|
||||||
|
let release = release.clone();
|
||||||
|
let won = won.clone();
|
||||||
|
let done = done.clone();
|
||||||
|
spawners.push(spawn(move || {
|
||||||
|
loop {
|
||||||
|
match try_spawn(holder(release.clone())) {
|
||||||
|
Ok(h) => {
|
||||||
|
won.fetch_add(1, Ordering::AcqRel);
|
||||||
|
drop(h); // detached; slot held by the holder
|
||||||
|
}
|
||||||
|
Err(SpawnError::AtCapacity) => break,
|
||||||
|
Err(_) => unreachable!("non_exhaustive future-proofing"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
done.fetch_add(1, Ordering::AcqRel);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// Wait for every racer to hit AtCapacity.
|
||||||
|
while done.load(Ordering::Acquire) < SPAWNERS {
|
||||||
|
yield_now();
|
||||||
|
}
|
||||||
|
assert_eq!(won.load(Ordering::Acquire), up_for_grabs);
|
||||||
|
|
||||||
|
release.store(true, Ordering::Release);
|
||||||
|
for h in pre.into_iter().chain(spawners) {
|
||||||
|
h.join().unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_error_is_a_real_error() {
|
||||||
|
let e = SpawnError::AtCapacity;
|
||||||
|
let msg = format!("{e}");
|
||||||
|
assert!(
|
||||||
|
msg.contains("capacity"),
|
||||||
|
"Display should name the condition: {msg}"
|
||||||
|
);
|
||||||
|
let _: &dyn std::error::Error = &e;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user