Twinned from https://github.com/cfsh/ninefold/issues/758 by tools/beads/import_github.py's reconcile pass.
Raw data and reproduction recipes from the design 005 implementation (#686), so a later session can verify a number in minutes instead of re-deriving it in hours — and so the tooling built to get them is not rebuilt.
Every conclusion drawn from these lives on the issue that owns it (#755, #756, #757, #746, #747, #700, #668, #620, #598). This is the appendix, not the argument.
⚠️ All measurements on nf-dev-sg, 2026-08-07/08, Godot 4.7.1 mono, Xvfb + Mesa llvmpipe, nproc = 8, 15 GiB. ⚠️ Load is recorded with every number, because it dominates everything here — identical configurations varied 8.8 s → 13.3 s across the session.
---
budget_ms: 1⚠️ The single most useful technique from this chunk. The harness has no per-step timer, but budget_ms prints the elapsed wall clock of a wait *and its bracketing steps* whenever it overruns. Set it to 1 and every wait overruns, so every wait reports its real duration.
```bash python3 -c " import json d=json.load(open('tools/probes/new-pattern.json')) for i,s in enumerate(d['steps']): if 'wait' in s: s['budget_ms']=1 d['outDir']='probe_out/measure' json.dump(d,open('/tmp/measure.json','w'))" NINEFOLD_PROBE_SKIP_BUILD=1 ./probe.py /tmp/measure.json 2>&1 \ | grep -oE 'took [0-9]+ms' | grep -oE '[0-9]+' ```
Caveats: the window spans the step *before* the wait to the step *after* (#617 F1), so each figure is an upper bound on the wait alone — ratios are sound, absolute values are generous. Never commit the instrumented probe.
⚠️ A/B/A is not enough on this box. My first warm-vs-cold attempt ran 3 warm then 3 cold then 3 warm and showed a monotonic 13.1 → 9.4 s drift that swamped the effect. Alternate at the finest possible grain and record /proc/loadavg per run:
```bash for i in 1 2 3 4 5 6; do <make state A>; /usr/bin/time -f "A $i %e load=$(cut -d' ' -f1 /proc/loadavg)" <cmd> <make state B>; /usr/bin/time -f "B $i %e load=$(cut -d' ' -f1 /proc/loadavg)" <cmd> done ```
import PIL fails and there is no pip. This pure-stdlib decoder handles Godot's PNGs (8-bit, non-interlaced) and was written from scratch this session; keeping it here so nobody writes it again. Consider promoting it to tools/ if before/after work becomes routine.
```python """Minimal PNG reader + pixel diff — no third-party deps.""" import struct, sys, zlib
def read_png(path): data = open(path, "rb").read() assert data[:8] == b"\x89PNG\r\n\x1a\n", path pos, idat, meta = 8, [], None while pos < len(data): ln, typ = struct.unpack(">I4s", data[pos:pos+8]) body = data[pos+8:pos+8+ln] if typ == b"IHDR": meta = struct.unpack(">IIBBBBB", body) elif typ == b"IDAT": idat.append(body) pos += 12 + ln w, h, depth, ctype, _, _, interlace = meta assert depth == 8 and interlace == 0, meta nch = {0:1, 2:3, 3:1, 4:2, 6:4}[ctype] raw = zlib.decompress(b"".join(idat)) stride, out, prev, p = w*nch, [], bytearray(w*nch), 0 for _ in range(h): f = raw[p]; p += 1 line = bytearray(raw[p:p+stride]); p += stride for i in range(stride): a = line[i-nch] if i >= nch else 0 b = prev[i] c = prev[i-nch] if i >= nch else 0 x = line[i] if f == 1: x += a elif f == 2: x += b elif f == 3: x += (a+b)//2 elif f == 4: pp = a+b-c; pa, pb, pc = abs(pp-a), abs(pp-b), abs(pp-c) x += a if (pa <= pb and pa <= pc) else (b if pb <= pc else c) line[i] = x & 0xFF out.append(bytes(line)); prev = line return w, h, nch, out
def diff(pa, pb): w1, h1, c1, ra = read_png(pa); w2, h2, c2, rb = read_png(pb) if (w1, h1) != (w2, h2): return None return sum(1 for ya, yb in zip(ra, rb) for x in range(w1) if ya[x*c1:x*c1+3] != yb[x*c2:x*c2+3])
if __name__ == "__main__": n = diff(sys.argv[1], sys.argv[2]); print(f"{n} px differ") ```
⚠️ rm -rf .godot also deletes .godot/mono/temp/bin/*/Ninefold.dll, so the next run rebuilds and your "cold" measurement silently includes a ~22 s build. Keep mono/:
```bash find .godot -mindepth 1 -maxdepth 1 ! -name mono -exec rm -rf {} + ```
```bash
for i in $(seq 8); do (timeout 200 python3 -c "
while True: pass" &); done
```
⚠️ timeout bounds them so a crashed session cannot leave the box spinning. Verify with pgrep -f 'while True' afterwards.
---
main @ e93f726495 wait (frame-counted) · 32 wait_ms (wall-clock) · 2 budget_ms.
⚠️ #668's body says wait_ms is used 0 times; that was true at 3f9b5c9 and is not now.
| probe | wait_ms steps |
|---|---|
| explore-walk | 26 |
| playhead-song | 4 |
| butterbar-gate | 1 |
| split-notes-editor | 1 |
Highest wait counts: grid-scroll 93, focus-grammar 51, single-screen 28, roll-jumps 22, ring-scale 21, scale-200 21.
wait value histogram (n = 495)``` 2:86 3:80 4:77 5:30 6:61 8:52 9:2 10:21 12:7 14:23 15:3 16:3 18:3 20:7 30:5 40:1 45:30 50:3 80:1 ```
wait (n = 495)``` action:205 expect_focus:99 shot:59 focus:47 joybtn:27 expect_prop:20 expect_mode:7 expect_bar:4 END:4 joyhold:4 expect_pattern:3 expect_dial:3 expect_dialog:2 wait:2 expect_zone:2 joyup:2 expect_row_label:2 set_dial:1 expect_captures:1 joydown:1 ```
| successor class | n | of which ≥10 frames |
|---|---|---|
| a shot | 59 | 20 (34%) |
| an expect_* | 143 | 34 (24%) |
| an input | 293 | 53 (18%) |
Pre-shot waits are more likely to be large than pre-assertion ones, and 76% of pre-assertion waits are already small. Neither successor nor magnitude separates the classes → #756.
``` action:331 budget_ms:2 count:2 expect_bar:6 expect_captures:4 expect_chit:3 expect_dial:4 expect_dialog:2 expect_focus:123 expect_hint:4 expect_journal:2 expect_mode:8 expect_pattern:6 expect_pixel:3 expect_prop:40 expect_row_label:4 expect_zone:3 focus:80 frames:1 hold:5 is:40 joyaxis:2 joybtn:56 joydown:5 joyhold:8 joyup:5 key:2 ms:12 rgb:3 set_dial:3 shot:129 tol:3 value:2 wait:495 wait_ms:32 within:2 ```
This is the dataset behind CHEAP_STIMULI in tools/tests/test_probe_corpus.py. Regenerate:
```bash python3 -c " import json,glob,collections k=collections.Counter() for p in glob.glob('tools/probes/*.json'): for s in json.load(open(p))['steps']: for x in s: if not x.startswith('_'): k[x]+=1 print(dict(sorted(k.items())))" ```
---
SKIP_BUILD=1)| probe | seconds |
|---|---|
| grid-scroll | 52.7 |
| focus-grammar | 30.1 |
| single-screen | 24.7 |
| explore-walk | 20.3 |
These set the deadline ladder in #752 (import 300 s < probe 600 s < sweep 660 s).
new-pattern per-step, quiet vs loaded — the full tableMethod A1. Quiet = load 0.47, ×3. Loaded = eight spinners, ×2.
| step | wait | quiet (ms) | loaded (ms) | ratio |
|---|---|---|---|---|
| 0 | 45 | 3949 / 3771 / 3737 | 7807 / 8132 | 2.1× |
| 3 | 3 | 332 / 319 / 317 | 727 / 721 | 2.2× |
| 5 | 3 | 310 / 292 / 258 | 622 / 601 | 2.1× |
| 8 | 10 | 774 / 796 / 755 | 1678 / 1754 | 2.2× |
| 11 | 10 | 758 / 796 / 764 | 1766 / 1760 | 2.3× |
| 15 | 3 | 397 / 415 / 415 | 924 / 855 | 2.2× |
| 17 | 3 | 338 / 347 / 306 | 873 / 853 | 2.5× |
| 19 | 10 | 724 / 683 / 723 | 1778 / 1712 | 2.4× |
| 21 | 14 | 874 / 805 / 815 | 1916 / 2105 | 2.4× |
⚠️ wait: 45 is 3.9 s, not the 750 ms that 45÷60 implies — a naive conversion cuts it 5×. And frame waits get longer under load, so they already adapt safely → #756.
| pair | warm | cold | |---|---|---| | 1 | 9.25 | 8.84 | | 2 | 9.39 | 8.99 | | 3 | 13.29 | 10.76 | | 4 | 9.74 | 9.00 | | 5 | 10.13 | 9.58 | | 6 | 11.86 | 9.43 |
Warm slower 6/6. Refuted alternatives: no first-run penalty (9.57 / 9.00 / 8.84 flat after wiping .godot but for mono/); dotnet build is ~3 s of a warm probe.py (13.0 s with, 9.5 s without) → #755.
| | seconds | artifacts |
|---|---|---|
| cold (mono/ only), load 1.05 | 10.2 | 850 produced |
| warm re-import | 8.0 | — |
| cold, before dotnet build | 11.8 | 844, + autoload errors |
⚠️ Importing before the build emits Failed to create an autoload, script 'res://shared/VisualProbe.cs' is not compiling for every autoload. Both callers build first; do not reorder.
Cold and warm are only 1.3× apart — engine startup dominates, not the encode.
422 .import files → 422 artifacts. Walk 16 ms + parse 17 ms + existence 9 ms = 42 ms. Sources total 227.8 MB; hashing them all for a source_md5 staleness check is 845 ms — which is why staleness is delegated to Godot's own --import instead.
---
| workers | load at start | result | failing set | wall clock |
|---|---|---|---|---|
| 8 | 11.35 | 28 / 6 | butterbar-gate, explore-walk, focus-grammar, playhead-song, playtest-capture, sound-modal | 268 s |
| 6 | 1.16 | 32 / 2 | butterbar-gate, focus-grammar | 265 s |
| 2 | 3.63 | 33 / 1 | row-label-wrap | 334 s |
| 1 | 17.76 | 34 / 0 | — | 517 s |
⚠️ A different failing set at each level, and all six 8-way failures pass when re-run one at a time on the same box. The serial run was the *most* loaded → #746, #755, #757.
Serial 70 s → 8-way 32 s = 2.19×. Better than §2.2's 1.63×, which was 4-way on a box then reading 4 cores.
3 rounds each, 8 probes: per-worker 30 / 33 / 32 s, shared 31 / 30 / 32 s, zero failures both. S2's "one Xvfb per worker" is unnecessary → #746.
---
tonapse-home, same commit, cold vs warm import cache:
| shot | differing px | % of frame |
|---|---|---|
| 01-home | 1014 | 0.049% |
| 02-focus-moved | 3210 | 0.155% |
| 03-after-accept | 1488 | 0.072% |
Independently reproduces #619's 1995 px / 0.096% on focus-ring. After the preflight, 01 and 03 are byte-identical to the warm reference.
⚠️ 02-focus-moved is non-deterministic on its own — three consecutive runs at a constant warm cache differ by 320 and 488 px. Unrelated to the cache; do not use that shot as a pixel-exact reference.
```bash $ printf 'res://games/explore/assets/char2.png' | md5sum 1a287912085519e147e80773bd272dee $ grep dest_files games/explore/assets/char2.png.import dest_files=["res://.godot/imported/char2.png-1a287912085519e147e80773bd272dee.ctex"] ```
So editing an asset in place changes no filename, and a presence check reports a complete cache over stale art. Proven reversibly:
``` triangle.png overwritten in place: presence check -> "import 422/422 present" (the over-claim) after hand-run -> artifact b58af96e... (unchanged — probe.py does not force) after full sweep -> artifact e71a3ddb... (re-imported) source restored -> artifact b58af96e... ```
---
| gate | value | file |
|---|---|---|
| PianoRoll butterbar hold | 350 ms | PianoRoll.cs:132 |
| Chip long-press | 400 ms | Chip.cs:12 |
| RadialGesture long-press | 380 ms | RadialGesture.cs:22 |
| Dial stick-quiet after R3 | 300 ms | Dial.cs:212 |
| SongGrid ghost flash | 0.45 s | SongGrid.cs:1137 |
| Persistence debounce | 0.6 s | PersistenceDriver.cs:22 |
Regenerate: ```bash grep -rn "GetTicksMsec\|CreateTimer" games/ shared/ --include=*.cs | grep -v /tests/ ```
---
.tscn in the tree, each with exactly one ext_resource — its own script. No scene declares another.Blockbrains.cs:26 GD.Load<PackedScene>("res://games/blockbrains/board.tscn"), instanced per player at :251. Board.cs = 485 lines..tscn. ⚠️ Its root script is 1,365 lines — design 005 §4F's *"13k-LOC god root script"* is the whole game's count, not the root's.---
nproc = 8, 15 GiB RAM, 66 GiB free disk. Godot at /opt/godot/Godot_v4.7.1-stable_mono_linux.x86_64.
⚠️ The core count has been wrong three different ways — notes/machines.md said 2, design 005 §2.7 measured 4, it is 8. Read nproc at runtime; never quote a figure from prose.
.godot sizes: 8.5 MB with mono/ only (a fresh clone), ~222–230 MB imported.
⚠️ At the time of measurement, three dev worktrees on this box (argo, review-cadence, session-state) had mono/-only .godot — i.e. every UI gate run from them rendered with no textures or audio and exited 0. That is #619, live.
Refs #686 · Refs #755 · Refs #756 · Refs #757 · Refs #746 · Refs #747 · Refs #619 · Refs #668
None.
No comments.