Coverage for openhcs/pyqt_gui/services/persistent_system_monitor.py: 0.0%
151 statements
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-14 05:57 +0000
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-14 05:57 +0000
1"""
2Persistent System Monitor for PyQt GUI
4Uses a single persistent QThread to continuously collect system metrics
5without creating/destroying threads repeatedly. This prevents UI hanging
6and provides smooth, responsive performance monitoring.
7"""
9import time
10import logging
11import subprocess
12import platform
13from typing import Dict, Any, Optional
14from collections import deque
16from PyQt6.QtCore import QThread, pyqtSignal, QMutex, QMutexLocker
18import psutil
20try:
21 import GPUtil
22 GPU_AVAILABLE = True
23except ImportError:
24 GPU_AVAILABLE = False
26logger = logging.getLogger(__name__)
29def is_wsl() -> bool:
30 """Check if running in Windows Subsystem for Linux."""
31 return 'microsoft' in platform.uname().release.lower()
34def get_cpu_freq_mhz() -> int:
35 """Get CPU frequency in MHz, with WSL compatibility."""
36 if is_wsl():
37 try:
38 output = subprocess.check_output(
39 ['powershell.exe', '-Command',
40 'Get-CimInstance -ClassName Win32_Processor | Select-Object -ExpandProperty CurrentClockSpeed'],
41 stderr=subprocess.DEVNULL,
42 timeout=2 # Add timeout to prevent hanging
43 )
44 return int(output.strip())
45 except Exception:
46 return 0
47 try:
48 freq = psutil.cpu_freq()
49 return int(freq.current) if freq else 0
50 except Exception:
51 return 0
54class PersistentSystemMonitorThread(QThread):
55 """
56 Persistent thread that continuously collects system metrics.
58 This thread stays alive and runs a continuous loop, collecting metrics
59 at regular intervals and emitting signals with the results. This is much
60 more efficient than creating/destroying threads repeatedly.
61 """
63 # Signals
64 metrics_updated = pyqtSignal(dict) # Emitted when new metrics are available
65 error_occurred = pyqtSignal(str) # Emitted when an error occurs
67 def __init__(self, update_interval: float = 1.0, history_length: int = 60):
68 """
69 Initialize the persistent monitor thread.
71 Args:
72 update_interval: Time between updates in seconds
73 history_length: Number of historical data points to keep
74 """
75 super().__init__()
77 self.update_interval = update_interval
78 self.history_length = history_length
79 self._stop_requested = False
81 # Thread-safe data storage
82 self._mutex = QMutex()
83 self.cpu_history = deque(maxlen=history_length)
84 self.ram_history = deque(maxlen=history_length)
85 self.gpu_history = deque(maxlen=history_length)
86 self.vram_history = deque(maxlen=history_length)
87 self.time_stamps = deque(maxlen=history_length)
89 # Initialize with zeros
90 for _ in range(history_length):
91 self.cpu_history.append(0)
92 self.ram_history.append(0)
93 self.gpu_history.append(0)
94 self.vram_history.append(0)
95 self.time_stamps.append(0)
97 # Cache for current metrics
98 self._current_metrics: Dict[str, Any] = {}
100 def run(self):
101 """Main thread loop - continuously collect metrics."""
102 logger.debug("Persistent system monitor thread started")
104 while not self._stop_requested:
105 try:
106 # Collect all metrics
107 metrics = self._collect_metrics()
109 # Update history with thread safety
110 with QMutexLocker(self._mutex):
111 self.cpu_history.append(metrics.get('cpu_percent', 0))
112 self.ram_history.append(metrics.get('ram_percent', 0))
113 self.gpu_history.append(metrics.get('gpu_percent', 0))
114 self.vram_history.append(metrics.get('vram_percent', 0))
115 self.time_stamps.append(time.time())
117 # Cache current metrics
118 self._current_metrics = metrics.copy()
120 # Emit signal with new metrics
121 self.metrics_updated.emit(metrics)
123 # Sleep for the update interval with frequent stop checks
124 sleep_ms = int(self.update_interval * 1000)
125 sleep_chunks = max(1, sleep_ms // 100) # Check every 100ms
126 chunk_size = sleep_ms // sleep_chunks
128 for _ in range(sleep_chunks):
129 if self._stop_requested:
130 break
131 self.msleep(chunk_size)
133 except Exception as e:
134 logger.warning(f"Error collecting system metrics: {e}")
135 self.error_occurred.emit(str(e))
136 # Sleep longer on error to avoid spam, but still check for stop
137 for _ in range(20): # 20 * 100ms = 2 seconds
138 if self._stop_requested:
139 break
140 self.msleep(100)
142 logger.debug("Persistent system monitor thread stopped")
144 def _collect_metrics(self) -> Dict[str, Any]:
145 """Collect all system metrics in one go."""
146 metrics = {}
148 try:
149 # CPU usage
150 cpu_percent = psutil.cpu_percent(interval=None)
151 metrics['cpu_percent'] = cpu_percent
153 # RAM usage
154 ram = psutil.virtual_memory()
155 metrics['ram_percent'] = ram.percent
156 metrics['ram_used_gb'] = ram.used / (1024**3)
157 metrics['ram_total_gb'] = ram.total / (1024**3)
158 metrics['ram_available_gb'] = ram.available / (1024**3)
160 # CPU info
161 metrics['cpu_cores'] = psutil.cpu_count()
162 metrics['cpu_freq_mhz'] = get_cpu_freq_mhz()
164 # GPU usage
165 if GPU_AVAILABLE:
166 try:
167 gpus = GPUtil.getGPUs()
168 if gpus:
169 gpu = gpus[0] # Use first GPU
170 gpu_load = gpu.load * 100
171 vram_util = gpu.memoryUtil * 100
173 metrics.update({
174 'gpu_percent': gpu_load,
175 'vram_percent': vram_util,
176 'gpu_name': gpu.name,
177 'gpu_temp': gpu.temperature,
178 'vram_used_mb': gpu.memoryUsed,
179 'vram_total_mb': gpu.memoryTotal
180 })
181 else:
182 # No GPUs found
183 metrics.update({
184 'gpu_percent': 0.0,
185 'vram_percent': 0.0,
186 'gpu_name': 'No GPU Found',
187 'gpu_temp': 0,
188 'vram_used_mb': 0,
189 'vram_total_mb': 0
190 })
191 except Exception as e:
192 # GPU monitoring failed
193 logger.debug(f"GPU monitoring failed: {e}")
194 metrics.update({
195 'gpu_percent': 0.0,
196 'vram_percent': 0.0,
197 'gpu_name': 'GPU Error',
198 'gpu_temp': 0,
199 'vram_used_mb': 0,
200 'vram_total_mb': 0
201 })
202 else:
203 # GPUtil not available
204 metrics.update({
205 'gpu_percent': 0.0,
206 'vram_percent': 0.0,
207 'gpu_name': 'GPUtil Not Available',
208 'gpu_temp': 0,
209 'vram_used_mb': 0,
210 'vram_total_mb': 0
211 })
213 except Exception as e:
214 logger.warning(f"Error in metrics collection: {e}")
215 # Return defaults on error
216 metrics = {
217 'cpu_percent': 0.0,
218 'ram_percent': 0.0,
219 'ram_used_gb': 0.0,
220 'ram_total_gb': 0.0,
221 'ram_available_gb': 0.0,
222 'cpu_cores': 0,
223 'cpu_freq_mhz': 0,
224 'gpu_percent': 0.0,
225 'vram_percent': 0.0,
226 'gpu_name': 'Error',
227 'gpu_temp': 0,
228 'vram_used_mb': 0,
229 'vram_total_mb': 0
230 }
232 return metrics
234 def stop_monitoring(self):
235 """Request the thread to stop monitoring."""
236 self._stop_requested = True
238 def get_current_metrics(self) -> Dict[str, Any]:
239 """Get the current cached metrics (thread-safe)."""
240 with QMutexLocker(self._mutex):
241 return self._current_metrics.copy() if self._current_metrics else {}
243 def get_history_data(self) -> Dict[str, Any]:
244 """Get historical data (thread-safe)."""
245 with QMutexLocker(self._mutex):
246 return {
247 'cpu_history': list(self.cpu_history),
248 'ram_history': list(self.ram_history),
249 'gpu_history': list(self.gpu_history),
250 'vram_history': list(self.vram_history),
251 'time_stamps': list(self.time_stamps)
252 }
254 def set_update_interval(self, interval: float):
255 """Set the update interval in seconds."""
256 self.update_interval = interval
259class PersistentSystemMonitor:
260 """
261 System monitor that uses a persistent background thread.
263 This provides a simple interface to the persistent monitoring thread,
264 ensuring the UI never blocks during metrics collection.
265 """
267 def __init__(self, update_interval: float = 1.0, history_length: int = 60):
268 """
269 Initialize the persistent system monitor.
271 Args:
272 update_interval: Time between updates in seconds
273 history_length: Number of historical data points to keep
274 """
275 self.thread = PersistentSystemMonitorThread(update_interval, history_length)
276 self._is_running = False
278 def __del__(self):
279 """Destructor - ensure thread is stopped."""
280 try:
281 self.stop_monitoring()
282 except:
283 pass # Ignore errors during destruction
285 def start_monitoring(self):
286 """Start the monitoring thread."""
287 if not self._is_running:
288 self.thread.start()
289 self._is_running = True
290 logger.debug("Persistent system monitor started")
292 def stop_monitoring(self):
293 """Stop the monitoring thread."""
294 if self._is_running:
295 logger.debug("Stopping persistent system monitor...")
296 self.thread.stop_monitoring()
298 # Wait for clean shutdown with shorter timeout
299 if not self.thread.wait(2000): # Wait up to 2 seconds
300 logger.warning("System monitor thread did not stop cleanly, terminating...")
301 self.thread.terminate()
302 self.thread.wait(1000) # Give terminate a chance
304 self._is_running = False
305 logger.debug("Persistent system monitor stopped")
307 def get_current_metrics(self) -> Dict[str, Any]:
308 """Get current metrics without blocking."""
309 return self.thread.get_current_metrics()
311 def get_history_data(self) -> Dict[str, Any]:
312 """Get historical data without blocking."""
313 return self.thread.get_history_data()
315 def connect_signals(self, metrics_callback=None, error_callback=None):
316 """Connect to thread signals."""
317 if metrics_callback:
318 self.thread.metrics_updated.connect(metrics_callback)
319 if error_callback:
320 self.thread.error_occurred.connect(error_callback)
322 def set_update_interval(self, interval: float):
323 """Set the update interval."""
324 self.thread.set_update_interval(interval)