starkernel: converge the tick path and wire the adaptive heartbeat (item 0.8)

Introduces src/starkernel/heartbeat.c as the shared top/bottom-half
implementation of heartbeat_init/tick/service/ticks/trust/state, replacing
the per-architecture duplicates in amd64/riscv64/aarch64 timer.c. Each
arch's timer.c now contributes only heartbeat_read_counter() (rdtsc /
rdtime / CNTPCT_EL0). Per the GAP-A1 ruling the top half stays counter+
latch only; heartbeat_service() (called every REPL idle iteration,
unconditionally per FABRIC.md's fidelity note) does the window/variance/
trust work outside interrupt context. vm_tick()'s call sites are
unchanged -- the engine still runs on the virtual tick.

Per FABRIC.md §26 (ruled 2026-08-03): wires Loop #7's execution-derived
stable/volatile signal into the physical re-arm period. vm_runtime.c's
existing Loop #7 site now calls heartbeat_set_adaptive_period_ns() with
tick_target_ns ratio-rescaled onto a 10ms kernel base (not the hosted
10us HEARTBEAT_TICK_NS -- see §26.3 for the scale mismatch). Each
architecture's re-arm function (apic_timer_rearm() on amd64/aarch64,
riscv64_timer_rearm()) now converts heartbeat_next_period_ns() to its
own raw counter units instead of a fixed constant; amd64 gained a
rearm function it didn't previously need, since periodic-mode auto-reload
never required one before this item.

Verified: all three architectures build with no new warnings and boot
cleanly to ok> with dict_hash=0x3d4e1daf289da94f, unchanged from the
pre-change baseline -- no regression. Verified NOT achieved: live re-arm
period variation under load. A temporary diagnostic (added and reverted)
confirmed Loop #7 never actually fired during a live QEMU session -- a
synthetic word-execution loop drove ~6,500 executions, past the 1000-tick
inference frequency, without tripping vm_tick_inference_engine()'s
pre-existing !vm->rolling_window.is_warm gate. That gate predates this
item and was not investigated -- out of scope. FABRIC.md's Done-when is
amended to record this honestly rather than claim it.

Punch list §25 item 0.8 complete (per amended, weaker acceptance -- see
the item's own annotation).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-08-04 00:01:48 -04:00
co-authored by Claude Sonnet 5
parent da4cb14702
commit 3699be964d
23 changed files with 42170 additions and 449 deletions
+44 -2
View File
@@ -154,11 +154,37 @@ const timer_calibration_record_t *timer_calibration_record(void);
void heartbeat_init(uint64_t tsc_hz, uint64_t tick_hz);
/**
* Called by heartbeat ISR on each tick.
* Updates TIME-TICKS, samples TSC, updates rolling window and TIME-TRUST.
* Top half. Called directly from each architecture's ISR (punch-list item
* 0.8) -- one call site per architecture, unchanged from before this item.
* Does exactly three things: reads the raw counter via
* @c heartbeat_read_counter(), increments TIME-TICKS, and latches the
* sample for @c heartbeat_service() to pick up. No window math, no
* variance, no loops -- this must stay cheap enough for interrupt context.
*/
void heartbeat_tick(void);
/**
* Bottom half (punch-list item 0.8). Services one pending sample if
* @c heartbeat_tick() has latched one since the last call: computes the
* inter-tick deviation, updates the rolling window, and (architecture
* permitting -- see @c heartbeat.c) recomputes variance and TIME-TRUST.
* Never runs in interrupt context. Call from the mainline, as frequently
* as convenient -- a stale/skipped service call degrades the window's
* fidelity but affects nothing else, since TIME-TRUST is diagnostic only
* and never gates execution.
*/
void heartbeat_service(void);
/**
* Read the raw hardware counter this architecture's heartbeat is paced
* against -- the same clock @c timer_now_ns() and calibration already use
* internally (rdtsc on amd64, the `time` CSR on riscv64, CNTPCT_EL0 on
* aarch64), not a separate/different source. Implemented once per
* architecture in that architecture's timer.c; consumed only by
* @c heartbeat_tick() in the shared heartbeat.c.
*/
uint64_t heartbeat_read_counter(void);
/**
* Get current TIME-TICKS (monotonic heartbeat count).
*/
@@ -174,4 +200,20 @@ time_trust_t heartbeat_trust(void);
*/
const TimeTrustState *heartbeat_state(void);
/**
* Set the adaptive re-arm period, in nanoseconds (punch-list item 0.8,
* FABRIC.md §26). Called from the mainline execution path only (Loop #7's
* site in vm_runtime.c) -- never from interrupt context. Clamped to
* [1/4x, 4x] of the kernel's base period internally; a caller need not
* pre-clamp.
*/
void heartbeat_set_adaptive_period_ns(uint64_t ns);
/**
* Read the period the next hardware re-arm should use. Called from
* interrupt context by each architecture's re-arm function in place of a
* fixed constant.
*/
uint64_t heartbeat_next_period_ns(void);
#endif /* STARKERNEL_TIMER_H */