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}