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:
Chuck
2026-09-11 08:42:49 -04:00
committed by GitHub
co-authored by Claude Opus 5
parent dcd6e39c96
commit 577f5501a6
2 changed files with 52 additions and 6 deletions
+26 -1
View File
@@ -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()