perf(images): cache the removal marks so a listing page reads no extra table (#480)

The marks reload only when this process writes or clears one, or a restore
replaces the database. The route snapshot gains the preview route.
This commit is contained in:
Vito Sansevero
2026-10-29 10:18:51 -07:00
parent a5125fd42f
commit 0c554467d6
5 changed files with 42 additions and 3 deletions
+17
View File
@@ -140,6 +140,21 @@ BUMP_MARKS_SQL = (
)
# Bumped whenever this process writes or clears a mark (even in a transaction
# that later rolls back): readers that cache the marks reload when it moves.
_generation = 0
def generation() -> int:
"""The marks generation of this process (see ``_generation``)."""
return _generation
def _bump() -> None:
global _generation
_generation += 1
def has_marks_table(conn: sqlite3.Connection) -> bool:
return (
conn.execute(
@@ -167,6 +182,7 @@ def write_mark(
):
conn.execute(_MARK_SQL, (locator, int(size), int(mtime_ns), now))
conn.execute(BUMP_MARKS_SQL)
_bump()
def clear_marks(conn: sqlite3.Connection, locators) -> int:
@@ -183,6 +199,7 @@ def clear_marks(conn: sqlite3.Connection, locators) -> int:
removed += conn.execute(_UNMARK_SQL, (locator,)).rowcount
if removed:
conn.execute(BUMP_MARKS_SQL)
_bump()
return removed
+6
View File
@@ -425,6 +425,12 @@ class PromptManagerAPI(
# Reviewed maintenance plans under single-use tokens; a restart
# forgets them (py/api/maintenance.py).
self._maintenance_plans = PlanStore()
# #480: the removal marks the workspace listing hides, read once now so
# a listing page reads no table until a mark or the database changes.
try:
self._removal_marks()
except Exception:
self.logger.warning("Removal marks could not be read", exc_info=True)
# A safety backup interrupted by a crash leaves its temp copy behind.
try:
+1 -1
View File
@@ -884,7 +884,7 @@ class AdminRoutesMixin:
media_files = []
seen = set()
# #480: a file removed from the library is never a copy or a keeper.
marks = self.db.removal_marks()
marks = self._removal_marks()
for slot, root in enumerate(roots):
for path in _collect_media_files([root], extensions):
try:
+17 -2
View File
@@ -46,9 +46,11 @@ from .job_routes import busy_response
from .maintenance import MAINTENANCE_PREVIEW_LEASE, _fail_job, _failure, backup_note
try:
from ...database import image_removal_schema
from ...database.annotation_schema import MAX_LOCATOR_LENGTH
from ...utils import trash
except ImportError:
from database import image_removal_schema
from database.annotation_schema import MAX_LOCATOR_LENGTH
from utils import trash
@@ -417,6 +419,19 @@ class ImageDeleteMixin:
# ── the workspace listing ─────────────────────────────────────────
def _removal_marks(self) -> dict:
"""The removal marks, cached until a mark changes or the database does.
A listing page reads no table while nothing changed: the cache follows
this process's marks generation and the database identity (a restore).
"""
key = (self._database_identity(self.db), image_removal_schema.generation())
cached = getattr(self, "_marks_cache", None)
if cached is None or cached[0] != key:
cached = (key, self.db.removal_marks())
self._marks_cache = cached
return cached[1]
def _hidden_entries(self, listings) -> set:
"""Normalized paths of listed files a removal mark hides (blocking).
@@ -424,7 +439,7 @@ class ImageDeleteMixin:
``lstat``'ed: a mark hides a file while its size and ``mtime_ns`` are
still the ones it was removed with.
"""
marks = self.db.removal_marks()
marks = self._removal_marks()
if not marks:
return set()
located = self._workspace_locators(listings)
@@ -443,7 +458,7 @@ class ImageDeleteMixin:
def _removed_paths(self, paths) -> set:
"""The absolute paths among *paths* a removal mark hides (blocking)."""
marks = self.db.removal_marks()
marks = self._removal_marks()
if not marks:
return set()
hidden = set()
+1
View File
@@ -118,6 +118,7 @@
"POST /prompt_manager/image-annotations/observe",
"POST /prompt_manager/image-annotations/suggest",
"POST /prompt_manager/images/clear-thumbnails",
"POST /prompt_manager/images/delete/preview",
"POST /prompt_manager/images/generate-thumbnails",
"POST /prompt_manager/images/generate-thumbnails/progress",
"POST /prompt_manager/images/link",