code wiki / (root) / nx_enginelab_store.nx

nx_enginelab_store.nx source

↩ module page · 147 lines · 7519 B

1// nx_enginelab_store.nx -- EL1: PERSIST A CAPTURE AND REOPEN IT, for the engine instrument. 2// 3// WHY THIS IS A THIRD MODULE AND NOT PART OF THE SPINE. Same principle that moved golden-image out: 4// nx_enginelab_lib is PURE BUFFER OPS so a wasm world can adopt it, and file I/O is exactly the thing 5// that stops being portable. A consumer that only records frames and zones should not acquire an 6// openat/write/read dependency to do it. So the spine stays pure, and persistence lives here. 7// 8// WHAT EL1 IS FOR. Until now a capture existed only inside the process that took it, which means it 9// could not be attached to a bug report, diffed across machines, or replayed. It also means EL3 10// (inspect real resource contents per draw) is impossible, because inspecting contents needs a 11// capture that OUTLIVES the process. This is that file. 12// 13// THE LOAD-BEARING REQUIREMENT IS THE REFUSAL, NOT THE ROUND-TRIP. Anyone can write bytes and read 14// them back. The done-rule for this rung is that a TRUNCATED FILE IS REFUSED RATHER THAN PARTIALLY 15// PARSED, because a half-read capture parses into a SHORTER capture that looks completely healthy -- 16// fewer frames, fewer draws, every analyzer happily computing a confident wrong answer over it. That 17// is the silent class this whole board exists to catch, and a persistence layer is where it is 18// easiest to introduce. Hence: a declared byte length, a checksum, and a NAMED reason per failure. 19// 20// AND THE COVERAGE COUNTERS TRAVEL WITH THE DATA. The store header carries drop and error counts, and 21// they are serialised WITH the records. A capture that dropped events under load must not reload 22// claiming complete coverage -- if it did, persistence would launder a partial measurement into a 23// clean-looking one, which is worse than not persisting at all. 24// license_tier: ORIGINAL No hardware writes (Rule 26). 25import "nx_syscalls.nx" 26import "nx_enginelab_lib.nx" 27 28const ELS_MAGIC: i64 = 1162892876 // 'NXEL' as a decimal constant, one witness that this is our file 29const ELS_VERSION: i64 = 1 30const ELS_PFX: i64 = 4 // magic, version, declared_bytes, checksum 31const ELS_HASH_MUL: i64 = 1000003 32const ELS_HASH_MOD: i64 = 2147483647 33 34// NAMED REASONS. A persistence layer that returns a null pointer teaches the caller nothing, and the 35// caller then guesses -- usually that the file is absent, which is only one of six possibilities. 36const ELS_OK: i64 = 0 37const ELS_UNREADABLE: i64 = 1 // could not read the path at all 38const ELS_TOO_SMALL: i64 = 2 // shorter than the fixed prefix: cannot even be inspected 39const ELS_BAD_MAGIC: i64 = 3 // not one of our captures 40const ELS_BAD_VERSION: i64 = 4 // ours, but a format this build cannot read 41const ELS_SIZE_MISMATCH: i64 = 5 // declared length disagrees with the bytes actually present 42const ELS_CHECKSUM: i64 = 6 // right length, wrong content 43 44// reason-out slots: the reason travels WITH the numbers that decided it 45const ELS_R_CODE: i64 = 0 46const ELS_R_DECLARED: i64 = 1 47const ELS_R_ACTUAL: i64 = 2 48const ELS_R_SLOTS: i64 = 3 49 50func els_ck(w: *i64, from: i64, to: i64) -> i64 { 51 var h: i64 = 0 52 var i: i64 = from 53 while i < to { 54 h = (h * ELS_HASH_MUL + w[i]) % ELS_HASH_MOD 55 i = i + 1 56 } 57 return h 58} 59 60func els_slots_for(n: i64) -> i64 { return ELS_PFX + EL_H + n * EL_REC } 61 62// Write the whole store -- header AND records -- to path. ONE write syscall for the whole buffer: 63// building the image and emitting it once is the difference between a capture that costs a syscall 64// per event and one that costs a syscall per capture. 65// Returns 0 on success, or a negative reason. 66func el_capture_write(st: *i64, path: *u8) -> i64 { 67 if el_ok(st) == 0 { return 0 - ELS_UNREADABLE } 68 let n: i64 = st[EL_H_N] 69 let slots: i64 = els_slots_for(n) 70 let bytes: i64 = slots * EL_I64 71 let w: *i64 = sys_mmap(bytes) as *i64 72 if (w as i64) == 0 { return 0 - ELS_UNREADABLE } 73 w[0] = ELS_MAGIC 74 w[1] = ELS_VERSION 75 w[2] = bytes 76 w[3] = 0 77 // the STORE HEADER travels with the records: drop counts, error counts and the frame ordinal are 78 // part of what the capture MEANS, not incidental bookkeeping 79 var i: i64 = 0 80 while i < EL_H { w[ELS_PFX + i] = st[i]; i = i + 1 } 81 var j: i64 = 0 82 while j < n * EL_REC { 83 w[ELS_PFX + EL_H + j] = st[EL_REC_BASE + j] 84 j = j + 1 85 } 86 w[3] = els_ck(w, ELS_PFX, slots) 87 let fd: i64 = sys_openat_wr(path, MODE_0644) 88 if fd < 0 { sys_munmap(w as *u8, bytes); return 0 - ELS_UNREADABLE } 89 let got: i64 = sys_write(fd, w as *u8, bytes) 90 sys_close(fd) 91 sys_munmap(w as *u8, bytes) 92 if got != bytes { return 0 - ELS_SIZE_MISMATCH } 93 return ELS_OK 94} 95 96// Read a capture back. Returns a store pointer, or 0 with rs[ELS_R_CODE] naming WHY. 97// EVERY REJECTION PATH IS CHECKED BEFORE ANY RECORD IS TRUSTED, and in an order that cannot read out 98// of bounds: size-for-prefix first, then magic, then version, then declared-vs-actual, then checksum. 99func el_capture_read(path: *u8, rs: *i64) -> *i64 { 100 var k: i64 = 0 101 while k < ELS_R_SLOTS { rs[k] = 0; k = k + 1 } 102 rs[ELS_R_CODE] = ELS_UNREADABLE 103 let lb: *i64 = sys_mmap(16) as *i64 104 let buf: *u8 = sys_read_file(path, lb) 105 if (buf as i64) == 0 { return 0 as *i64 } 106 let actual: i64 = lb[0] 107 rs[ELS_R_ACTUAL] = actual 108 // A FILE TOO SHORT TO HOLD THE PREFIX CANNOT BE INSPECTED AT ALL. Checking this first is what 109 // makes every later read in-bounds; doing it after reading the magic would be reading the magic 110 // out of a buffer that may not contain one. 111 if actual < ELS_PFX * EL_I64 { rs[ELS_R_CODE] = ELS_TOO_SMALL; return 0 as *i64 } 112 let w: *i64 = buf as *i64 113 if w[0] != ELS_MAGIC { rs[ELS_R_CODE] = ELS_BAD_MAGIC; return 0 as *i64 } 114 if w[1] != ELS_VERSION { rs[ELS_R_CODE] = ELS_BAD_VERSION; return 0 as *i64 } 115 let declared: i64 = w[2] 116 rs[ELS_R_DECLARED] = declared 117 // THE TRUNCATION TOOTH. A short file would otherwise parse into a SHORTER capture that looks 118 // perfectly healthy -- fewer frames, fewer draws, every analyzer confidently wrong. Refusing on a 119 // declared-vs-actual mismatch is the whole reason the length is written down. 120 if declared != actual { rs[ELS_R_CODE] = ELS_SIZE_MISMATCH; return 0 as *i64 } 121 let n: i64 = w[ELS_PFX + EL_H_N] 122 if n < 0 { rs[ELS_R_CODE] = ELS_SIZE_MISMATCH; return 0 as *i64 } 123 if els_slots_for(n) * EL_I64 != actual { rs[ELS_R_CODE] = ELS_SIZE_MISMATCH; return 0 as *i64 } 124 let want: i64 = w[3] 125 w[3] = 0 126 let have: i64 = els_ck(w, ELS_PFX, els_slots_for(n)) 127 w[3] = want 128 if have != want { rs[ELS_R_CODE] = ELS_CHECKSUM; return 0 as *i64 } 129 // Rebuild at the ORIGINAL capacity, not at n: a store reopened with cap==n would report itself 130 // as full, and a caller appending to it would start dropping events for a reason that belongs to 131 // the reader rather than to the run being examined. 132 let cap: i64 = w[ELS_PFX + EL_H_CAP] 133 var mkcap: i64 = cap 134 if mkcap < n { mkcap = n } 135 let st: *i64 = el_new(mkcap) 136 if (st as i64) == 0 { rs[ELS_R_CODE] = ELS_UNREADABLE; return 0 as *i64 } 137 var i: i64 = 0 138 while i < EL_H { st[i] = w[ELS_PFX + i]; i = i + 1 } 139 st[EL_H_CAP] = mkcap 140 var j: i64 = 0 141 while j < n * EL_REC { 142 st[EL_REC_BASE + j] = w[ELS_PFX + EL_H + j] 143 j = j + 1 144 } 145 rs[ELS_R_CODE] = ELS_OK 146 return st 147}