modemwars/swiftlink/testGuard.py
2026-08-23 02:09:40 -05:00

129 lines
6.3 KiB
Python

#!/usr/bin/env python3
# testGuard.py - is the NMI re-entrancy guard actually being used, and how often?
#
# python3 testGuard.py <disk.d64> <rate> [tag] [windowSeconds]
#
# The guard is only interesting if second NMIs really do arrive while a pass of commNmiHandler is
# running. Two tracepoints answer that with the emulator's own counters:
#
# trace exec e685 - every entry to the handler
# trace exec e6f0 - nmiLeaveNote: the INC that a nested entry leaves behind instead of running
# the handler body
#
# Both are armed for a short window on a live 38400-baud link and the lines are counted, so the
# result is "N entries, M of them nested", measured rather than argued. A trace does not stop the
# machine, but it does put a line on the monitor socket per hit, so the window is deliberately short.
import os
import re
import sys
import time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from viceHarness import ViceSession, aciaArgs, readByte, ACIA_BASE, SCRATCH
from testTwoMachines import NullModemRelay, pickModemOpponent, answerModemPrompts
from testRealtime import (BAUD_INDEX, LOGS, NTSC_HZ, command, flush, pauseAll, resumeAll,
stopwatch, waitForPhase)
from testHighRates import moduleResident
from testStack import collect
ENTRY = 0xE685
NOTE = 0xE6F0 # nmiLeaveNote - keep in step with the source
def main():
disk = os.path.abspath(sys.argv[1])
rate = int(sys.argv[2])
tag = sys.argv[3] if len(sys.argv) > 3 else "g"
window = float(sys.argv[4]) if len(sys.argv) > 4 else 5.0
os.makedirs(LOGS, exist_ok=True)
relay = NullModemRelay()
a = ViceSession(disk, os.path.join(LOGS, f"vice.{tag}.A.log"),
aciaArgs(rsDevAddress=f"127.0.0.1:{relay.port}"), label="A", warp=False)
b = ViceSession(disk, os.path.join(LOGS, f"vice.{tag}.B.log"),
aciaArgs(rsDevAddress=f"127.0.0.1:{relay.port}", dev=1), label="B", warp=False)
sessions = [(a, "A"), (b, "B")]
try:
for session, sessionTag in sessions:
session.connect()
session.mon("warp on")
for session, sessionTag in sessions:
session.bootPastLoader()
session.findWindow()
session.focus()
# Warp off the way testHighRates.py does it - with both emulators stopped, and read back,
# because a monitor command sent to a RUNNING emulator is consumed by the break-in and the
# machine can be left in warp. The first version of this script did that, and its trace
# covered 62 emulated seconds inside an eight-second read window.
pauseAll(sessions)
for session, sessionTag in sessions:
command(session, "warp off")
print(f"[{sessionTag}] {command(session, 'warp').strip()}", flush=True)
resumeAll(sessions)
for session, sessionTag in sessions:
pickModemOpponent(session, sessionTag, "a")
time.sleep(2)
for session, sessionTag in sessions:
if not moduleResident(session, sessionTag):
raise SystemExit(f"{sessionTag}: the opponent module never loaded")
pauseAll([(session, sessionTag)])
if rate:
command(session, f"> e055 {BAUD_INDEX[rate]:02x}")
else:
# rate 0 means "touch nothing": leave baudIndex at the disk value, which is adopt, and
# let the driver take the line settings off the chip itself. On this rig that chip
# reads $00, so what is really being tested is the fallback and the default path
# together - the way an ordinary boot with nothing configured comes up.
print(f"[{sessionTag}] adopt mode: baudIndex left at the disk default", flush=True)
command(session, "del")
resumeAll([(session, sessionTag)])
answerModemPrompts(a, "A", "a")
answerModemPrompts(b, "B", "o")
ok, phases = waitForPhase(sessions, 3, 180)
print(f"both in the packet phase: {ok} ({phases})", flush=True)
for session, sessionTag in sessions:
pauseAll([(session, sessionTag)])
command(session, "del")
command(session, f"trace exec {ENTRY:04x}")
command(session, f"trace exec {NOTE:04x}")
resumeAll([(session, sessionTag)])
# The window is bracketed by both machines' own free-running cycle counters, so the run
# says for itself whether it was at true C64 speed.
pauseAll(sessions)
startCycles = {sessionTag: stopwatch(session) for session, sessionTag in sessions}
resumeAll(sessions)
t0 = time.time()
bodies, broke = collect(sessions, window, 16 << 20, tag)
wall = time.time() - t0
pauseAll(sessions)
endCycles = {sessionTag: stopwatch(session) for session, sessionTag in sessions}
resumeAll(sessions)
for sessionTag in startCycles:
span = endCycles[sessionTag] - startCycles[sessionTag]
print(f"[{sessionTag}] window {wall:.2f} s wall, {span} emulated cycles = "
f"{span / NTSC_HZ:.2f} emulated s = {100.0 * span / NTSC_HZ / wall:.1f}% of real "
f"time", flush=True)
for session, sessionTag in sessions:
pauseAll([(session, sessionTag)])
command(session, "del")
state = readByte(session, 0xE5BD)
phase = readByte(session, 0xE040)
errs = readByte(session, 0xE047)
resumeAll([(session, sessionTag)])
body = bodies[sessionTag]
entries = len(re.findall(r"C:\$?%04x" % ENTRY, body))
notes = len(re.findall(r"C:\$?%04x" % NOTE, body))
control = readByte(session, ACIA_BASE + 3)
index = readByte(session, 0xE055)
print(f"[{sessionTag}] baudIndex=${index:02X} control=${control:02X}", flush=True)
print(f"[{sessionTag}] {entries} handler entries, {notes} of them nested "
f"({(100.0 * notes / entries) if entries else 0:.1f}%) in {window:.0f} s; "
f"nmiHandlerState=${state:02X} connectionPhase={phase} linkErrorCount={errs}",
flush=True)
finally:
for session, _ in sessions:
session.close()
relay.stop()
return 0
sys.exit(main())