feat(gui): Implement event-driven API metrics updates and decouple from render loop

This commit is contained in:
2026-02-23 16:38:23 -05:00
parent 0c27aa6c6b
commit 2dd6145bd8
4 changed files with 138 additions and 57 deletions

123
gui.py
View File

@@ -496,6 +496,11 @@ class App:
self._is_script_blinking = False
self._script_blink_start_time = 0.0
# Subscribe to API lifecycle events
ai_client.events.on("request_start", self._on_api_event)
ai_client.events.on("response_received", self._on_api_event)
ai_client.events.on("tool_execution", self._on_api_event)
self.perf_monitor = PerformanceMonitor()
self.perf_history = {
"frame_time": [0.0] * 100,
@@ -822,40 +827,44 @@ class App:
total = usage["input_tokens"] + usage["output_tokens"]
dpg.set_value("ai_token_usage", f"Tokens: {total} (In: {usage['input_tokens']} Out: {usage['output_tokens']})")
def _update_telemetry_panel(self):
"""Updates the token budget visualizer in the Provider panel."""
# Update history bleed stats for all providers (throttled)
now = time.time()
if now - self._last_bleed_update_time > 2.0:
self._last_bleed_update_time = now
stats = ai_client.get_history_bleed_stats()
if dpg.does_item_exist("token_budget_bar"):
percentage = stats.get("percentage", 0.0)
dpg.set_value("token_budget_bar", percentage / 100.0 if percentage else 0.0)
if dpg.does_item_exist("token_budget_label"):
current = stats.get("current", 0)
limit = stats.get("limit", 0)
dpg.set_value("token_budget_label", f"{current:,} / {limit:,}")
def _on_api_event(self, *args, **kwargs):
"""Callback for ai_client events. Queues a telemetry refresh on the main thread."""
with self._pending_gui_tasks_lock:
self._pending_gui_tasks.append({"action": "refresh_api_metrics"})
# Update Gemini-specific cache stats (throttled with diagnostics)
if now - self._last_diag_update_time > 10.0:
self._last_diag_update_time = now
if dpg.does_item_exist("gemini_cache_label"):
if self.current_provider == "gemini":
try:
cache_stats = ai_client.get_gemini_cache_stats()
count = cache_stats.get("cache_count", 0)
size_bytes = cache_stats.get("total_size_bytes", 0)
size_kb = size_bytes / 1024.0
text = f"Gemini Caches: {count} ({size_kb:.1f} KB)"
dpg.set_value("gemini_cache_label", text)
dpg.configure_item("gemini_cache_label", show=True)
except Exception as e:
# If the API call fails, just hide the label
dpg.configure_item("gemini_cache_label", show=False)
else:
def _refresh_api_metrics(self):
"""Updates the token budget and cache stats visualizers."""
self._last_bleed_update_time = time.time()
# History bleed
stats = ai_client.get_history_bleed_stats()
if dpg.does_item_exist("token_budget_bar"):
percentage = stats.get("percentage", 0.0)
dpg.set_value("token_budget_bar", percentage / 100.0 if percentage else 0.0)
if dpg.does_item_exist("token_budget_label"):
current = stats.get("current", 0)
limit = stats.get("limit", 0)
dpg.set_value("token_budget_label", f"{current:,} / {limit:,}")
# Gemini cache
if dpg.does_item_exist("gemini_cache_label"):
if self.current_provider == "gemini":
try:
cache_stats = ai_client.get_gemini_cache_stats()
count = cache_stats.get("cache_count", 0)
size_bytes = cache_stats.get("total_size_bytes", 0)
size_kb = size_bytes / 1024.0
text = f"Gemini Caches: {count} ({size_kb:.1f} KB)"
dpg.set_value("gemini_cache_label", text)
dpg.configure_item("gemini_cache_label", show=True)
except Exception:
dpg.configure_item("gemini_cache_label", show=False)
else:
dpg.configure_item("gemini_cache_label", show=False)
def _update_performance_diagnostics(self):
"""Updates performance diagnostics displays (throttled)."""
now = time.time()
# Update Diagnostics panel (throttled for smoothness)
if now - self._last_perf_update_time > 0.5:
@@ -2221,6 +2230,34 @@ class App:
dpg.add_line_series(list(range(100)), self.perf_history["cpu"], label="cpu usage", tag="perf_cpu_plot")
dpg.set_axis_limits("axis_cpu_y", 0, 100)
def _process_pending_gui_tasks(self):
"""Processes tasks queued from background threads on the main thread."""
if not self._pending_gui_tasks:
return
with self._pending_gui_tasks_lock:
gui_tasks = self._pending_gui_tasks[:]
self._pending_gui_tasks.clear()
for task in gui_tasks:
try:
action = task.get("action")
if action == "set_value":
item = task.get("item")
val = task.get("value")
if item and dpg.does_item_exist(item):
dpg.set_value(item, val)
elif action == "click":
item = task.get("item")
if item and dpg.does_item_exist(item):
cb = dpg.get_item_callback(item)
if cb:
cb()
elif action == "refresh_api_metrics":
self._refresh_api_metrics()
except Exception as e:
print(f"Error executing GUI hook task: {e}")
def run(self):
dpg.create_context()
dpg.configure_app(docking=True, docking_space=True, init_file="dpg_layout.ini")
@@ -2272,25 +2309,7 @@ class App:
# Process queued API GUI tasks
self.perf_monitor.start_component("GUI_Tasks")
with self._pending_gui_tasks_lock:
gui_tasks = self._pending_gui_tasks[:]
self._pending_gui_tasks.clear()
for task in gui_tasks:
try:
action = task.get("action")
if action == "set_value":
item = task.get("item")
val = task.get("value")
if item and dpg.does_item_exist(item):
dpg.set_value(item, val)
elif action == "click":
item = task.get("item")
if item and dpg.does_item_exist(item):
cb = dpg.get_item_callback(item)
if cb:
cb()
except Exception as e:
print(f"Error executing GUI hook task: {e}")
self._process_pending_gui_tasks()
self.perf_monitor.end_component("GUI_Tasks")
# Handle retro arcade blinking effect
@@ -2394,7 +2413,7 @@ class App:
self.perf_monitor.end_component("Comms")
self.perf_monitor.start_component("Telemetry")
self._update_telemetry_panel()
self._update_performance_diagnostics()
self.perf_monitor.end_component("Telemetry")
self.perf_monitor.end_frame()