fix: reduce CPU usage, fix Vegas refresh, throttle high-FPS ticks (#304)

* fix: reduce CPU usage, fix Vegas mid-cycle refresh, and throttle high-FPS plugin ticks

Web UI Info plugin was causing 90%+ CPU on RPi4 due to frequent subprocess
calls and re-rendering. Fixed by: trying socket-based IP detection first
(zero subprocess overhead), caching AP mode checks with 60s TTL, reducing
IP refresh from 30s to 5m, caching rendered display images, and loading
fonts once at init.

Vegas mode was not updating the display mid-cycle because hot_swap_content()
reset the scroll position to 0 on every recomposition. Now saves and
restores scroll position for mid-cycle updates.

High-FPS display loop was calling _tick_plugin_updates() 125x/sec with no
benefit. Added throttled wrapper that limits to 1 call/sec.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR review — respect plugin update_interval, narrow exception handlers

Make _tick_plugin_updates_throttled default to no-throttle (min_interval=0)
so plugin-configured update_interval values are never silently capped.
The high-FPS call site passes an explicit 1.0s interval.

Narrow _load_font exception handler from bare Exception to
FileNotFoundError | OSError so unexpected errors surface.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(vegas): scale scroll position proportionally on mid-cycle hot-swap

When content width changes during a mid-cycle recomposition (e.g., a
plugin gains or loses items), blindly restoring the old scroll_position
and total_distance_scrolled could overshoot the new total_scroll_width
and trigger immediate false completion. Scale both values proportionally
to the new width and clamp scroll_position to stay in bounds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-04-02 08:46:52 -04:00
committed by GitHub
parent 5ea2acd897
commit efe6b1fe23
3 changed files with 133 additions and 91 deletions

View File

@@ -32,7 +32,10 @@ class DisplayController:
def __init__(self):
start_time = time.time()
logger.info("Starting DisplayController initialization")
# Throttle tracking for _tick_plugin_updates in high-FPS loops
self._last_plugin_tick_time = 0.0
# Initialize ConfigManager and wrap with ConfigService for hot-reload
config_manager = ConfigManager()
enable_hot_reload = os.environ.get('LEDMATRIX_HOT_RELOAD', 'true').lower() == 'true'
@@ -719,6 +722,22 @@ class DisplayController:
except Exception: # pylint: disable=broad-except
logger.exception("Error running scheduled plugin updates")
def _tick_plugin_updates_throttled(self, min_interval: float = 0.0):
"""Throttled version of _tick_plugin_updates for high-FPS loops.
Args:
min_interval: Minimum seconds between calls. When <= 0 the
call passes straight through to _tick_plugin_updates so
plugin-configured update_interval values are never capped.
"""
if min_interval <= 0:
self._tick_plugin_updates()
return
now = time.time()
if now - self._last_plugin_tick_time >= min_interval:
self._last_plugin_tick_time = now
self._tick_plugin_updates()
def _sleep_with_plugin_updates(self, duration: float, tick_interval: float = 1.0):
"""Sleep while continuing to service plugin update schedules."""
if duration <= 0:
@@ -1787,7 +1806,7 @@ class DisplayController:
logger.exception("Error during display update")
time.sleep(display_interval)
self._tick_plugin_updates()
self._tick_plugin_updates_throttled(min_interval=1.0)
self._poll_on_demand_requests()
self._check_on_demand_expiration()

View File

@@ -275,13 +275,19 @@ class RenderPipeline:
"""
Hot-swap to new composed content.
Called when staging buffer has updated content.
Swaps atomically to prevent visual glitches.
Called when staging buffer has updated content or pending updates exist.
Preserves scroll position for mid-cycle updates to prevent visual jumps.
Returns:
True if swap occurred
"""
try:
# Save scroll position for mid-cycle updates
saved_position = self.scroll_helper.scroll_position
saved_total_distance = self.scroll_helper.total_distance_scrolled
saved_total_width = max(1, self.scroll_helper.total_scroll_width)
was_mid_cycle = not self._cycle_complete
# Process any pending updates
self.stream_manager.process_updates()
self.stream_manager.swap_buffers()
@@ -289,7 +295,19 @@ class RenderPipeline:
# Recompose with updated content
if self.compose_scroll_content():
self.stats['hot_swaps'] += 1
logger.debug("Hot-swap completed")
# Restore scroll position for mid-cycle updates so the
# scroll continues from where it was instead of jumping to 0
if was_mid_cycle:
new_total_width = max(1, self.scroll_helper.total_scroll_width)
progress_ratio = min(saved_total_distance / saved_total_width, 0.999)
self.scroll_helper.total_distance_scrolled = progress_ratio * new_total_width
self.scroll_helper.scroll_position = min(
saved_position,
float(new_total_width - 1)
)
self.scroll_helper.scroll_complete = False
self._cycle_complete = False
logger.debug("Hot-swap completed (mid_cycle_restore=%s)", was_mid_cycle)
return True
return False