capsule_birth.c hardcoded every new VM's initial Stadium quota grant to split from Hera specifically. Since a grant always halves whatever the donor currently has, Hera's own free list converges toward empty after a bounded number of grants — independent of whether the Stadium as a whole still had spare capacity, since VMs she'd granted to earlier typically still held nearly all of their own share untouched. Past that point every subsequent VM birth's Stadium grant would be silently refused (soft-failed, non-fatal by existing design), even with plenty of capacity sitting idle elsewhere. Fixed by adding an O(1)-maintained free_count to StadiumVMQuota (incremented in stadium_evict(), decremented at both of stadium_admit()'s free-list-pop sites, set/adjusted in stadium_grant_quota()'s own split — this also let grant_quota drop its old O(free-list length) counting walk in favor of an O(1) read) and stadium_best_donor(), an O(live VM count) scan over quota slots returning whichever in-use VM currently has the most free cells. capsule_birth.c's birth path now splits from that VM instead of unconditionally vm_uuid_hera(). Verified with another full rerun of the 3x9x3 std79 DoE campaign from scratch — same discipline as the prior Stadium fix (any defect repair reruns the whole DoE from the top) — one continuous boot per architecture, all 9 identities simultaneously live throughout. 81/81 trials correct, 0 mismatches, DOE-RUN header sequence md5-identical to every prior run. aarch64 ~280s total (vs ~290s for the O(ncells)-scan fix alone — confirms no regression). Both known Stadium defects are now closed together on one clean campaign rerun. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EXieurDfDSsDFdnSyusuWo
824 lines
34 KiB
C
824 lines
34 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.
|
||
|
||
*/
|
||
|
||
/**
|
||
* stadium.c - The Stadium cell definitions (item 3.1) and boot-time
|
||
* allocation (item 3.2).
|
||
*
|
||
* Also the translation unit that makes stadium.h's compile-time assertions
|
||
* actually get compiled on all three architectures, not merely included by
|
||
* something that never builds.
|
||
*/
|
||
|
||
#include "starkernel/vm/stadium.h"
|
||
|
||
#ifdef __STARKERNEL__
|
||
|
||
#include "starkernel/kmalloc.h"
|
||
#include "starkernel/pmm.h"
|
||
#include "starkernel/console.h"
|
||
#include "starkernel/hal/hal.h"
|
||
#include "starkernel/q48_16.h" /* Q48_ONE -- item 4.1's reservoir starts each VM's quota at 1.0 */
|
||
#include "vm.h" /* VM_MEMORY_SIZE -- the per-VM footprint stadium_max_vm_count() budgets against */
|
||
#include "block_subsystem.h" /* blk_flush() -- STADIUM_BEHAVIOUR_MIGRATE's real write-back action */
|
||
#include "starkernel/session.h" /* session_register()/session_set_pinned() -- FABRIC-2.md §H.12 step 4 */
|
||
|
||
static StadiumCell *stadium_cell_array = (StadiumCell *)0;
|
||
static uint8_t *stadium_bitmap = (uint8_t *)0;
|
||
static uint16_t *stadium_owner = (uint16_t *)0; /* widened from uint8_t
|
||
2026-08-15: the VM-count bound is no longer capped at
|
||
255 (see stadium_max_vm_count_val below), so a byte
|
||
could silently wrap. 65535 is ample headroom. */
|
||
static size_t stadium_ncells = 0;
|
||
static int stadium_initialized = 0;
|
||
static size_t stadium_max_vm_count_val = 0; /* computed at boot, see
|
||
stadium_boot_init() and stadium_max_vm_count() */
|
||
|
||
/* Sentinel for the header's `link` field while it is reused as a free-list
|
||
* next-pointer (item 3.7): link is uint32_t, but STADIUM_CELL_NONE is
|
||
* (size_t)-1 -- 64 bits wide on this target. Casting (size_t)-1 down to
|
||
* uint32_t truncates to the same bit pattern as this constant (safe), but
|
||
* casting THIS constant back up to size_t does not sign-extend to
|
||
* STADIUM_CELL_NONE (unsafe) -- hence the explicit link_to_size()/
|
||
* size_to_link() conversions below rather than a raw cast either direction. */
|
||
#define STADIUM_LINK_NONE ((uint32_t)-1)
|
||
|
||
static uint32_t size_to_link(size_t v) {
|
||
return (v == STADIUM_CELL_NONE) ? STADIUM_LINK_NONE : (uint32_t)v;
|
||
}
|
||
|
||
static size_t link_to_size(uint32_t v) {
|
||
return (v == STADIUM_LINK_NONE) ? STADIUM_CELL_NONE : (size_t)v;
|
||
}
|
||
|
||
/*
|
||
* StadiumVMQuota - one VM's ownership record (item 3.7, FABRIC-0.md §22.3).
|
||
* See stadium.h's stadium_admit() doc for why vm_id needs a linear search
|
||
* rather than direct indexing.
|
||
*/
|
||
typedef struct {
|
||
VMUuid vm_id;
|
||
int in_use;
|
||
size_t free_head;
|
||
size_t free_count; /* FABRIC-3.md SXVI donor-floor fix: O(1)-maintained
|
||
* length of this VM's own free list -- lets
|
||
* stadium_grant_quota()'s caller pick whichever VM
|
||
* actually has the most spare capacity to split
|
||
* from, instead of always Hera specifically (the
|
||
* old policy silently starved new VM births once
|
||
* HERA's own list ran thin, even while other VMs
|
||
* granted a generous initial half sat on nearly all
|
||
* of it unused). Kept in exact lockstep with every
|
||
* free_head push/pop below and in stadium_evict()/
|
||
* stadium_admit(). */
|
||
size_t resident_head; /* FABRIC-3.md aarch64 Stadium/COOL scaling fix:
|
||
* head of this VM's own resident-cell list, threaded
|
||
* through stadium_resident_next[]/stadium_resident_prev[]
|
||
* (global, indexed by cell, below). Lets
|
||
* stadium_admit()'s eviction-fallback scan walk only
|
||
* this VM's own residents -- what its doc already
|
||
* claimed ("scoped to that SAME VM's own resident
|
||
* patrons only") -- instead of filtering the entire
|
||
* stadium_ncells array by owner, which is what the
|
||
* scan actually did before this fix. STADIUM_CELL_NONE
|
||
* when empty. */
|
||
uint64_t reservoir; /* item 4.1, FABRIC-0.md §17.7 -- Q48.16, heat this VM's
|
||
* quota holds but no resident patron has claimed.
|
||
* Invariant: Σ(resident patron heat) + reservoir ==
|
||
* Q48_ONE, checked the same way vm_physics_conserved()
|
||
* checks the fleet sum. */
|
||
} StadiumVMQuota;
|
||
|
||
/* kmalloc'd at stadium_boot_init() to stadium_max_vm_count_val entries,
|
||
* replacing the old static StadiumVMQuota stadium_quotas[STADIUM_MAX_VM_COUNT]
|
||
* (2026-08-15: the bound is computed from RAM, not a compile-time constant). */
|
||
static StadiumVMQuota *stadium_quotas = (StadiumVMQuota *)0;
|
||
|
||
/* Doubly-linked resident-list threading, one entry per cell, parallel to
|
||
* stadium_owner[] -- kmalloc'd at stadium_boot_init() to stadium_ncells
|
||
* entries each. Only meaningful for cells currently resident (bitmap bit
|
||
* set); STADIUM_LINK_NONE-equivalent (via link_to_size()/size_to_link(),
|
||
* defined below) terminates each end. Doubly-linked because stadium_evict()
|
||
* removes by cell_index directly (not by list-walk), so O(1) unlink needs a
|
||
* prev pointer, not just next. */
|
||
static uint32_t *stadium_resident_next = (uint32_t *)0;
|
||
static uint32_t *stadium_resident_prev = (uint32_t *)0;
|
||
|
||
/* resident_list_push - O(1) insert cell_index at the front of slot's own
|
||
* resident list. Caller must have already set bitmap/owner for cell_index. */
|
||
static void resident_list_push(int slot, size_t cell_index) {
|
||
size_t old_head = stadium_quotas[slot].resident_head;
|
||
|
||
stadium_resident_prev[cell_index] = size_to_link(STADIUM_CELL_NONE);
|
||
stadium_resident_next[cell_index] = size_to_link(old_head);
|
||
if (old_head != STADIUM_CELL_NONE) {
|
||
stadium_resident_prev[old_head] = size_to_link(cell_index);
|
||
}
|
||
stadium_quotas[slot].resident_head = cell_index;
|
||
}
|
||
|
||
/* resident_list_remove - O(1) splice cell_index out of slot's own resident
|
||
* list. Caller must call this BEFORE clearing cell_index's bitmap bit. */
|
||
static void resident_list_remove(int slot, size_t cell_index) {
|
||
size_t prev = link_to_size(stadium_resident_prev[cell_index]);
|
||
size_t next = link_to_size(stadium_resident_next[cell_index]);
|
||
|
||
if (prev != STADIUM_CELL_NONE) {
|
||
stadium_resident_next[prev] = size_to_link(next);
|
||
} else {
|
||
stadium_quotas[slot].resident_head = next;
|
||
}
|
||
if (next != STADIUM_CELL_NONE) {
|
||
stadium_resident_prev[next] = size_to_link(prev);
|
||
}
|
||
}
|
||
|
||
/* Returns the quota slot index for vm_id, or -1 if none is granted. */
|
||
static int quota_slot_for_vm(VMUuid vm_id) {
|
||
size_t i;
|
||
for (i = 0; i < stadium_max_vm_count_val; i++) {
|
||
if (stadium_quotas[i].in_use && vm_uuid_equal(stadium_quotas[i].vm_id, vm_id)) return i;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
/* Freestanding: no libc printf. Prints an unsigned decimal, no leading zeros. */
|
||
static void console_put_u64(uint64_t v) {
|
||
char buf[21];
|
||
int i = 20;
|
||
buf[20] = '\0';
|
||
if (v == 0) {
|
||
console_puts("0");
|
||
return;
|
||
}
|
||
while (v > 0 && i > 0) {
|
||
buf[--i] = (char)('0' + (v % 10));
|
||
v /= 10;
|
||
}
|
||
console_puts(&buf[i]);
|
||
}
|
||
|
||
int stadium_boot_init(void) {
|
||
/* Budgeted from kmalloc_get_stats(), not pmm_get_stats() (corrected
|
||
* 2026-08-15 -- see this function's doc in stadium.h): kmalloc_init()
|
||
* (M6) already carved its own fixed-size heap out of PMM before this
|
||
* ever runs, and every allocation below draws from that heap, not raw
|
||
* PMM, so PMM's own free-byte figure was the wrong pool to budget
|
||
* against. */
|
||
kmalloc_stats_t kstats = kmalloc_get_stats();
|
||
uint64_t budget_bytes = (kstats.free_bytes * (uint64_t)STADIUM_MEMORY_PERCENT) / 100u;
|
||
size_t ncells = (size_t)(budget_bytes / STADIUM_CELL_BYTES);
|
||
size_t bitmap_bytes = (ncells + 7u) / 8u;
|
||
|
||
if (ncells == 0) {
|
||
console_println("Stadium: boot-time allocation skipped (0 cells from memory budget)");
|
||
return -1;
|
||
}
|
||
|
||
StadiumCell *cells = (StadiumCell *)kmalloc(ncells * STADIUM_CELL_BYTES);
|
||
uint8_t *bitmap = (uint8_t *)kmalloc(bitmap_bytes);
|
||
uint16_t *owner = (uint16_t *)kmalloc(ncells * sizeof(uint16_t));
|
||
uint32_t *resident_next = (uint32_t *)kmalloc(ncells * sizeof(uint32_t));
|
||
uint32_t *resident_prev = (uint32_t *)kmalloc(ncells * sizeof(uint32_t));
|
||
if (!cells || !bitmap || !owner || !resident_next || !resident_prev) {
|
||
console_println("Stadium: kmalloc failed for boot-time allocation");
|
||
if (cells) kfree(cells);
|
||
if (bitmap) kfree(bitmap);
|
||
if (owner) kfree(owner);
|
||
if (resident_next) kfree(resident_next);
|
||
if (resident_prev) kfree(resident_prev);
|
||
return -1;
|
||
}
|
||
|
||
/* VM population bound (2026-08-15, replaces compile-time
|
||
* STADIUM_MAX_VM_COUNT): STADIUM_VM_MEMORY_PERCENT of whatever's left
|
||
* in the kmalloc heap AFTER the cell array above, divided by
|
||
* VM_MEMORY_SIZE (the real per-VM footprint -- 5 MiB, include/vm.h --
|
||
* not the small quota/word-slot bookkeeping tables). Floored to 1 so
|
||
* Hera can always boot; no ceiling otherwise (Captain Bob, 2026-08-15:
|
||
* the population is not knowable in advance). Clamped to what the
|
||
* uint16_t owner array can index, logged if that ever actually bites. */
|
||
kmalloc_stats_t kstats_after_cells = kmalloc_get_stats();
|
||
uint64_t vm_budget_bytes = (kstats_after_cells.free_bytes * (uint64_t)STADIUM_VM_MEMORY_PERCENT) / 100u;
|
||
size_t max_vm_count = (size_t)(vm_budget_bytes / VM_MEMORY_SIZE);
|
||
if (max_vm_count < 1u) max_vm_count = 1u;
|
||
if (max_vm_count > 65535u) {
|
||
console_println("Stadium: computed VM count exceeds owner-index range, clamped to 65535");
|
||
max_vm_count = 65535u;
|
||
}
|
||
|
||
StadiumVMQuota *quotas = (StadiumVMQuota *)kmalloc(max_vm_count * sizeof(StadiumVMQuota));
|
||
if (!quotas) {
|
||
console_println("Stadium: kmalloc failed for VM quota table");
|
||
kfree(cells);
|
||
kfree(bitmap);
|
||
kfree(owner);
|
||
kfree(resident_next);
|
||
kfree(resident_prev);
|
||
return -1;
|
||
}
|
||
|
||
{
|
||
uint8_t *raw = (uint8_t *)cells;
|
||
size_t n = ncells * STADIUM_CELL_BYTES;
|
||
size_t i;
|
||
for (i = 0; i < n; i++) raw[i] = 0;
|
||
}
|
||
{
|
||
size_t i;
|
||
for (i = 0; i < bitmap_bytes; i++) bitmap[i] = 0;
|
||
}
|
||
|
||
/* Item 3.7: chain every cell into one free list, ascending index order
|
||
* (so the first-ever pop returns cell 0, per item 3.6), granted whole
|
||
* to vm_id 0 (Hera) -- the only VM that exists (item 0.1). Reuses each
|
||
* cell's own `link` field as the next-free-cell pointer while
|
||
* unresident; see stadium_admit()'s doc for the scope of that reuse. */
|
||
{
|
||
size_t i;
|
||
for (i = 0; i < ncells; i++) {
|
||
cells[i].header.link = size_to_link((i + 1 < ncells) ? (i + 1) : STADIUM_CELL_NONE);
|
||
cells[i].header.contains = STADIUM_CONTAINS_NONE;
|
||
owner[i] = 0;
|
||
/* Not on any resident list yet -- nothing is resident until
|
||
* stadium_admit() puts it there. */
|
||
resident_next[i] = size_to_link(STADIUM_CELL_NONE);
|
||
resident_prev[i] = size_to_link(STADIUM_CELL_NONE);
|
||
}
|
||
}
|
||
{
|
||
size_t i;
|
||
for (i = 0; i < max_vm_count; i++) {
|
||
quotas[i].vm_id = vm_uuid_none();
|
||
quotas[i].in_use = 0;
|
||
quotas[i].free_head = STADIUM_CELL_NONE;
|
||
quotas[i].free_count = 0;
|
||
quotas[i].resident_head = STADIUM_CELL_NONE;
|
||
quotas[i].reservoir = 0;
|
||
}
|
||
}
|
||
quotas[0].vm_id = vm_uuid_hera();
|
||
quotas[0].in_use = 1;
|
||
quotas[0].free_head = 0;
|
||
quotas[0].free_count = ncells;
|
||
/* item 4.1, §17.7: at quota-grant time, before any resident patron
|
||
* exists, the reservoir holds the VM's entire conserved share -- mirrors
|
||
* Hera holding the fleet's whole Q48_ONE before any other VM is born. */
|
||
quotas[0].reservoir = Q48_ONE;
|
||
|
||
stadium_cell_array = cells;
|
||
stadium_bitmap = bitmap;
|
||
stadium_owner = owner;
|
||
stadium_resident_next = resident_next;
|
||
stadium_resident_prev = resident_prev;
|
||
stadium_ncells = ncells;
|
||
stadium_quotas = quotas;
|
||
stadium_max_vm_count_val = max_vm_count;
|
||
stadium_initialized = 1;
|
||
|
||
console_puts("Stadium: ");
|
||
console_put_u64((uint64_t)ncells);
|
||
console_puts(" cells (");
|
||
console_put_u64((uint64_t)(ncells * STADIUM_CELL_BYTES) / 1024u);
|
||
console_puts(" KB), ");
|
||
console_put_u64((uint64_t)max_vm_count);
|
||
console_println(" VM slots");
|
||
|
||
return 0;
|
||
}
|
||
|
||
int stadium_is_initialized(void) {
|
||
return stadium_initialized;
|
||
}
|
||
|
||
size_t stadium_cell_count(void) {
|
||
return stadium_ncells;
|
||
}
|
||
|
||
size_t stadium_max_vm_count(void) {
|
||
return stadium_max_vm_count_val;
|
||
}
|
||
|
||
StadiumCell *stadium_cells(void) {
|
||
return stadium_cell_array;
|
||
}
|
||
|
||
uint8_t *stadium_header_bitmap(void) {
|
||
return stadium_bitmap;
|
||
}
|
||
|
||
/*
|
||
* Exhaustive switch, no default: §13/§18.3 require the behaviour set to be
|
||
* closed and fixed at build time. With -Wall -Werror, omitting a case here
|
||
* for a tag that exists is a build failure, not a silent gap -- the compiler
|
||
* enforces closedness, not just this comment.
|
||
*
|
||
* MIGRATE (blocks) is real, FABRIC-2.md §B: write-back via blk_flush(),
|
||
* called with the departing patron's identity (the LBN) -- stadium_evict()
|
||
* calls this BEFORE zeroing the header, so `identity` is still valid here.
|
||
*
|
||
* DELIVER (Hermes messages) needs no extra payload action here, confirmed
|
||
* 2026-08-26 (FABRIC-2.md §B): capsules/hermes/init.4th's MSG-ALLOC already
|
||
* admits every message with this behaviour, and MSG-FREE-NODE (called from
|
||
* both explicit ack and heat-driven reap) already evicts it -- has been
|
||
* real and live on every boot for weeks. The actual delivery action
|
||
* (VM-EXEC of the payload, MSG-DELIVER) already runs in FORTH, decoupled
|
||
* from and prior to Stadium reap; by the time this dispatches, delivery is
|
||
* already done. Was printed as "(stub)" for a long time regardless, which
|
||
* was simply wrong, not a real gap -- fixed to print the departing
|
||
* message's identity, same shape as MIGRATE's lbn print, instead of
|
||
* claiming to be unimplemented.
|
||
*
|
||
* COOL (words, item 4.1; VMs, FABRIC-2.md §B) needs no extra payload action
|
||
* here either, same reasoning as DELIVER: stadium_evict()'s own universal
|
||
* reservoir credit is the whole of what "cooling off the floor" means for
|
||
* both patron kinds -- real and live for both since item 4.1 and this
|
||
* session respectively. Fixed the same way, to print the departing
|
||
* patron's identity (word_id for a word, 0 -- the patron-zero convention --
|
||
* for a VM) instead of a misleading "(stub)" label.
|
||
*
|
||
* EXPIRE (ACL) alone remains genuinely unbuilt -- no ACL-Stadium
|
||
* integration of any kind exists yet (FABRIC-2.md §B).
|
||
*/
|
||
void stadium_dispatch(size_t cell_index, StadiumBehaviour behaviour) {
|
||
console_puts("Stadium: dispatch cell=");
|
||
console_put_u64((uint64_t)cell_index);
|
||
console_puts(" behaviour=");
|
||
switch (behaviour) {
|
||
case STADIUM_BEHAVIOUR_MIGRATE: {
|
||
uint32_t lbn = (uint32_t)stadium_cell_array[cell_index].header.identity;
|
||
console_puts("MIGRATE lbn=");
|
||
console_put_u64((uint64_t)lbn);
|
||
console_println("");
|
||
blk_flush(lbn);
|
||
break;
|
||
}
|
||
case STADIUM_BEHAVIOUR_DELIVER: {
|
||
uint64_t msg_idx = stadium_cell_array[cell_index].header.identity;
|
||
console_puts("DELIVER msg_idx=");
|
||
console_put_u64(msg_idx);
|
||
console_println("");
|
||
break;
|
||
}
|
||
case STADIUM_BEHAVIOUR_EXPIRE:
|
||
console_println("EXPIRE (stub)");
|
||
break;
|
||
case STADIUM_BEHAVIOUR_COOL: {
|
||
uint64_t identity = stadium_cell_array[cell_index].header.identity;
|
||
console_puts("COOL identity=");
|
||
console_put_u64(identity);
|
||
console_println("");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
/*
|
||
* FABRIC-0.md §19.2/§19.3: density is heat / mass, read on demand from fields
|
||
* already in the header -- not a value a scheduler maintains. mass == 0
|
||
* (an empty or never-admitted slot; everything is zero-initialized until
|
||
* something is actually born into the Stadium, which nothing yet does)
|
||
* returns 0 rather than dividing by zero. An out-of-range cell_index also
|
||
* returns 0 -- there is no patron there to be dense.
|
||
*/
|
||
uint64_t stadium_density(size_t cell_index) {
|
||
StadiumPatronHeader *header;
|
||
|
||
if (cell_index >= stadium_ncells) return 0;
|
||
|
||
header = &stadium_cell_array[cell_index].header;
|
||
if (header->mass == 0) return 0;
|
||
|
||
return header->heat / (uint64_t)header->mass;
|
||
}
|
||
|
||
/* STADIUM_FLAG_PIN moved to stadium.h (FABRIC-2.md §H.12 step 3) -- see its
|
||
* doc comment there for why. */
|
||
|
||
static int bitmap_get(size_t cell_index) {
|
||
return (stadium_bitmap[cell_index / 8u] >> (cell_index % 8u)) & 1u;
|
||
}
|
||
|
||
static void bitmap_set(size_t cell_index) {
|
||
stadium_bitmap[cell_index / 8u] |= (uint8_t)(1u << (cell_index % 8u));
|
||
}
|
||
|
||
static void bitmap_clear(size_t cell_index) {
|
||
stadium_bitmap[cell_index / 8u] &= (uint8_t)~(1u << (cell_index % 8u));
|
||
}
|
||
|
||
/*
|
||
* FABRIC-0.md §17.2: reap means leaves the floor, not destroyed. Refuses a
|
||
* pinned header (§3) or one with a non-none `contains` (item 1.1: a patron
|
||
* holding another cannot be reaped). Refuses an out-of-range index or a cell
|
||
* whose discriminator bit is not set -- nothing resident there to reap.
|
||
*
|
||
* §20.5 #3 / item 3.6: panics, does not return, if a resident cell 0 (Hera,
|
||
* patron zero) is ever selected -- checked before pin/contains below, on
|
||
* purpose, so a wrongly-cleared pin cannot silently swallow the violation
|
||
* via the ordinary refusal path instead of surfacing it.
|
||
*/
|
||
int stadium_evict(size_t cell_index) {
|
||
StadiumPatronHeader *header;
|
||
uint16_t slot;
|
||
|
||
if (cell_index >= stadium_ncells) return -1;
|
||
if (!bitmap_get(cell_index)) return -1;
|
||
|
||
if (cell_index == STADIUM_HERA_CELL_INDEX) {
|
||
sk_hal_panic("Stadium: eviction selected patron zero (Hera) -- governor invariant broken");
|
||
}
|
||
|
||
header = &stadium_cell_array[cell_index].header;
|
||
if (header->flags & STADIUM_FLAG_PIN) return -1;
|
||
if (header->contains != STADIUM_CONTAINS_NONE) return -1;
|
||
|
||
stadium_dispatch(cell_index, (StadiumBehaviour)header->behaviour);
|
||
slot = stadium_owner[cell_index];
|
||
resident_list_remove((int)slot, cell_index);
|
||
bitmap_clear(cell_index);
|
||
|
||
/* item 4.1, §17.7: the departing patron's remaining heat must flow back
|
||
* to its owner's reservoir before the cell returns to the free list, or
|
||
* every reap leaks heat and Σ(resident) + reservoir drifts below
|
||
* Q48_ONE. Captured BEFORE the zero-fill below, which would otherwise
|
||
* destroy it. */
|
||
stadium_quotas[slot].reservoir += header->heat;
|
||
|
||
{
|
||
uint8_t *raw = (uint8_t *)header;
|
||
size_t i;
|
||
for (i = 0; i < sizeof(*header); i++) raw[i] = 0;
|
||
}
|
||
|
||
/* Item 3.7: return the freed cell to its owning VM's free list. contains
|
||
* must be set to the real "none" sentinel here, not left at the zero
|
||
* the fill above just wrote -- 0 is Hera's valid index, so a stray zero
|
||
* would make this freed cell look permanently "contains Hera" to the
|
||
* very next admit() that pops it. */
|
||
header->link = size_to_link(stadium_quotas[slot].free_head);
|
||
header->contains = STADIUM_CONTAINS_NONE;
|
||
stadium_quotas[slot].free_head = cell_index;
|
||
stadium_quotas[slot].free_count++;
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*
|
||
* FABRIC-0.md §19.3, §22.3, item 3.7: admit into vm_id's own quota. Pops that
|
||
* VM's free-list head first (O(1), no comparison needed -- §19.3's density
|
||
* rule only governs the full case). Only if that list is empty does this
|
||
* fall back to eviction, scoped to that SAME VM's own residents (quota
|
||
* isolation), finding the least-dense evictable one (pinned and
|
||
* contains-gated residents are skipped, never eviction candidates) and
|
||
* evicting it only if the candidate is strictly denser.
|
||
*/
|
||
size_t stadium_admit(VMUuid vm_id, const StadiumPatronHeader *candidate) {
|
||
int slot;
|
||
size_t idx;
|
||
size_t least_dense_index = STADIUM_CELL_NONE;
|
||
uint64_t least_dense_value = 0;
|
||
uint64_t candidate_density;
|
||
|
||
if (!stadium_initialized || !candidate) return STADIUM_CELL_NONE;
|
||
|
||
/* Multi-cell patrons need a continuation chain, and no header field is
|
||
* documented as carrying one's first index -- see this function's doc
|
||
* in stadium.h. Refuse rather than admit only the header and leak the
|
||
* rest, which would break capacity conservation. */
|
||
if (candidate->mass != 1) return STADIUM_CELL_NONE;
|
||
|
||
/* contains must be a real "none" or a real index -- catches garbage/
|
||
* uninitialized values, though not a zero-initialized candidate that
|
||
* meant "none": 0 is Hera's valid index, so that case is a caller
|
||
* contract issue this function cannot detect (see the header doc). */
|
||
if (candidate->contains != STADIUM_CONTAINS_NONE &&
|
||
candidate->contains >= stadium_ncells) return STADIUM_CELL_NONE;
|
||
|
||
slot = quota_slot_for_vm(vm_id);
|
||
if (slot < 0) return STADIUM_CELL_NONE;
|
||
|
||
if (stadium_quotas[slot].free_head != STADIUM_CELL_NONE) {
|
||
idx = stadium_quotas[slot].free_head;
|
||
stadium_quotas[slot].free_head = link_to_size(stadium_cell_array[idx].header.link);
|
||
stadium_quotas[slot].free_count--;
|
||
stadium_cell_array[idx].header = *candidate;
|
||
bitmap_set(idx);
|
||
/* Item 4.2 fix (§25.7): record ownership so stadium_evict()'s
|
||
* reservoir credit and free-list return land on the VM that actually
|
||
* admitted this patron, not whatever owner[idx] held at boot. */
|
||
stadium_owner[idx] = (uint16_t)slot;
|
||
resident_list_push(slot, idx);
|
||
return idx;
|
||
}
|
||
|
||
/* FABRIC-3.md aarch64 Stadium/COOL scaling fix: this used to be a
|
||
* for (i = 0; i < stadium_ncells; i++) scan of the ENTIRE global array,
|
||
* filtered down to this VM's own owned cells by an `owner[i] != slot`
|
||
* check inside the loop -- O(stadium_ncells) on every call that reaches
|
||
* here, not O(this VM's own resident count) as the function's own doc
|
||
* above already claimed ("scoped to that SAME VM's own residents").
|
||
* Root-caused live (2026-09-11/12): word-execution admission
|
||
* (stadium_word_dispatch(), stadium_words.c) calls stadium_admit() once
|
||
* per distinct word a VM's own capsule executes, and each VM's granted
|
||
* quota is a small, halving fraction of Hera's original pool -- once
|
||
* that quota's free list empties (a handful of words in), EVERY further
|
||
* new word this VM ever executes hit this scan. At stadium_ncells in
|
||
* the hundreds of thousands (aarch64's -m 4096 vs. amd64/riscv64's
|
||
* -m 1024 --> a 4x larger kmalloc heap --> a 4x larger Stadium, see
|
||
* FABRIC-3.md SXV/SXVI), repeated hundred-thousand-cell scans compounded
|
||
* into the observed 90+ minute stall. Walking this VM's own
|
||
* resident_head list instead bounds the cost by how many cells this VM
|
||
* actually owns (<= its granted quota, itself <= a small constant
|
||
* multiple of DICTIONARY_SIZE in practice), independent of
|
||
* stadium_ncells. */
|
||
for (idx = stadium_quotas[slot].resident_head; idx != STADIUM_CELL_NONE;
|
||
idx = link_to_size(stadium_resident_next[idx])) {
|
||
StadiumPatronHeader *h = &stadium_cell_array[idx].header;
|
||
|
||
if (h->flags & STADIUM_FLAG_PIN) continue;
|
||
if (h->contains != STADIUM_CONTAINS_NONE) continue;
|
||
|
||
{
|
||
uint64_t d = stadium_density(idx);
|
||
if (least_dense_index == STADIUM_CELL_NONE || d < least_dense_value) {
|
||
least_dense_index = idx;
|
||
least_dense_value = d;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (least_dense_index == STADIUM_CELL_NONE) return STADIUM_CELL_NONE;
|
||
|
||
candidate_density = candidate->heat / (uint64_t)candidate->mass; /* mass == 1, guaranteed above */
|
||
if (candidate_density <= least_dense_value) return STADIUM_CELL_NONE;
|
||
|
||
if (stadium_evict(least_dense_index) != 0) return STADIUM_CELL_NONE;
|
||
|
||
/* stadium_evict() just pushed least_dense_index onto quotas[slot]'s free
|
||
* list -- its owner is `slot`, the same quota we scoped the search to.
|
||
* Single-threaded today (§21.1/§21.2: real concurrency is step-one, not
|
||
* yet built), so nothing else can have touched the list meanwhile; pop
|
||
* it straight back off. */
|
||
idx = stadium_quotas[slot].free_head;
|
||
stadium_quotas[slot].free_head = link_to_size(stadium_cell_array[idx].header.link);
|
||
stadium_quotas[slot].free_count--;
|
||
stadium_cell_array[idx].header = *candidate;
|
||
bitmap_set(idx);
|
||
/* Same fix as the free-list-pop path above -- stadium_evict() just wrote
|
||
* owner[idx] = slot as part of reaping least_dense_index, so this is
|
||
* currently a no-op in practice, but it must not be assumed to stay a
|
||
* no-op: this is the correctness statement, not a redundant write. */
|
||
stadium_owner[idx] = (uint16_t)slot;
|
||
resident_list_push(slot, idx);
|
||
return idx;
|
||
}
|
||
|
||
/*
|
||
* FABRIC-0.md item 4.1a: one-time initial quota grant, not item 1.3's
|
||
* (still-unbuilt) recurring transfer. See stadium.h's doc for the full
|
||
* argument. from_slot's free-list length is now read directly from its
|
||
* O(1)-maintained free_count (FABRIC-3.md SXVI donor-floor fix) rather than
|
||
* walked -- the walk below only detaches the first `half` cells into
|
||
* new_vm_id's own list, reassigning owner as it goes.
|
||
*/
|
||
int stadium_grant_quota(VMUuid new_vm_id, VMUuid from_vm_id) {
|
||
int from_slot, new_slot, i;
|
||
size_t half, idx, last_new, new_head, remainder_head;
|
||
|
||
if (!stadium_initialized) return -1;
|
||
if (quota_slot_for_vm(new_vm_id) >= 0) return -1;
|
||
|
||
from_slot = quota_slot_for_vm(from_vm_id);
|
||
if (from_slot < 0) return -1;
|
||
|
||
new_slot = -1;
|
||
for (i = 0; i < (int)stadium_max_vm_count_val; i++) {
|
||
if (!stadium_quotas[i].in_use) { new_slot = i; break; }
|
||
}
|
||
if (new_slot < 0) return -1;
|
||
|
||
half = stadium_quotas[from_slot].free_count / 2;
|
||
if (half == 0) return -1; /* fewer than 2 free cells -- nothing to split */
|
||
|
||
new_head = stadium_quotas[from_slot].free_head;
|
||
idx = new_head;
|
||
last_new = STADIUM_CELL_NONE;
|
||
for (i = 0; i < (int)half; i++) {
|
||
stadium_owner[idx] = (uint16_t)new_slot;
|
||
last_new = idx;
|
||
idx = link_to_size(stadium_cell_array[idx].header.link);
|
||
}
|
||
/* idx now points to the first cell staying with from_slot (or
|
||
* STADIUM_CELL_NONE if half == count, i.e. an even list fully moved). */
|
||
remainder_head = idx;
|
||
stadium_cell_array[last_new].header.link = size_to_link(STADIUM_CELL_NONE);
|
||
stadium_quotas[from_slot].free_head = remainder_head;
|
||
stadium_quotas[from_slot].free_count -= half;
|
||
|
||
stadium_quotas[new_slot].vm_id = new_vm_id;
|
||
stadium_quotas[new_slot].in_use = 1;
|
||
stadium_quotas[new_slot].free_head = new_head;
|
||
stadium_quotas[new_slot].free_count = half;
|
||
stadium_quotas[new_slot].resident_head = STADIUM_CELL_NONE;
|
||
stadium_quotas[new_slot].reservoir = Q48_ONE;
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*
|
||
* stadium_best_donor - FABRIC-3.md SXVI donor-floor fix: which currently
|
||
* in-use VM has the most free (unclaimed) cells right now. Callers granting
|
||
* a new VM's initial quota (capsule_birth.c) should split from THIS VM, not
|
||
* unconditionally from Hera -- the old always-Hera policy meant her own free
|
||
* list converged toward empty after a bounded number of grants (each grant
|
||
* halves what remains) while every VM she had already granted to typically
|
||
* still held almost all of its own share untouched, silently starving every
|
||
* later birth even though the Stadium as a whole was nowhere near full.
|
||
* O(stadium_max_vm_count_val) -- a linear scan over quota SLOTS (bounded by
|
||
* live VM population, not cell count), not the cells themselves.
|
||
*
|
||
* @return The VM with the largest free_count, or vm_uuid_none() if no VM
|
||
* holds a quota yet (Stadium not initialized, or called before
|
||
* Hera's own boot-time grant).
|
||
*/
|
||
VMUuid stadium_best_donor(void) {
|
||
size_t i;
|
||
size_t best_count = 0;
|
||
int found = 0;
|
||
VMUuid best = vm_uuid_none();
|
||
|
||
if (!stadium_initialized) return best;
|
||
|
||
for (i = 0; i < stadium_max_vm_count_val; i++) {
|
||
if (!stadium_quotas[i].in_use) continue;
|
||
if (!found || stadium_quotas[i].free_count > best_count) {
|
||
best_count = stadium_quotas[i].free_count;
|
||
best = stadium_quotas[i].vm_id;
|
||
found = 1;
|
||
}
|
||
}
|
||
return best;
|
||
}
|
||
|
||
/* Shared by stadium_cell_heat_get()/_set(): resident AND owned by vm_id's
|
||
* own quota slot. Returns the quota slot on success, -1 on any refusal. */
|
||
static int owned_resident_slot(VMUuid vm_id, size_t cell_index) {
|
||
int slot = quota_slot_for_vm(vm_id);
|
||
if (slot < 0) return -1;
|
||
if (cell_index >= stadium_ncells) return -1;
|
||
if (!bitmap_get(cell_index)) return -1;
|
||
if (stadium_owner[cell_index] != (uint16_t)slot) return -1;
|
||
return slot;
|
||
}
|
||
|
||
uint64_t stadium_cell_heat_get(VMUuid vm_id, size_t cell_index) {
|
||
if (owned_resident_slot(vm_id, cell_index) < 0) return 0;
|
||
return stadium_cell_array[cell_index].header.heat;
|
||
}
|
||
|
||
int stadium_cell_heat_set(VMUuid vm_id, size_t cell_index, uint64_t new_heat) {
|
||
int slot = owned_resident_slot(vm_id, cell_index);
|
||
uint64_t old_heat, delta, pulled;
|
||
|
||
if (slot < 0) return -1;
|
||
|
||
old_heat = stadium_cell_array[cell_index].header.heat;
|
||
if (new_heat == old_heat) return 0;
|
||
|
||
if (new_heat > old_heat) {
|
||
delta = new_heat - old_heat;
|
||
pulled = (delta > stadium_quotas[slot].reservoir) ? stadium_quotas[slot].reservoir : delta;
|
||
if (pulled < delta) return -1; /* insufficient -- no partial credit, no mutation */
|
||
stadium_quotas[slot].reservoir -= pulled;
|
||
} else {
|
||
delta = old_heat - new_heat;
|
||
stadium_quotas[slot].reservoir += delta;
|
||
}
|
||
|
||
stadium_cell_array[cell_index].header.heat = new_heat;
|
||
return 0;
|
||
}
|
||
|
||
uint64_t stadium_reservoir_pull(VMUuid vm_id, uint64_t amount) {
|
||
int slot = quota_slot_for_vm(vm_id);
|
||
uint64_t pulled;
|
||
|
||
if (slot < 0) return 0;
|
||
|
||
pulled = (amount > stadium_quotas[slot].reservoir) ? stadium_quotas[slot].reservoir : amount;
|
||
stadium_quotas[slot].reservoir -= pulled;
|
||
return pulled;
|
||
}
|
||
|
||
void stadium_reservoir_push(VMUuid vm_id, uint64_t amount) {
|
||
int slot = quota_slot_for_vm(vm_id);
|
||
|
||
if (slot < 0) return;
|
||
stadium_quotas[slot].reservoir += amount;
|
||
}
|
||
|
||
uint64_t stadium_reservoir_peek(VMUuid vm_id) {
|
||
int slot = quota_slot_for_vm(vm_id);
|
||
|
||
if (slot < 0) return 0;
|
||
return stadium_quotas[slot].reservoir;
|
||
}
|
||
|
||
int stadium_quota_slot_for_vm(VMUuid vm_id) {
|
||
return quota_slot_for_vm(vm_id);
|
||
}
|
||
|
||
uint64_t stadium_resident_sum(VMUuid vm_id) {
|
||
int slot = quota_slot_for_vm(vm_id);
|
||
uint64_t sum = 0;
|
||
size_t i;
|
||
|
||
if (slot < 0) return 0;
|
||
|
||
for (i = 0; i < stadium_ncells; i++) {
|
||
if (!bitmap_get(i)) continue;
|
||
if (stadium_owner[i] != (uint16_t)slot) continue;
|
||
sum += stadium_cell_array[i].header.heat;
|
||
}
|
||
return sum;
|
||
}
|
||
|
||
/*
|
||
* FABRIC-0.md item 3.6 / item 4.1: see stadium.h's doc. Idempotent via the
|
||
* item-3.1 discriminator bitmap -- if cell 0 already reads as resident,
|
||
* something already birthed her (or, if it isn't actually Hera, something
|
||
* else already claimed cell 0 -- either way this must not clobber it).
|
||
*
|
||
* FABRIC-2.md §H.12 step 4: pinning no longer happens by setting
|
||
* STADIUM_FLAG_PIN on the candidate before admission -- session.c's
|
||
* session_set_pinned() is now the sole choke point for that bit (§H.10).
|
||
* Hera is admitted unpinned, then registered as session zero
|
||
* (self-referential parent, matching capsule_run.h's parent_vm_id ==
|
||
* vm_id convention for the root) and pinned through the session layer.
|
||
* Session registration/pinning is soft-fail (logged, not fatal) -- Hera's
|
||
* actual Stadium admission above already succeeded and is what the
|
||
* patron-zero invariant is about; session.c not yet being initialized
|
||
* must not break that.
|
||
*/
|
||
int stadium_birth_hera(void) {
|
||
StadiumPatronHeader candidate;
|
||
size_t idx;
|
||
|
||
if (!stadium_initialized) return -1;
|
||
if (stadium_ncells > 0 && bitmap_get(STADIUM_HERA_CELL_INDEX)) return 0;
|
||
|
||
{
|
||
uint8_t *raw = (uint8_t *)&candidate;
|
||
size_t i;
|
||
for (i = 0; i < sizeof(candidate); i++) raw[i] = 0;
|
||
}
|
||
candidate.identity = 0;
|
||
candidate.heat = 0;
|
||
candidate.ttl = 0;
|
||
candidate.link = STADIUM_LINK_NONE;
|
||
candidate.contains = STADIUM_CONTAINS_NONE;
|
||
candidate.mass = 1;
|
||
candidate.flags = 0;
|
||
candidate.behaviour = (uint8_t)STADIUM_BEHAVIOUR_COOL;
|
||
|
||
idx = stadium_admit(vm_uuid_hera(), &candidate);
|
||
if (idx == STADIUM_CELL_NONE) return -1; /* refused; should not happen (quota is fresh and empty) */
|
||
if (idx != STADIUM_HERA_CELL_INDEX) {
|
||
sk_hal_panic("Stadium: birth_hera did not land on cell 0 -- patron-zero invariant broken");
|
||
}
|
||
|
||
{
|
||
Session *s = session_register(vm_uuid_hera(), vm_uuid_hera(), "Hera");
|
||
if (s) {
|
||
s->stadium_cell = idx;
|
||
session_set_pinned(vm_uuid_hera(), 1);
|
||
} else {
|
||
console_println("Stadium: session_register failed for Hera (session.c not yet initialized?)");
|
||
}
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
#endif /* __STARKERNEL__ */ |