code wiki / (root) / nx_enginelab_analysis.nx

nx_enginelab_analysis.nx source

↩ module page · 311 lines · 13310 B

1// nx_enginelab_analysis.nx -- EL2 replay, EL4 shader binding, EL5 stack sampling, EL6 call tree. 2// 3// WHY A FOURTH MODULE AND NOT MORE OF THE SPINE. Two lanes built the same rung inside one hour today 4// and the collision was caught by the LINKER rather than by either lane looking first. The cheap 5// structural answer is that a lane writes in files it owns and declares what it is taking BEFORE it 6// writes: these four rungs are declared to the sibling lane, and event kinds 9 and 10 are claimed 7// explicitly, because a silently reused KIND value is worse than a duplicate name -- the linker 8// cannot catch it, and every analyzer would read the record as a valid one of the wrong type. 9// 10// EVERYTHING HERE IS PURE BUFFER. No path, no descriptor, no syscall beyond allocation. The spine 11// stays adoptable by a wasm world and so does this. 12// license_tier: ORIGINAL No hardware writes (Rule 26). 13import "nx_syscalls.nx" 14import "nx_enginelab_lib.nx" 15 16// CLAIMED EVENT KINDS. The spine uses 1-8; the sibling lane takes 11 and up. 17const EL_K_BIND: i64 = 9 // EL_F_A = shader id, EL_F_B = pipeline stage 18const EL_K_SAMPLE: i64 = 10 // a periodic stack sample: EL_F_TS is the only thing that matters 19 20// ---- EL2: DETERMINISTIC REPLAY ------------------------------------------------------------------- 21// Recording a draw stream is not re-executing it. Replay walks the captured draws in order and folds 22// each one's pipeline, vertex count and bound state into a RUNNING digest, so two captures of the 23// same frame agree digest-for-digest and a capture with one draw altered diverges AT THAT ORDINAL. 24// The ordinal is the whole product: a bare mismatch tells you the frames differ, which you already 25// knew from looking at them. 26const EL_RP_DIGEST: i64 = 0 27const EL_RP_DRAWS: i64 = 1 28const EL_RP_SLOTS: i64 = 2 29 30func ela_fold(h: i64, v: i64) -> i64 { return (h * EL_HASH_MUL + v) % EL_HASH_MOD } 31 32func ela_draw_digest(st: *i64, r: i64, h: i64) -> i64 { 33 var x: i64 = ela_fold(h, el_get(st, r, EL_F_A)) 34 x = ela_fold(x, el_get(st, r, EL_F_B)) 35 x = ela_fold(x, el_get(st, r, EL_F_C)) 36 return ela_fold(x, el_get(st, r, EL_F_NAME)) 37} 38 39func el_replay(st: *i64, out: *i64) -> i64 { 40 out[EL_RP_DIGEST] = 0 41 out[EL_RP_DRAWS] = 0 42 let n: i64 = el_draw_count(st) 43 // A REPLAY OF NOTHING IS NOT A SUCCESSFUL REPLAY. Returning a digest of zero here would make two 44 // empty captures agree, which is the empty-set-passes defect wearing a replay costume. 45 if n == 0 { return EL_DIFF_UNMEASURABLE } 46 var h: i64 = 0 47 var i: i64 = 0 48 while i < n { 49 h = ela_draw_digest(st, el_draw_nth(st, i), h) 50 i = i + 1 51 } 52 out[EL_RP_DIGEST] = h 53 out[EL_RP_DRAWS] = n 54 return EL_DIFF_SAME 55} 56 57// out[0] = the first ordinal at which the two replays diverge. 0 SAME / 1 DIVERGENT / 3 UNMEASURABLE. 58func el_replay_diverge(a: *i64, b: *i64, out: *i64) -> i64 { 59 out[0] = 0 - 1 60 let na: i64 = el_draw_count(a) 61 let nb: i64 = el_draw_count(b) 62 if na == 0 { return EL_DIFF_UNMEASURABLE } 63 if nb == 0 { return EL_DIFF_UNMEASURABLE } 64 var lim: i64 = na 65 if nb < lim { lim = nb } 66 var ha: i64 = 0 67 var hb: i64 = 0 68 var i: i64 = 0 69 var found: i64 = 0 - 1 70 while i < lim { 71 if found < 0 { 72 ha = ela_draw_digest(a, el_draw_nth(a, i), ha) 73 hb = ela_draw_digest(b, el_draw_nth(b, i), hb) 74 if ha != hb { found = i } 75 } 76 i = i + 1 77 } 78 if found >= 0 { out[0] = found; return EL_DIFF_DIVERGENT } 79 if na != nb { out[0] = lim; return EL_DIFF_DIVERGENT } 80 return EL_DIFF_SAME 81} 82 83// ---- EL4: SHADER AND PIPELINE BINDING ------------------------------------------------------------ 84// A draw does not carry its shader; a BIND does, and the draw inherits whatever was last bound on 85// its own thread. Resolving that is state tracking, which is the thing a frame debugger does that a 86// log does not. 87func el_bind_shader(st: *i64, ts: i64, tid: i64, shader: i64, stage: i64) -> i64 { 88 return el_push(st, EL_K_BIND, ts, 0, tid, 0, shader, stage, 0) 89} 90 91// The shader in effect at draw ordinal `ord` for `stage`. Returns the shader id, or -1 when NOTHING 92// was ever bound for that stage on that thread -- an UNBOUND draw is a real and interesting state, 93// and reporting it as shader 0 would invent a binding that never happened. 94func el_shader_dump(st: *i64, ord: i64, stage: i64, out: *i64) -> i64 { 95 out[0] = 0 - 1 96 let d: i64 = el_draw_nth(st, ord) 97 if d < 0 { return 0 - 1 } 98 let dts: i64 = el_get(st, d, EL_F_TS) 99 let dtid: i64 = el_get(st, d, EL_F_TID) 100 let n: i64 = st[EL_H_N] 101 var best_ts: i64 = 0 - 1 102 var best: i64 = 0 - 1 103 var i: i64 = 0 104 while i < n { 105 if el_get(st, i, EL_F_KIND) == EL_K_BIND { 106 if el_get(st, i, EL_F_TID) == dtid { 107 if el_get(st, i, EL_F_B) == stage { 108 let ts: i64 = el_get(st, i, EL_F_TS) 109 if ts <= dts { 110 if ts >= best_ts { best_ts = ts; best = el_get(st, i, EL_F_A) } 111 } 112 } 113 } 114 } 115 i = i + 1 116 } 117 out[0] = best 118 return best 119} 120 121// ---- EL5: STACK SAMPLING ------------------------------------------------------------------------- 122// Every zone in the spine is an explicit begin/end in source. Sampling is the other half: periodic 123// observations attributed to whatever was executing, which is how you profile code you did not 124// instrument. 125// 126// THE HORIZON IS PUBLISHED, NOT IMPLIED. A sampler at interval I cannot resolve anything shorter 127// than I -- a zone briefer than one interval may collect ZERO samples and is invisible, not cheap. 128// Reporting attribution without stating that bound is how a sampling profiler quietly reports the 129// absence of evidence as evidence of absence, so the interval and the horizon ride in the same out 130// structure as the counts. 131// The sampling interval a caller declares. NAMED and overridable rather than buried: the interval IS 132// the horizon, so a number chosen here silently decides what the profiler is structurally unable to 133// see. 1000us is a declared default, not a measured one. 134const EL_SM_INTERVAL_US_DEFAULT: i64 = 1000 135const EL_SM_ATTRIB: i64 = 0 136const EL_SM_TOTAL: i64 = 1 137const EL_SM_UNZONED: i64 = 2 // samples that landed in no zone at all -- reported, never dropped 138const EL_SM_INTERVAL: i64 = 3 139const EL_SM_HORIZON: i64 = 4 // anything shorter than this can be missed entirely 140const EL_SM_SLOTS: i64 = 5 141 142func el_sample_stack(st: *i64, ts: i64, tid: i64) -> i64 { 143 return el_push(st, EL_K_SAMPLE, ts, 0, tid, 0, 0, 0, 0) 144} 145 146// The INNERMOST closed zone containing ts on tid, or -1. Innermost = the containing zone that STARTED 147// LATEST, which for properly nested scopes is the one actually executing. 148func ela_innermost(st: *i64, ts: i64, tid: i64) -> i64 { 149 let n: i64 = st[EL_H_N] 150 var best: i64 = 0 - 1 151 var best_ts: i64 = 0 - 1 152 var i: i64 = 0 153 while i < n { 154 if el_get(st, i, EL_F_KIND) == EL_K_ZONE { 155 if el_get(st, i, EL_F_TID) == tid { 156 let d: i64 = el_get(st, i, EL_F_DUR) 157 if d != EL_DUR_OPEN { 158 let z: i64 = el_get(st, i, EL_F_TS) 159 if z <= ts { 160 if ts < z + d { 161 if z >= best_ts { best_ts = z; best = i } 162 } 163 } 164 } 165 } 166 } 167 i = i + 1 168 } 169 return best 170} 171 172func el_sample_attribute(st: *i64, name: i64, interval_us: i64, out: *i64) -> i64 { 173 var k: i64 = 0 174 while k < EL_SM_SLOTS { out[k] = 0; k = k + 1 } 175 out[EL_SM_INTERVAL] = interval_us 176 out[EL_SM_HORIZON] = interval_us 177 let n: i64 = st[EL_H_N] 178 var i: i64 = 0 179 while i < n { 180 if el_get(st, i, EL_F_KIND) == EL_K_SAMPLE { 181 out[EL_SM_TOTAL] = out[EL_SM_TOTAL] + 1 182 let z: i64 = ela_innermost(st, el_get(st, i, EL_F_TS), el_get(st, i, EL_F_TID)) 183 if z < 0 { out[EL_SM_UNZONED] = out[EL_SM_UNZONED] + 1 } 184 else { if el_get(st, z, EL_F_NAME) == name { out[EL_SM_ATTRIB] = out[EL_SM_ATTRIB] + 1 } } 185 } 186 i = i + 1 187 } 188 return out[EL_SM_ATTRIB] 189} 190 191// ---- EL6: THE CALL TREE -------------------------------------------------------------------------- 192// THE RECONCILIATION IS THE TOOTH, not the drawing. Every microsecond of CPU time must be attributed 193// EXACTLY ONCE: the sum of every zone's SELF time must equal the sum of every ROOT zone's INCLUSIVE 194// time. If those two disagree the tree is double-counting or losing time, and no amount of rendering 195// it prettily would reveal that. The arithmetic already exists in the spine's child-total accounting; 196// this only reads it. 197const EL_FL_ROOTINCL: i64 = 0 198const EL_FL_ALLSELF: i64 = 1 199const EL_FL_NODES: i64 = 2 200const EL_FL_ROOTS: i64 = 3 201const EL_FL_OPEN: i64 = 4 202const EL_FL_SLOTS: i64 = 5 203 204func el_flame_totals(st: *i64, out: *i64) -> i64 { 205 var k: i64 = 0 206 while k < EL_FL_SLOTS { out[k] = 0; k = k + 1 } 207 let n: i64 = st[EL_H_N] 208 var i: i64 = 0 209 while i < n { 210 if el_get(st, i, EL_F_KIND) == EL_K_ZONE { 211 let d: i64 = el_get(st, i, EL_F_DUR) 212 if d == EL_DUR_OPEN { out[EL_FL_OPEN] = out[EL_FL_OPEN] + 1 } 213 else { 214 out[EL_FL_NODES] = out[EL_FL_NODES] + 1 215 out[EL_FL_ALLSELF] = out[EL_FL_ALLSELF] + (d - el_get(st, i, EL_F_C)) 216 if el_zone_is_root(st, i) == 1 { 217 out[EL_FL_ROOTS] = out[EL_FL_ROOTS] + 1 218 out[EL_FL_ROOTINCL] = out[EL_FL_ROOTINCL] + d 219 } 220 } 221 } 222 i = i + 1 223 } 224 // 1 when every microsecond is attributed exactly once 225 if out[EL_FL_ROOTINCL] == out[EL_FL_ALLSELF] { return 1 } 226 return 0 227} 228 229// The enclosing zone of idx: the containing zone that started latest and is not idx itself. 230// DERIVED BY CONTAINMENT rather than recorded at begin, because the parent index is not in the 231// record and this module does not edit the spine. Stated cost: this is O(n) per node, so the tree 232// build is O(n squared) -- acceptable for a capture, and named here rather than discovered later. 233func ela_parent(st: *i64, idx: i64) -> i64 { 234 let ts: i64 = el_get(st, idx, EL_F_TS) 235 let dur: i64 = el_get(st, idx, EL_F_DUR) 236 let tid: i64 = el_get(st, idx, EL_F_TID) 237 let n: i64 = st[EL_H_N] 238 var best: i64 = 0 - 1 239 var best_ts: i64 = 0 - 1 240 var j: i64 = 0 241 while j < n { 242 if j != idx { 243 if el_get(st, j, EL_F_KIND) == EL_K_ZONE { 244 if el_get(st, j, EL_F_TID) == tid { 245 let jd: i64 = el_get(st, j, EL_F_DUR) 246 if jd != EL_DUR_OPEN { 247 let jt: i64 = el_get(st, j, EL_F_TS) 248 if jt <= ts { 249 if jt + jd >= ts + dur { 250 var enclosing: i64 = 1 251 if jt == ts { if j > idx { enclosing = 0 } } 252 if enclosing == 1 { if jt >= best_ts { best_ts = jt; best = j } } 253 } 254 } 255 } 256 } 257 } 258 } 259 j = j + 1 260 } 261 return best 262} 263 264const EL_FL_MAXDEPTH: i64 = EL_STACK_MAX 265const EL_FL_SEMI: i64 = 59 266const EL_FL_SPACE: i64 = 32 267const EL_FL_NEWLINE: i64 = 10 268 269// Folded-stack emission: one line per closed zone, `root;...;leaf <self_us>`. Names are ids because 270// the spine interns names as ids and never holds strings -- the caller owns the id-to-text mapping. 271// REFUSES rather than truncates: a folded stack cut in half is a DIFFERENT tree that still parses. 272func el_flame_emit(st: *i64, buf: *u8, cap: i64) -> i64 { 273 let n: i64 = st[EL_H_N] 274 let chain: *i64 = sys_mmap(EL_FL_MAXDEPTH * EL_I64) as *i64 275 var p: i64 = 0 276 var i: i64 = 0 277 var refused: i64 = 0 278 while i < n { 279 if refused == 0 { 280 if el_get(st, i, EL_F_KIND) == EL_K_ZONE { 281 let d: i64 = el_get(st, i, EL_F_DUR) 282 if d != EL_DUR_OPEN { 283 var depth: i64 = 0 284 var cur: i64 = i 285 while cur >= 0 { 286 if depth < EL_FL_MAXDEPTH { chain[depth] = el_get(st, cur, EL_F_NAME); depth = depth + 1 } 287 cur = ela_parent(st, cur) 288 } 289 var q: i64 = depth - 1 290 while q >= 0 { 291 if p + EL_CAP_LINE_MAX > cap { refused = 1; q = 0 - 1 } 292 else { 293 p = nxi_buf(buf, p, chain[q]) 294 if q > 0 { buf[p] = EL_FL_SEMI as u8; p = p + 1 } 295 q = q - 1 296 } 297 } 298 if refused == 0 { 299 buf[p] = EL_FL_SPACE as u8; p = p + 1 300 p = nxi_buf(buf, p, d - el_get(st, i, EL_F_C)) 301 buf[p] = EL_FL_NEWLINE as u8; p = p + 1 302 } 303 } 304 } 305 } 306 i = i + 1 307 } 308 sys_munmap(chain as *u8, EL_FL_MAXDEPTH * EL_I64) 309 if refused == 1 { return 0 - 1 } 310 return p 311}