Skip to content

Commit 6edeba4

Browse files
authored
Merge pull request #2839 from zas/fix_slowdown
Fix plugin search slowdown with multi-layered caching
2 parents b7cd2e2 + b6ed565 commit 6edeba4

File tree

5 files changed

+178
-50
lines changed

5 files changed

+178
-50
lines changed

picard/plugin3/manager.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1469,21 +1469,33 @@ def check_updates(self):
14691469

14701470
return updates
14711471

1472-
def get_plugin_update_status(self, plugin):
1472+
def get_plugin_update_status(self, plugin, force_refresh=False):
14731473
"""Check if a single plugin has an update available."""
1474+
log.debug("Checking update status for plugin: %s", plugin.plugin_id)
1475+
14741476
if not plugin.manifest or not plugin.manifest.uuid:
14751477
return False
14761478

14771479
metadata = self._metadata.get_plugin_metadata(plugin.manifest.uuid)
14781480
if not metadata or not metadata.url:
14791481
return False
14801482

1483+
current_ref = metadata.ref or 'main'
1484+
1485+
# Try cached result first (unless force refresh)
1486+
if not force_refresh:
1487+
cached_result = self._refs_cache.get_cached_update_status(plugin.plugin_id, current_ref)
1488+
if cached_result is not None:
1489+
log.debug("Using cached update status for plugin: %s", plugin.plugin_id)
1490+
return cached_result
1491+
14811492
try:
14821493
backend = git_backend()
14831494
repo = backend.create_repository(plugin.local_path)
14841495
current_commit = repo.get_head_target()
14851496

14861497
# Fetch without updating (suppress progress output)
1498+
log.debug("Making network request for plugin: %s", plugin.plugin_id)
14871499
callbacks = backend.create_remote_callbacks()
14881500
for remote in repo.get_remotes():
14891501
repo.fetch_remote(remote, None, callbacks._callbacks)
@@ -1549,13 +1561,18 @@ def get_plugin_update_status(self, plugin):
15491561
return False
15501562

15511563
repo.free()
1552-
return current_commit != latest_commit
1564+
has_update = current_commit != latest_commit
1565+
# Cache the result with current ref
1566+
self._refs_cache.cache_update_status(plugin.plugin_id, has_update, current_ref)
1567+
return has_update
15531568
except KeyError:
15541569
# Ref not found, no update available
1570+
self._refs_cache.cache_update_status(plugin.plugin_id, False, current_ref)
15551571
return False
15561572
except Exception as e:
15571573
# Log unexpected errors
15581574
log.warning("Failed to check update for plugin %s: %s", plugin.plugin_id, e)
1575+
# Don't cache errors
15591576
return False
15601577

15611578
def get_plugin_remote_url(self, plugin):

picard/plugin3/refs_cache.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,3 +395,35 @@ def update_cache_from_local_repo(self, repo_path, url, versioning_scheme):
395395
except Exception as e:
396396
log.debug('Failed to update cache from local repo: %s', e)
397397
return []
398+
399+
def cache_update_status(self, plugin_id, has_update, current_ref=None):
400+
"""Cache update status for a plugin."""
401+
cache = self.load_cache()
402+
if 'update_status' not in cache:
403+
cache['update_status'] = {}
404+
405+
cache['update_status'][plugin_id] = {
406+
'has_update': has_update,
407+
'current_ref': current_ref,
408+
'timestamp': time.time(),
409+
}
410+
self.save_cache(cache)
411+
412+
def get_cached_update_status(self, plugin_id, current_ref=None, ttl=REFS_CACHE_TTL):
413+
"""Get cached update status for a plugin."""
414+
cache = self.load_cache()
415+
update_cache = cache.get('update_status', {})
416+
417+
if plugin_id not in update_cache:
418+
return None
419+
420+
entry = update_cache[plugin_id]
421+
422+
# Check if ref has changed (invalidates cache)
423+
if current_ref and entry.get('current_ref') != current_ref:
424+
return None
425+
426+
if time.time() - entry['timestamp'] > ttl:
427+
return None # Expired
428+
429+
return entry['has_update']

picard/ui/options/plugins3.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
2121

2222
from datetime import datetime
23+
from functools import partial
2324

2425
from PyQt6 import QtCore, QtWidgets
2526

@@ -86,7 +87,7 @@ def setup_ui(self):
8687

8788
self.refresh_button = QtWidgets.QPushButton(_("Refresh List"))
8889
self.refresh_button.setToolTip(_("Refresh the plugin list to reflect current state"))
89-
self.refresh_button.clicked.connect(self.load)
90+
self.refresh_button.clicked.connect(self._refresh_list)
9091
toolbar_layout.addWidget(self.refresh_button)
9192

9293
toolbar_layout.addStretch()
@@ -157,6 +158,13 @@ def load(self):
157158
except Exception as e:
158159
self._show_status(_("Error loading plugins: {}").format(str(e)))
159160

161+
def _refresh_list(self):
162+
"""Refresh plugin list and update status."""
163+
self.load()
164+
# Also refresh update status
165+
self.plugin_list.refresh_update_status()
166+
self._filter_plugins() # Refresh display to show update indicators
167+
160168
def _show_disabled_state(self):
161169
"""Show UI when plugin system is disabled."""
162170
self.plugin_list.clear()
@@ -215,7 +223,12 @@ def _update_details_button_text(self):
215223

216224
def _on_plugin_selected(self, plugin):
217225
"""Handle plugin selection."""
218-
self.plugin_details.show_plugin(plugin)
226+
# Get cached update status to avoid network call
227+
has_update = None
228+
if plugin and hasattr(self.plugin_list, '_update_status_cache'):
229+
has_update = self.plugin_list._update_status_cache.get(plugin.plugin_id)
230+
231+
self.plugin_details.show_plugin(plugin, has_update)
219232
# Update button text since details are now shown
220233
self._update_details_button_text()
221234

@@ -254,6 +267,10 @@ def _check_for_updates(self):
254267
# Use the manager's check_updates method (which handles versioning schemes correctly)
255268
updates = self.plugin_manager.check_updates()
256269

270+
# Refresh update status in UI after checking
271+
self.plugin_list.refresh_update_status()
272+
self._filter_plugins() # Refresh display to show update indicators
273+
257274
if updates:
258275
# Convert UpdateCheck objects to plugins for the dialog
259276
plugins_with_updates = []
@@ -296,7 +313,9 @@ def _refresh_registry(self):
296313

297314
# Reload the page to show updated registry data (but don't overwrite status)
298315
self.all_plugins = self.plugin_manager.plugins
299-
self._filter_plugins()
316+
# Refresh update status after registry refresh
317+
self.plugin_list.refresh_update_status()
318+
self._filter_plugins() # Refresh display to show update indicators
300319
self._show_enabled_state()
301320
# Update tooltip with fresh registry info
302321
self._update_registry_tooltip()
@@ -376,7 +395,9 @@ def _update_next_plugin(self, async_manager):
376395
self.check_updates_button.setEnabled(True)
377396
self.install_button.setEnabled(True)
378397
self._show_status(_("All plugin updates completed"))
379-
self.load() # Refresh plugin list
398+
# Refresh update status after batch updates
399+
self.plugin_list.refresh_update_status()
400+
self._filter_plugins() # Refresh display to show updated status
380401
return
381402

382403
plugin = self._update_queue.pop(0)
@@ -385,10 +406,10 @@ def _update_next_plugin(self, async_manager):
385406
async_manager.update_plugin(
386407
plugin=plugin,
387408
progress_callback=None,
388-
callback=lambda result, am=async_manager: self._on_plugin_update_complete(result, am),
409+
callback=partial(self._on_plugin_update_complete, async_manager),
389410
)
390411

391-
def _on_plugin_update_complete(self, result, async_manager):
412+
def _on_plugin_update_complete(self, async_manager, result):
392413
"""Handle individual plugin update completion."""
393414
if not result.success:
394415
error_msg = str(result.error) if result.error else _("Unknown error")

picard/ui/widgets/plugindetailswidget.py

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
# along with this program; if not, write to the Free Software
2020
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
2121

22+
from functools import partial
23+
2224
from PyQt6 import QtCore, QtGui, QtWidgets
2325

2426
from picard.i18n import gettext as _
@@ -119,8 +121,13 @@ def setup_ui(self):
119121
# Initially hide everything
120122
self.setVisible(False)
121123

122-
def show_plugin(self, plugin):
123-
"""Show details for the given plugin."""
124+
def show_plugin(self, plugin, has_update=None):
125+
"""Show details for the given plugin.
126+
127+
Args:
128+
plugin: Plugin to show
129+
has_update: Optional cached update status to avoid network call
130+
"""
124131
self.current_plugin = plugin
125132

126133
if plugin is None:
@@ -154,8 +161,12 @@ def show_plugin(self, plugin):
154161
self.git_ref_label.setText(self._get_git_ref_display(plugin))
155162
self.git_url_label.setText(self._get_git_url_display(plugin))
156163

157-
# Check if update is available and enable/disable update button
158-
self.update_button.setEnabled(self._has_update_available(plugin))
164+
# Check if update is available - use cached value if provided, otherwise disable button
165+
if has_update is not None:
166+
self.update_button.setEnabled(has_update)
167+
else:
168+
# Don't check for updates during normal display to avoid network calls
169+
self.update_button.setEnabled(False)
159170

160171
# Enable/disable description button based on long description availability
161172
has_long_desc = self.plugin_manager.long_description_as_html(plugin) is not None
@@ -240,13 +251,12 @@ def _uninstall_plugin(self):
240251
# Fallback to old method if plugin list not found
241252
self._perform_uninstall()
242253

243-
def _on_uninstall_complete(self, result):
254+
def _on_uninstall_complete(self, plugin, result):
244255
"""Handle uninstall completion."""
245256
if result.success:
246257
# Emit the same signal as context menu for status updates
247-
if hasattr(self, '_uninstalling_plugin') and hasattr(self.parent(), 'plugin_state_changed'):
248-
self.parent().plugin_state_changed.emit(self._uninstalling_plugin, "uninstalled")
249-
delattr(self, '_uninstalling_plugin')
258+
if hasattr(self.parent(), 'plugin_state_changed'):
259+
self.parent().plugin_state_changed.emit(plugin, "uninstalled")
250260
self.show_plugin(None) # Clear details
251261
self.plugin_uninstalled.emit() # Signal that plugin was uninstalled
252262
else:
@@ -360,10 +370,11 @@ def _perform_uninstall(self):
360370
dialog = UninstallPluginDialog(self.current_plugin, self)
361371
if dialog.exec() == QtWidgets.QDialog.DialogCode.Accepted:
362372
try:
363-
self._uninstalling_plugin = self.current_plugin # Store for callback
364373
async_manager = AsyncPluginManager(self.plugin_manager)
365374
async_manager.uninstall_plugin(
366-
self.current_plugin, purge=dialog.purge_config, callback=self._on_uninstall_complete
375+
self.current_plugin,
376+
purge=dialog.purge_config,
377+
callback=partial(self._on_uninstall_complete, self.current_plugin),
367378
)
368379
except Exception as e:
369380
QtWidgets.QMessageBox.critical(
@@ -378,20 +389,22 @@ def _perform_update(self):
378389

379390
async_manager = AsyncPluginManager(self.plugin_manager)
380391
async_manager.update_plugin(
381-
plugin=self.current_plugin, progress_callback=None, callback=self._on_update_complete
392+
plugin=self.current_plugin,
393+
progress_callback=None,
394+
callback=partial(self._on_update_complete, self.current_plugin),
382395
)
383396

384-
def _on_update_complete(self, result):
397+
def _on_update_complete(self, plugin, result):
385398
"""Handle update completion."""
386399
self.update_button.setText(_("Update"))
387400

388401
if result.success:
389402
self.plugin_updated.emit() # Signal that plugin was updated
390403
# Emit the same signal as context menu for status updates
391404
if hasattr(self.parent(), 'plugin_state_changed'):
392-
self.parent().plugin_state_changed.emit(self.current_plugin, "updated")
393-
# Refresh the display
394-
self.show_plugin(self.current_plugin)
405+
self.parent().plugin_state_changed.emit(plugin, "updated")
406+
# Refresh the display - plugin should no longer have update available
407+
self.show_plugin(plugin, False)
395408
else:
396409
self.update_button.setEnabled(True)
397410
error_msg = str(result.error) if result.error else _("Unknown error")

0 commit comments

Comments
 (0)