code wiki / (root) / nx_evict_journal.nx

nx_evict_journal.nx source

↩ module page · 154 lines · 5713 B

1// nx_evict_journal.nx -- append-only eviction visibility log. 2// 3// Per cardinal: cooperative-resource-arbitration. Every eviction, 4// demote, throttle, and migration is logged so the user can audit 5// after the fact. This is the structural EXCEED-axis vs Docker's 6// silent kernel OOM-killer: there, a container disappears and you 7// dig through dmesg hoping to find the reason. Here, every event is 8// in ~/.nishi/evict.jsonl with the displacing cell named. 9// 10// Composes: 11// nx_budget -- denied-allocation events log here 12// nx_yield -- DEMOTED / THROTTLED outcomes log here 13// nx_homeostasis -- migration decisions log here 14// nx_attention_class -- the class of both displaced + displacing 15// cells is preserved for forensics 16// 17// V1 ships in-memory ring buffer. The on-disk JSONL append is queued 18// once nx_jsonl_writer lands; for now the journal is queryable via 19// nx_evict_dump pointer access. 20// 21// Gap list (V1 honest perf verdict): 22// - no disk persistence (in-memory only this phase) 23// - no time-windowed query (caller walks full buffer) 24// - ring-buffer wraps silently at capacity (older entries lost) 25// - no peer-mesh propagation for distributed evictions 26// 27// genealogy_id: nishi_cardinal_2026-05-17_cooperative_resource_arbitration 28// lineage_id: substrate_evict_journal_v1 29// 30// nx_safety_envelope: 31// intended_use: "Append-only eviction/demote/throttle log for 32// forensic audit; never silent eviction" 33// sil_target: SIL2 34// evidence: [append_only_invariant, every_event_logged, 35// displacing_cell_named] 36// verdict: NOT_YET_EVALUATED 37 38import "nx_syscalls.nx" 39import "nx_tier.nx" 40 41// ===== Sealed enum: NxEvictReason ================================= 42 43const NX_EVR_BUDGET_DENIED: nx_int = 0 44const NX_EVR_YIELD_DEMOTED: nx_int = 1 45const NX_EVR_YIELD_THROTTLED: nx_int = 2 46const NX_EVR_MIGRATED: nx_int = 3 47const NX_EVR_TERMINATED: nx_int = 4 // immune layer killed the cell 48const NX_EVR_N_REASONS: nx_int = 5 49 50func nx_evr_is_valid(r: nx_int) -> nx_int { 51 if r < 0 { return 0 } 52 if r >= NX_EVR_N_REASONS { return 0 } 53 return 1 54} 55 56// ===== Struct: NxEvictEntry ====================================== 57// 58// One entry per event. resource_kind aliases NX_RES_* from nx_budget; 59// displaced_by is 0 when the event has no displacing cell (e.g., 60// over-budget self-denial). 61 62struct NxEvictEntry { 63 ts_us: nx_size, 64 cell_id: nx_int, 65 reason: nx_int, 66 resource_kind: nx_int, 67 attention_class: nx_int, 68 displaced_by: nx_int, 69} 70 71// ===== Struct: NxEvictJournal ===================================== 72// 73// Fixed-capacity ring buffer. Head advances on each append; when head 74// reaches capacity it wraps to 0 and the journal is full. count tracks 75// total appends ever (not modular) so callers can detect overwrites. 76 77struct NxEvictJournal { 78 entries: *NxEvictEntry, 79 capacity: nx_size, 80 head: nx_size, 81 count: nx_size, 82} 83 84// Default capacity tuned for NX_TIER_WORKSTATION: 1024 entries = 85// roughly 48 KiB. On NX_TIER_MCU the caller should reduce capacity. 86const NX_EVICT_DEFAULT_CAPACITY: nx_size = 1024 87 88// ===== Constructor =============================================== 89 90func nx_evict_journal_new(capacity: nx_size) -> *NxEvictJournal { 91 let j: *NxEvictJournal = (sys_mmap(40)) as *NxEvictJournal 92 let entry_bytes: nx_size = capacity * 48 93 j.entries = (sys_mmap(entry_bytes)) as *NxEvictEntry 94 j.capacity = capacity 95 j.head = 0 96 j.count = 0 97 return j 98} 99 100// ===== nx_evict_log =============================================== 101// 102// Append one event. Caller supplies ts_us from its monotonic clock so 103// the journal does not pull a syscall on the hot eviction path. 104 105func nx_evict_log(j: *NxEvictJournal, 106 ts_us: nx_size, 107 cell_id: nx_int, 108 reason: nx_int, 109 resource_kind: nx_int, 110 attention_class: nx_int, 111 displaced_by: nx_int) -> nx_int { 112 if nx_evr_is_valid(reason) == 0 { return 1 } 113 let slot: *NxEvictEntry = (j.entries as i64 + (j.head as i64) * 48) as *NxEvictEntry 114 slot.ts_us = ts_us 115 slot.cell_id = cell_id 116 slot.reason = reason 117 slot.resource_kind = resource_kind 118 slot.attention_class = attention_class 119 slot.displaced_by = displaced_by 120 j.head = j.head + 1 121 if j.head >= j.capacity { j.head = 0 } 122 j.count = j.count + 1 123 return 0 124} 125 126// ===== nx_evict_at ================================================ 127// 128// Read entry at index. Returns NULL for out-of-range. Caller must not 129// retain the pointer across subsequent nx_evict_log calls; the ring 130// will overwrite that slot in time. 131 132func nx_evict_at(j: *NxEvictJournal, idx: nx_size) -> *NxEvictEntry { 133 if idx >= j.capacity { return (0 as i64) as *NxEvictEntry } 134 return (j.entries as i64 + (idx as i64) * 48) as *NxEvictEntry 135} 136 137// ===== nx_evict_count_for_cell ==================================== 138// 139// Walk the live portion of the ring and tally events for one cell. 140// Useful for homeostasis to spot eviction-storm patterns (>= 5 events 141// for the same cell within the journal window is migration signal). 142 143func nx_evict_count_for_cell(j: *NxEvictJournal, cell_id: nx_int) -> nx_int { 144 var hits: nx_int = 0 145 var i: nx_size = 0 146 var live: nx_size = j.count 147 if live > j.capacity { live = j.capacity } 148 while i < live { 149 let e: *NxEvictEntry = (j.entries as i64 + (i as i64) * 48) as *NxEvictEntry 150 if e.cell_id == cell_id { hits = hits + 1 } 151 i = i + 1 152 } 153 return hits 154}