Say what the face sync is doing while it does it

The face pass set the status to "checking faces…" once and then said nothing
until it was finished. On a library whose first export after a re-index is 9,849
photographs and 85 MB of shards, that is eight minutes of a progress bar sitting
still — which is indistinguishable from a hang, and was reported as one twice.

Nothing was wrong with the sync. The only fault was that it was silent.

Three places now report, which are the three that take real time:

- **Preparing**, per image with a count, since this is the long one and the only
  one whose length the user cannot guess from anything on screen.
- **Sending**, per shard with its size, because a face shard carries crops and
  runs to tens of megabytes — one of them is a visible wait on any connection.
  Announced before the upload rather than after, since the wait *is* the upload.
- **Taking in** a peer's shard, which is a download and then a row-by-row merge.

The export reports every 25 images rather than every one, so the channel behind
it stays lost in the write it accompanies. `export_to_shards` keeps its old
signature and delegates, so the callers that do not want progress do not grow a
parameter for it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-28 21:56:16 +02:00
co-authored by Claude Opus 5
parent 35febf31dd
commit a1e8f494b9
2 changed files with 60 additions and 4 deletions
+27 -1
View File
@@ -531,6 +531,23 @@ pub fn export_to_shards(
conn: &Connection,
store: &mut FaceShardStore,
model_id: &str,
) -> Result<usize, CatalogError> {
export_to_shards_reporting(conn, store, model_id, &mut |_, _| {})
}
/// [`export_to_shards`], reporting how far it has got.
///
/// `progress` is called with `(done, total)` as images are written. The first
/// export after a re-index is thousands of photographs and the better part of
/// ten minutes, and a sync that says nothing for that long is indistinguishable
/// from one that has hung — which is exactly how it was reported. Called every
/// few dozen images rather than every one, so the channel behind it is not the
/// expensive part of the loop.
pub fn export_to_shards_reporting(
conn: &Connection,
store: &mut FaceShardStore,
model_id: &str,
progress: &mut dyn FnMut(usize, usize),
) -> Result<usize, CatalogError> {
let mut q = conn.prepare(
"SELECT r.file_id, fi.image_id, fi.source_edge, fi.indexed_at
@@ -545,8 +562,16 @@ pub fn export_to_shards(
})?
.collect::<Result<_, _>>()?;
/// How often to report. Small enough that a bar moves visibly, large
/// enough that the reporting is lost in the write it accompanies.
const REPORT_EVERY: usize = 25;
let total = rows.len();
let mut exported = 0;
for (file_id, image_id, edge, indexed_at) in rows {
for (seen, (file_id, image_id, edge, indexed_at)) in rows.into_iter().enumerate() {
if seen.is_multiple_of(REPORT_EVERY) {
progress(seen, total);
}
// **Not `contains`.** Asking only whether the shard has heard of this
// image means it is exported once and never again — so re-indexing it
// updates the catalog and nothing else, and every other device keeps
@@ -593,6 +618,7 @@ pub fn export_to_shards(
)?;
exported += 1;
}
progress(total, total);
Ok(exported)
}