- New spinlock primitive (spinlock.h/.c): CAS on an owner word (core+1), interrupts masked while any lock is held (state kept in the per-core block, so release order doesn't matter). Recursion, release-not-held, held-at-switch and destroyed-while-held are counted and logged, never silent. Locks are no-ops until the boot core's block is installed (riscv64's monolithic early boot prints before sk_percpu_boot_init()). - Kernel locks in a documented order: hermes, switch, registry, doe, log, console, heap, vmm, pmm. sf_mutex (dict_lock/tuning_lock) uses the same lock word. - Hermes: public functions over *_locked internals; never held across the drain's vm_interpret_kind(); switch notifications after dropping the lock; event payload moved off static storage. Switch table and registry never call out while held (slot/entry copied under the lock). - No lock crosses a switch: checked at context switch (leave, resume) and the idle point. - Probe (removed): every lock balances on all three ISAs; 0 held at switch over 7.5M-51M checks; 0 recursions, 0 bad releases. - Standard sequence on amd64/aarch64/riscv64: hashes and G byte-identical to MC-0a, MINT/WIREBIND/USE ok, conservation true, 0 idle growth. - Block subsystem split out as MC-0b2 (Captain Bob's ruling). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
229 lines
9.7 KiB
C
229 lines
9.7 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.
|
||
*/
|
||
|
||
/**
|
||
* doe_region.c - Growable per-trial DoE-campaign-summary ring on Artemis's
|
||
* disk. See starkernel/doe_region.h for the format/interface design.
|
||
*
|
||
* Mirrors log_region.c's own read/write/grow/evict shape exactly (same
|
||
* blk_meta_zone_read()/write() accessor, same control-header CRC
|
||
* discipline), minus the priority-eviction logic that doesn't apply here
|
||
* (no level concept for a trial summary -- plain FIFO eviction only).
|
||
*/
|
||
|
||
#include "starkernel/doe_region.h"
|
||
|
||
#include <stddef.h>
|
||
#include <string.h>
|
||
|
||
#include "block_subsystem.h" /* blk_meta_zone_read/write, compute_crc64 */
|
||
#include "starkernel/spinlock.h" /* FABRIC-3.10.8 MC-0b -- g_lock_doe */
|
||
|
||
static uint64_t doe_region_ctrl_compute_crc(const doe_region_ctrl_t *ctrl) {
|
||
size_t crc_span = offsetof(doe_region_ctrl_t, hdr_crc);
|
||
return compute_crc64((const uint8_t *)ctrl, crc_span);
|
||
}
|
||
|
||
/* §XXXV.12: a devblock holds DOE_SLOTS_PER_DEVBLOCK slots via one
|
||
* read-modify-write, so a crash mid-write can corrupt siblings of the
|
||
* slot actually being written. This is how a reader (doe_region_
|
||
* count_for_isa(), and any future host-side extractor) tells a genuine
|
||
* record from that corruption -- same offsetof-span discipline as the
|
||
* control header's own CRC above. */
|
||
static uint64_t doe_region_slot_compute_crc(const doe_trial_slot_t *slot) {
|
||
size_t crc_span = offsetof(doe_trial_slot_t, slot_crc);
|
||
return compute_crc64((const uint8_t *)slot, crc_span);
|
||
}
|
||
|
||
/* Reads the control header; returns 0 and *out populated only if magic,
|
||
* version, and CRC all check out. Any other result (blank, foreign,
|
||
* corrupt, or a read failure) is treated identically by the caller: "not
|
||
* yet initialized," matching log_region_ctrl_read()'s own discipline. */
|
||
static int doe_region_ctrl_read(doe_region_ctrl_t *out) {
|
||
if (blk_meta_zone_read(DOE_REGION_DEVBLOCK_FROM_TOP_BASE, (uint8_t *)out) != 0) return -1;
|
||
if (DOE_REGION_GET_MAGIC(out->magic) != (uint32_t)(DOE_REGION_MAGIC & 0xFFFFFFFFull)) return -1;
|
||
if (DOE_REGION_GET_VERSION(out->magic) != DOE_REGION_VERSION_0) return -1;
|
||
uint64_t want_crc = doe_region_ctrl_compute_crc(out);
|
||
if (want_crc != out->hdr_crc) return -1;
|
||
return 0;
|
||
}
|
||
|
||
static int doe_region_ctrl_write(doe_region_ctrl_t *ctrl) {
|
||
ctrl->hdr_crc = doe_region_ctrl_compute_crc(ctrl);
|
||
return blk_meta_zone_write(DOE_REGION_DEVBLOCK_FROM_TOP_BASE, (const uint8_t *)ctrl) == 0 ? 0 : -1;
|
||
}
|
||
|
||
static void doe_region_ctrl_init_fresh(doe_region_ctrl_t *ctrl) {
|
||
memset(ctrl, 0, sizeof(*ctrl));
|
||
ctrl->magic = DOE_REGION_PACK(DOE_REGION_VERSION_0);
|
||
ctrl->devblocks = DOE_REGION_INITIAL_DEVBLOCKS;
|
||
ctrl->head_slot = 0;
|
||
ctrl->tail_slot = 0;
|
||
ctrl->record_count = 0;
|
||
}
|
||
|
||
/* Reads/writes the devblock holding slot_index (DOE_SLOTS_PER_DEVBLOCK
|
||
* slots per devblock), one devblock_from_top past the control header for
|
||
* every DOE_SLOTS_PER_DEVBLOCK slots. */
|
||
static uint32_t doe_region_slot_devblock_from_top(uint32_t slot_index) {
|
||
return DOE_REGION_DEVBLOCK_FROM_TOP_BASE + 1u + (slot_index / DOE_SLOTS_PER_DEVBLOCK);
|
||
}
|
||
|
||
static int doe_region_write_slot(uint32_t slot_index, const doe_trial_slot_t *slot) {
|
||
uint8_t devblock_buf[4096];
|
||
uint32_t dft = doe_region_slot_devblock_from_top(slot_index);
|
||
uint32_t slot_in_devblock = slot_index % DOE_SLOTS_PER_DEVBLOCK;
|
||
|
||
/* Read-modify-write: blk_meta_zone_*() only operates at whole-devblock
|
||
* granularity, and a devblock holds DOE_SLOTS_PER_DEVBLOCK slots, so a
|
||
* single-slot write must preserve its siblings. A read failure on a
|
||
* never-yet-written devblock (blank fence content) is not fatal here --
|
||
* proceed with a zeroed buffer, matching log_region_write_slot()'s own
|
||
* discipline. */
|
||
if (blk_meta_zone_read(dft, devblock_buf) != 0) {
|
||
memset(devblock_buf, 0, sizeof(devblock_buf));
|
||
}
|
||
memcpy(devblock_buf + (size_t)slot_in_devblock * DOE_SLOT_SIZE, slot, sizeof(*slot));
|
||
return blk_meta_zone_write(dft, devblock_buf) == 0 ? 0 : -1;
|
||
}
|
||
|
||
/* Returns 0 and *out populated only if the slot's own CRC checks out --
|
||
* a corrupted sibling slot (§XXXV.12's own devblock-sharing risk) reads
|
||
* back as a failure here, not as a plausible-looking garbage record. */
|
||
static int doe_region_read_slot(uint32_t slot_index, doe_trial_slot_t *out) {
|
||
uint8_t devblock_buf[4096];
|
||
uint32_t dft = doe_region_slot_devblock_from_top(slot_index);
|
||
uint32_t slot_in_devblock = slot_index % DOE_SLOTS_PER_DEVBLOCK;
|
||
|
||
if (blk_meta_zone_read(dft, devblock_buf) != 0) return -1;
|
||
memcpy(out, devblock_buf + (size_t)slot_in_devblock * DOE_SLOT_SIZE, sizeof(*out));
|
||
if (doe_region_slot_compute_crc(out) != out->slot_crc) return -1;
|
||
return 0;
|
||
}
|
||
|
||
/* FABRIC-3.10.8 MC-0b: append and count run under g_lock_doe (the public
|
||
* functions below); these are their bodies. */
|
||
static int doe_region_append_locked(uint32_t run_id, uint32_t cfg, uint32_t rep, uint32_t nw,
|
||
uint32_t mode, uint32_t fail_count,
|
||
uint64_t fleet_k_q48, uint32_t fleet_conserved,
|
||
uint64_t switch_count_cumulative,
|
||
const char *isa, uint32_t isa_len) {
|
||
doe_region_ctrl_t ctrl;
|
||
if (doe_region_ctrl_read(&ctrl) != 0) {
|
||
doe_region_ctrl_init_fresh(&ctrl);
|
||
}
|
||
|
||
uint32_t total_slots = ctrl.devblocks * DOE_SLOTS_PER_DEVBLOCK;
|
||
if (ctrl.record_count >= total_slots) {
|
||
if (ctrl.devblocks < DOE_REGION_MAX_DEVBLOCKS) {
|
||
/* Grow: new slots appear at indices [old_total_slots ..
|
||
* new_total_slots-1], nothing already written moves -- safe
|
||
* at any point in the ring's lifecycle (see log_region.c's
|
||
* own identical reasoning). */
|
||
ctrl.devblocks += DOE_REGION_GROWTH_INCREMENT;
|
||
if (ctrl.devblocks > DOE_REGION_MAX_DEVBLOCKS) ctrl.devblocks = DOE_REGION_MAX_DEVBLOCKS;
|
||
total_slots = ctrl.devblocks * DOE_SLOTS_PER_DEVBLOCK;
|
||
}
|
||
if (ctrl.record_count >= total_slots) {
|
||
/* Still full (growth ceiling already reached) -- plain FIFO
|
||
* eviction, no priority concept for a trial summary. */
|
||
ctrl.head_slot = (ctrl.head_slot + 1u) % total_slots;
|
||
ctrl.record_count--;
|
||
}
|
||
}
|
||
|
||
doe_trial_slot_t slot;
|
||
memset(&slot, 0, sizeof(slot));
|
||
slot.fleet_k_q48 = fleet_k_q48;
|
||
slot.switch_count_cumulative = switch_count_cumulative;
|
||
slot.run_id = run_id;
|
||
slot.cfg = cfg;
|
||
slot.rep = rep;
|
||
slot.nw = nw;
|
||
slot.mode = mode;
|
||
slot.fail_count = fail_count;
|
||
slot.fleet_conserved = fleet_conserved;
|
||
|
||
uint32_t isa_n = isa_len;
|
||
if (isa_n > DOE_SLOT_ISA_MAX) isa_n = DOE_SLOT_ISA_MAX;
|
||
if (isa && isa_n) memcpy(slot.isa, isa, isa_n);
|
||
|
||
slot.slot_crc = doe_region_slot_compute_crc(&slot);
|
||
|
||
if (doe_region_write_slot(ctrl.tail_slot, &slot) != 0) return -1;
|
||
|
||
ctrl.tail_slot = (ctrl.tail_slot + 1u) % total_slots;
|
||
if (ctrl.record_count < total_slots) ctrl.record_count++;
|
||
|
||
return doe_region_ctrl_write(&ctrl) == 0 ? 0 : -1;
|
||
}
|
||
|
||
int doe_region_append(uint32_t run_id, uint32_t cfg, uint32_t rep, uint32_t nw,
|
||
uint32_t mode, uint32_t fail_count,
|
||
uint64_t fleet_k_q48, uint32_t fleet_conserved,
|
||
uint64_t switch_count_cumulative,
|
||
const char *isa, uint32_t isa_len) {
|
||
int rc;
|
||
sk_spin_lock(&g_lock_doe);
|
||
rc = doe_region_append_locked(run_id, cfg, rep, nw, mode, fail_count, fleet_k_q48,
|
||
fleet_conserved, switch_count_cumulative, isa, isa_len);
|
||
sk_spin_unlock(&g_lock_doe);
|
||
return rc;
|
||
}
|
||
|
||
static uint32_t doe_region_count_for_isa_locked(const char *isa, uint32_t isa_len) {
|
||
doe_region_ctrl_t ctrl;
|
||
if (doe_region_ctrl_read(&ctrl) != 0) return 0;
|
||
|
||
/* Build the full DOE_SLOT_ISA_MAX-byte zero-padded tag exactly as
|
||
* doe_region_append() stores it, so this compares the whole field
|
||
* (including trailing zero padding) -- not just a prefix, which would
|
||
* wrongly match e.g. "amd" against a stored "amd64". */
|
||
char want[DOE_SLOT_ISA_MAX];
|
||
memset(want, 0, sizeof(want));
|
||
uint32_t isa_n = isa_len;
|
||
if (isa_n > DOE_SLOT_ISA_MAX) isa_n = DOE_SLOT_ISA_MAX;
|
||
if (isa && isa_n) memcpy(want, isa, isa_n);
|
||
|
||
uint32_t total_slots = ctrl.devblocks * DOE_SLOTS_PER_DEVBLOCK;
|
||
uint32_t count = 0;
|
||
uint32_t idx = ctrl.head_slot;
|
||
for (uint32_t i = 0; i < ctrl.record_count; i++) {
|
||
doe_trial_slot_t slot;
|
||
if (doe_region_read_slot(idx, &slot) == 0) {
|
||
if (memcmp(slot.isa, want, DOE_SLOT_ISA_MAX) == 0) {
|
||
count++;
|
||
}
|
||
}
|
||
idx = (idx + 1u) % total_slots;
|
||
}
|
||
return count;
|
||
}
|
||
|
||
uint32_t doe_region_count_for_isa(const char *isa, uint32_t isa_len) {
|
||
uint32_t n;
|
||
sk_spin_lock(&g_lock_doe);
|
||
n = doe_region_count_for_isa_locked(isa, isa_len);
|
||
sk_spin_unlock(&g_lock_doe);
|
||
return n;
|
||
}
|