mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-25 19:00:31 +00:00
615d8a365e
stack_of, validate_stack and regular-album membership looked a photo up by scanning every stack or the album's list, so an "In a Stack" or "Album" smart album, or a query filtered to an album, cost photos x list length (2.7 s and 0.6-0.8 s at 100k photos). Derived indexes (never saved) answer them now: built on first use, kept current by the six ops that change stacks or album photos in apply_inner, so undo, failed-batch rollback and journal replay follow. A file with a photo in two stacks falls back to the scan. query() works out each sort key once (File Name was lower-cased twice per comparison) and tests `only` as a set; smart-album passes prepare the filter once; rules name flag, label and kind without formatting per photo. A property test checks the indexes against a scan through random op sequences, rollbacks, snapshots, replay, clones and undo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
93 lines
4.2 KiB
Rust
93 lines
4.2 KiB
Rust
//! What library queries cost at 100k photos (ignored:
|
||
//! `cargo test --release -p lightcraft-catalog --lib scale_100k -- --ignored --nocapture`).
|
||
|
||
use std::time::Instant;
|
||
|
||
use serde_json::json;
|
||
|
||
use super::*;
|
||
|
||
/// 100k photos, 10k two-photo stacks, a 20k-photo album, keywords and flags.
|
||
fn library(n: u64) -> (Catalog, AlbumId) {
|
||
let mut c = Catalog::new();
|
||
let mut ops: Vec<Op> = (0..n)
|
||
.map(|i| {
|
||
let mut p = Photo::new(
|
||
PhotoId(i + 1),
|
||
Source::Demo { scene: (i % 20) as u32 },
|
||
&format!("IMG_{:06}.jpg", mix64(i) % 1_000_000),
|
||
"JPEG",
|
||
6000,
|
||
4000,
|
||
"2026-01-01T00:00:00",
|
||
);
|
||
p.captured = Some(format!("20{:02}-{:02}-{:02}T{:02}:00:00", 10 + i % 16, 1 + i % 12, 1 + i % 28, i % 24));
|
||
p.rating = (i % 6) as u8;
|
||
p.flag = if i % 7 == 0 { Flag::Pick } else { Flag::None };
|
||
p.meta.keywords = vec![format!("kw{}", i % 500), "travel|italy".into()];
|
||
Op::AddPhoto { photo: Box::new(p) }
|
||
})
|
||
.collect();
|
||
ops.extend(
|
||
(0..n / 10)
|
||
.map(|s| Op::AddStack { stack: Stack { id: StackId(s + 1), photos: vec![PhotoId(s * 10 + 1), PhotoId(s * 10 + 2)], collapsed: false } }),
|
||
);
|
||
let album = AlbumId(1);
|
||
let mut a = Album::new(album, "Big");
|
||
a.photos = (0..n).step_by(5).map(|i| PhotoId(i + 1)).collect();
|
||
ops.push(Op::AddAlbum { album: a });
|
||
let t = Instant::now();
|
||
c.apply(Op::Batch { ops }).unwrap();
|
||
eprintln!("build {n} photos, {} stacks: {:.0} ms", n / 10, t.elapsed().as_secs_f64() * 1e3);
|
||
(c, album)
|
||
}
|
||
|
||
fn smart(c: &mut Catalog, id: u64, rules: serde_json::Value) -> AlbumId {
|
||
let mut a = Album::new(AlbumId(id), format!("Smart {id}"));
|
||
a.smart = Some(Box::new(Filter { rule_set: Some(serde_json::from_value(rules).unwrap()), ..Default::default() }));
|
||
c.apply(Op::AddAlbum { album: a }).unwrap();
|
||
AlbumId(id)
|
||
}
|
||
|
||
/// Best of five runs (the machine may be busy with other work).
|
||
fn time<T>(what: &str, f: impl Fn() -> T) -> T {
|
||
let mut best = f64::MAX;
|
||
let mut r = None;
|
||
for _ in 0..5 {
|
||
let t = Instant::now();
|
||
r = Some(f());
|
||
best = best.min(t.elapsed().as_secs_f64() * 1e3);
|
||
}
|
||
eprintln!("{what}: {best:.1} ms");
|
||
r.unwrap()
|
||
}
|
||
|
||
#[test]
|
||
#[ignore]
|
||
fn scale_100k() {
|
||
let (mut c, album) = library(100_000);
|
||
let all = Filter::default();
|
||
time("query, capture date", || c.query(&all, &Sort::default()));
|
||
time("query, file name", || c.query(&all, &Sort { key: SortKey::FileName, ..Default::default() }));
|
||
time("query, text", || c.query(&Filter { text: "kw42".into(), ..Default::default() }, &Sort::default()));
|
||
time("query, album of 20k", || c.query(&Filter { album: Some(album), ..Default::default() }, &Sort::default()));
|
||
let only: Vec<PhotoId> = (0..5000).map(|i| PhotoId(i * 7 + 1)).collect();
|
||
time("query, only 5k ids", || c.query(&Filter { only: only.clone(), ..Default::default() }, &Sort::default()));
|
||
let stacked = smart(&mut c, 10, json!({"match": "all", "rules": [{"field": "stacked", "op": "is", "value": true}]}));
|
||
let flagged = smart(
|
||
&mut c,
|
||
11,
|
||
json!({"match": "all", "rules": [{"field": "flag", "op": "is", "value": "pick"}, {"field": "kind", "op": "is", "value": "image"}]}),
|
||
);
|
||
let keyword = smart(&mut c, 12, json!({"match": "all", "rules": [{"field": "keywords", "op": "contains", "value": "kw42"}]}));
|
||
let in_album = smart(&mut c, 13, json!({"match": "all", "rules": [{"field": "album", "op": "is", "value": album.0}]}));
|
||
let recent = smart(&mut c, 14, json!({"match": "all", "rules": [{"field": "captureDate", "op": "inLast", "value": {"n": 2, "unit": "years"}}]}));
|
||
for (what, a) in [("stacked", stacked), ("flag + kind", flagged), ("keyword", keyword), ("in album", in_album), ("in the last 2 years", recent)] {
|
||
let n = time(&format!("smart album count, {what}"), || c.album_count(a));
|
||
eprintln!(" = {n}");
|
||
}
|
||
time("albums_of", || c.albums_of(PhotoId(6)));
|
||
time("arrange stacks", || c.arrange_stacks(&c.query(&all, &Sort::default())));
|
||
time("stack_of × 1000", || (0..1000u64).filter(|i| c.stack_of(PhotoId(i * 97 + 1)).is_some()).count());
|
||
}
|