- gen_shader_profiles.py + shader-profiles/: Iris per-pack option files resolved from each pack's profile.* lines (Iris has no profile key) - bench.py: flag rows shader_not_loaded when Iris falls back to vanilla rendering (analyze.py treats it as invalid) - matrix: shader_options session key, trial jar globs pinned to the downloaded Modrinth builds, post-join settle 45 s (B0 v1 showed rep 1 still warming at 10 s)
623 lines
25 KiB
Python
623 lines
25 KiB
Python
#!/usr/bin/env python3
|
|
"""Run the BENCH-MATRIX blocks declared in matrix.toml through `bench.py session`.
|
|
|
|
python3 matrix.py list # blocks, session counts, est. time
|
|
python3 matrix.py plan B3 [--param k=v] # the bench.py commands a block runs
|
|
python3 matrix.py run B0,B1 [--dry-run] # sequential, resumable
|
|
python3 matrix.py status # progress for the current mod set
|
|
|
|
Resume: ~/onion-bench/results/matrix-state.json records each session (key
|
|
"<block>#<index>") with a hash of its exact command and the instance mods/ hash; a
|
|
session is skipped only when both still match. A failed session (non-zero exit: crash,
|
|
timeout, no join) is retried once, then the run stops. Running any block other than B0
|
|
warns loudly when B0 was not run under the current mod set. Stdlib only.
|
|
"""
|
|
import argparse
|
|
import glob
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
import shlex
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import tomllib
|
|
from pathlib import Path
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
MATRIX = HERE / "matrix.toml"
|
|
OUT_DIR = HERE.parent / "proposed-configs" / "out"
|
|
GAME = Path.home() / "onion-bench" / "prism" / "instances" / "OnionBench" / ".minecraft"
|
|
STATE = Path.home() / "onion-bench" / "results" / "matrix-state.json"
|
|
|
|
SCENE_ORDER = ["s1", "s2", "s2s", "s3", "s4", "s5"]
|
|
S5_SECONDS = 10 + 120 # bench.py: at S1, wait 10 s, then the 120 s tour (all captured)
|
|
JVM_SETS = {"J0", "J1", "J2", "J3"}
|
|
DISPLAYS = {"desktop", "gamescope", "gamescope-headless"}
|
|
PARAM_RE = re.compile(r"\$\{(\w+)\}")
|
|
|
|
TOP_KEYS = {"defaults", "worlds", "trials", "missing_variants", "block"}
|
|
DEFAULT_KEYS = {"reps", "warmup", "capture", "settle", "display", "res", "world", "shader", "dh",
|
|
"launch_overhead_s"}
|
|
BLOCK_KEYS = {"id", "title", "rule", "proxy_note", "manual_checks", "sessions", "params",
|
|
"deferred", "manual", "interleave"}
|
|
SESSION_KEYS = {"variant", "plan", "reps", "repeat", "tag", "jvm", "heap", "shader",
|
|
"shader_profile", "shader_options", "dh", "disable_mods", "add_mods", "world", "warmup",
|
|
"capture", "settle", "manual", "note", "display", "res"}
|
|
MISSING_KEYS = {"id", "blocks", "contents"}
|
|
# Flags `bench.py session` accepts; bench_argv must emit only these.
|
|
BENCH_SESSION_FLAGS = {
|
|
"--variant", "--plan", "--reps", "--warmup", "--capture", "--settle", "--label", "--jvm",
|
|
"--heap", "--boot-timeout", "--timeout", "--any-order", "--kill-stale", "--dry-run",
|
|
"--display", "--res", "--shader", "--shader-options", "--dh", "--disable-mods",
|
|
"--add-mods", "--world-zip",
|
|
}
|
|
|
|
|
|
class MatrixError(Exception):
|
|
pass
|
|
|
|
|
|
def log(msg):
|
|
print(f"[matrix] {msg}", flush=True)
|
|
|
|
|
|
# ------------------------------------------------------------------ loading / validation
|
|
|
|
def load(path=MATRIX, out_dir=OUT_DIR):
|
|
"""Parse + validate matrix.toml. Raises MatrixError listing every problem."""
|
|
with open(path, "rb") as f:
|
|
m = tomllib.load(f)
|
|
errors = validate(m, out_dir)
|
|
if errors:
|
|
raise MatrixError("matrix.toml invalid:\n " + "\n ".join(errors))
|
|
return m
|
|
|
|
|
|
def split_plan(plan):
|
|
if isinstance(plan, str):
|
|
return [s.strip() for s in plan.split(",") if s.strip()]
|
|
return list(plan)
|
|
|
|
|
|
def _pos_int(v):
|
|
return isinstance(v, int) and not isinstance(v, bool) and v > 0
|
|
|
|
|
|
def validate(m, out_dir=OUT_DIR):
|
|
err = []
|
|
for k in m:
|
|
if k not in TOP_KEYS:
|
|
err.append(f"unknown top-level key {k!r}")
|
|
d = m.get("defaults", {})
|
|
for k in d:
|
|
if k not in DEFAULT_KEYS:
|
|
err.append(f"defaults: unknown key {k!r}")
|
|
worlds, trials = m.get("worlds", {}), m.get("trials", {})
|
|
missing_ids = set()
|
|
for mv in m.get("missing_variants", []):
|
|
for k in mv:
|
|
if k not in MISSING_KEYS:
|
|
err.append(f"missing_variants {mv.get('id')}: unknown key {k!r}")
|
|
if not mv.get("id") or not mv.get("contents"):
|
|
err.append("missing_variants entry needs id and contents")
|
|
missing_ids.add(mv.get("id"))
|
|
_check_common(d, "defaults", worlds, trials, err)
|
|
|
|
seen = set()
|
|
for b in m.get("block", []):
|
|
bid = b.get("id")
|
|
where = f"block {bid}"
|
|
if not bid:
|
|
err.append("block without id")
|
|
continue
|
|
if bid in seen:
|
|
err.append(f"{where}: duplicate id")
|
|
seen.add(bid)
|
|
for k in b:
|
|
if k not in BLOCK_KEYS:
|
|
err.append(f"{where}: unknown key {k!r}")
|
|
if not b.get("rule"):
|
|
err.append(f"{where}: missing decision rule")
|
|
params = set(b.get("params", []))
|
|
sessions = b.get("sessions", [])
|
|
if not sessions and not (b.get("manual") or b.get("deferred")):
|
|
err.append(f"{where}: no sessions (mark manual or deferred)")
|
|
groups = set()
|
|
for i, s in enumerate(sessions):
|
|
sw = f"{where} session {i}"
|
|
for k in s:
|
|
if k not in SESSION_KEYS:
|
|
err.append(f"{sw}: unknown key {k!r}")
|
|
for k in ("variant", "jvm", "plan"):
|
|
v = s.get(k)
|
|
for p in PARAM_RE.findall(v if isinstance(v, str) else ",".join(v or [])):
|
|
if p not in params:
|
|
err.append(f"{sw}: ${{{p}}} not declared in params")
|
|
var = s.get("variant")
|
|
if not var:
|
|
err.append(f"{sw}: missing variant")
|
|
elif not PARAM_RE.search(var) and not (Path(out_dir) / var).is_dir() \
|
|
and var not in missing_ids:
|
|
err.append(f"{sw}: variant {var!r} not in {out_dir} nor in missing_variants")
|
|
plan = split_plan(s.get("plan", []))
|
|
if not plan:
|
|
err.append(f"{sw}: empty plan")
|
|
concrete = [p for p in plan if not PARAM_RE.search(p)]
|
|
for p in concrete:
|
|
if p not in SCENE_ORDER:
|
|
err.append(f"{sw}: unknown scene {p!r} (S6 is manual-only)")
|
|
idx = [SCENE_ORDER.index(p) for p in concrete if p in SCENE_ORDER]
|
|
if idx != sorted(set(idx)):
|
|
err.append(f"{sw}: plan must be in S1->S5 order without duplicates")
|
|
if "repeat" in s and not _pos_int(s["repeat"]):
|
|
err.append(f"{sw}: repeat must be a positive int")
|
|
if "heap" in s and not _pos_int(s["heap"]):
|
|
err.append(f"{sw}: heap must be a positive int (MB)")
|
|
jvm = s.get("jvm")
|
|
if jvm is not None and jvm not in JVM_SETS and not PARAM_RE.search(jvm):
|
|
err.append(f"{sw}: jvm {jvm!r} not in {sorted(JVM_SETS)}")
|
|
for t in s.get("add_mods", []):
|
|
if t not in trials:
|
|
err.append(f"{sw}: add_mods {t!r} not in [trials]")
|
|
if not isinstance(s.get("disable_mods", []), list):
|
|
err.append(f"{sw}: disable_mods must be a list")
|
|
_check_common(s, sw, worlds, trials, err)
|
|
g = (var, s.get("tag"))
|
|
if g in groups:
|
|
err.append(f"{sw}: variant {var!r} repeated without a distinct tag "
|
|
f"(analyze.py would merge them); use repeat or tag")
|
|
groups.add(g)
|
|
return err
|
|
|
|
|
|
def _check_common(s, where, worlds, trials, err):
|
|
for k in ("reps", "warmup", "capture", "settle"):
|
|
if k in s and not _pos_int(s[k]):
|
|
err.append(f"{where}: {k} must be a positive int")
|
|
if "display" in s and s["display"] not in DISPLAYS:
|
|
err.append(f"{where}: display {s['display']!r} not in {sorted(DISPLAYS)}")
|
|
if "res" in s and not re.fullmatch(r"\d+x\d+", str(s["res"])):
|
|
err.append(f"{where}: res must be WxH")
|
|
if "dh" in s and s["dh"] not in ("on", "off"):
|
|
err.append(f"{where}: dh must be on/off")
|
|
if "world" in s and s["world"] not in worlds:
|
|
err.append(f"{where}: world {s['world']!r} not in [worlds]")
|
|
if "shader_options" in s and s.get("shader", "off") == "off":
|
|
err.append(f"{where}: shader_options needs shader = <pack>")
|
|
|
|
|
|
def block_map(m):
|
|
return {b["id"]: b for b in m.get("block", [])}
|
|
|
|
|
|
def missing_variant_ids(m):
|
|
return {mv["id"] for mv in m.get("missing_variants", [])}
|
|
|
|
|
|
# ------------------------------------------------------------------ expansion / commands
|
|
|
|
def _subst(v, params):
|
|
if not isinstance(v, str):
|
|
return v
|
|
return PARAM_RE.sub(lambda mo: params.get(mo.group(1), mo.group(0)), v)
|
|
|
|
|
|
def expand(m, block, params=None, overrides=None):
|
|
"""Flat list of concrete sessions for a block (repeat / interleave applied)."""
|
|
params, overrides = params or {}, overrides or {}
|
|
d = dict(m.get("defaults", {}))
|
|
d.update({k: v for k, v in overrides.items() if v is not None})
|
|
groups = []
|
|
for si, s in enumerate(block.get("sessions", [])):
|
|
merged = {k: v for k, v in d.items() if k != "launch_overhead_s"}
|
|
merged.update(s)
|
|
for k in ("display", "res"): # CLI overrides beat per-session values too
|
|
if overrides.get(k) is not None:
|
|
merged[k] = overrides[k]
|
|
merged["variant"] = _subst(merged["variant"], params)
|
|
if merged.get("jvm") is not None:
|
|
merged["jvm"] = _subst(merged["jvm"], params)
|
|
merged["plan"] = [_subst(p, params) for p in split_plan(merged["plan"])]
|
|
tag = s.get("tag")
|
|
merged["label"] = f"{block['id']}.{tag}" if tag else block["id"]
|
|
merged["source"] = si
|
|
groups.append([dict(merged, run=r + 1) for r in range(s.get("repeat", 1))])
|
|
out = []
|
|
if block.get("interleave"):
|
|
for r in range(max((len(g) for g in groups), default=0)):
|
|
out += [g[r] for g in groups if r < len(g)]
|
|
else:
|
|
for g in groups:
|
|
out += g
|
|
for i, s in enumerate(out):
|
|
s["index"] = i
|
|
s["block"] = block["id"]
|
|
return out
|
|
|
|
|
|
def unresolved(sess):
|
|
"""Placeholder names still present in a session (missing --param)."""
|
|
vals = [sess["variant"], sess.get("jvm") or "", *sess["plan"]]
|
|
return sorted({p for v in vals for p in PARAM_RE.findall(v)})
|
|
|
|
|
|
def resolve_path(p):
|
|
p = os.path.expanduser(p)
|
|
hits = sorted(glob.glob(p))
|
|
return hits[-1] if hits else None
|
|
|
|
|
|
def shader_options_path(p):
|
|
"""shader_options are harness-relative unless absolute / ~."""
|
|
p = Path(os.path.expanduser(p))
|
|
return p if p.is_absolute() else HERE / p
|
|
|
|
|
|
def bench_argv(m, sess, live_world=False):
|
|
"""`bench.py session` argv for one session, plus a list of missing inputs."""
|
|
missing = []
|
|
a = ["session", "--variant", sess["variant"], "--plan", ",".join(sess["plan"]),
|
|
"--reps", str(sess.get("reps", 3)), "--label", sess["label"]]
|
|
for k in ("warmup", "capture", "settle"):
|
|
if k in sess:
|
|
a += [f"--{k}", str(sess[k])]
|
|
for k in ("display", "res", "shader", "dh", "jvm"):
|
|
if sess.get(k) is not None:
|
|
a += [f"--{k}", str(sess[k])]
|
|
if sess.get("heap"):
|
|
a += ["--heap", str(sess["heap"])]
|
|
if sess.get("shader_options"):
|
|
so = shader_options_path(sess["shader_options"])
|
|
if not so.is_file():
|
|
missing.append(f"shader options {so} (run gen_shader_profiles.py)")
|
|
a += ["--shader-options", str(so)]
|
|
if sess.get("disable_mods"):
|
|
a += ["--disable-mods", ",".join(sess["disable_mods"])]
|
|
if sess.get("add_mods"):
|
|
jars = []
|
|
for t in sess["add_mods"]:
|
|
pat = m["trials"][t]
|
|
hit = resolve_path(pat)
|
|
if hit is None:
|
|
missing.append(f"trial jar {t}: {pat}")
|
|
hit = os.path.expanduser(pat)
|
|
jars.append(hit)
|
|
a += ["--add-mods", ",".join(jars)]
|
|
if sess.get("world") and not live_world:
|
|
pat = m["worlds"][sess["world"]]
|
|
hit = resolve_path(pat)
|
|
if hit is None:
|
|
missing.append(f"world {sess['world']}: {pat} (or run with --live-world)")
|
|
hit = os.path.expanduser(pat)
|
|
a += ["--world-zip", hit]
|
|
return a, missing
|
|
|
|
|
|
def variant_missing(sess, out_dir=OUT_DIR):
|
|
return not (Path(out_dir) / sess["variant"]).is_dir()
|
|
|
|
|
|
def session_seconds(sess, overhead):
|
|
reps, w, c = sess.get("reps", 3), sess.get("warmup", 15), sess.get("capture", 30)
|
|
scenes = sum(S5_SECONDS if p == "s5" else w + c for p in sess["plan"]) * reps
|
|
return overhead + sess.get("settle", 10) + scenes
|
|
|
|
|
|
def spec_hash(argv, sess):
|
|
spec = {"argv": argv, "manual": bool(sess.get("manual"))}
|
|
if sess.get("shader_options"): # a regenerated profile file means a new spec
|
|
so = shader_options_path(sess["shader_options"])
|
|
spec["shader_options_sha256"] = (hashlib.sha256(so.read_bytes()).hexdigest()
|
|
if so.is_file() else None)
|
|
blob = json.dumps(spec, sort_keys=True)
|
|
return hashlib.sha256(blob.encode()).hexdigest()[:16]
|
|
|
|
|
|
def fmt_cmd(argv):
|
|
return "python3 bench.py " + shlex.join(argv)
|
|
|
|
|
|
# ------------------------------------------------------------------ state
|
|
|
|
def mods_hash(mods_dir):
|
|
"""Same algorithm as bench.py mods_hash (name + size of every file in mods/)."""
|
|
h = hashlib.sha256()
|
|
files = sorted(p for p in Path(mods_dir).glob("*") if p.is_file())
|
|
for p in files:
|
|
h.update(f"{p.name}\0{p.stat().st_size}\n".encode())
|
|
return h.hexdigest()
|
|
|
|
|
|
class State:
|
|
def __init__(self, path):
|
|
self.path = Path(path)
|
|
self.data = {"version": 1, "sessions": {}, "b0": None}
|
|
if self.path.exists():
|
|
self.data.update(json.loads(self.path.read_text()))
|
|
|
|
def save(self):
|
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
tmp = self.path.with_suffix(".tmp")
|
|
tmp.write_text(json.dumps(self.data, indent=1, sort_keys=True))
|
|
tmp.replace(self.path)
|
|
|
|
@staticmethod
|
|
def key(sess):
|
|
return f"{sess['block']}#{sess['index']}"
|
|
|
|
def entry(self, sess):
|
|
return self.data["sessions"].get(self.key(sess))
|
|
|
|
def is_done(self, sess, spec, mods):
|
|
e = self.entry(sess)
|
|
return bool(e and e["status"] == "ok" and e["spec"] == spec and e["mods_hash"] == mods)
|
|
|
|
def record(self, sess, spec, mods, status, rc, attempts):
|
|
self.data["sessions"][self.key(sess)] = {
|
|
"spec": spec, "mods_hash": mods, "status": status, "rc": rc,
|
|
"attempts": attempts, "label": sess["label"], "variant": sess["variant"],
|
|
"ts": time.strftime("%Y-%m-%dT%H:%M:%S")}
|
|
|
|
|
|
def b0_warning(state, current):
|
|
b0 = state.data.get("b0")
|
|
if not b0:
|
|
return "B0 (noise floor) has not been completed - win thresholds are undefined"
|
|
if b0["mods_hash"] != current:
|
|
return (f"MOD SET CHANGED since B0 (B0 under {b0['mods_hash'][:12]}, now "
|
|
f"{current[:12]}): B0 MUST BE RE-RUN before results are comparable")
|
|
return None
|
|
|
|
|
|
def banner(msg):
|
|
bar = "!" * 78
|
|
print(f"{bar}\n!! {msg}\n{bar}", file=sys.stderr, flush=True)
|
|
|
|
|
|
# ------------------------------------------------------------------ run
|
|
|
|
def default_runner(argv):
|
|
return subprocess.run([sys.executable, str(HERE / "bench.py"), *argv], cwd=HERE).returncode
|
|
|
|
|
|
def run_blocks(m, block_ids, state, mods_dir, runner=default_runner, params=None,
|
|
overrides=None, dry_run=False, include_manual=False, include_deferred=False,
|
|
live_world=False, skip_blocked=False, out_dir=OUT_DIR):
|
|
"""Run blocks in order. Returns 0 ok, 1 session failed, 2 preflight problem."""
|
|
bm = block_map(m)
|
|
for bid in block_ids:
|
|
if bid not in bm:
|
|
raise MatrixError(f"unknown block {bid}; known: {', '.join(bm)}")
|
|
# preflight: build every session's command, collect what can't run
|
|
todo, problems = [], []
|
|
for bid in block_ids:
|
|
b = bm[bid]
|
|
if b.get("deferred") and not include_deferred:
|
|
log(f"{bid}: deferred ({b.get('proxy_note', 'fleet-only')}) - skipped")
|
|
continue
|
|
if b.get("manual"):
|
|
log(f"{bid}: manual block - run by hand ({b.get('proxy_note', '')})")
|
|
continue
|
|
for s in expand(m, b, params, overrides):
|
|
if s.get("manual") and not include_manual:
|
|
log(f"{bid}#{s['index']} {s['label']}: manual - skipped")
|
|
continue
|
|
miss = [f"param ${{{p}}} (use --param {p}=...)" for p in unresolved(s)]
|
|
if not miss and variant_missing(s, out_dir):
|
|
miss.append(f"variant {s['variant']} not generated (see missing_variants)")
|
|
argv, inputs = bench_argv(m, s, live_world)
|
|
miss += inputs
|
|
if miss:
|
|
problems.append((s, miss))
|
|
continue
|
|
todo.append((s, argv))
|
|
for s, miss in problems:
|
|
log(f"BLOCKED {s['block']}#{s['index']} {s['label']}: " + "; ".join(miss))
|
|
if problems and not (skip_blocked or dry_run):
|
|
log("fix the above, or pass --skip-blocked (and/or --live-world)")
|
|
return 2
|
|
mods0 = mods_hash(mods_dir)
|
|
if "B0" not in block_ids and any(s["block"] != "B0" for s, _ in todo):
|
|
w = b0_warning(state, mods0)
|
|
if w:
|
|
banner(w)
|
|
total = len(todo)
|
|
for n, (s, argv) in enumerate(todo, 1):
|
|
spec = spec_hash(argv, s)
|
|
tagline = f"[{n}/{total}] {s['block']}#{s['index']} {s['label']}"
|
|
if state.is_done(s, spec, mods0):
|
|
log(f"{tagline}: done - skip")
|
|
continue
|
|
if dry_run:
|
|
log(f"{tagline}: {fmt_cmd(argv)}")
|
|
continue
|
|
now = mods_hash(mods_dir)
|
|
if now != mods0:
|
|
banner(f"instance mods/ changed during the run ({mods0[:12]} -> {now[:12]}); "
|
|
"a session did not restore it - stopping")
|
|
return 1
|
|
rc = None
|
|
for attempt in (1, 2):
|
|
log(f"{tagline}: attempt {attempt}: {fmt_cmd(argv)}")
|
|
rc = runner(argv)
|
|
if rc == 0:
|
|
break
|
|
log(f"{tagline}: exit {rc}" + (" - retrying once" if attempt == 1 else ""))
|
|
status = "ok" if rc == 0 else "failed"
|
|
state.record(s, spec, mods0, status, rc, attempt)
|
|
if s["block"] == "B0":
|
|
_maybe_close_b0(m, state, mods0, params, overrides, live_world)
|
|
state.save()
|
|
if status != "ok":
|
|
log(f"{tagline}: failed twice - stopping the run")
|
|
return 1
|
|
return 0
|
|
|
|
|
|
def _maybe_close_b0(m, state, mods, params, overrides, live_world):
|
|
b = block_map(m).get("B0")
|
|
if not b:
|
|
return
|
|
for s in expand(m, b, params, overrides):
|
|
argv, _ = bench_argv(m, s, live_world)
|
|
if not state.is_done(s, spec_hash(argv, s), mods):
|
|
return
|
|
state.data["b0"] = {"mods_hash": mods, "ts": time.strftime("%Y-%m-%dT%H:%M:%S")}
|
|
|
|
|
|
# ------------------------------------------------------------------ list / plan / status
|
|
|
|
def block_summary(m, b, overhead, out_dir=OUT_DIR):
|
|
ss = expand(m, b)
|
|
auto = [s for s in ss if not s.get("manual") and not b.get("manual")]
|
|
blocked = [s for s in auto if not unresolved(s) and variant_missing(s, out_dir)]
|
|
secs = sum(session_seconds(s, overhead) for s in auto)
|
|
return {"sessions": len(ss), "auto": len(auto), "manual": len(ss) - len(auto),
|
|
"blocked": len(blocked), "seconds": secs}
|
|
|
|
|
|
def cmd_list(m, a):
|
|
overhead = m.get("defaults", {}).get("launch_overhead_s", 60)
|
|
print(f"| block | title | sessions | auto | manual | blocked (variant) | est. min | notes |")
|
|
print("| --- | --- | --- | --- | --- | --- | --- | --- |")
|
|
tot = {"sessions": 0, "auto": 0, "seconds": 0}
|
|
for b in m.get("block", []):
|
|
s = block_summary(m, b, overhead)
|
|
notes = []
|
|
if b.get("deferred"):
|
|
notes.append("deferred")
|
|
if b.get("manual"):
|
|
notes.append("manual block")
|
|
if b.get("params"):
|
|
notes.append("params: " + ",".join(b["params"]))
|
|
print(f"| {b['id']} | {b.get('title', '')} | {s['sessions']} | {s['auto']} | "
|
|
f"{s['manual']} | {s['blocked']} | {s['seconds'] / 60:.0f} | {'; '.join(notes)} |")
|
|
if not b.get("deferred"):
|
|
for k in tot:
|
|
tot[k] += s[k]
|
|
print(f"\nnot deferred: {tot['sessions']} sessions ({tot['auto']} unattended), "
|
|
f"est. {tot['seconds'] / 3600:.1f} h unattended")
|
|
return 0
|
|
|
|
|
|
def cmd_plan(m, a):
|
|
bm = block_map(m)
|
|
for bid in a.blocks.split(","):
|
|
if bid not in bm:
|
|
raise MatrixError(f"unknown block {bid}")
|
|
b = bm[bid]
|
|
print(f"## {bid} {b.get('title', '')}" + (" (DEFERRED)" if b.get("deferred") else "")
|
|
+ (" (MANUAL)" if b.get("manual") else ""))
|
|
print(f"rule: {b['rule']}")
|
|
if b.get("proxy_note"):
|
|
print(f"proxy: {b['proxy_note']}")
|
|
for c in b.get("manual_checks", []):
|
|
print(f"manual check: {c}")
|
|
for s in expand(m, b, a.params, overrides(a)):
|
|
argv, miss = bench_argv(m, s, a.live_world)
|
|
pre = "# MANUAL: " if s.get("manual") else ""
|
|
print(f"{pre}{fmt_cmd(argv)}")
|
|
if s.get("shader_profile") and not s.get("shader_options"):
|
|
print(f"# set Iris profile {s['shader_profile']} for {s['shader']} first")
|
|
elif s.get("shader_profile"):
|
|
print(f"# Iris profile {s['shader_profile']} via shader options file")
|
|
if s.get("note"):
|
|
print(f"# note: {s['note']}")
|
|
if unresolved(s):
|
|
print(f"# needs --param {','.join(unresolved(s))}")
|
|
if variant_missing(s) and not unresolved(s):
|
|
print(f"# BLOCKED: variant {s['variant']} not generated")
|
|
for x in miss:
|
|
print(f"# missing: {x}")
|
|
print()
|
|
return 0
|
|
|
|
|
|
def cmd_status(m, a):
|
|
state = State(a.state)
|
|
mods = mods_hash(a.mods_dir) if Path(a.mods_dir).is_dir() else ""
|
|
w = b0_warning(state, mods)
|
|
if w:
|
|
banner(w)
|
|
print(f"mods hash {mods[:12]} | state {a.state}")
|
|
print("| block | done | failed | pending | manual/deferred |")
|
|
print("| --- | --- | --- | --- | --- |")
|
|
for b in m.get("block", []):
|
|
done = failed = pending = skipped = 0
|
|
for s in expand(m, b, a.params, overrides(a)):
|
|
if s.get("manual") or b.get("manual") or b.get("deferred"):
|
|
skipped += 1
|
|
continue
|
|
argv, _ = bench_argv(m, s, a.live_world)
|
|
e = state.entry(s)
|
|
if state.is_done(s, spec_hash(argv, s), mods):
|
|
done += 1
|
|
elif e and e["status"] == "failed":
|
|
failed += 1
|
|
else:
|
|
pending += 1
|
|
print(f"| {b['id']} | {done} | {failed} | {pending} | {skipped} |")
|
|
return 0
|
|
|
|
|
|
def cmd_run(m, a):
|
|
state = State(a.state)
|
|
return run_blocks(m, a.blocks.split(","), state, a.mods_dir, params=a.params,
|
|
overrides=overrides(a), dry_run=a.dry_run,
|
|
include_manual=a.include_manual, include_deferred=a.include_deferred,
|
|
live_world=a.live_world, skip_blocked=a.skip_blocked)
|
|
|
|
|
|
def overrides(a):
|
|
return {"display": getattr(a, "display", None), "res": getattr(a, "res", None)}
|
|
|
|
|
|
def parse_params(items):
|
|
out = {}
|
|
for it in items or []:
|
|
k, sep, v = it.partition("=")
|
|
if not sep:
|
|
raise MatrixError(f"--param needs name=value, got {it!r}")
|
|
out[k] = v
|
|
return out
|
|
|
|
|
|
def main(argv=None):
|
|
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
|
|
ap.add_argument("--matrix", default=str(MATRIX))
|
|
ap.add_argument("--state", default=str(STATE))
|
|
ap.add_argument("--mods-dir", default=str(GAME / "mods"))
|
|
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
sub.add_parser("list", help="blocks, session counts, estimated time")
|
|
common = argparse.ArgumentParser(add_help=False)
|
|
common.add_argument("--param", action="append", default=[], help="name=value")
|
|
common.add_argument("--display", choices=sorted(DISPLAYS))
|
|
common.add_argument("--res")
|
|
common.add_argument("--live-world", action="store_true",
|
|
help="don't restore world zips; use saves/onionbench as-is")
|
|
p = sub.add_parser("plan", parents=[common], help="print the bench.py commands")
|
|
p.add_argument("blocks")
|
|
r = sub.add_parser("run", parents=[common], help="run blocks sequentially (resumable)")
|
|
r.add_argument("blocks", help="comma list, e.g. B0,B1")
|
|
r.add_argument("--dry-run", action="store_true")
|
|
r.add_argument("--include-manual", action="store_true")
|
|
r.add_argument("--include-deferred", action="store_true")
|
|
r.add_argument("--skip-blocked", action="store_true",
|
|
help="skip sessions with missing variants/inputs instead of aborting")
|
|
sub.add_parser("status", parents=[common], help="progress for the current mod set")
|
|
a = ap.parse_args(argv)
|
|
try:
|
|
a.params = parse_params(getattr(a, "param", []))
|
|
m = load(a.matrix)
|
|
return {"list": cmd_list, "plan": cmd_plan, "run": cmd_run,
|
|
"status": cmd_status}[a.cmd](m, a)
|
|
except MatrixError as e:
|
|
print(f"matrix.py: {e}", file=sys.stderr)
|
|
return 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|