From eb6e8edafe9db6f5c9912bbfd3a5284625ed427b Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Tue, 21 Jul 2026 19:47:42 +0100 Subject: [PATCH] =?UTF-8?q?apic:=20time-bound=20the=20TSC=20warp=20check?= =?UTF-8?q?=20=E2=80=94=2047=20s=20of=20AP=20bring-up=20becomes=20~0.3=20s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First real per-process logs off the stick (the logging track paying for itself): kernel.log showed 16-core bring-up costing 47 s — per-core gaps of 2-21 s — on a machine whose clocksource is the TSC, so every AP runs the pairwise warp check. QEMU always picks HPET, so the harness never executed this path at all. The check was bounded by ITERATIONS: 1<<20 warp ticks, each a locked read-modify-write on a cacheline two cores fight over — microseconds under real contention, not the nanosecond the '~1 ms' comment assumed — and the 1<<32-PAUSE rendezvous 'bound' is ~2 minutes on modern Intel (PAUSE ~140 cycles). Both are now bounded by TIME measured on the TSC itself: ~5 ms of pairwise hammering per core (Linux's check_tsc_warp budget — ample to catch a lagging TSC) and a ~100 ms rendezvous window. --- .gitignore | 2 +- system/kernel/architecture/x86_64/apic.zig | 37 +++++++++++++++------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index 674959d..e1c5be8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,4 @@ zig-out/ .idea/ .claude/ -.github/ \ No newline at end of file +.github//var/ diff --git a/system/kernel/architecture/x86_64/apic.zig b/system/kernel/architecture/x86_64/apic.zig index 73efa17..9757c77 100644 --- a/system/kernel/architecture/x86_64/apic.zig +++ b/system/kernel/architecture/x86_64/apic.zig @@ -529,8 +529,22 @@ var warp_ap_ready: u32 = 0; var warp_stop: u32 = 0; var warp_checks: u32 = 0; // completed per-AP rendezvous count (for the tsc-sync test) -const warp_rounds: u32 = 1 << 20; // locked reads on the BSP: ~1 ms at GHz rates -const warp_spin_limit: u64 = 1 << 32; // bound every rendezvous wait so a lost core can't hang boot +// The warp check is bounded by TIME, not iterations: a warp tick is a locked +// read-modify-write on a cacheline two cores are fighting over — microseconds +// under real contention, not the nanosecond an uncontended count assumes (a +// 1<<20-round budget measured 2-21 SECONDS per core on a 16-core machine), and +// a PAUSE costs ~140 cycles on modern Intel, so an iteration-counted await +// mis-measures by two orders of magnitude too. ~5 ms of pairwise hammering per +// core is plenty to catch a lagging TSC (Linux's check_tsc_warp budget), and +// ~100 ms is a generous rendezvous window for a healthy core. +const warp_check_ns: u64 = 5_000_000; // per-AP pairwise check duration +const warp_await_ns: u64 = 100_000_000; // rendezvous wait before giving up + +/// TSC ticks for `ns` nanoseconds (valid whenever the warp check runs: the TSC +/// is the clocksource, so tsc_hz is calibrated). +fn warpTicksFor(ns: u64) u64 { + return @intCast(@as(u128, ns) * tsc_hz / 1_000_000_000); +} fn warpTick() void { while (@cmpxchgWeak(u32, &warp_lock, 0, 1, .acquire, .monotonic) != null) asm volatile ("pause"); @@ -545,11 +559,11 @@ fn warpTick() void { @atomicStore(u32, &warp_lock, 0, .release); } -/// Spin (bounded) until `flag` is nonzero; false on timeout. +/// Spin (time-bounded) until `flag` is nonzero; false on timeout. fn warpAwait(flag: *u32) bool { - var spins: u64 = 0; - while (@atomicLoad(u32, flag, .acquire) == 0) : (spins += 1) { - if (spins >= warp_spin_limit) return false; + const deadline = rdtsc() +% warpTicksFor(warp_await_ns); + while (@atomicLoad(u32, flag, .acquire) == 0) { + if (rdtsc() -% deadline < (1 << 62)) return false; // past the deadline asm volatile ("pause"); } return true; @@ -568,8 +582,8 @@ pub fn checkWarpSource() void { @atomicStore(u32, &warp_bsp_ready, 0, .release); return; } - var i: u32 = 0; - while (i < warp_rounds) : (i += 1) warpTick(); + const deadline = rdtsc() +% warpTicksFor(warp_check_ns); + while (rdtsc() -% deadline >= (1 << 62)) warpTick(); // until the time budget is spent @atomicStore(u32, &warp_stop, 1, .release); @atomicStore(u32, &warp_bsp_ready, 0, .release); warp_checks += 1; @@ -583,9 +597,10 @@ pub fn checkWarpTarget() void { if (clock_source != .tsc) return; if (!warpAwait(&warp_bsp_ready)) return; @atomicStore(u32, &warp_ap_ready, 1, .release); - var spins: u64 = 0; - while (@atomicLoad(u32, &warp_stop, .acquire) == 0) : (spins += 1) { - if (spins >= warp_spin_limit) return; + // The BSP owns the budget; this bound only protects against a lost BSP. + const deadline = rdtsc() +% warpTicksFor(2 * warp_check_ns + warp_await_ns); + while (@atomicLoad(u32, &warp_stop, .acquire) == 0) { + if (rdtsc() -% deadline < (1 << 62)) return; // past the deadline warpTick(); } }