Fix Memory Leak(maybe??)
This commit is contained in:
1 parent
8e4ef78cd7
commit
84af1ab3bc
5 files changed
+396
-138
No files matched your search
@@ -1,3 +1,6 @@
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('Gdk', '3.0')
|
||||
from gi.repository import Gdk, GLib, Gtk
|
||||
|
||||
from fabric.bluetooth import BluetoothClient, BluetoothDevice
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
from fabric.widgets.scale import Scale
|
||||
from widgets.wayland import WaylandWindow as Window
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.box import Box
|
||||
from services.mpris import MprisPlayer, MprisPlayerManager
|
||||
|
||||
# Standard library imports
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from typing import List
|
||||
import threading
|
||||
import weakref
|
||||
from typing import List, Optional, Dict, Set
|
||||
import gc
|
||||
|
||||
# Fabric imports
|
||||
from fabric.widgets.scale import Scale
|
||||
from widgets.wayland import WaylandWindow as Window
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.utils import bulk_connect, invoke_repeater, cooldown
|
||||
from fabric.utils.helpers import get_relative_path
|
||||
from fabric.widgets.image import Image
|
||||
@@ -22,12 +24,17 @@ from fabric.widgets.svg import Svg
|
||||
from gi.repository import GLib, GObject
|
||||
from loguru import logger
|
||||
|
||||
# Local imports
|
||||
from services.mpris import MprisPlayer, MprisPlayerManager
|
||||
import config.data as data
|
||||
|
||||
CACHE_DIR = f"{data.CACHE_DIR}/media"
|
||||
|
||||
# Shared MPRIS manager to reduce memory usage
|
||||
# Global memory management
|
||||
_shared_mpris_manager = None
|
||||
_widget_cache = weakref.WeakValueDictionary()
|
||||
_artwork_cache = {}
|
||||
_max_artwork_cache_size = 10 # Limit artwork cache to prevent memory bloat
|
||||
|
||||
|
||||
def get_shared_mpris_manager():
|
||||
@@ -38,16 +45,81 @@ def get_shared_mpris_manager():
|
||||
return _shared_mpris_manager
|
||||
|
||||
|
||||
def cleanup_artwork_cache():
|
||||
"""Clean up artwork cache to prevent memory leaks."""
|
||||
global _artwork_cache
|
||||
if len(_artwork_cache) > _max_artwork_cache_size:
|
||||
# Keep only the most recent items
|
||||
items = list(_artwork_cache.items())
|
||||
_artwork_cache = dict(items[-_max_artwork_cache_size:])
|
||||
# Force garbage collection
|
||||
gc.collect()
|
||||
|
||||
|
||||
def cleanup_old_cache_files():
|
||||
"""Clean up old artwork cache files (older than 6 hours for more aggressive cleanup)."""
|
||||
"""Clean up old artwork cache files aggressively to save memory."""
|
||||
try:
|
||||
if not os.path.exists(CACHE_DIR):
|
||||
return
|
||||
|
||||
import time
|
||||
|
||||
current_time = time.time()
|
||||
six_hours_ago = current_time - (6 * 60 * 60) # 6 hours instead of 24
|
||||
# Clean files older than 2 hours (more aggressive)
|
||||
two_hours_ago = current_time - (2 * 60 * 60)
|
||||
|
||||
for filename in os.listdir(CACHE_DIR):
|
||||
filepath = os.path.join(CACHE_DIR, filename)
|
||||
try:
|
||||
if os.path.isfile(filepath):
|
||||
file_mod_time = os.path.getmtime(filepath)
|
||||
if file_mod_time < two_hours_ago:
|
||||
os.remove(filepath)
|
||||
logger.debug(f"Cleaned up old cache file: {filename}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to clean up cache file {filename}: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during cache cleanup: {e}")
|
||||
|
||||
|
||||
def get_artwork_cached(url: str) -> Optional[str]:
|
||||
"""Get artwork with memory-efficient caching."""
|
||||
if not url:
|
||||
return None
|
||||
|
||||
# Check memory cache first
|
||||
if url in _artwork_cache:
|
||||
return _artwork_cache[url]
|
||||
|
||||
# Clean cache if too large
|
||||
cleanup_artwork_cache()
|
||||
|
||||
try:
|
||||
# Create cache directory if it doesn't exist
|
||||
os.makedirs(CACHE_DIR, exist_ok=True)
|
||||
|
||||
# Generate cache filename
|
||||
safe_filename = re.sub(r'[^\w\-_.]', '_', urllib.parse.urlparse(url).path.split('/')[-1])
|
||||
if not safe_filename:
|
||||
safe_filename = f"artwork_{hash(url)}"
|
||||
|
||||
cache_file = os.path.join(CACHE_DIR, safe_filename)
|
||||
|
||||
# Check if cached file exists and is recent (within 2 hours)
|
||||
if os.path.exists(cache_file):
|
||||
import time
|
||||
if time.time() - os.path.getmtime(cache_file) < 7200: # 2 hours
|
||||
_artwork_cache[url] = cache_file
|
||||
return cache_file
|
||||
|
||||
# Download and cache
|
||||
urllib.request.urlretrieve(url, cache_file)
|
||||
_artwork_cache[url] = cache_file
|
||||
return cache_file
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cache artwork from {url}: {e}")
|
||||
return None
|
||||
|
||||
for filename in os.listdir(CACHE_DIR):
|
||||
filepath = os.path.join(CACHE_DIR, filename)
|
||||
@@ -87,9 +159,11 @@ class EmbeddedExpandedPlayer(Box):
|
||||
self.player_content = PlayerBoxStack(self.mpris_manager)
|
||||
|
||||
# Add escape key binding for navigation back
|
||||
self._keybinding_added = False
|
||||
try:
|
||||
if hasattr(self.control_center, "add_keybinding"):
|
||||
self.control_center.add_keybinding("Escape", self._on_back_clicked)
|
||||
self._keybinding_added = True
|
||||
except Exception:
|
||||
pass # Ignore if keybinding fails
|
||||
|
||||
@@ -122,34 +196,78 @@ class EmbeddedExpandedPlayer(Box):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
"""Clean up resources"""
|
||||
if hasattr(self.player_content, "destroy"):
|
||||
self.player_content.destroy()
|
||||
super().destroy()
|
||||
"""Clean up resources and prevent memory leaks"""
|
||||
logger.debug("🗑️ EmbeddedExpandedPlayer cleanup starting")
|
||||
|
||||
try:
|
||||
# Destroy player content (PlayerBoxStack)
|
||||
if hasattr(self, 'player_content') and hasattr(self.player_content, "destroy"):
|
||||
self.player_content.destroy()
|
||||
|
||||
# Clean up back button
|
||||
if hasattr(self, 'back_button') and hasattr(self.back_button, "destroy"):
|
||||
self.back_button.destroy()
|
||||
|
||||
# Clean up any other widgets we might have
|
||||
for child in list(self.get_children()):
|
||||
try:
|
||||
child.destroy()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to destroy child widget: {e}")
|
||||
|
||||
# Clean up keybinding if it was added
|
||||
if hasattr(self, '_keybinding_added') and self._keybinding_added:
|
||||
try:
|
||||
if hasattr(self.control_center, "remove_keybinding"):
|
||||
self.control_center.remove_keybinding("Escape")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to remove keybinding: {e}")
|
||||
|
||||
# Aggressively clean global caches
|
||||
global _widget_cache, _artwork_cache
|
||||
_widget_cache.clear()
|
||||
_artwork_cache.clear()
|
||||
logger.debug("🗑️ Cleared global widget and artwork caches")
|
||||
|
||||
# Clear references
|
||||
if hasattr(self, 'control_center'):
|
||||
self.control_center = None
|
||||
if hasattr(self, 'mpris_manager'):
|
||||
self.mpris_manager = None
|
||||
|
||||
# Force garbage collection
|
||||
gc.collect()
|
||||
logger.debug("🗑️ EmbeddedExpandedPlayer cleanup completed")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during EmbeddedExpandedPlayer cleanup: {e}")
|
||||
finally:
|
||||
super().destroy()
|
||||
|
||||
|
||||
class PlayerBoxStack(Box):
|
||||
"""A widget that displays the current player information."""
|
||||
"""Memory-optimized widget that displays current player information."""
|
||||
|
||||
def __init__(self, mpris_manager: MprisPlayerManager, **kwargs):
|
||||
# Clean up old cache files on startup
|
||||
cleanup_old_cache_files()
|
||||
|
||||
# The player stack
|
||||
# The player stack with memory-efficient settings
|
||||
self.player_stack = Stack(
|
||||
# transition_type="slide-left-right",
|
||||
# transition_duration=500,
|
||||
name="player-stack",
|
||||
# Disable transitions to reduce memory usage
|
||||
transition_type="none",
|
||||
)
|
||||
self.current_stack_pos = 0
|
||||
|
||||
# List to store player buttons
|
||||
# Store player buttons - cleaned up via explicit removal
|
||||
self.player_buttons: list[Button] = []
|
||||
self._player_widgets: Dict[str, Box] = weakref.WeakValueDictionary()
|
||||
|
||||
# Track signal connections for cleanup
|
||||
# Track signal connections for cleanup using weak references
|
||||
self._signal_connections = []
|
||||
|
||||
# Create a "No media playing" placeholder
|
||||
# Create a lightweight "No media playing" placeholder
|
||||
self.no_media_box = self._create_no_media_box()
|
||||
|
||||
super().__init__(orientation="v", name="media", children=[self.player_stack])
|
||||
@@ -160,7 +278,7 @@ class PlayerBoxStack(Box):
|
||||
|
||||
self.mpris_manager = mpris_manager
|
||||
|
||||
# Track connections for cleanup - store (object, handler_id) tuples
|
||||
# Track connections for cleanup
|
||||
connections = bulk_connect(
|
||||
self.mpris_manager,
|
||||
{
|
||||
@@ -168,43 +286,92 @@ class PlayerBoxStack(Box):
|
||||
"player-vanished": self.on_lost_player,
|
||||
},
|
||||
)
|
||||
# Store as (object, handler_id) tuples
|
||||
for handler_id in connections:
|
||||
self._signal_connections.append((self.mpris_manager, handler_id))
|
||||
|
||||
# Process existing players
|
||||
for player in self.mpris_manager.players: # type: ignore
|
||||
logger.info(
|
||||
f"[PLAYER MANAGER] player found: {player.get_property('player-name')}",
|
||||
)
|
||||
logger.info(f"[PLAYER MANAGER] player found: {player.get_property('player-name')}")
|
||||
self.on_new_player(self.mpris_manager, player)
|
||||
|
||||
# Schedule periodic memory cleanup and store source ID
|
||||
self._cleanup_source_id = GLib.timeout_add_seconds(300, self._periodic_cleanup) # Every 5 minutes
|
||||
|
||||
def _periodic_cleanup(self):
|
||||
"""Periodic cleanup to prevent memory leaks."""
|
||||
try:
|
||||
# Clean artwork cache
|
||||
cleanup_artwork_cache()
|
||||
|
||||
# Remove destroyed widgets from tracking
|
||||
destroyed_keys = []
|
||||
for key, widget in self._player_widgets.items():
|
||||
if not widget or not widget.get_parent():
|
||||
destroyed_keys.append(key)
|
||||
|
||||
for key in destroyed_keys:
|
||||
if key in self._player_widgets:
|
||||
del self._player_widgets[key]
|
||||
|
||||
# Force garbage collection
|
||||
gc.collect()
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error during periodic cleanup: {e}")
|
||||
|
||||
return True # Continue timer
|
||||
|
||||
def destroy(self):
|
||||
"""Clean up resources when the widget is destroyed."""
|
||||
# Disconnect all signal connections
|
||||
for obj, handler_id in self._signal_connections:
|
||||
try:
|
||||
obj.disconnect(handler_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to disconnect signal: {e}")
|
||||
self._signal_connections.clear()
|
||||
|
||||
# Clean up player buttons
|
||||
for button in self.player_buttons:
|
||||
try:
|
||||
button.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
self.player_buttons.clear()
|
||||
|
||||
# Clean up player boxes
|
||||
for child in self.player_stack.get_children():
|
||||
if hasattr(child, "destroy") and child != self.no_media_box:
|
||||
try:
|
||||
# Cancel any pending cleanup timer
|
||||
if hasattr(self, '_cleanup_source_id') and self._cleanup_source_id:
|
||||
try:
|
||||
child.destroy()
|
||||
GLib.source_remove(self._cleanup_source_id)
|
||||
except Exception:
|
||||
pass # Timer may have already been removed
|
||||
|
||||
# Disconnect all signal connections
|
||||
for obj, handler_id in self._signal_connections:
|
||||
try:
|
||||
if obj and hasattr(obj, 'disconnect'):
|
||||
obj.disconnect(handler_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to disconnect signal: {e}")
|
||||
self._signal_connections.clear()
|
||||
|
||||
# Clean up player widgets
|
||||
for widget in list(self._player_widgets.values()):
|
||||
try:
|
||||
if widget and hasattr(widget, "destroy"):
|
||||
widget.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
self._player_widgets.clear()
|
||||
|
||||
# Clean up player buttons explicitly
|
||||
for button in list(self.player_buttons):
|
||||
try:
|
||||
if button and hasattr(button, "destroy"):
|
||||
button.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
super().destroy()
|
||||
# Clean up stack children
|
||||
for child in self.player_stack.get_children():
|
||||
if hasattr(child, "destroy") and child != self.no_media_box:
|
||||
try:
|
||||
child.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Force final garbage collection
|
||||
gc.collect()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during PlayerBoxStack cleanup: {e}")
|
||||
finally:
|
||||
super().destroy()
|
||||
|
||||
def _create_no_media_box(self):
|
||||
"""Create a placeholder box for when no media is playing."""
|
||||
|
||||
+158
-89
@@ -1,40 +1,66 @@
|
||||
from gi.repository import Gdk, GLib
|
||||
# Standard library imports
|
||||
import contextlib
|
||||
import gc
|
||||
import weakref
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
# Fabric imports
|
||||
from fabric.core.service import Service, Signal, Property
|
||||
from fabric.widgets.window import Window
|
||||
from fabric.widgets.overlay import Overlay
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.centerbox import CenterBox
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.scale import Scale
|
||||
from fabric.widgets.revealer import Revealer
|
||||
from fabric.utils import idle_add, get_relative_path, invoke_repeater
|
||||
from fabric.widgets.image import Image
|
||||
|
||||
# Gi imports
|
||||
import gi
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version("GLib", "2.0")
|
||||
from gi.repository import Gtk, GLib, Gdk
|
||||
|
||||
# Local imports
|
||||
from modules.controlcenter.bluetooth import BluetoothConnections
|
||||
from modules.controlcenter.wifi import WifiConnections
|
||||
from modules.controlcenter.player import PlayerBoxStack
|
||||
from modules.controlcenter.per_app_volume import PerAppVolumeControl
|
||||
from modules.controlcenter.expanded_player import EmbeddedExpandedPlayer
|
||||
from services.modus import modus_service
|
||||
from widgets.wayland import WaylandWindow as Window
|
||||
from loguru import logger
|
||||
|
||||
# Memory monitoring
|
||||
from debug_memory import set_memory_baseline, log_memory
|
||||
from fabric.utils import idle_add
|
||||
from fabric.utils.helpers import (
|
||||
get_relative_path,
|
||||
)
|
||||
from fabric.utils.helpers import get_relative_path
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.centerbox import CenterBox
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.scale import Scale
|
||||
from fabric.widgets.svg import Svg
|
||||
from widgets.wayland import WaylandWindow as Window
|
||||
|
||||
# Local imports
|
||||
from modules.controlcenter.bluetooth import BluetoothConnections
|
||||
from modules.controlcenter.wifi import WifiConnections
|
||||
from services.brightness import Brightness
|
||||
from utils.roam import audio_service, modus_service
|
||||
from widgets.wayland import WaylandWindow as Window
|
||||
from modules.controlcenter.player import PlayerBoxStack
|
||||
from modules.controlcenter.per_app_volume import PerAppVolumeControl
|
||||
from modules.controlcenter.expanded_player import EmbeddedExpandedPlayer
|
||||
from modules.controlcenter.expanded_player import EmbeddedExpandedPlayer, get_shared_mpris_manager
|
||||
from services.mpris import MprisPlayerManager
|
||||
from fabric.widgets.svg import Svg
|
||||
from fabric.utils.helpers import get_relative_path
|
||||
|
||||
brightness_service = Brightness.get_initial()
|
||||
|
||||
# Shared MPRIS manager to reduce memory usage
|
||||
_shared_mpris_manager = None
|
||||
|
||||
|
||||
def get_shared_mpris_manager():
|
||||
"""Get shared MPRIS manager instance to reduce memory usage."""
|
||||
global _shared_mpris_manager
|
||||
if _shared_mpris_manager is None:
|
||||
_shared_mpris_manager = MprisPlayerManager()
|
||||
return _shared_mpris_manager
|
||||
# Memory management globals
|
||||
_widget_cache = weakref.WeakValueDictionary()
|
||||
_cleanup_timer_id = None
|
||||
|
||||
|
||||
class ModusControlCenter(Window):
|
||||
@@ -348,65 +374,41 @@ class ModusControlCenter(Window):
|
||||
|
||||
self.children = self.center_box
|
||||
|
||||
# Set memory baseline for monitoring
|
||||
set_memory_baseline("Control Center Initialized")
|
||||
|
||||
# Connect to visibility changes for cleanup
|
||||
self.connect("notify::visible", self._on_visibility_changed)
|
||||
|
||||
# Periodic cleanup timer (every 5 minutes when visible)
|
||||
GLib.timeout_add_seconds(300, self._periodic_cleanup)
|
||||
|
||||
def destroy(self):
|
||||
"""Proper cleanup when control center is destroyed"""
|
||||
# Disconnect all signal connections
|
||||
for connection in self._signal_connections:
|
||||
try:
|
||||
if hasattr(connection, "disconnect"):
|
||||
connection.disconnect()
|
||||
elif hasattr(connection, "handler_disconnect"):
|
||||
# For GObject connections
|
||||
connection.handler_disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
self._signal_connections.clear()
|
||||
|
||||
# Clean up all widgets
|
||||
self._cleanup_when_hidden()
|
||||
|
||||
# Clean up managers
|
||||
if hasattr(self, "wifi_man") and self.wifi_man:
|
||||
try:
|
||||
if hasattr(self.wifi_man, "destroy"):
|
||||
self.wifi_man.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if hasattr(self, "bluetooth_man") and self.bluetooth_man:
|
||||
try:
|
||||
if hasattr(self.bluetooth_man, "destroy"):
|
||||
self.bluetooth_man.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
super().destroy()
|
||||
# Start periodic memory cleanup (every 5 minutes)
|
||||
global _cleanup_timer_id
|
||||
_cleanup_timer_id = GLib.timeout_add_seconds(300, self._periodic_cleanup)
|
||||
|
||||
def _on_visibility_changed(self, widget, param):
|
||||
"""Handle visibility changes for memory management"""
|
||||
if not self.get_visible():
|
||||
logger.debug("Control center hidden - starting cleanup")
|
||||
self._cleanup_when_hidden()
|
||||
|
||||
def _cleanup_when_hidden(self):
|
||||
"""Clean up resources when widget is hidden to reduce memory usage"""
|
||||
# Clean up music widget content if it exists
|
||||
if self._music_widget_content and hasattr(
|
||||
self._music_widget_content, "destroy"
|
||||
):
|
||||
log_memory("🧹 BEFORE cleanup when hidden")
|
||||
logger.debug("Starting hidden cleanup - freeing widget memory")
|
||||
# Clean up music widget content properly
|
||||
if self._music_widget_content:
|
||||
try:
|
||||
# Remove from parent container first
|
||||
current_children = list(self.music_widget.children)
|
||||
if self._music_widget_content in current_children:
|
||||
current_children.remove(self._music_widget_content)
|
||||
self.music_widget.children = current_children
|
||||
# Destroy the content to free memory
|
||||
self._music_widget_content.destroy()
|
||||
|
||||
# Destroy the content to free memory completely
|
||||
if hasattr(self._music_widget_content, "destroy"):
|
||||
self._music_widget_content.destroy()
|
||||
|
||||
logger.debug("Destroyed music widget content to free memory")
|
||||
self._music_widget_content = None
|
||||
self._music_initialized = False
|
||||
except Exception as e:
|
||||
@@ -423,33 +425,43 @@ class ModusControlCenter(Window):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Clean up expanded player widget
|
||||
# Clean up expanded player widget - BUT preserve for reuse
|
||||
if self._expanded_player_widget and hasattr(
|
||||
self._expanded_player_widget, "destroy"
|
||||
):
|
||||
try:
|
||||
self._expanded_player_widget.destroy()
|
||||
self._expanded_player_widget = None
|
||||
self._expanded_player_initialized = False
|
||||
except Exception:
|
||||
pass
|
||||
# DON'T destroy the widget, just clean its internal state
|
||||
if hasattr(self._expanded_player_widget, 'player_content'):
|
||||
# Clean up only the player content, keep the widget shell
|
||||
if hasattr(self._expanded_player_widget.player_content, '_periodic_cleanup'):
|
||||
self._expanded_player_widget.player_content._periodic_cleanup()
|
||||
logger.debug("Cleaned expanded player internal state (preserved for reuse)")
|
||||
# DON'T set to None - keep for reuse!
|
||||
# self._expanded_player_widget = None
|
||||
# self.expanded_player_widgets = None
|
||||
# self.expanded_player_center_box = None
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to clean expanded player state: {e}")
|
||||
|
||||
# Reset widget containers to free memory
|
||||
# Reset widget containers to free memory (except expanded player for reuse)
|
||||
self.bluetooth_widgets = None
|
||||
self.wifi_widgets = None
|
||||
self.per_app_volume_widgets = None
|
||||
self.expanded_player_widgets = None
|
||||
# Keep expanded_player_widgets for reuse!
|
||||
# self.expanded_player_widgets = None
|
||||
|
||||
# Reset center boxes
|
||||
# Reset center boxes (except expanded player for reuse)
|
||||
self.bluetooth_center_box = None
|
||||
self.wifi_center_box = None
|
||||
self.per_app_volume_center_box = None
|
||||
self.expanded_player_center_box = None
|
||||
# Keep expanded_player_center_box for reuse!
|
||||
# self.expanded_player_center_box = None
|
||||
|
||||
# Force garbage collection
|
||||
import gc
|
||||
|
||||
gc.collect()
|
||||
log_memory("🧹 AFTER cleanup when hidden")
|
||||
|
||||
def _periodic_cleanup(self):
|
||||
"""Periodic cleanup to keep memory usage low"""
|
||||
@@ -467,14 +479,22 @@ class ModusControlCenter(Window):
|
||||
def _ensure_music_widget(self):
|
||||
"""Lazy load music widget content"""
|
||||
if not self._music_initialized:
|
||||
log_memory("🎼 BEFORE creating music widget content")
|
||||
logger.debug("Creating new PlayerBoxStack instance")
|
||||
self._music_widget_content = PlayerBoxStack(
|
||||
get_shared_mpris_manager(), control_center=self
|
||||
)
|
||||
log_memory("🎼 AFTER creating PlayerBoxStack")
|
||||
|
||||
# Add to the music widget's children list
|
||||
current_children = list(self.music_widget.children)
|
||||
current_children.append(self._music_widget_content)
|
||||
self.music_widget.children = current_children
|
||||
self._music_initialized = True
|
||||
log_memory("🎼 AFTER adding to music widget container")
|
||||
else:
|
||||
logger.debug("Music widget already initialized")
|
||||
log_memory("♻️ REUSING existing music widget")
|
||||
|
||||
def _ensure_bluetooth_widgets(self):
|
||||
"""Lazy load bluetooth widgets"""
|
||||
@@ -556,10 +576,16 @@ class ModusControlCenter(Window):
|
||||
self.per_app_volume_center_box.set_size_request(300, -1)
|
||||
|
||||
def _ensure_expanded_player_widgets(self):
|
||||
"""Lazy load expanded player widgets"""
|
||||
"""Lazy load expanded player widgets with reuse optimization"""
|
||||
if self.expanded_player_widgets is None:
|
||||
log_memory("🔧 BEFORE creating expanded player widgets")
|
||||
logger.debug("Creating new expanded player widgets")
|
||||
if self._expanded_player_widget is None:
|
||||
logger.debug("Creating new EmbeddedExpandedPlayer instance")
|
||||
self._expanded_player_widget = EmbeddedExpandedPlayer(self)
|
||||
log_memory("🔧 AFTER creating EmbeddedExpandedPlayer")
|
||||
else:
|
||||
logger.debug("Reusing existing EmbeddedExpandedPlayer instance")
|
||||
|
||||
self.expanded_player_widgets = Box(
|
||||
orientation="vertical",
|
||||
@@ -573,6 +599,10 @@ class ModusControlCenter(Window):
|
||||
start_children=[self.expanded_player_widgets]
|
||||
)
|
||||
self.expanded_player_center_box.set_size_request(300, -1)
|
||||
log_memory("🔧 AFTER creating expanded player containers")
|
||||
else:
|
||||
logger.debug("Reusing existing expanded player widgets")
|
||||
log_memory("♻️ REUSING existing expanded player widgets")
|
||||
|
||||
def set_dont_disturb(self, *_):
|
||||
self.focus_mode = not self.focus_mode
|
||||
@@ -669,16 +699,20 @@ class ModusControlCenter(Window):
|
||||
self.has_per_app_volume_open = False
|
||||
|
||||
def open_expanded_player(self, *_):
|
||||
log_memory("🎵 BEFORE opening expanded player")
|
||||
self._ensure_expanded_player_widgets()
|
||||
idle_add(lambda *_: self.set_children(self.expanded_player_center_box))
|
||||
self.has_expanded_player_open = True
|
||||
# Refresh the player when opening
|
||||
if self._expanded_player_widget:
|
||||
self._expanded_player_widget.refresh()
|
||||
log_memory("🎵 AFTER opening expanded player")
|
||||
|
||||
def close_expanded_player(self, *_):
|
||||
log_memory("🔒 BEFORE closing expanded player")
|
||||
idle_add(lambda *_: self.set_children(self.center_box))
|
||||
self.has_expanded_player_open = False
|
||||
log_memory("🔒 AFTER closing expanded player")
|
||||
|
||||
def _set_mousecapture(self, visible: bool):
|
||||
if visible:
|
||||
@@ -776,24 +810,59 @@ class ModusControlCenter(Window):
|
||||
self.set_visible(False)
|
||||
|
||||
def destroy(self):
|
||||
"""Clean up resources when widget is destroyed"""
|
||||
# Disconnect all signal connections
|
||||
for connection in self._signal_connections:
|
||||
try:
|
||||
connection.disconnect()
|
||||
except:
|
||||
pass
|
||||
"""Enhanced cleanup of resources when widget is destroyed."""
|
||||
try:
|
||||
# Cancel any periodic cleanup timers
|
||||
global _cleanup_timer_id
|
||||
if _cleanup_timer_id:
|
||||
GLib.source_remove(_cleanup_timer_id)
|
||||
_cleanup_timer_id = None
|
||||
|
||||
# Clean up heavy components
|
||||
if self._wifi_man:
|
||||
self._wifi_man.destroy()
|
||||
if self._bluetooth_man:
|
||||
self._bluetooth_man.destroy()
|
||||
if self._music_widget_content:
|
||||
self._music_widget_content.destroy()
|
||||
if self._per_app_volume_widget:
|
||||
self._per_app_volume_widget.destroy()
|
||||
if self._expanded_player_widget:
|
||||
self._expanded_player_widget.destroy()
|
||||
# Disconnect all signal connections with better error handling
|
||||
for connection in self._signal_connections:
|
||||
try:
|
||||
if connection and hasattr(connection, 'disconnect'):
|
||||
connection.disconnect()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to disconnect signal: {e}")
|
||||
self._signal_connections.clear()
|
||||
|
||||
super().destroy()
|
||||
# Clean up heavy components with memory optimization
|
||||
components_to_cleanup = [
|
||||
('wifi_man', getattr(self, 'wifi_man', None)),
|
||||
('bluetooth_man', getattr(self, 'bluetooth_man', None)),
|
||||
('_music_widget_content', self._music_widget_content),
|
||||
('_per_app_volume_widget', self._per_app_volume_widget),
|
||||
('_expanded_player_widget', self._expanded_player_widget)
|
||||
]
|
||||
|
||||
for attr_name, component in components_to_cleanup:
|
||||
if component and hasattr(component, 'destroy'):
|
||||
try:
|
||||
component.destroy()
|
||||
setattr(self, attr_name, None)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to destroy {attr_name}: {e}")
|
||||
|
||||
# Clean up widget containers and references
|
||||
widget_containers = [
|
||||
'bluetooth_widgets', 'wifi_widgets', 'per_app_volume_widgets',
|
||||
'expanded_player_widgets', 'bluetooth_center_box', 'wifi_center_box',
|
||||
'per_app_volume_center_box', 'expanded_player_center_box'
|
||||
]
|
||||
|
||||
for container in widget_containers:
|
||||
if hasattr(self, container):
|
||||
setattr(self, container, None)
|
||||
|
||||
# Clear any cached widgets
|
||||
global _widget_cache
|
||||
_widget_cache.clear()
|
||||
|
||||
# Force garbage collection for immediate memory cleanup
|
||||
gc.collect()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during control center destruction: {e}")
|
||||
finally:
|
||||
super().destroy()
|
||||
@@ -132,6 +132,22 @@ class PlayerBoxStack(Box):
|
||||
|
||||
super().destroy()
|
||||
|
||||
def _periodic_cleanup(self):
|
||||
"""Light cleanup for reuse - clean internal state but preserve widget"""
|
||||
try:
|
||||
# Reset to no media state
|
||||
self.player_stack.children = [self.no_media_box]
|
||||
|
||||
# Clear player buttons but don't destroy them
|
||||
self.player_buttons.clear()
|
||||
|
||||
# Reset stack position
|
||||
self.current_stack_pos = 0
|
||||
|
||||
logger.debug("PlayerBoxStack periodic cleanup completed")
|
||||
except Exception as e:
|
||||
logger.warning(f"PlayerBoxStack periodic cleanup failed: {e}")
|
||||
|
||||
def _create_no_media_box(self):
|
||||
"""Create a placeholder box for when no media is playing."""
|
||||
fallback_cover_path = f"{data.HOME_DIR}/.current.wall"
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('Gdk', '3.0')
|
||||
from gi.repository import Gdk, GLib, Gtk
|
||||
|
||||
from fabric.widgets.box import Box
|
||||
|
||||
Reference in new issue
Block a user