790 lines
29 KiB
C
790 lines
29 KiB
C
/*
|
||
StarForth — Steady-State Virtual Machine Runtime
|
||
|
||
Copyright (c) 2023–2025 Robert A. James
|
||
All rights reserved.
|
||
|
||
This file is part of the StarForth project.
|
||
|
||
Licensed under the StarForth License, Version 1.0 (the "License");
|
||
you may not use this file except in compliance with the License.
|
||
|
||
You may obtain a copy of the License at:
|
||
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
|
||
|
||
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||
express or implied, including but not limited to the warranties of
|
||
merchantability, fitness for a particular purpose, and noninfringement.
|
||
|
||
See the License for the specific language governing permissions and
|
||
limitations under the License.
|
||
|
||
StarForth — Steady-State Virtual Machine Runtime
|
||
Copyright (c) 2023–2025 Robert A. James
|
||
All rights reserved.
|
||
|
||
This file is part of the StarForth project.
|
||
|
||
Licensed under the StarForth License, Version 1.0 (the "License");
|
||
you may not use this file except in compliance with the License.
|
||
|
||
You may obtain a copy of the License at:
|
||
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
|
||
|
||
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||
express or implied, including but not limited to the warranties of
|
||
merchantability, fitness for a particular purpose, and noninfringement.
|
||
|
||
See the License for the specific language governing permissions and
|
||
limitations under the License.
|
||
|
||
*/
|
||
|
||
/*
|
||
* vm_time.c - Heartbeat, timers, entropy/heat tracking, decay
|
||
*
|
||
* Time is a first-class liar — isolated here to keep the James Law
|
||
* machinery from infecting everything else.
|
||
*
|
||
* Contains:
|
||
* - heartbeat logic
|
||
* - timers
|
||
* - entropy / heat tracking
|
||
* - decay / accumulation
|
||
* - any "tick" based logic
|
||
*/
|
||
|
||
#include "vm_internal.h"
|
||
#include "../include/inference_engine.h"
|
||
#include "../include/log.h"
|
||
#include "../include/physics_metadata.h"
|
||
#include "../include/physics_hotwords_cache.h"
|
||
#include "../include/rolling_window_of_truth.h"
|
||
#include "../include/dictionary_heat_optimization.h"
|
||
#include "../include/ssm_jacquard.h"
|
||
#include "../include/platform_alloc.h"
|
||
|
||
#include <string.h>
|
||
#include <errno.h>
|
||
#include <time.h>
|
||
|
||
/* ====================== Forward Declarations ======================= */
|
||
|
||
static void vm_tick_apply_background_decay(VM *vm, uint64_t now_ns);
|
||
static void vm_heartbeat_update_l8(VM *vm);
|
||
|
||
#if HEARTBEAT_THREAD_ENABLED
|
||
static void* heartbeat_thread_main(void *arg);
|
||
#endif
|
||
|
||
/* ====================== Snapshot Publishing ======================= */
|
||
|
||
void vm_heartbeat_publish_snapshot(VM *vm)
|
||
{
|
||
if (!vm)
|
||
return;
|
||
|
||
uint32_t current = heartbeat_snapshot_index_load(&vm->heartbeat.snapshot_index) & 1u;
|
||
uint32_t next = current ^ 1u;
|
||
HeartbeatSnapshot *snapshot = &vm->heartbeat.snapshots[next];
|
||
|
||
snapshot->published_tick = vm->heartbeat.tick_count;
|
||
snapshot->published_ns = sf_monotonic_ns();
|
||
snapshot->window_width = vm->rolling_window.effective_window_size;
|
||
snapshot->actual_window_size = (uint32_t)(vm->rolling_window.total_executions < ROLLING_WINDOW_SIZE
|
||
? vm->rolling_window.total_executions
|
||
: ROLLING_WINDOW_SIZE);
|
||
snapshot->decay_slope_q48 = vm->decay_slope_q48;
|
||
snapshot->hot_word_count = vm->hot_word_count_at_check;
|
||
snapshot->stale_word_count = vm->stale_word_count_at_check;
|
||
snapshot->total_heat = vm->total_heat_at_last_check;
|
||
|
||
heartbeat_snapshot_index_store(&vm->heartbeat.snapshot_index, next);
|
||
}
|
||
|
||
/* ====================== VM Heartbeat (Time-Driven Tuning) ======================= */
|
||
|
||
/**
|
||
* @brief Central heartbeat dispatcher for time-driven tuning operations
|
||
*
|
||
* Aggregates all periodic optimization tasks (Loop #3 and Loop #5) into one place.
|
||
* Designed as plugin architecture - new tuning operations can be added as plugins.
|
||
*
|
||
* Options for integration:
|
||
* - Synchronous (now): Called from main execution loop, every N executions
|
||
* - Background thread (future): Runs in separate thread, decoupled from VM execution
|
||
*
|
||
* @param vm Pointer to VM instance
|
||
*/
|
||
void vm_tick(VM* vm)
|
||
{
|
||
if (!vm || !vm->heartbeat.heartbeat_enabled)
|
||
return;
|
||
|
||
vm->heartbeat.tick_count++;
|
||
|
||
/* Unified Inference Engine (Phase 2: Replaces Loops #3 & #5)
|
||
* Runs every HEARTBEAT_INFERENCE_FREQUENCY ticks to infer optimal:
|
||
* - Window width (via variance inflection detection)
|
||
* - Decay slope (via exponential regression on heat trajectory)
|
||
*/
|
||
if ((vm->heartbeat.tick_count - vm->heartbeat.last_inference_tick) >= HEARTBEAT_INFERENCE_FREQUENCY)
|
||
{
|
||
vm_tick_inference_engine(vm);
|
||
}
|
||
|
||
/* Plugin 2: System State Monitoring (Future) */
|
||
/* vm_tick_system_monitor(vm); */
|
||
|
||
/* Plugin 3: Formal Verification State Update (Future) */
|
||
/* vm_tick_formal_state_sync(vm); */
|
||
}
|
||
|
||
/**
|
||
* @brief Loop #5: Context-aware window tuning via binary chop search
|
||
*
|
||
* Uses prefetch accuracy to guide window size adaptation.
|
||
* Binary search converges on optimal effective_window_size for current workload.
|
||
*
|
||
* @param vm Pointer to VM instance
|
||
*/
|
||
void vm_tick_window_tuner(VM* vm)
|
||
{
|
||
if (!vm || !vm->rolling_window.is_warm || !ENABLE_PIPELINING)
|
||
return;
|
||
|
||
RollingWindowOfTruth *window = &vm->rolling_window;
|
||
PipelineGlobalMetrics *metrics = &vm->pipeline_metrics;
|
||
|
||
/* Calculate current prefetch accuracy */
|
||
if (metrics->prefetch_attempts == 0)
|
||
return; /* Not enough data yet */
|
||
|
||
double current_accuracy = (double)metrics->prefetch_hits / (double)metrics->prefetch_attempts;
|
||
|
||
/* Binary chop suggests next window size to try */
|
||
uint32_t suggested_size = window->effective_window_size; /* Default: no change */
|
||
|
||
if (metrics->window_tuning_checks == 0)
|
||
{
|
||
/* First check: try shrinking by 25% */
|
||
suggested_size = (window->effective_window_size * 75) / 100;
|
||
}
|
||
else
|
||
{
|
||
/* Compare current accuracy to last check */
|
||
double accuracy_delta = current_accuracy - metrics->last_checked_accuracy;
|
||
|
||
if (accuracy_delta > 0.01) /* Improvement threshold: 1% */
|
||
{
|
||
/* Accuracy improved! Try shrinking more */
|
||
uint32_t smaller = (window->effective_window_size * 75) / 100;
|
||
suggested_size = (smaller > ADAPTIVE_MIN_WINDOW_SIZE) ? smaller : ADAPTIVE_MIN_WINDOW_SIZE;
|
||
}
|
||
else if (accuracy_delta < -0.01)
|
||
{
|
||
/* Accuracy degraded. Try growing instead */
|
||
uint32_t larger = (window->effective_window_size * 133) / 100; /* Grow by ~33% */
|
||
suggested_size = (larger < ROLLING_WINDOW_SIZE) ? larger : ROLLING_WINDOW_SIZE;
|
||
}
|
||
/* else: Plateau, stick with current size */
|
||
}
|
||
|
||
/* Apply if different */
|
||
if (suggested_size != window->effective_window_size)
|
||
{
|
||
log_message(LOG_DEBUG,
|
||
"HEARTBEAT[window]: %u → %u (accuracy %.2f%%, %lu/%lu prefetch hits)",
|
||
window->effective_window_size,
|
||
suggested_size,
|
||
current_accuracy * 100.0,
|
||
metrics->prefetch_hits,
|
||
metrics->prefetch_attempts);
|
||
|
||
window->effective_window_size = suggested_size;
|
||
}
|
||
|
||
/* Record for next iteration */
|
||
metrics->last_checked_window_size = window->effective_window_size;
|
||
metrics->last_checked_accuracy = current_accuracy;
|
||
metrics->window_tuning_checks++;
|
||
}
|
||
|
||
/**
|
||
* @brief Loop #3: Heat decay slope validation via periodic measurement
|
||
*
|
||
* Validates that linear decay is actually helping optimize dictionary caching.
|
||
* Measures stale word ratio, hot word count, and average heat distribution.
|
||
*
|
||
* @param vm Pointer to VM instance
|
||
*/
|
||
void vm_tick_slope_validator(VM* vm)
|
||
{
|
||
if (!vm)
|
||
return;
|
||
|
||
/* Collect snapshot of current state */
|
||
uint64_t hot_word_count = 0;
|
||
uint64_t stale_word_count = 0;
|
||
uint64_t total_heat = 0;
|
||
uint32_t word_count = 0;
|
||
|
||
/* Scan dictionary and categorize words by heat level */
|
||
sf_mutex_lock(&vm->dict_lock);
|
||
for (DictEntry *e = vm->latest; e != NULL; e = e->link)
|
||
{
|
||
if (e->execution_heat > HOTWORDS_EXECUTION_HEAT_THRESHOLD)
|
||
hot_word_count++;
|
||
else if (e->execution_heat > 0 && e->execution_heat < 10)
|
||
stale_word_count++;
|
||
|
||
total_heat += e->execution_heat;
|
||
word_count++;
|
||
}
|
||
sf_mutex_unlock(&vm->dict_lock);
|
||
|
||
double avg_heat = (word_count > 0) ? (double)total_heat / (double)word_count : 0.0;
|
||
double stale_ratio = (word_count > 0) ? (double)stale_word_count / (double)word_count : 0.0;
|
||
|
||
/* === LOOP #3: INFERENCE ENGINE ===
|
||
* Compare current measurements to baseline from last check
|
||
* Decide whether decay is too fast, too slow, or optimal
|
||
*/
|
||
int new_slope_direction = 0; /* -1: decrease slope, 0: stable, +1: increase slope */
|
||
|
||
sf_mutex_lock(&vm->tuning_lock);
|
||
|
||
if (vm->word_count_at_check > 0)
|
||
{
|
||
/* Calculate trend in stale words: absolute count delta indicates accumulation/clearing */
|
||
int64_t stale_delta = (int64_t)stale_word_count - (int64_t)vm->stale_word_count_at_check;
|
||
|
||
/* INFERENCE: If stale words INCREASING, decay is too slow → increase slope */
|
||
/* If stale words DECREASING, decay is working (or too fast) → monitor */
|
||
if (stale_delta > 5) /* Threshold: 5+ additional stale words signals problem */
|
||
{
|
||
/* Stale words accumulating: decay is insufficient */
|
||
new_slope_direction = 1;
|
||
log_message(LOG_DEBUG,
|
||
"HEARTBEAT[slope]: stale_delta=%ld, decay TOO SLOW, increase slope",
|
||
(long)stale_delta);
|
||
}
|
||
else if (stale_delta < -5) /* Threshold: 5+ fewer stale words signals clearing */
|
||
{
|
||
/* Stale words clearing: decay is aggressive (potentially too fast) */
|
||
/* Only decrease slope if avg_heat is dropping below target */
|
||
if (avg_heat < 5.0)
|
||
{
|
||
new_slope_direction = -1;
|
||
log_message(LOG_DEBUG,
|
||
"HEARTBEAT[slope]: stale_delta=%ld, avg_heat=%.1f, decay TOO FAST, decrease slope",
|
||
(long)stale_delta, avg_heat);
|
||
}
|
||
else
|
||
{
|
||
log_message(LOG_DEBUG,
|
||
"HEARTBEAT[slope]: stale_delta=%ld, decay working, hold slope",
|
||
(long)stale_delta);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
log_message(LOG_DEBUG,
|
||
"HEARTBEAT[slope]: stale_delta=%ld (stable), hold slope",
|
||
(long)stale_delta);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
log_message(LOG_DEBUG,
|
||
"HEARTBEAT[slope]: baseline measurement - hot_words=%lu, stale_ratio=%.2f%%, avg_heat=%.1f",
|
||
hot_word_count,
|
||
stale_ratio * 100.0,
|
||
avg_heat);
|
||
}
|
||
|
||
/* === APPLY SLOPE ADJUSTMENT ===
|
||
* Only adjust if direction changed (hysteresis to prevent oscillation)
|
||
*/
|
||
if (new_slope_direction != vm->decay_slope_direction && new_slope_direction != 0)
|
||
{
|
||
vm->decay_slope_direction = new_slope_direction;
|
||
|
||
/* Calculate adjustment in Q48.16: 5% change per cycle */
|
||
uint64_t adjustment = (vm->decay_slope_q48 * 5) / 100;
|
||
if (adjustment < 1) adjustment = 1; /* Minimum increment */
|
||
|
||
uint64_t old_slope = vm->decay_slope_q48;
|
||
if (new_slope_direction > 0)
|
||
{
|
||
vm->decay_slope_q48 += adjustment;
|
||
}
|
||
else if (new_slope_direction < 0)
|
||
{
|
||
vm->decay_slope_q48 = (vm->decay_slope_q48 > adjustment)
|
||
? (vm->decay_slope_q48 - adjustment)
|
||
: 1; /* Floor at 1 */
|
||
}
|
||
|
||
/* Log the adjustment as human-readable double */
|
||
double old_slope_dbl = (double)old_slope / 65536.0;
|
||
double new_slope_dbl = (double)vm->decay_slope_q48 / 65536.0;
|
||
log_message(LOG_DEBUG,
|
||
"HEARTBEAT[slope]: ADJUSTED slope from %.3f to %.3f (direction=%d)",
|
||
old_slope_dbl,
|
||
new_slope_dbl,
|
||
new_slope_direction);
|
||
}
|
||
|
||
/* Store baseline for next comparison */
|
||
vm->hot_word_count_at_check = hot_word_count;
|
||
vm->total_heat_at_last_check = total_heat;
|
||
vm->stale_word_count_at_check = stale_word_count;
|
||
vm->word_count_at_check = word_count;
|
||
|
||
sf_mutex_unlock(&vm->tuning_lock);
|
||
|
||
(void)stale_ratio; /* Suppress unused warning */
|
||
}
|
||
|
||
static void vm_tick_apply_background_decay(VM *vm, uint64_t now_ns)
|
||
{
|
||
if (!vm)
|
||
return;
|
||
|
||
sf_mutex_lock(&vm->dict_lock);
|
||
|
||
DictEntry *cursor = NULL;
|
||
if (vm->heartbeat_decay_cursor_id != WORD_ID_INVALID)
|
||
cursor = vm_dictionary_lookup_by_word_id(vm, vm->heartbeat_decay_cursor_id);
|
||
if (!cursor)
|
||
cursor = vm->latest;
|
||
|
||
size_t processed = 0;
|
||
while (cursor && processed < HEARTBEAT_DECAY_BATCH)
|
||
{
|
||
/* Tick-based, not wall-clock -- see physics_metadata_apply_linear_decay().
|
||
* now_ns is kept only to refresh last_decay_ns for diagnostics. */
|
||
uint64_t elapsed_ticks = vm->heartbeat.tick_count - cursor->physics.last_decay_tick;
|
||
physics_metadata_apply_linear_decay(cursor, elapsed_ticks, vm);
|
||
cursor->physics.last_decay_tick = vm->heartbeat.tick_count;
|
||
cursor->physics.last_decay_ns = now_ns;
|
||
|
||
cursor = cursor->link;
|
||
processed++;
|
||
}
|
||
|
||
vm->heartbeat_decay_cursor_id = (cursor && cursor->word_id != WORD_ID_INVALID)
|
||
? cursor->word_id
|
||
: WORD_ID_INVALID;
|
||
|
||
sf_mutex_unlock(&vm->dict_lock);
|
||
}
|
||
|
||
/**
|
||
* @brief L8 FINAL INTEGRATION: Jacquard mode selector heartbeat update
|
||
*
|
||
* Collects metrics from L1-L7 physics layers and feeds them to the L8 Jacquard
|
||
* mode selector. L8 then chooses the optimal configuration mode based on workload
|
||
* characteristics and applies it to the runtime.
|
||
*
|
||
* This is the SOLE policy engine - all adaptive decisions flow through L8.
|
||
*
|
||
* @param vm Pointer to VM instance
|
||
*/
|
||
static void vm_heartbeat_update_l8(VM *vm)
|
||
{
|
||
if (!vm || !vm->ssm_l8_state)
|
||
return;
|
||
|
||
ssm_l8_state_t *l8 = (ssm_l8_state_t*)vm->ssm_l8_state;
|
||
|
||
/* === Collect L1-L7 Metrics and Convert to L8 Format === */
|
||
ssm_l8_metrics_t metrics = {0};
|
||
|
||
/* L2: Rolling window entropy (normalized diversity) */
|
||
/* Use unique word count / total executions as entropy proxy */
|
||
uint32_t unique_words = (vm->rolling_window.total_executions > 0) ?
|
||
(uint32_t)(vm->rolling_window.effective_window_size) : 0;
|
||
metrics.entropy = (double)unique_words / (double)(ROLLING_WINDOW_SIZE > 0 ? ROLLING_WINDOW_SIZE : 1);
|
||
|
||
/* L4: Pipelining metrics → CV (coefficient of variation) */
|
||
if (vm->pipeline_metrics.prefetch_attempts > 0) {
|
||
double accuracy = (double)vm->pipeline_metrics.prefetch_hits / (double)vm->pipeline_metrics.prefetch_attempts;
|
||
metrics.cv = 1.0 - accuracy; /* Invert: high accuracy = low CV (stability) */
|
||
} else {
|
||
metrics.cv = 0.5; /* Default moderate CV if no data */
|
||
}
|
||
|
||
/* L3: Decay slope → temporal locality signal (Q48.16 → double) */
|
||
double decay_slope = (double)vm->decay_slope_q48 / 65536.0;
|
||
metrics.temporal_decay = (decay_slope > 0.0) ? (1.0 / decay_slope) : 0.0; /* Higher slope = stronger temporal locality */
|
||
if (metrics.temporal_decay > 1.0) metrics.temporal_decay = 1.0; /* Clamp to [0,1] */
|
||
|
||
/* L5/L6: Inference stability for hysteresis + table ANOVA tracking */
|
||
{
|
||
int early_exited;
|
||
sf_mutex_lock(&vm->tuning_lock);
|
||
early_exited = (vm->last_inference_outputs && vm->last_inference_outputs->early_exited);
|
||
sf_mutex_unlock(&vm->tuning_lock);
|
||
metrics.stability_score = early_exited ? 0.9 : 0.1;
|
||
metrics.inference_early_exited = early_exited;
|
||
/* Inference ran this tick if last_inference_tick == tick_count */
|
||
metrics.inference_ran_this_tick =
|
||
(vm->heartbeat.tick_count == vm->heartbeat.last_inference_tick) ? 1 : 0;
|
||
}
|
||
|
||
if (l8->table) {
|
||
/* === Adaptive table-based selection === */
|
||
uint8_t old_config = l8->table->current_config;
|
||
CDTuning cd_tuning = cd_tuning_word();
|
||
uint32_t recent_ids[CD_MAX_CLASSIFY_DEPTH];
|
||
uint32_t recent_count = rolling_window_get_recent_sequence(
|
||
&vm->rolling_window, cd_tuning.id_window_depth, recent_ids);
|
||
ssm_l8_update_table(l8, &metrics,
|
||
(ssm_config_t*)vm->ssm_config,
|
||
vm->rolling_window.effective_window_size,
|
||
recent_ids, recent_count);
|
||
if (l8->table->current_config != old_config) {
|
||
uint8_t bits = l8->table->entries[l8->table->current_config].config_bits;
|
||
ssm_config_t *cfg = (ssm_config_t*)vm->ssm_config;
|
||
log_message(LOG_DEBUG,
|
||
"L8[TABLE]: Config %u → %u regime=%u score=%u "
|
||
"L1=%d L2=%d L3=%d L4=%d L5=%d L6=%d L7=%d",
|
||
(unsigned)old_config,
|
||
(unsigned)l8->table->current_config,
|
||
(unsigned)l8->table->current_regime,
|
||
(unsigned)l8->table->regime_scores
|
||
[l8->table->current_regime * l8->table->num_configs
|
||
+ l8->table->current_config],
|
||
cfg->L1_heat_tracking, cfg->L2_rolling_window,
|
||
cfg->L3_linear_decay, cfg->L4_pipelining,
|
||
cfg->L5_window_inference, cfg->L6_decay_inference,
|
||
cfg->L7_adaptive_heartrate);
|
||
(void)bits;
|
||
}
|
||
} else {
|
||
/* === Legacy threshold classifier === */
|
||
ssm_l8_mode_t old_mode = l8->current_mode;
|
||
ssm_l8_update(&metrics, l8);
|
||
if (l8->current_mode != old_mode) {
|
||
ssm_config_t *config = (ssm_config_t*)vm->ssm_config;
|
||
ssm_apply_mode(l8, config);
|
||
log_message(LOG_DEBUG,
|
||
"L8[JACQUARD]: Mode %s → %s (entropy=%.2f, cv=%.2f, temporal=%.2f)",
|
||
ssm_l8_mode_name(old_mode),
|
||
ssm_l8_mode_name(l8->current_mode),
|
||
metrics.entropy,
|
||
metrics.cv,
|
||
metrics.temporal_decay);
|
||
}
|
||
}
|
||
|
||
/* L1's cache->enabled is the runtime toggle physics_pre_execute's
|
||
* hotwords-cache checks actually read (see physics_execution_hooks.c);
|
||
* sync it here rather than re-deriving ssm_config on the hot dispatch
|
||
* path every word execution. Idempotent -- cheap even when unchanged. */
|
||
if (vm->hotwords_cache && vm->ssm_config) {
|
||
hotwords_cache_set_enabled(vm->hotwords_cache,
|
||
((ssm_config_t*)vm->ssm_config)->L1_heat_tracking);
|
||
}
|
||
}
|
||
|
||
void vm_heartbeat_run_cycle(VM *vm)
|
||
{
|
||
if (!vm || !vm->heartbeat.heartbeat_enabled)
|
||
return;
|
||
|
||
/* L8 FINAL INTEGRATION: Core heartbeat operations */
|
||
vm_tick(vm);
|
||
vm_tick_apply_background_decay(vm, sf_monotonic_ns());
|
||
rolling_window_service(&vm->rolling_window);
|
||
dict_adaptive_optimization_pass(vm); /* Adaptive dictionary optimization */
|
||
|
||
/* L8 FINAL INTEGRATION: Jacquard mode selector - the sole policy engine */
|
||
vm_heartbeat_update_l8(vm);
|
||
|
||
vm_heartbeat_publish_snapshot(vm);
|
||
|
||
/* Phase 2: Real-time heartbeat metrics emission (DISABLED)
|
||
* Re-enable with --heartbeat-log=full or add a runtime flag.
|
||
*/
|
||
#if 0
|
||
HeartbeatTickSnapshot tick_snapshot;
|
||
heartbeat_capture_tick_snapshot(vm, &tick_snapshot);
|
||
heartbeat_emit_tick_row(vm, &tick_snapshot);
|
||
#endif
|
||
}
|
||
|
||
#if HEARTBEAT_THREAD_ENABLED
|
||
static void* heartbeat_thread_main(void *arg)
|
||
{
|
||
VM *vm = (VM*)arg;
|
||
if (!vm || !vm->heartbeat.worker)
|
||
return NULL;
|
||
|
||
HeartbeatWorker *worker = vm->heartbeat.worker;
|
||
worker->running = 1;
|
||
|
||
/* IMPORTANT: No startup delay to allow heartbeat to emit during short DoE runs.
|
||
* Original 50ms delay avoided race conditions during word registration, but prevented
|
||
* real-time metrics emission in fast-completing tests. */
|
||
|
||
while (!worker->stop_requested)
|
||
{
|
||
vm_heartbeat_run_cycle(vm);
|
||
|
||
uint64_t tick_ns = worker->tick_ns ? worker->tick_ns : HEARTBEAT_TICK_NS;
|
||
struct timespec req;
|
||
req.tv_sec = (time_t)(tick_ns / 1000000000ULL);
|
||
req.tv_nsec = (long)(tick_ns % 1000000000ULL);
|
||
|
||
while (!worker->stop_requested && nanosleep(&req, &req) == -1 && errno == EINTR)
|
||
{
|
||
/* Retry with remaining time */
|
||
}
|
||
}
|
||
|
||
worker->running = 0;
|
||
return NULL;
|
||
}
|
||
#endif
|
||
|
||
void vm_snapshot_read(const VM* vm, HeartbeatSnapshot* out_snapshot)
|
||
{
|
||
if (!vm || !out_snapshot)
|
||
return;
|
||
|
||
uint32_t index = heartbeat_snapshot_index_load(&vm->heartbeat.snapshot_index) & 1u;
|
||
*out_snapshot = vm->heartbeat.snapshots[index];
|
||
}
|
||
|
||
/**
|
||
* @brief Phase 2: Unified Inference Engine - Adaptive Window & Decay Slope Tuning
|
||
*
|
||
* Coordinates inference on rolling window of truth to determine:
|
||
* - Optimal adaptive window width (via variance inflection detection)
|
||
* - Optimal decay slope (via exponential regression on heat trajectory)
|
||
*
|
||
* Uses ANOVA early-exit to skip full inference when variance is stable (<5% change).
|
||
* All math uses Q48.16 fixed-point (integer-only, no floating-point).
|
||
*
|
||
* Replaces legacy vm_tick_window_tuner() and vm_tick_slope_validator().
|
||
*
|
||
* @param vm Pointer to VM instance
|
||
*/
|
||
void vm_tick_inference_engine(VM* vm)
|
||
{
|
||
if (!vm || !vm->heartbeat.heartbeat_enabled || !vm->rolling_window.is_warm)
|
||
return;
|
||
|
||
/* DoE counter: inference engine invocations */
|
||
vm->heartbeat.inference_run_count++;
|
||
|
||
rolling_window_service(&vm->rolling_window);
|
||
|
||
/* Allocate InferenceOutputs if needed - protect with tuning_lock against race with doe_metrics */
|
||
sf_mutex_lock(&vm->tuning_lock);
|
||
if (!vm->last_inference_outputs)
|
||
{
|
||
vm->last_inference_outputs = sf_malloc(sizeof(InferenceOutputs));
|
||
if (!vm->last_inference_outputs)
|
||
{
|
||
sf_mutex_unlock(&vm->tuning_lock);
|
||
log_message(LOG_ERROR, "INFERENCE: Failed to allocate InferenceOutputs");
|
||
return;
|
||
}
|
||
memset(vm->last_inference_outputs, 0, sizeof(InferenceOutputs));
|
||
}
|
||
sf_mutex_unlock(&vm->tuning_lock);
|
||
|
||
/* === Collect Current Dictionary Metrics === */
|
||
uint64_t hot_word_count = 0;
|
||
uint64_t stale_word_count = 0;
|
||
uint64_t total_heat = 0;
|
||
uint32_t word_count = 0;
|
||
|
||
sf_mutex_lock(&vm->dict_lock);
|
||
for (DictEntry *e = vm->latest; e != NULL; e = e->link)
|
||
{
|
||
if (e->execution_heat > HOTWORDS_EXECUTION_HEAT_THRESHOLD)
|
||
hot_word_count++;
|
||
else if (e->execution_heat > 0 && e->execution_heat < 10)
|
||
stale_word_count++;
|
||
|
||
total_heat += e->execution_heat;
|
||
word_count++;
|
||
}
|
||
sf_mutex_unlock(&vm->dict_lock);
|
||
|
||
/* === Populate InferenceInputs === */
|
||
InferenceInputs inference_inputs = {
|
||
.window = &vm->rolling_window,
|
||
.vm = vm, /* Required for dictionary lookups in extract_heat_trajectory */
|
||
.trajectory_length = (vm->rolling_window.window_pos > 0)
|
||
? vm->rolling_window.window_pos
|
||
: vm->rolling_window.total_executions,
|
||
.prefetch_hits = vm->pipeline_metrics.prefetch_hits,
|
||
.prefetch_attempts = vm->pipeline_metrics.prefetch_attempts,
|
||
.hot_word_count = hot_word_count,
|
||
.stale_word_count = stale_word_count,
|
||
.total_heat = total_heat,
|
||
.word_count = word_count,
|
||
.last_total_heat = vm->total_heat_at_last_check,
|
||
.last_stale_count = vm->stale_word_count_at_check
|
||
};
|
||
|
||
/* === Run Unified Inference Engine === */
|
||
inference_engine_run(&inference_inputs, vm->last_inference_outputs);
|
||
|
||
/* === Apply Inferred Tuning Parameters === */
|
||
if (!vm->last_inference_outputs->early_exited)
|
||
{
|
||
/* Full inference was executed (not cached by ANOVA early-exit) */
|
||
|
||
/* 1. Apply adaptive window width */
|
||
if (vm->last_inference_outputs->adaptive_window_width > 0 &&
|
||
vm->last_inference_outputs->adaptive_window_width != vm->rolling_window.effective_window_size)
|
||
{
|
||
log_message(LOG_DEBUG,
|
||
"INFERENCE[window]: %u → %u (variance=%.6f Q48.16)",
|
||
vm->rolling_window.effective_window_size,
|
||
vm->last_inference_outputs->adaptive_window_width,
|
||
(double)vm->last_inference_outputs->window_variance_q48 / 65536.0);
|
||
vm->rolling_window.effective_window_size = vm->last_inference_outputs->adaptive_window_width;
|
||
}
|
||
|
||
/* 2. Apply adaptive decay slope */
|
||
sf_mutex_lock(&vm->tuning_lock);
|
||
if (vm->last_inference_outputs->adaptive_decay_slope > 0 &&
|
||
vm->last_inference_outputs->adaptive_decay_slope != vm->decay_slope_q48)
|
||
{
|
||
double old_slope_dbl = (double)vm->decay_slope_q48 / 65536.0;
|
||
double new_slope_dbl = (double)vm->last_inference_outputs->adaptive_decay_slope / 65536.0;
|
||
|
||
log_message(LOG_DEBUG,
|
||
"INFERENCE[slope]: %.3f → %.3f (fit_quality=%.6f Q48.16)",
|
||
old_slope_dbl,
|
||
new_slope_dbl,
|
||
(double)vm->last_inference_outputs->slope_fit_quality_q48 / 65536.0);
|
||
vm->decay_slope_q48 = vm->last_inference_outputs->adaptive_decay_slope;
|
||
}
|
||
sf_mutex_unlock(&vm->tuning_lock);
|
||
|
||
/* 3. Validate outputs */
|
||
if (!inference_outputs_validate(vm->last_inference_outputs))
|
||
{
|
||
log_message(LOG_WARN,
|
||
"INFERENCE: Output validation failed, ignoring results");
|
||
}
|
||
|
||
vm->heartbeat.last_inference_tick = vm->heartbeat.tick_count;
|
||
}
|
||
else
|
||
{
|
||
/* ANOVA early-exit: variance stable, using cached outputs */
|
||
vm->heartbeat.early_exit_count++;
|
||
log_message(LOG_DEBUG,
|
||
"INFERENCE: Early-exit (variance stable <5%%), using cached outputs");
|
||
}
|
||
|
||
/* Store baseline for next inference comparison */
|
||
sf_mutex_lock(&vm->tuning_lock);
|
||
vm->total_heat_at_last_check = total_heat;
|
||
vm->stale_word_count_at_check = stale_word_count;
|
||
vm->word_count_at_check = word_count;
|
||
sf_mutex_unlock(&vm->tuning_lock);
|
||
|
||
/* L8 FINAL INTEGRATION: Loop always-on */ /* === Loop #7: Adaptive Heartrate ===
|
||
* Adjust tick frequency based on system stability:
|
||
* - Variance stable (early_exited) → increase tick interval (less frequent)
|
||
* - Variance volatile (full inference) → decrease tick interval (more frequent)
|
||
*
|
||
* Bounds: [HEARTBEAT_TICK_NS / 4, HEARTBEAT_TICK_NS * 4]
|
||
*/
|
||
{
|
||
uint64_t current_tick_ns = vm->heartbeat.tick_target_ns;
|
||
uint64_t min_tick_ns = HEARTBEAT_TICK_NS / 4; /* 4x faster minimum */
|
||
uint64_t max_tick_ns = HEARTBEAT_TICK_NS * 4; /* 4x slower maximum */
|
||
|
||
if (vm->last_inference_outputs && vm->last_inference_outputs->early_exited)
|
||
{
|
||
/* System stable: slow down heartbeat by 25% */
|
||
uint64_t new_tick_ns = (current_tick_ns * 125) / 100;
|
||
if (new_tick_ns > max_tick_ns) new_tick_ns = max_tick_ns;
|
||
|
||
if (new_tick_ns != current_tick_ns)
|
||
{
|
||
vm->heartbeat.tick_target_ns = new_tick_ns;
|
||
if (vm->heartbeat.worker)
|
||
vm->heartbeat.worker->tick_ns = new_tick_ns;
|
||
|
||
log_message(LOG_DEBUG,
|
||
"HEARTBEAT[rate]: stable → slower tick %lu → %lu ns",
|
||
(unsigned long)current_tick_ns,
|
||
(unsigned long)new_tick_ns);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
/* System volatile: speed up heartbeat by 25% */
|
||
uint64_t new_tick_ns = (current_tick_ns * 80) / 100;
|
||
if (new_tick_ns < min_tick_ns) new_tick_ns = min_tick_ns;
|
||
|
||
if (new_tick_ns != current_tick_ns)
|
||
{
|
||
vm->heartbeat.tick_target_ns = new_tick_ns;
|
||
if (vm->heartbeat.worker)
|
||
vm->heartbeat.worker->tick_ns = new_tick_ns;
|
||
|
||
log_message(LOG_DEBUG,
|
||
"HEARTBEAT[rate]: volatile → faster tick %lu → %lu ns",
|
||
(unsigned long)current_tick_ns,
|
||
(unsigned long)new_tick_ns);
|
||
}
|
||
}
|
||
}
|
||
/* End always-on loop */}
|
||
|
||
/* ====================== Thread Start/Stop Wrappers ======================= */
|
||
|
||
void vm_heartbeat_start_thread(VM *vm)
|
||
{
|
||
#if HEARTBEAT_HAS_THREADS
|
||
if (!vm)
|
||
return;
|
||
|
||
vm->heartbeat.worker = sf_calloc(1, sizeof(HeartbeatWorker));
|
||
if (vm->heartbeat.worker)
|
||
{
|
||
vm->heartbeat.worker->tick_ns = HEARTBEAT_TICK_NS;
|
||
if (pthread_create(&vm->heartbeat.worker->thread, NULL, heartbeat_thread_main, vm) != 0)
|
||
{
|
||
log_message(LOG_WARN, "heartbeat: pthread_create failed (%d), falling back to inline mode", errno);
|
||
sf_free(vm->heartbeat.worker);
|
||
vm->heartbeat.worker = NULL;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
log_message(LOG_WARN, "heartbeat: worker allocation failed, using inline heartbeat");
|
||
}
|
||
#else
|
||
(void)vm;
|
||
#endif
|
||
}
|
||
|
||
void vm_heartbeat_stop_thread(VM *vm)
|
||
{
|
||
#if HEARTBEAT_HAS_THREADS
|
||
if (!vm || !vm->heartbeat.worker)
|
||
return;
|
||
|
||
vm->heartbeat.worker->stop_requested = 1;
|
||
pthread_join(vm->heartbeat.worker->thread, NULL);
|
||
sf_free(vm->heartbeat.worker);
|
||
vm->heartbeat.worker = NULL;
|
||
#else
|
||
(void)vm;
|
||
#endif
|
||
} |