Files
LithosAnanke/kernel/src/heartbeat.c
T
Claude 16b9747d33 chore(license): the source headers name the Starship License 1.0 (SL-1.0), as LICENSE does [skip ci]
347 headers said "Licensed under the StarForth License, Version 1.0" -- a
shortening (Captain Bob, 2026-11-08: "License file SL I think I shortened
accidently"); LICENSE is, and has been since the first commit, the
Starship License 1.0 (SL-1.0, SPDX LicenseRef-Starship-1.0). 349 files,
566 occurrences, now "Starship License, Version 1.0 (SL-1.0)".

Left as they are, pending a ruling: docs/patent/ (the patent figures),
docs/formal/ (the citable volumes) and the generated reports under
docs/working/experiments/ (32 files). Comments and license text only:
the amd64 kernel builds, every changed script parses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GE3SDvQn5EoxdGufk84zz2
2026-11-08 17:54:24 +00:00

263 lines
8.4 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
Licensed under the Starship License, Version 1.0 (SL-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-0.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-0.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"
#include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.md §XXVIII Stage 3 */
#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-0.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;
/* FABRIC-3.md §XXVIII, Stage 3 (2026-09-13): the one deliberate,
* documented amendment to this file's own top-half/bottom-half
* discipline above. sk_vm_switch_signal_tick() reaches into VM
* scheduling state from real ISR context -- something this file's
* header comment previously said nothing here did. It stays cheap
* (bounded iteration over a small slot table, no locking needed for
* the same reason nothing else here needs it: single hart, nothing
* concurrent with this ISR while it runs) and it never itself
* switches anything -- it only ever sets a flag, consumed later at a
* safe cooperative checkpoint on the mainline
* (execute_colon_word()). The GAP-A1 split itself is not overturned:
* everything else in this function is exactly as minimal as before. */
sk_vm_switch_signal_tick();
}
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-0.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;
}