/* 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 #include #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; }