#!/usr/bin/env python3 # testGuard.py - is the NMI re-entrancy guard actually being used, and how often? # # python3 testGuard.py [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())