proper tests

This commit is contained in:
2026-04-10 18:29:58 -03:00
parent e906b0a963
commit ea9dbf8772
16 changed files with 1077 additions and 15 deletions

164
ctrl/bench_delay.py Normal file
View File

@@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""Live scene detection latency benchmark (M4).
Measures time from a triggered visual change on the sender to a new JPEG
appearing in the receiver's frames/ directory.
Usage (run on receiver, sender accessible via SSH):
python ctrl/bench_delay.py --session-dir data/sessions/CURRENT --sender mariano@sender
python ctrl/bench_delay.py --frames-dir data/sessions/CURRENT/frames --sender mariano@sender
How it works:
1. Records the current frame count in frames/index.json
2. SSH to sender, triggers a visual change (xdotool workspace switch)
3. Polls frames/index.json for a new entry (or watches via mtime)
4. Measures wall-clock difference = scene detection latency
For repeated measurements, use --repeat N with --interval S between triggers.
"""
import argparse
import json
import logging
import os
import subprocess
import sys
import time
from pathlib import Path
log = logging.getLogger("bench_delay")
def get_frame_count(frames_dir: Path) -> int:
index = frames_dir / "index.json"
if not index.exists():
return 0
try:
return len(json.loads(index.read_text()))
except (json.JSONDecodeError, ValueError):
return 0
def get_latest_frame_mtime(frames_dir: Path) -> float:
index = frames_dir / "index.json"
if not index.exists():
return 0.0
return index.stat().st_mtime
def trigger_scene_change(sender: str, method: str = "workspace") -> float:
"""Trigger a visual change on the sender. Returns wall-clock time of trigger."""
if method == "workspace":
# xdotool switch workspace — causes a full-screen visual change
cmd = ["ssh", sender, "DISPLAY=:0 xdotool key super+Right"]
elif method == "color":
# Flash a fullscreen color using xterm (more dramatic change)
cmd = ["ssh", sender,
"DISPLAY=:0 bash -c 'xterm -fullscreen -bg red -e sleep 0.5 &'"]
else:
log.error("Unknown trigger method: %s", method)
sys.exit(1)
t = time.monotonic()
wall = time.time()
try:
subprocess.run(cmd, timeout=5, capture_output=True)
except subprocess.TimeoutExpired:
log.warning("SSH trigger timed out")
return wall
def wait_for_new_frame(frames_dir: Path, initial_count: int,
timeout: float = 15.0, poll_interval: float = 0.1) -> float | None:
"""Wait for a new frame to appear. Returns wall-clock time when detected, or None."""
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
count = get_frame_count(frames_dir)
if count > initial_count:
return time.time()
time.sleep(poll_interval)
return None
def run_measurement(frames_dir: Path, sender: str, method: str) -> dict:
initial_count = get_frame_count(frames_dir)
trigger_wall = trigger_scene_change(sender, method)
detected_wall = wait_for_new_frame(frames_dir, initial_count)
if detected_wall is None:
return {"trigger_wall": trigger_wall, "latency_s": None, "timed_out": True}
latency = detected_wall - trigger_wall
return {
"trigger_wall": trigger_wall,
"detected_wall": detected_wall,
"latency_s": round(latency, 3),
"timed_out": False,
}
def main():
parser = argparse.ArgumentParser(description="Scene detection latency benchmark")
parser.add_argument("--frames-dir", type=Path, help="Path to frames/ directory")
parser.add_argument("--session-dir", type=Path, help="Path to session directory")
parser.add_argument("--sender", required=True, help="SSH target for sender (user@host)")
parser.add_argument("--method", default="workspace", choices=["workspace", "color"],
help="How to trigger visual change")
parser.add_argument("--repeat", type=int, default=3, help="Number of measurements")
parser.add_argument("--interval", type=float, default=5.0, help="Seconds between triggers")
parser.add_argument("--json", action="store_true", help="Output JSON")
args = parser.parse_args()
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
datefmt="%H:%M:%S",
)
if args.session_dir:
frames_dir = args.session_dir / "frames"
elif args.frames_dir:
frames_dir = args.frames_dir
else:
parser.error("Provide --frames-dir or --session-dir")
return
if not frames_dir.exists():
log.error("Frames dir not found: %s", frames_dir)
sys.exit(1)
results = []
for i in range(args.repeat):
if i > 0:
time.sleep(args.interval)
log.info("Trigger %d/%d...", i + 1, args.repeat)
r = run_measurement(frames_dir, args.sender, args.method)
if r["timed_out"]:
log.warning("TIMEOUT (no frame in 15s)")
else:
log.info(" latency: %ss", r["latency_s"])
results.append(r)
latencies = [r["latency_s"] for r in results if r["latency_s"] is not None]
if args.json:
print(json.dumps({"measurements": results, "summary": {
"count": len(latencies),
"avg_s": round(sum(latencies) / len(latencies), 3) if latencies else None,
"min_s": round(min(latencies), 3) if latencies else None,
"max_s": round(max(latencies), 3) if latencies else None,
"timeouts": sum(1 for r in results if r["timed_out"]),
}}, indent=2))
else:
log.info("M4 Scene detection latency:")
if latencies:
log.info(" avg: %.1fs", sum(latencies) / len(latencies))
log.info(" min: %.1fs", min(latencies))
log.info(" max: %.1fs", max(latencies))
timeouts = sum(1 for r in results if r["timed_out"])
if timeouts:
log.warning(" timeouts: %d/%d", timeouts, len(results))
if __name__ == "__main__":
main()