#!/usr/bin/env python3 """Watch the REAL phase counter and every squadron's state, together. Three earlier runs polled `[*(0x828F35F8)+236]` and saw 0 forever -- that mirror is only written once the ordinal exceeds 1 (script-runtime-probe.md). This reads `[ScriptMission+40]`, the counter itself, plus the per-unit records the phase conditions actually test, so a phase advance and the state change that caused it are visible in one sample. It also does two things the earlier probes had to learn the hard way: * witnesses the freeze every 60 s (a frozen guest produces a perfectly clean-looking negative -- see mission-freeze-resume-spin.md); * re-locates the ScriptMission if the pointer stops validating, rather than silently reporting stale numbers. `--mission 0x...` skips the two full guest-memory sweeps `find_mission()` does and validates the given address with a handful of reads instead. That exists because the sweeps are the leading suspect for the in-mission freeze: runs with this probe attached froze at ~70/126/253 s, while the same runs without it went 936 s and 1064 s clean (mission-freeze-resume-spin.md). Sweep-free mode is the experiment that separates "the sweeps do it" from "the per-sample reads do it". Usage: phase_watch.py [secs] [every_s] [--mission 0xADDR] """ import os import sys import time sys.path.insert(0, __file__.rsplit('/', 1)[0]) import gmem import isl import squadron_state as S WATCH = ['ADN110', 'ADN111', 'ADN112'] def main(): ssb = isl.load(sys.argv[1]) sym2 = isl.symbols(ssb, 2) secs = float(sys.argv[2]) if len(sys.argv) > 2 else 900 every = float(sys.argv[3]) if len(sys.argv) > 3 else 5 given = None if '--mission' in sys.argv: given = int(sys.argv[sys.argv.index('--mission') + 1], 0) path = gmem.mem_path() size = os.path.getsize(path) import frozen t0 = time.time() with open(path, 'rb', buffering=0) as f: if given is not None: # Validate WITHOUT sweeping: the mission's symtab1 pointer must equal # the phase's, and the code base must be a plausible pointer. Both # are self-consistency checks internal to the two objects, so they # cost four reads instead of two full scans. code = S.u32(f, given + 24) s44 = S.u32(f, given + 44) ph = S.u32(f, given + 4) p244 = S.u32(f, ph + 244) if ph else None ok = (code and 0x10000 <= code < 0xFFFFFFF0 and s44 and s44 == p244) if not ok: print('given ScriptMission 0x%08X does not validate ' '(code=%s s44=%s phase244=%s)' % (given, code, s44, p244)) return 1 m, fb = given, None print('ScriptMission 0x%08X (given, validated, NO sweeps)' % m, flush=True) else: m, fb = S.find_mission(f, size, ssb) if m is None: print('ScriptMission not located'); return 1 print('file base 0x%08X ScriptMission 0x%08X' % (fb, m), flush=True) last = None next_frozen = 0.0 while time.time() - t0 < secs: now = time.time() - t0 if now >= next_frozen: next_frozen = now + 60 try: dead, _ = frozen.frozen(5.0) except Exception: dead = False if dead: print(' [%6.1fs] *** GUEST FROZEN -- readings below are about ' 'a dead world ***' % now, flush=True) r = S.read_states(f, m, sym2, WATCH) # Two script-VM counters, so a phase boundary and what it does to the # VM are visible in the same sample: # [phase+272+20] triggers queued (the container's element count) # [phase+216+8] coroutines alive (the active-thread list's count) # Built-in 100 is `reset_phase_threads`: it clears the container and # frees every thread but the caller, so BOTH should collapse at a # phase terminator. If they climb straight through one instead, that # reading is wrong -- which is the point of printing them. ph = S.u32(f, m + 4) pending = S.u32(f, ph + 272 + 20) if ph else None threads = S.u32(f, ph + 216 + 8) if ph else None key = (r['phase_ordinal'], r['finished'], r['active_records'], pending, threads, tuple((n, (v or {}).get('state')) for n, v in r['units'].items())) if key != last: print(' [%6.1fs] phase=%s finished=%s active=%3d pending=%s threads=%s %s' % ( now, r['phase_ordinal'], r['finished'], r['active_records'], pending, threads, ' '.join('%s:%s' % (n, (v or {}).get('state')) for n, v in r['units'].items())), flush=True) last = key time.sleep(every) print('done', flush=True) return 0 if __name__ == '__main__': sys.exit(main())