Assets: download online previews in LIFO fashion

When browsing online assets, download the last-requested preview
first. This ensures that, when scrolling through the assets, the
previews that are scrolled into view get priority over previews that
are scrolled out of view.

This introduces an optional keyword-only parameter to
`http.BackgroundDownloader.queue_download(queue_side=QueueSide)`,
where `QueueSide` is an enum with values `FRONT` and `BACK`.

The asset downloader always uses `BACK` for FIFO behaviour, and the
preview downloader always uses `FRONT` for LIFO behaviour.

This causes preview images in the asset browser to be downloaded
bottom-to-top (speaking visually here). This can be adjusted in
Blender's C++ code by requesting the thumbnails in the opposite order.

As an alternative implementation, a priority queue was discussed.
Previews that are scrolled out of view could then get a lower
priority. This means that Blender would have to track which previews
are requested for download, as well as track how many windows are
showing certain previews (because "scrolled out of view in one asset
browser" doesn't always mean "no longer visible"). Simply downloading
the previews in LIFO order was considerably simpler, and likely
sufficient.

Pull Request: https://projects.blender.org/blender/blender/pulls/161897
This commit is contained in:
Sybren A. Stüvel 2026-07-27 10:45:48 +02:00
parent c543fa302a
commit b9dc2c7ca4
2 changed files with 41 additions and 10 deletions

View file

@ -71,6 +71,7 @@ def download_asset_file(
asset_library_local_path,
reporter=AssetReporter(asset_library_url=asset_library_url),
on_queue_empty_callback=on_asset_download_queue_empty,
queue_side=http_dl.QueueSide.BACK, # FIFO queue.
)
downloader.start()
_asset_downloaders[asset_library_url] = downloader
@ -138,6 +139,7 @@ def download_preview(
asset_library_local_path,
reporter=PreviewReporter(),
on_queue_empty_callback=None,
queue_side=http_dl.QueueSide.FRONT, # LIFO queue.
)
downloader.start()
_preview_downloaders[asset_library_url] = downloader
@ -251,6 +253,9 @@ class AssetDownloader:
_HTTP_METHOD = "GET"
_queue_side: http_dl.QueueSide
"""Previews are queued at the front (LIFO), and assets at the back (FIFO)."""
def __init__(
self,
remote_url: str,
@ -258,6 +263,7 @@ class AssetDownloader:
*,
reporter: http_dl.DownloadReporter,
on_queue_empty_callback: QueueEmptyCallback | None,
queue_side: http_dl.QueueSide,
) -> None:
"""Create a downloader for assets of a specific asset library.
@ -285,6 +291,7 @@ class AssetDownloader:
)
self._bg_downloader = None
self._queue_side = queue_side
def _create_bg_downloader(self) -> None:
self._bg_downloader = http_dl.BackgroundDownloader(
@ -415,6 +422,7 @@ class AssetDownloader:
remote_url,
download_to_path,
http_method=self._HTTP_METHOD,
queue_side=self._queue_side,
)
return request_descr.url

View file

@ -464,6 +464,13 @@ class DownloaderOptions:
self.http_headers['user-agent'] = user_agent
class QueueSide(enum.Enum):
BACK = 'back'
"""Download goes to the back of the queue."""
FRONT = 'front'
"""Download goes to the front of the queue."""
class BackgroundDownloader:
"""Wrapper for a ConditionalDownloader + reporters.
@ -539,6 +546,7 @@ class BackgroundDownloader:
on_download_done: DownloadDoneCallback | None = None,
*,
http_method: str = 'GET',
queue_side: QueueSide = QueueSide.BACK,
) -> RequestDescription:
"""Queue up a download of some URL to a location on disk.
@ -566,13 +574,21 @@ class BackgroundDownloader:
# looking at this dict.
self._on_download_done_callbacks[http_req_descr].append(on_download_done)
if not is_already_queued:
if is_already_queued:
# Only queue a download once.
self._num_pending_downloads += 1
self._connection.send(PipeMessage(
msgtype=PipeMsgType.QUEUE_DOWNLOAD,
payload=(http_req_descr, local_path),
))
return http_req_descr
match queue_side:
case QueueSide.BACK:
msgtype = PipeMsgType.QUEUE_DOWNLOAD
case QueueSide.FRONT:
msgtype = PipeMsgType.QUEUE_DOWNLOAD_FRONT
self._num_pending_downloads += 1
self._connection.send(PipeMessage(
msgtype=msgtype,
payload=(http_req_descr, local_path),
))
return http_req_descr
@ -846,7 +862,14 @@ class PipeMsgType(enum.Enum):
"""Payload: BackgroundDownloader.QueuedDownload
Main -> Background process.
Queue a HTTP request for downloading.
Queue a HTTP request for downloading. It will be put at the end of the queue.
"""
QUEUE_DOWNLOAD_FRONT = 'queue-front'
"""Payload: BackgroundDownloader.QueuedDownload
Main -> Background process.
Queue a HTTP request for downloading. It will be put at the front of the queue.
"""
CANCEL_DOWNLOAD = 'cancel'
@ -963,11 +986,9 @@ def _download_queued_items(
tx_thread.start()
def unqueue_request(http_req_descr: RequestDescription) -> None:
"""Remove the given HTTP request from the download queue."""
# Reconstruct the download queue, skipping the given HTTP request.
# We can't use deque.remove() here, because the RequestDescription
# is only part of the objects in the queue.
# is only _part_ of the objects in the queue.
new_queue = [
(queued_req, queued_path)
for (queued_req, queued_path) in download_queue
@ -1000,6 +1021,8 @@ def _download_queued_items(
do_shutdown.set()
case PipeMsgType.QUEUE_DOWNLOAD:
download_queue.append(received_msg.payload)
case PipeMsgType.QUEUE_DOWNLOAD_FRONT:
download_queue.appendleft(received_msg.payload)
case PipeMsgType.CANCEL_DOWNLOAD:
assert isinstance(received_msg.payload, RequestDescription)
request_to_cancel: RequestDescription = received_msg.payload