The Images page, its sidebar counts, the subfolder list and the legacy
gallery each rebuilt the output listing when the 30 s cache expired, all
at once, and the subfolder list computed relative paths on the event
loop. Thumbnail writes also threw the listing away.
Keep one RootListing per output root (py/api/gallery_listing.py):
- one walk per root at a time; concurrent readers wait for it (shielded)
- a listing older than 30 s is served at once and refreshed in the
background
- the walk, the comparison with the previous listing and both folder
sets run in the executor; an unchanged walk keeps the same list object
- os.scandir probe: a missing root fails the walk and keeps the previous
listing (one warning per failure streak)
- Rescan waits for a walk started after the click and is a 500 when it
fails, even with a previous listing
- deletes keep the blocking invalidation; a generation counter that
survives drops discards walks that raced it
- thumbnail jobs, conversion and the legacy generate/clear routes no
longer drop the listing
- warm-up on the host's on_startup, cancelled on on_cleanup
Adds tools/vendor/perf/images_load_probe.mjs (maintainer tool, not CI)
and the TESTING.md section "Images listing cache (#348)".