From 30d838c3a0d9a45a31488eab918424bcf54f0b76 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Mon, 23 Feb 2026 14:50:44 -0500 Subject: [PATCH] feat(ui): Build Diagnostics Panel with real-time plots --- gui.py | 69 +++++++++++++++++++++++++++++++++++ tests/test_gui_diagnostics.py | 65 +++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 tests/test_gui_diagnostics.py diff --git a/gui.py b/gui.py index cf288fa..4e0abc1 100644 --- a/gui.py +++ b/gui.py @@ -460,6 +460,7 @@ class App: "Theme": "win_theme", "Last Script Output": "win_script_output", "Text Viewer": "win_text_viewer", + "Diagnostics": "win_diagnostics", } @@ -496,6 +497,12 @@ class App: self._script_blink_start_time = 0.0 self.perf_monitor = PerformanceMonitor() + self.perf_history = { + "frame_time": [0.0] * 100, + "fps": [0.0] * 100, + "cpu": [0.0] * 100, + "input_lag": [0.0] * 100 + } session_logger.open_session() ai_client.set_provider(self.current_provider, self.current_model) @@ -815,6 +822,32 @@ class App: else: dpg.configure_item("gemini_cache_label", show=False) + # Update Diagnostics panel + if dpg.is_item_shown("win_diagnostics"): + metrics = self.perf_monitor.get_metrics() + + # Update history + self.perf_history["frame_time"].append(metrics['last_frame_time_ms']) + self.perf_history["fps"].append(metrics['fps']) + self.perf_history["cpu"].append(metrics['cpu_percent']) + self.perf_history["input_lag"].append(metrics['input_lag_ms']) + + for k in self.perf_history: + if len(self.perf_history[k]) > 100: + self.perf_history[k].pop(0) + + # Update labels + dpg.set_value("perf_fps_text", f"{metrics['fps']:.1f}") + dpg.set_value("perf_frame_text", f"{metrics['last_frame_time_ms']:.1f}ms") + dpg.set_value("perf_cpu_text", f"{metrics['cpu_percent']:.1f}%") + dpg.set_value("perf_lag_text", f"{metrics['input_lag_ms']:.1f}ms") + + # Update plots + if dpg.does_item_exist("perf_frame_plot"): + dpg.set_value("perf_frame_plot", [list(range(100)), self.perf_history["frame_time"]]) + if dpg.does_item_exist("perf_cpu_plot"): + dpg.set_value("perf_cpu_plot", [list(range(100)), self.perf_history["cpu"]]) + def _append_comms_entry(self, entry: dict, idx: int): if not dpg.does_item_exist("comms_scroll"): return @@ -2110,6 +2143,42 @@ class App: with dpg.child_window(tag="text_viewer_wrap_container", width=-1, height=-1, border=False, show=False): dpg.add_text("", tag="text_viewer_wrap", wrap=0) + # ---- Diagnostics panel ---- + with dpg.window( + label="Diagnostics", + tag="win_diagnostics", + pos=(8, 804), + width=400, + height=380, + no_close=False, + ): + dpg.add_text("Performance Telemetry") + with dpg.group(horizontal=True): + dpg.add_text("FPS:") + dpg.add_text("0.0", tag="perf_fps_text", color=(180, 255, 180)) + dpg.add_spacer(width=20) + dpg.add_text("Frame:") + dpg.add_text("0.0ms", tag="perf_frame_text", color=(100, 200, 255)) + + with dpg.add_plot(label="Frame Time (ms)", height=100, width=-1, no_mouse_pos=True): + dpg.add_plot_axis(dpg.mvXAxis, label="samples", no_tick_labels=True) + with dpg.plot_axis(dpg.mvYAxis, label="ms"): + dpg.add_line_series(list(range(100)), self.perf_history["frame_time"], label="frame time", tag="perf_frame_plot") + dpg.set_axis_limits(dpg.last_item(), 0, 50) + + with dpg.group(horizontal=True): + dpg.add_text("CPU:") + dpg.add_text("0.0%", tag="perf_cpu_text", color=(255, 220, 100)) + dpg.add_spacer(width=20) + dpg.add_text("Input Lag:") + dpg.add_text("0.0ms", tag="perf_lag_text", color=(255, 180, 80)) + + with dpg.add_plot(label="CPU Usage (%)", height=100, width=-1, no_mouse_pos=True): + dpg.add_plot_axis(dpg.mvXAxis, label="samples", no_tick_labels=True) + with dpg.plot_axis(dpg.mvYAxis, label="%"): + dpg.add_line_series(list(range(100)), self.perf_history["cpu"], label="cpu usage", tag="perf_cpu_plot") + dpg.set_axis_limits(dpg.last_item(), 0, 100) + def run(self): dpg.create_context() dpg.configure_app(docking=True, docking_space=True, init_file="dpg_layout.ini") diff --git a/tests/test_gui_diagnostics.py b/tests/test_gui_diagnostics.py new file mode 100644 index 0000000..28f5cf7 --- /dev/null +++ b/tests/test_gui_diagnostics.py @@ -0,0 +1,65 @@ +import pytest +from unittest.mock import patch, MagicMock +import importlib.util +import sys +import dearpygui.dearpygui as dpg + +# Load gui.py as a module for testing +spec = importlib.util.spec_from_file_location("gui", "gui.py") +gui = importlib.util.module_from_spec(spec) +sys.modules["gui"] = gui +spec.loader.exec_module(gui) +from gui import App + +@pytest.fixture +def app_instance(): + dpg.create_context() + with patch('dearpygui.dearpygui.create_viewport'), \ + patch('dearpygui.dearpygui.setup_dearpygui'), \ + patch('dearpygui.dearpygui.show_viewport'), \ + patch('dearpygui.dearpygui.start_dearpygui'), \ + patch('gui.load_config', return_value={}), \ + patch.object(App, '_rebuild_files_list'), \ + patch.object(App, '_rebuild_shots_list'), \ + patch.object(App, '_rebuild_disc_list'), \ + patch.object(App, '_rebuild_disc_roles_list'), \ + patch.object(App, '_rebuild_discussion_selector'), \ + patch.object(App, '_refresh_project_widgets'): + + app = App() + yield app + dpg.destroy_context() + +def test_diagnostics_panel_initialization(app_instance): + assert "Diagnostics" in app_instance.window_info + assert app_instance.window_info["Diagnostics"] == "win_diagnostics" + assert "frame_time" in app_instance.perf_history + assert len(app_instance.perf_history["frame_time"]) == 100 + +def test_diagnostics_panel_updates(app_instance): + # Mock dependencies + mock_metrics = { + 'last_frame_time_ms': 10.0, + 'fps': 100.0, + 'cpu_percent': 50.0, + 'input_lag_ms': 5.0 + } + app_instance.perf_monitor.get_metrics = MagicMock(return_value=mock_metrics) + + with patch('dearpygui.dearpygui.is_item_shown', return_value=True), \ + patch('dearpygui.dearpygui.set_value') as mock_set_value, \ + patch('dearpygui.dearpygui.configure_item') as mock_configure_item, \ + patch('dearpygui.dearpygui.does_item_exist', return_value=True): + + # We also need to mock ai_client stats + with patch('ai_client.get_history_bleed_stats', return_value={}): + app_instance._update_telemetry_panel() + + # Verify UI updates + mock_set_value.assert_any_call("perf_fps_text", "100.0") + mock_set_value.assert_any_call("perf_frame_text", "10.0ms") + mock_set_value.assert_any_call("perf_cpu_text", "50.0%") + mock_set_value.assert_any_call("perf_lag_text", "5.0ms") + + # Verify history update + assert app_instance.perf_history["frame_time"][-1] == 10.0