151 lines
4.8 KiB
Python
Executable file
151 lines
4.8 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"),
|
|
]
|
|
|
|
|
|
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
|
|
return f"{ratio:.2f}x"
|
|
|
|
|
|
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.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))
|