perf(images): refresh the listing when the monitor sees a new output (#348, PR 2)
The image monitor now reports every settled output (linked or not, also when linking raised) with its resolved path through set_output_listener. The API's notify_output_settled hops from the monitor's worker thread onto the server's loop (call_soon_threadsafe) and does nothing before the loop is known or after cleanup. mark_root_dirty matches the path against each walk's resolved_root (normcase(realpath), computed in the executor by walk_root, now its fourth value) and marks the root dirty. A dirty root gets one background walk, no sooner than rewalk_interval = min(15, max(5, 5 x last walk's duration)) after the previous walk started (last_started_at / last_duration are recorded on every walk). _schedule_walk is shared with PR 1's delayed stale refresh: one deferred walk per root, never armed while a walk runs (the walk's end re-checks dirty: the trailing walk), at least 0.5 s away, the earlier deadline wins, and every walk start cancels it. A failed walk clears dirty. Rescan semantics are unchanged.
This commit is contained in:
+38
-2
@@ -106,6 +106,10 @@ class ImageGenerationHandler(FileSystemEventHandler):
|
||||
# ``queue_capture_thumbnail``, wired at start-up; #249). It only
|
||||
# queues: the preview is written on its own worker. None: no queue.
|
||||
self.thumbnail_queue: Optional[Callable[[str], str]] = None
|
||||
# Told about every settled output, with its resolved path (the API's
|
||||
# ``notify_output_settled``, wired at start-up; #348): the Images
|
||||
# listing walks that root again. None: nobody is told.
|
||||
self.output_listener: Optional[Callable[[str], None]] = None
|
||||
|
||||
# Read from GalleryConfig if available, otherwise use defaults
|
||||
try:
|
||||
@@ -259,8 +263,12 @@ class ImageGenerationHandler(FileSystemEventHandler):
|
||||
f"Image never settled or vanished, skipping: {image_path}"
|
||||
)
|
||||
return
|
||||
self.process_new_image(image_path, prompt_snapshot=prompt_snapshot)
|
||||
self._queue_thumbnail(image_path)
|
||||
try:
|
||||
self.process_new_image(image_path, prompt_snapshot=prompt_snapshot)
|
||||
self._queue_thumbnail(image_path)
|
||||
finally:
|
||||
# Linked or not, even if linking raised: the file exists.
|
||||
self._report_output(image_path)
|
||||
finally:
|
||||
with self._pending_lock:
|
||||
self._pending.pop(image_path, None)
|
||||
@@ -285,6 +293,25 @@ class ImageGenerationHandler(FileSystemEventHandler):
|
||||
return
|
||||
self.logger.debug(f"Preview {outcome}: {os.path.basename(image_path)}")
|
||||
|
||||
def _report_output(self, image_path: str) -> None:
|
||||
"""Tell the output listener a settled output exists, best effort.
|
||||
|
||||
The path is resolved here, on the worker thread, so the listener's
|
||||
event loop does no file I/O. A failure is logged, never raised: the
|
||||
output is then listed by the listing's 30 s refresh.
|
||||
"""
|
||||
listener = getattr(self, "output_listener", None)
|
||||
if listener is None:
|
||||
return
|
||||
try:
|
||||
listener(os.path.realpath(image_path))
|
||||
except Exception:
|
||||
self.logger.warning(
|
||||
f"{os.path.basename(image_path)} could not be reported to the "
|
||||
"Images listing",
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
def wait_until_settled(self, image_path: str, stop=None) -> bool:
|
||||
"""True once the file's size is non-zero and unchanged across two reads.
|
||||
|
||||
@@ -848,6 +875,14 @@ class ImageMonitor:
|
||||
self.logger = get_logger("prompt_manager.image_monitor")
|
||||
self.review_recorder: Optional[Callable[[str], str]] = None
|
||||
self.thumbnail_queue: Optional[Callable[[str], str]] = None
|
||||
self.output_listener: Optional[Callable[[str], None]] = None
|
||||
|
||||
def set_output_listener(self, listener: Optional[Callable[[str], None]]) -> None:
|
||||
"""Report every settled output's resolved path to ``listener`` (None
|
||||
stops it), on the running handler and every later one (#348)."""
|
||||
self.output_listener = listener
|
||||
if self.handler is not None:
|
||||
self.handler.output_listener = listener
|
||||
|
||||
def set_thumbnail_queue(
|
||||
self, queue_preview: Optional[Callable[[str], str]]
|
||||
@@ -925,6 +960,7 @@ class ImageMonitor:
|
||||
handler = ImageGenerationHandler(self.db_manager, self.prompt_tracker)
|
||||
handler.review_recorder = self.review_recorder
|
||||
handler.thumbnail_queue = self.thumbnail_queue
|
||||
handler.output_listener = self.output_listener
|
||||
observer = Observer()
|
||||
scheduled = []
|
||||
for output_dir in valid_directories:
|
||||
|
||||
Reference in New Issue
Block a user