From efe76d3addc002547ed1a3751e0dfc61d503ae03 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Mon, 13 Jul 2026 09:23:25 -0400 Subject: [PATCH] perf: hot-path micro fixes in the render loop (#403) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf: hot-path micro fixes in the render loop - _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 * fix: initialise wifi-status throttle state in __init__ 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 --------- Co-authored-by: Chuck Co-authored-by: Claude Fable 5 --- src/display_controller.py | 25 ++++++++++++++++++++----- src/vegas_mode/render_pipeline.py | 20 ++++++++++++++++---- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/display_controller.py b/src/display_controller.py index d6211ef7..f924db81 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -199,6 +199,10 @@ class DisplayController: 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. @@ -1684,7 +1688,7 @@ class DisplayController: 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 @@ -1852,7 +1856,7 @@ class DisplayController: 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 @@ -1910,7 +1914,7 @@ class DisplayController: 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: @@ -2403,6 +2407,16 @@ class DisplayController: 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. + # Both attributes are initialised in __init__. + now = time.time() + 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 + # Check if file exists if not self.wifi_status_file or not self.wifi_status_file.exists(): return None @@ -2453,13 +2467,14 @@ class DisplayController: 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 78452a27..7c9b7c90 100644 --- a/src/vegas_mode/render_pipeline.py +++ b/src/vegas_mode/render_pipeline.py @@ -66,6 +66,10 @@ class RenderPipeline: 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 @@ class RenderPipeline: ) # 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")