Extensions: improve checks for packages to upgrade

- All remote repositories are included in the check for outdated
  packages, the check-for-updates setting is only used to sync with
  the remote before checking for outdated packages.
- Defer checking for repositories to notify until after Blender
  has opened since checking for installed packages & outdated
  repositories could add some noticeable overhead.
This commit is contained in:
Campbell Barton
2024-05-31 23:13:58 +10:00
parent 1c315c62d2
commit a21f75d04d
4 changed files with 82 additions and 72 deletions

View File

@@ -212,11 +212,11 @@ def repos_to_notify():
prefs = bpy.context.preferences
extension_repos = prefs.extensions.repos
repos_remote = []
for repo_item in extension_repos:
if not repo_item.enabled:
continue
if not repo_item.use_sync_on_startup:
continue
if not repo_item.use_remote_url:
continue
remote_url = repo_item.remote_url
@@ -247,8 +247,20 @@ def repos_to_notify():
if repo_is_empty:
continue
# NOTE: offline checks are handled by the notification (not here).
repos_notify.append(
repos_remote.append(repo_item)
# Update all repos together or none, to avoid bothering users
# multiple times in a day.
do_online_sync = False
for repo_item in repos_remote:
if not repo_item.use_sync_on_startup:
continue
if repo_index_outdated(repo_item.directory):
do_online_sync = True
break
for repo_item in repos_remote:
repos_notify.append((
bl_extension_ops.RepoItem(
name=repo_item.name,
directory=repo_directory,
@@ -257,14 +269,10 @@ def repos_to_notify():
use_cache=repo_item.use_cache,
access_token=repo_item.access_token if repo_item.use_access_token else "",
),
)
repo_item.use_sync_on_startup and do_online_sync,
))
# Update all repos together or none, to avoid bothering users
# multiple times in a day.
if repo_index_outdated(repo_item.directory):
do_online_sync = True
return repos_notify, do_online_sync
return repos_notify
# -----------------------------------------------------------------------------
@@ -593,11 +601,8 @@ def register():
if not bpy.app.background:
if prefs.view.show_extensions_updates:
repos_notify, do_online_sync = repos_to_notify()
if repos_notify:
from . import bl_extension_notify
bl_extension_notify.update_non_blocking(repos=repos_notify, do_online_sync=do_online_sync)
del repos_notify
from . import bl_extension_notify
bl_extension_notify.update_non_blocking(repos_fn=repos_to_notify)
def unregister():

View File

@@ -28,7 +28,9 @@ USE_GRACEFUL_EXIT = False
# Special value to signal no packages can be updated because all repositories are blocked by being offline.
STATE_DATA_ALL_OFFLINE = object()
WM_EXTENSIONS_CHECKING = -1
# `wmWindowManager.extensions_updates` from C++
WM_EXTENSIONS_UPDATE_UNSET = -2
WM_EXTENSIONS_UPDATE_CHECKING = -1
# -----------------------------------------------------------------------------
@@ -142,7 +144,7 @@ def sync_apply_locked(repos_notify, repos_notify_files, unique_ext):
return any_lock_errors, any_stale_errors
def sync_status_generator(repos_notify, do_online_sync):
def sync_status_generator(repos_fn):
import atexit
# Generator results...
@@ -153,18 +155,22 @@ def sync_status_generator(repos_notify, do_online_sync):
# Setup The Update #
# ################ #
repos_notify_orig = repos_notify
if not bpy.app.online_access:
repos_notify = [repo for repo in repos_notify if repo.remote_url.startswith("file://")]
if not repos_notify:
# Special case, early exit.
yield (STATE_DATA_ALL_OFFLINE, 0, ())
return
yield None
any_offline = len(repos_notify) != len(repos_notify_orig)
del repos_notify_orig
# Calculate the repositories.
# This may be an expensive so yield once before running.
repos_and_do_online = list(repos_fn())
assert isinstance(repos_and_do_online, list)
if not bpy.app.online_access:
# Allow file-system only sync.
repos_and_do_online = [
(repo, do_online_sync) for repo, do_online_sync in repos_and_do_online
if repo.remote_url.startswith("file://")
]
if not repos_and_do_online:
return
# An extension unique to this session.
unique_ext = "@{:x}".format(os.getpid())
@@ -172,7 +178,7 @@ def sync_status_generator(repos_notify, do_online_sync):
from functools import partial
cmd_batch_partial = []
for repo_item in repos_notify:
for repo_item, do_online_sync in repos_and_do_online:
# Local only repositories should still refresh, but not run the sync.
assert repo_item.remote_url
cmd_batch_partial.append(partial(
@@ -231,7 +237,7 @@ def sync_status_generator(repos_notify, do_online_sync):
update_total = -1
any_lock_errors = False
repos_notify_files = [[] for _ in repos_notify]
repos_notify_files = [[] for _ in repos_and_do_online]
is_debug = bpy.app.debug
while True:
@@ -274,15 +280,13 @@ def sync_status_generator(repos_notify, do_online_sync):
# ################### #
# Finalize The Update #
# ################### #
repos_notify = [repo for repo, _do_online_sync in repos_and_do_online]
any_lock_errors, any_stale_errors = sync_apply_locked(repos_notify, repos_notify_files, unique_ext)
update_total = sync_status_count_outdated_extensions(repos_notify)
if any_lock_errors:
extra_warnings.append(" Failed to acquire lock!")
if any_stale_errors:
extra_warnings.append(" Unexpected change in repository!")
if any_offline:
extra_warnings.append(" Skipping online repositories!")
yield (cmd_batch.calc_status_data(), update_total, extra_warnings)
else:
yield None
@@ -309,31 +313,28 @@ class NotifyHandle:
"splash_region",
"sync_info",
"do_online_sync",
"_repos",
"_repos_fn",
"is_complete",
"_sync_generator",
)
def __init__(self, repos_notify, do_online_sync):
def __init__(self, repos_fn):
self.splash_region = None
self._repos = repos_notify
self._repos_fn = repos_fn
self._sync_generator = None
self.is_complete = False
# status_data, update_count, extra_warnings.
self.sync_info = None
self.do_online_sync = do_online_sync
def run(self):
assert self._sync_generator is None
self._sync_generator = iter(sync_status_generator(self._repos, self.do_online_sync))
self._sync_generator = iter(sync_status_generator(self._repos_fn))
def run_ensure(self):
if self.is_running():
return False
return
self.run()
return True
def run_step(self):
assert self._sync_generator is not None
@@ -349,17 +350,16 @@ class NotifyHandle:
def updates_count(self):
if self.sync_info is None:
return WM_EXTENSIONS_CHECKING
return WM_EXTENSIONS_UPDATE_CHECKING
_status_data, update_count, _extra_warnings = self.sync_info
return update_count
def ui_text(self):
if self.sync_info is None:
return "Checking for Extension Updates", 'NONE', WM_EXTENSIONS_CHECKING
return "Checking for Extension Updates", 'NONE', WM_EXTENSIONS_UPDATE_CHECKING
status_data, update_count, extra_warnings = self.sync_info
do_online_sync = self.do_online_sync
text, icon = bl_extension_utils.CommandBatch.calc_status_text_icon_from_data(
status_data, update_count, do_online_sync,
status_data, update_count,
)
# Not more than 1-2 of these (failed to lock, some repositories offline .. etc).
for warning in extra_warnings:
@@ -408,12 +408,13 @@ def _ui_refresh_apply(*, notify):
def _ui_refresh_timer():
if not _notify_queue:
if wm.extensions_updates == WM_EXTENSIONS_UPDATE_CHECKING:
wm.extensions_updates = WM_EXTENSIONS_UPDATE_UNSET
return None
wm = bpy.context.window_manager
notify = _notify_queue[0]
if notify.run_ensure():
wm.extensions_updates = WM_EXTENSIONS_CHECKING
notify.run_ensure()
default_wait = TIME_WAIT_STEP
@@ -425,11 +426,19 @@ def _ui_refresh_timer():
# Nothing changed, no action is needed (waiting for a response).
return default_wait
# Some content was found, set checking.
# Avoid doing this early because the icon flickers in cases when
# it's not needed and it gets turned off quickly.
if wm.extensions_updates == WM_EXTENSIONS_UPDATE_UNSET:
wm.extensions_updates = WM_EXTENSIONS_UPDATE_CHECKING
# If the generator exited, either step to the next action or early exit here.
if sync_info is ...:
_ui_refresh_apply(notify=notify)
if len(_notify_queue) <= 1:
# Keep the item because the text should remain displayed for the splash.
# Keep `_notify_queuy[0]` because the text should remain displayed for the splash.
if wm.extensions_updates == WM_EXTENSIONS_UPDATE_CHECKING:
wm.extensions_updates = WM_EXTENSIONS_UPDATE_UNSET
return None
# Move onto the next item.
del _notify_queue[0]
@@ -449,17 +458,16 @@ def _ui_refresh_timer():
# Public API
def update_non_blocking(*, repos, do_online_sync):
"""
Perform a non-blocking update on ``repos``.
Updates are queued in case some are already running.
"""
# TODO: it's possible this preferences requests updates just after check-for-updates on startup.
# The checks should be de-duplicated. For now just ensure the checks don't interfere with each other.
assert bool(repos), "Unexpected empty repository list passed in"
_notify_queue.append(NotifyHandle(repos, do_online_sync))
def update_non_blocking(*, repos_fn):
# Perform a non-blocking update on ``repos``.
# Updates are queued in case some are already running.
# `repos_fn` A generator or function that returns a list of ``(RepoItem, do_online_sync)`` pairs.
# Some repositories don't check for update on startup for e.g.
_notify_queue.append(NotifyHandle(repos_fn))
if not bpy.app.timers.is_registered(_ui_refresh_timer):
bpy.app.timers.register(_ui_refresh_timer, first_interval=TIME_WAIT_INIT, persistent=True)
return True
def update_in_progress():

View File

@@ -309,14 +309,13 @@ class notify_info:
pass
case None:
from .bl_extension_notify import update_non_blocking
if repos_notify := [repo for repo in repos if repo.remote_url]:
update_non_blocking(repos=repos_notify, do_online_sync=True)
notify_info._update_state = False
# Starting.
in_progress = True
else:
# Nothing to do, finished.
notify_info._update_state = True
# Assume nothing to do, finished.
notify_info._update_state = True
if repos_notify := [(repo, True) for repo in repos if repo.remote_url]:
if update_non_blocking(repos_fn=lambda: repos_notify):
# Correct assumption, starting.
notify_info._update_state = False
in_progress = True
case False:
from .bl_extension_notify import update_in_progress
if update_in_progress():

View File

@@ -878,7 +878,6 @@ class CommandBatch:
def calc_status_text_icon_from_data(
status_data: CommandBatch_StatusFlag,
update_count: int,
do_online_sync: bool,
) -> Tuple[str, str]:
# Generate a nice UI string for a status-bar & splash screen (must be short).
#
@@ -893,12 +892,11 @@ class CommandBatch:
else:
fail_text = ", some actions failed"
if status_data.flag == 1 << CommandBatchItem.STATUS_NOT_YET_STARTED or \
status_data.flag & 1 << CommandBatchItem.STATUS_RUNNING:
if do_online_sync:
return "Checking for Extension Updates Online{:s}".format(fail_text), 'SORTTIME'
else:
return "Checking for Extension Updates{:s}".format(fail_text), 'SORTTIME'
if (
status_data.flag == (1 << CommandBatchItem.STATUS_NOT_YET_STARTED) or
status_data.flag & (1 << CommandBatchItem.STATUS_RUNNING)
):
return "Checking for Extension Updates{:s}".format(fail_text), 'SORTTIME'
if status_data.flag == 1 << CommandBatchItem.STATUS_COMPLETE:
if update_count > 0: