#!/usr/bin/env python3 # testCost.py - what one pass of commNmiHandler actually costs, in cycles, measured. # # python3 testCost.py [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()