Fix Memory leak(maybe)

This commit is contained in:
tr1x_em committed 2025-08-06 18:44:43 +05:30
1 parent 6de682784e
commit 7d169ce1ef
4 files changed
+180 -54

No files matched your search

+60 -31
View File
@@ -12,6 +12,7 @@ import urllib.parse
import urllib.request
from typing import List
import threading
import weakref # For memory-efficient references
from fabric.utils import bulk_connect, invoke_repeater, cooldown
from fabric.widgets.image import Image
@@ -24,9 +25,19 @@ import config.data as data
CACHE_DIR = f"{data.CACHE_DIR}/media"
# 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
def cleanup_old_cache_files():
"""Clean up old artwork cache files (older than 1 day)."""
"""Clean up old artwork cache files (older than 6 hours for more aggressive cleanup)."""
try:
if not os.path.exists(CACHE_DIR):
return
@@ -34,14 +45,14 @@ def cleanup_old_cache_files():
import time
current_time = time.time()
one_day_ago = current_time - (24 * 60 * 60) # 24 hours
six_hours_ago = current_time - (6 * 60 * 60) # 6 hours instead of 24
for filename in os.listdir(CACHE_DIR):
filepath = os.path.join(CACHE_DIR, filename)
try:
if os.path.isfile(filepath):
file_mtime = os.path.getmtime(filepath)
if file_mtime < one_day_ago:
if file_mtime < six_hours_ago:
os.unlink(filepath)
except Exception:
pass # Ignore individual file errors
@@ -61,7 +72,7 @@ class EmbeddedExpandedPlayer(Box):
)
self.control_center = control_center
self.mpris_manager = MprisPlayerManager()
self.mpris_manager = get_shared_mpris_manager()
# Create back button
self.back_button = Button(
@@ -197,10 +208,10 @@ class PlayerBoxStack(Box):
from gi.repository import GdkPixbuf
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
fallback_cover_path, 80, 80, True
fallback_cover_path, 60, 60, True
)
album_cover_image.set_from_pixbuf(pixbuf)
album_cover_image.set_size_request(80, 80)
album_cover_image.set_size_request(60, 60)
except Exception as e:
logger.warning(f"Failed to load fallback image in no-media box: {e}")
@@ -208,7 +219,7 @@ class PlayerBoxStack(Box):
name="macos-album-image",
children=[album_cover_image],
)
album_cover.set_size_request(80, 80)
album_cover.set_size_request(60, 60)
image_stack = Box(h_align="start", v_align="center", name="player-image-stack")
image_stack.children = [album_cover]
@@ -218,7 +229,7 @@ class PlayerBoxStack(Box):
label="No media playing",
name="player-title",
justification="left",
max_chars_width=15,
max_chars_width=12,
ellipsization="end",
h_align="start",
)
@@ -227,7 +238,7 @@ class PlayerBoxStack(Box):
label="",
name="player-artist",
justification="left",
max_chars_width=15,
max_chars_width=12,
ellipsization="end",
h_align="start",
visible=False, # Hide artist and album when no media
@@ -237,7 +248,7 @@ class PlayerBoxStack(Box):
label="",
name="player-album",
justification="left",
max_chars_width=15,
max_chars_width=12,
ellipsization="end",
h_align="start",
visible=False, # Hide artist and album when no media
@@ -483,7 +494,7 @@ class PlayerBox(Box):
self.player_stack = player_stack
self.fallback_cover_path = f"{data.HOME_DIR}/.current.wall"
self.image_size = 80 # Large album art for macOS style
self.image_size = 60 # Smaller album art for compact layout
self.icon_size = 15
# State
@@ -512,7 +523,7 @@ class PlayerBox(Box):
label="No Title",
name="macos-player-title",
justification="left",
max_chars_width=30,
max_chars_width=25,
ellipsization="end",
h_align="start",
h_expand=True,
@@ -522,7 +533,7 @@ class PlayerBox(Box):
label="No Artist",
name="macos-player-artist",
justification="left",
max_chars_width=30,
max_chars_width=25,
ellipsization="end",
h_align="start",
h_expand=True,
@@ -540,10 +551,10 @@ class PlayerBox(Box):
from gi.repository import GdkPixbuf
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
self.fallback_cover_path, 80, 80, True
self.fallback_cover_path, 60, 60, True
)
self.album_cover_image.set_from_pixbuf(pixbuf)
self.album_cover_image.set_size_request(80, 80)
self.album_cover_image.set_size_request(60, 60)
except Exception as e:
logger.warning(f"Failed to load fallback image: {e}")
@@ -552,7 +563,7 @@ class PlayerBox(Box):
name="macos-album-image",
children=[self.album_cover_image],
)
self.album_cover.set_size_request(80, 80)
self.album_cover.set_size_request(60, 60)
self.image_stack = Box(
h_align="start",
@@ -621,7 +632,7 @@ class PlayerBox(Box):
name="macos-button-box",
h_align="center",
h_expand=True,
spacing=15, # Wider spacing for macOS look
spacing=10, # Reduced spacing for macOS look
)
# Define controls_box BEFORE using it in track_info
@@ -629,7 +640,7 @@ class PlayerBox(Box):
name="macos-player-controls",
orientation="v",
h_expand=True,
spacing=8,
spacing=6,
h_align="center",
children=[self.button_box],
)
@@ -637,7 +648,7 @@ class PlayerBox(Box):
# Track info with inline controls - expands to fill available space
self.track_info = Box(
name="macos-track-info",
spacing=6, # Reduced spacing
spacing=4, # Reduced spacing
orientation="v",
v_align="start",
h_align="fill", # Fill all available horizontal space
@@ -718,7 +729,7 @@ class PlayerBox(Box):
self.outer_box = Box(
name="macos-outer-player-box",
orientation="horizontal",
spacing=15, # Good spacing between elements
spacing=10, # Reduced spacing between elements
h_expand=True,
v_expand=True,
v_align="center",
@@ -746,8 +757,15 @@ class PlayerBox(Box):
def destroy(self):
"""Clean up all resources when the widget is destroyed."""
# Cancel any ongoing downloads
# Cancel any ongoing downloads immediately
self._download_cancelled = True
# Wait for download thread to finish (with timeout)
if self.current_download_thread and self.current_download_thread.is_alive():
try:
self.current_download_thread.join(timeout=1.0) # 1 second timeout
except Exception:
pass
# Disconnect all signal connections
for connection in self._signal_connections:
@@ -757,8 +775,15 @@ class PlayerBox(Box):
logger.warning(f"Failed to disconnect signal: {e}")
self._signal_connections.clear()
# Clean up temp files
# Clean up temp files aggressively
self._cleanup_temp_files()
# Clear image references
if hasattr(self, 'album_cover_image'):
try:
self.album_cover_image.set_from_pixbuf(None)
except Exception:
pass
super().destroy()
@@ -892,21 +917,25 @@ class PlayerBox(Box):
if image_path and os.path.isfile(image_path):
# Create scaled pixbuf to ensure proper size
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
image_path, 80, 80, True
image_path, 60, 60, True
)
self.album_cover_image.set_from_pixbuf(pixbuf)
# Explicitly clear pixbuf reference to help GC
del pixbuf
else:
# Use fallback image with scaling
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
self.fallback_cover_path, 80, 80, True
self.fallback_cover_path, 60, 60, True
)
self.album_cover_image.set_from_pixbuf(pixbuf)
# Explicitly clear pixbuf reference to help GC
del pixbuf
except Exception as e:
logger.warning(f"Failed to update album cover image: {e}")
# Try simple file setting as last resort
try:
self.album_cover_image.set_from_file(self.fallback_cover_path)
self.album_cover_image.set_size_request(80, 80)
self.album_cover_image.set_size_request(60, 60)
except Exception as e2:
logger.error(f"Failed to set fallback image: {e2}")
@@ -954,16 +983,16 @@ class PlayerBox(Box):
if self._download_cancelled:
return
# Clean up old temp files first (keep only last 2 to avoid excessive cleanup)
if len(self.temp_artwork_files) > 2:
old_files = self.temp_artwork_files[:-2]
# Clean up old temp files first (keep only last 1 to reduce memory)
if len(self.temp_artwork_files) > 1:
old_files = self.temp_artwork_files[:-1]
for old_file in old_files:
try:
if os.path.exists(old_file):
os.unlink(old_file)
except Exception:
pass
self.temp_artwork_files = self.temp_artwork_files[-2:]
self.temp_artwork_files = self.temp_artwork_files[-1:]
# Check again if cancelled
if self._download_cancelled:
@@ -1066,7 +1095,7 @@ class Thing(Box):
def __init__(self, **kwargs):
super().__init__(
name="thing",
size=(600, 200),
size=(480, 160),
orientation="vertical",
spacing=0,
children=[
@@ -1088,7 +1117,7 @@ class ExpandedPlayer(Window):
anchor="top right",
layer="top",
exclusivity="auto",
child=PlayerBoxStack(MprisPlayerManager()),
child=PlayerBoxStack(get_shared_mpris_manager()),
visible=False,
)
self.add_keybinding("Escape", self.set_child_visible(False))
+108 -11
View File
@@ -1,4 +1,5 @@
from gi.repository import Gdk, GLib
from loguru import logger
from fabric.utils import idle_add
from fabric.utils.helpers import (
@@ -22,6 +23,16 @@ from services.mpris import MprisPlayerManager
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
# FIX: Icon not showing up in control center
# TODO: Add Player
@@ -325,6 +336,43 @@ class ModusControlCenter(Window):
# 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()
def _on_visibility_changed(self, widget, param):
"""Handle visibility changes for memory management"""
@@ -332,23 +380,72 @@ class ModusControlCenter(Window):
self._cleanup_when_hidden()
def _cleanup_when_hidden(self):
"""Clean up resources when widget is hidden"""
"""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 not self._music_initialized:
# Remove from the parent container
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
self._music_widget_content = None
if self._music_widget_content and hasattr(self._music_widget_content, 'destroy'):
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()
self._music_widget_content = None
self._music_initialized = False
except Exception as e:
logger.warning(f"Failed to cleanup music widget content: {e}")
# Reset lazy loading flags to allow re-initialization when shown again
# Keep initialized flags to avoid recreating heavy components unnecessarily
# Clean up per-app volume widget
if self._per_app_volume_widget and hasattr(self._per_app_volume_widget, 'destroy'):
try:
self._per_app_volume_widget.destroy()
self._per_app_volume_widget = None
self._per_app_volume_initialized = False
except Exception:
pass
# Clean up expanded player widget
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
# Reset widget containers to free memory
self.bluetooth_widgets = None
self.wifi_widgets = None
self.per_app_volume_widgets = None
self.expanded_player_widgets = None
# Reset center boxes
self.bluetooth_center_box = None
self.wifi_center_box = None
self.per_app_volume_center_box = None
self.expanded_player_center_box = None
# Force garbage collection
import gc
gc.collect()
def _periodic_cleanup(self):
"""Periodic cleanup to keep memory usage low"""
if not self.get_visible():
# If not visible, do aggressive cleanup
self._cleanup_when_hidden()
return True # Continue timer
# Light cleanup when visible
import gc
gc.collect()
return True # Continue timer
def _ensure_music_widget(self):
"""Lazy load music widget content"""
if not self._music_initialized:
self._music_widget_content = PlayerBoxStack(MprisPlayerManager(), control_center=self)
self._music_widget_content = PlayerBoxStack(get_shared_mpris_manager(), control_center=self)
# Add to the music widget's children list
current_children = list(self.music_widget.children)
current_children.append(self._music_widget_content)
+4 -4
View File
@@ -816,16 +816,16 @@ class PlayerBox(Box):
if self._download_cancelled:
return
# Clean up old temp files first (keep only last 2 to avoid excessive cleanup)
if len(self.temp_artwork_files) > 2:
old_files = self.temp_artwork_files[:-2]
# Clean up old temp files first (keep only last 1 to reduce memory)
if len(self.temp_artwork_files) > 1:
old_files = self.temp_artwork_files[:-1]
for old_file in old_files:
try:
if os.path.exists(old_file):
os.unlink(old_file)
except Exception:
pass
self.temp_artwork_files = self.temp_artwork_files[-2:]
self.temp_artwork_files = self.temp_artwork_files[-1:]
# Check again if cancelled
if self._download_cancelled: