mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 14:25:08 +00:00
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>
88 lines
3.9 KiB
Python
88 lines
3.9 KiB
Python
"""Snapshot write policy for the display preview mirror.
|
|
|
|
The display service mirrors frames to /tmp/led_matrix_preview.png, which
|
|
serves two consumers with different needs:
|
|
|
|
- The web UI's live preview (SSE reader in web_interface/app.py) wants
|
|
fresh frames — but only while a browser is actually watching.
|
|
- The health check (web_interface/blueprints/api_v3/misc.py, hardware status)
|
|
uses the file's AGE as a liveness proxy: age >= 60s reads as degraded.
|
|
|
|
PNG-encoding every frame at 5 fps forever — identical frames, no viewers —
|
|
was one of the biggest fixed CPU costs on the Pi. This module is the pure
|
|
decision logic (extracted so it's unit-testable off-Pi; display_manager
|
|
imports rgbmatrix unconditionally and can't be):
|
|
|
|
WRITE — encode + atomically replace the snapshot file
|
|
TOUCH — os.utime only: keeps the health-check mtime fresh and lets
|
|
the SSE reader (mtime-gated) resend at a low rate, without
|
|
paying for a PNG encode of an unchanged frame
|
|
SKIP — do nothing
|
|
|
|
Policy:
|
|
- With a fresh viewer marker: changed frames write at up to 1/VIEWER_INTERVAL.
|
|
- Without viewers: changed frames still write at 1/IDLE_INTERVAL so the
|
|
preview page shows something recent on open.
|
|
- Unchanged frames are never re-encoded; the mtime is touched every
|
|
TOUCH_INTERVAL so the health check (60s threshold) never degrades.
|
|
|
|
The writer and the SSE reader (web_interface/app.py) have two periods, not
|
|
one shared value. The reader sends each write it sees, so the preview shows
|
|
at most one frame per VIEWER_INTERVAL. (That was 0.2 s while the reader
|
|
slept 1 s between reads, so four encodes in five were overwritten unread.)
|
|
The reader checks the file's mtime every VIEWER_POLL_INTERVAL, which is only
|
|
a stat, and so sends each write within that long of it landing. Equal
|
|
periods would alias: two unsynchronised 1 s clocks leave the preview up to a
|
|
second stale, and now and then 2 s between frames.
|
|
|
|
decide() is monotone in frame_changed: SKIP for a changed frame means SKIP
|
|
for an unchanged one. DisplayManager relies on that to skip hashing the
|
|
frame when even a changed one would be skipped; test_snapshot_policy.py
|
|
checks it.
|
|
|
|
If any constant here changes, re-check the health threshold in
|
|
api_v3/misc.py (get_hardware_status) — TOUCH_INTERVAL must stay well under it.
|
|
"""
|
|
|
|
from enum import Enum
|
|
|
|
# Snapshot cadence with a browser preview open (seconds): the shortest gap
|
|
# between two preview frames.
|
|
VIEWER_INTERVAL = 1.0
|
|
# How often the web SSE reader checks the snapshot's mtime (seconds). Must
|
|
# stay well under VIEWER_INTERVAL -- half of it at most -- or the two clocks
|
|
# alias (see above).
|
|
VIEWER_POLL_INTERVAL = 0.25
|
|
# Snapshot cadence with no viewers — cheap freshness for page-open (seconds).
|
|
IDLE_INTERVAL = 30.0
|
|
# Max age of the last write/touch before bumping mtime for the health
|
|
# check. MUST stay well under get_hardware_status's 60s degraded threshold.
|
|
TOUCH_INTERVAL = 20.0
|
|
# A viewer marker older than this no longer counts as a live viewer.
|
|
VIEWER_MARKER_FRESH_SEC = 5.0
|
|
|
|
|
|
class SnapshotAction(Enum):
|
|
WRITE = "write"
|
|
TOUCH = "touch"
|
|
SKIP = "skip"
|
|
|
|
|
|
def decide(now: float, last_write_ts: float, last_touch_ts: float,
|
|
viewer_fresh: bool, frame_changed: bool) -> SnapshotAction:
|
|
"""Decide what to do with the current frame.
|
|
|
|
Args:
|
|
now: current monotonic-ish timestamp (same clock as the ts args)
|
|
last_write_ts: when a frame was last actually encoded+written
|
|
last_touch_ts: when the file mtime was last bumped (write or touch)
|
|
viewer_fresh: a browser preview is currently watching
|
|
frame_changed: the frame differs from the last WRITTEN frame
|
|
"""
|
|
interval = VIEWER_INTERVAL if viewer_fresh else IDLE_INTERVAL
|
|
if frame_changed and (now - last_write_ts) >= interval:
|
|
return SnapshotAction.WRITE
|
|
if (now - max(last_write_ts, last_touch_ts)) >= TOUCH_INTERVAL:
|
|
return SnapshotAction.TOUCH
|
|
return SnapshotAction.SKIP
|