Files
Nathaniel LedfordandClaude Opus 5.5 2783e5de9b PERF-IMPORT: a scan, an import and a sidecar write no longer make a table of the whole library first
Setting up Synchronize Folder, an import review or an import made a table
of every photo in the library on the session's thread before the first
file was looked at, and naming one photo's sidecar worked out who owns
every shared sidecar name in the library. In a library of 3.5 million
photos (`scan_setup_bench`, release, a folder of 500): 5.8 s before a
Synchronize scan, 2.6 s before an import review, 1.7 s before an import,
2.4 s for each sidecar written.

- A scan keeps the library as it is (photos are shared, not copied) and
  looks up the files it finds (`library_files`) and the contents it probes
  (`library_hashes`) on its own thread.
- An import looks the library up with its first files
  (`ImportJob::look_up_library`), from the library and the options as they
  were when it was set up.
- The owners of shared sidecar names are worked out for the photos asked
  about (`StemOwners::among`): all five callers name only those photos.
- Synchronize Folder names its photos' sidecars in the scan, after it has
  seen that the folder is there.

Now 0.9 s, 0.05 s, 0.05 s and under 1 s. What is found, and how sidecars
are named, is unchanged: the tests hold the owners against the table of
the whole library as it was made before, and the key made without
allocating against the key, for every string of up to six of the
characters that decide it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 10:52:56 -04:00

64 lines
3.2 KiB
Rust

//! What a scan, an import and Synchronize Folder cost before they read the disk, and what
//! naming one photo's sidecar costs, by the size of the library (`N`, default 100,000 photos; the
//! folder looked at holds 500 of them).
//!
//! `N=3500000 cargo run --release -p lightcraft-engine --example scan_setup_bench`
use std::time::Instant;
use lightcraft_catalog::{Op, Photo, Source};
use lightcraft_engine::Session;
fn main() {
let n: u64 = std::env::var("N").ok().and_then(|v| v.parse().ok()).unwrap_or(100_000);
let mut s = Session::new();
let t = Instant::now();
for chunk in (0..n).collect::<Vec<_>>().chunks(10_000) {
for i in chunk {
let id = s.catalog.alloc_photo_id();
let name = format!("IMG_{i:08}.jpg");
let path = format!("/Volumes/Photos/Library/Archive/2019/2019-07 Summer/Trip to the coast/Day {:05}/extra/{name}", i / 500);
let mut p = Photo::new(id, Source::File { path }, &name, "JPEG", 4032, 3024, "2026-09-30T12:00:00");
p.file_size = 3_400_000 + i;
p.captured = Some(format!("2019-07-{:02}T10:{:02}:{:02}", 1 + i % 28, i % 60, (i / 60) % 60));
p.content_hash = Some(format!("{:032x}", i.wrapping_mul(0x9e37_79b9_7f4a_7c15)));
s.catalog.apply(Op::AddPhoto { photo: Box::new(p) }).unwrap();
}
}
eprintln!("{n} photos built in {:.2} s", t.elapsed().as_secs_f64());
let folder = "/Volumes/Photos/Library/Archive/2019/2019-07 Summer/Trip to the coast/Day 00000".to_string();
for run in 0..3 {
let ms = |t: Instant| t.elapsed().as_secs_f64() * 1e3;
let t = Instant::now();
let f = lightcraft_catalog::Filter { library_folder: Some(folder.clone()), ..Default::default() };
let ids = s.catalog.query(&f, &lightcraft_catalog::Sort::default());
let query = ms(t);
let t = Instant::now();
let namings = s.sidecar_namings(&ids);
let naming = ms(t);
let t = Instant::now();
let one = ids.first().map(|id| s.sidecar_naming(*id));
let naming_one = ms(t);
let t = Instant::now();
let (input, paths) = lightcraft_engine::import::ScanInput::new(&mut s, std::slice::from_ref(&folder));
let scan = ms(t);
// (the folder is not on this disk: the scan finds no file, and its cost is what it makes of the library)
let t = Instant::now();
let out = lightcraft_engine::import::scan_with(input, &paths, &Default::default());
let scanned = ms(t);
let t = Instant::now();
let job = lightcraft_engine::import::ImportJob::new(&mut s, Default::default());
let import = ms(t);
let t = Instant::now();
let sync = lightcraft_engine::sync::SyncInput::new(&mut s, &folder, false);
let whole = ms(t);
println!(
"N={n} run {run}: query {query:.0} ms ({} ids) | sidecar_namings {naming:.0} ms | sidecar_naming of one {naming_one:.0} ms ({one:?}) | ScanInput::new {scan:.0} ms + scan_with {scanned:.0} ms ({} found) | ImportJob::new {import:.0} ms (ok {}) | SyncInput::new {whole:.0} ms (ok {})",
ids.len(),
out.candidates.len(),
job.is_ok(),
sync.is_ok()
);
let _ = namings;
}
}