Add feature cache status scanning with train/classify warnings - #424
Conversation
|
Follow-up commit (d77999a): the in-memory status was being invalidated wholesale on run completion and rebuilt lazily, which meant the repopulating scan ran synchronously on the GUI thread inside the next Train/Classify click handler — a visible stall on a large project on network storage. Two changes:
Because scan results replace the whole map, Still no eager update at the moment features are written: the workers that write them don't report back what they wrote, and the rescan is metadata-only. Verified on a real project with real caches on disk, driving a real offscreen MainWindow: after a single-video classification finishes, only that video's status is dropped, the background rescan repopulates both videos, the newly computed features are reflected ( |
There was a problem hiding this comment.
🟡 Changes recommended
There are correctness and lifecycle issues in the new cache-status/threading logic (status completeness across multiple cache dirs and scan-thread management) that should be addressed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
This PR adds a lightweight, read-only scan of a project’s on-disk feature cache, stores the results in memory on the Project, and uses that status to (1) display cache details in Get Info and (2) warn before Train/Classify when required window-size features are missing (and would otherwise be computed mid-run with no user-facing explanation).
Changes:
- Add cache inspection in
jabs-io(per-identity metadata-only scanning) and project-level aggregation/scanning utilities. - Introduce
ProjectAPIs to store/refresh/invalidate cache status and query videos missing a required window size (optionally scoped to labeled identities for training). - Integrate status into the GUI (background scan on project open, Get Info feature-cache section, train/classify preflight warning), plus docs and test coverage.
File summaries
| File | Description |
|---|---|
| tests/ui/test_video_list_widget.py | Adds coverage for “Get Info” rescanning behavior and error fallback. |
| tests/ui/test_video_info_dialog.py | New tests asserting feature cache section rendering in the dialog. |
| tests/ui/test_main_window.py | New tests for background scan result handling and thread lifecycle behavior. |
| tests/ui/test_feature_cache_text.py | New tests for Qt-free formatting helpers used by the dialog. |
| tests/ui/test_feature_cache_scan_thread.py | New tests for scan thread emission, failure handling, and termination behavior. |
| tests/ui/test_central_widget.py | Adds tests for window-size selection and train/classify gating confirmation logic. |
| tests/project/test_project_feature_cache_status.py | New tests for Project’s in-memory status store and query APIs. |
| tests/project/test_feature_cache_status.py | New tests for per-video aggregation and on-disk scan behavior. |
| src/jabs/ui/main_window/video_list_widget.py | Passes per-video cache status into the info dialog; adds per-video rescan with fallback. |
| src/jabs/ui/main_window/menu_handlers.py | After clearing cache, triggers a background rescan to reflect the empty cache. |
| src/jabs/ui/main_window/main_window.py | Starts/stops background feature-cache scans and stores results on the open project. |
| src/jabs/ui/main_window/central_widget.py | Adds train/classify warnings when window-size features are missing; invalidates status after runs. |
| src/jabs/ui/feature_cache_text.py | Adds pure string formatting helpers (size/format/window-size display). |
| src/jabs/ui/feature_cache_scan_thread.py | Implements background scan thread with cooperative termination. |
| src/jabs/ui/dialogs/video_info_dialog.py | Adds a Feature Cache section driven by VideoFeatureCacheStatus. |
| src/jabs/resources/docs/user_guide/gui.md | Documents new Feature Computation warning behavior and expanded Get Info. |
| src/jabs/project/project.py | Adds cache status storage, refresh/invalidation, and missing-features queries + labeled identity selection. |
| src/jabs/project/feature_cache_status.py | Implements per-video status aggregation and project/video cache scanners. |
| src/jabs/project/init.py | Re-exports new cache status types and scan functions. |
| packages/jabs-io/tests/feature_cache/test_inspection.py | New tests for metadata-only identity cache inspection across formats and failure modes. |
| packages/jabs-io/src/jabs/io/feature_cache/inspection.py | Adds inspect_identity_cache() + IdentityCacheInfo for read-only cache inspection. |
| packages/jabs-io/src/jabs/io/feature_cache/init.py | Exposes inspection API from the feature_cache package. |
| docs/user-guide/gui.md | Mirrors user-guide updates for the online docs copy. |
Review details
- Files reviewed: 23/23 changed files
- Comments generated: 3
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
|
all three were real. Addressed in cd42238. 1. Concurrent scan threads ( Fixed by coalescing to a single in-flight scan: when a refresh arrives while one is running, that scan is asked to stop (so its now-stale results are never emitted) and a replacement is queued, started from Measured with real threads, five refreshes fired back to back and each video's scan slowed down:
2. 3. Tests added: per-identity per-frame merging (including the multi-pose-hash case), |
There was a problem hiding this comment.
🟡 Changes recommended
The shutdown path can still allow a running child QThread to outlive the MainWindow (ignored wait() timeout), risking “QThread destroyed while running” instability on slow/unresponsive storage.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (2)
src/jabs/project/project.py:626
- labeled_identities() calls VideoManager.load_annotations() for every video, and load_annotations() performs a filesystem exists() check and JSON load each time. This means clicking Train in a large project can cause synchronous disk I/O over all annotation files (potentially slow on network storage) just to compute the warning scope.
labeled: dict[str, set[int]] = {}
for video in self._video_manager.videos:
annotations = self._video_manager.load_annotations(video)
if annotations is None:
continue
src/jabs/ui/main_window/central_widget.py:1307
- The docstring for _cleanup_classify_thread() says it cleans up the training thread, which is misleading and makes log/traceback reading harder when debugging classification failures.
"""clean up the training thread"""
- Files reviewed: 23/23 changed files
- Comments generated: 1
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
|
Addressed in 43e0c70 (plus the two suppressed comments). Details, including where I landed differently than suggested: Shutdown wait ignoring I did not adopt the "longer/blocking wait" part of the suggestion: an unbounded wait trades a harmless exit-time abort for an unquittable app, on exactly the slow/unresponsive storage this feature targets. The cooperative stop is checked between videos and cannot interrupt a filesystem call that never returns, so waiting longer only postpones the same outcome. Instead Two alternatives I tested and rejected:
Measured on the wedged-storage simulation, quitting via the window close button:
A responsive scan is unaffected: it stops cooperatively in ~0.1 s and is never terminated. Suppressed comment: Suppressed comment: 985 tests pass, ruff clean. New tests cover the escalation ladder (responsive stop, forced stop, unstoppable thread logged) and the short-circuit (asserting |
There was a problem hiding this comment.
🟡 Changes recommended
The new jabs-io cache inspection module currently has verified error-handling and optional-dependency issues (notably unconditional h5py import and directory-size scanning raising OSError) that can break cache scans/imports in supported environments.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (2)
packages/jabs-io/src/jabs/io/feature_cache/inspection.py:110
- _directory_size() can raise OSError if directory.iterdir() fails (e.g., unreadable/vanishing cache dir). That would bubble out of inspect* and contradict inspect_identity_cache()’s contract of returning None (or otherwise not raising) for unreadable caches; it also risks aborting project-wide scans on transient filesystem errors.
def _directory_size(directory: Path) -> int:
"""Return the total size in bytes of the files directly inside a directory."""
total = 0
for path in directory.iterdir():
if path.is_file():
try:
total += path.stat().st_size
except OSError:
logger.debug("Could not stat cache file %s", path, exc_info=True)
return total
packages/jabs-io/src/jabs/io/feature_cache/inspection.py:117
- After making h5py optional, _inspect_hdf5_cache() should explicitly handle the
h5py is Nonecase; otherwise it will raise AttributeError when attemptingh5py.File(...), which again violates inspect_identity_cache()’s “unreadable cache behaves like absent” contract.
def _inspect_hdf5_cache(identity_dir: Path) -> IdentityCacheInfo | None:
"""Inspect an HDF5 (``features.h5``) cache directory."""
path = identity_dir / _HDF5_FILENAME
try:
with h5py.File(path, "r") as f:
- Files reviewed: 23/23 changed files
- Comments generated: 1
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
|
Addressed in 4ae32e2, but by going the other way on the premise: h5py is now a required dependency of HDF5 isn't a format-specific backend in JABS — pose files, prediction files and the default feature cache are all HDF5 — and the "optional" extra was already fiction. Five modules in this package import h5py unconditionally at module scope, and have for a long time:
Only So rather than adding a lazy import and
Suppressed comment: Suppressed comment: handle 983 tests on this branch plus 238 in |
There was a problem hiding this comment.
🟡 Changes recommended
The new cache-coverage check used for Train/Classify warnings currently treats stale/incomplete caches as “cached,” and the classification cleanup leaves stale _classification_targets state.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (2)
src/jabs/project/feature_cache_status.py:144
has_window_features()currently only checks whetherwindow_sizeappears in each identity’swindow_sizes. This means a video can be treated as “cached” even when the cache is known to be unusable and will be recomputed mid-run (e.g., stalefeature_versionor missing per-frame features). That undermines the Train/Classify warning gate, sinceProject.videos_missing_window_features()relies on this method.
IdentityFeatures recomputes on FeatureVersionException/DistanceScaleException/PoseHashException (see src/jabs/feature_extraction/features.py:278-292), so a stale cache should be treated as missing for warning purposes, and identities lacking per-frame features should not be considered covered.
cached = self._window_sizes_by_identity()
if identities is None:
if self.expected_identity_count is None:
return False
identities = range(self.expected_identity_count)
return all(window_size in cached.get(identity, frozenset()) for identity in identities)
src/jabs/ui/main_window/central_widget.py:1334
_cleanup_classify_thread()invalidates cache status based on_classification_targetsbut never clears_classification_targetsafterward. This leaves stale targets on the widget instance and can cause later invalidations (or any other logic that inspects_classification_targets) to act on the wrong video set.
# Same as _cleanup_training_thread, for the videos this run classified
# (``None`` means every video was targeted, so every status is dropped).
self._project.invalidate_feature_cache_status(self._classification_targets)
self.feature_cache_changed.emit()
- Files reviewed: 27/28 changed files
- Comments generated: 0 new
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
|
Both fixed in 6ea9a7b.
The fix separates the two questions the status object answers:
One subtlety worth calling out: the loadable sizes are merged across an identity's pose-hash subdirectories per directory, not per field. A window size present only in a stale directory no longer counts because a sibling directory happens to be current — no single cache there could serve the run. Verified against real caches built by Two causes of recomputation still can't be seen from a status scan, and I've documented them on the method rather than leaving them implicit: a pose file that changed since the cache was written (detecting it means hashing every pose file, too slow for a status scan) and a distance-unit setting that no longer matches the cache. The second one is detectable —
While adding tests I also fixed a latent trap in the ones I wrote earlier: 989 tests on this branch, ruff clean. |
(cherry picked from commit 7f29526)
Summary
Adds an in-memory picture of a project's on-disk feature cache and uses it in two places: the video Get Info dialog, and a warning before Train / Classify when features are not cached for the selected window size (they would otherwise be computed mid-run, which can take minutes per video with no explanation to the user).
What's new
Cache inspection (
jabs-io) —inspect_identity_cache()reports what one per-identity cache directory holds: format, feature version, pose hash, cached window sizes, whether per-frame features are present, size on disk. It reads only metadata (HDF5 attributes ormetadata.json), never feature data, and never validates: an unreadable cache is reported the same as an absent one. Parquet window sizes are only reported when thewindow_{size}.parquetfile actually exists, so the result reflects what could really be loaded.Per-video status (
jabs.project.feature_cache_status) —VideoFeatureCacheStatusaggregates the per-identity summaries:window_sizes(cached for every cached identity),partial_window_sizes,cached_identity_count/is_complete,cache_formats,is_stale,cm_units,size_bytes, andhas_window_features(window_size, identities=None). Both on-disk layouts are handled: flatfeatures/<video>/<id>/and the CLI'sfeatures/<video>/<hash>/<id>/.Nothing here validates a cache against its pose file: that would mean hashing every pose file, which is far too slow for a status scan. A cache whose pose file changed still reports as present and is detected when the features are actually loaded.
Background scan at project open —
FeatureCacheScanThreadscans the project off the main thread when it opens (the scan touches every per-identity cache directory, slow enough on network storage to matter) and the results are stored on theProject. The thread supports cooperative termination and is stopped inMainWindow.closeEvent.Project API —
feature_cache_status,set_feature_cache_status(),refresh_feature_cache_status(video),invalidate_feature_cache_status(),videos_missing_window_features(...), andlabeled_identities(behaviors). Videos with no stored status are scanned on demand, so the queries never depend on the background scan having finished. Training and classification invalidate the status when they finish, since a run caches whatever it computed.Get Info — new Feature Cache section: cache directory, identities cached, window sizes (and partial ones), format, feature version with an out-of-date flag, distance units, size on disk, and a warning row when per-frame features are missing. The dialog re-scans the single video so what it shows is current.
Train / Classify warning — a Yes/No dialog naming the number of videos whose features are missing, with the video list in the details section. The scope differs per action so the warning is accurate rather than noisy: training checks only labeled identities (it never reads features for unlabeled ones), while classification checks every identity of every target video.
Verification
jabs-io. Ruff clean.IdentityFeaturesoutput for both cache formats: the scanner agrees with what the production write path produces (3 identities, window sizes 5 and 10, correct version, not stale).MainWindowthrough the background scan to confirm results cross the thread boundary and land on the project.Docs
gui.md(both the online copy and the in-app copy) gets a Feature Computation section and an expanded Get Info description.