[Track 2] Parallel-scoped global clock fixes timebase-desync livelock

In --parallel mode a long run livelocked: the scheduler spun
"advanced to deadline 3000 waking hw=2 idx=0" ~14k times in
microseconds. Root cause: each guest thread owns ctx.timebase
(+1/instr in step_block), and all kernel deadline arithmetic read
Scheduler::ctx(hw_id).timebase as "now". But the parallel worker
extracts its PpcContext via mem::replace(ctx_mut_ref, PpcContext::new())
— leaving a ZEROED timebase in the slot while it steps unlocked — and
advance_all_timebases_to only walks runqueue (never idle_ctx). So the
coordinator's coord_pre_round drain and a woken thread's parse_timeout
could read a zeroed/stale basis decoupled from the deadline the
scheduler just advanced to. The thread re-armed the same constant
deadline forever; the global clock never moved.

Fix: add a single monotonic Scheduler::global_clock, advanced by the
per-block retired-instruction count on each parallel writeback and
floored up by advance_all_timebases_to. Kernel deadline reads route
through KernelState::now_basis_at(hw_id), which returns global_clock
ONLY when parallel_active; lockstep keeps reading the exact pre-existing
ctx(hw_id).timebase expression, so the deterministic lockstep trace is
byte-identical (sylpheed_n50m golden unchanged, zero re-baseline).

Verified:
- 50M --parallel run completes (was: hung). Deadlines now strictly
  increasing 5.4M -> 49.1M (18097 unique of 18116; max repeat 2) vs
  pre-fix constant 3000 x ~14000.
- sylpheed_n50m golden byte-identical via plain `check` (no persist).
- Full suite 665/665 green.

Note: an intermittent parallel hang/crash (~1-2/20 at -n 5M) is
pre-existing (master 1/20, this build 2/20 — within noise) and distinct
from the timebase livelock: it is a parallel-race class (e.g. the
unsafe block_ptr deref in run_execution_parallel). Tracked separately;
lockstep remains the recommendation for long runs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-12 19:32:14 +02:00
parent 6271ba1f55
commit 0332d1990d
4 changed files with 71 additions and 4 deletions

View File

@@ -351,6 +351,19 @@ pub struct Scheduler {
/// Sorted by deadline ascending. Scheduler wakes the first entry via
/// `advance_to_next_wake` when a round finds nothing runnable.
timed_waits: Vec<(u64, ThreadRef)>,
/// Parallel-mode coherent monotonic clock. In `--parallel`, workers
/// extract their `PpcContext` (leaving a zeroed timebase in the slot)
/// and step unlocked, so `ctx(hw_id).timebase` is NOT a coherent "now"
/// — a coordinator that reads it can see a stale/zero basis decoupled
/// from the deadline it just advanced to, re-arming the same constant
/// deadline forever (timebase-desync livelock). This field is the
/// single authoritative "now" the parallel coordinator and kernel
/// deadline-arithmetic read instead. Advanced by `advance_global_clock`
/// (per-block retired-instruction count) on each parallel writeback and
/// floored up by `advance_all_timebases_to`. LOCKSTEP never reads it
/// (gated by `KernelState::parallel_active`), so it has zero effect on
/// the deterministic lockstep trace.
global_clock: u64,
/// Global count of TLS slots allocated — `spawn` pre-sizes new threads'
/// `tls_values` to this.
tls_slot_count: usize,
@@ -389,6 +402,7 @@ impl Scheduler {
order,
rng_state,
timed_waits: Vec::new(),
global_clock: 0,
tls_slot_count: 0,
non_empty_runnable: 0,
rotation_cursor: 0,
@@ -1114,6 +1128,29 @@ impl Scheduler {
}
}
}
// Keep the parallel-mode coherent clock at least as far forward as
// any deadline we fast-forward to (idle/timer/wake advances). This
// only mutates the new `global_clock` field — lockstep never reads
// it — so it cannot perturb the deterministic lockstep trace.
self.global_clock = self.global_clock.max(deadline);
}
/// Parallel-mode coherent "now" (see [`Self::global_clock`] field doc).
/// Read by the kernel deadline-arithmetic ONLY when
/// `KernelState::parallel_active`; lockstep keeps reading per-thread
/// `ctx(hw_id).timebase`.
#[inline]
pub fn global_clock(&self) -> u64 {
self.global_clock
}
/// Advance the parallel-mode coherent clock by `n` retired instructions.
/// Called from the parallel worker writeback with the block's executed
/// count so "now" tracks aggregate guest progress. Never called in
/// lockstep (the clock stays 0 and unread there).
#[inline]
pub fn advance_global_clock(&mut self, n: u64) {
self.global_clock = self.global_clock.saturating_add(n);
}
/// Fast-forward the timebase to the earliest pending timed wait and