feat(sched): scheduler perf/stability baseline recorder (ADR-0022 prereq 2, PR-1) - #217
Merged
Conversation
…req 2, PR-1) ADR-0022 defers the COM2-latency fix behind two prerequisites; prereq 1 (the latency gate) shipped. This is prereq 2: a scheduler perf/stability baseline so a future scheduler-constant change is MEASURED, not blind (ADR-0016). Measurement-only — no timer HZ / SCHED_QUANTUM_TICKS / service-model change. An adversarial design panel refuted the naive metric: aggregate context switches are dominated by voluntary yield()s (every long-lived citizen busy- yields with a non-blocking recv, resetting quantum_counter thousands of times per 10 ms tick), so switch_count/tick is BOTH quantum-insensitive (a gate on it is vacuous) AND host-throughput dependent (CI-flaky). The fix is a dedicated preempt_count in the scheduler, incremented ONLY at the timer-quantum-expiry reschedule in scheduler_tick (never on SYS_YIELD), which is 1/quantum-paced by construction. Exposure (measurement-only, minimal): - scheduler.c: preempt_count + scheduler_get_preempts() and a read-only scheduler_get_fairness() (max reschedule gap, run-count spread, runnable count over the roster). - process.c: the dead `now = 0 /* TODO */` timing stub in process_switch_to is made live (timer_get_ticks()), and a per-PCB sched_picks counter added, so last_scheduled/runtime_* stop being always-zero. - SYSINFO_SCHED sub-op (no new syscall -> no ADR-0020 golden churn) emits one atomic line "SCHED: switches=.. preempt=.. ticks=.. maxgap=.. spread=.. runnable=.."; qsh `sched` command (argument-free); QosVM.sched() in the host bridge. Gate `make ci-smoke-sched` (CI job scheduler-baseline, NOT release-gating) runs scripts/test_qos_sched.py in REPORT-ONLY mode: records the tick-normalized baseline and asserts only a non-vacuous liveness floor (>=100 switches and >=12 runnable over the window), so it reddens on a dead/wedged scheduler but arms no perf band yet. Empirical calibration (WSL, this session) SELECTED the follow-up's gated scalar — preempt-per-1000-guest-ticks tracks 1/quantum almost exactly, host-invariant: q=5 (default): 199.5-200.0 (idle+load) switch/1000t ~520 q=20: 49.9, 49.8 switch/1000t ~131 q=80: harness times out (system too sluggish to fill the window) So a 4x quantum change moves it 4x with <0.5% variance. The follow-up arms a floor of ~100 (= q5-median x 0.5), revert-confirmed by a SCHED_QUANTUM_TICKS bump to 20 (q=80 grinds the harness itself, so 20 is the practical lever). Build clean under -Werror; format/cppcheck/api-consistency/YAML green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
PR-1 of ADR-0022 prerequisite 2 — a scheduler perf/stability baseline so a future scheduler-constant change (the deferred COM2-latency fix) is measured, not blind (ADR-0016). Measurement-only: no timer HZ /
SCHED_QUANTUM_TICKS/ service-model change.The design panel earned its keep
An adversarial design panel refuted the obvious metric: aggregate context switches are dominated by voluntary
yield()s — every long-lived citizen busy-yields with a non-blocking recv, resettingquantum_counterthousands of times per 10 ms tick, so the timer-preemption path is nearly dead at rest.switch_count/tickis therefore both quantum-insensitive (a gate on it is vacuous) and host-throughput dependent (CI-flaky). The fix is a dedicatedpreempt_countincremented only at the timer-quantum-expiry reschedule inscheduler_tick(never onSYS_YIELD) — 1/quantum-paced by construction.Instrumentation (minimal, measurement-only)
scheduler.c:preempt_count+scheduler_get_preempts(), and a read-onlyscheduler_get_fairness()(max reschedule gap, run-count spread, runnable count).process.c: the deadnow = 0 /* TODO */stub inprocess_switch_togoes live (timer_get_ticks()), plus a per-PCBsched_pickscounter — solast_scheduled/runtime_*stop being always-zero.SYSINFO_SCHEDsub-op (no new syscall → no ADR-0020 golden churn): one atomic lineSCHED: switches=.. preempt=.. ticks=.. maxgap=.. spread=.. runnable=.., driven by a qshschedcommand (argument-free) andQosVM.sched().Report-only gate
make ci-smoke-sched(CI jobscheduler-baseline, not release-gating) runsscripts/test_qos_sched.pyin report-only mode: it records the tick-normalized baseline and asserts only a non-vacuous liveness floor (≥100 switches and ≥12 runnable over the window), so it reddens on a dead/wedged scheduler but arms no perf band yet.Calibration selected PR-2's gated scalar
Empirical WSL calibration — preempt-per-1000-guest-ticks tracks 1/quantum almost exactly, host-invariantly:
SCHED_QUANTUM_TICKSA 4× quantum change moves it 4× with <0.5 % variance. The follow-up arms a floor of ~100 (= q5-median × 0.5), revert-confirmed by a
SCHED_QUANTUM_TICKSbump to 20 (q=80 grinds the harness itself, so 20 is the practical lever).Verification
-Werror;clang-format/cppcheck/check-api-consistency.sh/ YAML all green.make ci-smoke-schedpasses locally (idle P=199.9, load P=200, liveness floor asserted).🤖 Generated with Claude Code