-
-
Notifications
You must be signed in to change notification settings - Fork 0
perf: eliminate redundant copies, fix memory leaks, use platform abstraction #50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -84,109 +84,137 @@ def is_linux() -> bool: | |
| return sys.platform in ("linux", "linux2") | ||
|
|
||
|
|
||
| _singleton_cache: dict[str, object] = {} | ||
|
|
||
|
|
||
| def _clear_singleton_cache() -> None: | ||
| """Clear singleton cache (for testing).""" | ||
| _singleton_cache.clear() | ||
|
|
||
|
|
||
| def get_window_manager() -> WindowManager: | ||
| """Create a platform-appropriate WindowManager instance. | ||
| """Get the platform-appropriate WindowManager singleton. | ||
|
|
||
| Returns: | ||
| WindowManager implementation for current platform | ||
|
|
||
| Raises: | ||
| RuntimeError: If platform is not supported | ||
| """ | ||
| platform = get_platform_name() | ||
| if platform == "windows": | ||
| from .windows import WindowManagerWindows | ||
| key = "window_manager" | ||
| if key not in _singleton_cache: | ||
| platform = get_platform_name() | ||
| if platform == "windows": | ||
| from .windows import WindowManagerWindows | ||
|
|
||
| return WindowManagerWindows() | ||
| elif platform == "linux": | ||
| from .linux import WindowManagerLinux | ||
| _singleton_cache[key] = WindowManagerWindows() | ||
| elif platform == "linux": | ||
| from .linux import WindowManagerLinux | ||
|
|
||
| return WindowManagerLinux() | ||
| raise RuntimeError(f"Unsupported platform: {platform}") | ||
| _singleton_cache[key] = WindowManagerLinux() | ||
| else: | ||
| raise RuntimeError(f"Unsupported platform: {platform}") | ||
| return _singleton_cache[key] # type: ignore[return-value] | ||
|
|
||
|
|
||
| def get_window_capture(max_workers: int = 4) -> WindowCapture: | ||
| """Create a platform-appropriate WindowCapture instance. | ||
| """Get the platform-appropriate WindowCapture singleton. | ||
|
|
||
| Args: | ||
| max_workers: Number of capture worker threads | ||
| max_workers: Number of capture worker threads (only used on first call) | ||
|
|
||
| Returns: | ||
| WindowCapture implementation for current platform | ||
|
|
||
| Raises: | ||
| RuntimeError: If platform is not supported | ||
| """ | ||
| platform = get_platform_name() | ||
| if platform == "windows": | ||
| from .windows import WindowCaptureWindows | ||
| key = "window_capture" | ||
| if key not in _singleton_cache: | ||
| platform = get_platform_name() | ||
| if platform == "windows": | ||
| from .windows import WindowCaptureWindows | ||
|
|
||
| return WindowCaptureWindows(max_workers=max_workers) | ||
| elif platform == "linux": | ||
| from .linux import WindowCaptureLinux | ||
| _singleton_cache[key] = WindowCaptureWindows(max_workers=max_workers) | ||
| elif platform == "linux": | ||
| from .linux import WindowCaptureLinux | ||
|
|
||
| return WindowCaptureLinux(max_workers=max_workers) | ||
| raise RuntimeError(f"Unsupported platform: {platform}") | ||
| _singleton_cache[key] = WindowCaptureLinux(max_workers=max_workers) | ||
| else: | ||
| raise RuntimeError(f"Unsupported platform: {platform}") | ||
| return _singleton_cache[key] # type: ignore[return-value] | ||
|
|
||
|
|
||
| def get_screen_manager() -> ScreenManager: | ||
| """Create a platform-appropriate ScreenManager instance. | ||
| """Get the platform-appropriate ScreenManager singleton. | ||
|
|
||
| Returns: | ||
| ScreenManager implementation for current platform | ||
|
|
||
| Raises: | ||
| RuntimeError: If platform is not supported | ||
| """ | ||
| platform = get_platform_name() | ||
| if platform == "windows": | ||
| from .windows import ScreenManagerWindows | ||
| key = "screen_manager" | ||
| if key not in _singleton_cache: | ||
| platform = get_platform_name() | ||
| if platform == "windows": | ||
| from .windows import ScreenManagerWindows | ||
|
|
||
| return ScreenManagerWindows() | ||
| elif platform == "linux": | ||
| from .linux import ScreenManagerLinux | ||
| _singleton_cache[key] = ScreenManagerWindows() | ||
| elif platform == "linux": | ||
| from .linux import ScreenManagerLinux | ||
|
|
||
| return ScreenManagerLinux() | ||
| raise RuntimeError(f"Unsupported platform: {platform}") | ||
| _singleton_cache[key] = ScreenManagerLinux() | ||
| else: | ||
| raise RuntimeError(f"Unsupported platform: {platform}") | ||
| return _singleton_cache[key] # type: ignore[return-value] | ||
|
|
||
|
|
||
| def get_eve_path_resolver() -> EVEPathResolver: | ||
| """Create a platform-appropriate EVEPathResolver instance. | ||
| """Get the platform-appropriate EVEPathResolver singleton. | ||
|
|
||
| Returns: | ||
| EVEPathResolver implementation for current platform | ||
|
|
||
| Raises: | ||
| RuntimeError: If platform is not supported | ||
| """ | ||
| platform = get_platform_name() | ||
| if platform == "windows": | ||
| from .windows import EVEPathResolverWindows | ||
| key = "eve_path_resolver" | ||
| if key not in _singleton_cache: | ||
| platform = get_platform_name() | ||
| if platform == "windows": | ||
| from .windows import EVEPathResolverWindows | ||
|
|
||
| return EVEPathResolverWindows() | ||
| elif platform == "linux": | ||
| from .linux import EVEPathResolverLinux | ||
| _singleton_cache[key] = EVEPathResolverWindows() | ||
| elif platform == "linux": | ||
| from .linux import EVEPathResolverLinux | ||
|
|
||
| return EVEPathResolverLinux() | ||
| raise RuntimeError(f"Unsupported platform: {platform}") | ||
| _singleton_cache[key] = EVEPathResolverLinux() | ||
| else: | ||
| raise RuntimeError(f"Unsupported platform: {platform}") | ||
| return _singleton_cache[key] # type: ignore[return-value] | ||
|
|
||
|
|
||
| def get_hotkey_helper() -> HotkeyHelper: | ||
| """Create a platform-appropriate HotkeyHelper instance. | ||
| """Get the platform-appropriate HotkeyHelper singleton. | ||
|
|
||
| Returns: | ||
| HotkeyHelper implementation for current platform | ||
|
|
||
| Raises: | ||
| RuntimeError: If platform is not supported | ||
| """ | ||
| platform = get_platform_name() | ||
| if platform == "windows": | ||
| from .windows import HotkeyHelperWindows | ||
|
|
||
| return HotkeyHelperWindows() | ||
| elif platform == "linux": | ||
| from .linux import HotkeyHelperLinux | ||
|
|
||
| return HotkeyHelperLinux() | ||
| raise RuntimeError(f"Unsupported platform: {platform}") | ||
| key = "hotkey_helper" | ||
| if key not in _singleton_cache: | ||
| platform = get_platform_name() | ||
| if platform == "windows": | ||
| from .windows import HotkeyHelperWindows | ||
|
|
||
| _singleton_cache[key] = HotkeyHelperWindows() | ||
| elif platform == "linux": | ||
| from .linux import HotkeyHelperLinux | ||
|
|
||
| _singleton_cache[key] = HotkeyHelperLinux() | ||
| else: | ||
| raise RuntimeError(f"Unsupported platform: {platform}") | ||
| return _singleton_cache[key] # type: ignore[return-value] | ||
|
Comment on lines
95
to
+220
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The introduction of a singleton cache is a good performance improvement. However, the implementation is very repetitive across all the factory functions ( |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -154,24 +154,17 @@ class WindowManagerLinux(WindowManager): | |
| """Linux window management using wmctrl and xdotool.""" | ||
|
|
||
| def get_window_list(self) -> list[tuple[str, str]]: | ||
| """Get list of all windows using wmctrl.""" | ||
| try: | ||
| result = run_x11_subprocess(["wmctrl", "-l"], timeout=2) | ||
| stdout = result.stdout.decode("utf-8", errors="replace") | ||
|
|
||
| windows = [] | ||
| for line in stdout.strip().split("\n"): | ||
| if line: | ||
| parts = line.split(None, 3) | ||
| if len(parts) >= 4: | ||
| window_id = parts[0] | ||
| window_title = parts[3] | ||
| windows.append((window_id, window_title)) | ||
|
|
||
| return windows | ||
| except (OSError, subprocess.SubprocessError) as e: | ||
| logger.warning(f"Failed to get window list: {e}") | ||
| return [] | ||
| """Get list of all windows using wmctrl (cached).""" | ||
| output = _get_wmctrl_window_list() | ||
| windows = [] | ||
| for line in output.strip().split("\n"): | ||
| if line: | ||
| parts = line.split(None, 3) | ||
| if len(parts) >= 4: | ||
| window_id = parts[0] | ||
| window_title = parts[3] | ||
| windows.append((window_id, window_title)) | ||
| return windows | ||
|
|
||
| def get_eve_windows(self) -> list[tuple[str, str]]: | ||
| """Get list of EVE Online windows.""" | ||
|
|
@@ -404,7 +397,11 @@ def capture_window_sync(self, window_id: str, scale: float = 1.0) -> Image.Image | |
| return self._capture_imagemagick(window_id) | ||
|
|
||
| def _capture_xlib(self, window_id: str) -> Image.Image | None: | ||
| """Capture window using python-xlib (no subprocess).""" | ||
| """Capture window using python-xlib (no subprocess). | ||
|
|
||
| Returns an RGBX image to avoid the overhead of converting to RGB. | ||
| The UI layer handles RGBX natively via QImage.Format_RGBX8888. | ||
| """ | ||
| try: | ||
| # Thread-local Display to avoid locking on hot path | ||
| if not hasattr(_thread_local, "display"): | ||
|
|
@@ -414,8 +411,8 @@ def _capture_xlib(self, window_id: str) -> Image.Image | None: | |
| window = disp.create_resource_object("window", int(window_id, 16)) | ||
| geom = window.get_geometry() | ||
| raw = window.get_image(0, 0, geom.width, geom.height, X.ZPixmap, 0xFFFFFFFF) | ||
| img = Image.frombytes("RGBX", (geom.width, geom.height), raw.data, "raw", "BGRX") | ||
| return img.convert("RGB") | ||
| # Return RGBX directly — avoids a full-image copy from .convert("RGB") | ||
| return Image.frombytes("RGBX", (geom.width, geom.height), raw.data, "raw", "BGRX") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change to return an 'RGBX' image directly is a great performance optimization. However, it seems the corresponding test |
||
| except Exception as e: | ||
| logger.debug(f"Xlib capture failed for {window_id}: {e}") | ||
| return None | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great performance improvement. To make it even more efficient, you can avoid calling
.lower()twice on the message.