Skip to content
Merged
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
28 changes: 27 additions & 1 deletion docs/adr/0022-com2-latency-scheduler-bound.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,33 @@
# 22. COM2 Round-Trip Latency Is Scheduler-Cadence-Bound

Date: 2026-07-11
Status: Accepted (2026-07-13; the deferral decision + both prerequisites are complete — the latency fix itself remains a deliberately deferred future epic)
Status: Accepted (2026-07-15; the latency fix SHIPPED — the I/O-priority boost, see the final update)

> **Update (2026-07-15) — the deferred epic is implemented: the I/O-priority boost.**
> The option table's principled fix shipped, hardened by a 4-lens adversarial design panel
> (31 findings: 7 blockers, 15 majors — all folded in). Mechanism: a per-PCB `io_boost` flag;
> set (a) at the end of every successful `ipc_send` for a READY receiver (own irq bracket —
> ipc_send had no existing one) and (b) from `scheduler_tick` when the registered COM2 holder
> has RX pending. `pick_next`'s DEFAULT round-robin arm prefers a boosted READY process;
> the flag is consumed only at the dispatch COMMIT (pick_next stays pure — it is also used
> as a bare predicate by the CPUKILL pre-check). Guards the panel forced: a sticky 0xFF
> absent-device latch (an unbacked COM2 port reads all-ones — the boost would otherwise fire
> every tick of every COM2-less CI boot); `SCHED_BOOST_MAX_CONSEC=2` (a boost ping-pong would
> otherwise monopolize every yield-path reschedule AND, by resetting the quantum counter,
> keep quantum expiry itself from firing — starving the roster invisibly to every liveness
> floor); the COM2-holder registration is (pid, generation) + live-cap-validated at the boost
> site and cleared in process_destroy (a bare pid would boost whatever recycles it — the
> ADR-0023 lesson); `boost_count` counts HONORED picks at the commit (a set-side counter is
> the switch_count vacuity trap). Gates re-armed in the same increment: ci-smoke-latency now
> asserts PING median < 0.30 s (boost-off phase-locks to the full ~0.45 s rotation — fail-side
> structural on any host) AND a positive honored-boost delta (binding effect to mechanism),
> plus STATUS median < 0.6 s with a 3-of-5 success floor; ci-smoke-sched LOAD mode paces its
> ping load at a fixed cadence (boost-invariant load profile) and arms a max_gap <= 600-tick
> starvation ceiling — the only metric that can referee the boost's fairness guard.
> Revert-confirm: `SCHED_IO_BOOST 0` reddens both latency assertions while ci-smoke-sched
> stays green (the preempt band is structurally boost-invariant: the expiry increment precedes
> the pick). The boost carries no authority (caps still gate every action) and no audit event
> (it would flood the 256-entry ring); its instrument is the counter.

> **Update (2026-07-11).** Prerequisite 1 below — a bounded latency assertion — **shipped** as
> `ci-smoke-latency` (`scripts/test_qos_latency.py`, via the new `QosVM.ping()` one-hop primitive).
Expand Down
9 changes: 9 additions & 0 deletions kernel/include/kernel/com2_uart.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,13 @@ uint32_t com2_write(const uint8_t *buf, uint32_t len);
* number of bytes read (0 if none waiting). */
uint32_t com2_read(uint8_t *buf, uint32_t len);

/* True iff a COM2 RX byte is waiting (ADR-0022 I/O boost). ONE LSR read,
* safe for scheduler_tick's IRQ context: reading LSR never consumes data.
* Returns 0 — permanently, via a sticky latch — when the device is ABSENT:
* an unbacked port reads 0xFF (all error bits + TEMT + THRE + DR at once,
* impossible on real silicon), and nearly every CI boot wires only COM1, so
* without the latch the boost would fire on every tick of every functional
* gate. Also 0 when COM2 is uninitialized or the TX-wedge latch tripped. */
int com2_rx_pending(void);

#endif /* COM2_UART_H */
8 changes: 8 additions & 0 deletions kernel/include/kernel/process.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ typedef struct process {
uint64_t last_scheduled; /* Timer tick at which the process was last picked (ADR-0022) */
uint64_t
sched_picks; /* Count of times the scheduler has picked this process (ADR-0022 fairness) */
/* ADR-0022 I/O boost: this process's awaited I/O (an IPC delivery, or a
* COM2 RX byte for the registered holder) arrived — pick it out of turn
* at the next reschedule. Advisory: pick_next still requires READY +
* context_valid; consumed (cleared) only at dispatch COMMIT in
* scheduler_reschedule/scheduler_kill_current, so a predicate call can
* never eat it and a process dispatched by ANY path sheds a stale flag.
* Zeroed for every incarnation by init_process_pcb's memset. */
uint8_t io_boost;

/* IPC integration - queues managed internally by PID via ipc_process_init() */
uint32_t port_count; /* Number of owned IPC ports */
Expand Down
44 changes: 44 additions & 0 deletions kernel/include/kernel/scheduler.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,27 @@
/* Scheduling quantum in timer ticks (ticks are 10 ms at TIMER_DEFAULT_HZ) */
#define SCHED_QUANTUM_TICKS 5

/* ADR-0022 I/O-priority boost (the deferred latency epic). When 1, a process
* whose awaited I/O just arrived — an IPC message delivered to its mailbox,
* or a COM2 RX byte for the registered COM2 holder — is flagged io_boost and
* picked out of turn at the next reschedule (default round-robin arm only),
* collapsing a COM2 hop from ~one full rotation (~450 ms at rest) to
* tick-detect (<=10 ms) + one quantum remainder (<=50 ms). 0 restores the
* exact pre-epic pick behavior — the latency gate's revert-confirm flag. */
#define SCHED_IO_BOOST 1

/* Max CONSECUTIVE boosted picks before one plain round-robin pick is forced
* (the starvation guard). Two IPC-chatty peers ping-ponging sends would
* otherwise monopolize every yield-path reschedule AND — because each yield
* resets the global quantum counter — keep quantum expiry (and preempt_count,
* and the watchdog's turn) from ever firing. K=2 guarantees >=1 plain pick
* per 3, so a rotation of N runnable completes within ~3N picks and max_gap
* stays within a small multiple of today's envelope (refereed by the armed
* maxgap ceiling in ci-smoke-sched); a 1-boost PING hop rides through
* untouched and STATUS hops absorb at most a couple of plain slices, well
* inside the armed 0.6 s bound. */
#define SCHED_BOOST_MAX_CONSEC 2

/* Attach the scheduler to the timer tick. Call after process_init(). */
status_t scheduler_init(void);

Expand All @@ -36,6 +57,29 @@ uint64_t scheduler_get_switches(void);
* host-invariant quantity a scheduler baseline is built on (see scheduler.c). */
uint64_t scheduler_get_preempts(void);

/* HONORED I/O boosts (ADR-0022): counted only when a boosted pick is actually
* COMMITTED to the interrupt frame — never on flag-sets — so the counter is
* non-vacuous by construction (the switch_count lesson: a set-side counter
* reads nonzero while the pick path is dead). The latency gate asserts a
* positive delta across its ping batch to bind the measured latency win to
* the boost mechanism rather than an unrelated cadence change. */
uint64_t scheduler_get_boosts(void);

/* Flag `pid` for one out-of-turn pick iff it is currently READY (ADR-0022).
* Called by ipc_send after a successful enqueue (inside the caller's irq
* bracket) and by the COM2 RX check in scheduler_tick. Advisory only: worst
* case is one extra or one missed boost; never corrupts scheduling state. */
void scheduler_boost_if_ready(uint32_t pid);

/* Register/clear the COM2-holder for the RX boost, as a (pid, generation)
* pair — a bare pid would re-attach to whatever recycles it (the ADR-0023
* lesson). Set at every start_slot grant_com2 mint (overwrite semantics, so
* a watchdog rebirth re-registers structurally); cleared in process_destroy.
* The boost site additionally validates the live COM2 device cap, keeping
* the capability table the single source of truth for the authority. */
void scheduler_com2_holder_set(uint32_t pid, uint32_t generation);
void scheduler_com2_holder_clear(uint32_t pid);

/* ADR-0022 prereq-2 fairness/tail snapshot over the runnable ring-3 citizens.
* Writes max reschedule gap (max now - last_scheduled), the run-count spread
* (max - min sched_picks), and the runnable count. Read-only; `now` is the
Expand Down
22 changes: 22 additions & 0 deletions kernel/src/com2_uart.c
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,25 @@ uint32_t com2_read(uint8_t *buf, uint32_t len) {
}
return n;
}

/* Sticky: set once LSR reads 0xFF — the unbacked-port signature (all error
* bits + TEMT + THRE + DR simultaneously, impossible on real silicon). QEMU
* boots with a single -serial (nearly every functional CI gate, and the
* qBraid/laptop boots) leave 0x2F8 unassigned, and unassigned I/O reads
* all-ones — so without this latch the ADR-0022 RX-boost check would see
* DATA_READY on EVERY tick of every COM2-less boot and hand swarm-svc a
* permanent unearned scheduling skew. Latched once; one io_inb saved per
* tick thereafter. */
static uint8_t com2_absent = 0;

int com2_rx_pending(void) {
if (!com2_ready || com2_dead || com2_absent) {
return 0;
}
uint8_t lsr = io_inb(COM2_PORT_BASE + UART_LSR);
if (lsr == 0xFF) {
com2_absent = 1;
return 0;
}
return (lsr & LSR_DATA_READY) ? 1 : 0;
}
15 changes: 15 additions & 0 deletions kernel/src/ipc/ipc.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <kernel/boot.h>
#include <kernel/memory.h>
#include <kernel/process.h>
#include <kernel/scheduler.h> /* scheduler_boost_if_ready (ADR-0022 I/O boost) */

/* ============================================================================
* Internal Constants
Expand Down Expand Up @@ -434,6 +435,20 @@ ipc_result_t ipc_send(uint32_t receiver_id, const ipc_message_t *msg, uint64_t t

if (result == IPC_SUCCESS) {
ipc_global_stats.total_sent++;
/* I/O-priority boost (ADR-0022): the receiver's awaited message just
* landed — flag it for one out-of-turn pick so the hop costs a
* reschedule, not a full round-robin rotation (~450 ms at rest).
* Only on SUCCESS (a dropped/full-queue send must not boost), only
* if READY (checked inside), and in its OWN irq bracket: ipc_send is
* otherwise unbracketed and today's callers are all cli'd syscalls,
* but IF=1 kernel-thread senders are documented-plausible (the
* health monitor / net thread pattern) and an unbracketed
* check-then-set racing the timer IRQ could stamp the flag on a PCB
* the CPUKILL latch just terminated. Advisory flag: worst case is
* one extra or one missed boost, never corrupted scheduling state. */
uint64_t bflags = ipc_irq_save();
scheduler_boost_if_ready(receiver_id);
ipc_irq_restore(bflags);
}

return result;
Expand Down
7 changes: 7 additions & 0 deletions kernel/src/process.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include <kernel/capability.h>
#include <kernel/manifest.h>
#include <kernel/interrupts.h> /* timer_get_ticks() for the ADR-0022 sched timing seam */
#include <kernel/scheduler.h> /* scheduler_com2_holder_clear (ADR-0022 RX boost) */
#include <kernel/qpu.h>
#include <kernel/quantum.h>
#include <kernel/gdt.h>
Expand Down Expand Up @@ -478,6 +479,12 @@ status_t process_destroy(uint32_t pid) {
process->capability_root = CAP_ID_INVALID;
process->capability_count = 0;

/* Drop the ADR-0022 COM2-RX boost registration if this pid held it —
* same pid-reuse funnel argument as the unlink above: a recycled pid
* must never inherit the holder's scheduling boost. (No-op for every
* process but the COM2 holder; start_slot re-registers on rebirth.) */
scheduler_com2_holder_clear(pid);

/* Clear the intent manifest with the caps (epic #135). INVARIANT: this
* must run before state=UNUSED below — every pid-reuse path funnels
* through process_destroy, so clear-here + bind-before-schedule is what
Expand Down
Loading
Loading