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 -2
View File
@@ -116,6 +116,12 @@ pub fn faces_outstanding(
ORDER BY i.id",
faces::embedder_sql("fi.model_id")
))?;
// The store's index in one read rather than a probe per image; see
// `audit`, which splits the same list the same way.
let held = store.held(FACE_TIER).unwrap_or_else(|e| {
log::warn!("faces: reading the thumbnail index: {e}");
Default::default()
});
let rows = stmt
.query_map([faces::embedder_of(model_id)], |r| {
Ok(FaceRequest {
@@ -134,7 +140,7 @@ pub fn faces_outstanding(
// a whole-library button that could only reach photographs the user had
// personally zoomed into. `repairs::spawn` is that
// requirement implemented; this one is the local-only variant.
.filter(|req| store.contains(req.file_id, FACE_TIER))
.filter(|req| held.contains(&req.file_id))
.collect();
Ok(rows)
}
@@ -270,12 +276,21 @@ pub fn audit(
)",
faces::embedder_sql("fi.model_id")
))?;
// One read of the store's index, not one probe per outstanding image:
// `contains` answers the same question, and asked four thousand times
// it cost more than every query above put together. A store whose index
// cannot be read is treated as holding nothing, which is what `contains`
// reports for it too.
let held = store.held(FACE_TIER).unwrap_or_else(|e| {
log::warn!("faces: reading the thumbnail index: {e}");
Default::default()
});
let (mut ready, mut awaiting) = (0u64, 0u64);
for file_id in stmt
.query_map([faces::embedder_of(model_id)], |r| r.get::<_, i64>(0))?
.filter_map(Result::ok)
{
if store.contains(file_id as u64, FACE_TIER) {
if held.contains(&(file_id as u64)) {
ready += 1;
} else {
awaiting += 1;