Files
psxrecomp/tools/cycle_testrom/measure.py
Matthew Stanley 47bcfecb21 feat(timing): ruler #2 Beetle half live — synthetic boot disc + oracle component costs
The test ROM now boots on a real BIOS. The license region (required or the BIOS
drops to its shell) is extracted from a disc the developer OWNS via the in-tree
dumpsxiso and embedded by mkpsxiso — LOCAL ONLY, gitignored, never redistributed.
Both backends boot disc/cyctest.cue identically (real BIOS -> SYSTEM.CNF -> EXE),
so CPU/cache state matches.

- disc/cyctest.xml + disc/SYSTEM.CNF: mkpsxiso project (tracked). license_data.dat
  + built .bin/.cue gitignored (copyrighted Sony data, local only).
- measure.py: per-anchor consecutive-hit Δ = per-iteration cycle cost; reports
  per-component cost via baseline subtraction.
- README: boot-disc build + license-extraction recipe + the oracle results.

Beetle ORACLE component costs (the HW targets for the cost model):
  baseline 3 | alu +1 | load +5 | load2 +11 (2nd load +1 = ReadFudge)
  | div +38 (~36 stall) | div_spaced +38 (fillers ABSORBED) | mult +15 (~13 stall)
Native currently charges 0 for div/mult stall — the dominant gaps. div_spaced==div
proves the stall must be a completion-timestamp + stall-on-mflo-read (absorbable),
NOT a flat charge on the divu.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:52:25 -07:00

78 lines
3.1 KiB
Python

#!/usr/bin/env python3
"""measure.py — per-iteration cycle delta for each test-ROM anchor on one backend.
Arms cyc_watch single-anchor on each loop top, dumps, and reports the modal
consecutive-hit delta = one iteration's cycle cost. Run against Beetle (4382,
the oracle) and native (the cost model) and compare.
Usage: python measure.py [--port 4382] [--json cycle_testrom.exe.anchors.json]
"""
import argparse, collections, json, socket, sys, time
def query(port, cmd, host="127.0.0.1", timeout=10.0):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(timeout); s.connect((host, port))
try:
s.sendall((json.dumps(cmd) + "\n").encode())
buf = bytearray(); depth = 0; instr = False; esc = False; started = False
while True:
ch = s.recv(1 << 20)
if not ch: break
buf.extend(ch)
i = 0
while i < len(buf):
c = buf[i]
if instr:
if esc: esc = False
elif c == 0x5C: esc = True
elif c == 0x22: instr = False
elif c == 0x22: instr = True
elif c == 0x7B: depth += 1; started = True
elif c == 0x7D:
depth -= 1
if started and depth == 0:
return json.loads(buf[:i+1].decode())
i += 1
finally:
s.close()
return json.loads(bytes(buf).decode())
def modal_delta(entries):
cy = [int(e["cycles"]) for e in entries]
ds = [cy[i+1]-cy[i] for i in range(len(cy)-1)]
if not ds: return None, ds
return collections.Counter(ds).most_common(1)[0][0], ds
def main():
p = argparse.ArgumentParser()
p.add_argument("--port", type=int, default=4382)
p.add_argument("--json", default="cycle_testrom.exe.anchors.json")
p.add_argument("--hits", type=int, default=8)
p.add_argument("--wait", type=float, default=1.5)
a = p.parse_args()
meta = json.load(open(a.json))
anchors = meta["anchors"]
label = "beetle" if a.port == 4382 else ("native" if a.port == 4500 else f"port{a.port}")
print(f"=== per-iteration cycle delta on {label} (port {a.port}) ===")
results = {}
base = None
for name, addr in anchors.items():
query(a.port, {"cmd": "cyc_watch", "pc": addr, "n": a.hits})
time.sleep(a.wait)
d = query(a.port, {"cmd": "cyc_watch_dump"})
md, ds = modal_delta(d.get("entries", []))
results[name] = md
if name == "baseline": base = md
comp = f" (component = {md - base:+d})" if (base is not None and md is not None and name != "baseline") else ""
print(f" {name:12s} {addr} per-iter={md}{comp} hits={d.get('hits')}")
print()
if base is not None:
print("Component costs (per-iter minus baseline):")
for name, md in results.items():
if name == "baseline" or md is None: continue
print(f" {name:12s} {md - base:+d}")
return 0
if __name__ == "__main__":
sys.exit(main())