From 6b1ad31d418db54b7f2c079a50a22b4a09224e4f Mon Sep 17 00:00:00 2001 From: philippe-ynput Date: Wed, 20 May 2026 17:11:16 +0200 Subject: [PATCH 1/5] perf(loader/ui): optimize inspector refresh during drag selection Prevent unnecessary refreshes while mouse is pressed during drag selection in the ReviewInspector. The inspector now only updates when the selection is finalized (on mouse release) rather than during the selection process. This reduces UI flickering and improves performance when selecting multiple items via drag in the review inspector panel. Signed-off-by: philippe-ynput --- .../tools/loader/ui/review_inspector.py | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/loader/ui/review_inspector.py b/client/ayon_core/tools/loader/ui/review_inspector.py index 2160fc390e6..de4029b9d01 100644 --- a/client/ayon_core/tools/loader/ui/review_inspector.py +++ b/client/ayon_core/tools/loader/ui/review_inspector.py @@ -53,6 +53,8 @@ def __init__( self._current_thumb_key: str = "" # Use a dict as an ordered set to track the currently selected indices self._current_selection: dict[QtCore.QModelIndex, None] = {} + # Track if mouse is currently pressed for drag selection + self._mouse_pressed: bool = False self._build() @@ -168,6 +170,8 @@ def set_view(self, view: QtWidgets.QAbstractItemView) -> None: self._on_selection_changed ) self._view.model().modelReset.disconnect(self._on_model_reset) + # Remove event filter from previous view's viewport + self._view.viewport().removeEventFilter(self) except (RuntimeError, TypeError): pass @@ -175,12 +179,36 @@ def set_view(self, view: QtWidgets.QAbstractItemView) -> None: self._view.activated.connect(self._on_activated) self._view.selection_changed.connect(self._on_selection_changed) self._view.model().modelReset.connect(self._on_model_reset) + # Install event filter on the viewport to track mouse press/release + self._view.viewport().installEventFilter(self) def _on_model_reset(self) -> None: """Clear the selection when the model is reset.""" self._current_selection.clear() self._update() + def eventFilter( + self, obj: QtCore.QObject, event: QtCore.QEvent + ) -> bool: + """Track mouse press/release events on the view's viewport. + + Args: + obj: The watched object. + event: The event. + + Returns: + True if the event was handled, False otherwise. + """ + if obj is self._view.viewport(): + if event.type() == QtCore.QEvent.Type.MouseButtonPress: + self._mouse_pressed = True + elif event.type() == QtCore.QEvent.Type.MouseButtonRelease: + self._mouse_pressed = False + # Trigger update on mouse release for finalized selection + if self.isVisible(): + self._update() + return super().eventFilter(obj, event) + def _on_activated(self, index: QtCore.QModelIndex) -> None: """Activation is typically when the user double-clicks on an item. We want to show the inspector in this case if it's not visible. @@ -206,7 +234,10 @@ def _on_selection_changed( for idx in deselected.indexes(): if idx.column() == 0: self._current_selection.pop(idx, None) - self._update() + # Only update immediately if mouse is not pressed (keyboard selection) + # Otherwise, update will be triggered on mouse release + if not self._mouse_pressed: + self._update() def _update(self) -> None: """Update the inspector with the current selection, if visible.""" From 28c6b7fa0dc56d1c3d3eda559af127b0d35451fe Mon Sep 17 00:00:00 2001 From: philippe-ynput Date: Wed, 20 May 2026 17:58:45 +0200 Subject: [PATCH 2/5] refactor(loader/ui): remove unnecessary blank line in ReviewTable class Signed-off-by: philippe-ynput --- client/ayon_core/tools/loader/ui/_review_table.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/loader/ui/_review_table.py b/client/ayon_core/tools/loader/ui/_review_table.py index 1e6d487d319..1149493641b 100644 --- a/client/ayon_core/tools/loader/ui/_review_table.py +++ b/client/ayon_core/tools/loader/ui/_review_table.py @@ -277,7 +277,6 @@ def _on_display_type_changed(self, display_type: str) -> None: if active is self._table: self._eagerly_enqueue_visible_thumbnails() - self.display_type_changed.emit(active) def _on_group_by_options_changed( From c9f5b3f9080276522d294ddc0df008d6d9ff725f Mon Sep 17 00:00:00 2001 From: philippe-ynput Date: Wed, 20 May 2026 18:28:43 +0200 Subject: [PATCH 3/5] refactor(loader/ui): implement dynamic throttling for inspector updates Introduce a QTimer-based throttling mechanism in ReviewInspector to dynamically adjust update frequency during drag selection operations. The implementation measures the base update time during the first selection and scales the throttle interval proportionally to the number of selected items. This provides adaptive performance optimization while maintaining responsiveness during interactive selection operations. Timer is properly managed with start/stop calls during mouse press/release events to ensure clean resource handling. Signed-off-by: philippe-ynput --- .../tools/loader/ui/review_inspector.py | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/review_inspector.py b/client/ayon_core/tools/loader/ui/review_inspector.py index de4029b9d01..370543d1287 100644 --- a/client/ayon_core/tools/loader/ui/review_inspector.py +++ b/client/ayon_core/tools/loader/ui/review_inspector.py @@ -1,5 +1,6 @@ from __future__ import annotations +import time from collections import defaultdict from ayon_ui_qt.components.buttons import AYButton @@ -55,6 +56,14 @@ def __init__( self._current_selection: dict[QtCore.QModelIndex, None] = {} # Track if mouse is currently pressed for drag selection self._mouse_pressed: bool = False + # Timer for throttling updates during drag selection + self._update_timer = QtCore.QTimer() + self._update_timer.setSingleShot(True) + self._update_timer.timeout.connect(self._throttled_update) + # Interval for throttling updates (in milliseconds) + self._base_update_time: int = 0 + # Flag to indicate if the first update has been measured + self._first_update_measured: bool = False self._build() @@ -182,14 +191,29 @@ def set_view(self, view: QtWidgets.QAbstractItemView) -> None: # Install event filter on the viewport to track mouse press/release self._view.viewport().installEventFilter(self) + def _throttled_update(self) -> None: + """Throttled update method called by the timer during drag selection.""" + if self._mouse_pressed: + start_time = time.time() + self._update() + elapsed_time = time.time() - start_time + num_selected = len(self._current_selection) + if not self._first_update_measured and num_selected > 0: + # Calculate the update interval based on the first update time + self._base_update_time = max( + 16, + int((elapsed_time * 1000) / num_selected), + ) + self._first_update_measured = True + print(f"base update time: {self._base_update_time} ms") + self._update_timer.start(self._base_update_time * num_selected) + def _on_model_reset(self) -> None: """Clear the selection when the model is reset.""" self._current_selection.clear() self._update() - def eventFilter( - self, obj: QtCore.QObject, event: QtCore.QEvent - ) -> bool: + def eventFilter(self, obj: QtCore.QObject, event: QtCore.QEvent) -> bool: """Track mouse press/release events on the view's viewport. Args: @@ -202,8 +226,11 @@ def eventFilter( if obj is self._view.viewport(): if event.type() == QtCore.QEvent.Type.MouseButtonPress: self._mouse_pressed = True + # Trigger an immediate update on first mouse down + self._throttled_update() elif event.type() == QtCore.QEvent.Type.MouseButtonRelease: self._mouse_pressed = False + self._update_timer.stop() # Trigger update on mouse release for finalized selection if self.isVisible(): self._update() From cffb23c99aed6de080429d77f39816dd13b35e1d Mon Sep 17 00:00:00 2001 From: philippe-ynput Date: Tue, 26 May 2026 15:53:27 +0200 Subject: [PATCH 4/5] perf(loader/ui): optimize inspector refresh timing during drag selection Calculate update interval in milliseconds for more precise timing Sort thumbnail keys to reduce cache misses Remove debug print statement Signed-off-by: philippe-ynput --- client/ayon_core/tools/loader/ui/review_inspector.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/review_inspector.py b/client/ayon_core/tools/loader/ui/review_inspector.py index 370543d1287..af1baee296e 100644 --- a/client/ayon_core/tools/loader/ui/review_inspector.py +++ b/client/ayon_core/tools/loader/ui/review_inspector.py @@ -196,16 +196,16 @@ def _throttled_update(self) -> None: if self._mouse_pressed: start_time = time.time() self._update() - elapsed_time = time.time() - start_time + elapsed_time_ms = (time.time() - start_time) * 1000 num_selected = len(self._current_selection) if not self._first_update_measured and num_selected > 0: # Calculate the update interval based on the first update time self._base_update_time = max( 16, - int((elapsed_time * 1000) / num_selected), + int(elapsed_time_ms / num_selected), ) self._first_update_measured = True - print(f"base update time: {self._base_update_time} ms") + # print(f"base update time: {self._base_update_time} ms") self._update_timer.start(self._base_update_time * num_selected) def _on_model_reset(self) -> None: @@ -316,6 +316,7 @@ def _update(self) -> None: version_ids.append(vid) if thumb_keys: + thumb_keys = sorted(thumb_keys) # limit cache misses self._load_thumbnail(thumb_keys) else: self._current_thumb_key = "" From d2808a59de6baf61523ca7cd24472a640733cceb Mon Sep 17 00:00:00 2001 From: philippe-ynput Date: Thu, 28 May 2026 15:56:39 +0200 Subject: [PATCH 5/5] perf(loader/ui): enforce minimum refresh interval during drag selection Ensure inspector refreshes maintain at least 16ms interval to prevent excessive updates during rapid drag selection Signed-off-by: philippe-ynput --- client/ayon_core/tools/loader/ui/review_inspector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/loader/ui/review_inspector.py b/client/ayon_core/tools/loader/ui/review_inspector.py index af1baee296e..62b16b0669c 100644 --- a/client/ayon_core/tools/loader/ui/review_inspector.py +++ b/client/ayon_core/tools/loader/ui/review_inspector.py @@ -206,7 +206,7 @@ def _throttled_update(self) -> None: ) self._first_update_measured = True # print(f"base update time: {self._base_update_time} ms") - self._update_timer.start(self._base_update_time * num_selected) + self._update_timer.start(max(16, self._base_update_time * num_selected)) def _on_model_reset(self) -> None: """Clear the selection when the model is reset."""