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.
This commit is contained in:
2026-09-26 13:28:50 -04:00
parent d537e4a965
commit 87badb6f99
3 changed files with 79 additions and 5 deletions
+4 -3
View File
@@ -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),
+65
View File
@@ -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<i64> {
let c = cat.connection();
let mut stmt = c.prepare(&sql).unwrap();
let rows = stmt.query_map([], |r| r.get::<_, i64>(0)).unwrap();
rows.collect::<Result<Vec<_>, _>>().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
+10 -2
View File
@@ -349,9 +349,17 @@ pub(super) fn total_images_filtered(
filter: &RatingFilter,
) -> Result<usize, dr_catalog::CatalogError> {
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),
)?;