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
+11 -2
View File
@@ -60,8 +60,17 @@ fn main() {
time("load_people", 5, || {
identity::load_people(&catalog, MODEL_ID).unwrap();
});
time("load_faces (largest person)", 5, || {
identity::load_faces(&catalog, &store, biggest).unwrap();
time("load_faces (largest person, cold)", 5, || {
identity::load_faces(&catalog, &store, biggest, Default::default()).unwrap();
});
// What a confirm click costs: the grid's own crops handed back in.
let mut cells = identity::load_faces(&catalog, &store, biggest, Default::default()).unwrap();
time("load_faces (largest person, redraw)", 5, || {
let cut = cells
.drain(..)
.filter_map(|c| Some((c.face, c.crop?)))
.collect();
cells = identity::load_faces(&catalog, &store, biggest, cut).unwrap();
});
time("audit (coverage line)", 5, || {
dr_ui::faces::audit(&catalog, &store, MODEL_ID, &registry).unwrap();
+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]);
}
+11 -1
View File
@@ -260,7 +260,17 @@ pub fn refresh(
match (ctl.selected.get(), store.as_deref()) {
(Some(person), Some(store)) => {
let cells = identity::load_faces(cat, store, person).unwrap_or_else(|e| {
// The crops the grid is showing now, handed over to be reused
// rather than decoded again — see `load_faces`. Drained, not
// cloned: a crop is 64 KB of pixels and the largest groups hold
// hundreds.
let cut = ctl
.faces
.borrow_mut()
.drain(..)
.filter_map(|c| Some((c.face, c.crop?)))
.collect();
let cells = identity::load_faces(cat, store, person, cut).unwrap_or_else(|e| {
log::warn!("identity: reading faces: {e}");
Vec::new()
});