stadium: make VM population bound RAM-derived, not a static array of 4
Replaces STADIUM_MAX_VM_COUNT (Kconfig, hardcoded default 4) with a boot-time computation, mirroring the pattern stadium_boot_init() already used for the cell pool. New Kconfig STADIUM_VM_MEMORY_PERCENT (default 50): max_vm_count = (kmalloc_get_stats().free_bytes after the cell array * STADIUM_VM_MEMORY_PERCENT / 100) / VM_MEMORY_SIZE, floored to 1, no ceiling (population is not knowable in advance - could be 4, could be 4000). stadium_quotas and word_slots (plus stat_promotions/stat_evictions) are now kmalloc'd to the computed count instead of declared with a macro. New accessor stadium_max_vm_count() replaces every STADIUM_MAX_VM_COUNT reference, including capsule_birth.c's birth-refusal gate. Two things found and fixed along the way: - The existing cell-pool budget was sourced from pmm_get_stats(), which reflects physical pages PMM hasn't handed to any subsystem yet - but the actual allocation is kmalloc(), which draws from the separate, fixed-size heap kmalloc_init() (M6) already carved out of PMM before stadium_boot_init() ever runs. Budgeting against PMM's leftover and allocating from the kmalloc heap are two different pools. Both the cell budget and the new VM-count budget now source from kmalloc_get_stats() instead. - stadium_owner[] (which VM's quota owns each cell) was uint8_t, capped at 255 slots by a compile-time assert tied to the old macro. Widened to uint16_t (65535 slots of headroom) with a runtime clamp + log if the computed count ever exceeds that, since there's no ceiling anymore. Three-arch QEMU acceptance: all clean to ok>, computed VM count genuinely differs by actual available RAM (amd64/riscv64: 50 slots at -m 1024, aarch64: 101 slots), Stadium conservation invariant identical across all three (resident_sum=43691 reservoir=21845 sum=65536). logs/20260815-080526/amd64, logs/20260815-080826/aarch64, logs/20260815-080952/riscv64. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
154eddeab0
commit
00e657019e
@@ -34,7 +34,6 @@
|
||||
#include "starkernel/vm/stadium.h" /* item 4.1a -- stadium_grant_quota() */
|
||||
#include "vm.h"
|
||||
#include "platform_alloc.h"
|
||||
#include "starforth_config.h" /* STADIUM_MAX_VM_COUNT */
|
||||
|
||||
/*===========================================================================
|
||||
* VM Execution Hooks
|
||||
@@ -451,7 +450,7 @@ CapsuleRunResult capsule_birth_baby(
|
||||
CapsuleValidateResult vr = capsule_validate(cap, arena, dir->arena_size, 1);
|
||||
if (vr != CAPSULE_VALID) return CAPSULE_RUN_ERR_INVALID;
|
||||
|
||||
if (vm_registry_live_count() >= STADIUM_MAX_VM_COUNT) {
|
||||
if (vm_registry_live_count() >= stadium_max_vm_count()) {
|
||||
capsule_parity_log_birth_failed(vm_uuid_none(), cap->capsule_id,
|
||||
CAPSULE_RUN_ERR_FLEET_FULL, 0);
|
||||
return CAPSULE_RUN_ERR_FLEET_FULL;
|
||||
|
||||
+81
-32
@@ -39,12 +39,18 @@
|
||||
#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 */
|
||||
|
||||
static StadiumCell *stadium_cell_array = (StadiumCell *)0;
|
||||
static uint8_t *stadium_bitmap = (uint8_t *)0;
|
||||
static uint8_t *stadium_owner = (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
|
||||
@@ -79,12 +85,15 @@ typedef struct {
|
||||
* checks the fleet sum. */
|
||||
} StadiumVMQuota;
|
||||
|
||||
static StadiumVMQuota stadium_quotas[STADIUM_MAX_VM_COUNT];
|
||||
/* 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;
|
||||
|
||||
/* Returns the quota slot index for vm_id, or -1 if none is granted. */
|
||||
static int quota_slot_for_vm(VMUuid vm_id) {
|
||||
int i;
|
||||
for (i = 0; i < STADIUM_MAX_VM_COUNT; i++) {
|
||||
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;
|
||||
@@ -107,8 +116,14 @@ static void console_put_u64(uint64_t v) {
|
||||
}
|
||||
|
||||
int stadium_boot_init(void) {
|
||||
pmm_stats_t pmm = pmm_get_stats();
|
||||
uint64_t budget_bytes = (pmm.free_bytes * (uint64_t)STADIUM_MEMORY_PERCENT) / 100u;
|
||||
/* 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;
|
||||
|
||||
@@ -117,9 +132,9 @@ int stadium_boot_init(void) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
StadiumCell *cells = (StadiumCell *)kmalloc(ncells * STADIUM_CELL_BYTES);
|
||||
StadiumCell *cells = (StadiumCell *)kmalloc(ncells * STADIUM_CELL_BYTES);
|
||||
uint8_t *bitmap = (uint8_t *)kmalloc(bitmap_bytes);
|
||||
uint8_t *owner = (uint8_t *)kmalloc(ncells);
|
||||
uint16_t *owner = (uint16_t *)kmalloc(ncells * sizeof(uint16_t));
|
||||
if (!cells || !bitmap || !owner) {
|
||||
console_println("Stadium: kmalloc failed for boot-time allocation");
|
||||
if (cells) kfree(cells);
|
||||
@@ -128,6 +143,32 @@ int stadium_boot_init(void) {
|
||||
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);
|
||||
return -1;
|
||||
}
|
||||
|
||||
{
|
||||
uint8_t *raw = (uint8_t *)cells;
|
||||
size_t n = ncells * STADIUM_CELL_BYTES;
|
||||
@@ -153,33 +194,37 @@ int stadium_boot_init(void) {
|
||||
}
|
||||
}
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < STADIUM_MAX_VM_COUNT; i++) {
|
||||
stadium_quotas[i].vm_id = vm_uuid_none();
|
||||
stadium_quotas[i].in_use = 0;
|
||||
stadium_quotas[i].free_head = STADIUM_CELL_NONE;
|
||||
stadium_quotas[i].reservoir = 0;
|
||||
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].reservoir = 0;
|
||||
}
|
||||
}
|
||||
stadium_quotas[0].vm_id = vm_uuid_hera();
|
||||
stadium_quotas[0].in_use = 1;
|
||||
stadium_quotas[0].free_head = 0;
|
||||
quotas[0].vm_id = vm_uuid_hera();
|
||||
quotas[0].in_use = 1;
|
||||
quotas[0].free_head = 0;
|
||||
/* 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. */
|
||||
stadium_quotas[0].reservoir = Q48_ONE;
|
||||
quotas[0].reservoir = Q48_ONE;
|
||||
|
||||
stadium_cell_array = cells;
|
||||
stadium_bitmap = bitmap;
|
||||
stadium_owner = owner;
|
||||
stadium_ncells = ncells;
|
||||
stadium_initialized = 1;
|
||||
stadium_cell_array = cells;
|
||||
stadium_bitmap = bitmap;
|
||||
stadium_owner = owner;
|
||||
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_println(" KB)");
|
||||
console_puts(" KB), ");
|
||||
console_put_u64((uint64_t)max_vm_count);
|
||||
console_println(" VM slots");
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -192,6 +237,10 @@ 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;
|
||||
}
|
||||
@@ -276,7 +325,7 @@ static void bitmap_clear(size_t cell_index) {
|
||||
*/
|
||||
int stadium_evict(size_t cell_index) {
|
||||
StadiumPatronHeader *header;
|
||||
uint8_t slot;
|
||||
uint16_t slot;
|
||||
|
||||
if (cell_index >= stadium_ncells) return -1;
|
||||
if (!bitmap_get(cell_index)) return -1;
|
||||
@@ -361,7 +410,7 @@ size_t stadium_admit(VMUuid vm_id, const StadiumPatronHeader *candidate) {
|
||||
/* 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] = (uint8_t)slot;
|
||||
stadium_owner[idx] = (uint16_t)slot;
|
||||
return idx;
|
||||
}
|
||||
|
||||
@@ -369,7 +418,7 @@ size_t stadium_admit(VMUuid vm_id, const StadiumPatronHeader *candidate) {
|
||||
StadiumPatronHeader *h;
|
||||
|
||||
if (!bitmap_get(i)) continue;
|
||||
if (stadium_owner[i] != (uint8_t)slot) continue;
|
||||
if (stadium_owner[i] != (uint16_t)slot) continue;
|
||||
h = &stadium_cell_array[i].header;
|
||||
if (h->flags & STADIUM_FLAG_PIN) continue;
|
||||
if (h->contains != STADIUM_CONTAINS_NONE) continue;
|
||||
@@ -403,7 +452,7 @@ size_t stadium_admit(VMUuid vm_id, const StadiumPatronHeader *candidate) {
|
||||
* 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] = (uint8_t)slot;
|
||||
stadium_owner[idx] = (uint16_t)slot;
|
||||
return idx;
|
||||
}
|
||||
|
||||
@@ -425,7 +474,7 @@ int stadium_grant_quota(VMUuid new_vm_id, VMUuid from_vm_id) {
|
||||
if (from_slot < 0) return -1;
|
||||
|
||||
new_slot = -1;
|
||||
for (i = 0; i < STADIUM_MAX_VM_COUNT; i++) {
|
||||
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;
|
||||
@@ -444,7 +493,7 @@ int stadium_grant_quota(VMUuid new_vm_id, VMUuid from_vm_id) {
|
||||
idx = new_head;
|
||||
last_new = STADIUM_CELL_NONE;
|
||||
for (i = 0; i < (int)half; i++) {
|
||||
stadium_owner[idx] = (uint8_t)new_slot;
|
||||
stadium_owner[idx] = (uint16_t)new_slot;
|
||||
last_new = idx;
|
||||
idx = link_to_size(stadium_cell_array[idx].header.link);
|
||||
}
|
||||
@@ -469,7 +518,7 @@ static int owned_resident_slot(VMUuid vm_id, size_t cell_index) {
|
||||
if (slot < 0) return -1;
|
||||
if (cell_index >= stadium_ncells) return -1;
|
||||
if (!bitmap_get(cell_index)) return -1;
|
||||
if (stadium_owner[cell_index] != (uint8_t)slot) return -1;
|
||||
if (stadium_owner[cell_index] != (uint16_t)slot) return -1;
|
||||
return slot;
|
||||
}
|
||||
|
||||
@@ -539,7 +588,7 @@ uint64_t stadium_resident_sum(VMUuid vm_id) {
|
||||
|
||||
for (i = 0; i < stadium_ncells; i++) {
|
||||
if (!bitmap_get(i)) continue;
|
||||
if (stadium_owner[i] != (uint8_t)slot) continue;
|
||||
if (stadium_owner[i] != (uint16_t)slot) continue;
|
||||
sum += stadium_cell_array[i].header.heat;
|
||||
}
|
||||
return sum;
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
|
||||
#include "starkernel/vm/stadium.h"
|
||||
#include "starkernel/console.h"
|
||||
#include "starkernel/kmalloc.h"
|
||||
#include "starkernel/q48_16.h" /* Q48_ONE -- diagnostic print only */
|
||||
#include "vm.h" /* DICTIONARY_SIZE, WORD_ID_INVALID */
|
||||
|
||||
@@ -55,15 +56,61 @@ typedef struct {
|
||||
uint64_t last_decay_tick;
|
||||
} StadiumWordSlot;
|
||||
|
||||
static StadiumWordSlot word_slots[STADIUM_MAX_VM_COUNT][DICTIONARY_SIZE];
|
||||
static int words_initialized = 0;
|
||||
static uint64_t stat_promotions[STADIUM_MAX_VM_COUNT];
|
||||
static uint64_t stat_evictions[STADIUM_MAX_VM_COUNT];
|
||||
/* Row-per-quota-slot, kmalloc'd at stadium_words_init() to stadium_max_vm_
|
||||
* count() rows of DICTIONARY_SIZE entries each -- replaces the old static
|
||||
* word_slots[STADIUM_MAX_VM_COUNT][DICTIONARY_SIZE] (2026-08-15: the VM
|
||||
* count bound is computed from RAM, not a compile-time constant, so this
|
||||
* can no longer be a flat static array). */
|
||||
static StadiumWordSlot **word_slots = (StadiumWordSlot **)0;
|
||||
static int words_initialized = 0;
|
||||
static uint64_t *stat_promotions = (uint64_t *)0;
|
||||
static uint64_t *stat_evictions = (uint64_t *)0;
|
||||
|
||||
void stadium_words_init(void) {
|
||||
int slot;
|
||||
size_t slot;
|
||||
uint32_t i;
|
||||
for (slot = 0; slot < STADIUM_MAX_VM_COUNT; slot++) {
|
||||
size_t count = stadium_max_vm_count();
|
||||
|
||||
if (words_initialized) return; /* not re-entrant -- see stadium_words.h */
|
||||
|
||||
if (count == 0) {
|
||||
console_println("Stadium words: init skipped (Stadium not initialized)");
|
||||
return;
|
||||
}
|
||||
|
||||
word_slots = (StadiumWordSlot **)kmalloc(count * sizeof(StadiumWordSlot *));
|
||||
stat_promotions = (uint64_t *)kmalloc(count * sizeof(uint64_t));
|
||||
stat_evictions = (uint64_t *)kmalloc(count * sizeof(uint64_t));
|
||||
if (!word_slots || !stat_promotions || !stat_evictions) {
|
||||
console_println("Stadium words: kmalloc failed for per-VM tables");
|
||||
if (word_slots) kfree(word_slots);
|
||||
if (stat_promotions) kfree(stat_promotions);
|
||||
if (stat_evictions) kfree(stat_evictions);
|
||||
word_slots = (StadiumWordSlot **)0;
|
||||
stat_promotions = (uint64_t *)0;
|
||||
stat_evictions = (uint64_t *)0;
|
||||
return;
|
||||
}
|
||||
|
||||
for (slot = 0; slot < count; slot++) {
|
||||
word_slots[slot] = (StadiumWordSlot *)kmalloc(DICTIONARY_SIZE * sizeof(StadiumWordSlot));
|
||||
if (!word_slots[slot]) {
|
||||
console_println("Stadium words: kmalloc failed for a word-slot row");
|
||||
/* Free everything allocated so far, including this row's
|
||||
* predecessors, and bail the same all-or-nothing way
|
||||
* stadium_boot_init() does. */
|
||||
{
|
||||
size_t j;
|
||||
for (j = 0; j < slot; j++) kfree(word_slots[j]);
|
||||
}
|
||||
kfree(word_slots);
|
||||
kfree(stat_promotions);
|
||||
kfree(stat_evictions);
|
||||
word_slots = (StadiumWordSlot **)0;
|
||||
stat_promotions = (uint64_t *)0;
|
||||
stat_evictions = (uint64_t *)0;
|
||||
return;
|
||||
}
|
||||
for (i = 0; i < DICTIONARY_SIZE; i++) {
|
||||
word_slots[slot][i].cell_index = STADIUM_CELL_NONE;
|
||||
word_slots[slot][i].last_decay_tick = 0;
|
||||
|
||||
Reference in New Issue
Block a user