"""Seek slack: how long a branch point can stop delivery (STATUS 4, FINDINGS 51). 19_ring_stream.py asks whether a container ARRIVES in time, and prints one "seek slack" column derived statically as (usable ring - maxrec)/mean record. That is a capacity estimate and it quietly assumes the ring is full when the seek happens. It is not, and the difference is the whole finding: A ring's slack is ACCUMULATED, not owned. It is built out of the surplus between the pipe and the wire demand, at (pipe - wire) bytes per second, and a seek spends all of it. How long a branch point can stall is a property of the ring; how soon the NEXT branch point can be afforded is a property of the surplus, and a bigger ring makes that one WORSE. This is the paced-rig model (tools/bench/stream.lua with DLX_PACE=1) written independently, and it exists to be compared against it, not to replace it. The rig drives a real 68000 through a real ring and is the measurement; this is the cheap sweep that says where to point it. Where they disagree, the rig wins. python3 tools/analysis/20_seek_slack.py [container ...] --kbps R [R ...] [--ring KB [KB ...]] `--kbps` is REQUIRED and takes no default, for the reason FINDINGS 50 gives. """ import sys, os, argparse sys.path.insert(0, "tools/encoder") import numpy as np from dlx import DLX import ratectl as RC SECTOR = 512 def records(path): d = DLX(path) rec = np.array(d.record_lengths(), np.int64) return d, rec def paced_sim(rec, ring, fill_per_frame, ticks_per_frame=8): """Paced-decoder ring sim. Returns TWO per-tick lookahead series. THE ANSWER IS BRACKETED TO ONE RECORD AND IS NOT SHARPER THAN THAT. At these rates the pipe delivers almost exactly one record per frame slot, so "how many records are resident at slot i" depends on whether you look before or after that slot's delivery -- and the two answers differ by one, every time. Sampled after, this agreed with the rig's ceiling in 33 of 35 cells; sampled before, it was exactly one record lower in 33 of 35. Neither is wrong. Picking the one that matched would have been fitting the model to the measurement and then reporting the agreement as a cross-check, so both are returned and the caller prints the range. The rig sits at the top of it. The producer is `aligned` (19_ring_stream.py): it will not start a record it cannot finish before the end of the ring, and it will not place one over bytes the decoder still owns. The decoder consumes exactly one record per frame time and releases it whole. Sub-stepping matters. Delivery and consumption interleave inside a frame time on the rig -- the producer runs on MAME's machine-frame notifier, ~5x per 12fps slot -- and a model that delivers a whole frame's bytes at once can place a record into space the decoder has not released yet, or refuse one it has. Eight sub-steps is well past the point the answer stops moving. """ n = len(rec) live = [] # [idx, off, len] still owned by the decoder wcur, nsent, credit = 0, 0, 0.0 lo, hi, ring_ref, rate_ref = [], [], 0, 0 def overlaps(off, ln): return any(off < r[1] + r[2] and r[1] < off + ln for r in live) for i in range(n): if nsent < n: lo.append(sum(1 for r in live if r[0] >= i)) for _ in range(ticks_per_frame): credit += fill_per_frame / ticks_per_frame while nsent < n: r = int(rec[nsent]) if credit < r: rate_ref += 1 break w, hole = wcur, 0 if w + r > ring: w, hole = 0, ring - wcur if overlaps(w, r): ring_ref += 1 break # sector quantisation: a partial sector is not resident credit -= r live.append([nsent, w, r]) wcur, nsent = w + r, nsent + 1 if nsent < n: hi.append(sum(1 for r in live if r[0] >= i)) # the decoder consumed record i during the slot and releases it whole live = [r for r in live if r[0] > i] return np.array(lo), np.array(hi), ring_ref, rate_ref def main(): ap = argparse.ArgumentParser() ap.add_argument("containers", nargs="*", default=["tmp/rc_fr_singe_scsi_span.dlx"]) ap.add_argument("--kbps", type=float, nargs="+", required=True, help="delivered pipe rates, KB/s. REQUIRED, no default " "(FINDINGS 50): this project has never measured the " "delivery pipe and a default is how the last unmeasured " "one stayed load-bearing for five sessions.") ap.add_argument("--ring", type=float, nargs="+", default=[64, 96, 128, 192, 256, 384, 512]) a = ap.parse_args() FPS = 12 for path in a.containers: if not os.path.exists(path): print(f"{path}: MISSING -- skipped\n"); continue d, rec = records(path) wire = rec.mean() * FPS / 1024 + RC.AUDIO_KBPS print(f"=== {path}: {d.nframes} frames @ {d.fps}fps, mean record " f"{rec.mean()/1024:.1f} KB, wire {wire:.1f} KB/s") print(f"{'ring KB':>8} {'pipe':>8} {'ceiling':>9} {'build s':>8} " f"{'mean':>11} bound") for ring_kb in a.ring: ring = int(ring_kb * 1024) if rec.max() > ring: print(f"{ring_kb:>8.0f} maxrec {rec.max():,} does not fit") continue for kbps in a.kbps: fill = ((kbps - RC.AUDIO_KBPS) * 1024 / FPS) if kbps > 0 else 1e12 lo, hi, ring_ref, rate_ref = paced_sim(rec, ring, fill) c_lo, c_hi = int(lo.max()), int(hi.max()) build = int(np.argmax(hi >= c_hi)) if len(hi) else -1 print(f"{ring_kb:>8.0f} {kbps:>8.0f} " f"{f'{c_lo}-{c_hi}':>9} {build/FPS:>8.2f} " f"{f'{lo.mean():.1f}-{hi.mean():.1f}':>11} " f"{'ring' if ring_ref else 'rate'}") # The surplus model, stated so it can be checked against the sweep # above rather than believed: slack accrues at (pipe - wire) and a # full ring holds `ceiling` records, so a branch point costs about # ceiling*mean_record/(pipe - wire) seconds of play to earn back. print() print("Slack is accumulated, not owned. A bigger ring raises the ceiling AND") print("lengthens the climb to it: the surplus (pipe - wire) is what fills it,") print("and that is set by the encoder and the medium, not by the buffer.") if __name__ == "__main__": main()