Do not recount face coverage on every confirm, and count it without listing

Every click on the Identity screen's face grid — confirm, reject, split,
rename, merge — redrew the whole screen, and the redraw recomputed the
coverage line. That line lists every repair's outstanding images to count
them: six scans of the images table with a correlated EXISTS over the
8 KB face rows, an ORDER BY the job's visiting order, a Target with its
path per row, and a thumbnail-index query per image with faces. On the
reference library (24k images, 19k faces) that was ~200 ms of the
~540 ms each click cost, spent computing a figure a confirm cannot change.

`refresh` now takes what changed: `Changed::Identities` re-reads the rail
and the grid and leaves the coverage line alone; `Changed::Library` — an
open, a sweep ending or stopped, the face data deleted — re-reads it too.

For the times it does run, `repairs::counts` counts instead of building
and dropping the lists, and the thumbnail store is read once
(`ThumbStore::held`) rather than probed once per image in the audit, the
outstanding list and the proxy repair.

`identity_bench` is the measurement: the reads a click performs and the
batch writes, timed against a copy of a real catalog.
This commit is contained in:
2026-09-20 10:56:36 +02:00
parent d790961b28
commit e0e193efb4
6 changed files with 291 additions and 28 deletions
+17
View File
@@ -240,6 +240,23 @@ impl ThumbStore {
.collect()
}
/// Every file id stored at one size, read from the index in one query.
///
/// For a pass that asks about thousands of images at once — the face
/// audit, the repair lists. Each [`contains`](Self::contains) is a
/// prepared statement and a b-tree probe; asked ten thousand times over a
/// scan it costs more than the scan does, where one walk of the index is
/// a few milliseconds and answers every row.
pub fn held(&self, size: ThumbSize) -> Result<std::collections::HashSet<u64>, ThumbError> {
let mut stmt = self
.index
.prepare("SELECT file_id FROM entries WHERE size = ?1")?;
let ids = stmt
.query_map([size as i64], |r| r.get::<_, i64>(0))?
.collect::<Result<Vec<_>, _>>()?;
Ok(ids.into_iter().map(|id| id as u64).collect())
}
/// Store a thumbnail, opening a new shard if the active one is full.
///
/// Re-storing an existing id overwrites in place rather than migrating it