diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 82db7085..520034f4 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -464,7 +464,15 @@ search cannot find a *schedule*. 210 s of verified-live flight** ≈115 game-seconds. Establishes that nothing arrives in the first ~115 game-s of Stage 02 phase 1 while the player kills 8 craft. Does NOT settle the question — t=170/210/240 route entries are still out - of reach. Also: the 42-record anomaly recurred and the discard rule + of reach. +* ✅ **Run 17 reproduces it (n=2)**: no stalls through **t=240 s** (~132 game-s), + 9 losses, 0 confirmed arrivals — so nothing arrives past the t=90 and t=120 + route entries either. 4 flicker `up`s correctly rejected (~1/min, which is why + the persistence rule matters). 🔴 **Startup eats ~100 s of a ~350 s budget**: + `enumerate_craft` iterated every 4-byte word of 32 MB in Python (8M steps) to + find 14 fixed needles. **Replaced with `bytes.find()` per definition VA — not + yet run**; if it recovers that time the window reaches ~340 s ≈ 187 game-s, + finally covering the **t=170** entry. Also: the 42-record anomaly recurred and the discard rule correctly refused the run; a rescan-until-baseline retry is now in place to test whether it climbs. All three witness failures share one pattern: a shortcut in *selecting* the witness, each caught diff --git a/docs/re/mission-arrival-watch.md b/docs/re/mission-arrival-watch.md index 6667c768..ad2d2ccb 100644 --- a/docs/re/mission-arrival-watch.md +++ b/docs/re/mission-arrival-watch.md @@ -353,3 +353,40 @@ still out of reach — but it does establish that **nothing arrives in the first One flicker `up` was surfaced and correctly not counted. Sixteen runs, still no arrival. The difference is that this one is evidence. + +--- + +# Run 17: the clean result reproduces, and the window is being eaten by startup + +## ✅ Reproduced (n = 2) + +``` +tick witnesses: 4584 candidates, using 32 at 11/s +t= 0s … t=240s no stall flag on any sample +9 losses, 4 flicker `up` events (none from zero), 0 confirmed arrivals +``` + +Second consecutive run with no stalls, confirming that disabling the periodic +rescan is what fixed the freezes. The trustworthy negative now extends to +**240 s of verified-live flight ≈ 132 game-seconds**, past the route table's +t = 90 and t = 120 entries with nothing arriving. + +Four increases were surfaced and all correctly rejected — the flicker rate is +substantial, roughly one per minute, which is exactly why the persistence rule +matters. + +## 🔴 Startup costs ~100 s of a ~350 s budget + +The arithmetic does not add up unless something slow sits between them: boot +finished at 249 s, the probe ran 240 s, and the turn's 595 s cap fired. That +leaves about **100 s unaccounted for** — the witness calibration plus the initial +craft enumeration. + +`enumerate_craft` was iterating **every 4-byte word of 32 MB in Python**, 8 +million steps, to find 14 fixed needles. Replaced with `bytes.find()` per +definition VA, which is the same search at C speed and is what the vtable scan +already did. + +If that recovers most of the 100 s, the observation window grows from ~240 s to +~340 s — about **187 game-seconds**, which would finally reach the **t = 170** +route entry. Not yet run. diff --git a/tools/re-capture/wave7_probe.py b/tools/re-capture/wave7_probe.py index de748946..c79179bf 100755 --- a/tools/re-capture/wave7_probe.py +++ b/tools/re-capture/wave7_probe.py @@ -39,24 +39,32 @@ def scan_vt(fd, size, vt): return sorted(out) def enumerate_craft(fd, defs, want): - """Heavy: full heap scan. Returns [(base, unit, roster_off)].""" + """Full heap scan. Returns [(base, unit, roster_off)]. + + Was iterating every 4-byte word of 32 MB in Python -- 8 million steps -- and + the startup cost worked out at roughly 100 s of wall-clock, which is a third + of the observation window a turn can afford. Each definition VA is a fixed + 4-byte needle, so bytes.find() does the same search at C speed. + """ lo, hi = gmem.va_to_off(entities2.ENT_VA_LO), gmem.va_to_off(entities2.ENT_VA_HI) out, pos = [], lo while pos < hi: m = min(1 << 24, hi - pos) blob = os.pread(fd, m, pos) - for k in range(0, len(blob) - 3, 4): - nm = defs.get(blob[k:k+4]) - if not nm: continue - base = pos + k - DELTA - head = os.pread(fd, WIN, base) - own = None - for j in range(0, len(head) - 3, 4): - (p,) = struct.unpack_from('>I', head, j) - if p in want: own = want[p]; break - out.append((base, nm, own)) + for needle, nm in defs.items(): + i = blob.find(needle) + while i != -1: + if (pos + i) % 4 == 0: + base = pos + i - DELTA + head = os.pread(fd, WIN, base) + own = None + for j in range(0, len(head) - 3, 4): + (p,) = struct.unpack_from('>I', head, j) + if p in want: own = want[p]; break + out.append((base, nm, own)) + i = blob.find(needle, i + 1) pos += m - return out + return sorted(out) def alive(fd, base): try: return struct.unpack('>f', os.pread(fd, 4, base + HULL))[0] > 0