mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +00:00
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:
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user