diff --git a/gui.py b/gui.py index 29ad951..17d40d4 100644 --- a/gui.py +++ b/gui.py @@ -502,6 +502,7 @@ class App: ai_client.comms_log_callback = self._on_comms_entry ai_client.tool_log_callback = self._on_tool_log mcp_client.perf_monitor_callback = self.perf_monitor.get_metrics + self.perf_monitor.alert_callback = self._on_performance_alert # ---------------------------------------------------------------- project loading @@ -755,6 +756,17 @@ class App: """Called from background thread when a tool call completes.""" session_logger.log_tool_call(script, result, None) + def _on_performance_alert(self, message: str): + """Called by PerformanceMonitor when a threshold is exceeded.""" + alert_text = f"[PERFORMANCE ALERT] {message}. Please consider optimizing recent changes or reducing load." + # Inject into history as a 'System' message or similar + with self._pending_history_adds_lock: + self._pending_history_adds.append({ + "role": "System", + "content": alert_text, + "ts": project_manager.now_ts() + }) + def _flush_pending_comms(self): """Called every frame from the main render loop.""" with self._pending_comms_lock: diff --git a/performance_monitor.py b/performance_monitor.py index bd0fdf9..bf0fbb5 100644 --- a/performance_monitor.py +++ b/performance_monitor.py @@ -17,6 +17,16 @@ class PerformanceMonitor: self._last_input_time = None self._input_lag_ms = 0.0 + # Alerts + self.alert_callback = None + self.thresholds = { + 'frame_time_ms': 33.3, # < 30 FPS + 'cpu_percent': 80.0, + 'input_lag_ms': 100.0 + } + self._last_alert_time = 0 + self._alert_cooldown = 30 # seconds + # Start CPU usage monitoring thread self._stop_event = threading.Event() self._cpu_thread = threading.Thread(target=self._monitor_cpu, daemon=True) @@ -48,6 +58,8 @@ class PerformanceMonitor: if self._last_input_time is not None: self._input_lag_ms = (end_time - self._last_input_time) * 1000.0 self._last_input_time = None + + self._check_alerts() elapsed_since_fps = end_time - self._fps_last_time if elapsed_since_fps >= 1.0: @@ -55,6 +67,27 @@ class PerformanceMonitor: self._frame_count = 0 self._fps_last_time = end_time + def _check_alerts(self): + if not self.alert_callback: + return + + now = time.time() + if now - self._last_alert_time < self._alert_cooldown: + return + + metrics = self.get_metrics() + alerts = [] + if metrics['last_frame_time_ms'] > self.thresholds['frame_time_ms']: + alerts.append(f"Frame time high: {metrics['last_frame_time_ms']:.1f}ms") + if metrics['cpu_percent'] > self.thresholds['cpu_percent']: + alerts.append(f"CPU usage high: {metrics['cpu_percent']:.1f}%") + if metrics['input_lag_ms'] > self.thresholds['input_lag_ms']: + alerts.append(f"Input lag high: {metrics['input_lag_ms']:.1f}ms") + + if alerts: + self._last_alert_time = now + self.alert_callback("; ".join(alerts)) + def get_metrics(self): with self._cpu_lock: cpu_usage = self._cpu_usage diff --git a/tests/test_performance_monitor.py b/tests/test_performance_monitor.py index ec05193..79877ca 100644 --- a/tests/test_performance_monitor.py +++ b/tests/test_performance_monitor.py @@ -1,5 +1,6 @@ import unittest import time +from unittest.mock import MagicMock from performance_monitor import PerformanceMonitor class TestPerformanceMonitor(unittest.TestCase): @@ -33,5 +34,18 @@ class TestPerformanceMonitor(unittest.TestCase): self.assertGreaterEqual(metrics['input_lag_ms'], 20) self.assertLess(metrics['input_lag_ms'], 40) + def test_alerts_triggering(self): + mock_callback = MagicMock() + self.monitor.alert_callback = mock_callback + self.monitor.thresholds['frame_time_ms'] = 5.0 # Low threshold + self.monitor._alert_cooldown = 0 # No cooldown for test + + self.monitor.start_frame() + time.sleep(0.01) # 10ms > 5ms + self.monitor.end_frame() + + mock_callback.assert_called_once() + self.assertIn("Frame time high", mock_callback.call_args[0][0]) + if __name__ == '__main__': unittest.main()