mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 14:25:08 +00:00
perf(plugins): stop re-deriving a display() signature the caller already cached (#549)
display_controller resolves once, and caches, whether a plugin's display() takes a display_mode keyword -- self._plugin_accepts_display_mode, populated right before the dispatch. It then handed the executor a types.SimpleNamespace wrapping a closure, and execute_display() ran inspect.signature() on that to work out the same thing. Because the SimpleNamespace is rebuilt per call, the callable was new every time, so nothing inside the executor could ever cache it either. Measured at ~39us per dispatch on a Pi 4, for a value the caller had a line earlier. execute_display() now takes accepts_display_mode, falling back to inspecting only when a caller does not pass it, so existing callers are unaffected. Also documents two things that read as bugs and are not: - execute_with_timeout()'s timeout is advisory. Nothing cancels the thread -- Python cannot -- so on expiry the operation runs to completion in the background and only the caller gives up. A permanently hung plugin leaks a daemon thread per attempt. This is why callers holding a lock across the call must release it from inside the wrapped callable, as run()'s _release_display_lock already does. - Only the first display() of each mode goes through the executor; the per-frame loops call display() directly. That is deliberate: a thread per frame would cost more than an advisory timeout buys. Both loops now say so, so the asymmetry does not read as an oversight. Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2169,7 +2169,14 @@ class DisplayController:
|
||||
types.SimpleNamespace(display=_display_target),
|
||||
plugin_id,
|
||||
force_clear=self.force_change,
|
||||
display_mode=active_mode if _accepts_display_mode else None
|
||||
display_mode=active_mode if _accepts_display_mode else None,
|
||||
# Already resolved and cached above.
|
||||
# Without this the executor re-derives
|
||||
# it with inspect.signature() against
|
||||
# the SimpleNamespace built two lines
|
||||
# up -- a fresh callable every call, so
|
||||
# nothing there can ever cache.
|
||||
accepts_display_mode=_accepts_display_mode
|
||||
)
|
||||
except Exception: # pragma: no cover - defensive;
|
||||
# execute_display catches everything
|
||||
@@ -2492,6 +2499,15 @@ class DisplayController:
|
||||
1.0 / display_interval
|
||||
)
|
||||
|
||||
# Deliberate: frames after the first call
|
||||
# display() directly rather than through
|
||||
# PluginExecutor. The executor spawns a thread per
|
||||
# call, which at this loop's frame rate would cost
|
||||
# more than the advisory timeout it buys -- and
|
||||
# that timeout cannot cancel a hung plugin anyway
|
||||
# (see execute_with_timeout). The first dispatch
|
||||
# above still goes through it, so load-time
|
||||
# failures are still caught and recorded.
|
||||
while True:
|
||||
_frame_start = time.perf_counter()
|
||||
try:
|
||||
@@ -2564,6 +2580,15 @@ class DisplayController:
|
||||
display_interval
|
||||
)
|
||||
|
||||
# Deliberate: frames after the first call
|
||||
# display() directly rather than through
|
||||
# PluginExecutor. The executor spawns a thread per
|
||||
# call, which at this loop's frame rate would cost
|
||||
# more than the advisory timeout it buys -- and
|
||||
# that timeout cannot cancel a hung plugin anyway
|
||||
# (see execute_with_timeout). The first dispatch
|
||||
# above still goes through it, so load-time
|
||||
# failures are still caught and recorded.
|
||||
while True:
|
||||
time.sleep(display_interval)
|
||||
self._tick_plugin_updates()
|
||||
|
||||
Reference in New Issue
Block a user