perf(images): start a stale listing's refresh walk a second after the read (#348)
After the faster walk, a read that found the listing stale still took 0.5-1.0 s against 0.07 s: it was served from the old listing but ran beside the refresh walk it had just started, sharing the GIL. The refresh now starts REFRESH_DELAY (1 s) after that read through a per-root rewalk_handle, so the page's own requests finish first. One pending refresh per root and none while a walk runs; any walk start (a cold read, Rescan, the warm-up) cancels a pending one; on_cleanup cancels them before its first await.
This commit is contained in:
@@ -548,6 +548,11 @@ class PromptManagerAPI(
|
||||
async def cleanup_gallery_listings(self, app=None):
|
||||
"""Stop the background listing walks (aiohttp on_cleanup)."""
|
||||
self._workspace_warm_enabled = False
|
||||
# Before any await, so no delayed refresh can start a walk meanwhile.
|
||||
for record in self._root_listings.values():
|
||||
handle, record.rewalk_handle = record.rewalk_handle, None
|
||||
if handle is not None:
|
||||
handle.cancel()
|
||||
warm, self._workspace_warm_task = self._workspace_warm_task, None
|
||||
if warm is not None:
|
||||
warm.cancel()
|
||||
|
||||
@@ -21,6 +21,11 @@ from typing import Callable, NamedTuple
|
||||
# patch it.
|
||||
REFRESH_AFTER = 30
|
||||
|
||||
# The stale read is served at once; its refresh walk starts this many seconds
|
||||
# later, so the walk does not share the GIL with the page's own requests
|
||||
# (#348 live test: reads during the walk took 2.2 s instead of 0.07 s).
|
||||
REFRESH_DELAY = 1.0
|
||||
|
||||
# A reader with no listing gives up (500) after this many walks in a row were
|
||||
# discarded by invalidations made while they ran.
|
||||
MAX_DISCARDED_WALKS = 3
|
||||
@@ -75,6 +80,8 @@ class RootListing:
|
||||
flight: asyncio.Future | None = None
|
||||
# True while walks keep failing: one warning per streak.
|
||||
failing: bool = False
|
||||
# The delayed refresh not started yet (loop.call_later), or None.
|
||||
rewalk_handle: asyncio.TimerHandle | None = None
|
||||
|
||||
def drop(self) -> None:
|
||||
self.files = None
|
||||
|
||||
+18
-2
@@ -1422,9 +1422,22 @@ class ImageRoutesMixin:
|
||||
)
|
||||
|
||||
def _refresh_if_stale(self, output_path, record):
|
||||
"""Walk a stale listing again, REFRESH_DELAY after this read (on the loop).
|
||||
|
||||
At most one pending refresh per root, none while a walk runs.
|
||||
"""
|
||||
age = _time.monotonic() - (record.walked_at or 0.0)
|
||||
if age >= gallery_listing.REFRESH_AFTER:
|
||||
self._schedule_walk(output_path)
|
||||
if age < gallery_listing.REFRESH_AFTER:
|
||||
return
|
||||
if record.flight is not None or record.rewalk_handle is not None:
|
||||
return
|
||||
record.rewalk_handle = asyncio.get_running_loop().call_later(
|
||||
gallery_listing.REFRESH_DELAY, self._delayed_walk, output_path, record
|
||||
)
|
||||
|
||||
def _delayed_walk(self, output_path, record):
|
||||
record.rewalk_handle = None
|
||||
self._schedule_walk(output_path)
|
||||
|
||||
def _schedule_walk(self, output_path):
|
||||
"""Start a background walk of *output_path* unless one is running."""
|
||||
@@ -1436,6 +1449,9 @@ class ImageRoutesMixin:
|
||||
"""Start the root's one walk; returns its flight future (on the loop)."""
|
||||
loop = asyncio.get_running_loop()
|
||||
self._listing_loop = loop
|
||||
handle, record.rewalk_handle = record.rewalk_handle, None
|
||||
if handle is not None:
|
||||
handle.cancel() # this walk is the refresh it was waiting to start
|
||||
record.walk_seq += 1
|
||||
flight = loop.create_future()
|
||||
record.flight = flight
|
||||
|
||||
Reference in New Issue
Block a user