|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""BACKEND=uci read pacing: a uci_fence after every UCI register WRITE, |
| 3 | +none after a READ (src/net/uci/uci_regs.inc, uci_read_fence). |
| 4 | +
|
| 5 | +Runs the SHIPPED machine code (build/c64-https.prg at build/labels.txt's |
| 6 | +addresses) on test_uci_abort_recovery.py's interpreter and Command |
| 7 | +Interface model, through net_init, a connect, data / idle / EOF / reset |
| 8 | +polls, a send and a close, and logs every access to $DF1C-$DF1F with the |
| 9 | +uci_fence executions that follow it before the next access. A fence is |
| 10 | +recognised by its bytes as assembled (that module's FENCE), whether it is |
| 11 | +inline or inside uci_settle. |
| 12 | +
|
| 13 | + every image each WRITE to $DF1C/$DF1D is followed by a fence before |
| 14 | + the next UCI access. |
| 15 | + default no READ is followed by a fence, except the one kept before |
| 16 | + net_poll's header read (label uci_poll_stage_fence). |
| 17 | + UCI_READ_FENCE=1 (flags.stamp) |
| 18 | + every READ is followed by a fence (the previous products). |
| 19 | +
|
| 20 | +What it does not see: a delay that is not a uci_fence (a NOP sled, a |
| 21 | +loop), the trust store's uci_dos.s (not on these flows; still fenced), |
| 22 | +and anything about the FPGA's real timing -- the hardware A/B in the PR |
| 23 | +is that evidence. |
| 24 | +
|
| 25 | + make BACKEND=uci ... && python3 tools/check_uci_read_pacing.py [--expect default|fenced] |
| 26 | +
|
| 27 | +--expect overrides what flags.stamp says, to show the check red on the |
| 28 | +other image. Standalone, like tools/check_m3_client.py. Exit 0 pass, |
| 29 | +1 fail, 2 cannot run. |
| 30 | +""" |
| 31 | +from __future__ import annotations |
| 32 | + |
| 33 | +import sys |
| 34 | +from pathlib import Path |
| 35 | + |
| 36 | +sys.path.insert(0, str(Path(__file__).resolve().parent)) |
| 37 | +import test_uci_abort_recovery as A # noqa: E402 |
| 38 | +import test_uci_data_acc as base # noqa: E402 |
| 39 | + |
| 40 | +UCI_REGS = range(0xDF1C, 0xDF20) |
| 41 | + |
| 42 | + |
| 43 | +class Log: |
| 44 | + def __init__(self): |
| 45 | + self.events = [] # [kind, addr, [fence pcs after it]] |
| 46 | + |
| 47 | + def access(self, kind, addr): |
| 48 | + if addr in UCI_REGS: |
| 49 | + self.events.append([kind, addr, []]) |
| 50 | + |
| 51 | + def fence(self, pc): |
| 52 | + if self.events: |
| 53 | + self.events[-1][2].append(pc) |
| 54 | + |
| 55 | + |
| 56 | +def _traced(model, log): |
| 57 | + read, write = model.read, model.write |
| 58 | + |
| 59 | + def r(addr): |
| 60 | + log.access("r", addr) |
| 61 | + return read(addr) |
| 62 | + |
| 63 | + def w(addr, value): |
| 64 | + log.access("w", addr) |
| 65 | + return write(addr, value) |
| 66 | + model.read, model.write = r, w |
| 67 | + return model |
| 68 | + |
| 69 | + |
| 70 | +class PacingCPU(A.FastCPU): |
| 71 | + log = None |
| 72 | + |
| 73 | + def step(self): |
| 74 | + pc = self.pc |
| 75 | + ram = self.mem.ram |
| 76 | + if ram[pc] == 0x48 and ram[pc:pc + len(A.FENCE)] == A.FENCE: |
| 77 | + self.log.fence(pc) |
| 78 | + super().step() |
| 79 | + |
| 80 | + |
| 81 | +def _machine(replies, log): |
| 82 | + model = _traced(A.AbortModel(replies), log) |
| 83 | + cpu, mem, labels = A._require(model) |
| 84 | + pcpu = PacingCPU(mem) |
| 85 | + pcpu.log = log |
| 86 | + return pcpu, mem, labels, model |
| 87 | + |
| 88 | + |
| 89 | +def flows(): |
| 90 | + """(name, Log) for each traced session.""" |
| 91 | + out = [] |
| 92 | + log = Log() |
| 93 | + cpu, mem, labels, uci = _machine([A.CONNECT_OK], log) |
| 94 | + cpu.call(labels["net_init"]) |
| 95 | + A._assert_connected(cpu, mem, uci, labels, "pacing: connect") |
| 96 | + out.append(("init+connect", log)) |
| 97 | + for name, reply in (("poll data", (b"\x40\x00" + bytes(range(64)), A.OK)), |
| 98 | + ("poll idle", (b"\xff\xff", b"02,NO DATA: 11")), |
| 99 | + ("poll EOF", (b"\x00\x00", b"01,CONNECTION CLOSED BY HOST")), |
| 100 | + ("poll reset", (b"\xff\xff", b"02,NO DATA: 104"))): |
| 101 | + log = Log() |
| 102 | + cpu, mem, labels, uci = _machine([reply], log) |
| 103 | + mem.write(labels["uci_socket_id"], 1) |
| 104 | + mem.write(labels["net_tcp_state"], A.NET_TCP_CONNECTED) |
| 105 | + cpu.call(labels["net_poll"], budget=8_000_000) |
| 106 | + out.append((name, log)) |
| 107 | + log = Log() |
| 108 | + cpu, mem, labels, uci = _machine([(b"\x3a\x00", A.OK), (b"", A.OK)], log) |
| 109 | + mem.write(labels["uci_socket_id"], 1) |
| 110 | + mem.write(labels["net_tcp_state"], A.NET_TCP_CONNECTED) |
| 111 | + mem.write(labels["net_send_len"], 58) |
| 112 | + mem.write(labels["net_send_len"] + 1, 0) |
| 113 | + cpu.a, cpu.x = 0x00, 0x04 |
| 114 | + cpu.call(labels["net_tcp_send"], budget=4_000_000) |
| 115 | + cpu.call(labels["net_tcp_close"], budget=4_000_000) |
| 116 | + out.append(("send+close", log)) |
| 117 | + return out, labels |
| 118 | + |
| 119 | + |
| 120 | +def _fenced_image(): |
| 121 | + stamp = base.LABELS.parent / "flags.stamp" |
| 122 | + for line in stamp.read_text().splitlines(): |
| 123 | + if line.startswith("CA65FLAGS="): |
| 124 | + return "-D UCI_READ_FENCE" in line |
| 125 | + raise A.Unavailable("%s has no CA65FLAGS line" % stamp) |
| 126 | + |
| 127 | + |
| 128 | +def main(argv) -> int: |
| 129 | + expect = None |
| 130 | + if len(argv) == 3 and argv[1] == "--expect" and argv[2] in ("default", "fenced"): |
| 131 | + expect = argv[2] == "fenced" |
| 132 | + elif len(argv) != 1: |
| 133 | + print(__doc__) |
| 134 | + return 2 |
| 135 | + try: |
| 136 | + fenced = _fenced_image() if expect is None else expect |
| 137 | + traces, labels = flows() |
| 138 | + except (A.Unavailable, FileNotFoundError) as exc: |
| 139 | + print("COULD NOT RUN: %s" % exc) |
| 140 | + return 2 |
| 141 | + keep = labels.get("uci_poll_stage_fence") |
| 142 | + if keep is None: |
| 143 | + print("COULD NOT RUN: uci_poll_stage_fence is not in labels.txt") |
| 144 | + return 2 |
| 145 | + fails = 0 |
| 146 | + for name, log in traces: |
| 147 | + ev = log.events |
| 148 | + writes = [e for e in ev[:-1] if e[0] == "w"] |
| 149 | + reads = [e for e in ev[:-1] if e[0] == "r"] |
| 150 | + bare = [e for e in writes if not e[2]] |
| 151 | + if fenced: |
| 152 | + bad = [e for e in reads if not e[2]] |
| 153 | + what = "unfenced read(s) in an UCI_READ_FENCE=1 image" |
| 154 | + else: |
| 155 | + bad = [e for e in reads if any(pc != keep for pc in e[2])] |
| 156 | + what = "fenced read(s) in the default image" |
| 157 | + ok = len(writes) > 3 and not bare and not bad |
| 158 | + fails += not ok |
| 159 | + print("%s %-13s %3d writes, %4d reads; %s%s" % ( |
| 160 | + "PASS" if ok else "FAIL", name, len(writes), len(reads), |
| 161 | + "%d write(s) with no fence after" % len(bare) if bare else "writes fenced", |
| 162 | + "; %d %s, first $%04X" % (len(bad), what, bad[0][1]) if bad else "")) |
| 163 | + print("\n%d/%d flows pass (%s image)" % (len(traces) - fails, len(traces), |
| 164 | + "fenced" if fenced else "default")) |
| 165 | + return 1 if fails else 0 |
| 166 | + |
| 167 | + |
| 168 | +if __name__ == "__main__": |
| 169 | + sys.exit(main(sys.argv)) |
0 commit comments