fix: reduce viz decay to 3 frames, add max-age safety cap, add FPS stats
Validate and Test / validate-patches (push) Failing after 12s

- DECAY_FRAMES default 8→3 (100ms visual trail)
- MAX_NOTE_AGE safety cap: force-expire notes after 15 frames
- Track note age separately from decay to prevent stale notes
- Add FPS and active_notes to viz stats for diagnostics
- Fix router reconnect: unregister old fd, not new one
This commit is contained in:
2026-06-24 03:27:10 -07:00
parent 0382450f8b
commit 5115518b5f
+22 -14
View File
@@ -7,7 +7,8 @@ import socket, msgpack, time, os, select, collections
VIZ_PORT = int(os.environ.get("VIZ_PORT", "8082")) VIZ_PORT = int(os.environ.get("VIZ_PORT", "8082"))
ROUTER_SOCK = os.environ.get("ROUTER_SOCK", "/var/run/arduino-router.sock") ROUTER_SOCK = os.environ.get("ROUTER_SOCK", "/var/run/arduino-router.sock")
DECAY_FRAMES = int(os.environ.get("DECAY_FRAMES", "8")) DECAY_FRAMES = int(os.environ.get("DECAY_FRAMES", "3"))
MAX_NOTE_AGE = int(os.environ.get("MAX_NOTE_AGE", "15"))
COLS = 13 COLS = 13
ROWS = 8 ROWS = 8
@@ -23,8 +24,6 @@ POLLOUT = select.POLLOUT
SOCK_BUFFER = 4096 SOCK_BUFFER = 4096
ROUTER_RECONNECT_DELAY = 0.1 ROUTER_RECONNECT_DELAY = 0.1
_frame_hooks = [] # for metrics
def pack_frame(pixels): def pack_frame(pixels):
bits = 0 bits = 0
@@ -87,7 +86,6 @@ def main():
next_frame_time = time.monotonic() next_frame_time = time.monotonic()
pending_frames = FrameQueue(maxsize=5) pending_frames = FrameQueue(maxsize=5)
router_registered = False
router_fd = router.fileno() router_fd = router.fileno()
_poll.register(router, POLLIN | POLLOUT) _poll.register(router, POLLIN | POLLOUT)
@@ -95,6 +93,8 @@ def main():
frames_dropped = 0 frames_dropped = 0
router_errors = 0 router_errors = 0
stats_last_write = time.monotonic() stats_last_write = time.monotonic()
fps_sample_start = time.monotonic()
fps_sample_frames = 0
while True: while True:
now = time.monotonic() now = time.monotonic()
@@ -127,7 +127,7 @@ def main():
vel = int(parts[3]) if len(parts) >= 4 else 0 vel = int(parts[3]) if len(parts) >= 4 else 0
msg_type = status & 0xF0 msg_type = status & 0xF0
if msg_type == 0x90 and vel > 0: if msg_type == 0x90 and vel > 0:
active_notes[note] = DECAY_FRAMES active_notes[note] = {"decay": DECAY_FRAMES, "age": 0}
elif msg_type == 0x80 or (msg_type == 0x90 and vel == 0): elif msg_type == 0x80 or (msg_type == 0x90 and vel == 0):
active_notes.pop(note, None) active_notes.pop(note, None)
@@ -153,26 +153,32 @@ def main():
router_errors += 1 router_errors += 1
pending_frames.push(frame) pending_frames.push(frame)
time.sleep(ROUTER_RECONNECT_DELAY) time.sleep(ROUTER_RECONNECT_DELAY)
old_fd = router_fd
try: try:
router.close() router.close()
except OSError: except OSError:
pass pass
router = connect_router() router = connect_router()
router_fd = router.fileno() router_fd = router.fileno()
_poll.unregister(router) try:
_poll.unregister(old_fd)
except (KeyError, OSError):
pass
_poll.register(router, POLLIN | POLLOUT) _poll.register(router, POLLIN | POLLOUT)
# Render frame when due # Render frame when due
if now >= next_frame_time: if now >= next_frame_time:
next_frame_time = now + frame_interval next_frame_time = now + frame_interval
fps_sample_frames += 1
pixels = [[0] * COLS for _ in range(ROWS)] pixels = [[0] * COLS for _ in range(ROWS)]
expired = [] expired = []
for note, decay in list(active_notes.items()): for note, state in list(active_notes.items()):
if decay <= 0: state["age"] += 1
state["decay"] -= 1
if state["decay"] <= 0 or state["age"] > MAX_NOTE_AGE:
expired.append(note) expired.append(note)
continue continue
active_notes[note] = decay - 1
note_min, note_max = 36, 96 note_min, note_max = 36, 96
row = 7 - int((note - note_min) / (note_max - note_min) * 7) row = 7 - int((note - note_min) / (note_max - note_min) * 7)
row = max(0, min(7, row)) row = max(0, min(7, row))
@@ -195,14 +201,14 @@ def main():
# Write stats periodically (every 2s) # Write stats periodically (every 2s)
if now - stats_last_write >= 2.0: if now - stats_last_write >= 2.0:
_write_stats(frames_sent, frames_dropped, pending_frames) fps = fps_sample_frames / (now - fps_sample_start) if (now - fps_sample_start) > 0 else 0
_write_stats(frames_sent, frames_dropped, pending_frames, fps, len(active_notes))
stats_last_write = now stats_last_write = now
fps_sample_start = now
fps_sample_frames = 0
_stats_last_write = 0.0 def _write_stats(sent, dropped, pending, fps, active_count):
def _write_stats(sent, dropped, pending):
import json import json
try: try:
with open(STATS_FILE, "w") as f: with open(STATS_FILE, "w") as f:
@@ -211,6 +217,8 @@ def _write_stats(sent, dropped, pending):
"frames_dropped": dropped, "frames_dropped": dropped,
"pending_queue": len(pending), "pending_queue": len(pending),
"queue_full": pending.full(), "queue_full": pending.full(),
"fps": round(fps, 1),
"active_notes": active_count,
"ts": time.time() "ts": time.time()
}, f) }, f)
except OSError: except OSError: