/* 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_blocks.c - Block patrons on the Stadium (FABRIC-2.md §B/§D, * MIGRATE punch-list item). See stadium_blocks.h for the design rationale. */ #include "starkernel/vm/stadium_blocks.h" #ifdef __STARKERNEL__ #include "starkernel/vm/stadium.h" #include "starkernel/console.h" #include "starkernel/kmalloc.h" #include "starkernel/q48_16.h" /* Q48_ONE */ #include "starforth_config.h" /* STADIUM_BLOCK_HEAT_QUANTUM, STADIUM_BLOCK_COOL_RATE_Q48, STADIUM_BLOCK_TRACK_CAP_MULT */ /* * StadiumBlockSlot - one entry in the fixed-capacity open-addressing hash * table. Keyed by (quota_slot, lbn) together -- LBN numbering is global * across VMs (unlike word_id), but two VMs touching the SAME lbn must still * land in different Stadium cells under different quotas, so both fields * are part of the key (same lesson stadium_words.c's item 4.2 fix already * applied for word_id). * * `state` is three-valued, not a bool: standard open-addressing deletion * needs EMPTY vs. TOMBSTONE distinguished from OCCUPIED, or reclaiming a * stale entry's slot (self-healing, see stadium_word_dispatch()'s identical * problem for the dense word map) would silently break the probe chain for * any other key that happened to hash into the same bucket and probed past * this slot to find its own home. */ typedef enum { BLOCK_SLOT_EMPTY = 0, BLOCK_SLOT_OCCUPIED, BLOCK_SLOT_TOMBSTONE } StadiumBlockSlotState; typedef struct { uint8_t state; int16_t quota_slot; uint32_t lbn; size_t cell_index; uint64_t last_decay_tick; } StadiumBlockSlot; static StadiumBlockSlot *block_slots = (StadiumBlockSlot *)0; static size_t block_slots_cap = 0; static int blocks_initialized = 0; /* Per-quota-slot counters, kmalloc'd to stadium_max_vm_count() entries -- * same reasoning as stadium_words.c's stat_promotions/stat_evictions: a * single global counter can't answer "how is vm_id's own table doing" * once more than one VM holds a quota. */ static uint64_t *stat_promotions = (uint64_t *)0; static uint64_t *stat_evictions = (uint64_t *)0; void stadium_blocks_init(void) { size_t vm_count; size_t cell_count; size_t i; if (blocks_initialized) return; /* not re-entrant -- see stadium_blocks.h */ cell_count = stadium_cell_count(); vm_count = stadium_max_vm_count(); if (cell_count == 0 || vm_count == 0) { console_println("Stadium blocks: init skipped (Stadium not initialized)"); return; } block_slots_cap = cell_count * (size_t)STADIUM_BLOCK_TRACK_CAP_MULT; block_slots = (StadiumBlockSlot *)kmalloc(block_slots_cap * sizeof(StadiumBlockSlot)); stat_promotions = (uint64_t *)kmalloc(vm_count * sizeof(uint64_t)); stat_evictions = (uint64_t *)kmalloc(vm_count * sizeof(uint64_t)); if (!block_slots || !stat_promotions || !stat_evictions) { console_println("Stadium blocks: kmalloc failed for tracking tables"); if (block_slots) kfree(block_slots); if (stat_promotions) kfree(stat_promotions); if (stat_evictions) kfree(stat_evictions); block_slots = (StadiumBlockSlot *)0; block_slots_cap = 0; stat_promotions = (uint64_t *)0; stat_evictions = (uint64_t *)0; return; } for (i = 0; i < block_slots_cap; i++) { block_slots[i].state = BLOCK_SLOT_EMPTY; block_slots[i].quota_slot = -1; block_slots[i].lbn = 0; block_slots[i].cell_index = STADIUM_CELL_NONE; block_slots[i].last_decay_tick = 0; } for (i = 0; i < vm_count; i++) { stat_promotions[i] = 0; stat_evictions[i] = 0; } blocks_initialized = 1; } static int cell_is_resident(size_t idx) { const uint8_t *bm = stadium_header_bitmap(); if (!bm) return 0; return (bm[idx / 8u] >> (idx % 8u)) & 1u; } /* * block_hash - Multiplicative mix of (quota_slot, lbn) into [0, block_slots_cap). * No libc hash dependency (freestanding kernel); Knuth's multiplicative * method (constant 2654435761, the standard 32-bit golden-ratio prime) is * enough to spread a monotonically-touched LBN sequence across buckets -- * this table's job is short probe chains under normal access patterns, not * cryptographic distribution. */ static size_t block_hash(int16_t quota_slot, uint32_t lbn) { uint32_t key = lbn ^ ((uint32_t)(uint16_t)quota_slot << 16) ^ ((uint32_t)(uint16_t)quota_slot); key = key * 2654435761u; return (size_t)(key % (uint32_t)block_slots_cap); } /* * table_probe - Single linear-probe pass serving both lookup and insertion. * Walks at most block_slots_cap slots starting at the key's hash bucket. * * - If an OCCUPIED slot matching (quota_slot, lbn) is found: returns its * index, *out_found = 1. * - Else, if any EMPTY or TOMBSTONE slot was seen along the way: returns * the FIRST such slot's index (so insertion reuses the earliest opening, * standard open-addressing practice), *out_found = 0. * - Else (table genuinely full, no opening and no match): returns * block_slots_cap, *out_found = 0. * * Probing must continue past TOMBSTONE slots when searching for a match -- * a tombstone marks "something used to live here," not "the chain ends * here," or a live entry further down the same chain would become * unreachable. */ static size_t table_probe(int16_t quota_slot, uint32_t lbn, int *out_found) { size_t start = block_hash(quota_slot, lbn); size_t first_open = block_slots_cap; /* sentinel: none seen yet */ size_t i; for (i = 0; i < block_slots_cap; i++) { size_t idx = (start + i) % block_slots_cap; StadiumBlockSlot *s = &block_slots[idx]; if (s->state == BLOCK_SLOT_EMPTY) { if (first_open == block_slots_cap) first_open = idx; break; /* empty slot ends any possible chain for this key */ } if (s->state == BLOCK_SLOT_TOMBSTONE) { if (first_open == block_slots_cap) first_open = idx; continue; } /* OCCUPIED */ if (s->quota_slot == quota_slot && s->lbn == lbn) { *out_found = 1; return idx; } } *out_found = 0; return first_open; /* block_slots_cap if truly full */ } /* * block_dispatch_pull - Reservoir pull for block-touch admission, same * Q48_ONE / 3 floor stadium_word_dispatch()'s word_dispatch_pull() enforces * and for the identical reason (FABRIC-0.md §25.5/§25.7): block-touch * admission alone must never be able to drain a VM's reservoir below the * floor other reservoir consumers (word execution, application-level * pulls) depend on. Own copy, not shared code across files -- same * decision stadium_words.c's own version already made. */ static uint64_t block_dispatch_pull(VMUuid vm_id, uint64_t want) { uint64_t available = stadium_reservoir_peek(vm_id); uint64_t floor = Q48_ONE / 3; uint64_t pullable = (available > floor) ? (available - floor) : 0; uint64_t capped = (want < pullable) ? want : pullable; return stadium_reservoir_pull(vm_id, capped); } void stadium_block_dispatch(VMUuid vm_id, uint32_t lbn, uint64_t heartbeat_ticks) { int slot; int found; size_t idx; int stale = 0; if (!blocks_initialized) return; slot = stadium_quota_slot_for_vm(vm_id); if (slot < 0) return; idx = table_probe((int16_t)slot, lbn, &found); if (found) { StadiumBlockSlot *bs = &block_slots[idx]; size_t cell = bs->cell_index; if (cell == STADIUM_CELL_NONE || cell >= stadium_cell_count() || !cell_is_resident(cell)) { stale = 1; } else if (stadium_cells()[cell].header.identity != (uint64_t)lbn) { stale = 1; } if (!stale) { StadiumPatronHeader *h = &stadium_cells()[cell].header; uint64_t elapsed = heartbeat_ticks - bs->last_decay_tick; if (elapsed > 0) { uint64_t per_tick = (h->heat * (uint64_t)STADIUM_BLOCK_COOL_RATE_Q48) >> 16; uint64_t cooled = per_tick * elapsed; if (cooled > h->heat) cooled = h->heat; if (cooled > 0) { h->heat -= cooled; stadium_reservoir_push(vm_id, cooled); } bs->last_decay_tick = heartbeat_ticks; } h->heat += block_dispatch_pull(vm_id, (uint64_t)STADIUM_BLOCK_HEAT_QUANTUM); return; } /* Stale: reclaim this exact slot -- idx is still this key's home. */ bs->state = BLOCK_SLOT_TOMBSTONE; stat_evictions[slot]++; } if (idx >= block_slots_cap) return; /* table full: skip tracking this touch */ { uint64_t pulled = block_dispatch_pull(vm_id, (uint64_t)STADIUM_BLOCK_HEAT_QUANTUM); StadiumPatronHeader candidate; uint8_t *raw = (uint8_t *)&candidate; size_t i; size_t cell_idx; for (i = 0; i < sizeof(candidate); i++) raw[i] = 0; candidate.identity = (uint64_t)lbn; candidate.heat = pulled; candidate.ttl = 0; candidate.link = 0; /* unused for block patrons, same as words */ candidate.contains = STADIUM_CONTAINS_NONE; candidate.mass = 1; candidate.flags = 0; /* unpinned */ candidate.behaviour = (uint8_t)STADIUM_BEHAVIOUR_MIGRATE; cell_idx = stadium_admit(vm_id, &candidate); if (cell_idx == STADIUM_CELL_NONE) { stadium_reservoir_push(vm_id, pulled); /* rollback: preserve conservation */ return; } block_slots[idx].state = BLOCK_SLOT_OCCUPIED; block_slots[idx].quota_slot = (int16_t)slot; block_slots[idx].lbn = lbn; block_slots[idx].cell_index = cell_idx; block_slots[idx].last_decay_tick = heartbeat_ticks; stat_promotions[slot]++; } } /* Freestanding: no libc printf. Same small utility stadium_words.c * duplicates locally (and stadium.c before it) -- 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]); } void stadium_blocks_print_boot_diagnostics(VMUuid vm_id) { int slot = stadium_quota_slot_for_vm(vm_id); uint64_t promotions = 0, evictions = 0; uint64_t resident_sum = 0; uint64_t reservoir; size_t i; if (!blocks_initialized || slot < 0) { console_println("Stadium blocks: not initialized or vm_id holds no quota"); return; } promotions = stat_promotions[slot]; evictions = stat_evictions[slot]; /* Block-only resident sum: walk this VM's own table entries, same * self-healing residency check stadium_block_dispatch() uses -- a * stale entry contributes 0, not garbage. Not exposed as a separate * API (unlike stadium_words_resident_heat()) since nothing else needs * it yet; add one if a caller shows up. */ for (i = 0; i < block_slots_cap; i++) { StadiumBlockSlot *bs = &block_slots[i]; size_t cell; if (bs->state != BLOCK_SLOT_OCCUPIED || bs->quota_slot != (int16_t)slot) continue; cell = bs->cell_index; if (cell == STADIUM_CELL_NONE || cell >= stadium_cell_count() || !cell_is_resident(cell)) continue; if (stadium_cells()[cell].header.identity != (uint64_t)bs->lbn) continue; resident_sum += stadium_cells()[cell].header.heat; } reservoir = stadium_reservoir_peek(vm_id); console_puts("Stadium blocks: promotions="); console_put_u64(promotions); console_puts(" evictions="); console_put_u64(evictions); console_println(""); console_puts("Stadium blocks resident_sum="); console_put_u64(resident_sum); console_puts(" reservoir="); console_put_u64(reservoir); console_println(" (block-only term, not a standalone Q48_ONE check -- reservoir is shared)"); } #endif /* __STARKERNEL__ */