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

163 lines
7.6 KiB
Python

#!/usr/bin/env python3
# testCost.py - what one pass of commNmiHandler actually costs, in cycles, measured.
#
# python3 testCost.py <disk.d64> <rate> [tag] [captureSeconds]
#
# The handler's cost does not depend on the line rate - it is the same code either way - so it can be
# measured on a link that is healthy and busy and then compared with the character period at any
# rate. 19200 is the right place to measure it: heavy traffic in both directions, and (per
# highRateReport.md and testStack.py) no wedge.
#
# Five tracepoints, all inside the handler, so every entry->exit pair can be classified:
# $E685 the first instruction of the handler - entry, with SP and the cycle counter
# $E699 sta aciaRxByte - this pass took a character off the chip
# $E6C9 dec uartPendingCount - this pass counted a transmitted byte off
# $E6D6 jsr aciaPutData - this pass handed the chip a new byte
# $E6E9 rti / $E67F jmp (nmiChainVector) - the two exits
# The cycle stamp VICE prints is taken before the traced instruction executes, so an entry->exit span
# excludes the 7 cycles the CPU spends taking the NMI and the 6 the RTI costs; both are added back
# where the report quotes a whole interrupt.
import os
import sys
import time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from viceHarness import ViceSession, aciaArgs, SCRATCH
from testTwoMachines import NullModemRelay, pickModemOpponent, answerModemPrompts
from testRealtime import (BAUD_INDEX, LOGS, NTSC_HZ, SHOTS, command, decodeControl, pauseAll,
resumeAll, snapshot, showState, waitForPhase)
from testHighRates import moduleResident
from testWedge import TRACE_LINE, captureTrace
ENTRY = 0xE685
RX_BYTE = 0xE699
TX_DEC = 0xE6C9
TX_PUT = 0xE6D6
EXITS = (0xE6E9, 0xE67F)
BITS_PER_CHAR = 10 # 8N1
def classify(body, span, tag):
events = [(int(pc, 16), int(sp, 16), int(cycle)) for pc, sp, cycle in TRACE_LINE.findall(body)]
passes = []
current = None
for pc, sp, cycle in events:
if pc == ENTRY:
if current is not None:
current["nested"] = True
current = {"start": cycle, "sp": sp, "rx": False, "tx": False, "put": False,
"nested": False}
elif current is None:
continue
elif pc == RX_BYTE:
current["rx"] = True
elif pc == TX_DEC:
current["tx"] = True
elif pc == TX_PUT:
current["put"] = True
elif pc in EXITS:
current["cycles"] = cycle - current["start"]
passes.append(current)
current = None
groups = {}
for row in passes:
if "cycles" not in row:
continue
name = ("receive+transmit" if row["rx"] and row["put"] else
"receive only" if row["rx"] else
"transmit only" if row["put"] else
"transmit, ring ran dry" if row["tx"] else "neither half had work")
groups.setdefault(name, []).append(row["cycles"])
cycles = span[1] - span[0]
print(f"\n ---- one pass of commNmiHandler [{tag}] ----", flush=True)
print(f" window {cycles} cycles = {cycles / NTSC_HZ:.3f} emulated s, "
f"{len(passes)} complete passes = {len(passes) / (cycles / NTSC_HZ):.0f}/s", flush=True)
out = {}
for name, values in sorted(groups.items(), key=lambda item: -len(item[1])):
values.sort()
whole = [value + 13 for value in values] # + 7 to take the NMI, + 6 for the RTI
print(f" {name}: {len(values)} passes, trace-to-trace min {values[0]} median "
f"{values[len(values) // 2]} max {values[-1]} cycles; whole interrupt "
f"{whole[len(whole) // 2]} cycles", flush=True)
out[name] = {"count": len(values), "min": values[0], "median": values[len(values) // 2],
"max": values[-1], "wholeInterrupt": whole[len(whole) // 2]}
busy = sum(row["cycles"] for row in passes if "cycles" in row) + 13 * len(passes)
print(f" duty cycle in this window: {100.0 * busy / cycles:.1f}%", flush=True)
print("\n what that costs per character time, at each rate the table offers:", flush=True)
print(f" {'rate':>6} {'char period':>12} {'one pass':>9} {'two passes':>11} "
f"{'verdict':>28}", flush=True)
full = out.get("receive+transmit", {}).get("wholeInterrupt")
one = out.get("receive only", {}).get("wholeInterrupt") or full
two = (one or 0) + (out.get("transmit only", {}).get("wholeInterrupt") or one or 0)
for rate in (300, 1200, 2400, 4800, 9600, 19200, 38400):
period = NTSC_HZ / (rate / BITS_PER_CHAR)
share = 100.0 * (two or 0) / period
verdict = "cannot keep up" if share >= 100 else f"{share:.0f}% of the CPU"
print(f" {rate:>6} {period:>11.1f}c {one:>8}c {two:>10}c {verdict:>28}", flush=True)
return out
def main():
disk = os.path.abspath(sys.argv[1])
rate = int(sys.argv[2])
tag = sys.argv[3] if len(sys.argv) > 3 else "c"
seconds = float(sys.argv[4]) if len(sys.argv) > 4 else 10.0
os.makedirs(SHOTS, exist_ok=True)
os.makedirs(LOGS, exist_ok=True)
relay = NullModemRelay()
args = aciaArgs(rsDevAddress=f"127.0.0.1:{relay.port}", baud=38400)
a = ViceSession(disk, f"{SCRATCH}/cost.{tag}.a.vice.log", args, label="A", warp=False)
b = ViceSession(disk, f"{SCRATCH}/cost.{tag}.b.vice.log", args, label="B", warp=False)
sessions = [(a, "A"), (b, "B")]
try:
a.connect()
b.connect()
for session, _ in sessions:
command(session, "warp on")
a.bootPastLoader(waitSecs=900)
b.bootPastLoader(waitSecs=900)
pauseAll(sessions)
for session, _ in sessions:
command(session, "warp off")
resumeAll(sessions)
a.findWindow()
b.findWindow()
a.focus()
b.focus()
time.sleep(25)
pickModemOpponent(a, "A", "a")
pickModemOpponent(b, "B", "o")
for session, sessionTag in sessions:
if not moduleResident(session, sessionTag):
raise SystemExit(f"{sessionTag}: the opponent module never loaded")
pauseAll([(session, sessionTag)])
command(session, f"> e055 {BAUD_INDEX[rate]:02x}")
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 state in [snapshot(session, sessionTag) for session, sessionTag in sessions]:
print(f" [{state['tag']}] control {decodeControl(state['aciaControl'])}", flush=True)
showState(state)
addresses = [ENTRY, RX_BYTE, TX_DEC, TX_PUT] + list(EXITS)
results = {}
for round in range(2):
for session, sessionTag in sessions:
body, span = captureTrace(session, sessionTag, addresses, seconds, 8 << 20,
f"{LOGS}/cost.{tag}.{sessionTag}.{round}.trace.txt")
results[f"{sessionTag}{round}"] = classify(body, span, f"{sessionTag} round {round}")
print("\n================ SUMMARY ================", flush=True)
for key, value in results.items():
print(f" {key}: {value}", flush=True)
finally:
a.close()
b.close()
time.sleep(1)
relay.stop()
for index in (0, 1):
print(f"[relay] conn{index} total {len(relay.log[index])} bytes", flush=True)
if __name__ == "__main__":
main()