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>
248 lines
7.4 KiB
C
248 lines
7.4 KiB
C
/*
|
||
StarForth — Steady-State Virtual Machine Runtime
|
||
|
||
Copyright (c) 2023–2025 Robert A. James
|
||
All rights reserved.
|
||
|
||
Licensed under the StarForth License, Version 1.0
|
||
*/
|
||
|
||
/**
|
||
* heartbeat.c - Shared M5 heartbeat / TIME-TRUST engine (punch-list item 0.8)
|
||
*
|
||
* One implementation of heartbeat_init()/heartbeat_ticks()/heartbeat_trust()/
|
||
* heartbeat_state(), shared by all three architectures. Each architecture's
|
||
* timer.c contributes only heartbeat_read_counter() — the one thing that is
|
||
* genuinely per-ISA (rdtsc / rdtime / CNTPCT_EL0).
|
||
*
|
||
* Top half / bottom half split (FABRIC.md §25.1 item 0.8, per the GAP-A1
|
||
* ruling in §16.4/§18.4): heartbeat_tick() is called from interrupt context
|
||
* and does nothing but read the counter, bump TIME-TICKS, and latch a
|
||
* pending sample. heartbeat_service() runs on the mainline (the REPL idle
|
||
* loop) and does the window/variance/trust work. Neither one feeds patron
|
||
* state — the engine stays on the virtual tick per §18.4, unchanged by this
|
||
* file.
|
||
*
|
||
* Adaptive re-arm period (FABRIC.md §26, ruled 2026-08-03): Loop #7
|
||
* (vm_runtime.c) computes an execution-derived stable/volatile signal and
|
||
* calls heartbeat_set_adaptive_period_ns() with it, rescaled to this file's
|
||
* kernel-appropriate base (10 ms, matching the 100 Hz rate item 0.1-0.7
|
||
* configured) rather than the hosted 10 µs HEARTBEAT_TICK_NS base. Each
|
||
* architecture's re-arm function reads heartbeat_next_period_ns() and
|
||
* converts it to raw counter units instead of using a fixed constant.
|
||
* Single writer (mainline, via vm_tick()'s Loop #7 site), single reader
|
||
* (the ISR's re-arm call) — no lock needed, per §21.1's finding that
|
||
* nothing here is actually concurrent on one hart.
|
||
*/
|
||
|
||
#include "starkernel/timer.h"
|
||
|
||
#if defined(ARCH_AMD64)
|
||
#define HEARTBEAT_HAS_VARIANCE 1
|
||
#else
|
||
#define HEARTBEAT_HAS_VARIANCE 0
|
||
#endif
|
||
|
||
/* Kernel-side adaptive-period base: 10 ms, matching the 100 Hz hardware
|
||
* rate established throughout items 0.1-0.7. Deliberately NOT
|
||
* HEARTBEAT_TICK_NS (include/starforth_config.h) -- that constant is the
|
||
* hosted pthread-worker's 10 µs base and is three orders of magnitude too
|
||
* fast for a bare-metal ISR period (FABRIC.md §26.3). Loop #7's decision
|
||
* logic is reused unmodified; only the base it scales differs. */
|
||
#define HEARTBEAT_BASE_PERIOD_NS 10000000ULL
|
||
|
||
static TimeTrustState g_heartbeat;
|
||
|
||
/* Top/bottom half handoff. */
|
||
static volatile uint64_t g_pending_counter;
|
||
static volatile int g_pending_valid;
|
||
|
||
/* Adaptive re-arm period, clamped to the same [1/4x, 4x] band Loop #7
|
||
* itself enforces on tick_target_ns, so a caller cannot runaway the ISR
|
||
* rate even if it forwarded an unclamped value. */
|
||
static volatile uint64_t g_adaptive_period_ns = HEARTBEAT_BASE_PERIOD_NS;
|
||
|
||
#if HEARTBEAT_HAS_VARIANCE
|
||
|
||
/**
|
||
* @brief Push a signed delta value into the heartbeat rolling window.
|
||
*/
|
||
static void window_push(TimeWindow *w, int64_t delta)
|
||
{
|
||
w->deltas[w->pos] = delta;
|
||
w->pos = (w->pos + 1) % TIME_WINDOW_SIZE;
|
||
if (w->count < TIME_WINDOW_SIZE) {
|
||
w->count++;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief Compute the relative variance of heartbeat deltas as a Q48.16 value.
|
||
*/
|
||
static q48_16_t window_variance_q48(const TimeWindow *w, uint64_t expected_delta)
|
||
{
|
||
if (w->count < 2 || expected_delta == 0) {
|
||
return 0;
|
||
}
|
||
|
||
int64_t sum = 0;
|
||
for (uint32_t i = 0; i < w->count; i++) {
|
||
sum += w->deltas[i];
|
||
}
|
||
int64_t mean = sum / (int64_t)w->count;
|
||
|
||
uint64_t sum_sq = 0;
|
||
for (uint32_t i = 0; i < w->count; i++) {
|
||
int64_t diff = w->deltas[i] - mean;
|
||
if (diff > 0x7FFFFFFF) diff = 0x7FFFFFFF;
|
||
if (diff < -0x7FFFFFFF) diff = -0x7FFFFFFF;
|
||
sum_sq += (uint64_t)(diff * diff);
|
||
}
|
||
uint64_t var_tsc = sum_sq / w->count;
|
||
|
||
uint64_t exp_sq = expected_delta;
|
||
if (exp_sq > 0xFFFFFFFF) {
|
||
var_tsc >>= 16;
|
||
exp_sq >>= 8;
|
||
}
|
||
exp_sq = exp_sq * exp_sq;
|
||
if (exp_sq == 0) return 0;
|
||
|
||
if (var_tsc > 0x0000FFFFFFFFFFFFULL) {
|
||
var_tsc = 0x0000FFFFFFFFFFFFULL;
|
||
}
|
||
|
||
return (var_tsc << 16) / exp_sq;
|
||
}
|
||
|
||
/**
|
||
* @brief Derive TIME-TRUST from a Q48.16 relative variance value.
|
||
*/
|
||
static q48_16_t variance_to_trust(q48_16_t variance)
|
||
{
|
||
q48_16_t denom = q48_add(Q48_ONE, variance);
|
||
if (denom == 0) {
|
||
return Q48_ONE;
|
||
}
|
||
return q48_div(Q48_ONE, denom);
|
||
}
|
||
|
||
#endif /* HEARTBEAT_HAS_VARIANCE */
|
||
|
||
/**
|
||
* @brief Initialise the M5 heartbeat / TIME-TRUST subsystem.
|
||
*
|
||
* Shared across all three architectures -- the fallback and calibration
|
||
* formula never differed between them, only what heartbeat_service() does
|
||
* with the resulting window did.
|
||
*/
|
||
void heartbeat_init(uint64_t tsc_hz, uint64_t tick_hz)
|
||
{
|
||
g_heartbeat.ticks = 0;
|
||
g_heartbeat.last_tsc = 0;
|
||
g_heartbeat.total_samples = 0;
|
||
g_heartbeat.variance = 0;
|
||
g_heartbeat.trust = Q48_ONE;
|
||
|
||
g_heartbeat.window.pos = 0;
|
||
g_heartbeat.window.count = 0;
|
||
for (int i = 0; i < TIME_WINDOW_SIZE; i++) {
|
||
g_heartbeat.window.deltas[i] = 0;
|
||
}
|
||
|
||
g_heartbeat.expected_delta = (tick_hz > 0 && tsc_hz > 0)
|
||
? (tsc_hz / tick_hz) : 10000000ULL;
|
||
|
||
g_pending_valid = 0;
|
||
g_adaptive_period_ns = HEARTBEAT_BASE_PERIOD_NS;
|
||
}
|
||
|
||
void heartbeat_tick(void)
|
||
{
|
||
g_pending_counter = heartbeat_read_counter();
|
||
g_heartbeat.ticks++;
|
||
g_pending_valid = 1;
|
||
}
|
||
|
||
void heartbeat_service(void)
|
||
{
|
||
if (!g_pending_valid) {
|
||
return;
|
||
}
|
||
uint64_t now = g_pending_counter;
|
||
g_pending_valid = 0;
|
||
|
||
TimeTrustState *s = &g_heartbeat;
|
||
s->total_samples++;
|
||
|
||
if (s->total_samples == 1) {
|
||
s->last_tsc = now;
|
||
return;
|
||
}
|
||
|
||
uint64_t actual_delta = now - s->last_tsc;
|
||
s->last_tsc = now;
|
||
int64_t deviation = (int64_t)actual_delta - (int64_t)s->expected_delta;
|
||
|
||
#if HEARTBEAT_HAS_VARIANCE
|
||
window_push(&s->window, deviation);
|
||
s->variance = window_variance_q48(&s->window, s->expected_delta);
|
||
s->trust = variance_to_trust(s->variance);
|
||
#else
|
||
/* RISC-V `time` / AArch64 CNTPCT_EL0 are architecturally invariant
|
||
* counters -- no statistical quality estimate needed (matches the
|
||
* per-arch rationale this file replaces). */
|
||
(void)deviation;
|
||
s->trust = Q48_ONE;
|
||
#endif
|
||
}
|
||
|
||
uint64_t heartbeat_ticks(void)
|
||
{
|
||
return g_heartbeat.ticks;
|
||
}
|
||
|
||
time_trust_t heartbeat_trust(void)
|
||
{
|
||
return g_heartbeat.trust;
|
||
}
|
||
|
||
const TimeTrustState *heartbeat_state(void)
|
||
{
|
||
return &g_heartbeat;
|
||
}
|
||
|
||
/**
|
||
* @brief Set the adaptive re-arm period (FABRIC.md §26).
|
||
*
|
||
* Called from vm_runtime.c's Loop #7 site on the mainline execution path
|
||
* (never interrupt context) with a value already rescaled to this file's
|
||
* HEARTBEAT_BASE_PERIOD_NS. Clamped defensively to the same [1/4x, 4x]
|
||
* band Loop #7 itself enforces around its own base, so a caller forwarding
|
||
* an unclamped or wrongly-scaled value cannot run the ISR away.
|
||
*
|
||
* @param ns Desired period in nanoseconds for the next re-arm.
|
||
*/
|
||
void heartbeat_set_adaptive_period_ns(uint64_t ns)
|
||
{
|
||
uint64_t lo = HEARTBEAT_BASE_PERIOD_NS / 4;
|
||
uint64_t hi = HEARTBEAT_BASE_PERIOD_NS * 4;
|
||
if (ns < lo) ns = lo;
|
||
if (ns > hi) ns = hi;
|
||
g_adaptive_period_ns = ns;
|
||
}
|
||
|
||
/**
|
||
* @brief Read the period the next hardware re-arm should use.
|
||
*
|
||
* Called from interrupt context by each architecture's re-arm function
|
||
* (apic_timer_rearm(), riscv64_timer_rearm(), the AArch64 equivalent) in
|
||
* place of the fixed constant those functions used before item 0.8.
|
||
*
|
||
* @return Current adaptive period in nanoseconds.
|
||
*/
|
||
uint64_t heartbeat_next_period_ns(void)
|
||
{
|
||
return g_adaptive_period_ns;
|
||
}
|