From 87badb6f99c956b9fc303410b7e96e7330903039 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sat, 26 Sep 2026 13:13:15 -0400 Subject: [PATCH] Count the grid by subtracting the hidden burst frames, not probing per image The grid's total is read on every scroll reload (`load_window` compares it to notice a delete). On the reference library it cost 1.3-1.5 ms best-of-50 by catalog_bench, 2-3.6 ms on a busy machine, and the issue measured 4 ms. `uncollapsed` asked every visible image whether a collapsed burst stands in for it -- two primary-key probes per image, 19,000 times, on a library with no bursts at all: SCAN i USING INDEX images_grid_order CORRELATED SCALAR SUBQUERY SEARCH bm USING INTEGER PRIMARY KEY (rowid=?) CORRELATED SCALAR SUBQUERY SEARCH be USING INTEGER PRIMARY KEY (rowid=?) `total_images_filtered` now counts what the filter keeps and subtracts the frames `bursts::collapsed_away_frames` lists, under the same filter: SCALAR SUBQUERY: SCAN i USING INDEX images_grid_order SCALAR SUBQUERY: SCAN bm; SEARCH be ...; SEARCH i USING INTEGER PRIMARY KEY The second half walks only `burst_members`. Each image is in it at most once (it is the key), and the filter is applied to both halves, so the subtraction removes exactly the rows the predicate used to drop. The new fragment sits beside `not_collapsed_away` in bursts.rs, and a test holds the two to the same rows with bursts open and closed. After: 0.3 ms, the same count (19,152). The cells query keeps the predicate: it is a window with a LIMIT and needs the rows, not their number. The rated grid count (3.5-4 ms with a one-star filter) is unchanged: its cost is the rating subquery per image, and changing how `RatingFilter` spells it changes every grid and timeline query, which is left for its own change. --- core/dr-catalog/examples/catalog_bench.rs | 7 +-- core/dr-catalog/src/bursts.rs | 65 +++++++++++++++++++++++ ui/dr-ui/src/library/spans.rs | 12 ++++- 3 files changed, 79 insertions(+), 5 deletions(-) diff --git a/core/dr-catalog/examples/catalog_bench.rs b/core/dr-catalog/examples/catalog_bench.rs index 3f94f21..fda9265 100644 --- a/core/dr-catalog/examples/catalog_bench.rs +++ b/core/dr-catalog/examples/catalog_bench.rs @@ -170,11 +170,12 @@ const RATED_AT_LEAST_ONE: &str = " AND coalesce((SELECT dv.rating FROM versions /// `dr_ui::library::total_images_filtered`, spelled as it is there. fn grid_count(conn: &rusqlite::Connection, rated: &str) -> i64 { - let folded = dr_catalog::bursts::not_collapsed_away("i"); + let visible = "i.shadowed_by IS NULL AND i.trashed_at IS NULL"; + let hidden = dr_catalog::bursts::collapsed_away_frames("i"); conn.query_row( &format!( - "SELECT count(*) FROM images i - WHERE i.shadowed_by IS NULL AND i.trashed_at IS NULL{rated} AND {folded}" + "SELECT (SELECT count(*) FROM images i WHERE {visible}{rated}) + - (SELECT count(*) FROM {hidden} AND {visible}{rated})" ), [], |r| r.get(0), diff --git a/core/dr-catalog/src/bursts.rs b/core/dr-catalog/src/bursts.rs index 3c6ce17..a0f85f4 100644 --- a/core/dr-catalog/src/bursts.rs +++ b/core/dr-catalog/src/bursts.rs @@ -722,6 +722,31 @@ pub fn not_collapsed_away(image: &str) -> String { ) } +/// SQL for the rows [`not_collapsed_away`] drops, as a `FROM ... WHERE` +/// joining each such frame to its image under the alias `image`. +/// +/// For counting. A count that applies [`not_collapsed_away`] to every row +/// pays two primary-key probes per image to find the handful a collapsed +/// burst hides; counting everything and subtracting what this lists walks +/// only `burst_members`, which is empty on a library without bursts. The +/// caller appends its own conditions on `image` with `AND`, the same ones +/// it counted the whole with, so the subtraction takes away only rows the +/// whole included. `image_id` is `burst_members`' key, so no image is +/// listed twice. +/// +/// The two must describe the same rows: change one, change both, and +/// `the_collapsed_frames_are_what_the_predicate_drops` will say if they drift. +/// +/// Never interpolate anything user-supplied as `image`. +pub fn collapsed_away_frames(image: &str) -> String { + format!( + "burst_members bm CROSS JOIN images {image} ON {image}.id = bm.image_id + WHERE bm.representative = 0 + AND NOT EXISTS (SELECT 1 FROM burst_expanded be + WHERE be.burst_id = bm.burst_id)" + ) +} + #[cfg(test)] mod tests { use super::*; @@ -1235,6 +1260,46 @@ mod tests { assert_eq!(visible(cat.connection()), vec![1, 2, 3, 4]); } + #[test] + fn the_collapsed_frames_are_what_the_predicate_drops() { + // `collapsed_away_frames` is `not_collapsed_away` turned inside out + // for counting; the two must name the same rows, open or closed. + let cat = seeded(&[ + (1, 1000, Some(0xFF00)), + (2, 1001, Some(0xFF00)), + (3, 1002, Some(0xFF00)), + (4, 9000, Some(0xAA00)), + (5, 9001, Some(0xAA00)), + (6, 20000, Some(0xFF00)), + ]); + regroup(cat.connection(), Rules::default()).unwrap(); + let ids = |sql: String| -> Vec { + let c = cat.connection(); + let mut stmt = c.prepare(&sql).unwrap(); + let rows = stmt.query_map([], |r| r.get::<_, i64>(0)).unwrap(); + rows.collect::, _>>().unwrap() + }; + let dropped = || { + ids(format!( + "SELECT id FROM images i WHERE NOT {} ORDER BY id", + not_collapsed_away("i") + )) + }; + let listed = || { + ids(format!( + "SELECT i.id FROM {} ORDER BY i.id", + collapsed_away_frames("i") + )) + }; + + assert_eq!(listed(), dropped()); + set_expanded(cat.connection(), ImageId(1), false).unwrap(); + assert_eq!(dropped(), vec![2, 3]); + assert_eq!(listed(), dropped()); + set_expanded(cat.connection(), ImageId(4), false).unwrap(); + assert_eq!(listed(), dropped()); + } + #[test] fn a_library_with_no_bursts_hides_nothing() { // The predicate is in every grid query, so its cost and its effect on a diff --git a/ui/dr-ui/src/library/spans.rs b/ui/dr-ui/src/library/spans.rs index c6e2aac..38e104e 100644 --- a/ui/dr-ui/src/library/spans.rs +++ b/ui/dr-ui/src/library/spans.rs @@ -349,9 +349,17 @@ pub(super) fn total_images_filtered( filter: &RatingFilter, ) -> Result { let rated = filter.sql(); - let folded = uncollapsed("i"); + // Everything the filter keeps, less the frames a collapsed burst stands + // in for -- rather than `uncollapsed` asked of every image. Paid on + // every scroll reload: the predicate probed `burst_members` and + // `burst_expanded` once per visible image, 19,000 times on the reference + // library, to hide none. The subtraction walks only the burst members. + let hidden = dr_catalog::bursts::collapsed_away_frames("i"); let n: i64 = catalog.connection().query_row( - &format!("SELECT count(*) FROM images i WHERE {VISIBLE}{rated}{folded}"), + &format!( + "SELECT (SELECT count(*) FROM images i WHERE {VISIBLE}{rated}) + - (SELECT count(*) FROM {hidden} AND {VISIBLE}{rated})" + ), [], |r| r.get(0), )?;