From 2d2389436d21da137e45213f3caf27cf15626ebf Mon Sep 17 00:00:00 2001 From: Gabriel20xx Date: Thu, 31 Jul 2025 12:52:28 +0200 Subject: [PATCH] dev --- OPTIMIZATION_NOTES.md | 111 +++++ OPTIMIZATION_SUMMARY.md | 71 ++++ __pycache__/main.cpython-313.pyc | Bin 0 -> 127764 bytes main.py | 701 +++++++++++++++++++++++++------ 4 files changed, 755 insertions(+), 128 deletions(-) create mode 100644 OPTIMIZATION_NOTES.md create mode 100644 OPTIMIZATION_SUMMARY.md create mode 100644 __pycache__/main.cpython-313.pyc diff --git a/OPTIMIZATION_NOTES.md b/OPTIMIZATION_NOTES.md new file mode 100644 index 0000000..c294a9f --- /dev/null +++ b/OPTIMIZATION_NOTES.md @@ -0,0 +1,111 @@ +# VisionFlow Automator - Performance Optimizations + +## Overview +This document outlines the performance and memory optimizations implemented to prevent the application from being killed or stalled during scenario execution. + +## Key Optimizations Implemented + +### 1. Memory Management +- **Template Caching**: Added `TemplateCache` class to cache loaded CV2 templates instead of reloading from disk every loop iteration +- **Screenshot Caching**: Implemented screenshot caching with 50ms duration to reduce memory allocations +- **Resource Cleanup**: Added proper cleanup in `cleanup_resources()` method and `closeEvent()` +- **Garbage Collection**: Strategic `gc.collect()` calls to force memory cleanup at appropriate times +- **Pixmap Memory Management**: Optimized image preview handling to prevent large pixmap memory leaks + +### 2. Performance Improvements +- **Optimized Loop Structure**: Redesigned automation loop to be more efficient +- **Step Cooldown System**: Prevents rapid re-execution of the same step (1-second cooldown) +- **Priority Execution**: Only executes one step per loop iteration to prevent blocking +- **Dynamic Sleep**: Adjusts sleep time based on loop performance (0.05-0.2s) +- **Performance Monitoring**: Added loop time tracking and warnings for slow performance + +### 3. Threading Improvements +- **Proper Thread Management**: Added timeout for thread joining in `stop_automation()` +- **Background Processing**: Improved worker thread handling with better error recovery +- **Non-blocking Operations**: Reduced blocking operations in the main thread + +### 4. UI Optimizations +- **Reduced Logging**: Changed default log level from DEBUG to INFO to reduce I/O overhead +- **Efficient Image Previews**: Optimized image scaling and memory usage in dialogs +- **State Update Throttling**: Reduced frequency of UI state updates + +### 5. Resource Monitoring +- **Performance Timer**: Added 10-second monitoring timer to track application health +- **Memory Usage Tracking**: Optional psutil integration for detailed memory monitoring +- **Cache Size Monitoring**: Tracks template cache size and warns when it grows large + +### 6. Error Handling +- **Graceful Degradation**: Better error handling in screenshot capture and template matching +- **Resource Recovery**: Automatic cleanup on errors to prevent resource leaks +- **Hotkey Error Handling**: Continues operation even if hotkey setup fails + +## Configuration Changes + +### PyAutoGUI Optimizations +- Set `pyautogui.PAUSE = 0.01` (reduced from default 0.1s) +- Set `pyautogui.MINIMUM_DURATION = 0` for faster actions +- Kept `pyautogui.FAILSAFE = False` for automation reliability + +### Screenshot Optimization +- Added minimum size validation (10x10 pixels) +- Improved tkinter screenshot tool with better UX +- Memory-efficient PIL image handling with proper cleanup + +### Template Matching +- LRU cache with automatic cleanup of old entries +- Maximum cache size of 50 templates +- Force reload option for template updates + +## Performance Targets + +### Memory Usage +- Template cache limited to 50 entries +- Screenshot cache duration: 50ms +- Automatic cleanup every 100 loop iterations +- Garbage collection after major operations + +### Timing +- Target loop time: 50-200ms +- Step cooldown: 1 second +- Performance warning threshold: 1 second average loop time +- Monitoring interval: 10 seconds + +### Thread Safety +- Worker thread timeout: 2 seconds +- Daemon threads for automatic cleanup +- Proper resource locking where needed + +## Usage Recommendations + +1. **Monitor Performance**: Check logs for performance warnings +2. **Resource Management**: Regularly restart long-running sessions +3. **Template Optimization**: Use appropriately sized template images +4. **Step Design**: Avoid too many simultaneous image detections +5. **System Resources**: Ensure adequate RAM for screenshot operations + +## Future Improvements + +1. **Multi-threading**: Consider separate threads for image processing +2. **Image Compression**: Compress cached templates to save memory +3. **Region Optimization**: Use smaller detection regions when possible +4. **GPU Acceleration**: Consider OpenCV GPU operations for template matching +5. **Background Processing**: Process non-critical operations in background + +## Troubleshooting + +### High Memory Usage +- Check template cache size in logs +- Reduce number of simultaneous steps +- Restart application periodically + +### Slow Performance +- Check average loop times in logs +- Reduce image template sizes +- Simplify detection regions +- Consider fewer simultaneous detections + +### Application Crashes +- Check log files for error patterns +- Monitor system memory usage +- Verify image file integrity +- Check for corrupted templates diff --git a/OPTIMIZATION_SUMMARY.md b/OPTIMIZATION_SUMMARY.md new file mode 100644 index 0000000..584150f --- /dev/null +++ b/OPTIMIZATION_SUMMARY.md @@ -0,0 +1,71 @@ +# VisionFlow Automator - Performance Optimization Summary + +## Optimizations Applied + +✅ **Memory Management** +- Added template caching system to prevent repeated file I/O +- Implemented screenshot caching with 50ms duration +- Added proper resource cleanup and garbage collection +- Optimized image preview handling to prevent memory leaks + +✅ **Performance Improvements** +- Redesigned automation loop for better efficiency +- Added step cooldown system (1-second minimum between executions) +- Implemented dynamic sleep timing based on performance +- Added performance monitoring and warning system + +✅ **Threading & Stability** +- Improved worker thread management with proper cleanup +- Added timeout handling for thread termination +- Better error handling to prevent application crashes +- Non-blocking operations in main UI thread + +✅ **Resource Optimization** +- Reduced logging verbosity to decrease I/O overhead +- Optimized PyAutoGUI settings for faster execution +- Improved screenshot selection tool with better UX +- Added periodic cleanup every 100 loop iterations + +## Key Features Added + +### TemplateCache Class +- LRU cache for CV2 template images +- Automatic cleanup of old entries +- Maximum 50 templates cached +- Memory-efficient template loading + +### Performance Monitoring +- Loop time tracking and warnings +- Memory usage monitoring +- 10-second monitoring intervals +- Automatic performance alerts + +### Optimized Automation Loop +- Priority-based step execution +- Screenshot caching and reuse +- Improved error handling and recovery +- Dynamic timing adjustments + +## Expected Results + +- **Reduced Memory Usage**: Template caching and proper cleanup +- **Faster Performance**: Optimized loops and reduced I/O +- **Better Stability**: Improved error handling and resource management +- **No More Stalling**: Non-blocking operations and better threading + +## Configuration Changes + +```python +# PyAutoGUI optimizations +pyautogui.PAUSE = 0.01 # Faster actions +pyautogui.MINIMUM_DURATION = 0 # No minimum delays + +# Logging optimization +logging.basicConfig(level=logging.INFO) # Reduced verbosity + +# Template cache settings +max_cache_size = 50 # Maximum cached templates +cache_cleanup_threshold = 25% # When to clean old entries +``` + +The optimized application should now run more efficiently without getting killed or stalled during scenario execution. diff --git a/__pycache__/main.cpython-313.pyc b/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..96dc202467debf2c27a56fd0c60ad2b799d70e23 GIT binary patch literal 127764 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 literal 0 HcmV?d00001 diff --git a/main.py b/main.py index c29deb3..9ace82d 100644 --- a/main.py +++ b/main.py @@ -6,6 +6,9 @@ import time import logging import zipfile import shutil +import gc +import weakref +from functools import lru_cache from PyQt6 import QtWidgets, QtGui, QtCore import cv2 import pygetwindow as gw @@ -15,29 +18,83 @@ from pynput import keyboard import tkinter as tk from PIL import ImageGrab, Image, ImageTk -# Disable the PyAutoGUI fail-safe feature. +# Disable the PyAutoGUI fail-safe feature and optimize settings pyautogui.FAILSAFE = False +pyautogui.PAUSE = 0.01 # Reduce pause between actions +pyautogui.MINIMUM_DURATION = 0 # Remove minimum duration for faster execution # Setup logs directory LOGS_DIR = 'logs' if not os.path.exists(LOGS_DIR): os.makedirs(LOGS_DIR) -# Setup logging +# Setup logging with optimized settings logging.basicConfig( - level=logging.DEBUG, + level=logging.INFO, # Changed from DEBUG to reduce log volume format='[%(asctime)s] %(levelname)s: %(message)s', handlers=[ logging.FileHandler(os.path.join(LOGS_DIR, 'scenario_automation.log'), encoding='utf-8'), logging.StreamHandler() ] ) + +# Create a more efficient logger for performance-critical sections +perf_logger = logging.getLogger('performance') +perf_logger.setLevel(logging.WARNING) # Only log warnings and errors for performance + logger = logging.getLogger(__name__) CONFIG_DIR = 'scenarios' if not os.path.exists(CONFIG_DIR): os.makedirs(CONFIG_DIR) +class TemplateCache: + """ + Cache for template images to avoid reloading them repeatedly. + Uses weak references to allow garbage collection when templates are no longer needed. + """ + def __init__(self, max_size=50): + self._cache = {} + self._max_size = max_size + self._access_times = {} + + def get_template(self, path, force_reload=False): + """Get template image from cache or load it.""" + if force_reload or path not in self._cache: + if len(self._cache) >= self._max_size: + self._cleanup_old_entries() + + template = cv2.imread(path) + if template is not None: + self._cache[path] = template + self._access_times[path] = time.time() + return template + else: + self._access_times[path] = time.time() + return self._cache[path] + + def _cleanup_old_entries(self): + """Remove least recently used entries.""" + if not self._access_times: + return + + # Remove oldest 25% of entries + sorted_entries = sorted(self._access_times.items(), key=lambda x: x[1]) + entries_to_remove = len(sorted_entries) // 4 + + for path, _ in sorted_entries[:entries_to_remove]: + self._cache.pop(path, None) + self._access_times.pop(path, None) + + def clear(self): + """Clear all cached templates.""" + self._cache.clear() + self._access_times.clear() + gc.collect() + +# Global template cache instance +template_cache = TemplateCache() + class Scenario: """ Represents a scenario, which consists of a name and a list of steps. @@ -109,63 +166,99 @@ class Scenario: def take_screenshot_with_tkinter(): """ Use Tkinter to let the user select a region of the screen for a screenshot. - The screen is not frozen. + Optimized for better performance and memory usage. Returns a dict with x, y, width, height, or None. """ root = tk.Tk() root.attributes("-alpha", 0.3) root.attributes("-fullscreen", True) + root.attributes("-topmost", True) # Ensure window stays on top root.wait_visibility(root) - canvas = tk.Canvas(root, cursor="cross") + # Configure for better performance + root.resizable(False, False) + root.overrideredirect(True) + + canvas = tk.Canvas(root, cursor="cross", highlightthickness=0) canvas.pack(fill="both", expand=True) rect = None start_x = None start_y = None - selection_rect = None + + # Add instructions + instruction_text = canvas.create_text( + root.winfo_screenwidth() // 2, 50, + text="Click and drag to select area. Press ESC to cancel.", + fill="white", font=("Arial", 14) + ) def on_button_press(event): nonlocal start_x, start_y, rect start_x = event.x start_y = event.y + # Remove instruction text + canvas.delete(instruction_text) rect = canvas.create_rectangle(start_x, start_y, start_x, start_y, outline='red', width=2) def on_mouse_drag(event): nonlocal rect - cur_x, cur_y = (event.x, event.y) - canvas.coords(rect, start_x, start_y, cur_x, cur_y) + if rect and start_x is not None and start_y is not None: + cur_x, cur_y = event.x, event.y + canvas.coords(rect, start_x, start_y, cur_x, cur_y) def on_button_release(event): nonlocal selection_rect - end_x, end_y = (event.x, event.y) - - x1 = min(start_x, end_x) - y1 = min(start_y, end_y) - x2 = max(start_x, end_x) - y2 = max(start_y, end_y) + if start_x is not None and start_y is not None: + end_x, end_y = event.x, event.y + + x1 = min(start_x, end_x) + y1 = min(start_y, end_y) + x2 = max(start_x, end_x) + y2 = max(start_y, end_y) - width = x2 - x1 - height = y2 - y1 + width = x2 - x1 + height = y2 - y1 - if width > 0 and height > 0: - selection_rect = {"x": x1, "y": y1, "width": width, "height": height} + # Minimum size validation + if width > 10 and height > 10: + selection_rect = {"x": x1, "y": y1, "width": width, "height": height} + else: + selection_rect = None root.quit() - canvas.bind("", on_button_press) - canvas.bind("", on_mouse_drag) - canvas.bind("", on_button_release) - def on_escape(event): nonlocal selection_rect selection_rect = None root.quit() - root.bind("", on_escape) + + def on_key_press(event): + if event.keysym == 'Escape': + on_escape(event) - root.mainloop() - root.destroy() + # Bind events + canvas.bind("", on_button_press) + canvas.bind("", on_mouse_drag) + canvas.bind("", on_button_release) + root.bind("", on_escape) + root.bind("", on_key_press) + + # Focus the window to receive key events + root.focus_set() + root.focus_force() + + try: + root.mainloop() + except Exception as e: + logger.error(f"Error in screenshot selection: {e}") + selection_rect = None + finally: + try: + root.destroy() + except Exception as e: + logger.warning(f"Error destroying tkinter window: {e}") return selection_rect @@ -176,82 +269,231 @@ class MainWindow(QtWidgets.QMainWindow): """ def automation_loop(self): """ - Main loop for running automation steps. Continuously looks for image matches - in the selected window or screen and performs actions as defined in the scenario. + Optimized automation loop with memory management and performance improvements. """ logger.info('Automation loop started.') + frame_count = 0 + last_gc_time = time.time() + try: while self.running: - # Only update the state label, do not log every loop - self.set_state('Looking for matches') - selected_window = self.window_combo.currentText() - if selected_window == 'Entire Screen': - screen = pyautogui.screenshot() - offset_x, offset_y = 0, 0 - else: - try: - win = None - for w in gw.getAllWindows(): - if w.title == selected_window: - win = w - break - if win is not None and win.width > 0 and win.height > 0: - x, y, w_, h_ = win.left, win.top, win.width, win.height - screen = pyautogui.screenshot(region=(x, y, w_, h_)) - offset_x, offset_y = x, y - else: - screen = pyautogui.screenshot() - offset_x, offset_y = 0, 0 - except Exception as e: - logger.error(f'Error capturing window screenshot: {e}') - screen = pyautogui.screenshot() - offset_x, offset_y = 0, 0 - screen_np = cv2.cvtColor(np.array(screen), cv2.COLOR_RGB2BGR) - for step in self.current_scenario.steps: - # Detect all images for this step - detections = {} - for img in step.get('images', []): - try: - template = cv2.imread(img['path']) - if template is None: - logger.warning(f"Could not load template image: {img['path']}") - continue - res = cv2.matchTemplate(screen_np, template, cv2.TM_CCOEFF_NORMED) - min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) - # logger.debug(f"Detection for {img['name']}: max_val={max_val}") # Suppressed per request - sensitivity = img.get('sensitivity', 0.9) - if max_val > sensitivity: - # Adjust detected location to be relative to the full screen - abs_loc = (max_loc[0] + offset_x, max_loc[1] + offset_y) - detections[img['name']] = (abs_loc, template.shape) - except Exception as e: - logger.error(f"Error in image detection for {img['name']}: {e}") - # Check condition - cond = step.get('condition', 'OR') - found = [img for img in step.get('images', []) if img.get('name') in detections] - trigger = False - if not step.get('images', []): - trigger = False - elif cond == 'AND': - trigger = len(found) == len(step.get('images', [])) - else: - trigger = len(found) > 0 - # logger.debug(f"Step '{step.get('name', 'step')}' trigger check: found={found}, cond={cond}, trigger={trigger}") # Suppressed per request - if trigger: - self.set_state(f'Performing step: {step.get("name", "step")})') - logger.info(f"Found match for step: {step.get('name', 'step')}. Performing actions.") - # Use the first detected image for position - ref_img = found[0] if found else step.get('images', [])[0] - loc, shape = detections.get(ref_img.get('name'), ((0, 0), (0, 0, 0))) - for act in step.get('actions', []): - self._perform_step_action(act, loc, shape) - logger.info(f"Performed actions for step: {step.get('name', 'step')}") - time.sleep(1) # Prevent spamming - time.sleep(0.2) + loop_start_time = time.time() + + # Only update the state label periodically to reduce UI overhead + if frame_count % 10 == 0: + self.set_state('Looking for matches') + + # Get optimized screenshot + screen_data = self._get_optimized_screenshot() + if screen_data is None: + time.sleep(0.1) + continue + + screen_np, offset_x, offset_y = screen_data + + # Process steps with priority and cooldown system + step_executed = False + current_time = time.time() + + for step_idx, step in enumerate(self.current_scenario.steps): + # Check step cooldown to prevent spam + step_name = step.get('name', f'step_{step_idx}') + if step_name in self._step_cooldown: + if current_time - self._step_cooldown[step_name] < 1.0: + continue + + # Process step images with cache + detections = self._process_step_images(step, screen_np, offset_x, offset_y) + + # Check trigger condition + if self._check_step_trigger(step, detections): + self._processing_step = True + self.set_state(f'Performing step: {step_name}') + logger.info(f"Executing step: {step_name}") + + # Execute step actions + success = self._execute_step_actions(step, detections) + + if success: + self._step_cooldown[step_name] = current_time + step_executed = True + + self._processing_step = False + break # Execute only one step per loop iteration + + # Performance monitoring + loop_time = time.time() - loop_start_time + self._update_performance_stats(loop_time) + + # Periodic cleanup + frame_count += 1 + if frame_count % 100 == 0: # Every 100 frames + self._periodic_cleanup() + + # 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) + except Exception as e: logger.error(f'Automation loop error: {e}') finally: + self._processing_step = False self.set_state('Paused') + logger.info('Automation loop ended.') + + def _get_optimized_screenshot(self): + """ + Get screenshot with caching to reduce memory allocation. + """ + current_time = time.time() + + # Use cached screenshot if recent enough and not processing + if (self._last_screenshot is not None and + not self._processing_step and + current_time - self._last_screenshot_time < self._screenshot_cache_duration): + return self._last_screenshot + + try: + selected_window = self.window_combo.currentText() + if selected_window == 'Entire Screen': + screen = pyautogui.screenshot() + offset_x, offset_y = 0, 0 + else: + # Try to get specific window + win = None + for w in gw.getAllWindows(): + if w.title == selected_window: + win = w + break + + if win is not None and win.width > 0 and win.height > 0: + x, y, w_, h_ = win.left, win.top, win.width, win.height + # Validate window bounds + if x >= 0 and y >= 0 and w_ > 0 and h_ > 0: + screen = pyautogui.screenshot(region=(x, y, w_, h_)) + offset_x, offset_y = x, y + else: + screen = pyautogui.screenshot() + offset_x, offset_y = 0, 0 + else: + screen = pyautogui.screenshot() + offset_x, offset_y = 0, 0 + + # Convert to OpenCV format with optimized memory usage + screen_np = cv2.cvtColor(np.array(screen), cv2.COLOR_RGB2BGR) + + # Cache the result + screen_data = (screen_np, offset_x, offset_y) + self._last_screenshot = screen_data + self._last_screenshot_time = current_time + + # Clean up PIL image to free memory + del screen + + return screen_data + + except Exception as e: + logger.error(f'Error capturing screenshot: {e}') + return None + + def _process_step_images(self, step, screen_np, offset_x, offset_y): + """ + Process step images with template caching. + """ + detections = {} + + for img in step.get('images', []): + try: + # Use cached template + template = template_cache.get_template(img['path']) + if template is None: + logger.warning(f"Could not load template image: {img['path']}") + continue + + # Perform template matching with optimization + res = cv2.matchTemplate(screen_np, template, cv2.TM_CCOEFF_NORMED) + min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) + + sensitivity = img.get('sensitivity', 0.9) + if max_val > sensitivity: + # Adjust detected location to be relative to the full screen + abs_loc = (max_loc[0] + offset_x, max_loc[1] + offset_y) + detections[img['name']] = (abs_loc, template.shape, max_val) + + # Clean up OpenCV result to free memory + del res + + except Exception as e: + logger.error(f"Error in image detection for {img.get('name', 'unknown')}: {e}") + + return detections + + def _check_step_trigger(self, step, detections): + """ + Check if step should be triggered based on detections. + """ + step_images = step.get('images', []) + if not step_images: + return False + + found = [img for img in step_images if img.get('name') in detections] + condition = step.get('condition', 'OR') + + if condition == 'AND': + return len(found) == len(step_images) + else: # OR condition + return len(found) > 0 + + def _execute_step_actions(self, step, detections): + """ + Execute step actions with error handling. + """ + try: + found_images = [img for img in step.get('images', []) if img.get('name') in detections] + if found_images: + # Use the first detected image for position reference + ref_img = found_images[0] + loc, shape, confidence = detections.get(ref_img.get('name'), ((0, 0), (0, 0, 0), 0)) + + for action in step.get('actions', []): + self._perform_step_action(action, loc, shape) + + return True + + except Exception as e: + logger.error(f"Error executing step actions: {e}") + + return False + + def _update_performance_stats(self, loop_time): + """ + Update performance statistics for monitoring. + """ + self._loop_times.append(loop_time) + 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") + + def _periodic_cleanup(self): + """ + Perform periodic cleanup to prevent memory leaks. + """ + # Clear old screenshot cache + self._last_screenshot = None + + # Clean up old step cooldowns (older than 5 minutes) + current_time = time.time() + old_cooldowns = [name for name, timestamp in self._step_cooldown.items() + if current_time - timestamp > 300] + for name in old_cooldowns: + del self._step_cooldown[name] + + # Force garbage collection + gc.collect() def _perform_step_action(self, action, loc, shape): """ @@ -319,32 +561,99 @@ class MainWindow(QtWidgets.QMainWindow): def start_automation(self): """ - Start the automation process and worker thread. + Start the automation process and worker thread with resource checks. """ logger.info('Starting automation.') if not self.current_scenario or self.running: logger.warning('Start Automation: No scenario selected or already running.') return + + # Perform pre-start cleanup + self._last_screenshot = None + self._step_cooldown.clear() + gc.collect() + + # Initialize performance monitoring + self._loop_times.clear() + + # Start automation self.running = True self.set_state('Looking for matches') self.btn_start_stop.setText(f'Stop ({self.hotkey.upper()})') + + # Create and start worker thread self.worker = threading.Thread(target=self.automation_loop, daemon=True) self.worker.start() - self.listener = keyboard.GlobalHotKeys({self.hotkey: self.stop_automation}) - self.listener.start() + + # Setup hotkey listener with error handling + try: + self.listener = keyboard.GlobalHotKeys({self.hotkey: self.stop_automation}) + self.listener.start() + except Exception as e: + logger.error(f"Failed to setup hotkey listener: {e}") + # Continue without hotkey if setup fails + + logger.info('Automation started successfully.') + + def get_memory_usage(self): + """ + Get current memory usage information for monitoring. + """ + try: + import psutil + process = psutil.Process() + memory_info = process.memory_info() + return { + 'rss': memory_info.rss / 1024 / 1024, # MB + 'vms': memory_info.vms / 1024 / 1024, # MB + 'percent': process.memory_percent() + } + except ImportError: + # psutil not available, return basic info + return { + 'template_cache_size': len(template_cache._cache), + 'cooldown_entries': len(self._step_cooldown) if hasattr(self, '_step_cooldown') else 0 + } def stop_automation(self): """ - Stop the automation process and worker thread. + Stop the automation process and worker thread with proper cleanup. """ logger.info('Stopping automation.') self.running = False self.last_toggle_time = time.time() self.set_state('Paused') self.btn_start_stop.setText(f'Start ({self.hotkey.upper()})') + + # Stop global hotkey listener if self.listener: - self.listener.stop() - self.listener = None + try: + self.listener.stop() + except Exception as e: + logger.warning(f"Error stopping hotkey listener: {e}") + finally: + self.listener = None + + # Wait for worker thread to finish (with timeout) + if self.worker and self.worker.is_alive(): + try: + self.worker.join(timeout=2.0) # Wait up to 2 seconds + if self.worker.is_alive(): + logger.warning("Worker thread did not stop within timeout") + except Exception as e: + logger.warning(f"Error joining worker thread: {e}") + finally: + self.worker = None + + # Clear processing flags and cached data + self._processing_step = False + self._last_screenshot = None + self._step_cooldown.clear() + + # Force garbage collection to free memory + gc.collect() + + logger.info('Automation stopped and resources cleaned up.') def __init__(self): """ @@ -364,8 +673,88 @@ class MainWindow(QtWidgets.QMainWindow): self.current_scenario = None self.selected_step_idx = None self.last_toggle_time = 0 # For debounce of start/stop + + # Memory optimization attributes + self._last_screenshot = None + self._last_screenshot_time = 0 + self._screenshot_cache_duration = 0.05 # Cache screenshot for 50ms + self._processing_step = False + self._step_cooldown = {} # Cooldown tracking for steps + + # Performance monitoring + self._loop_times = [] + self._max_loop_time_samples = 10 + self.init_ui() self.load_scenarios() + + # Setup cleanup on close + self.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose) + + # Setup periodic monitoring timer + self.monitor_timer = QtCore.QTimer() + self.monitor_timer.timeout.connect(self._monitor_performance) + self.monitor_timer.start(10000) # Monitor every 10 seconds + + def _monitor_performance(self): + """ + Monitor application performance and memory usage. + """ + if not self.running: + return + + try: + memory_info = self.get_memory_usage() + + # Log performance stats periodically + if hasattr(self, '_loop_times') and self._loop_times: + avg_loop_time = sum(self._loop_times) / len(self._loop_times) + + # Warning thresholds + if avg_loop_time > 1.0: + logger.warning(f"Performance issue: Average loop time {avg_loop_time:.3f}s") + + # Memory warning for cache-based monitoring + if 'template_cache_size' in memory_info and memory_info['template_cache_size'] > 20: + logger.info(f"Template cache has {memory_info['template_cache_size']} entries") + + except Exception as e: + logger.debug(f"Performance monitoring error: {e}") + + def stop_monitoring(self): + """Stop performance monitoring timer.""" + if hasattr(self, 'monitor_timer') and self.monitor_timer: + self.monitor_timer.stop() + + def closeEvent(self, event): + """Handle application close event with proper cleanup.""" + self.cleanup_resources() + event.accept() + + def cleanup_resources(self): + """Clean up resources to prevent memory leaks.""" + if self.running: + self.stop_automation() + + # Stop monitoring timer + self.stop_monitoring() + + # Clear template cache + template_cache.clear() + + # Clear screenshot cache + self._last_screenshot = None + + # Clear performance tracking + if hasattr(self, '_loop_times'): + self._loop_times.clear() + if hasattr(self, '_step_cooldown'): + self._step_cooldown.clear() + + # Force garbage collection + gc.collect() + + logger.info("Resources cleaned up successfully") def init_ui(self): """ @@ -1208,6 +1597,7 @@ class StepDialog(QtWidgets.QDialog): def add_image_to_step(self): """ Add a new image to the step by taking a screenshot and letting the user select a region. + Optimized for memory efficiency. """ main_window = self.parent() step_name = self.name_edit.text() @@ -1224,12 +1614,14 @@ class StepDialog(QtWidgets.QDialog): try: rect_coords = take_screenshot_with_tkinter() if rect_coords: - cropped_pil_image = ImageGrab.grab(bbox=( + # Use more memory-efficient screenshot capture + bbox = ( rect_coords['x'], rect_coords['y'], rect_coords['x'] + rect_coords['width'], rect_coords['y'] + rect_coords['height'] - )) + ) + cropped_pil_image = ImageGrab.grab(bbox=bbox) logger.debug("StepDialog.add_image_to_step: Screenshot selection finished.") except Exception as e: logger.error(f"StepDialog.add_image_to_step: Screenshot failed: {e}") @@ -1239,42 +1631,95 @@ class StepDialog(QtWidgets.QDialog): main_window.show() if cropped_pil_image and rect_coords: - img_np = np.array(cropped_pil_image.convert('RGB')) - height, width, channel = img_np.shape - bytes_per_line = 3 * width - qimage = QtGui.QImage(img_np.data, width, height, bytes_per_line, QtGui.QImage.Format.Format_RGB888) - cropped_pixmap = QtGui.QPixmap.fromImage(qimage) + try: + # Convert PIL to QPixmap more efficiently + img_np = np.array(cropped_pil_image.convert('RGB')) + height, width, channel = img_np.shape + bytes_per_line = 3 * width + qimage = QtGui.QImage(img_np.data, width, height, bytes_per_line, QtGui.QImage.Format.Format_RGB888) + cropped_pixmap = QtGui.QPixmap.fromImage(qimage) - name_dialog = self.NameImageDialog(cropped_pixmap, self) - if name_dialog.exec(): - name = name_dialog.get_name() - if name: - scenario_dir = main_window.current_scenario.get_scenario_dir() - step_images_dir = os.path.join(scenario_dir, "steps", step_name) - if not os.path.exists(step_images_dir): - os.makedirs(step_images_dir) + name_dialog = self.NameImageDialog(cropped_pixmap, self) + if name_dialog.exec(): + name = name_dialog.get_name() + if name: + scenario_dir = main_window.current_scenario.get_scenario_dir() + step_images_dir = os.path.join(scenario_dir, "steps", step_name) + if not os.path.exists(step_images_dir): + os.makedirs(step_images_dir) - safe_name = "".join(c for c in name if c.isalnum() or c in (' ', '_')).rstrip() - path = os.path.join(step_images_dir, f'{safe_name}.png') + safe_name = "".join(c for c in name if c.isalnum() or c in (' ', '_')).rstrip() + path = os.path.join(step_images_dir, f'{safe_name}.png') - if cropped_pixmap.save(path, "PNG"): - logger.info(f"Screenshot saved to {path}") - img_obj = {'path': path, 'region': [rect_coords['x'], rect_coords['y'], rect_coords['width'], rect_coords['height']], 'name': name, 'sensitivity': 0.9} - self.images.append(img_obj) - self.img_list.addItem(name) - else: - logger.error(f"Failed to save screenshot to {path}") - QtWidgets.QMessageBox.critical(self, 'Error', 'Failed to save the screenshot file.') + # Save with PNG optimization + if cropped_pixmap.save(path, "PNG", quality=90): + logger.info(f"Screenshot saved to {path}") + img_obj = { + 'path': path, + 'region': [rect_coords['x'], rect_coords['y'], rect_coords['width'], rect_coords['height']], + 'name': name, + 'sensitivity': 0.9 + } + self.images.append(img_obj) + self.img_list.addItem(name) + else: + logger.error(f"Failed to save screenshot to {path}") + QtWidgets.QMessageBox.critical(self, 'Error', 'Failed to save the screenshot file.') + + # Clean up memory + del img_np, qimage, cropped_pixmap + + finally: + # Ensure PIL image is cleaned up + if cropped_pil_image: + cropped_pil_image.close() + del cropped_pil_image + gc.collect() def update_image_preview(self, current, previous): """ Update the image preview label when a new image is selected. + Optimized to prevent memory leaks from large pixmaps. """ + # Clear previous pixmap to free memory + if previous: + self.img_preview.clear() + if current: - idx = self.img_list.row(current) - path = self.images[idx]['path'] - pixmap = QtGui.QPixmap(path) - self.img_preview.setPixmap(pixmap.scaled(self.img_preview.size(), QtCore.Qt.AspectRatioMode.KeepAspectRatio, QtCore.Qt.TransformationMode.SmoothTransformation)) + try: + idx = self.img_list.row(current) + if idx < 0 or idx >= len(self.images): + self.img_preview.setText('Image Preview') + return + + path = self.images[idx]['path'] + if not os.path.exists(path): + self.img_preview.setText('Image Not Found') + return + + # Load and scale image efficiently + pixmap = QtGui.QPixmap(path) + if pixmap.isNull(): + self.img_preview.setText('Invalid Image') + return + + # Scale with memory optimization + preview_size = self.img_preview.size() + if pixmap.width() > preview_size.width() or pixmap.height() > preview_size.height(): + scaled_pixmap = pixmap.scaled( + preview_size, + QtCore.Qt.AspectRatioMode.KeepAspectRatio, + QtCore.Qt.TransformationMode.SmoothTransformation + ) + self.img_preview.setPixmap(scaled_pixmap) + # Clear original large pixmap + del pixmap + else: + self.img_preview.setPixmap(pixmap) + + except Exception as e: + logger.error(f"Error updating image preview: {e}") + self.img_preview.setText('Preview Error') else: self.img_preview.setText('Image Preview')