perf(display): skip preview work nobody reads (lazy checksum, 1 Hz snapshot writer) (#717)

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>
This commit is contained in:
Chuck
2026-10-01 20:27:20 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 1ecf3aba03
commit f72d69c2b0
13 changed files with 804 additions and 47 deletions
+19 -5
View File
@@ -33,6 +33,7 @@ from src.web_interface.error_handler import describe_exception
from src.common.path_safety import (
resolve_under, safe_path_component, safe_relative_parts,
)
from src.common import snapshot_policy
from werkzeug.exceptions import HTTPException
from src.exceptions import ConfigError
from src.plugin_system.plugin_catalog import PluginCatalog
@@ -797,12 +798,19 @@ def display_preview_generator():
"""Generate display preview updates from snapshot file"""
snapshot_path = display_preview.SNAPSHOT_PATH
# Viewer marker: this generator only runs while the broadcaster has
# subscribers (it exits with no clients), so touching the marker each
# loop tells the DISPLAY service a browser is actually watching — it
# only pays for full-rate PNG snapshot encodes while this stays fresh
# subscribers (it exits with no clients), so touching the marker tells
# the DISPLAY service a browser is actually watching — it only pays for
# viewer-rate PNG snapshot encodes while this stays fresh
# (see src/common/snapshot_policy.py).
viewer_marker_path = "/tmp/led_matrix_preview_viewer" # nosec B108 - fixed path matches display_manager
last_modified = None
# While there is a snapshot, its mtime is checked every
# VIEWER_POLL_INTERVAL, so a new frame goes out soon after it lands
# rather than up to a second later. The rest keeps its old once-a-second
# pace: the marker touch (the display counts it fresh for
# VIEWER_MARKER_FRESH_SEC), and passes with no snapshot or an error,
# each of which sends a message.
last_marker_touch = None
def _touch_viewer_marker():
try:
@@ -821,10 +829,15 @@ def display_preview_generator():
width, height = logical_size({})
while True:
delay = 1.0
try:
_touch_viewer_marker()
now = time.monotonic()
if last_marker_touch is None or now - last_marker_touch >= 1.0:
_touch_viewer_marker()
last_marker_touch = now
# Check if snapshot file exists and has been modified
if os.path.exists(snapshot_path):
delay = snapshot_policy.VIEWER_POLL_INTERVAL
current_modified = os.path.getmtime(snapshot_path)
# Only read if file is new or has been updated
@@ -842,10 +855,11 @@ def display_preview_generator():
yield display_preview.preview_payload(width, height, None)
except Exception:
delay = 1.0
app.logger.error("SSE generator error", exc_info=True)
yield {'error': 'An error occurred; see server logs'}
time.sleep(1.0) # the snapshot is re-read only when its mtime changes
time.sleep(delay) # the snapshot is re-read only when its mtime changes
# Logs generator for SSE
def logs_generator():