From 512e38647a4d47a69b87249da0a45f4996d3b243 Mon Sep 17 00:00:00 2001 From: Chuck Date: Sun, 12 Jul 2026 12:24:31 -0400 Subject: [PATCH 1/2] perf: hot-path micro fixes in the render loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _check_wifi_status_message stat'd the status file on every render iteration (60+ fps) for a message whose lifetime is seconds; throttle the check to 1 Hz with a cached result. - Demote the per-iteration "Display active, processing mode" INFO to DEBUG and convert the remaining eager f-string logs to lazy % args — the devpi baseline showed ~9 journald lines/sec, which is both noise and SD-card wear. - Vegas cycle-end blank frame: hoist the inline PIL import and reuse a preallocated buffer instead of allocating per cycle wrap. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FqzC1nzTWL4kaqgMaQZFam --- src/display_controller.py | 20 +++++++++++++++----- src/vegas_mode/render_pipeline.py | 20 ++++++++++++++++---- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/display_controller.py b/src/display_controller.py index 8ae64f34f..7fc895afd 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -1635,7 +1635,7 @@ def run(self): self._sleep_with_plugin_updates(60) continue - logger.info(f"Display active, processing mode: {self.current_display_mode}") + logger.debug("Display active, processing mode: %s", self.current_display_mode) # Plugins update on their own schedules - no forced sync updates needed # Each plugin has its own update_interval and background services @@ -1803,7 +1803,7 @@ def run(self): if self.plugin_manager and hasattr(self.plugin_manager, 'health_tracker') and self.plugin_manager.health_tracker: should_skip = self.plugin_manager.health_tracker.should_skip_plugin(plugin_id) if should_skip: - logger.info(f"Skipping plugin {plugin_id} due to circuit breaker (mode: {active_mode})") + logger.info("Skipping plugin %s due to circuit breaker (mode: %s)", plugin_id, active_mode) display_result = False # Skip to next mode - let existing logic handle it manager_to_display = None @@ -1861,7 +1861,7 @@ def run(self): if isinstance(result, bool): display_result = result if not display_result: - logger.info(f"Plugin {plugin_id} display() returned False for mode {active_mode}") + logger.info("Plugin %s display() returned False for mode %s", plugin_id, active_mode) # Record success if display completed without exception if self.plugin_manager and hasattr(self.plugin_manager, 'health_tracker') and self.plugin_manager.health_tracker: @@ -2354,6 +2354,15 @@ def _check_wifi_status_message(self) -> Optional[Dict[str, Any]]: Returns None on any error or if message is expired/invalid. """ try: + # Throttle the existence stat to ~1 Hz: this runs on every render + # iteration (60+ fps), and the file usually doesn't exist — the + # status message's lifetime is measured in seconds anyway. + now = time.time() + if (now - getattr(self, '_wifi_status_check_ts', 0.0)) < 1.0: + return self._wifi_status_last_result + self._wifi_status_check_ts = now + self._wifi_status_last_result = None + # Check if file exists if not self.wifi_status_file or not self.wifi_status_file.exists(): return None @@ -2404,13 +2413,14 @@ def _check_wifi_status_message(self) -> Optional[Dict[str, Any]]: pass return None - # Message is valid and not expired - return { + # Message is valid and not expired — cache for the throttle window + self._wifi_status_last_result = { 'message': message, 'timestamp': timestamp, 'duration': duration, 'expires_at': expires_at } + return self._wifi_status_last_result except Exception as e: # Catch-all for any unexpected errors - log but don't break the display diff --git a/src/vegas_mode/render_pipeline.py b/src/vegas_mode/render_pipeline.py index 3851a4df0..5d12e60f5 100644 --- a/src/vegas_mode/render_pipeline.py +++ b/src/vegas_mode/render_pipeline.py @@ -66,6 +66,10 @@ def __init__( else display_manager.height ) + # Reusable blank frame for cycle-end pushes (allocated lazily, + # re-blacked before each reuse) + self._blank_frame = None + # ScrollHelper for optimized scrolling self.scroll_helper = ScrollHelper( self.display_width, @@ -234,11 +238,19 @@ def render_frame(self) -> bool: ) # Push blank immediately so the hardware never shows any # post-wrap content while the coordinator recomposes the - # next cycle (~100 ms). + # next cycle (~100 ms). The blank is allocated once and + # reused across cycle wraps (fresh paste each time in case + # a consumer drew on the previous one). try: - from PIL import Image as _Image - blank = _Image.new('RGB', (self.display_width, self.display_height)) - self.display_manager.image = blank + if self._blank_frame is None or self._blank_frame.size != ( + self.display_width, self.display_height): + self._blank_frame = Image.new( + 'RGB', (self.display_width, self.display_height)) + else: + self._blank_frame.paste( + (0, 0, 0), + (0, 0, self.display_width, self.display_height)) + self.display_manager.image = self._blank_frame self.display_manager.update_display() except Exception: logger.exception("Failed to write blank frame to display at cycle end") From dd5bc93d875ef5a6732328ff6b15b9b22d5a9767 Mon Sep 17 00:00:00 2001 From: Chuck Date: Mon, 13 Jul 2026 09:03:52 -0400 Subject: [PATCH 2/2] fix: initialise wifi-status throttle state in __init__ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codacy (pylint access-member-before-definition) on #403: the throttled early-return read _wifi_status_last_result relying on the non-local invariant that the first call always passes the throttle window and assigns it. Correct at runtime, but fragile — initialise both throttle fields in the constructor and drop the getattr fallback. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FqzC1nzTWL4kaqgMaQZFam --- src/display_controller.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/display_controller.py b/src/display_controller.py index 7fc895afd..402011e6f 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -199,6 +199,10 @@ def _follower_gated_update(): self.wifi_status_file = WIFI_STATUS_FILE self.wifi_status_active = False self.wifi_status_expires_at: Optional[float] = None + # _check_wifi_status_message throttle state (checked at frame rate, + # stat'd at most once per second) + self._wifi_status_check_ts = 0.0 + self._wifi_status_last_result: Optional[Dict[str, Any]] = None # Plugin display() signature cache — must be initialised before the plugin # loading loop below so the .pop() invalidation at load time is always safe. @@ -2357,8 +2361,9 @@ def _check_wifi_status_message(self) -> Optional[Dict[str, Any]]: # Throttle the existence stat to ~1 Hz: this runs on every render # iteration (60+ fps), and the file usually doesn't exist — the # status message's lifetime is measured in seconds anyway. + # Both attributes are initialised in __init__. now = time.time() - if (now - getattr(self, '_wifi_status_check_ts', 0.0)) < 1.0: + if (now - self._wifi_status_check_ts) < 1.0: return self._wifi_status_last_result self._wifi_status_check_ts = now self._wifi_status_last_result = None