swayfx-mosaic/tooling/probes/step20c-buffer-identity/analyze.py

163 lines
7.9 KiB
Python

#!/usr/bin/env python3
"""Analyze one private fixed-geometry buffer run without assigning a frame to input."""
import hashlib
import json
import pathlib
import re
import statistics
import sys
from PIL import Image
if len(sys.argv) != 4:
raise SystemExit("usage: analyze.py LAB RUN_DIRECTORY BUFFER_DIRECTORY")
lab, run, buffers = map(pathlib.Path, sys.argv[1:])
lines = (run / "compositor.log").read_text(errors="replace").splitlines()
out = lab / "results"
out.mkdir(exist_ok=True)
def records(kind):
found = []
for line in lines:
if kind not in line:
continue
tail = line.split(kind, 1)[1]
values = dict(re.findall(r"([a-z_]+)=([^ ]+)", tail))
values["kind"] = kind
found.append(values)
return found
def save(name, value):
(out / (name + ".json")).write_text(json.dumps(value, indent=2) + "\n")
observed = records("BI_BUFFER")
direct = {int(x["id"]): x for x in records("BI_DIRECT")}
reads = {int(x["id"]): x for x in records("BI_READ")}
commits = {int(x["id"]): x for x in records("BI_COMMIT")}
event_buffers = {int(x["id"]): x for x in records("BI_EVENT_BUFFER")}
locked = {int(x["id"]): x for x in records("BI_LOCKED")}
held_present = {int(x["id"]): x for x in records("BI_PRESENT_HOLD")}
released = {int(x["id"]): x for x in records("BI_RELEASE")}
second_reads = {int(x["id"]): x for x in records("BI_PRESENT_READ")}
native_commits = records("PI_OUTPUT_COMMIT")
native_presents = records("PI_OUTPUT_PRESENT")
stages = records("BI_STAGE")
# The synchronous commit callback logs its scene observation immediately
# after the native event line. Do not attach a later logical IPC state.
scene = {}
for i, line in enumerate(lines):
if "PI_OUTPUT_COMMIT" not in line:
continue
v = dict(re.findall(r"([a-z_]+)=([^ ]+)", line.split("PI_OUTPUT_COMMIT", 1)[1]))
if i + 1 < len(lines) and "PI_SCENE" in lines[i + 1]:
z = dict(re.findall(r"([a-z_]+)=([^ ]+)", lines[i + 1].split("PI_SCENE", 1)[1]))
if z.get("output") == v.get("output"):
scene[(v["output"], v["commit_seq"])] = z
presents = {(v["output"], v["commit_seq"]): v for v in native_presents}
fingerprints = []
lifetimes = []
correlations = []
pixels = []
scene_states = []
for entry in observed:
ident = int(entry["id"])
commit = commits.get(ident, {})
key = (commit.get("output"), commit.get("commit_seq"))
current_scene = scene.get(key, {})
path = buffers / f"buffer-{ident}.xrgb"
readable = reads.get(ident, {}).get("status") == "READ" and path.is_file()
fp = {"id": ident, "read_status": reads.get(ident, {}).get("status", "UNOBSERVED"),
"direct_available": direct.get(ident, {}).get("available"),
"source_format": entry.get("dmabuf_format"), "read_format": "DRM_FORMAT_XRGB8888",
"width": 800, "height": 600, "stride": 3200,
"state_flags": entry.get("state"), "output_transform": "normal (fixed runner)",
"source_crop": entry.get("src"), "destination": entry.get("dst")}
if readable:
raw = path.read_bytes()
if len(raw) != 800 * 600 * 4:
raise ValueError(f"invalid snapshot length for {ident}: {len(raw)}")
# SceneFX FX glReadPixels rows start at the bottom. DRM XRGB8888 on
# this little-endian host stores B,G,R,X; normalize top-down RGB.
rgb = Image.frombytes("RGB", (800, 600), raw, "raw", "BGRX", 0, -1)
colors = dict((tuple(color), count) for count, color in rgb.getcolors(480001))
counts = {"A_yellow": colors.get((224, 192, 32), 0),
"B_green": colors.get((32, 208, 160), 0)}
fp.update(raw_sha256=hashlib.sha256(raw).hexdigest(),
rgb_sha256=hashlib.sha256(rgb.tobytes()).hexdigest(),
byte_count=len(raw), padding_bytes_per_row=0)
pixels.append({"id": ident, "output": key[0], "commit_seq": key[1],
"roi_output_pixels": [0, 0, 800, 600],
"scroll": current_scene.get("scroll"),
"tile_scene": current_scene.get("tile_scene"),
"clip": current_scene.get("clip"), "counts": counts})
later = buffers / f"buffer-{ident}-present.xrgb"
if later.exists():
later_raw = later.read_bytes()
fp["present_read_sha256"] = hashlib.sha256(later_raw).hexdigest()
fp["present_read_equal"] = later_raw == raw
fingerprints.append(fp)
lifetimes.append({"id": ident, "locked": ident in locked,
"present_while_locked": ident in held_present,
"release_reason": released.get(ident, {}).get("reason"),
"lock_age_ns": released.get(ident, {}).get("age_ns")})
correlations.append({"id": ident, "output": key[0], "commit_seq": key[1],
"accepted": commit.get("accepted") == "1",
"same_buffer_in_native_callback": event_buffers.get(ident, {}).get("same") == "1",
"native_present": key in presents,
"presented": presents.get(key, {}).get("presented"),
"read_status": fp["read_status"]})
scene_states.append({"id": ident, "output": key[0], "commit_seq": key[1], **current_scene})
summary = {"observed_buffers": len(observed), "read_buffers": sum(x["read_status"] == "READ" for x in fingerprints),
"direct_available": sum(x["direct_available"] == "1" for x in fingerprints),
"native_commits": len(native_commits), "native_presents": len(native_presents),
"accepted_sampled": sum(x["accepted"] for x in correlations),
"same_native_state_buffer": sum(x["same_buffer_in_native_callback"] for x in correlations),
"sampled_with_present": sum(x["native_present"] for x in correlations),
"sampled_released": sum(x["release_reason"] is not None for x in lifetimes),
"released_on_present": sum(x["release_reason"] == "present" for x in lifetimes),
"superseded_before_present": sum(x["release_reason"] == "superseded" for x in lifetimes),
"present_while_locked": sum(x["present_while_locked"] for x in lifetimes),
"second_reads": len(second_reads),
"second_reads_equal": sum(x.get("present_read_equal") is True for x in fingerprints),
"runner_failure": json.loads((run / "exit-status.json").read_text()).get("failure")}
build_ns, post_build_to_commit_ns, present_delay_ns = [], [], []
per_output = {}
for stage in stages:
name, phase = stage.get("output"), stage.get("phase")
now = int(stage["ns"])
if phase == "PRE_BUILD":
per_output[name] = {"pre": now}
elif phase == "POST_BUILD" and "pre" in per_output.get(name, {}):
per_output[name]["post"] = now
build_ns.append(now - per_output[name]["pre"])
elif phase == "COMMIT_ACCEPTED" and "post" in per_output.get(name, {}):
post_build_to_commit_ns.append(now - per_output[name]["post"])
for c in native_commits:
p = presents.get((c["output"], c["commit_seq"]))
if p:
present_delay_ns.append(int(p["ns"]) - int(c["ns"]))
summary["timing_median_ns"] = {
"build": int(statistics.median(build_ns)) if build_ns else None,
"post_build_to_commit": int(statistics.median(post_build_to_commit_ns)) if post_build_to_commit_ns else None,
"commit_to_present": int(statistics.median(present_delay_ns)) if present_delay_ns else None,
}
save("commits", native_commits)
save("presentations", native_presents)
save("buffer-lifetime", lifetimes)
save("buffer-fingerprints", fingerprints)
save("scene-states", {"stages": stages, "sampled_commits": scene_states})
save("correlation", correlations)
save("pixel-analysis", pixels)
save("matrix", {"fixed_geometry": "HEADLESS-1 (-800,0), HEADLESS-2 (0,0); 800x600; scale=1; normal",
"run": run.name, "sampled_snapshots": len(fingerprints),
"pointer_to_commit_identity": "UNOBSERVED", "physical_presentation": "NOT TESTED"})
save("summary", summary)
print(json.dumps(summary, indent=2))