Optimize memory management and cleanup processes in MainWindow and TemplateCache

This commit is contained in:
2025-08-07 17:10:47 +02:00
parent 7dbb161ded
commit 2f8b3ba872
+196 -15
View File
@@ -602,8 +602,9 @@ class TemplateCache:
"""
Cache for template images to avoid reloading them repeatedly.
Uses weak references to allow garbage collection when templates are no longer needed.
Optimized for low memory usage.
"""
def __init__(self, max_size=50):
def __init__(self, max_size=20): # Reduced from 50 to 20 for memory efficiency
self._cache = {}
self._max_size = max_size
self._access_times = {}
@@ -628,13 +629,16 @@ class TemplateCache:
if not self._access_times:
return
# Remove oldest 25% of entries
# Remove oldest 40% of entries (more aggressive cleanup)
sorted_entries = sorted(self._access_times.items(), key=lambda x: x[1])
entries_to_remove = len(sorted_entries) // 4
entries_to_remove = max(1, len(sorted_entries) * 2 // 5) # Remove at least 1, or 40%
for path, _ in sorted_entries[:entries_to_remove]:
self._cache.pop(path, None)
self._access_times.pop(path, None)
# Force garbage collection after cleanup
gc.collect()
def clear(self):
"""Clear all cached templates."""
@@ -1316,11 +1320,24 @@ class MainWindow(QtWidgets.QMainWindow):
loop_time = time.time() - loop_start_time
self._update_performance_stats(loop_time)
# Periodic cleanup
# Periodic cleanup and memory management
frame_count += 1
if frame_count % 100 == 0: # Every 100 frames
current_time = time.time()
# More frequent memory cleanup
if frame_count % 50 == 0: # Every 50 frames (reduced from 100)
self._periodic_cleanup()
# Garbage collection every 30 seconds
if current_time - last_gc_time > 30:
gc.collect()
last_gc_time = current_time
# Clear screenshot cache periodically to prevent memory buildup
if (self._last_screenshot is not None and
current_time - self._last_screenshot_time > 2.0): # Clear after 2 seconds
self._last_screenshot = None
# Dynamic sleep based on performance
sleep_time = max(0.05, 0.2 - loop_time) if not step_executed else 0.1
time.sleep(sleep_time)
@@ -1753,16 +1770,22 @@ class MainWindow(QtWidgets.QMainWindow):
# Memory optimization attributes
self._last_screenshot = None
self._last_screenshot_time = 0
self._screenshot_cache_duration = 0.05 # Cache screenshot for 50ms
self._screenshot_cache_duration = 0.03 # Reduced from 0.05 to 0.03 for faster memory freeing
self._processing_step = False
self._step_cooldown = {} # Cooldown tracking for steps
# Performance monitoring
self._loop_times = []
self._max_loop_time_samples = 10
self._max_loop_time_samples = 5 # Reduced from 10 to 5 for memory efficiency
self._last_warning_messages = {} # Track last warning messages to prevent duplicates
self._warning_cooldown = 30 # Seconds between duplicate warnings
# Memory management
self._memory_cleanup_timer = QtCore.QTimer()
self._memory_cleanup_timer.timeout.connect(self._periodic_memory_cleanup)
self._memory_cleanup_timer.start(300000) # Cleanup every 5 minutes
self._last_memory_cleanup = time.time()
# Resource usage tracking for max/average values
self._resource_history = {
'memory_mb': [],
@@ -1773,7 +1796,7 @@ class MainWindow(QtWidgets.QMainWindow):
'gpu_utilization': [],
'loop_times': []
}
self._max_resource_samples = 50 # Keep last 50 samples for statistics
self._max_resource_samples = 30 # Reduced from 50 to 30 for memory efficiency
self.init_ui()
self.load_scenarios()
@@ -2053,7 +2076,10 @@ class MainWindow(QtWidgets.QMainWindow):
else:
system_gpu_text = "GPU: Not detected\nClick for info"
system_gpu_color = colors['text_light']
self.system_gpu_label.mousePressEvent = lambda event: self._show_gpu_info()
# Clear any existing event handler first to prevent memory leaks
self.system_gpu_label.mousePressEvent = None
# Use a proper connection instead of lambda to prevent circular reference
self.system_gpu_label.mousePressEvent = self._on_gpu_label_clicked
self.system_gpu_label.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.PointingHandCursor))
self.system_gpu_label.setText(system_gpu_text)
@@ -2086,7 +2112,10 @@ class MainWindow(QtWidgets.QMainWindow):
else:
program_ram_text = "RAM: N/A\npsutil needed"
program_ram_color = colors['warning']
self.program_ram_label.mousePressEvent = lambda event: self._show_psutil_info()
# Clear any existing event handler first to prevent memory leaks
self.program_ram_label.mousePressEvent = None
# Use a proper connection instead of lambda to prevent circular reference
self.program_ram_label.mousePressEvent = self._on_ram_label_clicked
self.program_ram_label.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.PointingHandCursor))
self.program_ram_label.setText(program_ram_text)
@@ -2151,6 +2180,12 @@ class MainWindow(QtWidgets.QMainWindow):
self.monitor_timer.stop()
if hasattr(self, 'theme_monitor_timer') and self.theme_monitor_timer:
self.theme_monitor_timer.stop()
if hasattr(self, '_memory_cleanup_timer') and self._memory_cleanup_timer:
self._memory_cleanup_timer.stop()
# Clear caches when monitoring stops
self._last_screenshot = None
gc.collect()
def _show_psutil_info(self):
"""Show information about installing psutil for enhanced monitoring."""
@@ -2200,6 +2235,14 @@ class MainWindow(QtWidgets.QMainWindow):
msg.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok)
msg.exec()
def _on_gpu_label_clicked(self, event):
"""Handle GPU label click event without lambda to prevent memory leaks."""
self._show_gpu_info()
def _on_ram_label_clicked(self, event):
"""Handle RAM label click event without lambda to prevent memory leaks."""
self._show_psutil_info()
def _on_update_interval_changed(self):
"""Handle resource monitoring update interval change."""
interval = self.update_interval_combo.currentData()
@@ -2448,18 +2491,60 @@ class MainWindow(QtWidgets.QMainWindow):
event.accept()
def cleanup_resources(self):
"""Clean up resources to prevent memory leaks."""
"""Enhanced cleanup to prevent memory leaks."""
if self.running:
self.stop_automation()
# Stop monitoring timer
# Stop and disconnect all timers
self.stop_monitoring()
# Disconnect all signal connections to prevent circular references
try:
# Disconnect timer signals
if hasattr(self, 'monitor_timer') and self.monitor_timer:
self.monitor_timer.timeout.disconnect()
self.monitor_timer.stop()
self.monitor_timer.deleteLater()
self.monitor_timer = None
if hasattr(self, 'theme_monitor_timer') and self.theme_monitor_timer:
self.theme_monitor_timer.timeout.disconnect()
self.theme_monitor_timer.stop()
self.theme_monitor_timer.deleteLater()
self.theme_monitor_timer = None
if hasattr(self, '_memory_cleanup_timer') and self._memory_cleanup_timer:
self._memory_cleanup_timer.timeout.disconnect()
self._memory_cleanup_timer.stop()
self._memory_cleanup_timer.deleteLater()
self._memory_cleanup_timer = None
# Clear event handlers that use lambda functions to prevent circular references
if hasattr(self, 'system_gpu_label'):
self.system_gpu_label.mousePressEvent = None
self.system_gpu_label.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.ArrowCursor))
if hasattr(self, 'program_ram_label'):
self.program_ram_label.mousePressEvent = None
self.program_ram_label.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.ArrowCursor))
except Exception as e:
logger.debug(f"Error disconnecting signals: {e}")
# Clear template cache
template_cache.clear()
# Clear screenshot cache
self._last_screenshot = None
self._last_screenshot_time = 0
# Clear GPU memory cache
if hasattr(get_gpu_memory_info, '_cached_info'):
delattr(get_gpu_memory_info, '_cached_info')
if hasattr(get_gpu_memory_info, '_cache_time'):
delattr(get_gpu_memory_info, '_cache_time')
if hasattr(get_gpu_memory_info, '_updating'):
delattr(get_gpu_memory_info, '_updating')
# Clear performance tracking
if hasattr(self, '_loop_times'):
@@ -2470,11 +2555,100 @@ class MainWindow(QtWidgets.QMainWindow):
for key in self._resource_history:
self._resource_history[key].clear()
# Force garbage collection
gc.collect()
# Clear warning message cache
if hasattr(self, '_last_warning_messages'):
self._last_warning_messages.clear()
# Clear any geometry save timer
if hasattr(self, '_geometry_save_timer'):
self._geometry_save_timer.stop()
self._geometry_save_timer.deleteLater()
self._geometry_save_timer = None
# Clear dialog-related attributes to prevent references
if hasattr(self, '_edit_step_idx'):
delattr(self, '_edit_step_idx')
if hasattr(self, '_step_dialog') and self._step_dialog:
self._step_dialog.deleteLater()
self._step_dialog = None
# Clear image preview pixmaps if any dialogs are open
try:
for widget in QtWidgets.QApplication.allWidgets():
if hasattr(widget, 'img_preview') and hasattr(widget.img_preview, 'clear'):
widget.img_preview.clear()
# Disconnect any remaining signal connections
if hasattr(widget, 'disconnect'):
try:
widget.disconnect()
except:
pass
except:
pass
# Force garbage collection multiple times for better cleanup
for _ in range(3):
gc.collect()
logger.info("Resources cleaned up successfully")
def _periodic_memory_cleanup(self):
"""Periodic memory cleanup to prevent memory accumulation."""
current_time = time.time()
# Only cleanup if enough time has passed since last cleanup
if current_time - self._last_memory_cleanup < 300: # 5 minutes
return
try:
# Clear old screenshot cache if not used recently
if (self._last_screenshot is not None and
current_time - self._last_screenshot_time > 60): # 1 minute old
self._last_screenshot = None
self._last_screenshot_time = 0
# Limit resource history size to prevent memory growth
max_history_size = 50 # Reduced from default to save memory
if hasattr(self, '_resource_history'):
for key in self._resource_history:
if len(self._resource_history[key]) > max_history_size:
# Keep only the most recent entries
self._resource_history[key] = self._resource_history[key][-max_history_size:]
# Limit loop times tracking
if hasattr(self, '_loop_times') and len(self._loop_times) > self._max_loop_time_samples:
self._loop_times = self._loop_times[-self._max_loop_time_samples:]
# Clear old step cooldowns (older than 10 minutes)
if hasattr(self, '_step_cooldown'):
cutoff_time = current_time - 600 # 10 minutes
self._step_cooldown = {k: v for k, v in self._step_cooldown.items() if v > cutoff_time}
# Clear old warning message cache
if hasattr(self, '_last_warning_messages'):
cutoff_time = current_time - self._warning_cooldown * 2
self._last_warning_messages = {
k: v for k, v in self._last_warning_messages.items()
if v[0] > cutoff_time
}
# Template cache cleanup - remove least recently used items if cache is large
if hasattr(template_cache, '_cache') and len(template_cache._cache) > 15:
# Clear 1/3 of the cache to reduce memory
items_to_remove = len(template_cache._cache) // 3
cache_items = list(template_cache._cache.items())
for i in range(items_to_remove):
del template_cache._cache[cache_items[i][0]]
# Force garbage collection
gc.collect()
self._last_memory_cleanup = current_time
logger.debug("Periodic memory cleanup completed")
except Exception as e:
logger.debug(f"Error during periodic memory cleanup: {e}")
def is_system_dark_theme(self):
"""
Detect if the system is using dark theme.
@@ -3702,6 +3876,12 @@ class MainWindow(QtWidgets.QMainWindow):
self._step_dialog.deleteLater()
self._step_dialog = None
def _on_edit_step_accepted_handler(self):
"""Handler to avoid lambda function for edit step dialog"""
if hasattr(self, '_edit_step_idx'):
self._on_edit_step_accepted(self._edit_step_idx)
delattr(self, '_edit_step_idx')
def edit_step(self):
"""
Open the dialog to edit the selected step in the current scenario.
@@ -3711,7 +3891,8 @@ class MainWindow(QtWidgets.QMainWindow):
return
step = self.current_scenario.steps[idx]
self._step_dialog = StepDialog(self, step)
self._step_dialog.accepted.connect(lambda: self._on_edit_step_accepted(idx))
self._edit_step_idx = idx # Store idx to avoid lambda
self._step_dialog.accepted.connect(self._on_edit_step_accepted_handler)
self._step_dialog.show()
def _on_edit_step_accepted(self, idx):