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}