perf(tui): cache event renders and eliminate redundant work during running state

Three more performance issues in the running state hot path:

Per-event render cache in _get_rendered_events_content: every 350ms tick
during active streaming caused a full re-render of all events in the
conversation — every chat message through AgentMessageRenderer (including
Pygments syntax highlighting for code blocks) and every tool event. Chat
messages and completed/failed tool events are now cached by (event_id,
status) and only re-rendered when their status changes. Running tool events
are re-rendered each tick as their content may still update.

Skip duplicate _update_agent_status_display in _update_ui_from_tracer when
the dot animation timer is active: _animate_dots (60ms) already calls it
for "running" agents, so the unconditional call from _update_ui_from_tracer
(350ms) was redundant, doubling the widget update rate during active scans.

Fix _get_agent_name_for_vulnerability to use per-agent tool execution index
instead of scanning all tool_executions, consistent with the other O(N)
scan fixes from the previous commit.
This commit is contained in:
Matthew Flanagan 2026-05-21 17:35:11 +10:00
parent 3508da4beb
commit 4b17b57696
No known key found for this signature in database

View file

@ -715,6 +715,7 @@ class StrixTUIApp(App): # type: ignore[misc]
self._streaming_render_cache: dict[str, tuple[int, Any]] = {}
self._last_streaming_len: dict[str, int] = {}
self._event_render_cache: dict[str, Any] = {}
self._scan_thread: threading.Thread | None = None
self._scan_stop_event = threading.Event()
@ -925,7 +926,8 @@ class StrixTUIApp(App): # type: ignore[misc]
self._update_chat_view()
self._update_agent_status_display()
if self._dot_animation_timer is None:
self._update_agent_status_display()
self._update_stats_display()
@ -1091,11 +1093,24 @@ class StrixTUIApp(App): # type: ignore[misc]
for event in events:
content: Any = None
event_id = event["id"]
if event["type"] == "chat":
content = self._render_chat_content(event["data"])
if event_id in self._event_render_cache:
content = self._event_render_cache[event_id]
else:
content = self._render_chat_content(event["data"])
if content is not None:
self._event_render_cache[event_id] = content
elif event["type"] == "tool":
content = self._render_tool_content_simple(event["data"])
status = event["data"].get("status", "")
cache_key = f"{event_id}_{status}"
if cache_key in self._event_render_cache:
content = self._event_render_cache[cache_key]
else:
content = self._render_tool_content_simple(event["data"])
if content is not None and status in ("completed", "failed", "error"):
self._event_render_cache[cache_key] = content
if content:
if renderables:
@ -1344,13 +1359,13 @@ class StrixTUIApp(App): # type: ignore[misc]
def _get_agent_name_for_vulnerability(self, report_id: str) -> str | None:
"""Find the agent name that created a vulnerability report."""
for _exec_id, tool_data in list(self.tracer.tool_executions.items()):
if tool_data.get("tool_name") == "create_vulnerability_report":
result = tool_data.get("result", {})
if isinstance(result, dict) and result.get("report_id") == report_id:
agent_id = tool_data.get("agent_id")
if agent_id and agent_id in self.tracer.agents:
name: str = self.tracer.agents[agent_id].get("name", "Unknown Agent")
for agent_id, agent_data in self.tracer.agents.items():
for exec_id in agent_data.get("tool_executions", []):
tool_data = self.tracer.tool_executions.get(exec_id)
if tool_data and tool_data.get("tool_name") == "create_vulnerability_report":
result = tool_data.get("result", {})
if isinstance(result, dict) and result.get("report_id") == report_id:
name: str = agent_data.get("name", "Unknown Agent")
return name
return None
@ -1494,6 +1509,7 @@ class StrixTUIApp(App): # type: ignore[misc]
self._displayed_events.clear()
self._streaming_render_cache.clear()
self._last_streaming_len.clear()
self._event_render_cache.clear()
self.call_later(self._update_chat_view)
self._update_agent_status_display()