joeylib2/tools/uber-perf-table

176 lines
6 KiB
Python
Executable file

#!/usr/bin/env python3
"""Render a cross-port UBER perf table in Markdown.
Reads each port's joeylog.txt (the file UBER appends ops/sec lines to)
and emits a Markdown table comparing all ports against a reference.
The reference column shows absolute ops/sec; every other port shows a
ratio of (port / reference) so a glance tells you whether each target
is at, above, or below the reference.
Usage:
tools/uber-perf-table [--ref iigs] [--out PATH] [--repo PATH]
Defaults pull the 4 standard log paths under the repo's build/ tree;
pass --repo if running from elsewhere. The reference port (default
IIgs, which DESIGN.md calls the perf floor) defines the ratio
denominator. Ports whose log file is missing are listed in the table
header as "(no log)" and their columns are filled with "-".
Example, run from the repo root:
tools/uber-perf-table > PERF.md
To compare against a different reference (e.g. profile DOS as the
floor for a DOS-only project):
tools/uber-perf-table --ref dos
"""
import argparse
import os
import re
import sys
LINE_RE = re.compile(
r"UBER:\s+(?P<op>[^:]+):\s+\d+\s+iters\s+/\s+\d+\s+frames\s+=\s+(?P<ops>\d+)\s+ops/sec"
)
# Per-port log path relative to the repo root, in the column order the
# table renders. Reference port appears first.
PORTS = [
("iigs", "IIgs", "build/iigs/bin/joeylog.txt"),
("amiga", "Amiga", "build/amiga/bin/joeylog.txt"),
("atarist", "Atari ST", "build/atarist/bin/joeylog.txt"),
("dos", "DOS", "build/dos/bin/JOEYLOG.TXT"),
]
# Appended below the table when IIgs is the reference port. The IIgs is
# a 2.8 MHz 65816 moving a 32000-byte SHR screen, so any full-screen
# byte-move op has a hard throughput ceiling; numbers above it are a
# measurement artifact, not real speed.
IIGS_CEILING_NOTE = (
"\n"
"> **IIgs full-screen ceiling (~29 ops/sec).** A full-screen op moves\n"
"> 32000 bytes = 16000 word-stores x 6 cyc (STA long,X / PEI, both\n"
"> 3 cyc/byte) / 2.8 MHz ~= 34 ms ~= 29 ops/sec maximum. So\n"
"> `jlSurfaceClear`, `jlFillRect 320x200`, and `jlStagePresent full`\n"
"> cannot legitimately exceed ~29; a higher reading means the benchmark\n"
"> clock was perturbed. The clock is `jlFrameCount` = the VBL-interrupt-\n"
"> driven GetTick, so an op that holds `SEI` while it runs can over-\n"
"> report its ops/sec. Treat full-screen IIgs numbers above ~29 as\n"
"> inflated and cross-check against the cycle model.\n"
)
def parse_log(path):
"""Return {op: ops/sec} from a UBER log. Last entry per op wins."""
perf = {}
with open(path) as f:
for line in f:
m = LINE_RE.search(line)
if m:
perf[m.group("op").strip()] = int(m.group("ops"))
return perf
def format_ratio(port_ops, ref_ops):
if port_ops is None or ref_ops is None or ref_ops == 0:
return "-"
ratio = port_ops / ref_ops
cell = f"{ratio:.2f}x"
# Bold sub-parity ratios. DESIGN.md treats the reference port as
# the perf floor; ratios below 1.00 are the cells worth attention.
if ratio < 1.0:
cell = f"**{cell}**"
return cell
def render_table(port_data, ops_in_order, ref_key):
headers = ["Op"]
for key, label, _ in PORTS:
if not port_data[key]["loaded"]:
label = f"{label} (no log)"
if key == ref_key:
headers.append(f"{label} (ops/sec)")
else:
headers.append(f"{label} (vs {ref_key.upper()})")
rows = [headers, ["---"] * len(headers)]
for op in ops_in_order:
row = [op]
ref_ops = port_data[ref_key]["perf"].get(op)
for key, _, _ in PORTS:
ops = port_data[key]["perf"].get(op)
if key == ref_key:
row.append(str(ops) if ops is not None else "-")
else:
row.append(format_ratio(ops, ref_ops))
rows.append(row)
return "\n".join("| " + " | ".join(r) + " |" for r in rows)
def main(argv):
parser = argparse.ArgumentParser(
description="Render a cross-port UBER perf table in Markdown."
)
parser.add_argument("--ref", default="iigs",
choices=[p[0] for p in PORTS],
help="Reference port (default: iigs)")
parser.add_argument("--out", default=None,
help="Write table to this file instead of stdout")
parser.add_argument("--repo", default=None,
help="Repo root (default: parent of this script)")
args = parser.parse_args(argv[1:])
if args.repo is None:
args.repo = os.path.dirname(os.path.dirname(os.path.abspath(argv[0])))
port_data = {}
union_ops = []
seen = set()
for key, _, rel in PORTS:
path = os.path.join(args.repo, rel)
try:
perf = parse_log(path)
loaded = True
except OSError:
perf = {}
loaded = False
port_data[key] = {"perf": perf, "loaded": loaded, "path": path}
for op in perf:
if op not in seen:
seen.add(op)
union_ops.append(op)
if not port_data[args.ref]["loaded"]:
sys.stderr.write(
f"error: reference log missing: {port_data[args.ref]['path']}\n"
f" run UBER on {args.ref} first, then retry.\n"
)
return 2
if not union_ops:
sys.stderr.write("error: no UBER ops parsed from any log\n")
return 2
# Use the reference port's op order so the table matches UBER's
# natural sequence (surfaceClear first, joeyAudioIsPlayingMod last).
ref_ops = list(port_data[args.ref]["perf"].keys())
extras = [op for op in union_ops if op not in port_data[args.ref]["perf"]]
ordered = ref_ops + extras
table = render_table(port_data, ordered, args.ref)
if args.ref == "iigs":
table += "\n" + IIGS_CEILING_NOTE
if args.out:
with open(args.out, "w") as f:
f.write(table + "\n")
else:
sys.stdout.write(table + "\n")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))