From 3133ac2e62dc4321c6e0737e504aeede12de5a58 Mon Sep 17 00:00:00 2001 From: AlanRockefeller Date: Thu, 16 Apr 2026 16:22:03 -0500 Subject: [PATCH 1/2] Improve performance by wiring bulk metadata map into refresh path --- faststack/app.py | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/faststack/app.py b/faststack/app.py index 375dc78..d0a049d 100644 --- a/faststack/app.py +++ b/faststack/app.py @@ -635,7 +635,10 @@ def apply_filter(self, filter_string: str, filter_flags: list): if self._is_grid_view_active and self._thumbnail_model: self._grid_refreshes += 1 - self._thumbnail_model.refresh_from_controller(self.image_files) + self._thumbnail_model.refresh_from_controller( + self.image_files, + metadata_map=self._get_bulk_metadata_map(self.image_files), + ) self._path_resolver.update_from_model(self._thumbnail_model) self._grid_model_dirty = False else: @@ -683,7 +686,10 @@ def clear_filter(self): if self._is_grid_view_active and self._thumbnail_model: self._grid_refreshes += 1 - self._thumbnail_model.refresh_from_controller(self.image_files) + self._thumbnail_model.refresh_from_controller( + self.image_files, + metadata_map=self._get_bulk_metadata_map(self.image_files), + ) self._path_resolver.update_from_model(self._thumbnail_model) self._grid_model_dirty = False else: @@ -790,7 +796,10 @@ def set_sort_mode(self, mode: str): if self._is_grid_view_active and self._thumbnail_model: self._grid_refreshes += 1 - self._thumbnail_model.refresh_from_controller(self.image_files) + self._thumbnail_model.refresh_from_controller( + self.image_files, + metadata_map=self._get_bulk_metadata_map(self.image_files), + ) self._path_resolver.update_from_model(self._thumbnail_model) self._grid_model_dirty = False else: @@ -1139,7 +1148,10 @@ def load(self, skip_thumbnail_refresh: bool = False): and self._thumbnail_model.rowCount() == 0 ): self._grid_refreshes += 1 - self._thumbnail_model.refresh_from_controller(self.image_files) + self._thumbnail_model.refresh_from_controller( + self.image_files, + metadata_map=self._get_bulk_metadata_map(self.image_files), + ) self._path_resolver.update_from_model(self._thumbnail_model) self._grid_model_dirty = False @@ -1218,7 +1230,10 @@ def refresh_image_list(self): # Refresh thumbnail model if it exists (for external file changes or startup) if self._thumbnail_model and self._is_grid_view_active: self._grid_refreshes += 1 - self._thumbnail_model.refresh_from_controller(self.image_files) + self._thumbnail_model.refresh_from_controller( + self.image_files, + metadata_map=self._get_bulk_metadata_map(self.image_files), + ) self._path_resolver.update_from_model(self._thumbnail_model) self._grid_model_dirty = False @@ -2351,7 +2366,10 @@ def _set_grid_view_active(self, active: bool): self._grid_refreshes += 1 # Always use controller's list, even if empty. - self._thumbnail_model.refresh_from_controller(self.image_files) + self._thumbnail_model.refresh_from_controller( + self.image_files, + metadata_map=self._get_bulk_metadata_map(self.image_files), + ) # Update path resolver for the current directory self._path_resolver.update_from_model(self._thumbnail_model) @@ -2588,11 +2606,14 @@ def _get_metadata_dict(self, image_path: Path | str) -> dict: "todo": False, } - def _get_bulk_metadata_map(self) -> Dict[str, dict]: - """Get flattened metadata map for all images (for efficient grid refresh).""" + def _get_bulk_metadata_map(self, images=None) -> Dict[str, dict]: + """Get flattened metadata map for the given images (defaults to self.image_files). + + Used to avoid per-image sidecar lookups on the UI thread during grid refresh. + """ bulk_map = {} try: - for img in self.image_files: + for img in (images if images is not None else self.image_files): key = self.sidecar.metadata_key_for_path(img.path) meta = self.sidecar.get_metadata(img.path, create=False) if meta is None: From 10d8ee488b22fbc43b94ccad6c0499afa28ed0f6 Mon Sep 17 00:00:00 2001 From: AlanRockefeller Date: Thu, 16 Apr 2026 17:35:06 -0500 Subject: [PATCH 2/2] pass a builder (callable) instead of a prebuilt map, and have refresh_from_controller invoke it after filename filtering --- faststack/app.py | 12 ++++++------ faststack/thumbnail_view/model.py | 14 +++++++++++++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/faststack/app.py b/faststack/app.py index d0a049d..fda5ad9 100644 --- a/faststack/app.py +++ b/faststack/app.py @@ -637,7 +637,7 @@ def apply_filter(self, filter_string: str, filter_flags: list): self._grid_refreshes += 1 self._thumbnail_model.refresh_from_controller( self.image_files, - metadata_map=self._get_bulk_metadata_map(self.image_files), + metadata_map_fn=self._get_bulk_metadata_map, ) self._path_resolver.update_from_model(self._thumbnail_model) self._grid_model_dirty = False @@ -688,7 +688,7 @@ def clear_filter(self): self._grid_refreshes += 1 self._thumbnail_model.refresh_from_controller( self.image_files, - metadata_map=self._get_bulk_metadata_map(self.image_files), + metadata_map_fn=self._get_bulk_metadata_map, ) self._path_resolver.update_from_model(self._thumbnail_model) self._grid_model_dirty = False @@ -798,7 +798,7 @@ def set_sort_mode(self, mode: str): self._grid_refreshes += 1 self._thumbnail_model.refresh_from_controller( self.image_files, - metadata_map=self._get_bulk_metadata_map(self.image_files), + metadata_map_fn=self._get_bulk_metadata_map, ) self._path_resolver.update_from_model(self._thumbnail_model) self._grid_model_dirty = False @@ -1150,7 +1150,7 @@ def load(self, skip_thumbnail_refresh: bool = False): self._grid_refreshes += 1 self._thumbnail_model.refresh_from_controller( self.image_files, - metadata_map=self._get_bulk_metadata_map(self.image_files), + metadata_map_fn=self._get_bulk_metadata_map, ) self._path_resolver.update_from_model(self._thumbnail_model) self._grid_model_dirty = False @@ -1232,7 +1232,7 @@ def refresh_image_list(self): self._grid_refreshes += 1 self._thumbnail_model.refresh_from_controller( self.image_files, - metadata_map=self._get_bulk_metadata_map(self.image_files), + metadata_map_fn=self._get_bulk_metadata_map, ) self._path_resolver.update_from_model(self._thumbnail_model) self._grid_model_dirty = False @@ -2368,7 +2368,7 @@ def _set_grid_view_active(self, active: bool): # Always use controller's list, even if empty. self._thumbnail_model.refresh_from_controller( self.image_files, - metadata_map=self._get_bulk_metadata_map(self.image_files), + metadata_map_fn=self._get_bulk_metadata_map, ) # Update path resolver for the current directory diff --git a/faststack/thumbnail_view/model.py b/faststack/thumbnail_view/model.py index 926a2d0..6096d71 100644 --- a/faststack/thumbnail_view/model.py +++ b/faststack/thumbnail_view/model.py @@ -528,12 +528,19 @@ def remove_rows_by_path(self, paths: List[Path]) -> None: ) def refresh_from_controller( - self, images: List, metadata_map: Optional[Dict[str, dict]] = None + self, + images: List, + metadata_map: Optional[Dict[str, dict]] = None, + metadata_map_fn: Optional[Callable[[List], Dict[str, dict]]] = None, ): """Refresh images from a pre-loaded list without scanning disk. Folders are still scanned, but image entries are built from the provided objects. + + If ``metadata_map_fn`` is provided and ``metadata_map`` is not, + the map is built lazily after filename filtering so we only + fetch sidecar metadata for images that will actually be shown. """ self._next_source_reason = "refresh" cur, own = QThread.currentThread(), self.thread() @@ -556,6 +563,11 @@ def refresh_from_controller( needle = self._active_filter.lower() images = [img for img in images if needle in img.path.stem.lower()] + # Build metadata map lazily, after filename filter, so we + # don't do sidecar lookups for images that got filtered out. + if metadata_map is None and metadata_map_fn is not None: + metadata_map = metadata_map_fn(images) + # Apply active flag filters (AND logic) if self._active_filter_flags: flags = self._active_filter_flags