Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/gate-fleet-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ of them with `just verify-vz`.
| spec | `live-win-move` | 1 run / 26 assert | live-win-move.spec -- claim 0487 (milestone six, card G6 move/raise |
| spec | `live-win-syscall` | 1 run / 18 assert | live-win-syscall.spec -- claim 0487 (milestone six, card G6) class-B |
| spec | `live-wm-ipc` | 2 run / 9 assert | live-wm-ipc.spec -- M32 WMS7 (issue #627) app<->WM mailbox protocol (WM_RPC) on VZ |
| spec | `live-wm-pacing` | 1 run / 7 assert | live-wm-pacing.spec -- M53 card 1 (#1247): what the desktop's frame cadence |
| spec | `live-wm-pacing` | 1 run / 7 assert | live-wm-pacing.spec -- WMP (WM frame pacing) card 1 (#1247): what the |
| spec | `live-wm1` | 1 run / 13 assert | live-wm1.spec -- Lane 1 WM1 (#707, claim 919) class-B gate: eight concurrent user windows |
| spec | `live-wm3-taskbar` | 3 run / 13 assert | live-wm3-taskbar.spec -- M32 WM3 (Lane 1, #707): taskbar shows per-window entries, workspace-aware |
| spec | `live-wm4-paint` | 2 run / 4 assert | live-wm4-paint.spec -- M32 WM4 (Lane 1, #707): WM rest policy blends unfocused, focused pure |
Expand Down
16 changes: 13 additions & 3 deletions kernel/src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1892,7 +1892,10 @@ fn process_stdout(text: []const u8) void {

/// Claim 9187 IRQ chain, registered as the exception module's dispatcher:
/// ack from the GIC, handle the timer tick if the INTID is the timer's
/// PPI (re-arming the comparator), then EOI. Runs in IRQ context with a
/// PPI (re-arming the comparator), then EOI. WMP card 3 adds a second way a
/// timer PPI can arrive: a reschedule nudge, which runs the same rotation
/// without advancing the wall clock (see `timer.handle`'s return value).
/// Runs in IRQ context with a
/// register frame on the stack — NO console access (the heartbeat prints
/// from the shell idle loop, where a print cannot re-enter the polled
/// virtio TX path mid-flush). Claim 5275: on a timer PPI the scheduler
Expand All @@ -1912,15 +1915,22 @@ fn irq_dispatch() void {
if (intid < 16) {
smp.handle_sgi(intid);
} else if (timer.is_ppi(intid)) {
// WMP card 3: `handle` reports whether this delivery was the 1 Hz
// period boundary or a reschedule nudge, and that answer decides
// whether the wall clock advances. A secondary core only re-arms, so
// its beat is a period boundary by construction (it does no
// timekeeping anyway — the `c == 0` guard in `tick` keeps that
// core-0's).
var period_tick = true;
if (smp.core_id() == 0) {
timer.handle(); // core-0 timekeeping authority (tick record + re-arm)
period_tick = timer.handle(); // core-0 timekeeping authority (tick record + re-arm)
} else {
timer.arm(); // secondary core: re-arm only — no tick record
}
// SMP lift (claim 8477 follow-up): every core runs the tick; on
// secondary cores it runs ONLY the switch machinery on its own
// per-core staging (global timekeeping/registries stay core-0).
scheduler.tick();
scheduler.tick(period_tick);
} else {
virtio_custom.note_irq(intid);
}
Expand Down
31 changes: 31 additions & 0 deletions kernel/src/monitor.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4012,6 +4012,19 @@ fn cmd_timer(m: *Monitor, args: []const []const u8) ExecError {
m.console.print_u64(timer.irq_ticks);
m.console.puts(" poll=");
m.console.print_u64(timer.poll_ticks);
// WMP card 3: the nudge counters. `armed` > `served` means nudges were
// subsumed by the period boundary arriving first (harmless — the owed
// rotation ran from the period); `coalesced` is the surplus a wake burst
// did NOT pay for. `period_first` counts requests dropped because the 1 Hz
// boundary was already sooner than the 2 ms nudge would have been.
m.console.puts(" nudge_armed=");
m.console.print_u64(timer.nudge_armed_total);
m.console.puts(" nudge_served=");
m.console.print_u64(timer.nudge_served);
m.console.puts(" nudge_coalesced=");
m.console.print_u64(timer.nudge_coalesced);
m.console.puts(" nudge_period_first=");
m.console.print_u64(timer.nudge_period_first);
m.console.puts(" acked=");
m.console.print_u64(gic.acked_total()); // claim 7339: summed across cores
m.console.puts(" first=");
Expand Down Expand Up @@ -7213,6 +7226,24 @@ fn cmd_wm(m: *Monitor, args: []const []const u8) ExecError {
m.console.print_u64(info.flush_avg_ns / 1000);
m.console.puts(" flush_max_us=");
m.console.print_u64(info.flush_max_ns / 1000);
// WMP card 3: the scheduling half of the same latency. The nudge is
// what makes a woken WM run in milliseconds rather than at the next
// 1 Hz boundary, so the counters belong on the row that reports the
// latency they buy. Reported here rather than behind a `sched`
// command because the pacing gate reads THIS row: a latency claim
// that cannot be attributed to nudges would be unfalsifiable.
// requests = wakes that owed a rotation
// coalesced = of those, the ones that found one already owed
// nudges = comparators pulled forward
// served = of those, the ones that actually delivered a nudge
m.console.puts(" resched_requests=");
m.console.print_u64(scheduler.resched_requests);
m.console.puts(" resched_coalesced=");
m.console.print_u64(scheduler.resched_coalesced);
m.console.puts(" nudge_armed=");
m.console.print_u64(timer.nudge_armed_total);
m.console.puts(" nudge_served=");
m.console.print_u64(timer.nudge_served);
m.console.puts("\n");
// M32 WMS4 (issue #624): chrome observability — SET_WINDOW
// submissions counted, the broadcast policy's chrome kind, and the
Expand Down
105 changes: 103 additions & 2 deletions kernel/src/scheduler.zig
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ const serial_ring = @import("serial_ring.zig"); // Arc5 #243: serial snapshot fo
const virtio_file = @import("virtio_file.zig"); // Arc5 #243: tombstone write through the host file channel (HF6: the DATA partition is gone)
const smp = @import("smp.zig");
const spinlock = @import("spinlock.zig");
// WMP card 3: the reschedule nudge pulls this core's comparator forward so a
// woken task runs in milliseconds instead of at the next 1 Hz boundary.
const timer = @import("timer.zig");

const user_stack_section = if (builtin.object_format == .elf) ".userbss" else "__DATA,__userbss";

Expand Down Expand Up @@ -541,6 +544,17 @@ fn push_home_locked(id: usize) void {
const daif = ring_locks[home].lock();
ready_rings[home].push(id);
ring_locks[home].unlock(daif);
// WMP card 3: this is the single blocked->ready funnel, so it is also the
// single place a rotation can become owed. Placed AFTER the ring unlock
// so the nudge (which may immediately interrupt this core) can never
// observe a half-pushed ring; the task is already runnable and visible to
// the rotation by then.
//
// NOTE: the ROTATION does not come through here — `switch_context` pushes
// the preempted task back with a direct `ready_rings[c].push`. That is
// load-bearing: routing it through here would make every rotation request
// the next one, and the comparator would never stop firing.
request_resched();
}

/// The ready-membership invariant, asserted by the host tests after every
Expand Down Expand Up @@ -766,6 +780,79 @@ pub var user_stack: [task_stack_size]u8 align(4096) linksection(user_stack_secti
/// sys_yield. It lives in the already-mapped user BSS aperture and exposes no
/// privileged state beyond the fact that this task was preempted by a tick.
pub var user_timer_preemptions: u64 align(8) linksection(user_stack_section) = 0;

// ---------------------------------------------------------------------------
// WMP card 3: the reschedule request
// ---------------------------------------------------------------------------
/// A task became runnable while another is executing, so a rotation is OWED.
/// WMP card 1 (#1247) measured what the absence of this costs: the WM's
/// pointer response was 786-1216 ms typical and 3004 ms worst, because
/// round-robin evaluates preemption ONLY at the 1 Hz tick, so a woken task
/// waits a uniformly distributed 0-1 s to be scheduled. The request pulls the
/// core-0 comparator forward (`timer.nudge`), so the SAME IRQ rotation a
/// period tick uses runs ~2 ms later instead of ~1 s later.
///
/// The flag is the coalescing rule: at most one nudge is in flight between
/// rotations, so a burst of wakes costs ONE extra comparator fire, not one
/// per wake. It is cleared by every rotation on core 0 (the end of `tick`).
pub var resched_requested: bool = false;
/// Requests that owed a rotation (one per wake that found none pending).
pub var resched_requests: u64 = 0;
/// Requests that arrived while one was already owed — the coalesced surplus.
/// Non-zero here is what proves the coalescing is load-bearing rather than
/// the wake rate simply being too low to matter.
pub var resched_coalesced: u64 = 0;
/// Rotations that discharged a request (a nudge that did its job, or a period
/// boundary that subsumed one).
pub var resched_discharged: u64 = 0;

/// A task just became runnable while another is executing: ask the timer to
/// preempt us sooner than the next 1 Hz boundary. Called from the wake funnel
/// (`push_home_locked`), so it covers every blocked->ready transition —
/// event pushes (`sys_wait_event`), process-exit waiters, futex wakes, spawn,
/// and the app-timer/WM-pacing fires inside `on_tick`. Pure BSS writes plus a
/// comparator `msr`; safe in the SVC, IRQ and lock-held contexts those paths
/// run in (no console, no allocation, no lock).
///
/// Deliberately narrow:
/// * a no-op until preemption is armed (`start`), so boot-time wakes do not
/// fire comparators before the shell loop is the running context;
/// * core 0 only — that is the core whose PPI carries the shell/desktop
/// rotation, and the only core whose `timer.handle` consumes a nudge
/// (a secondary core re-arms without inspecting it, so a nudge armed
/// there would never be served);
/// * a wake raised from INSIDE a rotation (`on_tick`'s app timers, WM
/// pacing, `wake_expired`) is discharged free by that same rotation, so
/// the common tick-driven wake costs no extra interrupt at all.
pub fn request_resched() void {
if (!enabled_flag) return;
if (smp.core_id() != 0) return;
if (resched_requested) {
resched_coalesced +%= 1;
return;
}
resched_requested = true;
resched_requests +%= 1;
timer.nudge();
}

/// A rotation ran on core `c`: any request it was serving is discharged.
///
/// Split out of `tick` (rather than inlined at its tail) because `tick`'s
/// body is aarch64-only — it reads ELR_EL1/SPSR_EL1, which fault at EL0 — so
/// a host test cannot call it, while the coalescing rule below is precisely
/// what a host test must be able to pin. The tick wiring itself (and the
/// nudge's real comparator arithmetic) is proven live by the class-B
/// `live-wm-pacing` gate.
///
/// Core-gated: only core 0 ever raises a request, so a secondary core's
/// rotation must not swallow core 0's pending one.
pub fn discharge_resched(c: usize) void {
if (c == 0 and resched_requested) {
resched_requested = false;
resched_discharged +%= 1;
}
}
/// The idle task's static stack (BSS, like every other kernel global).
var idle_stack: [task_stack_size]u8 align(16) = undefined;
/// The monitor `spawn` command's dedicated demo stack; one spawn only, so
Expand Down Expand Up @@ -2351,7 +2438,14 @@ fn spawn_demo_entry() void {
/// stack (`exceptions.resume_frame[c]`); ELR_EL1/SPSR_EL1 still hold the
/// interrupted PC/PSTATE. The switch itself only programs ELR/SPSR and the
/// stub's restore frame — the stub does the register pop and eret.
pub fn tick() void {
/// `period_tick` is WMP card 3's distinction: TRUE when this timer PPI was
/// the 1 Hz period boundary (`timer.handle` returned true) and the wall clock
/// may advance, FALSE when it served a reschedule nudge. A nudge owes a
/// ROTATION and nothing else — the timekeeping beat (`on_tick`: tick_count,
/// the sleepers, app timers, WM pacing, CPU-limit accounting) must not run,
/// or scheduling latency would be paid for by silently running the clock
/// fast. The rotation below runs either way.
pub fn tick(period_tick: bool) void {
if (comptime builtin.cpu.arch != .aarch64) return;
if (!scheduling_active()) return;
const c = smp.core_id(); // per-core staging
Expand All @@ -2374,7 +2468,7 @@ pub fn tick() void {
// here — skipped => one 1 s cadence loss (the pre-existing skip
// semantic; claim 9498). Claim 881 slice 3: sched_lock no longer
// spans the rotation below — only this timekeeping beat.
if (c == 0 and evk_taken != null and sched_lock.try_lock()) {
if (period_tick and c == 0 and evk_taken != null and sched_lock.try_lock()) {
sched_lock_holder = smp.core_id();
on_tick();
sched_lock_release();
Expand Down Expand Up @@ -2443,6 +2537,13 @@ pub fn tick() void {
if (c != 0 and next_runnable_for(current[c], c) == null and (spsr & 0xf) != spsr_el0t_irqs) return;
timer_switch_context(exceptions.resume_frame[c], elr, spsr, exceptions.resume_sp_el0[c]);
apply_pending();
// WMP card 3: a rotation just ran, so any reschedule request it was
// serving is discharged. Clearing here rather than at entry is what makes
// a wake raised inside this same beat's `on_tick` (app timers, WM pacing,
// `wake_expired`) free: the request lands, then this discharges it, and
// no redundant nudge is armed. Nothing can wake between the rotation and
// this call — the IRQ handler is masked throughout.
discharge_resched(c);
}

/// Tick-only wrapper around the pure switch core. Keeping the source of the
Expand Down
Loading
Loading