feat(scheduler): RFC 005 wake slot — per-scheduler capacity-one wake cache
A thread-local Cell<Option<Pid>> per scheduler, checked before the shared
run queue. Runtime-selected via Config { wake_slot: bool }, default OFF
until the slot shootout accepts it (one binary benches both arms).
Push policy: slot-eligible iff the wake originates from actor context
(current_pid().is_some()) — the slot push replaces run_queue.push at the
tail of the unpark protocol's Parked → Queued CAS, so at-most-once-enqueued
holds verbatim as (slot ⊕ shared queue). Scheduler-context wakes (timer/IO
drain) and spawns always go shared (spawns never reach unpark_inner at all).
Displacement is Go semantics: newest wake takes the slot, occupant pushed
shared — moved, never copied.
Pop order: slot, then shared. Slot-popped actors skip reset_timeslice() and
inherit the waker's remaining slice; a handoff chain is bounded by one
slice, after which the preempt-yield re-enqueue goes shared (a yield is not
a wake) — the one-slice starvation bound, zero new counters. Idle and
AllDone are only reachable with an empty local slot by pop order; an
occupied slot elsewhere holds a Queued (live) actor, so the counter-first
termination argument is untouched.
Observability: per-thread slot_hits / slot_displacements (reset at run()
start so post-run stats() reads are per-run), SlotPush/SlotPop trace events.
Bench plan (roadmap v0.9 item 2): rq_runtime gains the slot on/off
dimension (SMARM_BENCH_SLOT, default "0 1") — ping-pong-pairs is the
target metric, yield-storm the regression guard, spawn-storm the
neutrality check. RQCSV grows a slot column; RQSLOT lines carry the
counters; bench_rq.sh aggregates both. Tests pin the push policy through
the counters (actor-context hits, spawn/join bypass, displacement,
default-off, per-run reset).
This commit is contained in:
+106
-42
@@ -1,25 +1,39 @@
|
||||
//! Runtime-level run-queue benches (ROADMAP_v0.5 phase 4).
|
||||
//! Runtime-level run-queue benches (ROADMAP_v0.5 phase 4; slot dimension
|
||||
//! added for the v0.9 slot shootout, RFC 005).
|
||||
//!
|
||||
//! These exercise the WHOLE scheduler with the compile-time-selected queue,
|
||||
//! so comparing variants means rebuilding per rq-* feature — that's what
|
||||
//! scripts/bench_rq.sh does. Workloads:
|
||||
//! scripts/bench_rq.sh does. The RFC 005 wake slot is a *runtime* Config
|
||||
//! knob, so one binary benches both arms; the slot on/off sweep happens
|
||||
//! inside this binary. Workloads:
|
||||
//!
|
||||
//! yield-storm — N actors yield K times each. Pure queue churn:
|
||||
//! every yield is a push + pop with nothing in between.
|
||||
//! Slot role: REGRESSION GUARD — yields never touch the
|
||||
//! slot, any slot-on delta is pop-path overhead.
|
||||
//! ping-pong-pairs — P channel pairs, M roundtrips each. Park/unpark
|
||||
//! latency through the queue.
|
||||
//! Slot role: TARGET METRIC — every send-wake is an
|
||||
//! actor-context unpark, the slot's home pattern.
|
||||
//! spawn-storm — S spawn+join of trivial actors. Slab + queue + free
|
||||
//! list under churn.
|
||||
//! Slot role: NEUTRALITY CHECK — spawns bypass the slot
|
||||
//! by policy; join wakes fire from finalize (scheduler
|
||||
//! context), also shared.
|
||||
//!
|
||||
//! Knobs (env):
|
||||
//! SMARM_BENCH_THREADS scheduler-count sweep, default "1 2 4"
|
||||
//! SMARM_BENCH_SLOT wake-slot sweep, default "0 1" (off then on)
|
||||
//! SMARM_BENCH_RUNS repetitions per config (median), default 5
|
||||
//! SMARM_BENCH_YIELD_ACTORS / _YIELDS default 200 / 500
|
||||
//! SMARM_BENCH_PAIRS / _ROUNDTRIPS default 32 / 1000
|
||||
//! SMARM_BENCH_SPAWNS default 5000
|
||||
//!
|
||||
//! Output: house table + one line per config:
|
||||
//! RQCSV,runtime,<variant>,<bench>,<threads>,<work>,<median_us>,<ops_per_s>
|
||||
//! RQCSV,runtime,<variant>,<slot>,<bench>,<threads>,<work>,<median_us>,<ops_per_s>
|
||||
//! plus, for slot-on configs, the RFC 005 observability counters:
|
||||
//! RQSLOT,<variant>,<bench>,<threads>,<slot_hits>,<slot_displacements>
|
||||
//! (hits/displacements are taken from the same run as the median time).
|
||||
//!
|
||||
//! NOTE: a 1-core sandbox validates the harness, not the scaling story;
|
||||
//! real curves come from the many-core box.
|
||||
@@ -49,9 +63,30 @@ fn env_threads() -> Vec<usize> {
|
||||
.unwrap_or_else(|_| vec![1, 2, 4])
|
||||
}
|
||||
|
||||
/// (total_ops, elapsed_µs) for one measured run.
|
||||
fn yield_storm(threads: usize, actors: usize, yields: usize) -> (u64, u128) {
|
||||
let rt = init(Config::exact(threads));
|
||||
fn env_slots() -> Vec<bool> {
|
||||
std::env::var("SMARM_BENCH_SLOT")
|
||||
.map(|v| {
|
||||
v.split_whitespace()
|
||||
.filter_map(|t| match t {
|
||||
"0" | "off" | "false" => Some(false),
|
||||
"1" | "on" | "true" => Some(true),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(|_| vec![false, true])
|
||||
}
|
||||
|
||||
/// One measured run: (total_ops, elapsed_µs, slot_hits, slot_displacements).
|
||||
struct Sample {
|
||||
ops: u64,
|
||||
us: u128,
|
||||
hits: u64,
|
||||
displacements: u64,
|
||||
}
|
||||
|
||||
fn yield_storm(threads: usize, slot: bool, actors: usize, yields: usize) -> Sample {
|
||||
let rt = init(Config::exact(threads).wake_slot(slot));
|
||||
let start = Instant::now();
|
||||
rt.run(move || {
|
||||
let handles: Vec<_> = (0..actors)
|
||||
@@ -67,11 +102,18 @@ fn yield_storm(threads: usize, actors: usize, yields: usize) -> (u64, u128) {
|
||||
let _ = h.join();
|
||||
}
|
||||
});
|
||||
((actors * yields) as u64, start.elapsed().as_micros())
|
||||
let us = start.elapsed().as_micros();
|
||||
let stats = rt.stats();
|
||||
Sample {
|
||||
ops: (actors * yields) as u64,
|
||||
us,
|
||||
hits: stats.slot_hits(),
|
||||
displacements: stats.slot_displacements(),
|
||||
}
|
||||
}
|
||||
|
||||
fn ping_pong_pairs(threads: usize, pairs: usize, roundtrips: usize) -> (u64, u128) {
|
||||
let rt = init(Config::exact(threads));
|
||||
fn ping_pong_pairs(threads: usize, slot: bool, pairs: usize, roundtrips: usize) -> Sample {
|
||||
let rt = init(Config::exact(threads).wake_slot(slot));
|
||||
let total = Arc::new(AtomicU64::new(0));
|
||||
let t2 = total.clone();
|
||||
let start = Instant::now();
|
||||
@@ -103,11 +145,18 @@ fn ping_pong_pairs(threads: usize, pairs: usize, roundtrips: usize) -> (u64, u12
|
||||
let _ = h.join();
|
||||
}
|
||||
});
|
||||
(total.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||
let us = start.elapsed().as_micros();
|
||||
let stats = rt.stats();
|
||||
Sample {
|
||||
ops: total.load(Ordering::Relaxed),
|
||||
us,
|
||||
hits: stats.slot_hits(),
|
||||
displacements: stats.slot_displacements(),
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_storm(threads: usize, spawns: usize) -> (u64, u128) {
|
||||
let rt = init(Config::exact(threads));
|
||||
fn spawn_storm(threads: usize, slot: bool, spawns: usize) -> Sample {
|
||||
let rt = init(Config::exact(threads).wake_slot(slot));
|
||||
let start = Instant::now();
|
||||
rt.run(move || {
|
||||
// Batches bound simultaneous liveness well below the slab cap.
|
||||
@@ -122,11 +171,19 @@ fn spawn_storm(threads: usize, spawns: usize) -> (u64, u128) {
|
||||
left -= n;
|
||||
}
|
||||
});
|
||||
(spawns as u64, start.elapsed().as_micros())
|
||||
let us = start.elapsed().as_micros();
|
||||
let stats = rt.stats();
|
||||
Sample {
|
||||
ops: spawns as u64,
|
||||
us,
|
||||
hits: stats.slot_hits(),
|
||||
displacements: stats.slot_displacements(),
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let threads_sweep = env_threads();
|
||||
let slot_sweep = env_slots();
|
||||
let runs = env_usize("SMARM_BENCH_RUNS", 5);
|
||||
let ya = env_usize("SMARM_BENCH_YIELD_ACTORS", 200);
|
||||
let yy = env_usize("SMARM_BENCH_YIELDS", 500);
|
||||
@@ -134,55 +191,62 @@ fn main() {
|
||||
let pr = env_usize("SMARM_BENCH_ROUNDTRIPS", 1000);
|
||||
let ss = env_usize("SMARM_BENCH_SPAWNS", 5000);
|
||||
|
||||
println!("\n{}", "=".repeat(86));
|
||||
println!(" runtime benches — variant={}, runs={runs} (median)", variant());
|
||||
println!("{}", "=".repeat(86));
|
||||
println!("\n{}", "=".repeat(106));
|
||||
println!(
|
||||
"{:>16} | {:>7} | {:>16} | {:>10} | {:>14}",
|
||||
"bench", "threads", "work", "median µs", "ops/s"
|
||||
" runtime benches — variant={}, runs={runs} (median)",
|
||||
variant()
|
||||
);
|
||||
println!("{}", "-".repeat(86));
|
||||
println!("{}", "=".repeat(106));
|
||||
println!(
|
||||
"{:>16} | {:>7} | {:>4} | {:>16} | {:>10} | {:>14} | {:>10} | {:>9}",
|
||||
"bench", "threads", "slot", "work", "median µs", "ops/s", "slot hits", "displaced"
|
||||
);
|
||||
println!("{}", "-".repeat(106));
|
||||
|
||||
type Bench = (&'static str, String, Box<dyn Fn(usize) -> (u64, u128)>);
|
||||
type Bench = (&'static str, String, Box<dyn Fn(usize, bool) -> Sample>);
|
||||
let benches: Vec<Bench> = vec![
|
||||
(
|
||||
"yield-storm",
|
||||
format!("{ya}x{yy}"),
|
||||
Box::new(move |t| yield_storm(t, ya, yy)),
|
||||
Box::new(move |t, s| yield_storm(t, s, ya, yy)),
|
||||
),
|
||||
(
|
||||
"ping-pong-pairs",
|
||||
format!("{pp}x{pr}"),
|
||||
Box::new(move |t| ping_pong_pairs(t, pp, pr)),
|
||||
Box::new(move |t, s| ping_pong_pairs(t, s, pp, pr)),
|
||||
),
|
||||
(
|
||||
"spawn-storm",
|
||||
format!("{ss}"),
|
||||
Box::new(move |t| spawn_storm(t, ss)),
|
||||
Box::new(move |t, s| spawn_storm(t, s, ss)),
|
||||
),
|
||||
];
|
||||
|
||||
for (name, work, f) in &benches {
|
||||
for &t in &threads_sweep {
|
||||
let mut ops = 0u64;
|
||||
let mut times: Vec<u128> = (0..runs)
|
||||
.map(|_| {
|
||||
let (o, us) = f(t);
|
||||
ops = o;
|
||||
us
|
||||
})
|
||||
.collect();
|
||||
times.sort_unstable();
|
||||
let median = times[times.len() / 2];
|
||||
let per_s = (ops as f64 / (median as f64 / 1e6)) as u64;
|
||||
println!(
|
||||
"{:>16} | {:>7} | {:>16} | {:>10} | {:>14}",
|
||||
name, t, work, median, per_s
|
||||
);
|
||||
println!(
|
||||
"RQCSV,runtime,{},{},{},{},{},{}",
|
||||
variant(), name, t, work, median, per_s
|
||||
);
|
||||
for &slot in &slot_sweep {
|
||||
let mut samples: Vec<Sample> = (0..runs).map(|_| f(t, slot)).collect();
|
||||
// Median by elapsed time; report the counters from that
|
||||
// same run so hits/time stay paired.
|
||||
samples.sort_unstable_by_key(|s| s.us);
|
||||
let mid = &samples[samples.len() / 2];
|
||||
let per_s = (mid.ops as f64 / (mid.us as f64 / 1e6)) as u64;
|
||||
let slot_str = if slot { "on" } else { "off" };
|
||||
println!(
|
||||
"{:>16} | {:>7} | {:>4} | {:>16} | {:>10} | {:>14} | {:>10} | {:>9}",
|
||||
name, t, slot_str, work, mid.us, per_s, mid.hits, mid.displacements
|
||||
);
|
||||
println!(
|
||||
"RQCSV,runtime,{},{},{},{},{},{},{}",
|
||||
variant(), slot_str, name, t, work, mid.us, per_s
|
||||
);
|
||||
if slot {
|
||||
println!(
|
||||
"RQSLOT,{},{},{},{},{}",
|
||||
variant(), name, t, mid.hits, mid.displacements
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user