perf: break animator and popup ref cycles, suspend idle timers
Break self -> animator -> lambda cycles in smooth switch and flat scale by stopping animators and disconnecting value handlers on destroy. Drop mutual OSD window/osd refs and clear the applet active-popup class ref. Suspend the dock animation loop when nothing animates, pause the desktop system info poller while unmapped, replace the OSD watchdog polling with a single shot at expiry, lower the notch visualizer tick rate, and cancel outstanding dock occlusion/rebuild timers on teardown.
This commit is contained in:
10 files changed
+152
-23
No files matched your search
@@ -156,7 +156,9 @@ class FlatScale(Gtk.DrawingArea, Widget):
|
||||
max_value=16.0,
|
||||
tick_widget=self,
|
||||
)
|
||||
self._anim_press.connect("notify::value", lambda *_: self.queue_draw())
|
||||
self._anim_press_handler = self._anim_press.connect(
|
||||
"notify::value", lambda *_: self.queue_draw()
|
||||
)
|
||||
|
||||
# Bubble open animator (0 → 1)
|
||||
self._anim_bubble_open = Animator(
|
||||
@@ -166,7 +168,9 @@ class FlatScale(Gtk.DrawingArea, Widget):
|
||||
max_value=1.0,
|
||||
tick_widget=self,
|
||||
)
|
||||
self._anim_bubble_open.connect("notify::value", lambda *_: self.queue_draw())
|
||||
self._anim_bubble_open_handler = self._anim_bubble_open.connect(
|
||||
"notify::value", lambda *_: self.queue_draw()
|
||||
)
|
||||
|
||||
# Bubble close animator (0 → 1, mapped as 1 - value)
|
||||
self._anim_bubble_close = Animator(
|
||||
@@ -176,7 +180,9 @@ class FlatScale(Gtk.DrawingArea, Widget):
|
||||
max_value=1.0,
|
||||
tick_widget=self,
|
||||
)
|
||||
self._anim_bubble_close.connect("notify::value", lambda *_: self.queue_draw())
|
||||
self._anim_bubble_close_handler = self._anim_bubble_close.connect(
|
||||
"notify::value", lambda *_: self.queue_draw()
|
||||
)
|
||||
|
||||
self._bubble_progress = 0.0 # 0.0 = hidden, 1.0 = fully visible
|
||||
self._bubble_closing = False
|
||||
@@ -459,6 +465,27 @@ class FlatScale(Gtk.DrawingArea, Widget):
|
||||
|
||||
cr.restore()
|
||||
|
||||
def destroy(self):
|
||||
"""Stop all animators and break the self <-> animator reference cycles."""
|
||||
animators = [
|
||||
(self._anim_press, self._anim_press_handler),
|
||||
(self._anim_bubble_open, self._anim_bubble_open_handler),
|
||||
(self._anim_bubble_close, self._anim_bubble_close_handler),
|
||||
]
|
||||
for animator, handler_id in animators:
|
||||
try:
|
||||
animator.stop()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
animator.disconnect(handler_id)
|
||||
except Exception:
|
||||
pass
|
||||
self._anim_press = None
|
||||
self._anim_bubble_open = None
|
||||
self._anim_bubble_close = None
|
||||
super().destroy()
|
||||
|
||||
def do_draw(self, cr: cairo.Context) -> bool:
|
||||
styles = self.do_resolve_style()
|
||||
|
||||
|
||||
@@ -56,7 +56,9 @@ class SmoothSwitch(Gtk.DrawingArea):
|
||||
if active:
|
||||
self._animator.value = 1.0
|
||||
|
||||
self._animator.connect("notify::value", lambda *_: self.queue_draw())
|
||||
self._anim_notify_handler = self._animator.connect(
|
||||
"notify::value", lambda *_: self.queue_draw()
|
||||
)
|
||||
|
||||
self.add_events(
|
||||
Gdk.EventMask.BUTTON_PRESS_MASK
|
||||
@@ -95,6 +97,22 @@ class SmoothSwitch(Gtk.DrawingArea):
|
||||
self._animator.value = self._animator.min_value
|
||||
self._animator.play()
|
||||
|
||||
def destroy(self):
|
||||
"""Stop the animator and break the self <-> animator reference cycle."""
|
||||
if getattr(self, "_animator", None) is not None:
|
||||
try:
|
||||
self._animator.stop()
|
||||
except Exception:
|
||||
pass
|
||||
if getattr(self, "_anim_notify_handler", None) is not None:
|
||||
try:
|
||||
self._animator.disconnect(self._anim_notify_handler)
|
||||
except Exception:
|
||||
pass
|
||||
self._anim_notify_handler = None
|
||||
self._animator = None
|
||||
super().destroy()
|
||||
|
||||
def _on_draw(self, _, cr: cairo.Context):
|
||||
w, h = self._width, self._height
|
||||
r = h / 2
|
||||
|
||||
@@ -114,6 +114,11 @@ class AppletWindow(PopupWindow):
|
||||
self.close_applet()
|
||||
|
||||
def destroy(self):
|
||||
if AppletWindow._active_popup == self:
|
||||
AppletWindow._active_popup = None
|
||||
parent = getattr(self, "_parent_dropdown", None)
|
||||
if parent is not None and getattr(parent, "_child_dropdown", None) is self:
|
||||
parent._child_dropdown = None
|
||||
if hasattr(self, "dismiss_layer"):
|
||||
self.dismiss_layer.destroy()
|
||||
if self._hide_timeout_id is not None:
|
||||
|
||||
@@ -37,6 +37,9 @@ class SystemInfoBase(Box):
|
||||
all_visible=True,
|
||||
**kwargs,
|
||||
)
|
||||
self._system_timer_id = None
|
||||
self.connect("map", self._on_map)
|
||||
self.connect("unmap", self._on_unmap)
|
||||
self.progress = self.create_progress_bar(name="progress")
|
||||
self.main_label = Label(
|
||||
label="0%\nLoading", justification="center", name="progress-label"
|
||||
@@ -65,14 +68,39 @@ class SystemInfoBase(Box):
|
||||
)
|
||||
|
||||
def start_updates(self):
|
||||
self.update()
|
||||
self._system_timer_id = invoke_repeater(SYSTEM_UPDATE_INTERVAL, self.update)
|
||||
self._start_update_timer()
|
||||
|
||||
def _start_update_timer(self):
|
||||
self._stop_update_timer()
|
||||
if not self.get_mapped():
|
||||
return
|
||||
self._system_timer_id = invoke_repeater(
|
||||
SYSTEM_UPDATE_INTERVAL, self._update_tick
|
||||
)
|
||||
|
||||
def _stop_update_timer(self):
|
||||
if getattr(self, "_system_timer_id", None):
|
||||
try:
|
||||
GLib.source_remove(self._system_timer_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Error removing system info timer: {e}")
|
||||
self._system_timer_id = None
|
||||
|
||||
def _update_tick(self) -> bool:
|
||||
if not self.get_mapped():
|
||||
self._system_timer_id = None
|
||||
return False
|
||||
return self.update()
|
||||
|
||||
def _on_map(self, *_):
|
||||
self._start_update_timer()
|
||||
|
||||
def _on_unmap(self, *_):
|
||||
self._stop_update_timer()
|
||||
|
||||
def destroy(self):
|
||||
"""Cleanup system info update timer"""
|
||||
if hasattr(self, "_system_timer_id") and self._system_timer_id:
|
||||
GLib.source_remove(self._system_timer_id)
|
||||
self._system_timer_id = None
|
||||
self._stop_update_timer()
|
||||
super().destroy()
|
||||
|
||||
def create_info_line(
|
||||
|
||||
@@ -64,4 +64,12 @@ class DockAnimator:
|
||||
canvas._needs_redraw = False
|
||||
canvas.queue_draw()
|
||||
|
||||
# Self-pause: stop ticking once nothing is animating and no redraw is
|
||||
# pending, so an idle dock consumes zero CPU. Restarted on mouse
|
||||
# motion/enter, size changes or model rebuilds.
|
||||
if not dirty and not canvas._needs_redraw:
|
||||
self._running = False
|
||||
self._timer_id = None
|
||||
return False
|
||||
|
||||
return self._running
|
||||
@@ -163,6 +163,9 @@ class DockCanvas(Gtk.DrawingArea):
|
||||
|
||||
def _on_size_allocate(self, widget, allocation) -> None:
|
||||
self._needs_redraw = True
|
||||
animator = getattr(self, "animator", None)
|
||||
if animator is not None:
|
||||
animator.start()
|
||||
|
||||
def do_get_preferred_height(self):
|
||||
h = self._canvas_height()
|
||||
@@ -327,6 +330,7 @@ class DockCanvas(Gtk.DrawingArea):
|
||||
|
||||
self.model.items = new_items
|
||||
self._needs_redraw = True
|
||||
self.animator.start()
|
||||
|
||||
def _update_trash_item(
|
||||
self,
|
||||
@@ -459,7 +463,7 @@ class DockCanvas(Gtk.DrawingArea):
|
||||
def debounced_update_dock_apps(self) -> None:
|
||||
if self._dock_update_timer:
|
||||
GLib.source_remove(self._dock_update_timer)
|
||||
self._dock_update_timer = GLib.timeout_add(50, self._do_update)
|
||||
self._dock_update_timer = GLib.timeout_add(100, self._do_update)
|
||||
|
||||
def _do_update(self) -> bool:
|
||||
self._dock_update_timer = None
|
||||
|
||||
+19
-2
@@ -57,6 +57,7 @@ class Dock(Window):
|
||||
self.is_hovered = False
|
||||
self.hide_ticket = 0
|
||||
self._occlusion_timer_id = None
|
||||
self._occlusion_check_pending = False
|
||||
self._hyprland_handlers = []
|
||||
self._destroyed = False
|
||||
|
||||
@@ -87,14 +88,15 @@ class Dock(Window):
|
||||
|
||||
def _check_occlusion_deferred(self) -> None:
|
||||
"""Debounce occlusion check to avoid rapid successive checks."""
|
||||
if hasattr(self, "_occlusion_check_pending") and self._occlusion_check_pending:
|
||||
if self._occlusion_check_pending:
|
||||
return
|
||||
self._occlusion_check_pending = True
|
||||
GLib.timeout_add(50, self._run_occlusion_check)
|
||||
self._occlusion_timer_id = GLib.timeout_add(50, self._run_occlusion_check)
|
||||
|
||||
def _run_occlusion_check(self) -> bool:
|
||||
"""Run the actual occlusion check."""
|
||||
self._occlusion_check_pending = False
|
||||
self._occlusion_timer_id = None
|
||||
if self._destroyed:
|
||||
return False
|
||||
try:
|
||||
@@ -138,7 +140,9 @@ class Dock(Window):
|
||||
if config().get("dock.enabled", True):
|
||||
self.show()
|
||||
self.revealer.set_reveal_child(True)
|
||||
self.canvas.animator.start()
|
||||
else:
|
||||
self.canvas.animator.stop()
|
||||
self.hide()
|
||||
|
||||
def on_hover_enter(self) -> None:
|
||||
@@ -165,6 +169,7 @@ class Dock(Window):
|
||||
|
||||
def _disconnect_occlusion_signals(self) -> None:
|
||||
"""Disconnect all Hyprland occlusion signal handlers."""
|
||||
self._cancel_occlusion_timer()
|
||||
hyprland = get_modus_service()._hyprland_connection
|
||||
for handler_id in self._hyprland_handlers:
|
||||
try:
|
||||
@@ -173,8 +178,20 @@ class Dock(Window):
|
||||
logger.warning(f"[Dock] Error disconnecting occlusion handler: {e}")
|
||||
self._hyprland_handlers.clear()
|
||||
|
||||
def _cancel_occlusion_timer(self) -> None:
|
||||
if self._occlusion_timer_id is not None:
|
||||
try:
|
||||
GLib.source_remove(self._occlusion_timer_id)
|
||||
except Exception as e:
|
||||
logger.debug(
|
||||
f"[Dock] Failed to remove occlusion timer {self._occlusion_timer_id}: {e}"
|
||||
)
|
||||
self._occlusion_timer_id = None
|
||||
self._occlusion_check_pending = False
|
||||
|
||||
def destroy(self) -> None:
|
||||
self._destroyed = True
|
||||
self._cancel_occlusion_timer()
|
||||
self._disconnect_occlusion_signals()
|
||||
from services.config import off_config_change
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ class BaseOSDContainer(Box):
|
||||
def _on_leave_notify(self, widget, event):
|
||||
if event.detail != Gdk.NotifyType.INFERIOR:
|
||||
self._is_hovered = False
|
||||
if self.watchdog_handler and self.window.get_visible():
|
||||
self._schedule_watchdog()
|
||||
|
||||
def is_hovered(self):
|
||||
return self._is_hovered
|
||||
@@ -92,20 +94,27 @@ class BaseOSDContainer(Box):
|
||||
self.window.hide()
|
||||
return False
|
||||
|
||||
def _schedule_watchdog(self) -> None:
|
||||
"""Arm a single-shot watchdog that hides the OSD at MAX_VISIBLE_TIME."""
|
||||
self.remove_watchdog_handler()
|
||||
remaining = self.show_timestamp + self.MAX_VISIBLE_TIME - time.time()
|
||||
delay_ms = max(50, int(remaining * 1000))
|
||||
self.watchdog_handler = GLib.timeout_add(delay_ms, self.watchdog_force_hide)
|
||||
|
||||
def watchdog_force_hide(self, *_):
|
||||
self.watchdog_handler = 0
|
||||
if not self.window.get_visible():
|
||||
self.cleanup_all_handlers()
|
||||
return False
|
||||
|
||||
elapsed = time.time() - self.show_timestamp
|
||||
if elapsed >= self.MAX_VISIBLE_TIME:
|
||||
if not self.is_hovered():
|
||||
self.hide_window()
|
||||
self.cleanup_all_handlers()
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return True
|
||||
if self.is_hovered():
|
||||
# Keep the OSD visible while hovered; re-check periodically.
|
||||
self.watchdog_handler = GLib.timeout_add(500, self.watchdog_force_hide)
|
||||
return False
|
||||
|
||||
self.hide_window()
|
||||
self.cleanup_all_handlers()
|
||||
return False
|
||||
|
||||
def update(self, *_):
|
||||
if self._update_in_progress:
|
||||
@@ -130,7 +139,7 @@ class BaseOSDContainer(Box):
|
||||
|
||||
self.focus()
|
||||
self.last_handler = invoke_repeater(1700, self.unfocus, initial_call=False)
|
||||
self.watchdog_handler = GLib.timeout_add(1000, self.watchdog_force_hide)
|
||||
self._schedule_watchdog()
|
||||
self._update_in_progress = False
|
||||
|
||||
def focus(self, *_):
|
||||
|
||||
@@ -51,6 +51,11 @@ class OSD(Box):
|
||||
return
|
||||
self.show_container(self.containers.get("brightness"))
|
||||
|
||||
def destroy(self):
|
||||
"""Break the OSD <-> window reference cycle before teardown."""
|
||||
self.window = None
|
||||
super().destroy()
|
||||
|
||||
|
||||
class OSDWindow(Window):
|
||||
def __init__(self, **kwargs):
|
||||
@@ -81,3 +86,11 @@ class OSDWindow(Window):
|
||||
if not get_config("panel.osd", True):
|
||||
return
|
||||
self.osd.containers["brightness"].update()
|
||||
|
||||
def destroy(self):
|
||||
"""Break the OSD <-> window reference cycle before teardown."""
|
||||
osd = getattr(self, "osd", None)
|
||||
self.osd = None
|
||||
if osd is not None:
|
||||
osd.window = None
|
||||
super().destroy()
|
||||
@@ -12,7 +12,7 @@ from utils.gtk_utils import svg_file
|
||||
|
||||
# tunables
|
||||
_BARS = 8 # number of bars
|
||||
_TICK_MS = 150 # ~7 fps — smooth with sine, very cheap
|
||||
_TICK_MS = 300 # ~3 fps — wall-clock sine curve, cheaper sampling
|
||||
_H_MIN = 2 # minimum total bar height (px, both halves)
|
||||
_H_MAX = 16 # maximum total bar height (px)
|
||||
_BAR_W = 1 # bar pixel width
|
||||
|
||||
Reference in new issue
Block a user