Fix aarch64 Stadium/COOL O(ncells) scan; rerun std79 DoE clean, 81/81 (FABRIC-3.md §XVI)
Build / build-amd64-iso (push) Waiting to run
Build / build-aarch64-iso (push) Waiting to run
Build / build-riscv64-img (push) Waiting to run

Root cause of the 90+ minute aarch64 VM-birth stall found in §XV: stadium_admit()'s
eviction-fallback scan iterated the entire stadium_ncells array filtered by owner,
not the calling VM's own resident cells as its own doc comment claimed. Combined
with stadium_grant_quota() always splitting from Hera's shrinking free list and
stadium_word_dispatch() calling stadium_admit() per distinct word a VM's capsule
executes, this compounded into a real O(n) blowup — catastrophic specifically on
aarch64 because its -m 4096 (vs 1024 on amd64/riscv64) inflates the kmalloc heap
kmalloc_init() bisects down to, which inflates stadium_ncells 4x (335,544 vs 83,886
cells, measured from boot logs).

Fixed by threading a real per-VM doubly-linked resident-cell list
(StadiumVMQuota.resident_head + stadium_resident_next[]/stadium_resident_prev[])
so the fallback scan is bounded by that VM's own resident count, not the global
cell array size.

Verified with a full rerun of the 3x9x3 std79 DoE campaign from scratch: one
continuous boot per architecture, all 9 identities simultaneously live throughout
(the 3-boot aarch64/riscv64 batching workaround is no longer needed). 81/81 trials
correct, 0 mismatches, DOE-RUN header sequence md5-identical across all three raw
logs. Identity 04's attach on aarch64, which stalled 90+ minutes before, now
completes in ~34s; full boot-to-DoE-complete in ~290s.

Corrects an earlier misreading (carried into §XV, std79-doe.fth's comments, and
the project memory note) that described the symptom as a runaway "335,000+ cycles"
dispatch counter — those were cell array indices, not an event count.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXieurDfDSsDFdnSyusuWo
This commit is contained in:
Robert Allan James
2026-09-11 16:37:44 -04:00
co-authored by Claude Sonnet 5
parent e2abc56306
commit 9eff122090
23 changed files with 94109 additions and 63 deletions
+95 -20
View File
@@ -2500,19 +2500,20 @@ baseline: **zero mismatches**. `T14`-equivalent (`-123 456 M* SWAP D.`) correctl
in every trial -- confirms §XII.4 bug 1's fix (`M*`, commit `9a09949`) holds under this DoE's own
`VM-EXEC`/native dispatch, not just the original exerciser's raw-text-feed path.
**aarch64: a second real bug found, worked around, not root-caused.** Attaching all 9 identities
simultaneously (matching amd64's approach) hit a severe wall partway through: identities
`bob`/`rajames`, `00`-`03` attached normally (each ~1-2 minutes), but the 6th simultaneously-live
VM (`04`) never completed -- QEMU stayed at 100%+ CPU, genuinely computing (confirmed via
`ps` CPU-time climbing steadily, not a deadlock), but produced next to no new console output for
over 90 minutes before the attempt was abandoned. The `Stadium: dispatch cell=NNNNN
behaviour=COOL` background patron-churn counter (visible in the log immediately before the
stall) had already reached **335,000+** cycles at that point -- tens of thousands more than the
same checkpoint shows for earlier, successful identities -- suggesting a genuine algorithmic
blowup (likely superlinear in live-VM count) in the Stadium COOL dispatch path specifically on
aarch64, not just "TCG is slower." amd64 attached all 8 non-`zuse` identities with no such cliff
at any point. **Not root-caused here** -- flagged as a real defect (see the discussion below);
this session's priority was completing the requested DoE, not chasing a new performance bug.
**aarch64: a second real bug found, worked around, not root-caused at the time (see §XVI for the
full root-cause and fix, added 2026-09-11/12 -- the paragraph below is kept as an accurate record
of what was actually observed live, but its causal framing was wrong and is corrected in §XVI).**
Attaching all 9 identities simultaneously (matching amd64's approach) hit a severe wall partway
through: identities `bob`/`rajames`, `00`-`03` attached normally (each ~1-2 minutes), but the 6th
simultaneously-live VM (`04`) never completed -- QEMU stayed at 100%+ CPU, genuinely computing
(confirmed via `ps` CPU-time climbing steadily, not a deadlock), but produced next to no new
console output for over 90 minutes before the attempt was abandoned. The `Stadium: dispatch
cell=NNNNN behaviour=COOL` lines visible in the log immediately before the stall showed cell
indices in the 335,000+ range -- at the time this was misread as a runaway *event counter*
("335,000+ cycles"); it is not one. Only a dozen or so such lines exist in the entire log --
`cell=NNNNN` is a cell *array index*, and 335,xxx sits almost exactly where Hera's own free list
converges after repeated halvings against a ~335,544-cell Stadium (see §XVI for the actual
mechanism). amd64 attached all 8 non-`zuse` identities with no such cliff at any point.
**Design revised to work around it:** each architecture other than amd64 runs as **3 boots of 3
simultaneously-live identities each** (`zuse`+`rajames`+`00`, `01`+`02`+`03`, `04`+`05`+`06`),
@@ -2534,11 +2535,85 @@ identities appearing exactly 3 times. Zero mismatches against baseline on either
identity, every replicate, every architecture, byte-identical to the single established
baseline. Raw logs and this verification are in `experiments/std79-doe/results-20260911/`.
**Follow-on item, not investigated further here:** the aarch64 Stadium/COOL dispatch stall.
Worth a dedicated investigation later -- candidates to check first: whether `Stadium:
dispatch cell=NNNNN` count genuinely scales worse than linearly with live VM count (the visible
symptom), and whether it's aarch64-specific in a real sense or just the first architecture where
TCG overhead pushed an existing superlinear cost over a wall-clock threshold nobody had hit
before (amd64's own per-tick cost may have the same underlying shape, just cheap enough per-tick
under native-ish TCG codegen that it never became visible in any campaign run so far).
**Follow-on item:** the aarch64 Stadium/COOL dispatch stall was investigated, root-caused, and
fixed -- see §XVI, and `experiments/std79-doe/results-20260911-stadium-fix/` for a clean rerun
of this entire campaign, one boot per architecture, all 9 identities simultaneously live
throughout, with the 3-boot batching workaround above no longer needed at all.
## XVI. aarch64 Stadium/COOL scaling stall — root-caused and fixed (2026-09-11/12)
**Investigation.** §XV's aarch64 stall was re-examined by hand-tracing the KRELTSC timestamps
around identity `04`'s birth: its *console* birth completed normally, but the subsequent gap
before its *user/runcap* birth was ~221.6 billion KRELTSC ticks -- roughly 26-27x identity `03`'s
entire birth cycle -- with **zero** log output in between, ruling out ordinary TCG slowness (a
genuinely slow-but-progressing computation would still print something eventually) in favor of a
real algorithmic cost blowup somewhere in the VM-birth path.
**Measured, not guessed:** both architectures' boot logs print their actual Stadium sizing --
`Stadium: 335544 cells (20971 KB), 202 VM slots` on aarch64 (`-m 4096`) vs. `Stadium: 83886
cells (5242 KB), 50 VM slots` on amd64 (`-m 1024`, same session) -- **exactly a 4.00x ratio**,
tracking the RAM ratio precisely. `kernel_main.c` always *requests* a 2 GiB kmalloc heap
(`KARGS_DEFAULT_HEAP_SIZE`); `kmalloc_init()` bisects that request down until the PMM can serve
it as one contiguous region, so aarch64's 4x RAM lets it land far closer to the full 2 GiB
request than amd64/riscv64's 1 GiB total RAM can. Both `stadium_ncells` and
`stadium_max_vm_count_val` are fixed percentages of that heap (`STADIUM_MEMORY_PERCENT`=1%,
`STADIUM_VM_MEMORY_PERCENT`=50% of the remainder), so this RAM difference propagates directly
into Stadium's internal array sizes.
**Root cause (`src/starkernel/vm/stadium.c`):** `stadium_grant_quota()` (called once per VM
birth, from `capsule_birth.c`) always splits *from Hera's own free list*, handing the new VM
half and keeping the remainder -- so Hera's free-list head position converges toward `ncells`
after repeated halvings (for `ncells`=335,544, roughly there after ~11 successive grants, which
is almost exactly where §XV's observed `cell=335,380..335,513` eviction indices sit -- not a
coincidence, a fingerprint of quota depletion). Once a newly-granted VM's *own* small quota runs
dry (a handful of distinct dictionary words into its own capsule's execution -- `stadium_admit()`
is called from `stadium_word_dispatch()`, `stadium_words.c`, once per first-execution of each
distinct word, not just once at VM birth), `stadium_admit()` fell back to a scan that its own doc
comment already claimed was "scoped to that SAME VM's own resident patrons only" -- but the
actual implementation was `for (i = 0; i < stadium_ncells; i++) { if (owner[i] != slot) continue;
... }`, an **O(stadium_ncells) full-array scan** (with a `stadium_density()` call per candidate),
not O(that VM's own resident count) as documented. At 335,544 cells on aarch64 (vs. 83,886 on
amd64), and hit repeatedly as more words execute against an ever-thinner per-VM quota, this
compounds into the observed 90+ minute stall -- the doc comment's stated intent and the actual
code had silently diverged.
**Fix:** threaded a real doubly-linked per-quota resident-cell list (`resident_head` on
`StadiumVMQuota`, plus global `stadium_resident_next[]`/`stadium_resident_prev[]` parallel
arrays, sized and zero-initialized in `stadium_boot_init()` alongside the existing bitmap/owner
arrays) so `stadium_admit()`'s eviction-fallback scan walks only the calling VM's own resident
cells -- bounded by that VM's granted quota (a small, RAM-independent number in practice), not
`stadium_ncells`. `resident_list_push()`/`resident_list_remove()` (`stadium.c`) maintain the list
at every point a cell becomes resident (both `stadium_admit()` success paths) or stops being
resident (`stadium_evict()`, called before the bitmap is cleared so the owning slot is still
known). No other call site touches cell residency, so this is a closed, self-contained fix.
**Verification: a full, clean, single-boot rerun of the entire §XV campaign on all three
architectures**, all 9 identities simultaneously live for the whole run in every case (the
3-boot batching workaround is no longer needed anywhere) --
`experiments/std79-doe/results-20260911-stadium-fix/`:
- amd64: boot + all 9 attaches + 27-trial run, one continuous QEMU session, **~165s total**.
- aarch64: same, **~290s total** -- identity `04`'s attach specifically, which stalled 90+
minutes before the fix, now completes in **~34s**.
- riscv64: same, **~161s total** (riscv64 was never affected, included for full re-verification
since the fix touches shared Stadium code all three architectures link against).
All three logs' `DOE-RUN,run_id,id_idx,id_label,rep` header sequences are **md5-identical**
(confirming the master Fisher-Yates shuffle is genuinely architecture-independent under the same
seed), and the canonical 28-number per-trial result string appears **exactly 27 times in each
log, byte-identical** -- **81/81 trials correct across all three architectures, 0 mismatches,
0 workarounds needed.** This is the first genuinely defect-free run of this campaign.
**Correction, superseding §XV's causal framing:** the "335,000+ cycles" language in §XV was a
misreading (cell array indices, not an event counter -- only ~12 total `Stadium: dispatch` lines
exist in the whole stalled log) carried into that section's live write-up and into
`experiments/std79-doe/std79-doe.fth`'s own comment block and the
`project_std79_doe_complete.md` memory note; all three have been corrected to point here.
**Latent secondary issue, recorded not fixed (out of scope for this pass):** the free-list
halving in `stadium_grant_quota()` has no floor -- after enough successive VM births Hera's own
free list drops below 2 cells and every subsequent grant is silently refused (`half == 0` -> -1),
which `capsule_birth.c` currently treats as non-fatal. Same root mechanism as the fix above, a
different (silent-failure-to-birth, not stall) symptom, not yet a problem at any VM count actually
exercised so far.
+1 -1
View File
@@ -1,5 +1,5 @@
# Capsule Block Manifest — Auto-generated
<!-- Generated by mkcapsule --manifest 2026-09-11T17:55:17Z -->
<!-- Generated by mkcapsule --manifest 2026-09-11T20:32:11Z -->
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
<!-- Hand-written justifications and immutability notes live -->
<!-- in MANIFEST.md alongside this auto-generated index. -->
BIN
View File
Binary file not shown.
+20 -10
View File
@@ -4,22 +4,32 @@ The formal successor to `experiments/std79-exerciser/`'s ad hoc campaign (see FA
requested as a genuine randomized full-factorial design matching this project's own DoE
methodology (`capsules/doe.4th`'s Fisher-Yates run-matrix shuffle) rather than convenience
batching — and written entirely in FORTH, not host-orchestrated shell scripting. See
FABRIC-3.md §XV for the full design writeup, including a real aarch64 bug found and worked
around along the way (not root-caused — flagged there for later).
FABRIC-3.md §XV for the design writeup and §XVI for the real aarch64 Stadium/COOL scaling bug
that was found, root-caused, and fixed (`src/starkernel/vm/stadium.c`, commit pending) — the
3-boot batching workaround below is now historical only; a single boot with all 9 identities
simultaneously live works cleanly on all three architectures as of the fix.
`std79-doe.fth` is not a capsule loaded via `EXEC` — feed it as raw text to a running REPL
(e.g. `socat - UNIX-CONNECT:<serial_sock> < std79-doe.fth`), same as the original exerciser, then
invoke `EXEC-STD79-DOE ( seed lo hi -- )` once loaded. `lo`/`hi` select which identity-index
range (0-8) this boot's live VMs cover — pass `0 8` if all 9 identities are simultaneously
attached in one boot (works on amd64); pass a narrower range (e.g. `0 2`, `3 5`, `6 8`) to run
the DoE across multiple smaller boots when attaching all 9 at once isn't practical (see §XV —
this is how aarch64 and riscv64 were actually run). Every boot must use the *same* seed so
range (0-8) this boot's live VMs cover — pass `0 8` for a single boot with all 9 identities
simultaneously attached (now confirmed working on all three architectures, see §XVI); a narrower
range (e.g. `0 2`, `3 5`, `6 8`) still works too, for running the DoE across multiple smaller
boots if ever needed for an unrelated reason. Every boot must use the *same* seed so
`INIT-MATRIX`/`SHUFFLE-MATRIX` reproduce the identical 27-slot master permutation; only the
`lo`/`hi` filter differs, so `run_id` (always the slot's true position in the master shuffle,
0-26) stays directly comparable across boots.
`results-20260911/` holds the raw captured serial output for the full campaign `amd64-doe-
raw.log` (all 27 trials, one boot, all 9 identities simultaneously live) and
`results-20260911/` holds the raw captured serial output from the *original* campaign (pre-fix):
`amd64-doe-raw.log` (all 27 trials, one boot, all 9 identities simultaneously live) and
`{aarch64,riscv64}-doe-batch{1,2,3}-raw.log` (27 trials each, split across 3 boots of 3
identities per the workaround above). **Result: 81/81 trials correct, 0 mismatches** — every
identity, every replicate, every architecture, byte-identical to a single established baseline.
identities, the then-necessary workaround). **Result: 81/81 trials correct, 0 mismatches.**
`results-20260911-stadium-fix/` holds the *post-fix* rerun — one boot per architecture, all 9
identities simultaneously live in every boot, same seed throughout. **Result: 81/81 trials
correct, 0 mismatches, byte-identical output to the original campaign** — and the DOE-RUN
header sequence (run_id/id_idx/id_label/rep assignment) is md5-identical across all three raw
logs, confirming the master shuffle is genuinely architecture-independent. Total per-architecture
wall-clock (boot + all 9 identity attaches + full 27-trial run, one continuous QEMU session):
amd64 ~165s, aarch64 ~290s, riscv64 ~161s — aarch64's identity 04 attach alone, which stalled
90+ minutes before the fix, now completes in ~34s.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+18 -15
View File
@@ -123,21 +123,24 @@ VARIABLE SW-I VARIABLE SW-J VARIABLE SW-VI VARIABLE SW-VJ
VARIABLE CURR-ID
VARIABLE CURR-REP
( Boot-batching support, added 2026-09-11 (FABRIC-3.md SXV): aarch64 hit a )
( severe, apparently superlinear per-tick slowdown once ~9-10 VMs stayed )
( simultaneously live at once (Stadium COOL dispatch cell counters running )
( into the hundreds of thousands with zero forward progress for 90+ )
( minutes) -- not yet root-caused, flagged as a real defect worth chasing )
( separately. Worked around by running each architecture as 3 boots of 3 )
( identities each (zuse+rajames+00, 01+02+03, 04+05+06), every boot sharing )
( the SAME seed so INIT-MATRIX/SHUFFLE-MATRIX produce the identical master )
( 27-slot permutation every time -- only ACTIVE-LO/ACTIVE-HI differ, so )
( each boot walks the FULL master order and simply skips any slot whose )
( identity isn't in its own live subset. The recorded run_id is always I )
( itself (the slot's true position in the master shuffle), never a )
( separately-incremented counter, so row order stays meaningful and )
( comparable across all 3 boots -- classic DoE "blocking": randomized )
( within, blocked across, by a practical constraint. )
( Boot-batching support, added 2026-09-11 (FABRIC-3.md SXV). Originally a )
( workaround for a real aarch64 Stadium/COOL scaling bug -- root-caused and )
( fixed 2026-09-11/12 (FABRIC-3.md SXVI, src/starkernel/vm/stadium.c): )
( stadium_grant_quota() halves the granting VM's own free list on every )
( birth with no floor, and stadium_admit()'s eviction fallback used to scan )
( the ENTIRE global cell array (stadium_ncells, in the hundreds of )
( thousands on aarch64's larger RAM-scaled Stadium) filtered by owner, )
( instead of walking just the calling VM's own resident cells as its own )
( doc comment already claimed -- fixed by threading a real per-VM resident )
( list. A single boot with all 9 identities simultaneously live now works )
( cleanly on all three architectures (see results-20260911-stadium-fix/), )
( so ACTIVE-LO/ACTIVE-HI is no longer load-bearing -- kept only as general )
( flexibility: every boot shares the SAME seed so INIT-MATRIX/SHUFFLE-MATRIX )
( produce the identical master 27-slot permutation regardless of how many )
( boots this runs across; only the ACTIVE-LO/ACTIVE-HI filter differs if )
( split. The recorded run_id is always I itself (the slot's true position )
( in the master shuffle), never a separately-incremented counter, so row )
( order stays meaningful and comparable across any number of boots. )
VARIABLE ACTIVE-LO
VARIABLE ACTIVE-HI
: ACTIVE? ( idx -- flag )
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,2 @@
UEFI firmware (version 2025.11-3ubuntu7 built at 11:11:58 on Mar 31 2026)
[=3h
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+100 -17
View File
@@ -80,6 +80,17 @@ typedef struct {
VMUuid vm_id;
int in_use;
size_t free_head;
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 ==
@@ -92,6 +103,45 @@ typedef struct {
* (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;
@@ -137,11 +187,15 @@ int stadium_boot_init(void) {
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));
if (!cells || !bitmap || !owner) {
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 (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;
}
@@ -168,6 +222,8 @@ int stadium_boot_init(void) {
kfree(cells);
kfree(bitmap);
kfree(owner);
kfree(resident_next);
kfree(resident_prev);
return -1;
}
@@ -193,15 +249,20 @@ int stadium_boot_init(void) {
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].reservoir = 0;
quotas[i].vm_id = vm_uuid_none();
quotas[i].in_use = 0;
quotas[i].free_head = STADIUM_CELL_NONE;
quotas[i].resident_head = STADIUM_CELL_NONE;
quotas[i].reservoir = 0;
}
}
quotas[0].vm_id = vm_uuid_hera();
@@ -215,6 +276,8 @@ int stadium_boot_init(void) {
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;
@@ -378,8 +441,9 @@ int stadium_evict(size_t cell_index) {
if (header->contains != STADIUM_CONTAINS_NONE) return -1;
stadium_dispatch(cell_index, (StadiumBehaviour)header->behaviour);
bitmap_clear(cell_index);
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
@@ -417,7 +481,6 @@ int stadium_evict(size_t cell_index) {
*/
size_t stadium_admit(VMUuid vm_id, const StadiumPatronHeader *candidate) {
int slot;
size_t i;
size_t idx;
size_t least_dense_index = STADIUM_CELL_NONE;
uint64_t least_dense_value = 0;
@@ -450,22 +513,41 @@ size_t stadium_admit(VMUuid vm_id, const StadiumPatronHeader *candidate) {
* 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;
}
for (i = 0; i < stadium_ncells; i++) {
StadiumPatronHeader *h;
/* 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 (!bitmap_get(i)) 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;
{
uint64_t d = stadium_density(i);
uint64_t d = stadium_density(idx);
if (least_dense_index == STADIUM_CELL_NONE || d < least_dense_value) {
least_dense_index = i;
least_dense_index = idx;
least_dense_value = d;
}
}
@@ -492,6 +574,7 @@ size_t stadium_admit(VMUuid vm_id, const StadiumPatronHeader *candidate) {
* 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;
}