mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-13 13:03:22 +09:00
693 lines
36 KiB
Python
Executable File
693 lines
36 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""60-reduce.py <stamp> [--logroot DIR] [--tools TREE] [--json OUT]
|
|
60-reduce.py --check-block RUN_DIR REPORT_JSON
|
|
|
|
Device window 2 verdicts, read from the window's own output directory (<logroot>/<stamp>).
|
|
|
|
GATE 3 (CONTRACT-P7 7.2), per case of gate3/cases.txt:
|
|
* ONE SESSION: every gate3/state/<arm>/<case>.done and every archived repeat's boot_id.txt, of
|
|
every arm, carries one boot_id, equal to session/boot-id.txt. A second boot_id, a record that
|
|
carries none, or no session/boot-id.txt makes the whole gate GATE3 INVALID-SESSION, whatever
|
|
the pictures say (the gate is a same-session reading);
|
|
* monolith x1 is the same-session control: it must itself pass, and its role logs must show no
|
|
'Config: IPC' / MOBILEGL_TRANSPORT line (the arm really was the monolith);
|
|
* inproc and spawn: the pair finished (state .done), every expected repeat present and
|
|
error-free, each repeat's arm proof (transport-proof.json passed), the device's own pass
|
|
verdict (ssim >= case threshold), ALL repeats OF THE ARM bit-identical (actual PNG SHA-256;
|
|
within-arm identity is the gate), and |ssim - monolith ssim| <= 0.0005 on every repeat; a
|
|
repeat whose logcat is byte-identical to the previous repeat's is flagged STALE;
|
|
* OpenRA: 1.000000 and mismatch 0 on every repeat of both split arms AND on monolith;
|
|
* cross-arm picture identity (inproc vs spawn vs monolith) is REPORTED per case and a mismatch
|
|
prints a WARN line - information, not a gate (ruling: integrator 2026-09-23);
|
|
* inproc-ra0 (RUN_AHEAD=0): recorded (ssim, sha, run-ahead=0 proof), never gating;
|
|
* a reading that passes but is not the gate's - a denominator other than 36, or a session that
|
|
is not reboot-clean - is GATE3 PASS-SUBSET / PASS-NOT-REBOOT-CLEAN, never PASS.
|
|
BSL STATS: per arm, maps-line / VmRSS / VmHWM peaks and the wbuf[] gauges (information).
|
|
CTS AFTER (CONTRACT-P7 7.3), per block vs device-window-1/CTS-base/report-<block>.json, over the
|
|
cases the AFTER run was asked to run (so a --limit subset is compared like for like):
|
|
* all five blocks (shader-image ssbo dsa texture packed-pixels) are REQUIRED, each with a
|
|
finished run (cts/runs/<block>/.done), a non-empty cts/report-<block>.json with results, and
|
|
a readable, non-empty caselist (runs/<block>/caselist.txt, the copy 50-cts-after.sh keeps;
|
|
else the file caselist.path names); a missing one makes the block MISSING and CTS INCOMPLETE -
|
|
a block of 0 cases is never a reading. The UBO block (GTF-GL46 uniform_buffer_object) is
|
|
recorded as 'unrun (not in this glcts)': the device glcts carries no GTF module (ID-P7-16);
|
|
* rate = Pass / (Pass + Fail), the contract formula: NotSupported, the warnings and the crashes
|
|
are NOT in the denominator; gate: delta >= -0.5 pp on this rate. Both rates are taken over the
|
|
cases that have a BASE and an AFTER result (a case BASE lacks prints a WARN, stays out of both
|
|
rates and still counts for new crashes). AFTER without a single Pass/Fail where BASE has some
|
|
is FAIL (the rate cannot be formed: every BASE Pass is gone); BASE without one is INCOMPLETE;
|
|
* crashes (Crash/Timeout/InternalError/ResourceError/DeviceHang/Incomplete, plus hung.txt) are
|
|
counted in their own column and are non-Pass; one that BASE does not already have is a NEW
|
|
crash, a hard red of its own (a Fail that became a Crash is a new crash);
|
|
* a block whose cases are not the tools tree's full p7-<block>-gl46.txt (a --limit run) makes an
|
|
all-PASS CTS reading 'PASS-SUBSET', never PASS;
|
|
* information only, never gating: the pre-ruling rate Pass / (results - NotSupported) of BASE
|
|
and AFTER and its delta, and a WARN when the NotSupported count moved.
|
|
Exit status: 0 only when GATE 3 is PASS (36/36, reboot-clean, one session) and CTS is PASS (five
|
|
full blocks); 1 otherwise (a section that did not run, a PASS-SUBSET / PASS-NOT-REBOOT-CLEAN
|
|
reading, INCOMPLETE / INVALID-SESSION, or FAIL); 2 on bad input.
|
|
--check-block RUN_DIR REPORT_JSON: the completeness test 50-cts-after.sh applies before it writes
|
|
a block's .done (the same code the verdict uses): exit 0 when the report has a result for every
|
|
non-skipped case of the run's caselist and unrun.txt is empty, 3 when not (the reason is printed).
|
|
"""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
|
|
TOL = 0.0005
|
|
GATE3_DENOMINATOR = 36
|
|
CTS_TOL_PP = 0.5
|
|
HARD = ("Crash", "Timeout", "InternalError", "ResourceError", "DeviceHang", "Incomplete")
|
|
SPLIT_ARMS = ("inproc", "spawn")
|
|
OPENRA = "OpenRA"
|
|
CTS_BLOCKS = ("shader-image", "ssbo", "dsa", "texture", "packed-pixels")
|
|
UBO_BLOCK = {"block": "ubo", "caselist": "tools/cts/caselists/p7-ubo-gl46.txt",
|
|
"cases": "GTF-GL46.gtf31.GL3Tests.uniform_buffer_object*", "verdict": "UNRUN",
|
|
"reasons": ["unrun (not in this glcts): the device glcts has no GTF module (ID-P7-16);"
|
|
" recorded, not gated"]}
|
|
REPEAT_DIR = re.compile(r"^repeat-(\d+)$")
|
|
KV = re.compile(r"([A-Za-z_][A-Za-z0-9_]*)=(\S*)")
|
|
# ConfigLoader.cpp: "Config: MOBILEGL_TRANSPORT=inproc|spawn - ..." (and its refusals) and
|
|
# "Config: IPC ring=..." are printed only when a transport resolves; the monolith prints neither.
|
|
NOT_MONOLITH = re.compile(r"Config: IPC|MOBILEGL_TRANSPORT=")
|
|
RULING = "ruling: integrator 2026-09-23"
|
|
|
|
|
|
def sha(path):
|
|
h = hashlib.sha256()
|
|
with open(path, "rb") as f:
|
|
for block in iter(lambda: f.read(1 << 20), b""):
|
|
h.update(block)
|
|
return h.hexdigest()
|
|
|
|
|
|
def read_kv(path):
|
|
out = {}
|
|
if path.is_file():
|
|
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
if "=" in line:
|
|
k, v = line.split("=", 1)
|
|
out[k.strip()] = v.strip()
|
|
return out
|
|
|
|
|
|
def read_record(path):
|
|
"""A one-line 'k=v k=v ...' record (gate3 .done, boot_id.txt) -> dict, or None if absent."""
|
|
try:
|
|
return dict(KV.findall(path.read_text(encoding="utf-8", errors="replace")))
|
|
except OSError:
|
|
return None
|
|
|
|
|
|
def first_line(path):
|
|
try:
|
|
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
except OSError:
|
|
return ""
|
|
return lines[0].strip() if lines else ""
|
|
|
|
|
|
# ------------------------------------------------------------------------------------------------
|
|
# gate 3
|
|
# ------------------------------------------------------------------------------------------------
|
|
def gate3_session(out, g):
|
|
"""One boot session for every record of every arm (ruling c)."""
|
|
want = first_line(out / "session" / "boot-id.txt")
|
|
problems, seen, unstamped = [], {}, []
|
|
|
|
def note(value, where):
|
|
if value:
|
|
seen.setdefault(value, []).append(where)
|
|
else:
|
|
unstamped.append(where)
|
|
|
|
state = g / "state"
|
|
for arm_dir in sorted(p for p in state.iterdir() if p.is_dir()) if state.is_dir() else []:
|
|
for done in sorted(arm_dir.glob("*.done")):
|
|
rec = read_record(done) or {}
|
|
where = "state/%s/%s" % (arm_dir.name, done.name)
|
|
note(rec.get("boot_id"), where)
|
|
if "boot_id_before" in rec:
|
|
note(rec["boot_id_before"], where + " (boot_id_before)")
|
|
archive = g / "archive"
|
|
for arm_dir in sorted(p for p in archive.iterdir() if p.is_dir()) if archive.is_dir() else []:
|
|
for case_dir in sorted(p for p in arm_dir.iterdir() if p.is_dir()):
|
|
for rep in sorted(p for p in case_dir.iterdir() if p.is_dir() and REPEAT_DIR.match(p.name)):
|
|
where = "archive/%s/%s/%s/boot_id.txt" % (arm_dir.name, case_dir.name, rep.name)
|
|
rec = read_record(rep / "boot_id.txt") or {}
|
|
note(rec.get("boot_id"), where)
|
|
if "boot_id_before" in rec:
|
|
note(rec["boot_id_before"], where + " (boot_id_before)")
|
|
if not want:
|
|
problems.append("no session/boot-id.txt: 21-preflight.sh records the session's boot_id there")
|
|
if unstamped:
|
|
problems.append("%d record(s) carry no boot_id (pre-ruling 30-gate3.sh, or a repeat the driver did "
|
|
"not stamp), e.g. %s" % (len(unstamped), unstamped[0]))
|
|
for boot in sorted(b for b in seen if b != want):
|
|
problems.append("boot_id %s != session %s in %d record(s), e.g. %s"
|
|
% (boot, want or "?", len(seen[boot]), seen[boot][0]))
|
|
return {"session_boot_id": want, "boot_ids": sorted(seen), "records": sum(len(v) for v in seen.values()),
|
|
"unstamped": len(unstamped), "problems": problems, "valid": not problems,
|
|
"reboot_clean": first_line(out / "session" / "reboot-clean.txt")}
|
|
|
|
|
|
def monolith_identity(row):
|
|
"""None when the repeat's role logs show a monolith, else what says otherwise (ruling f)."""
|
|
d = Path(row["dir"])
|
|
if not (d / "mobilegl.log").is_file():
|
|
return "no mobilegl.log archived: the arm's identity is unproven"
|
|
for name in ("mobilegl.log", "mobilegl.client.log", "mobilegl.server.log"):
|
|
p = d / name
|
|
if not p.is_file():
|
|
continue
|
|
for line in p.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
if NOT_MONOLITH.search(line):
|
|
return "%s: %s" % (name, line.strip()[:140])
|
|
return None
|
|
|
|
|
|
def gate3(out, ca):
|
|
g = out / "gate3"
|
|
cases_file = g / "cases.txt"
|
|
if not cases_file.is_file():
|
|
return None
|
|
cases = [c for c in cases_file.read_text().split() if c]
|
|
excluded = (g / "excluded.txt").read_text().split() if (g / "excluded.txt").is_file() else []
|
|
meta = read_kv(g / "arms.txt")
|
|
repeat = int(meta.get("repeat", "3"))
|
|
arms_present = [a for a in ("monolith", "inproc", "spawn", "inproc-ra0") if (g / "archive" / a).is_dir()]
|
|
summaries = {a: ca.summarize(g / "archive" / a) for a in arms_present}
|
|
session = gate3_session(out, g)
|
|
identity = {"monolith_repeats": 0, "monolith_proven": 0, "split_repeats": 0, "split_proofs_passed": 0}
|
|
|
|
def rows(arm, case):
|
|
s = summaries.get(arm)
|
|
if not s:
|
|
return []
|
|
return [r for r in s["rows"] if r["arm"] == case + "-DirectVulkan"]
|
|
|
|
def finished(arm, case):
|
|
return (g / "state" / arm / (case + ".done")).is_file()
|
|
|
|
def proof(row):
|
|
p = Path(row["dir"]) / "transport-proof.json"
|
|
try:
|
|
return json.loads(p.read_text(encoding="utf-8"))
|
|
except (OSError, ValueError):
|
|
return None
|
|
|
|
def logcat_sha(row):
|
|
p = Path(row["dir"]) / "logcat.txt"
|
|
return sha(p) if p.is_file() else None
|
|
|
|
def ra0_proof(row):
|
|
p = Path(row["dir"]) / "mobilegl.log"
|
|
if not p.is_file():
|
|
return "no-log"
|
|
lines = re.findall(r"Config: IPC[^\r\n]*", p.read_text(encoding="utf-8", errors="replace"))
|
|
if lines and all(re.search(r"\brun-ahead=0\b", line) for line in lines):
|
|
return "run-ahead=0"
|
|
return "NOT-PROVEN(" + ";".join(lines)[:120] + ")"
|
|
|
|
results = []
|
|
for case in cases:
|
|
reasons, incomplete = [], []
|
|
entry = {"case": case, "arms": {}}
|
|
mono = rows("monolith", case)
|
|
mono_ssim = mono_sha = None
|
|
if "monolith" in summaries and not finished("monolith", case):
|
|
incomplete.append("monolith: no state .done (pair unfinished; 30-gate3.sh retries it)")
|
|
if len(mono) != 1 or mono[0].get("error"):
|
|
incomplete.append("monolith: %s" % (mono[0].get("error") if mono else "not run"))
|
|
else:
|
|
m = mono[0]
|
|
mono_ssim, mono_sha = m.get("ssim_vs_golden"), m.get("actual_sha256")
|
|
not_mono = monolith_identity(m)
|
|
identity["monolith_repeats"] += 1
|
|
identity["monolith_proven"] += not_mono is None
|
|
entry["arms"]["monolith"] = {"ssim": mono_ssim, "sha": mono_sha, "identity": not_mono or "monolith",
|
|
"mismatch": m.get("mismatch_pixels_vs_golden"), "passed": m.get("passed")}
|
|
if not_mono:
|
|
reasons.append("monolith arm is NOT proven monolith (%s)" % not_mono)
|
|
if m.get("passed") is not True:
|
|
reasons.append("monolith control did not pass (ssim %s)" % mono_ssim)
|
|
if case == OPENRA and not (m.get("mismatch_pixels_vs_golden") == 0 and (mono_ssim or 0) >= 0.9999995):
|
|
reasons.append("OpenRA monolith not 1.000000/0 (%s/%s)" % (mono_ssim, m.get("mismatch_pixels_vs_golden")))
|
|
for arm in SPLIT_ARMS:
|
|
rs = rows(arm, case)
|
|
a = {"repeats": len(rs), "ssim": [r.get("ssim_vs_golden") for r in rs],
|
|
"sha": sorted({r.get("actual_sha256") for r in rs if r.get("actual_sha256")}), "proofs_passed": 0}
|
|
entry["arms"][arm] = a
|
|
if arm not in summaries:
|
|
incomplete.append(arm + ": not run")
|
|
continue
|
|
if not finished(arm, case):
|
|
incomplete.append("%s: no state .done (pair unfinished; 30-gate3.sh retries it)" % arm)
|
|
if len(rs) != repeat:
|
|
incomplete.append("%s: %d/%d repeats" % (arm, len(rs), repeat))
|
|
for r in rs:
|
|
if r.get("error"):
|
|
incomplete.append("%s rep%d: %s" % (arm, r["repeat"], r["error"]))
|
|
continue
|
|
if r.get("passed") is not True:
|
|
reasons.append("%s rep%d below threshold (ssim %s)" % (arm, r["repeat"], r.get("ssim_vs_golden")))
|
|
p = proof(r)
|
|
identity["split_repeats"] += 1
|
|
if not p or p.get("passed") is not True:
|
|
reasons.append("%s rep%d arm proof (transport-proof.json) not passed" % (arm, r["repeat"]))
|
|
else:
|
|
a["proofs_passed"] += 1
|
|
identity["split_proofs_passed"] += 1
|
|
if mono_ssim is not None and r.get("ssim_vs_golden") is not None:
|
|
d = abs(r["ssim_vs_golden"] - mono_ssim)
|
|
if d > TOL:
|
|
reasons.append("%s rep%d |ssim-monolith|=%.6f > %.4f" % (arm, r["repeat"], d, TOL))
|
|
if case == OPENRA and not (r.get("mismatch_pixels_vs_golden") == 0 and (r.get("ssim_vs_golden") or 0) >= 0.9999995):
|
|
reasons.append("OpenRA %s rep%d not 1.000000/0 (%s/%s)" % (arm, r["repeat"], r.get("ssim_vs_golden"), r.get("mismatch_pixels_vs_golden")))
|
|
shas = [r.get("actual_sha256") for r in rs if r.get("actual_sha256")]
|
|
if len(set(shas)) > 1:
|
|
reasons.append("%s repeats NOT bit-identical (%d distinct pictures)" % (arm, len(set(shas))))
|
|
lc = [logcat_sha(r) for r in rs]
|
|
for i in range(1, len(lc)):
|
|
if lc[i] and lc[i] == lc[i - 1]:
|
|
reasons.append("%s rep%d STALE (logcat identical to rep%d)" % (arm, rs[i]["repeat"], rs[i - 1]["repeat"]))
|
|
ra0 = rows("inproc-ra0", case)
|
|
if ra0:
|
|
r = ra0[0]
|
|
entry["arms"]["inproc-ra0"] = {"ssim": r.get("ssim_vs_golden"), "sha": r.get("actual_sha256"),
|
|
"passed": r.get("passed"), "proof": ra0_proof(r), "error": r.get("error")}
|
|
# Cross-arm identity: reported, never gating (ruling d).
|
|
ip = set(entry["arms"].get("inproc", {}).get("sha", []))
|
|
sp = set(entry["arms"].get("spawn", {}).get("sha", []))
|
|
entry["inproc_eq_spawn"] = bool(ip) and ip == sp
|
|
entry["split_eq_monolith"] = bool(ip) and ip == sp == {mono_sha}
|
|
entry["cross_arm"] = {"monolith": mono_sha, "inproc": sorted(ip), "spawn": sorted(sp),
|
|
"identical": (entry["split_eq_monolith"] if (ip and sp and mono_sha) else None)}
|
|
entry["verdict"] = "INCOMPLETE" if incomplete else ("FAIL" if reasons else "PASS")
|
|
entry["reasons"] = incomplete + reasons
|
|
results.append(entry)
|
|
verdicts = [e["verdict"] for e in results]
|
|
# A reading that is not the gate's own (CONTRACT-P7 7.2: the 36-case denominator, one
|
|
# reboot-clean session) may pass, but never as PASS: exit 0 is reserved for the gate verdict.
|
|
not_a_gate = []
|
|
if len(cases) != GATE3_DENOMINATOR:
|
|
not_a_gate.append("denominator is %d, not the gate's %d: a subset run is NOT a gate verdict"
|
|
% (len(cases), GATE3_DENOMINATOR))
|
|
if not session["reboot_clean"].startswith("reboot-clean:"):
|
|
not_a_gate.append("session not reboot-clean: NOT a gate verdict")
|
|
if not session["valid"]:
|
|
overall = "INVALID-SESSION"
|
|
elif verdicts and all(v == "PASS" for v in verdicts):
|
|
overall = "PASS"
|
|
if len(cases) != GATE3_DENOMINATOR:
|
|
overall = "PASS-SUBSET"
|
|
elif not_a_gate:
|
|
overall = "PASS-NOT-REBOOT-CLEAN"
|
|
elif "FAIL" in verdicts:
|
|
overall = "FAIL"
|
|
else:
|
|
overall = "INCOMPLETE"
|
|
return {"cases": results, "excluded": excluded, "repeat": repeat, "denominator": len(cases),
|
|
"overall": overall, "not_a_gate": not_a_gate, "arms_present": arms_present,
|
|
"session": session, "identity": identity}
|
|
|
|
|
|
def print_gate3(g3):
|
|
print("== GATE 3 (DirectVulkan x pbuffer; split repeat=%d; tol |ssim-monolith| <= %.4f) ==" % (g3["repeat"], TOL))
|
|
s = g3["session"]
|
|
print("-- session: boot_id %s; %d record(s) stamped; %s" % (s["session_boot_id"] or "?", s["records"],
|
|
s["reboot_clean"] or "no session/reboot-clean.txt"))
|
|
for problem in s["problems"]:
|
|
print(" ! INVALID-SESSION: " + problem)
|
|
w = max([len(e["case"]) for e in g3["cases"]] + [4])
|
|
print("%-*s %-10s %-11s %-24s %-24s %-11s %s" % (w, "case", "verdict", "monolith", "inproc(min..max) #sha", "spawn(min..max) #sha", "ra0", "i==s==m"))
|
|
for e in g3["cases"]:
|
|
def cell(arm):
|
|
a = e["arms"].get(arm)
|
|
if not a or not a.get("ssim"):
|
|
return "-"
|
|
vals = [v for v in a["ssim"] if isinstance(v, (int, float))]
|
|
if not vals:
|
|
return "err"
|
|
return "%.6f..%.6f #%d" % (min(vals), max(vals), len(a["sha"]))
|
|
mono = e["arms"].get("monolith", {}).get("ssim")
|
|
ra0 = e["arms"].get("inproc-ra0", {}).get("ssim")
|
|
print("%-*s %-10s %-11s %-24s %-24s %-11s %s" % (
|
|
w, e["case"], e["verdict"], "%.6f" % mono if isinstance(mono, (int, float)) else "-",
|
|
cell("inproc"), cell("spawn"), "%.6f" % ra0 if isinstance(ra0, (int, float)) else "-",
|
|
"yes" if e["split_eq_monolith"] else ("i==s" if e["inproc_eq_spawn"] else "no")))
|
|
for reason in e["reasons"]:
|
|
print("%-*s ! %s" % (w, "", reason))
|
|
ra0s = [(e["case"], e["arms"]["inproc-ra0"]) for e in g3["cases"] if "inproc-ra0" in e["arms"]]
|
|
if ra0s:
|
|
print("-- RUN_AHEAD=0 control (recorded, not gating): %d case(s); proofs: %s" % (
|
|
len(ra0s), ", ".join(sorted({r["proof"] for _, r in ra0s}))))
|
|
i = g3["identity"]
|
|
print("-- arm identity: monolith %d/%d repeat(s) with no Config: IPC / MOBILEGL_TRANSPORT line; "
|
|
"inproc+spawn transport-proof passed %d/%d" % (i["monolith_proven"], i["monolith_repeats"],
|
|
i["split_proofs_passed"], i["split_repeats"]))
|
|
judged = [e for e in g3["cases"] if e["cross_arm"]["identical"] is not None]
|
|
print("-- cross-arm picture identity (reported, NOT a gate; %s): %d/%d case(s) inproc==spawn==monolith" % (
|
|
RULING, sum(e["cross_arm"]["identical"] for e in judged), len(judged)))
|
|
for e in judged:
|
|
if not e["cross_arm"]["identical"]:
|
|
x = e["cross_arm"]
|
|
print("WARN cross-arm %s: monolith %s | inproc %s | spawn %s" % (
|
|
e["case"], (x["monolith"] or "-")[:12], ",".join(v[:12] for v in x["inproc"]) or "-",
|
|
",".join(v[:12] for v in x["spawn"]) or "-"))
|
|
print("-- excluded (red on monolith, CONTRACT-P7 7.1): " + (", ".join(g3["excluded"]) or "none"))
|
|
n = len(g3["cases"])
|
|
npass = sum(e["verdict"] == "PASS" for e in g3["cases"])
|
|
print("GATE3 %s: %d/%d cases PASS%s" % (g3["overall"], npass, n, "".join(" (%s)" % x for x in g3["not_a_gate"])))
|
|
|
|
|
|
# ------------------------------------------------------------------------------------------------
|
|
# bsl stats
|
|
# ------------------------------------------------------------------------------------------------
|
|
def bsl(out, ca):
|
|
b = out / "bsl"
|
|
if not b.is_dir():
|
|
return None
|
|
arms = {}
|
|
for arm_dir in sorted(p for p in b.iterdir() if p.is_dir()):
|
|
a = {"done": read_kv(arm_dir / ".done")}
|
|
a["peaks"] = (arm_dir / "peaks.txt").read_text() if (arm_dir / "peaks.txt").is_file() else ""
|
|
a["wbuf"] = (arm_dir / "wbuf-gauges.txt").read_text() if (arm_dir / "wbuf-gauges.txt").is_file() else ""
|
|
if (arm_dir / "archive").is_dir():
|
|
s = ca.summarize(arm_dir / "archive")
|
|
a["replay"] = [{k: r.get(k) for k in ("passed", "ssim_vs_golden", "actual_sha256", "error")} for r in s["rows"]]
|
|
arms[arm_dir.name] = a
|
|
return arms
|
|
|
|
|
|
def print_bsl(arms):
|
|
print("== BSL-ESC-MENU MOBILEGL_PIPE_STATS (maps lines / RSS peaks, wbuf[] gauges) ==")
|
|
for arm, a in arms.items():
|
|
print("[%s] %s" % (arm, " ".join("%s=%s" % kv for kv in a["done"].items())))
|
|
for r in a.get("replay", []):
|
|
print(" replay: passed=%s ssim=%s sha=%s %s" % (r["passed"], r["ssim_vs_golden"],
|
|
(r["actual_sha256"] or "-")[:16], r.get("error") or ""))
|
|
for line in a["peaks"].splitlines():
|
|
print(" " + line)
|
|
for line in a["wbuf"].splitlines() or ["(no wbuf[] line in the archived logs)"]:
|
|
print(" " + line.strip())
|
|
|
|
|
|
# ------------------------------------------------------------------------------------------------
|
|
# CTS AFTER vs BASE
|
|
# ------------------------------------------------------------------------------------------------
|
|
def read_list(path):
|
|
if not path.is_file():
|
|
return []
|
|
return [l.strip() for l in path.read_text(encoding="utf-8").splitlines() if l.strip() and not l.startswith("#")]
|
|
|
|
|
|
def rate(statuses):
|
|
"""CONTRACT-P7 7.3: rate = Pass / (Pass + Fail). NotSupported, the warnings (Compatibility /
|
|
Quality) and the crashes stay out of the denominator; crashes get their own count. The
|
|
pre-ruling Pass / (results - NotSupported) is kept as `results_rate`, information only."""
|
|
c = Counter(statuses)
|
|
p, f, ns = c["Pass"], c["Fail"], c["NotSupported"]
|
|
x = sum(c[s] for s in HARD)
|
|
n = len(statuses)
|
|
return {"pass": p, "fail": f, "ns": ns, "crash": x, "warn": n - p - f - ns - x, "results": n,
|
|
"rate": (p / (p + f)) if (p + f) else None,
|
|
"results_rate": (p / (n - ns)) if (n - ns) else None}
|
|
|
|
|
|
def load_results(path):
|
|
"""-> (results dict, None) or (None, why the report does not count)."""
|
|
if not path.is_file():
|
|
return None, "no %s" % path.name
|
|
if path.stat().st_size == 0:
|
|
return None, "%s is empty" % path.name
|
|
try:
|
|
results = json.loads(path.read_text(encoding="utf-8"))["results"]
|
|
except (OSError, ValueError, KeyError, TypeError) as error:
|
|
return None, "%s unreadable (%s)" % (path.name, error)
|
|
if not isinstance(results, dict) or not results:
|
|
return None, "%s has no results" % path.name
|
|
return results, None
|
|
|
|
|
|
def caselist_of(run):
|
|
"""-> (cases, source, None) for the cases a block's AFTER run was asked to run, or (None,
|
|
source, why) when that cannot be read. 50-cts-after.sh keeps a copy (runs/<b>/caselist.txt)
|
|
beside the path it ran (caselist.path), so the window's output reduces on its own after the
|
|
build tree that path names is gone; a named list that is gone or empty is NOT replaced by
|
|
the report's own cases (that would make a block of 0 - or of only what ran - look complete)."""
|
|
copy = run / "caselist.txt"
|
|
if copy.is_file():
|
|
src = copy
|
|
elif (run / "caselist.path").is_file():
|
|
src = Path((run / "caselist.path").read_text(encoding="utf-8", errors="replace").strip() or "(empty caselist.path)")
|
|
else:
|
|
return None, None, ("no runs/%s/caselist.txt or caselist.path: which cases the run was asked "
|
|
"to run is unknown" % run.name)
|
|
if not src.is_file():
|
|
return None, src, "the caselist the run used is gone (%s) and runs/%s/caselist.txt holds no copy" % (src, run.name)
|
|
cases = read_list(src)
|
|
if not cases:
|
|
return None, src, "the caselist the run used (%s) holds no case" % src
|
|
return cases, src, None
|
|
|
|
|
|
def block_scope(run, after):
|
|
"""The one completeness test of a finished block (the verdict below, and 50-cts-after.sh through
|
|
--check-block before it writes .done). -> (universe, source, missing, incomplete): universe =
|
|
the caselist minus run_cts's skipped.txt; `missing` says why there is no reading at all,
|
|
`incomplete` why the reading lacks cases."""
|
|
cases, src, why = caselist_of(run)
|
|
if why:
|
|
return [], src, [why], []
|
|
skipped = set(read_list(run / "skipped.txt"))
|
|
universe = [x for x in cases if x not in skipped]
|
|
if not universe:
|
|
return [], src, ["every case of %s is skipped: a block of 0 cases is no reading" % src], []
|
|
unrun = read_list(run / "unrun.txt")
|
|
without = [x for x in universe if x not in after]
|
|
incomplete = []
|
|
if unrun or without:
|
|
incomplete.append("%d case(s) without an AFTER result (unrun.txt %d)" % (len(without), len(unrun)))
|
|
return universe, src, [], incomplete
|
|
|
|
|
|
def cts_block(c, b, base_dir, canon_dir):
|
|
run = c / "runs" / b
|
|
if not c.is_dir():
|
|
return {"block": b, "verdict": "MISSING", "reasons": ["no cts/ directory: 50-cts-after.sh did not run"]}
|
|
if not (run / ".done").is_file():
|
|
return {"block": b, "verdict": "MISSING", "reasons": ["block not finished (no runs/%s/.done)" % b]}
|
|
after, why = load_results(c / ("report-%s.json" % b))
|
|
if why:
|
|
return {"block": b, "verdict": "MISSING", "reasons": [why]}
|
|
universe, src, missing, incomplete = block_scope(run, after)
|
|
if missing:
|
|
return {"block": b, "verdict": "MISSING", "reasons": missing}
|
|
base, why = load_results(base_dir / ("report-%s.json" % b))
|
|
if why:
|
|
return {"block": b, "verdict": "NO-BASE", "reasons": ["$BASE: " + why + " in " + str(base_dir)]}
|
|
unrun = read_list(run / "unrun.txt")
|
|
hung = read_list(run / "hung.txt")
|
|
# Like for like: both rates over the cases that have a BASE and an AFTER result.
|
|
both = [x for x in universe if x in after and x in base]
|
|
ra, rb = rate([after[x] for x in both]), rate([base[x] for x in both])
|
|
delta = None if ra["rate"] is None or rb["rate"] is None else (ra["rate"] - rb["rate"]) * 100.0
|
|
delta_results = (None if ra["results_rate"] is None or rb["results_rate"] is None
|
|
else (ra["results_rate"] - rb["results_rate"]) * 100.0)
|
|
new_crash = sorted(x for x in universe if after.get(x) in HARD and base.get(x) not in HARD)
|
|
new_crash += sorted(x for x in hung if x not in new_crash)
|
|
regress = sorted(x for x in universe if base.get(x) == "Pass" and after.get(x) not in (None, "Pass"))
|
|
fixed = sorted(x for x in universe if base.get(x) not in (None, "Pass") and after.get(x) == "Pass")
|
|
missing_base = sorted(x for x in universe if x not in base)
|
|
fail, notes, warnings = [], [], []
|
|
if new_crash:
|
|
fail.append("%d NEW crash(es)" % len(new_crash))
|
|
if rb["rate"] is None:
|
|
incomplete.append("$BASE has no Pass/Fail over this block's %d case(s): no contract rate to compare against"
|
|
% len(both))
|
|
elif ra["rate"] is None:
|
|
fail.append("AFTER has no Pass/Fail result (NS %d, W %d, X %d of %d): the contract rate cannot be formed "
|
|
"where $BASE's is %.3f%% - every $BASE Pass is lost" % (
|
|
ra["ns"], ra["warn"], ra["crash"], ra["results"], 100 * rb["rate"]))
|
|
if delta is not None and delta < -CTS_TOL_PP:
|
|
fail.append("delta %.3f pp < -%.1f pp (Pass/(Pass+Fail))" % (delta, CTS_TOL_PP))
|
|
if delta is not None and delta > CTS_TOL_PP:
|
|
notes.append("note: AFTER is %.3f pp ABOVE BASE (not a regression; review the fixed list)" % delta)
|
|
if missing_base:
|
|
warnings.append("%d case(s) have no $BASE result (e.g. %s): left out of both rates; a crash among them "
|
|
"still counts as NEW" % (len(missing_base), missing_base[0]))
|
|
if ra["ns"] != rb["ns"]:
|
|
warnings.append("NotSupported $BASE %d -> AFTER %d (not in the contract rate; a Pass that became NS is in "
|
|
"the Pass->not list)" % (rb["ns"], ra["ns"]))
|
|
canonical = canon_dir / ("p7-%s-gl46.txt" % b)
|
|
subset = set(read_list(src)) != set(read_list(canonical))
|
|
verdict = "FAIL" if fail else ("INCOMPLETE" if incomplete else "PASS")
|
|
return {"block": b, "cases": len(universe), "compared": len(both), "caselist": str(src),
|
|
"after": ra, "base": rb, "delta_pp": delta, "delta_pp_results_info": delta_results,
|
|
"new_crash": new_crash, "regressed": regress, "fixed": fixed, "missing_in_base": missing_base,
|
|
"unrun": len(unrun), "hung": hung, "verdict": verdict, "reasons": incomplete + fail + notes,
|
|
"warnings": warnings, "subset": subset}
|
|
|
|
|
|
def cts(out, base_dir, canon_dir):
|
|
c = out / "cts"
|
|
blocks = [cts_block(c, b, base_dir, canon_dir) for b in CTS_BLOCKS]
|
|
extra = sorted(p.stem[len("report-"):] for p in c.glob("report-*.json")
|
|
if p.stem[len("report-"):] not in CTS_BLOCKS) if c.is_dir() else []
|
|
verdicts = [b["verdict"] for b in blocks]
|
|
if "FAIL" in verdicts:
|
|
overall = "FAIL"
|
|
elif all(v == "PASS" for v in verdicts):
|
|
# Passing on a --limit subset (or on a list that is not the tools tree's) is not gate 5.
|
|
overall = "PASS-SUBSET" if any(b.get("subset") for b in blocks) else "PASS"
|
|
else:
|
|
overall = "INCOMPLETE"
|
|
return {"blocks": blocks, "overall": overall, "ubo": dict(UBO_BLOCK), "ignored_reports": extra,
|
|
"incomplete_blocks": [b["block"] for b in blocks if b["verdict"] not in ("PASS", "FAIL")]}
|
|
|
|
|
|
def print_cts(c):
|
|
print("== CTS AFTER (inproc x DirectVulkan) vs BASE (monolith x DirectVulkan, p7w1) ==")
|
|
print("gate (CONTRACT-P7 7.3): rate = Pass/(Pass+Fail) [NS, warnings W and crashes X are not in the denominator];")
|
|
print(" per block delta >= -%.1f pp and 0 NEW crash; all five blocks required. 'info' = the pre-ruling" % CTS_TOL_PP)
|
|
print(" Pass/(results-NS) delta (its rates on each block's 'info' line), for comparison only, never gating.")
|
|
print("%-14s %6s %-20s %8s %-20s %8s %9s %9s %5s %4s %5s %s" % (
|
|
"block", "cases", "BASE P/F/NS/W/X", "rate", "AFTER P/F/NS/W/X", "rate", "delta pp", "(info)",
|
|
"crash", "regr", "fixed", "verdict"))
|
|
|
|
def counts(r):
|
|
return "%d/%d/%d/%d/%d" % (r["pass"], r["fail"], r["ns"], r["warn"], r["crash"])
|
|
|
|
def pct(v):
|
|
return "%.3f%%" % (100 * v) if v is not None else "n/a"
|
|
|
|
def pp(v):
|
|
return "%+.3f" % v if v is not None else "n/a"
|
|
for b in c["blocks"]:
|
|
if "after" not in b:
|
|
print("%-14s %s: %s" % (b["block"], b["verdict"], "; ".join(b["reasons"])))
|
|
continue
|
|
print("%-14s %6d %-20s %8s %-20s %8s %9s %9s %5d %4d %5d %s%s" % (
|
|
b["block"], b["cases"], counts(b["base"]), pct(b["base"]["rate"]), counts(b["after"]),
|
|
pct(b["after"]["rate"]), pp(b["delta_pp"]), "(%s)" % pp(b["delta_pp_results_info"]),
|
|
len(b["new_crash"]), len(b["regressed"]), len(b["fixed"]), b["verdict"],
|
|
" (SUBSET)" if b["subset"] else ""))
|
|
print(" info pre-ruling Pass/(results-NS): BASE %s AFTER %s delta %s pp (not gating)" % (
|
|
pct(b["base"]["results_rate"]), pct(b["after"]["results_rate"]), pp(b["delta_pp_results_info"])))
|
|
for reason in b["reasons"]:
|
|
print(" ! " + reason)
|
|
for warning in b["warnings"]:
|
|
print("WARN %s: %s" % (b["block"], warning))
|
|
for x in b["new_crash"]:
|
|
print(" NEW CRASH " + x)
|
|
for x in b["regressed"][:40]:
|
|
print(" Pass->not " + x)
|
|
if len(b["regressed"]) > 40:
|
|
print(" ... %d more regressions in the JSON" % (len(b["regressed"]) - 40))
|
|
for x in b["fixed"][:20]:
|
|
print(" not->Pass " + x)
|
|
u = c["ubo"]
|
|
print("%-14s %s (%s)" % (u["block"], u["reasons"][0], u["cases"]))
|
|
if c["ignored_reports"]:
|
|
print(" note: report(s) outside the five gate blocks ignored: " + ", ".join(c["ignored_reports"]))
|
|
tail = ""
|
|
if c["incomplete_blocks"]:
|
|
tail += " (INCOMPLETE: %s)" % ", ".join("%s %s" % (b["block"], b["verdict"]) for b in c["blocks"]
|
|
if b["block"] in c["incomplete_blocks"])
|
|
if any(b.get("subset") for b in c["blocks"]):
|
|
tail += " (SUBSET run: %s not the tools tree's full caselist; not a gate verdict)" % ", ".join(
|
|
b["block"] for b in c["blocks"] if b.get("subset"))
|
|
print("CTS %s%s" % (c["overall"], tail))
|
|
|
|
|
|
def print_timing(out):
|
|
t = out / "timing.tsv"
|
|
if not t.is_file():
|
|
return
|
|
print("== TIMING ==")
|
|
for line in t.read_text().splitlines():
|
|
step, start, end, secs, note = (line.split("\t") + [""] * 5)[:5]
|
|
print(" %-18s %6ss %s -> %s %s" % (step, secs, start, end, note))
|
|
prog = out / "gate3" / "progress.tsv"
|
|
if prog.is_file():
|
|
per = {}
|
|
for line in prog.read_text().splitlines():
|
|
arm, case, rc, secs, _ = (line.split("\t") + [""] * 5)[:5]
|
|
per.setdefault(arm, []).append(int(secs))
|
|
for arm, v in per.items():
|
|
print(" gate3 %-11s %2d case invocations, mean %.0fs, total %ds" % (arm, len(v), sum(v) / len(v), sum(v)))
|
|
|
|
|
|
def check_block(run, report):
|
|
"""--check-block: 0 when the block is a complete AFTER reading, 3 when not (50-cts-after.sh)."""
|
|
after, why = load_results(report)
|
|
if why:
|
|
print("INCOMPLETE: " + why)
|
|
return 3
|
|
universe, src, missing, incomplete = block_scope(run, after)
|
|
if missing or incomplete:
|
|
print("INCOMPLETE: " + "; ".join(missing + incomplete))
|
|
return 3
|
|
print("complete: %d/%d case(s) of %s have a result" % (len(universe), len(universe), src))
|
|
return 0
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("stamp", nargs="?")
|
|
ap.add_argument("--logroot", default=os.environ.get("W2_LOGROOT", str(Path.home() / "w7/logs/p7w7")))
|
|
ap.add_argument("--tools", help="MobileGL tree holding compare_actuals.py and CTS-base (default: build.env)")
|
|
ap.add_argument("--json", help="write every section as JSON here (default <out>/verdict.json)")
|
|
ap.add_argument("--check-block", nargs=2, metavar=("RUN_DIR", "REPORT_JSON"),
|
|
help="only test one block's completeness (50-cts-after.sh, before it writes .done)")
|
|
args = ap.parse_args()
|
|
if args.check_block:
|
|
return check_block(Path(args.check_block[0]), Path(args.check_block[1]))
|
|
if not args.stamp:
|
|
ap.print_usage(sys.stderr)
|
|
return 2
|
|
out = Path(args.logroot) / args.stamp
|
|
if not out.is_dir():
|
|
print("no such window output: %s" % out, file=sys.stderr)
|
|
return 2
|
|
tools = args.tools or os.environ.get("W2_TOOLS_OVERRIDE") or read_kv(out / "build.env").get("BUILD_TREE")
|
|
if not tools:
|
|
print("no tools tree (--tools)", file=sys.stderr)
|
|
return 2
|
|
sys.path.insert(0, str(Path(tools) / "tools" / "trace_replay"))
|
|
sys.dont_write_bytecode = True # the tools tree is only read (it may be the read-only integration tree)
|
|
import compare_actuals as ca
|
|
base_dir = Path(tools) / "docs/Disaggregated/notes/p7/device-window-1/CTS-base"
|
|
report = {"stamp": args.stamp, "out": str(out)}
|
|
g3 = gate3(out, ca)
|
|
if g3:
|
|
print_gate3(g3)
|
|
else:
|
|
g3 = {"overall": "NOT-RUN"}
|
|
print("== GATE 3 ==\nGATE3 NOT-RUN (no gate3/cases.txt)")
|
|
print()
|
|
report["gate3"] = g3
|
|
b = bsl(out, ca)
|
|
if b:
|
|
print_bsl(b); print()
|
|
report["bsl"] = b
|
|
c = cts(out, base_dir, Path(tools) / "tools/cts/caselists")
|
|
print_cts(c); print()
|
|
report["cts"] = c
|
|
print_timing(out)
|
|
ok = g3["overall"] == "PASS" and c["overall"] == "PASS"
|
|
report["window"] = {"gate3": g3["overall"], "cts": c["overall"], "exit": 0 if ok else 1}
|
|
print("\nWINDOW2 GATE3 %s | CTS %s -> exit %d" % (g3["overall"], c["overall"], 0 if ok else 1))
|
|
Path(args.json or out / "verdict.json").write_text(json.dumps(report, indent=1) + "\n", encoding="utf-8")
|
|
return 0 if ok else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|