Build DOE-PERSIST-TRIAL: block-backed DoE campaign persistence, live-verified
FABRIC-3.md §XXXV.3/.4 designed this and it was never built -- two live campaigns got launched relying entirely on a live serial-log tail before this was caught. doe_region.c/.h mirrors log_region.c's own growable-ring pattern (own devblock_from_top fence at 98, past log_region's ceiling at 97) to persist one trial-summary record per completed trial straight to disk/artemis.img, surviving past QEMU exit with no host required. Verified end to end: booted amd64, called DOE-PERSIST-TRIAL at the console, clean BYE, then read the raw disk bytes back with QEMU fully dead -- confirmed the exact values passed in. Wired into both multiuser-doe.4th and per-isa-doe.4th's trial markers. Clean build, zero new warnings, all 3 ISAs; mkcapsule --lint passes both edited capsules. FABRIC-3.md §XXXV.10 documents the fix and the root-cause correction: two campaigns were launched on an unbuilt persistence design before this was caught and fixed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BWpNjdwPtFLuVLaAq44L9K
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
8b8e8e1405
commit
2da746c01c
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
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 */
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
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) {
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -19,6 +19,9 @@
|
||||
#include "starkernel/log_attrib.h" /* vm_log_attributed_vm() */
|
||||
#include "starkernel/capsule_birth.h" /* capsule_vm_registry_get, VMRegistryEntry */
|
||||
#include "starkernel/console.h" /* console_println() -- (LOG-APPEND-RAW) diagnostics */
|
||||
#include "starkernel/doe_region.h" /* doe_region_append() -- DOE-PERSIST-TRIAL */
|
||||
#include "starkernel/capsule_vm_physics.h" /* vm_physics_fleet_heat_sum(), vm_physics_conserved() */
|
||||
#include "starkernel/capsule_vm_switch_signal.h" /* sk_vm_switch_signal_switch_count() */
|
||||
#include <string.h> /* strlen */
|
||||
#endif
|
||||
|
||||
@@ -296,6 +299,59 @@ static void log_word_append_raw(VM *vm)
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ── DOE-PERSIST-TRIAL ( run cfg rep nw mode fail isa-addr isa-u -- ) ── */
|
||||
/*
|
||||
* Kernel-only. Writes one trial-summary record to Artemis's on-disk DoE
|
||||
* ring (doe_region.c), FABRIC-3.md §XXXV.3/.4. Called from
|
||||
* MU-EMIT-TRIAL-MARKER (multiuser-doe.4th / per-isa-doe.4th) right
|
||||
* alongside the existing console print, so a campaign's data survives
|
||||
* past the QEMU serial log without needing a live host tail.
|
||||
*
|
||||
* fleet_k_q48/fleet_conserved/switch_count_cumulative are NOT caller-
|
||||
* supplied -- read directly here via vm_physics_fleet_heat_sum(),
|
||||
* vm_physics_conserved(), sk_vm_switch_signal_switch_count(), same
|
||||
* discipline (LOG-APPEND-RAW) uses for its own source attribution: a
|
||||
* trial-end snapshot no FORTH caller could spoof or get stale by passing
|
||||
* the wrong value.
|
||||
*/
|
||||
#ifdef __STARKERNEL__
|
||||
static void log_word_doe_persist_trial(VM *vm)
|
||||
{
|
||||
if (vm->dsp < 7) {
|
||||
console_println("DOE-PERSIST-TRIAL: stack underflow");
|
||||
vm->error = 1;
|
||||
return;
|
||||
}
|
||||
cell_t isa_u = vm_pop(vm);
|
||||
cell_t isa_addr = vm_pop(vm);
|
||||
cell_t fail = vm_pop(vm);
|
||||
cell_t mode = vm_pop(vm);
|
||||
cell_t nw = vm_pop(vm);
|
||||
cell_t rep = vm_pop(vm);
|
||||
cell_t cfg = vm_pop(vm);
|
||||
cell_t run = vm_pop(vm);
|
||||
|
||||
if (isa_u < 0 || isa_addr < 0 || (isa_addr + isa_u) > (cell_t)VM_MEMORY_SIZE) {
|
||||
console_println("DOE-PERSIST-TRIAL: isa string address out of range");
|
||||
vm->error = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const char *isa = (const char *)&vm->memory[isa_addr];
|
||||
|
||||
uint64_t fleet_k_q48 = vm_physics_fleet_heat_sum();
|
||||
unsigned fleet_conserved = vm_physics_conserved() ? 1u : 0u;
|
||||
uint64_t switch_count = sk_vm_switch_signal_switch_count();
|
||||
|
||||
if (doe_region_append((uint32_t)run, (uint32_t)cfg, (uint32_t)rep, (uint32_t)nw,
|
||||
(uint32_t)mode, (uint32_t)fail,
|
||||
fleet_k_q48, fleet_conserved, switch_count,
|
||||
isa, (uint32_t)isa_u) != 0) {
|
||||
vm->error = 1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ── Registration ────────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
@@ -308,6 +364,7 @@ static void log_word_append_raw(VM *vm)
|
||||
* String literals: LOG-ERROR" LOG-WARN" LOG-INFO" LOG-TEST" LOG-DEBUG"
|
||||
* Stack string: LOG-ERROR-STR LOG-WARN-STR LOG-INFO-STR
|
||||
* LOG-TEST-STR LOG-DEBUG-STR
|
||||
* Kernel-only: (LOG-APPEND-RAW) DOE-PERSIST-TRIAL
|
||||
*/
|
||||
void register_log_words(VM *vm)
|
||||
{
|
||||
@@ -344,5 +401,6 @@ void register_log_words(VM *vm)
|
||||
|
||||
#ifdef __STARKERNEL__
|
||||
register_word(vm, "(LOG-APPEND-RAW)", log_word_append_raw);
|
||||
register_word(vm, "DOE-PERSIST-TRIAL", log_word_doe_persist_trial);
|
||||
#endif
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user