Merge branch 'dev'

This commit is contained in:
2025-07-31 14:35:23 +02:00
+792 -34
View File
@@ -44,6 +44,556 @@ perf_logger.setLevel(logging.WARNING) # Only log warnings and errors for perfor
logger = logging.getLogger(__name__)
def get_gpu_memory_info():
"""
Get GPU memory information using multiple methods.
Supports both discrete and integrated GPUs across Windows, Linux, and macOS.
Returns dict with GPU memory info or basic info if no GPU found.
Uses caching to prevent repeated expensive system calls.
"""
# Return cached info immediately if available and recent
current_time = time.time()
cache_duration = 30 # seconds
if (hasattr(get_gpu_memory_info, '_cached_info') and
hasattr(get_gpu_memory_info, '_cache_time') and
current_time - get_gpu_memory_info._cache_time < cache_duration):
return get_gpu_memory_info._cached_info
# If we're already updating in background, return cached or default
if hasattr(get_gpu_memory_info, '_updating') and get_gpu_memory_info._updating:
return getattr(get_gpu_memory_info, '_cached_info',
{'has_gpu': False, 'total_mb': 0, 'used_mb': 0, 'free_mb': 0,
'utilization_percent': 0, 'gpu_name': 'Loading...', 'method': 'loading'})
# Start background update
get_gpu_memory_info._updating = True
def update_gpu_info():
"""Background thread function to update GPU info."""
import json
import subprocess
import platform
import os
gpu_info = {'has_gpu': False, 'total_mb': 0, 'used_mb': 0, 'free_mb': 0, 'utilization_percent': 0, 'gpu_name': 'Unknown'}
try:
# Try nvidia-ml-py (NVIDIA GPUs - most detailed info)
try:
import pynvml
pynvml.nvmlInit()
handle = pynvml.nvmlDeviceGetHandleByIndex(0) # Get first GPU
# Get memory info
mem_info = pynvml.nvmlDeviceGetMemoryInfo(handle)
total_mb = mem_info.total / 1024 / 1024
used_mb = mem_info.used / 1024 / 1024
free_mb = mem_info.free / 1024 / 1024
utilization_percent = (used_mb / total_mb) * 100
# Get GPU name
gpu_name = pynvml.nvmlDeviceGetName(handle).decode('utf-8')
gpu_info.update({
'has_gpu': True,
'total_mb': total_mb,
'used_mb': used_mb,
'free_mb': free_mb,
'utilization_percent': utilization_percent,
'gpu_name': gpu_name,
'method': 'pynvml'
})
# Cache the result and mark as not updating
get_gpu_memory_info._cached_info = gpu_info
get_gpu_memory_info._cache_time = time.time()
get_gpu_memory_info._updating = False
return
except (ImportError, Exception):
pass
# Try GPUtil (NVIDIA GPUs alternative)
try:
import GPUtil
gpus = GPUtil.getGPUs()
if gpus:
gpu = gpus[0] # Get first GPU
total_mb = gpu.memoryTotal
used_mb = gpu.memoryUsed
free_mb = gpu.memoryFree
utilization_percent = (used_mb / total_mb) * 100
gpu_info.update({
'has_gpu': True,
'total_mb': total_mb,
'used_mb': used_mb,
'free_mb': free_mb,
'utilization_percent': utilization_percent,
'gpu_name': gpu.name,
'method': 'GPUtil'
})
# Cache the result and mark as not updating
get_gpu_memory_info._cached_info = gpu_info
get_gpu_memory_info._cache_time = time.time()
get_gpu_memory_info._updating = False
return
except (ImportError, Exception):
pass
# Try Windows-specific methods with very short timeouts
try:
# Try nvidia-smi command for NVIDIA GPUs
try:
result = subprocess.run(['nvidia-smi', '--query-gpu=name,memory.total,memory.used,memory.free', '--format=csv,noheader,nounits'],
capture_output=True, text=True, timeout=1) # Very short timeout
if result.returncode == 0 and result.stdout.strip():
lines = result.stdout.strip().split('\n')
if lines:
parts = lines[0].split(', ')
if len(parts) >= 4:
gpu_name = parts[0].strip()
total_mb = float(parts[1].strip())
used_mb = float(parts[2].strip())
free_mb = float(parts[3].strip())
utilization_percent = (used_mb / total_mb) * 100
gpu_info.update({
'has_gpu': True,
'total_mb': total_mb,
'used_mb': used_mb,
'free_mb': free_mb,
'utilization_percent': utilization_percent,
'gpu_name': gpu_name,
'method': 'nvidia-smi'
})
# Cache the result and mark as not updating
get_gpu_memory_info._cached_info = gpu_info
get_gpu_memory_info._cache_time = time.time()
get_gpu_memory_info._updating = False
return
except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError):
pass
# Skip expensive WMI calls for now to prevent hanging
# These can be re-enabled later if needed, but with proper threading
except Exception:
pass
except Exception:
pass
finally:
# Always mark as not updating and cache result
get_gpu_memory_info._cached_info = gpu_info
get_gpu_memory_info._cache_time = time.time()
get_gpu_memory_info._updating = False
# Start background thread for GPU detection
import threading
gpu_thread = threading.Thread(target=update_gpu_info, daemon=True)
gpu_thread.start()
# Return cached info if available, otherwise return loading state
return getattr(get_gpu_memory_info, '_cached_info',
{'has_gpu': False, 'total_mb': 0, 'used_mb': 0, 'free_mb': 0,
'utilization_percent': 0, 'gpu_name': 'Loading...', 'method': 'loading'})
gpu_info = {'has_gpu': False, 'total_mb': 0, 'used_mb': 0, 'free_mb': 0, 'utilization_percent': 0, 'gpu_name': 'Unknown'}
# Try nvidia-ml-py (NVIDIA GPUs - most detailed info)
try:
import pynvml
pynvml.nvmlInit()
handle = pynvml.nvmlDeviceGetHandleByIndex(0) # Get first GPU
# Get memory info
mem_info = pynvml.nvmlDeviceGetMemoryInfo(handle)
total_mb = mem_info.total / 1024 / 1024
used_mb = mem_info.used / 1024 / 1024
free_mb = mem_info.free / 1024 / 1024
utilization_percent = (used_mb / total_mb) * 100
# Get GPU name
gpu_name = pynvml.nvmlDeviceGetName(handle).decode('utf-8')
gpu_info.update({
'has_gpu': True,
'total_mb': total_mb,
'used_mb': used_mb,
'free_mb': free_mb,
'utilization_percent': utilization_percent,
'gpu_name': gpu_name,
'method': 'pynvml'
})
logger.debug(f"GPU detected via pynvml: {gpu_name}, {total_mb:.0f}MB total")
# Cache the result
get_gpu_memory_info._cached_info = gpu_info
get_gpu_memory_info._cache_time = current_time
return gpu_info
except (ImportError, Exception) as e:
logger.debug(f"pynvml not available or failed: {e}")
# Try GPUtil (NVIDIA GPUs alternative)
try:
import GPUtil
gpus = GPUtil.getGPUs()
if gpus:
gpu = gpus[0] # Get first GPU
total_mb = gpu.memoryTotal
used_mb = gpu.memoryUsed
free_mb = gpu.memoryFree
utilization_percent = (used_mb / total_mb) * 100
gpu_info.update({
'has_gpu': True,
'total_mb': total_mb,
'used_mb': used_mb,
'free_mb': free_mb,
'utilization_percent': utilization_percent,
'gpu_name': gpu.name,
'method': 'GPUtil'
})
logger.debug(f"GPU detected via GPUtil: {gpu.name}, {total_mb:.0f}MB total")
# Cache the result
get_gpu_memory_info._cached_info = gpu_info
get_gpu_memory_info._cache_time = current_time
return gpu_info
except (ImportError, Exception) as e:
logger.debug(f"GPUtil not available or failed: {e}")
# Try psutil for basic GPU info (limited)
try:
import psutil
# Check if there are any GPU-related processes
for proc in psutil.process_iter(['pid', 'name']):
try:
proc_name = proc.info['name'].lower()
if any(gpu_process in proc_name for gpu_process in ['nvidia', 'amd', 'intel', 'gpu']):
gpu_info.update({
'has_gpu': True,
'total_mb': 0, # Can't get detailed info with psutil
'used_mb': 0,
'free_mb': 0,
'utilization_percent': 0,
'gpu_name': 'Detected via process',
'method': 'psutil_detection'
})
logger.debug(f"GPU detected via psutil process detection: {proc_name}")
# Cache the result
get_gpu_memory_info._cached_info = gpu_info
get_gpu_memory_info._cache_time = current_time
return gpu_info
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
except ImportError:
pass
# Try Windows-specific methods
try:
# Try nvidia-smi command for NVIDIA GPUs
try:
result = subprocess.run(['nvidia-smi', '--query-gpu=name,memory.total,memory.used,memory.free', '--format=csv,noheader,nounits'],
capture_output=True, text=True, timeout=2) # Reduced timeout
if result.returncode == 0 and result.stdout.strip():
lines = result.stdout.strip().split('\n')
if lines:
parts = lines[0].split(', ')
if len(parts) >= 4:
gpu_name = parts[0].strip()
total_mb = float(parts[1].strip())
used_mb = float(parts[2].strip())
free_mb = float(parts[3].strip())
utilization_percent = (used_mb / total_mb) * 100
gpu_info.update({
'has_gpu': True,
'total_mb': total_mb,
'used_mb': used_mb,
'free_mb': free_mb,
'utilization_percent': utilization_percent,
'gpu_name': gpu_name,
'method': 'nvidia-smi'
})
return gpu_info
except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError):
pass
# Try Intel GPU detection via Windows Management Instrumentation (WMI)
try:
# Query for Intel integrated graphics
wmi_result = subprocess.run([
'powershell', '-Command',
"Get-WmiObject -Class Win32_VideoController | Where-Object {$_.Name -like '*Intel*' -or $_.Name -like '*UHD*' -or $_.Name -like '*Iris*' -or $_.Name -like '*HD Graphics*'} | Select-Object Name, AdapterRAM | ConvertTo-Json"
], capture_output=True, text=True, timeout=3) # Reduced timeout
if wmi_result.returncode == 0 and wmi_result.stdout.strip():
wmi_data = json.loads(wmi_result.stdout.strip())
# Handle both single GPU and multiple GPUs
if isinstance(wmi_data, dict):
wmi_data = [wmi_data]
for gpu_data in wmi_data:
if gpu_data.get('Name') and gpu_data.get('AdapterRAM'):
gpu_name = gpu_data['Name']
# AdapterRAM is in bytes, convert to MB
total_mb = int(gpu_data['AdapterRAM']) / (1024 * 1024)
gpu_info.update({
'has_gpu': True,
'total_mb': total_mb,
'used_mb': 0, # WMI doesn't provide usage info
'free_mb': total_mb, # Assume all free since we can't get usage
'utilization_percent': 0,
'gpu_name': gpu_name,
'method': 'WMI_Intel'
})
logger.debug(f"Intel GPU detected via WMI: {gpu_name}, {total_mb:.0f}MB")
return gpu_info
except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError, json.JSONDecodeError):
pass
# Try AMD GPU detection via WMI
try:
wmi_result = subprocess.run([
'powershell', '-Command',
"Get-WmiObject -Class Win32_VideoController | Where-Object {$_.Name -like '*AMD*' -or $_.Name -like '*Radeon*' -or $_.Name -like '*ATI*'} | Select-Object Name, AdapterRAM | ConvertTo-Json"
], capture_output=True, text=True, timeout=3) # Reduced timeout
if wmi_result.returncode == 0 and wmi_result.stdout.strip():
wmi_data = json.loads(wmi_result.stdout.strip())
# Handle both single GPU and multiple GPUs
if isinstance(wmi_data, dict):
wmi_data = [wmi_data]
for gpu_data in wmi_data:
if gpu_data.get('Name') and gpu_data.get('AdapterRAM'):
gpu_name = gpu_data['Name']
# AdapterRAM is in bytes, convert to MB
total_mb = int(gpu_data['AdapterRAM']) / (1024 * 1024)
gpu_info.update({
'has_gpu': True,
'total_mb': total_mb,
'used_mb': 0, # WMI doesn't provide usage info
'free_mb': total_mb, # Assume all free since we can't get usage
'utilization_percent': 0,
'gpu_name': gpu_name,
'method': 'WMI_AMD'
})
return gpu_info
except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError, json.JSONDecodeError):
pass
# Generic WMI query for any video controller
try:
wmi_result = subprocess.run([
'powershell', '-Command',
"Get-WmiObject -Class Win32_VideoController | Where-Object {$_.AdapterRAM -gt 0} | Select-Object Name, AdapterRAM | ConvertTo-Json"
], capture_output=True, text=True, timeout=3) # Reduced timeout
if wmi_result.returncode == 0 and wmi_result.stdout.strip():
wmi_data = json.loads(wmi_result.stdout.strip())
# Handle both single GPU and multiple GPUs
if isinstance(wmi_data, dict):
wmi_data = [wmi_data]
# Find the first GPU with significant memory (>= 512MB)
for gpu_data in wmi_data:
if gpu_data.get('Name') and gpu_data.get('AdapterRAM'):
total_mb = int(gpu_data['AdapterRAM']) / (1024 * 1024)
# Only consider GPUs with at least 512MB
if total_mb >= 512:
gpu_name = gpu_data['Name']
gpu_info.update({
'has_gpu': True,
'total_mb': total_mb,
'used_mb': 0, # WMI doesn't provide usage info
'free_mb': total_mb, # Assume all free since we can't get usage
'utilization_percent': 0,
'gpu_name': gpu_name,
'method': 'WMI_Generic'
})
return gpu_info
except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError, json.JSONDecodeError):
pass
except Exception as e:
logger.debug(f"Windows GPU detection methods failed: {e}")
# Try DirectX DXGI detection (Windows 10+)
try:
# Use PowerShell to query DirectX information
dxdiag_result = subprocess.run([
'powershell', '-Command',
"""
Add-Type -AssemblyName System.Windows.Forms
$dxdiag = Get-WmiObject -Class Win32_VideoController | Where-Object {$_.AdapterRAM -gt 536870912} | Select-Object Name, AdapterRAM, DriverVersion
$dxdiag | ConvertTo-Json
"""
], capture_output=True, text=True, timeout=3) # Reduced timeout
if dxdiag_result.returncode == 0 and dxdiag_result.stdout.strip():
dx_data = json.loads(dxdiag_result.stdout.strip())
if isinstance(dx_data, dict):
dx_data = [dx_data]
for gpu_data in dx_data:
if gpu_data.get('Name') and gpu_data.get('AdapterRAM'):
gpu_name = gpu_data['Name']
total_mb = int(gpu_data['AdapterRAM']) / (1024 * 1024)
gpu_info.update({
'has_gpu': True,
'total_mb': total_mb,
'used_mb': 0,
'free_mb': total_mb,
'utilization_percent': 0,
'gpu_name': gpu_name,
'method': 'DirectX_DXGI'
})
return gpu_info
except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError, json.JSONDecodeError):
pass
# Try Linux/macOS methods for integrated GPUs
try:
system = platform.system().lower()
if system == 'linux':
# Try lspci for GPU detection on Linux
try:
lspci_result = subprocess.run(['lspci', '-v'], capture_output=True, text=True, timeout=2) # Reduced timeout
if lspci_result.returncode == 0:
output = lspci_result.stdout.lower()
# Look for VGA/Display controllers
if any(keyword in output for keyword in ['vga', 'display', 'gpu', 'graphics']):
# Extract GPU names
lines = lspci_result.stdout.split('\n')
for line in lines:
if any(keyword in line.lower() for keyword in ['vga', 'display controller', '3d controller']):
# Try to extract GPU name from the line
if 'intel' in line.lower():
gpu_name = 'Intel Integrated Graphics (detected via lspci)'
elif 'amd' in line.lower() or 'ati' in line.lower():
gpu_name = 'AMD Integrated Graphics (detected via lspci)'
elif 'nvidia' in line.lower():
gpu_name = 'NVIDIA GPU (detected via lspci)'
else:
gpu_name = 'GPU detected via lspci'
gpu_info.update({
'has_gpu': True,
'total_mb': 0, # Can't get memory info from lspci
'used_mb': 0,
'free_mb': 0,
'utilization_percent': 0,
'gpu_name': gpu_name,
'method': 'lspci_linux'
})
return gpu_info
except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError):
pass
# Try checking /sys/class/drm for Intel iGPU on Linux
try:
drm_path = '/sys/class/drm'
if os.path.exists(drm_path):
drm_devices = os.listdir(drm_path)
intel_devices = [d for d in drm_devices if 'i915' in d or 'intel' in d.lower()]
if intel_devices:
gpu_info.update({
'has_gpu': True,
'total_mb': 0,
'used_mb': 0,
'free_mb': 0,
'utilization_percent': 0,
'gpu_name': 'Intel Integrated Graphics (detected via /sys/class/drm)',
'method': 'linux_drm'
})
return gpu_info
except Exception:
pass
elif system == 'darwin': # macOS
# Try system_profiler for macOS GPU detection
try:
profiler_result = subprocess.run([
'system_profiler', 'SPDisplaysDataType', '-json'
], capture_output=True, text=True, timeout=15)
if profiler_result.returncode == 0:
display_data = json.loads(profiler_result.stdout)
displays = display_data.get('SPDisplaysDataType', [])
for display in displays:
gpu_name = display.get('sppci_model', 'Unknown GPU')
vram = display.get('spdisplays_vram', '0 MB')
# Extract VRAM size
vram_mb = 0
if 'MB' in vram:
try:
vram_mb = float(vram.replace(' MB', ''))
except:
pass
elif 'GB' in vram:
try:
vram_mb = float(vram.replace(' GB', '')) * 1024
except:
pass
gpu_info.update({
'has_gpu': True,
'total_mb': vram_mb,
'used_mb': 0,
'free_mb': vram_mb,
'utilization_percent': 0,
'gpu_name': gpu_name,
'method': 'macos_system_profiler'
})
return gpu_info
except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError, json.JSONDecodeError):
pass
except Exception as e:
logger.debug(f"Linux/macOS GPU detection failed: {e}")
# Cache the result to prevent repeated expensive calls
get_gpu_memory_info._cached_info = gpu_info
get_gpu_memory_info._cache_time = current_time
return gpu_info
CONFIG_DIR = 'scenarios'
if not os.path.exists(CONFIG_DIR):
os.makedirs(CONFIG_DIR)
@@ -473,10 +1023,7 @@ class MainWindow(QtWidgets.QMainWindow):
if len(self._loop_times) > self._max_loop_time_samples:
self._loop_times.pop(0)
# Log performance warnings
avg_time = sum(self._loop_times) / len(self._loop_times)
if avg_time > 0.5: # If average loop time exceeds 500ms
logger.warning(f"Performance warning: Average loop time: {avg_time:.3f}s")
# Performance warnings are now handled by _monitor_performance with deduplication
def _periodic_cleanup(self):
"""
@@ -597,7 +1144,8 @@ class MainWindow(QtWidgets.QMainWindow):
def get_memory_usage(self):
"""
Get current memory usage information for monitoring.
Get current memory usage information for monitoring, including GPU memory.
Uses timeout protection to prevent UI hanging.
"""
try:
import psutil
@@ -608,6 +1156,13 @@ class MainWindow(QtWidgets.QMainWindow):
# Get system info
system_memory = psutil.virtual_memory()
# Get GPU information with error handling (non-blocking)
try:
gpu_info = get_gpu_memory_info()
except Exception as e:
logger.debug(f"GPU detection failed: {e}")
gpu_info = {'has_gpu': False, 'total_mb': 0, 'used_mb': 0, 'free_mb': 0, 'utilization_percent': 0, 'gpu_name': 'Error', 'method': 'error'}
return {
'process_memory_mb': memory_info.rss / 1024 / 1024, # MB
'process_memory_percent': process.memory_percent(),
@@ -616,22 +1171,18 @@ class MainWindow(QtWidgets.QMainWindow):
'system_memory_available_gb': system_memory.available / 1024 / 1024 / 1024, # GB
'template_cache_size': len(template_cache._cache) if hasattr(template_cache, '_cache') else 0,
'cooldown_entries': len(self._step_cooldown) if hasattr(self, '_step_cooldown') else 0,
'has_psutil': True
'has_psutil': True,
# GPU information
'gpu_has_gpu': gpu_info['has_gpu'],
'gpu_total_mb': gpu_info['total_mb'],
'gpu_used_mb': gpu_info['used_mb'],
'gpu_free_mb': gpu_info['free_mb'],
'gpu_utilization_percent': gpu_info['utilization_percent'],
'gpu_name': gpu_info['gpu_name'],
'gpu_method': gpu_info.get('method', 'none')
}
except ImportError:
# psutil not available, return basic info
return {
'process_memory_mb': 0,
'process_memory_percent': 0,
'cpu_percent': 0,
'system_memory_percent': 0,
'system_memory_available_gb': 0,
'template_cache_size': len(template_cache._cache) if hasattr(template_cache, '_cache') else 0,
'cooldown_entries': len(self._step_cooldown) if hasattr(self, '_step_cooldown') else 0,
'has_psutil': False
}
except Exception as e:
logger.warning(f"Error getting memory usage: {e}")
# psutil not available, minimal info without GPU detection to prevent hanging
return {
'process_memory_mb': 0,
'process_memory_percent': 0,
@@ -641,7 +1192,36 @@ class MainWindow(QtWidgets.QMainWindow):
'template_cache_size': len(template_cache._cache) if hasattr(template_cache, '_cache') else 0,
'cooldown_entries': len(self._step_cooldown) if hasattr(self, '_step_cooldown') else 0,
'has_psutil': False,
'error': str(e)
# Minimal GPU info to prevent hanging
'gpu_has_gpu': False,
'gpu_total_mb': 0,
'gpu_used_mb': 0,
'gpu_free_mb': 0,
'gpu_utilization_percent': 0,
'gpu_name': 'psutil not available',
'gpu_method': 'disabled'
}
except Exception as e:
logger.warning(f"Error getting memory usage (non-critical): {e}")
# Fallback minimal info to prevent hanging
return {
'process_memory_mb': 0,
'process_memory_percent': 0,
'cpu_percent': 0,
'system_memory_percent': 0,
'system_memory_available_gb': 0,
'template_cache_size': len(template_cache._cache) if hasattr(template_cache, '_cache') else 0,
'cooldown_entries': len(self._step_cooldown) if hasattr(self, '_step_cooldown') else 0,
'has_psutil': False,
'error': str(e),
# Minimal GPU info to prevent hanging
'gpu_has_gpu': False,
'gpu_total_mb': 0,
'gpu_used_mb': 0,
'gpu_free_mb': 0,
'gpu_utilization_percent': 0,
'gpu_name': 'Error occurred',
'gpu_method': 'error'
}
def stop_automation(self):
@@ -725,22 +1305,42 @@ class MainWindow(QtWidgets.QMainWindow):
# Setup cleanup on close
self.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose)
# Setup periodic monitoring timer
# Setup periodic monitoring timer (disabled by default to prevent hanging)
self.monitor_timer = QtCore.QTimer()
self.monitor_timer.timeout.connect(self._monitor_performance)
self.monitor_timer.start(2000) # Monitor every 2 seconds for responsive UI updates
# Start with resource monitoring disabled to prevent startup hanging
self.resource_monitoring_enabled = False
# Initial resource display update (delayed to prevent startup hanging)
QtCore.QTimer.singleShot(2000, self._enable_resource_monitoring)
def _enable_resource_monitoring(self):
"""Enable resource monitoring after startup delay."""
if not hasattr(self, 'resource_monitoring_enabled') or not self.resource_monitoring_enabled:
self.resource_monitoring_enabled = True
# Get initial interval from dropdown (default is 2 seconds)
initial_interval = self.update_interval_combo.currentData() or 2000
self.monitor_timer.start(initial_interval)
# Initial resource display update
QtCore.QTimer.singleShot(100, self._monitor_performance)
self._monitor_performance()
def _monitor_performance(self):
"""
Monitor application performance and memory usage.
Uses error handling to prevent UI hanging.
"""
# Check if monitoring is enabled
if not getattr(self, 'resource_monitoring_enabled', False):
return
try:
# Add timeout protection for memory info gathering
memory_info = self.get_memory_usage()
# Update resource display
# Update resource display (this should be fast)
self._update_resource_display(memory_info)
# Log performance stats periodically (only if running)
@@ -749,7 +1349,7 @@ class MainWindow(QtWidgets.QMainWindow):
# Warning thresholds with duplicate prevention
if avg_loop_time > 1.0:
warning_msg = f"Performance issue: Average loop time {avg_loop_time:.3f}s"
warning_msg = f"Performance warning: Average loop time {avg_loop_time:.3f}s"
self._log_warning_once("performance_slow", warning_msg)
# Memory warning for cache-based monitoring
@@ -767,8 +1367,23 @@ class MainWindow(QtWidgets.QMainWindow):
cpu_msg = f"High CPU usage: {memory_info['cpu_percent']:.1f}%"
self._log_warning_once("cpu_high", cpu_msg)
# GPU memory usage warning
if memory_info.get('gpu_has_gpu', False) and memory_info.get('gpu_utilization_percent', 0) > 90:
gpu_msg = f"High GPU memory usage: {memory_info['gpu_utilization_percent']:.1f}%"
self._log_warning_once("gpu_high", gpu_msg)
except Exception as e:
logger.debug(f"Performance monitoring error: {e}")
# If monitoring fails, show basic error info
try:
self.memory_label.setText("Memory: Monitor Error")
self.cpu_label.setText("CPU: Monitor Error")
self.system_memory_label.setText("System: Monitor Error")
self.cache_label.setText("Cache: Monitor Error")
self.gpu_label.setText("GPU: Monitor Error")
self.performance_label.setText(f"Monitor Error: {str(e)[:30]}...")
except:
pass # If even setting error text fails, just ignore
def _log_warning_once(self, warning_type, message):
"""
@@ -839,6 +1454,55 @@ class MainWindow(QtWidgets.QMainWindow):
self.cache_label.setText(cache_text)
self.cache_label.setStyleSheet(f'font-size: 9pt; color: {cache_color};')
# GPU memory info
gpu_method = memory_info.get('gpu_method', 'unknown')
if memory_info.get('gpu_has_gpu', False):
gpu_used_mb = memory_info.get('gpu_used_mb', 0)
gpu_total_mb = memory_info.get('gpu_total_mb', 0)
gpu_utilization = memory_info.get('gpu_utilization_percent', 0)
if gpu_total_mb > 0:
gpu_text = f"GPU: {gpu_used_mb:.0f}/{gpu_total_mb:.0f}MB ({gpu_utilization:.1f}%)"
gpu_color = '#d9534f' if gpu_utilization > 80 else '#f0ad4e' if gpu_utilization > 60 else '#5cb85c'
# Add GPU name as tooltip
gpu_name = memory_info.get('gpu_name', 'Unknown GPU')
self.gpu_label.setToolTip(f"GPU: {gpu_name} (detected via {gpu_method})")
else:
gpu_text = f"GPU: Detected ({gpu_method})"
gpu_color = '#5bc0de'
gpu_name = memory_info.get('gpu_name', 'Unknown GPU')
self.gpu_label.setToolTip(f"GPU: {gpu_name} (limited info via {gpu_method})")
elif gpu_method == 'loading':
gpu_text = "GPU: Loading..."
gpu_color = '#f0ad4e'
self.gpu_label.setToolTip("GPU detection in progress...")
else:
gpu_text = "GPU: Not detected"
gpu_color = '#777'
self.gpu_label.setToolTip(
"No GPU detected or GPU monitoring libraries not available.\n\n"
"For enhanced GPU monitoring:\n"
"• NVIDIA GPUs: pip install pynvml or GPUtil\n"
"• Integrated GPUs: Built-in Windows WMI support\n"
"• Linux: lspci and DRM detection\n"
"• macOS: system_profiler integration\n\n"
"Click for more information about GPU monitoring."
)
self.gpu_label.setText(gpu_text)
self.gpu_label.setStyleSheet(f'font-size: 9pt; color: {gpu_color};')
# Make GPU label clickable to show GPU info when no GPU detected or loading
if not memory_info.get('gpu_has_gpu', False) and gpu_method != 'loading':
self.gpu_label.mousePressEvent = lambda event: self._show_gpu_info()
self.gpu_label.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.PointingHandCursor))
else:
# Remove click handler if GPU is detected or loading
self.gpu_label.mousePressEvent = None
self.gpu_label.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.ArrowCursor))
# Performance info
if hasattr(self, '_loop_times') and self._loop_times:
avg_time = sum(self._loop_times) / len(self._loop_times)
@@ -863,6 +1527,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.cpu_label.setText("CPU: Error")
self.system_memory_label.setText("System: Error")
self.cache_label.setText("Cache: Error")
self.gpu_label.setText("GPU: Error")
self.performance_label.setText(f"Display Error: {str(e)[:30]}...")
def stop_monitoring(self):
@@ -878,7 +1543,10 @@ class MainWindow(QtWidgets.QMainWindow):
self.cpu_label.hide()
self.system_memory_label.hide()
self.cache_label.hide()
self.gpu_label.hide()
self.performance_label.hide()
self.interval_label.hide()
self.update_interval_combo.hide()
self.toggle_resources_btn.setText('Show Resources')
self.resource_widgets_visible = False
else:
@@ -887,7 +1555,10 @@ class MainWindow(QtWidgets.QMainWindow):
self.cpu_label.show()
self.system_memory_label.show()
self.cache_label.show()
self.gpu_label.show()
self.performance_label.show()
self.interval_label.show()
self.update_interval_combo.show()
self.toggle_resources_btn.setText('Hide Resources')
self.resource_widgets_visible = True
@@ -910,6 +1581,50 @@ class MainWindow(QtWidgets.QMainWindow):
msg.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok)
msg.exec()
def _show_gpu_info(self):
"""Show information about installing GPU monitoring libraries."""
msg = QtWidgets.QMessageBox(self)
msg.setWindowTitle("GPU Memory Monitoring")
msg.setIcon(QtWidgets.QMessageBox.Icon.Information)
msg.setText("GPU monitoring supports both discrete and integrated GPUs")
msg.setInformativeText(
"GPU Memory Monitoring Support:\n\n"
"NVIDIA GPUs (Discrete):\n"
"• pip install pynvml (recommended)\n"
"• pip install GPUtil (alternative)\n"
"• nvidia-smi command line tool\n\n"
"Integrated GPUs (Intel/AMD):\n"
"• Windows: WMI queries (built-in)\n"
"• Linux: lspci and /sys/class/drm\n"
"• macOS: system_profiler\n\n"
"Features enabled:\n"
"• GPU detection and identification\n"
"• VRAM/memory size (where available)\n"
"• Real-time usage (NVIDIA with libraries)\n"
"• Cross-platform support\n\n"
"Note: Integrated GPUs may show limited information\n"
"compared to discrete GPUs due to system limitations.\n"
"NVIDIA GPUs with proper libraries provide the most\n"
"detailed monitoring including real-time usage."
)
msg.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok)
msg.exec()
def _on_update_interval_changed(self):
"""Handle resource monitoring update interval change."""
interval = self.update_interval_combo.currentData()
if interval and hasattr(self, 'monitor_timer'):
# Stop current timer
self.monitor_timer.stop()
# Start timer with new interval
self.monitor_timer.start(interval)
logger.debug(f"Resource monitoring interval changed to {interval}ms")
# Immediately update display to show the change is active
QtCore.QTimer.singleShot(50, self._monitor_performance)
def _save_window_geometry(self):
"""Save the current window size and position to a config file."""
try:
@@ -931,13 +1646,14 @@ class MainWindow(QtWidgets.QMainWindow):
except (json.JSONDecodeError, IOError):
existing_config = {}
# Update with window geometry
# Update with window geometry and settings
existing_config['main_window'] = geometry_data
existing_config['update_interval'] = self.update_interval_combo.currentData()
with open(config_path, 'w') as f:
json.dump(existing_config, f, indent=2)
logger.debug(f"Saved window geometry: {geometry_data}")
logger.debug(f"Saved window geometry and settings: {geometry_data}")
except Exception as e:
logger.debug(f"Error saving window geometry: {e}")
@@ -983,6 +1699,16 @@ class MainWindow(QtWidgets.QMainWindow):
if geometry_data.get('maximized', False):
self.showMaximized()
# Restore update interval if saved
saved_interval = config.get('update_interval')
if saved_interval:
# Find the index of the saved interval in the combo box
for i in range(self.update_interval_combo.count()):
if self.update_interval_combo.itemData(i) == saved_interval:
self.update_interval_combo.setCurrentIndex(i)
break
logger.debug(f"Restored update interval: {saved_interval}ms")
logger.debug(f"Restored window geometry: x={x}, y={y}, w={width}, h={height}")
except (json.JSONDecodeError, IOError, KeyError) as e:
@@ -1184,13 +1910,6 @@ class MainWindow(QtWidgets.QMainWindow):
resource_layout.setSpacing(4)
resource_layout.setContentsMargins(6, 6, 6, 6)
# Toggle button for resource display
self.toggle_resources_btn = QtWidgets.QPushButton('Hide Resources')
self.toggle_resources_btn.setMaximumWidth(100)
self.toggle_resources_btn.setToolTip('Toggle resource usage display. Install psutil for detailed system metrics.')
self.toggle_resources_btn.clicked.connect(self._toggle_resource_display)
resource_layout.addWidget(self.toggle_resources_btn, 0, 2, 1, 1)
# Memory usage
self.memory_label = QtWidgets.QLabel('Memory: --')
self.memory_label.setStyleSheet('font-size: 9pt; color: #333;')
@@ -1211,11 +1930,50 @@ class MainWindow(QtWidgets.QMainWindow):
self.cache_label.setStyleSheet('font-size: 9pt; color: #333;')
resource_layout.addWidget(self.cache_label, 1, 1)
# GPU memory info
self.gpu_label = QtWidgets.QLabel('GPU: --')
self.gpu_label.setStyleSheet('font-size: 9pt; color: #333;')
resource_layout.addWidget(self.gpu_label, 1, 2)
# Performance info
self.performance_label = QtWidgets.QLabel('Performance: --')
self.performance_label.setStyleSheet('font-size: 9pt; color: #333;')
resource_layout.addWidget(self.performance_label, 2, 0, 1, 3)
# Controls row (toggle button and update interval) - placed in a separate row
self.toggle_resources_btn = QtWidgets.QPushButton('Hide Resources')
self.toggle_resources_btn.setMaximumWidth(100)
self.toggle_resources_btn.setToolTip('Toggle resource usage display. Install psutil for detailed system metrics.')
self.toggle_resources_btn.clicked.connect(self._toggle_resource_display)
resource_layout.addWidget(self.toggle_resources_btn, 3, 0)
# Update interval controls
self.interval_label = QtWidgets.QLabel('Update:')
self.interval_label.setStyleSheet('font-size: 9pt; color: #333;')
self.update_interval_combo = QtWidgets.QComboBox()
self.update_interval_combo.setMaximumWidth(80)
self.update_interval_combo.setToolTip('Set resource monitoring update interval')
# Add interval options (in milliseconds)
intervals = [
('0.5s', 500),
('1s', 1000),
('2s', 2000),
('3s', 3000),
('5s', 5000),
('10s', 10000)
]
for text, value in intervals:
self.update_interval_combo.addItem(text, value)
# Set default to 2 seconds (index 2)
self.update_interval_combo.setCurrentIndex(2)
self.update_interval_combo.currentIndexChanged.connect(self._on_update_interval_changed)
resource_layout.addWidget(self.interval_label, 3, 1)
resource_layout.addWidget(self.update_interval_combo, 3, 2)
self.resource_group.setLayout(resource_layout)
self.resource_widgets_visible = True