diag(runtime): wake-path counters in SchedulerStats + RuntimeStats::wake_diag; RQDIAG/DIAG lines in rq_runtime and general (target 5, finding 17)
This commit is contained in:
+93
-6
@@ -356,6 +356,25 @@ pub struct SchedulerStats {
|
||||
pub slot_hits: AtomicU64,
|
||||
/// RFC 005: slot occupants displaced to the shared queue by a newer wake.
|
||||
pub slot_displacements: AtomicU64,
|
||||
// --- wake-path diagnostics (target 5). Relaxed counters, cheap. ---
|
||||
/// unpark hit Parked from actor context → slot_push.
|
||||
pub unpark_slot: AtomicU64,
|
||||
/// unpark hit Parked from scheduler/foreign context → shared enqueue.
|
||||
pub unpark_queue: AtomicU64,
|
||||
/// unpark hit Running → RunningNotified (peer had not parked yet).
|
||||
pub unpark_notified: AtomicU64,
|
||||
/// park_return found the flag consumed → shared re-enqueue.
|
||||
pub park_flag_consumed: AtomicU64,
|
||||
/// Yield-intent re-enqueues.
|
||||
pub yield_requeues: AtomicU64,
|
||||
/// `enqueue` tail wake actually delivered a futex permit.
|
||||
pub enqueue_wakes: AtomicU64,
|
||||
/// Chain-rule wake actually delivered a permit.
|
||||
pub chain_wakes: AtomicU64,
|
||||
/// Times this scheduler entered Pop::Idle (about to futex-park).
|
||||
pub idle_parks: AtomicU64,
|
||||
/// Idle parks whose recheck aborted (WorkFound) — no futex.
|
||||
pub idle_recheck_hits: AtomicU64,
|
||||
}
|
||||
|
||||
impl SchedulerStats {
|
||||
@@ -365,8 +384,42 @@ impl SchedulerStats {
|
||||
run_queue_len: AtomicU64::new(0),
|
||||
slot_hits: AtomicU64::new(0),
|
||||
slot_displacements: AtomicU64::new(0),
|
||||
unpark_slot: AtomicU64::new(0),
|
||||
unpark_queue: AtomicU64::new(0),
|
||||
unpark_notified: AtomicU64::new(0),
|
||||
park_flag_consumed: AtomicU64::new(0),
|
||||
yield_requeues: AtomicU64::new(0),
|
||||
enqueue_wakes: AtomicU64::new(0),
|
||||
chain_wakes: AtomicU64::new(0),
|
||||
idle_parks: AtomicU64::new(0),
|
||||
idle_recheck_hits: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&self) {
|
||||
for c in [
|
||||
&self.slot_hits,
|
||||
&self.slot_displacements,
|
||||
&self.unpark_slot,
|
||||
&self.unpark_queue,
|
||||
&self.unpark_notified,
|
||||
&self.park_flag_consumed,
|
||||
&self.yield_requeues,
|
||||
&self.enqueue_wakes,
|
||||
&self.chain_wakes,
|
||||
&self.idle_parks,
|
||||
&self.idle_recheck_hits,
|
||||
] {
|
||||
c.store(0, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bump a per-thread diagnostic counter on the calling scheduler thread.
|
||||
macro_rules! diag {
|
||||
($inner:expr, $field:ident) => {
|
||||
SCHED_SLOT.with(|s| $inner.stats[s.get()].$field.fetch_add(1, Ordering::Relaxed))
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -422,6 +475,30 @@ impl RuntimeStats {
|
||||
.map(|s| s.slot_displacements.load(Ordering::Relaxed))
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Wake-path diagnostics summed across threads, as `name=value` pairs
|
||||
/// (target 5 instrumentation). Reset at the start of each `run()`.
|
||||
pub fn wake_diag(&self) -> String {
|
||||
let sum = |f: fn(&SchedulerStats) -> &AtomicU64| -> u64 {
|
||||
self.inner.stats.iter().map(|s| f(s).load(Ordering::Relaxed)).sum()
|
||||
};
|
||||
format!(
|
||||
"slot_hits={} displaced={} unpark_slot={} unpark_queue={} unpark_notified={} \
|
||||
flag_consumed={} yield_requeues={} enqueue_wakes={} chain_wakes={} \
|
||||
idle_parks={} idle_recheck_hits={}",
|
||||
sum(|s| &s.slot_hits),
|
||||
sum(|s| &s.slot_displacements),
|
||||
sum(|s| &s.unpark_slot),
|
||||
sum(|s| &s.unpark_queue),
|
||||
sum(|s| &s.unpark_notified),
|
||||
sum(|s| &s.park_flag_consumed),
|
||||
sum(|s| &s.yield_requeues),
|
||||
sum(|s| &s.enqueue_wakes),
|
||||
sum(|s| &s.chain_wakes),
|
||||
sum(|s| &s.idle_parks),
|
||||
sum(|s| &s.idle_recheck_hits),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1080,7 +1157,9 @@ impl RuntimeInner {
|
||||
// pure-compute hot path pays (almost) nothing. Bias is over-wake:
|
||||
// a spurious wake costs one futex round-trip and a failed pop; a
|
||||
// missed wake would cost a stranded actor.
|
||||
self.coord.wake_one_if_idle();
|
||||
if self.coord.wake_one_if_idle() {
|
||||
diag!(self, enqueue_wakes);
|
||||
}
|
||||
}
|
||||
|
||||
/// Make `pid` runnable if it is parked; coalesce or defer otherwise.
|
||||
@@ -1137,12 +1216,15 @@ impl RuntimeInner {
|
||||
// (install_actor enqueues directly), so they bypass the
|
||||
// slot by construction.
|
||||
if self.wake_slot && crate::actor::current_pid().is_some() {
|
||||
diag!(self, unpark_slot);
|
||||
self.slot_push(pid);
|
||||
} else {
|
||||
diag!(self, unpark_queue);
|
||||
self.enqueue(pid);
|
||||
}
|
||||
}
|
||||
Unpark::Notified => {
|
||||
diag!(self, unpark_notified);
|
||||
crate::te!(crate::trace::Event::UnparkDeferred(pid));
|
||||
}
|
||||
Unpark::Noop => {}
|
||||
@@ -1330,8 +1412,7 @@ impl Runtime {
|
||||
// RFC 005: slot counters reset at the START of a run (not the end),
|
||||
// so `stats()` read after `run()` returns reports that run's totals.
|
||||
for stat in &self.inner.stats {
|
||||
stat.slot_hits.store(0, Ordering::Relaxed);
|
||||
stat.slot_displacements.store(0, Ordering::Relaxed);
|
||||
stat.reset();
|
||||
}
|
||||
|
||||
// Spawn the initial actor through the public spawn path (which
|
||||
@@ -2171,13 +2252,17 @@ fn schedule_loop(inner: &Arc<RuntimeInner>, slot_idx: usize) {
|
||||
// The mandatory post-publish re-check: a producer that
|
||||
// enqueued (or a verdict input that flipped) before it
|
||||
// could see our idle bit has left us the evidence.
|
||||
let _ = inner.coord.park(slot_idx, tk_deadline, || {
|
||||
stats.idle_parks.fetch_add(1, Ordering::Relaxed);
|
||||
let pr = inner.coord.park(slot_idx, tk_deadline, || {
|
||||
!inner.run_queue.is_empty()
|
||||
|| (inner.live_actors.load(Ordering::Acquire) == 0
|
||||
&& inner.io_outstanding.load(Ordering::Acquire) == 0
|
||||
&& inner.io_fd_waiters.load(Ordering::Acquire) == 0)
|
||||
|| inner.coord.deadline_due()
|
||||
});
|
||||
if matches!(pr, crate::park::ParkResult::WorkFound) {
|
||||
stats.idle_recheck_hits.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
if tk_deadline.is_some() {
|
||||
// Hand the role back BEFORE firing: pop_due can run
|
||||
// `Send` thunks that insert new timers, and the
|
||||
@@ -2212,8 +2297,8 @@ fn schedule_loop(inner: &Arc<RuntimeInner>, slot_idx: usize) {
|
||||
// miss here is safe — the enqueue that created the surplus already
|
||||
// issued its own wake (RFC 018 no-lost-wake); this only sharpens
|
||||
// parallelism latency.
|
||||
if !inner.run_queue.is_empty() {
|
||||
inner.coord.wake_one_if_idle();
|
||||
if !inner.run_queue.is_empty() && inner.coord.wake_one_if_idle() {
|
||||
stats.chain_wakes.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
let slot = match inner.slot_at(pid) {
|
||||
@@ -2302,6 +2387,7 @@ fn schedule_loop(inner: &Arc<RuntimeInner>, slot_idx: usize) {
|
||||
// arriving mid-run coalesces into the re-queue.
|
||||
crate::te!(crate::trace::Event::Yield(pid));
|
||||
slot.word.yield_return(gen);
|
||||
diag!(inner, yield_requeues);
|
||||
inner.enqueue(pid);
|
||||
}
|
||||
YieldIntent::Park => {
|
||||
@@ -2342,6 +2428,7 @@ fn schedule_loop(inner: &Arc<RuntimeInner>, slot_idx: usize) {
|
||||
#[cfg(feature = "smarm-causal")]
|
||||
crate::causal::on_deschedule(slot, false);
|
||||
crate::te!(crate::trace::Event::UnparkFlagConsumed(pid));
|
||||
diag!(inner, park_flag_consumed);
|
||||
inner.enqueue(pid);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user