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:
sankalp committed 2026-08-01 19:53:15 +05:45
1 parent d8696b9198
commit 456545e6a6
10 files changed
+152 -23

No files matched your search

+33 -5
View File
@@ -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(
+8
View File
@@ -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
+5 -1
View File
@@ -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
View File
@@ -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
+19 -10
View File
@@ -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, *_):
+13
View File
@@ -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()
+1 -1
View File
@@ -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