First run of wave7_probe: zero stalled samples across the whole run, against three consecutive heavy-probe runs that stalled at roughly 27, 83 and 255 s. The fix works. The guest is also visibly healthier -- 19 losses against 8 in a heavy-probe run of comparable length -- so starving the emulator had been suppressing the very activity the probe existed to watch. It also reported the first arrival of the whole line of work, and that arrival does not survive inspection. A record went 1 -> 0 at t=229, 0 -> 2 at t=259 and 2 -> 0 at t=274. Two craft appearing and vanishing within fifteen seconds is not a wave. The same log contains the giveaway: at t=60 a record read 13 and at t=75 the same record read 14, an increase, with nothing printed, because the probe only surfaced decreases. The hull-based liveness read flickers, and a flicker that straddles zero was indistinguishable from an arrival under the old rule. The count therefore stands at zero confirmed arrivals in eleven runs. Two changes, neither yet exercised: every increase is printed rather than only those from zero, and an increase from zero counts only if it persists across two consecutive samples, with a candidate that returns to zero discarded as flicker. Recorded because it was close: under the old rule this run would have been written up as "first arrival observed", which would have been the strongest-looking result so far and wrong.
134 lines
5.9 KiB
Python
Executable File
134 lines
5.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Cheap per-record watch: the heavy rescan was stalling the guest.
|
|
|
|
A no-probe control ran 300 s clean while probed runs stalled at 27-255 s
|
|
(guest-stalls.md), so the 32 MB rescan every 12 s has to go. After one full
|
|
enumeration the craft bases and their roster links are known, and a sample only
|
|
needs the hull word at each base -- about 1.2 KB instead of 32 MB. A full rescan
|
|
runs only every RESCAN seconds to catch anything genuinely new.
|
|
"""
|
|
import os, sys, time, struct, collections, importlib.util
|
|
SD = __file__.rsplit('/', 1)[0]
|
|
sys.path.insert(0, SD)
|
|
import gmem, gworld, entities2
|
|
_w3 = importlib.util.spec_from_file_location('w3', SD + '/wave3_probe.py')
|
|
wave3 = importlib.util.module_from_spec(_w3); _w3.loader.exec_module(wave3)
|
|
|
|
ROSTER_VT = struct.pack('>I', 0x820AF030)
|
|
DELTA, WIN, LINK, HULL = 0x130, 0x400, 0x08, 0x154
|
|
BASELINE, RESCAN = 116, 90
|
|
|
|
def scan_vt(fd, size, vt):
|
|
out = []
|
|
for a, b in gmem.extents(fd, size):
|
|
pos = a
|
|
while pos < b:
|
|
m = min(1 << 24, b - pos)
|
|
blob = os.pread(fd, m, pos)
|
|
i = blob.find(vt)
|
|
while i != -1:
|
|
if (pos + i) % 4 == 0: out.append(pos + i)
|
|
i = blob.find(vt, i + 1)
|
|
pos += m
|
|
return sorted(out)
|
|
|
|
def enumerate_craft(fd, defs, want):
|
|
"""Heavy: full heap scan. Returns [(base, unit, roster_off)]."""
|
|
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))
|
|
pos += m
|
|
return out
|
|
|
|
def alive(fd, base):
|
|
try: return struct.unpack('>f', os.pread(fd, 4, base + HULL))[0] > 0
|
|
except Exception: return False
|
|
|
|
def main():
|
|
secs = int(sys.argv[1]) if len(sys.argv) > 1 else 300
|
|
every = int(sys.argv[2]) if len(sys.argv) > 2 else 15
|
|
w = gworld.World(); fd = w.fd
|
|
defs = entities2.definitions(w)
|
|
if not defs: print('NOT IN A MISSION'); return 2
|
|
roster = scan_vt(fd, w.size, ROSTER_VT)
|
|
print('roster records: %d (baseline %d)' % (len(roster), BASELINE))
|
|
if len(roster) != BASELINE:
|
|
print('DISCARD: not the reproduced baseline'); return 3
|
|
f = os.fdopen(os.dup(fd), 'rb')
|
|
label, want = {}, {}
|
|
for o in roster:
|
|
va = gmem.primary_va(o)
|
|
if va is None: continue
|
|
label[o] = wave3.resolve_id(f, w.size, o)[0] or '?'
|
|
want[va + LINK] = o
|
|
|
|
tick = None
|
|
lo = gmem.va_to_off(0xBC000000)
|
|
a = os.pread(fd, 1 << 20, lo); time.sleep(2.0); b = os.pread(fd, 1 << 20, lo)
|
|
for k in range(0, len(a) - 3, 4):
|
|
va, vb = struct.unpack_from('>I', a, k)[0], struct.unpack_from('>I', b, k)[0]
|
|
if va < vb and 5 < (vb - va) / 2.0 < 1 << 18: tick = lo + k; break
|
|
print('tick witness: %s' % (('%#x' % tick) if tick else 'NONE'))
|
|
last_tick = struct.unpack('>I', os.pread(fd, 4, tick))[0] if tick else 0
|
|
|
|
craft = enumerate_craft(fd, defs, want)
|
|
print('craft %d, linked %d' % (len(craft), sum(1 for c in craft if c[2])))
|
|
prev = collections.Counter(c[2] for c in craft if c[2] and alive(fd, c[0]))
|
|
print('t= 0s deployed=%d strengths %s'
|
|
% (len(prev), sorted(collections.Counter(prev.values()).items())), flush=True)
|
|
t0 = time.time(); last_rescan = t0; arr = los = 0; stalls = 0
|
|
pending, confirmed = {}, 0
|
|
while time.time() - t0 < secs:
|
|
time.sleep(every)
|
|
el = round(time.time() - t0)
|
|
if time.time() - last_rescan > RESCAN:
|
|
craft = enumerate_craft(fd, defs, want); last_rescan = time.time()
|
|
cur = collections.Counter(c[2] for c in craft if c[2] and alive(fd, c[0]))
|
|
st = ''
|
|
if tick:
|
|
now = struct.unpack('>I', os.pread(fd, 4, tick))[0]
|
|
if now <= last_tick: st = ' *** GUEST STALLED ***'; stalls += 1
|
|
last_tick = now
|
|
# Report EVERY increase, not just 0 -> n. The first run logged a record
|
|
# reading 13 then 14 with no event printed, which is how flicker in the
|
|
# hull-based liveness read hides: only decreases were being surfaced, so
|
|
# a spurious 0 -> 2 looked like an arrival while 13 -> 14 looked like
|
|
# nothing. An arrival must also PERSIST to count.
|
|
a_ = [(o, prev.get(o, 0), cur[o]) for o in cur if cur[o] > prev.get(o, 0)]
|
|
l_ = [(o, prev[o], cur.get(o, 0)) for o in prev if cur.get(o, 0) < prev[o]]
|
|
for o, x, y in a_:
|
|
if x == 0: pending[o] = pending.get(o, 0) + 1
|
|
for o in list(pending):
|
|
if cur.get(o, 0) == 0: pending.pop(o, None) # vanished: flicker
|
|
elif pending[o] == 2:
|
|
confirmed += 1
|
|
print(' *** CONFIRMED ARRIVAL %-26s now %d (persisted 2 samples)'
|
|
% (label.get(o, '?'), cur[o]), flush=True)
|
|
pending[o] = 3
|
|
arr += sum(1 for x in a_ if x[1] == 0); los += len(l_)
|
|
print('t=%4ds deployed=%d up=%d down=%d (cum up %d / down %d, confirmed %d)%s'
|
|
% (el, len(cur), len(a_), len(l_), arr, los, confirmed, st), flush=True)
|
|
for o, x, y in a_:
|
|
print(' up %-30s %d -> %d%s' % (label.get(o, '?'), x, y,
|
|
' <- candidate arrival' if x == 0 else ''), flush=True)
|
|
for o, x, y in l_: print(' loss %-30s %d -> %d' % (label.get(o, '?'), x, y), flush=True)
|
|
prev = cur
|
|
print('\nTOTAL candidate-up=%d down=%d CONFIRMED arrivals=%d stalled samples=%d'
|
|
% (arr, los, confirmed, stalls))
|
|
return 0
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|