Mid-scroll, update_display() no longer checksums every frame: it asks is_currently_scrolling() once per frame and reuses the answer, hashes only when dirty tracking can skip a static frame, and the preview snapshot asks its policy first and hashes only when a write or touch could follow (decide() is monotone, pinned by a property test). With the preview open the snapshot is written at most once a second (VIEWER_INTERVAL 1.0 s, was 0.2 s; the SSE stream re-read it once a second, so four encodes in five went unread); the stream now polls its mtime every 0.25 s (VIEWER_POLL_INTERVAL), so the preview stays about as fresh. The PNG is written at compress_level=1. --preview soaks are not comparable across this change.
Merged with #716: _scan_segments takes the frame's one scrolling answer and #716's static-handover pass-through, as soaked on ledpi (A B B A, 20 min each: main 0.118% / 0.113% late, with #716 and this 0.107% / 0.104%).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The display service PNG-encoded its frame to /tmp/led_matrix_preview.png
at 5 fps, 24/7 — identical frames, no viewers, per-call imports and a
chmod every write. On the devpi baseline the display service idles at
~92% CPU; this was one of its biggest fixed costs.
- New pure policy (src/common/snapshot_policy.py, unit-tested off-Pi):
WRITE changed frames at full rate only while a viewer is watching,
at a 30s idle cadence otherwise; NEVER re-encode unchanged frames —
bump mtime (os.utime) every 20s instead, keeping the health check's
snapshot-age liveness proxy (60s threshold in api_v3) green. Cross-
referencing comments guard the two constants.
- Viewer detection: the web SSE display broadcaster (which only runs
while browsers are subscribed) touches /tmp/led_matrix_preview_viewer
each loop; the display service stats it at most 1/s. On viewer
arrival the write clock resets so the first frame lands within ~1s.
- Hoisted the per-call pathlib/permission_utils imports; directory
permissions ensured once instead of every frame.
Claude-Session: https://claude.ai/code/session_01FqzC1nzTWL4kaqgMaQZFam
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>