proper tests
This commit is contained in:
164
ctrl/bench_delay.py
Normal file
164
ctrl/bench_delay.py
Normal 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()
|
||||
Reference in New Issue
Block a user