Reuse the face grid's decoded crops across a redraw

A confirm or a reject changes one row and redraws the whole grid, and the
redraw re-read every crop blob of the selected person (4 MB for the
largest) and decoded every one — 316 ms per click on the reference
library's 754-face person, to arrive at the pixels already on screen.

`load_faces` now takes the crops the previous load decoded, keyed by face,
and moves each into its new cell; the blob read is skipped when every face
is already in hand. `refresh` drains the old cells into it rather than
cloning them. The redraw is 2.6 ms.
This commit is contained in:
2026-09-20 10:56:36 +02:00
parent e0e193efb4
commit 97a854833d
4 changed files with 53 additions and 11 deletions
+29 -6
View File
@@ -261,18 +261,38 @@ pub fn load_people(
/// Crops come from the proxy the grid already built. An image whose proxy has
/// been evicted yields a cell with no crop rather than being dropped — the face
/// is still real, still counted, and still confirmable from its filename.
///
/// `cut` is whatever the previous load of this grid had already decoded,
/// keyed by face, and is consumed: a crop found there is moved into the new
/// cell and neither read from the catalog nor decoded again. A confirm or a
/// reject changes one face's row and redraws the whole grid, and without this
/// the redraw re-read four megabytes of JPEG and decoded seven hundred of
/// them — 300 ms on the reference library's largest person, per click, to
/// arrive at pixels the screen was already showing. Face ids are global, so
/// a map from another person's grid is merely useless, never wrong.
pub fn load_faces(
catalog: &Catalog,
store: &ThumbStore,
person: PersonId,
mut cut: std::collections::HashMap<FaceId, FaceCrop>,
) -> Result<Vec<FaceCell>, dr_catalog::CatalogError> {
let conn = catalog.connection();
let rows = faces::for_person(conn, person, true)?;
// The crops kept at detection time, in one query. Where a face has one this
// is the whole cost of drawing it — no proxy, no full-size JPEG decode, and
// no dependence on the thumbnail cache still holding the photograph.
let stored = faces::crops_for_person(conn, person, true)?;
// The crops kept at detection time, in one query — but only when a face
// is not already in hand. The common redraw has every face cached and
// skips the blob read entirely; a face the cache lacks (a regroup, a
// fresh sweep) costs the read for the whole person once, and it is
// cached from then on.
//
// Where a face has a stored crop this is the whole cost of drawing it —
// no proxy, no full-size JPEG decode, and no dependence on the thumbnail
// cache still holding the photograph.
let stored = if rows.iter().all(|f| cut.contains_key(&f.id)) {
Default::default()
} else {
faces::crops_for_person(conn, person, true)?
};
// The fallback path, for faces indexed before crops were kept. One decode
// per *image*, not per face: a group photograph holding six faces of one
@@ -283,7 +303,10 @@ pub fn load_faces(
let mut out = Vec::with_capacity(rows.len());
for f in rows {
let crop = match stored.get(&f.id).and_then(|b| decode_crop(b)) {
let crop = match cut
.remove(&f.id)
.or_else(|| stored.get(&f.id).and_then(|b| decode_crop(b)))
{
Some(c) => Some(c),
None => {
let entry = decoded
@@ -657,7 +680,7 @@ pub fn preview_split(
.map(|(c, _)| c)
.unwrap_or_default();
let cells = load_faces(catalog, store, person)?;
let cells = load_faces(catalog, store, person, Default::default())?;
if cells.len() < 2 {
return Ok(vec![cells]);
}