129 lines
6.3 KiB
Python
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())
|