163 lines
7.6 KiB
Python
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()
|