534 lines
21 KiB
C
534 lines
21 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.
|
||
*/
|
||
|
||
/**
|
||
* capsule_vm_physics.c - Dynamic VM Fleet Physics Implementation
|
||
*
|
||
* See capsule_vm_physics.h and
|
||
* docs/working/architecture/VM-PHYSICS-DYNAMIC-FLEET-DESIGN-20260705.md
|
||
* for the full design rationale.
|
||
*
|
||
* Concurrency: no locking. The only interrupt-driven kernel path
|
||
* (heartbeat_tick, from the timer ISR) touches only its own isolated
|
||
* TimeTrustState and never calls into mama_word_*, vm_interpret, or any
|
||
* capsule/dictionary state. HEARTBEAT_THREAD_ENABLED=0 means no separate
|
||
* OS thread runs physics logic either. BIRTH/KILL/VM-EXEC/VM-CALL/
|
||
* VM-STEP all execute synchronously in Hera's single interpreter
|
||
* context with nothing able to preempt them mid-transfer.
|
||
*
|
||
* L8 coupling (rev q): the only place this file reaches outside its own
|
||
* VMPhysics/VMFleetWindow state is l8_regime_modulation_q16(), a
|
||
* read-only look at Hera's own ssm_l8_state via sk_get_mama_vm() --
|
||
* modulating the fleet's empirically-recovered transfer rate uniformly,
|
||
* never deciding which VM receives heat. Still a passive observer: this
|
||
* changes how fast the fleet's own rate estimate responds, not who runs
|
||
* or who gets touched.
|
||
*/
|
||
|
||
#include "starkernel/capsule_vm_physics.h"
|
||
#include "starkernel/capsule_birth.h"
|
||
#include "starkernel/kmalloc.h"
|
||
#include "starkernel/q48_16.h"
|
||
#include "starkernel/console.h"
|
||
#include "starforth_config.h" /* HEARTBEAT_INFERENCE_FREQUENCY */
|
||
#include "starkernel/vm/bootstrap/sk_vm_bootstrap.h" /* sk_get_mama_vm */
|
||
#include "vm.h" /* VM, vm->ssm_l8_state */
|
||
#include "ssm_jacquard.h" /* ssm_l8_state_t, CDConfigTable */
|
||
|
||
/* Word-level ROLLING_WINDOW_SIZE defaults to 4096 because word executions
|
||
* number in the millions. Fleet touches are real VM-EXEC/VM-CALL/VM-STEP
|
||
* dispatches -- dozens to low hundreds over a full boot + acceptance run.
|
||
* 64 gives the regression enough samples to stabilize without being
|
||
* wastefully oversized for a 3-8 VM fleet. Compile-time constant, same
|
||
* tuning-knob convention as ROLLING_WINDOW_SIZE. */
|
||
#define VM_FLEET_WINDOW_DEPTH 64
|
||
|
||
/* |fleet_heat_sum - Q48_ONE| < 5% of Q48_ONE, mirrors fleet-k.4th's
|
||
* K-EPSILON (3277). */
|
||
#define VM_PHYSICS_EPSILON_Q48 3277ULL
|
||
|
||
typedef struct {
|
||
uint64_t execution_heat_q48;
|
||
uint64_t last_active_ns;
|
||
int is_live;
|
||
} VMPhysics;
|
||
|
||
typedef struct vm_physics_node {
|
||
uint32_t vm_id;
|
||
VMPhysics physics;
|
||
struct vm_physics_node *next;
|
||
} vm_physics_node_t;
|
||
|
||
/* kmalloc-backed linked list, same pattern as capsule_birth.c's
|
||
* vm_registry_head/vm_registry_count -- unbounded, not a fixed array. */
|
||
static vm_physics_node_t *vm_physics_head = (void *)0;
|
||
|
||
/* One recorded touch's inputs to the transfer law (amount = elapsed_us *
|
||
* slope >> 16, clamped to available heat) -- not a derived heat value.
|
||
* Rev o (VM-FLEET-ATTRACTOR-DESIGN-20260705.md) replaced the earlier
|
||
* heat-snapshot design (rev m) with this: since the transfer law is known
|
||
* exactly, the true rate can be recovered directly from any touch that
|
||
* wasn't clamped (rate = amount / elapsed_us), rather than curve-fitting
|
||
* a reconstructed trajectory against a synthetic time axis. */
|
||
typedef struct {
|
||
uint64_t elapsed_us; /* since this vm_id's previous touch; 0 if there
|
||
* was no previous touch to compare against */
|
||
uint64_t amount; /* elapsed_us * slope >> 16 at touch time, i.e.
|
||
* the rate-implied transfer before any clamp;
|
||
* 0 if not computed (first touch, zero slope,
|
||
* or non-monotonic clock) */
|
||
int clamped; /* 1 if the heat actually moved was capped below
|
||
* `amount` by what the rest of the fleet held --
|
||
* carries no rate information, excluded from
|
||
* the estimator rather than treated as a zero */
|
||
} VMFleetTouchSample;
|
||
|
||
typedef struct {
|
||
VMFleetTouchSample touch_samples[VM_FLEET_WINDOW_DEPTH];
|
||
uint32_t head;
|
||
uint32_t count; /* saturates at VM_FLEET_WINDOW_DEPTH */
|
||
int is_warm; /* count >= VM_FLEET_WINDOW_DEPTH */
|
||
} VMFleetWindow;
|
||
|
||
/* One instance, fleet-wide -- not per-VM state. */
|
||
static VMFleetWindow fleet_window = { { { 0, 0, 0 } }, 0, 0, 0 };
|
||
|
||
/* Below this many informative (unclamped) samples in a full window, skip
|
||
* the fit and keep the current slope rather than trust a handful of
|
||
* points -- same "skip, don't substitute a degenerate value" philosophy
|
||
* as the is_warm gate. Arbitrary but conservative relative to the
|
||
* 64-deep window: needs an eighth of the window to be informative. */
|
||
#define VM_PHYSICS_MIN_INFORMATIVE_SAMPLES 8
|
||
|
||
/* Shared adaptive slope, fit from aggregate fleet statistics (one slope
|
||
* for the whole fleet), exactly mirroring how the word engine infers one
|
||
* decay_slope_q48 per VM instance rather than one per word.
|
||
*
|
||
* Seeded at 65536/3 (Q48.16 for 1/3), matching the word engine's actual
|
||
* bootstrap value (src/vm_bootstrap.c / src/starkernel/vm/vm_bootstrap.c:
|
||
* "vm->decay_slope_q48 = (1ULL << 16) / 3"). The old comment on that field
|
||
* in include/vm.h ("starts at 2:1 = 131072") is stale relative to the real
|
||
* code -- 1/3 is what's actually seeded there, so 1/3 is what's mirrored
|
||
* here. This was previously seeded at literal 0, which cannot bootstrap:
|
||
* vm_physics_touch's transfer amount is elapsed_ns * slope >> 16, so a
|
||
* zero slope moves zero heat on every touch forever, which starves
|
||
* vm_physics_tick's regression of the very heat-trajectory signal it
|
||
* needs to infer a better slope -- a closed loop with no way out. Found
|
||
* via doe_log.c's per-VM heat CSV columns showing a dead-flat trajectory
|
||
* across a full boot (VM-FLEET-ATTRACTOR-DESIGN-20260705.md rev f). */
|
||
static uint64_t fleet_transfer_slope_q48 = 65536ULL / 3;
|
||
static uint64_t slope_fit_quality_q48 = 0;
|
||
|
||
static vm_physics_node_t *vm_physics_find(uint32_t vm_id)
|
||
{
|
||
vm_physics_node_t *n = vm_physics_head;
|
||
while (n) {
|
||
if (n->vm_id == vm_id) return n;
|
||
n = n->next;
|
||
}
|
||
return (void *)0;
|
||
}
|
||
|
||
/**
|
||
* vm_physics_transfer - The one conservative primitive everything else
|
||
* is a special case of. Subtracts amount from from (clamped at 0) and
|
||
* adds the same amount to to. Nothing is created or destroyed:
|
||
* sum(execution_heat for all LIVE VMs) is invariant across any call.
|
||
*/
|
||
static void vm_physics_transfer(VMPhysics *from, VMPhysics *to, uint64_t amount_q48)
|
||
{
|
||
uint64_t moved = (amount_q48 > from->execution_heat_q48)
|
||
? from->execution_heat_q48
|
||
: amount_q48;
|
||
from->execution_heat_q48 -= moved;
|
||
to->execution_heat_q48 += moved;
|
||
}
|
||
|
||
void vm_physics_init(uint32_t vm_id)
|
||
{
|
||
vm_physics_node_t *node = vm_physics_find(vm_id);
|
||
/* Hera (vm_id 0) is the fleet's single structural root -- the one
|
||
* entry capsule_vm_kill refuses to ever kill, and the same fixed
|
||
* point vm_physics_find_root_id below walks every parent chain up
|
||
* to. She is where the fleet's whole Q48_ONE initially resides;
|
||
* every other VM joins an already-nonzero fleet and starts at 0,
|
||
* per the "cold mass added to a closed system" reasoning -- which
|
||
* requires a nonzero sum to already exist, so Hera's own
|
||
* registration is where that sum is seeded. */
|
||
int is_root = (vm_id == 0);
|
||
|
||
if (node) {
|
||
node->physics.execution_heat_q48 = is_root ? Q48_ONE : 0;
|
||
node->physics.last_active_ns = 0;
|
||
node->physics.is_live = 1;
|
||
return;
|
||
}
|
||
|
||
node = (vm_physics_node_t *)kmalloc(sizeof(vm_physics_node_t));
|
||
if (!node) return;
|
||
|
||
node->vm_id = vm_id;
|
||
node->physics.execution_heat_q48 = is_root ? Q48_ONE : 0;
|
||
node->physics.last_active_ns = 0;
|
||
node->physics.is_live = 1;
|
||
node->next = vm_physics_head;
|
||
vm_physics_head = node;
|
||
}
|
||
|
||
/**
|
||
* vm_physics_find_root_id - Walk parent_vm_id edges up to Hera
|
||
*
|
||
* Every VM's parent_vm_id (VMRegistryEntry, capsule_run.h) is set once
|
||
* at its own birth and never rewritten -- including a dead VM's, so the
|
||
* walk continues correctly through an already-killed intermediate
|
||
* parent without any special dead-parent handling. Hera's own entry is
|
||
* self-referential (parent_vm_id == vm_id == 0), which is what stops
|
||
* the walk: she is the only node with no outbound edge.
|
||
*
|
||
* The 64-hop guard is defensive only -- a well-formed chain terminates
|
||
* in O(fleet depth) hops, nowhere near 64. It exists so a corrupted
|
||
* chain (which should never happen) returns the starting vm_id instead
|
||
* of spinning.
|
||
*/
|
||
static uint32_t vm_physics_find_root_id(uint32_t vm_id)
|
||
{
|
||
uint32_t cursor = vm_id;
|
||
uint32_t guard;
|
||
|
||
for (guard = 0; guard < 64; guard++) {
|
||
VMRegistryEntry entry;
|
||
|
||
if (capsule_vm_registry_get(cursor, &entry) != 0) break;
|
||
if (entry.parent_vm_id == cursor) break; /* self-referential: root */
|
||
cursor = entry.parent_vm_id;
|
||
}
|
||
return cursor;
|
||
}
|
||
|
||
void vm_physics_retire(uint32_t vm_id)
|
||
{
|
||
vm_physics_node_t *dying = vm_physics_find(vm_id);
|
||
vm_physics_node_t *root;
|
||
uint32_t root_id;
|
||
|
||
if (!dying || !dying->physics.is_live) return;
|
||
|
||
root_id = vm_physics_find_root_id(vm_id);
|
||
root = (root_id != vm_id) ? vm_physics_find(root_id) : (void *)0;
|
||
|
||
if (root) {
|
||
/* The dying VM's entire remaining heat flows to the root it
|
||
* chains up to (in practice always Hera today) -- no division,
|
||
* no weighting policy, no "no survivors" case, since a root is
|
||
* always there by construction. This is also what makes
|
||
* TRIPOD-TEST's kill-then-rebirth "K soak" check meaningful: a
|
||
* root VM (e.g. Hermes) being killed and later respawned by
|
||
* Hera doesn't lose heat across the gap, it just parks at Hera
|
||
* in the meantime. */
|
||
vm_physics_transfer(&dying->physics, &root->physics,
|
||
dying->physics.execution_heat_q48);
|
||
}
|
||
/* root_id == vm_id means the dying VM IS the root itself (Hera --
|
||
* shouldn't happen, she can't be killed) or the chain was broken
|
||
* (shouldn't happen, see vm_physics_find_root_id). Either way
|
||
* there's nowhere conservation-preserving to send the remainder;
|
||
* it is dropped along with the dying entry. */
|
||
|
||
dying->physics.execution_heat_q48 = 0;
|
||
dying->physics.is_live = 0;
|
||
}
|
||
|
||
void vm_physics_touch(uint32_t vm_id, uint64_t now_ns)
|
||
{
|
||
vm_physics_node_t *target = vm_physics_find(vm_id);
|
||
VMFleetTouchSample sample = { 0, 0, 0 };
|
||
|
||
if (!target || !target->physics.is_live) return;
|
||
|
||
if (fleet_transfer_slope_q48 > 0 &&
|
||
target->physics.last_active_ns > 0 &&
|
||
now_ns > target->physics.last_active_ns) {
|
||
|
||
/* Microseconds, not nanoseconds, matching the word engine's own
|
||
* convention (physics_metadata_apply_linear_decay,
|
||
* physics_metadata.c) that fleet_transfer_slope_q48's seed value
|
||
* was explicitly calibrated to mirror (see the comment on the
|
||
* static above). Using raw elapsed_ns here was an outright unit
|
||
* bug, not a design choice: real touch-to-touch gaps run to
|
||
* milliseconds, so (elapsed_ns * slope_q48) >> 16 overshot the
|
||
* fleet's entire conserved heat (Q48_ONE) by more than 10x on the
|
||
* very first post-seed touch, clamping to a full winner-take-all
|
||
* transfer in a single step instead of the gradual pull this was
|
||
* meant to model (VM-FLEET-ATTRACTOR-DESIGN-20260705.md rev n). */
|
||
uint64_t elapsed_us = (now_ns - target->physics.last_active_ns) / 1000;
|
||
uint64_t amount = (elapsed_us * fleet_transfer_slope_q48) >> 16;
|
||
|
||
sample.elapsed_us = elapsed_us;
|
||
sample.amount = amount;
|
||
|
||
if (amount > 0) {
|
||
/* Pull toward the touched VM, proportional to each other LIVE
|
||
* VM's current heat -- same fan-out shape as retire, mirror
|
||
* direction. Clamped to what the rest of the fleet actually
|
||
* holds, so this can never manufacture heat. */
|
||
vm_physics_node_t *n;
|
||
uint64_t others_total = 0;
|
||
uint64_t moved_total;
|
||
|
||
for (n = vm_physics_head; n; n = n->next) {
|
||
if (n != target && n->physics.is_live) {
|
||
others_total += n->physics.execution_heat_q48;
|
||
}
|
||
}
|
||
|
||
/* moved_total == 0 when others_total == 0 (nothing to pull --
|
||
* the fleet is already fully concentrated elsewhere), handled
|
||
* uniformly by the same clamp as a partial cap: either way
|
||
* the actual transfer fell short of what the rate implied, so
|
||
* this touch carries no usable rate information (rev o). */
|
||
moved_total = (amount > others_total) ? others_total : amount;
|
||
sample.clamped = (moved_total != amount);
|
||
|
||
if (moved_total > 0) {
|
||
for (n = vm_physics_head; n; n = n->next) {
|
||
if (n != target && n->physics.is_live) {
|
||
uint64_t share = (moved_total * n->physics.execution_heat_q48)
|
||
/ others_total;
|
||
vm_physics_transfer(&n->physics, &target->physics, share);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
target->physics.last_active_ns = now_ns;
|
||
|
||
/* Record this touch's transfer-law inputs, not a derived heat value
|
||
* (rev o, VM-FLEET-ATTRACTOR-DESIGN-20260705.md) -- see
|
||
* VMFleetTouchSample for why. */
|
||
fleet_window.touch_samples[fleet_window.head] = sample;
|
||
fleet_window.head = (fleet_window.head + 1) % VM_FLEET_WINDOW_DEPTH;
|
||
if (fleet_window.count < VM_FLEET_WINDOW_DEPTH) {
|
||
fleet_window.count++;
|
||
}
|
||
fleet_window.is_warm = (fleet_window.count >= VM_FLEET_WINDOW_DEPTH);
|
||
}
|
||
|
||
/**
|
||
* l8_regime_modulation_q16 - Read-only Q16 multiplier from Hera's own L8
|
||
*
|
||
* Gives L8 a real, passive-observer-respecting causal channel into the
|
||
* fleet's transfer rate (rev q, VM-FLEET-ATTRACTOR-DESIGN-20260705.md):
|
||
* previously nothing L8 did could affect fleet dynamics at all (rev j's
|
||
* architectural diagnosis for its null DoE result). Reuses Hera's
|
||
* adaptive table's current_regime -- the same 3-bit entropy/cv/temporal
|
||
* classification the bandit already stratifies its own scores by,
|
||
* rather than inventing a new signal -- as a proxy for fleet-wide
|
||
* dispatch activity, since all VM-EXEC/VM-CALL flows through her.
|
||
*
|
||
* modulation = (popcount(regime) + 1) * 0.5, i.e. 0.5x-2.0x: more of the
|
||
* three high-signal bits active scales the fleet's already-recovered
|
||
* rate up (track change more aggressively), fewer scales it down (damp
|
||
* toward the raw empirical rate). No per-VM favoritism -- this scales
|
||
* whatever vm_physics_tick() already computed uniformly, it doesn't
|
||
* decide who receives heat.
|
||
*
|
||
* Returns Q16 65536 (neutral 1.0x) if Hera or her adaptive table isn't
|
||
* available, so a missing signal never blocks or corrupts the estimate
|
||
* this multiplies.
|
||
*/
|
||
static uint32_t l8_regime_modulation_q16(void)
|
||
{
|
||
VM *mama = (VM*)sk_get_mama_vm();
|
||
ssm_l8_state_t *l8;
|
||
uint32_t popcount;
|
||
|
||
if (!mama || !mama->ssm_l8_state) return 65536u;
|
||
|
||
l8 = (ssm_l8_state_t*)mama->ssm_l8_state;
|
||
if (!l8->table) return 65536u; /* legacy path tracks no regime */
|
||
|
||
popcount = (uint32_t)(((l8->table->current_regime >> 2) & 1u) +
|
||
((l8->table->current_regime >> 1) & 1u) +
|
||
(l8->table->current_regime & 1u));
|
||
return (popcount + 1u) * 32768u;
|
||
}
|
||
|
||
void vm_physics_tick(uint64_t now_ns)
|
||
{
|
||
uint64_t rates[VM_FLEET_WINDOW_DEPTH];
|
||
uint32_t rate_count = 0;
|
||
uint32_t i;
|
||
|
||
(void)now_ns; /* Unlike the log-linear regression this replaced, the
|
||
* estimator below needs no external time reference --
|
||
* each sample already carries its own elapsed_us. */
|
||
|
||
/* Mirrors vm_tick_inference_engine's is_warm gate: skip rather than
|
||
* substitute a default slope. Expected to stay unwarmed (or warm
|
||
* with a low-quality fit) at the current 3-VM Tripod scale -- there
|
||
* simply aren't enough distinct touch events yet. */
|
||
if (!fleet_window.is_warm) return;
|
||
|
||
/* Direct rate recovery, not curve-fitting (rev o,
|
||
* VM-FLEET-ATTRACTOR-DESIGN-20260705.md, superseding the log-linear
|
||
* OLS approach of rev b-n): the transfer law is known exactly
|
||
* (amount = elapsed_us * slope >> 16), so any unclamped touch can be
|
||
* inverted directly for an exact per-sample rate. Order doesn't
|
||
* matter here -- a median over the observation window, not a
|
||
* time-indexed fit -- so this walks the fixed-size array directly
|
||
* rather than replaying ring order from fleet_window.head. */
|
||
for (i = 0; i < VM_FLEET_WINDOW_DEPTH; i++) {
|
||
VMFleetTouchSample *s = &fleet_window.touch_samples[i];
|
||
|
||
if (s->clamped || s->amount == 0 || s->elapsed_us == 0) continue;
|
||
|
||
rates[rate_count++] = (s->amount << 16) / s->elapsed_us;
|
||
}
|
||
|
||
/* slope_fit_quality_q48 as the informative fraction of the window --
|
||
* a real measure now, not a fixed placeholder. Reported even when
|
||
* there's too little to fit, same as a partial-sample regression
|
||
* would report low confidence rather than nothing at all. */
|
||
slope_fit_quality_q48 = ((uint64_t)rate_count << 16) / VM_FLEET_WINDOW_DEPTH;
|
||
|
||
/* Too few informative samples to trust: keep the current slope
|
||
* rather than overwrite it with an estimate from a handful of
|
||
* points (or zero) -- the same "skip, don't substitute a degenerate
|
||
* value" philosophy as the is_warm gate above. */
|
||
if (rate_count < VM_PHYSICS_MIN_INFORMATIVE_SAMPLES) return;
|
||
|
||
/* Insertion sort for the median -- rate_count <= VM_FLEET_WINDOW_DEPTH
|
||
* (64), no library qsort in this freestanding kernel-only file. */
|
||
for (i = 1; i < rate_count; i++) {
|
||
uint64_t key = rates[i];
|
||
uint32_t j = i;
|
||
|
||
while (j > 0 && rates[j - 1] > key) {
|
||
rates[j] = rates[j - 1];
|
||
j--;
|
||
}
|
||
rates[j] = key;
|
||
}
|
||
|
||
fleet_transfer_slope_q48 = (rates[rate_count / 2] * l8_regime_modulation_q16()) >> 16;
|
||
}
|
||
|
||
/* Fleet-wide heartbeat tick, distinct from any VM's own per-VM
|
||
* HeartbeatState.tick_count -- see the rationale in the header. Advances
|
||
* from whichever VM's own heartbeat cycle happens to fire, so the
|
||
* readiness signal reflects the fleet's aggregate activity rather than
|
||
* any one VM's (in practice, Hera's) personal word-execution rate. */
|
||
static uint64_t fleet_heartbeat_tick_count = 0;
|
||
static uint64_t fleet_last_inference_tick = 0;
|
||
|
||
void vm_physics_heartbeat_tick(uint64_t now_ns)
|
||
{
|
||
fleet_heartbeat_tick_count++;
|
||
if ((fleet_heartbeat_tick_count - fleet_last_inference_tick) >= HEARTBEAT_INFERENCE_FREQUENCY) {
|
||
vm_physics_tick(now_ns);
|
||
fleet_last_inference_tick = fleet_heartbeat_tick_count;
|
||
}
|
||
}
|
||
|
||
uint64_t vm_physics_fleet_heat_sum(void)
|
||
{
|
||
uint64_t total = 0;
|
||
vm_physics_node_t *n;
|
||
|
||
for (n = vm_physics_head; n; n = n->next) {
|
||
if (n->physics.is_live) {
|
||
total += n->physics.execution_heat_q48;
|
||
}
|
||
}
|
||
return total;
|
||
}
|
||
|
||
int vm_physics_conserved(void)
|
||
{
|
||
uint64_t sum = vm_physics_fleet_heat_sum();
|
||
uint64_t diff = (sum > Q48_ONE) ? (sum - Q48_ONE) : (Q48_ONE - sum);
|
||
return diff < VM_PHYSICS_EPSILON_Q48;
|
||
}
|
||
|
||
uint64_t vm_physics_heat_of(uint32_t vm_id)
|
||
{
|
||
vm_physics_node_t *n = vm_physics_find(vm_id);
|
||
if (!n || !n->physics.is_live) return 0;
|
||
return n->physics.execution_heat_q48;
|
||
}
|
||
|
||
/* Decimal print, same shape as parity.c's parity_put_u64 -- freestanding,
|
||
* no snprintf dependency. */
|
||
static void console_put_u64(uint64_t val)
|
||
{
|
||
char buf[21];
|
||
int i = 20;
|
||
|
||
buf[i--] = '\0';
|
||
if (val == 0) {
|
||
buf[i--] = '0';
|
||
} else {
|
||
while (val > 0 && i >= 0) {
|
||
buf[i--] = (char)('0' + (val % 10));
|
||
val /= 10;
|
||
}
|
||
}
|
||
console_puts(&buf[i + 1]);
|
||
}
|
||
|
||
void vm_physics_status(void)
|
||
{
|
||
uint64_t sum = vm_physics_fleet_heat_sum();
|
||
|
||
console_puts("VM-PHYSICS: fleet_heat_sum=");
|
||
console_put_u64(sum);
|
||
console_println("");
|
||
|
||
console_puts("VM-PHYSICS: conserved=");
|
||
console_println(vm_physics_conserved() ? "CONSERVED" : "DRIFTED");
|
||
|
||
console_puts("VM-PHYSICS: fleet_transfer_slope_q48=");
|
||
console_put_u64(fleet_transfer_slope_q48);
|
||
console_println("");
|
||
|
||
console_puts("VM-PHYSICS: slope_fit_quality_q48=");
|
||
console_put_u64(slope_fit_quality_q48);
|
||
console_println("");
|
||
|
||
console_puts("VM-PHYSICS: fleet_window_warm=");
|
||
console_println(fleet_window.is_warm ? "YES" : "NO");
|
||
|
||
/* Per-VM breakdown (VM-FLEET-ATTRACTOR-DESIGN-20260705.md transparency
|
||
* requirement): the aggregate above always reads Q.1 when conserved,
|
||
* which says nothing about how heat is actually split among live VMs. */
|
||
{
|
||
vm_physics_node_t *n;
|
||
for (n = vm_physics_head; n; n = n->next) {
|
||
VMRegistryEntry entry;
|
||
if (!n->physics.is_live) continue;
|
||
|
||
console_puts("VM-PHYSICS: vm_id=");
|
||
console_put_u64(n->vm_id);
|
||
|
||
if (capsule_vm_registry_get(n->vm_id, &entry) == 0) {
|
||
console_puts(" name=");
|
||
console_puts(entry.name);
|
||
}
|
||
|
||
console_puts(" heat_q48=");
|
||
console_put_u64(n->physics.execution_heat_q48);
|
||
console_println("");
|
||
}
|
||
}
|
||
}
|