Speed up page loads — pre-fetch bookmarks/cache as sets, off-load PIL conversion to a worker

This commit is contained in:
pax 2026-04-07 11:36:23 -05:00
parent 2b9bf22249
commit 74f948a3e8
2 changed files with 37 additions and 13 deletions

View File

@ -2,6 +2,7 @@
from __future__ import annotations
import asyncio
import hashlib
import zipfile
from collections import OrderedDict
@ -165,9 +166,11 @@ async def download_image(
gif_path = local.with_suffix(".gif")
if gif_path.exists():
return gif_path
# If the zip is cached but not yet converted, convert it now
# If the zip is cached but not yet converted, convert it now.
# PIL frame iteration is CPU-bound and would block the asyncio
# loop for hundreds of ms — run it in a worker thread instead.
if local.exists() and zipfile.is_zipfile(local):
return _convert_ugoira_to_gif(local)
return await asyncio.to_thread(_convert_ugoira_to_gif, local)
# Check if animated PNG/WebP was already converted to gif
if local.suffix.lower() in (".png", ".webp"):
@ -180,7 +183,7 @@ async def download_image(
if _is_valid_media(local):
# Convert animated PNG/WebP on access if not yet converted
if local.suffix.lower() in (".png", ".webp"):
converted = _convert_animated_to_gif(local)
converted = await asyncio.to_thread(_convert_animated_to_gif, local)
if converted != local:
return converted
return local
@ -233,12 +236,13 @@ async def download_image(
local.unlink()
raise ValueError("Downloaded file is not valid media")
# Convert ugoira zip to animated GIF
# Convert ugoira zip to animated GIF (PIL is sync + CPU-bound;
# off-load to a worker so we don't block the asyncio loop).
if local.suffix.lower() == ".zip" and zipfile.is_zipfile(local):
local = _convert_ugoira_to_gif(local)
local = await asyncio.to_thread(_convert_ugoira_to_gif, local)
# Convert animated PNG/WebP to GIF for Qt playback
elif local.suffix.lower() in (".png", ".webp"):
local = _convert_animated_to_gif(local)
local = await asyncio.to_thread(_convert_animated_to_gif, local)
finally:
pass # shared client stays open for connection reuse
return local

View File

@ -905,6 +905,7 @@ class BooruApp(QMainWindow):
QTimer.singleShot(100, self._clear_loading)
from ..core.config import saved_dir, saved_folder_dir
from ..core.cache import cached_path_for, cache_dir
site_id = self._site_combo.currentData()
# Pre-scan saved directories once instead of per-post exists() calls
@ -918,12 +919,22 @@ class BooruApp(QMainWindow):
if d.exists():
_folder_saved[folder] = {int(f.stem) for f in d.iterdir() if f.is_file() and f.stem.isdigit()}
# Pre-fetch bookmarks for the site once (used for folder checks)
# Pre-fetch bookmarks for the site once and project to a post-id set
# so the per-post check below is an O(1) membership test instead of
# a synchronous SQLite query (was N queries on the GUI thread).
_favs = self._db.get_bookmarks(site_id=site_id) if site_id else []
_bookmarked_ids: set[int] = {f.post_id for f in _favs}
# Pre-scan the cache dir into a name set so the per-post drag-path
# lookup is one stat-equivalent (one iterdir) instead of N stat calls.
_cd = cache_dir()
_cached_names: set[str] = set()
if _cd.exists():
_cached_names = {f.name for f in _cd.iterdir() if f.is_file()}
for i, (post, thumb) in enumerate(zip(posts, thumbs)):
# Bookmark status (DB)
if site_id and self._db.is_bookmarked(site_id, post.id):
if post.id in _bookmarked_ids:
thumb.set_bookmarked(True)
# Saved status (filesystem) — independent of bookmark
saved = post.id in _saved_ids
@ -934,9 +945,8 @@ class BooruApp(QMainWindow):
break
thumb.set_saved_locally(saved)
# Set drag path from cache
from ..core.cache import cached_path_for
cached = cached_path_for(post.file_url)
if cached.exists():
if cached.name in _cached_names:
thumb._cached_path = str(cached)
if post.preview_url:
@ -1012,13 +1022,23 @@ class BooruApp(QMainWindow):
return
from ..core.config import saved_dir
from ..core.cache import cached_path_for
from ..core.cache import cached_path_for, cache_dir
site_id = self._site_combo.currentData()
_sd = saved_dir()
_saved_ids: set[int] = set()
if _sd.exists():
_saved_ids = {int(f.stem) for f in _sd.iterdir() if f.is_file() and f.stem.isdigit()}
# Pre-fetch bookmarks → set, and pre-scan cache dir → set, so the
# per-post checks below avoid N synchronous SQLite/stat calls on the
# GUI thread (matches the optimisation in _on_search_done).
_favs = self._db.get_bookmarks(site_id=site_id) if site_id else []
_bookmarked_ids: set[int] = {f.post_id for f in _favs}
_cd = cache_dir()
_cached_names: set[str] = set()
if _cd.exists():
_cached_names = {f.name for f in _cd.iterdir() if f.is_file()}
posts = ss.append_queue[:]
ss.append_queue.clear()
start_idx = len(self._posts)
@ -1027,11 +1047,11 @@ class BooruApp(QMainWindow):
for i, (post, thumb) in enumerate(zip(posts, thumbs)):
idx = start_idx + i
if site_id and self._db.is_bookmarked(site_id, post.id):
if post.id in _bookmarked_ids:
thumb.set_bookmarked(True)
thumb.set_saved_locally(post.id in _saved_ids)
cached = cached_path_for(post.file_url)
if cached.exists():
if cached.name in _cached_names:
thumb._cached_path = str(cached)
if post.preview_url:
self._fetch_thumbnail(idx, post.preview_url)