From 86b8ad51ad26647cf92e4da913f55afff11b4875 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sun, 2 Aug 2026 13:37:10 +0545 Subject: [PATCH] perf(dock): cache icon surfaces, coalesce title fetches --- src/window/dock/canvas.py | 162 +++++++++++++++++++++++++++----------- 1 file changed, 114 insertions(+), 48 deletions(-) diff --git a/src/window/dock/canvas.py b/src/window/dock/canvas.py index 33a71c1..f21c1fa 100644 --- a/src/window/dock/canvas.py +++ b/src/window/dock/canvas.py @@ -1,4 +1,5 @@ import math +import time from typing import Dict, List, Optional, Tuple import cairo @@ -8,7 +9,6 @@ from fabric.utils import ( GdkPixbuf, GLib, Gtk, - get_desktop_applications, logger, os, ) @@ -23,7 +23,7 @@ from services.modus import ( launch_app, open_trash, ) -from utils.functions import clear_children, is_special_workspace_id +from utils.functions import clear_children, get_desktop_apps, is_special_workspace_id from utils.gtk_utils import svg_file from utils.icon_resolver import IconResolver @@ -75,6 +75,8 @@ class DockCanvas(Gtk.DrawingArea): self._focused_address: str = "" self._dock_update_timer: Optional[int] = None + self._pending_title_addresses: set = set() + self._title_update_timer: Optional[int] = None self._hyprland_event_handlers: List = [] self._last_requested_width: int = 0 self._last_requested_height: int = 0 @@ -84,6 +86,7 @@ class DockCanvas(Gtk.DrawingArea): self._trash_pixbuf: Optional[GdkPixbuf.Pixbuf] = None self._cached_trash_full: Optional[bool] = None self._cached_trash_size: int = 0 + self._trash_scan_time: float = 0.0 self.pinned_apps: List = self._read_pinned_apps() self.menu = Gtk.Menu() @@ -131,7 +134,7 @@ class DockCanvas(Gtk.DrawingArea): def _get_desktop_apps(self) -> list: if not self._desktop_apps: try: - self._desktop_apps = get_desktop_applications(include_hidden=False) + self._desktop_apps = get_desktop_apps() except Exception as e: logger.warning(f"[dock] Failed to load desktop applications: {e}") self._desktop_apps = [] @@ -188,6 +191,21 @@ class DockCanvas(Gtk.DrawingArea): def _clear_pixbuf_cache(self) -> None: self._pixbuf_cache.clear() + def _get_item_surface(self, item: DockItem): + pixbuf = item.pixbuf + if pixbuf is None: + return None + if item.icon_surface is not None and item.icon_surface_pixbuf is pixbuf: + return item.icon_surface + try: + surface = Gdk.cairo_surface_create_from_pixbuf(pixbuf, 1, None) + except Exception as e: + logger.warning(f"[DockCanvas] Failed to create icon surface: {e}") + return None + item.icon_surface = surface + item.icon_surface_pixbuf = pixbuf + return surface + def _get_app_icon_pixbuf( self, app_data, app=None, size: Optional[int] = None ) -> Optional[GdkPixbuf.Pixbuf]: @@ -212,8 +230,11 @@ class DockCanvas(Gtk.DrawingArea): try: clients = self._get_clients() - focused = self._get_focused_window() - self._focused_address = (focused or {}).get("address", "") + if not self._focused_address: + # Only the first rebuild needs an explicit focused-window query; + # subsequent focus is tracked by the activewindow event handler. + focused = self._get_focused_window() + self._focused_address = (focused or {}).get("address", "") except Exception as e: logger.error(f"[DockCanvas] Error fetching clients: {e}") clients = [] @@ -338,31 +359,40 @@ class DockCanvas(Gtk.DrawingArea): old_scales: Dict[str, float], size: int, ) -> None: - trash_full = self._trash_has_files() + # Cap the trash filesystem scan (exists + listdir) to once per second; + # the dock model may rebuild several times a second during window churn. + now = time.monotonic() if ( self._trash_pixbuf is None - or trash_full != self._cached_trash_full or size != self._cached_trash_size + or now - self._trash_scan_time > 1.0 ): - self._cached_trash_full = trash_full - self._cached_trash_size = size - trash_svg = "trash-full.svg" if trash_full else "trash-empty.svg" - try: - svg_w = svg_file(f"misc/{trash_svg}") - surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, size, size) - cr = cairo.Context(surface) - rect = Rsvg.Rectangle() - rect.x = rect.y = 0 - rect.width = size - rect.height = size - svg_w._handle.set_dpi(160) - svg_w._handle.render_document(cr, rect) - self._trash_pixbuf = Gdk.pixbuf_get_from_surface( - surface, 0, 0, size, size - ) - except Exception as e: - logger.warning(f"[canvas] Failed to render trash SVG: {e}") - self._trash_pixbuf = self._get_pixbuf("user-trash", size) + self._trash_scan_time = now + trash_full = self._trash_has_files() + if ( + self._trash_pixbuf is None + or trash_full != self._cached_trash_full + or size != self._cached_trash_size + ): + self._cached_trash_full = trash_full + self._cached_trash_size = size + trash_svg = "trash-full.svg" if trash_full else "trash-empty.svg" + try: + svg_w = svg_file(f"misc/{trash_svg}") + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, size, size) + cr = cairo.Context(surface) + rect = Rsvg.Rectangle() + rect.x = rect.y = 0 + rect.width = size + rect.height = size + svg_w._handle.set_dpi(160) + svg_w._handle.render_document(cr, rect) + self._trash_pixbuf = Gdk.pixbuf_get_from_surface( + surface, 0, 0, size, size + ) + except Exception as e: + logger.warning(f"[canvas] Failed to render trash SVG: {e}") + self._trash_pixbuf = self._get_pixbuf("user-trash", size) existing = self.model.get_by_id("trash") if existing and existing.is_trash: @@ -439,26 +469,46 @@ class DockCanvas(Gtk.DrawingArea): address = signal.data[0] if not address: return - # Fetch client data BEFORE iterating so the expensive IPC call - # doesn't hold a stale reference to model items across rebuilds. - clients = self._get_clients() - client = next((c for c in clients if c.get("address") == address), None) - if not client: - return - title = client.get("title", "") - # Re-verify the item still exists in the CURRENT model — a - # debounced rebuild may have replaced the list since the signal. - for item in self.model.items: - if item.instance_address == address: - app_class = item.app_class or "" - item.tooltip = ( - f"{app_class}: {title}" if title != app_class else app_class - ) - break + # Coalesce title-change bursts (e.g. terminal typing) into a single + # client fetch instead of one full `j/clients` IPC per event. + self._pending_title_addresses.add(address) + if self._title_update_timer: + GLib.source_remove(self._title_update_timer) + self._title_update_timer = GLib.timeout_add(150, self._apply_pending_titles) except Exception as e: logger.warning(f"[canvas] address = signal.data[0] failed: {e}") + self._needs_redraw = True + self.queue_draw() + + def _apply_pending_titles(self) -> bool: + self._title_update_timer = None + pending = self._pending_title_addresses + self._pending_title_addresses = set() + try: + if not pending: + return False + # Fetch client data once for the whole batch so the expensive IPC + # call doesn't hold a stale reference to model items across rebuilds. + clients = self._get_clients() + for client in clients: + address = client.get("address", "") + if address not in pending: + continue + title = client.get("title", "") + # Re-verify the item still exists in the CURRENT model — a + # debounced rebuild may have replaced the list since the signal. + for item in self.model.items: + if item.instance_address == address: + app_class = item.app_class or "" + item.tooltip = ( + f"{app_class}: {title}" if title != app_class else app_class + ) + break + except Exception as e: + logger.warning(f"[canvas] _apply_pending_titles failed: {e}") self._needs_redraw = True self.queue_draw() + return False def debounced_update_dock_apps(self) -> None: if self._dock_update_timer: @@ -584,16 +634,27 @@ class DockCanvas(Gtk.DrawingArea): return if item.pixbuf: + surface = self._get_item_surface(item) cr.save() - pb_w = item.pixbuf.get_width() - pb_h = item.pixbuf.get_height() - if pb_w > 0 and pb_h > 0: - sx = iw / pb_w - sy = ih / pb_h + sw = surface.get_width() if surface is not None else 0 + sh = surface.get_height() if surface is not None else 0 + if surface is not None and sw > 0 and sh > 0: + sx = iw / sw + sy = ih / sh cr.translate(ix, iy) cr.scale(sx, sy) - Gdk.cairo_set_source_pixbuf(cr, item.pixbuf, 0, 0) + cr.set_source_surface(surface, 0, 0) cr.paint() + else: + pb_w = item.pixbuf.get_width() + pb_h = item.pixbuf.get_height() + if pb_w > 0 and pb_h > 0: + sx = iw / pb_w + sy = ih / pb_h + cr.translate(ix, iy) + cr.scale(sx, sy) + Gdk.cairo_set_source_pixbuf(cr, item.pixbuf, 0, 0) + cr.paint() cr.restore() else: cr.save() @@ -1035,6 +1096,11 @@ class DockCanvas(Gtk.DrawingArea): GLib.source_remove(self._dock_update_timer) self._dock_update_timer = None + if self._title_update_timer: + GLib.source_remove(self._title_update_timer) + self._title_update_timer = None + self._pending_title_addresses.clear() + for handler_id in self._hyprland_event_handlers: try: self._hyprland_connection.disconnect(handler_id)