Measure what opening the catalog, a sync pass and a scan cost on a real library
Two benches for reading side by side before and after a change, against a copy of a real catalog, in the manner of identity_bench: - `dr-catalog --example catalog_bench CATALOG [FACES_DIR]` times `Catalog::open` and the backfill inside it step by step, the upload snapshot, a merge of the catalog with a copy of itself, and the face shard export and import in the steady state where nothing is new. - `persist_bench`, an ignored test in dr-ui's scan module because `persist` and `apply_judgement` are private to it, replays the catalog's own rows through `persist` (the largest folder, and the whole library) and looks up every `.drsc` sidecar the catalog has read. It works on a scratch copy and prints a fingerprint of what `persist` left, so two builds can be shown to agree. Both print best, median and CPU time; the CPU figure is the one to compare while other builds share the machine.
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
//! What the catalog's routine reads cost on a real library, off the GUI.
|
||||
//!
|
||||
//! cargo run --release -p dr-catalog --example catalog_bench -- CATALOG.sqlite [FACES_DIR]
|
||||
//!
|
||||
//! Times `Catalog::open` — which every worker thread pays, including the
|
||||
//! develop view's fetch of each original and each neighbour it prefetches —
|
||||
//! and the backfill that runs inside it, step by step. Run it against a
|
||||
//! *copy* of a real catalog: opening migrates and backfills, which write.
|
||||
//!
|
||||
//! The figures are for reading side by side before and after a change; they
|
||||
//! are not a gate. Compare the `cpu` column when the machine is busy.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use dr_catalog::{keywords, rating, schema, Catalog};
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let Some(path) = args.first().map(PathBuf::from) else {
|
||||
eprintln!("usage: catalog_bench CATALOG.sqlite");
|
||||
std::process::exit(2);
|
||||
};
|
||||
|
||||
// Once untimed, so a migration or a first backfill is not in the figures.
|
||||
drop(Catalog::open(&path).expect("catalog"));
|
||||
|
||||
time("Catalog::open", 20, || {
|
||||
drop(Catalog::open(&path).unwrap());
|
||||
});
|
||||
|
||||
let catalog = Catalog::open(&path).unwrap();
|
||||
let conn = catalog.connection();
|
||||
time("schema::backfill (all steps)", 20, || {
|
||||
schema::backfill(conn).unwrap();
|
||||
});
|
||||
time(" rating::ensure_default_versions", 20, || {
|
||||
rating::ensure_default_versions(conn).unwrap();
|
||||
});
|
||||
time(" rating::align_default_version_uuids", 20, || {
|
||||
rating::align_default_version_uuids(conn).unwrap();
|
||||
});
|
||||
time(" keywords::adopt_orphan_terms", 20, || {
|
||||
keywords::adopt_orphan_terms(conn).unwrap();
|
||||
});
|
||||
|
||||
// A sync pass: the upload snapshot, then a merge of the catalog with a
|
||||
// copy of itself — every row a match, which is the steady state.
|
||||
let scratch = path.with_extension("bench-snapshot");
|
||||
time("snapshot_for_upload", 3, || {
|
||||
let _ = std::fs::remove_file(&scratch);
|
||||
catalog.snapshot_for_upload(&scratch).unwrap();
|
||||
});
|
||||
println!(
|
||||
" snapshot size {:.1} MB",
|
||||
std::fs::metadata(&scratch).map(|m| m.len()).unwrap_or(0) as f64 / 1e6
|
||||
);
|
||||
let remote = path.with_extension("bench-remote");
|
||||
let _ = std::fs::remove_file(&remote);
|
||||
conn.execute("VACUUM INTO ?1", [remote.to_string_lossy().as_ref()])
|
||||
.unwrap();
|
||||
time("merge_remote_catalog (self)", 5, || {
|
||||
catalog.merge_remote_catalog(&remote).unwrap();
|
||||
});
|
||||
let _ = std::fs::remove_file(&scratch);
|
||||
let _ = std::fs::remove_file(&remote);
|
||||
|
||||
// The face half of a sync pass, against a copy of the face store: both
|
||||
// directions in the steady state, where nothing is new either way.
|
||||
if let Some(faces) = args.get(1).map(PathBuf::from) {
|
||||
let model = "scrfd_10g+w600k_mbf";
|
||||
let mut store = dr_catalog::FaceShardStore::open(&faces).unwrap();
|
||||
println!(
|
||||
" first export sent {}, first import adopted {}",
|
||||
dr_catalog::face_shard::export_to_shards(conn, &mut store, model).unwrap(),
|
||||
dr_catalog::face_shard::import_from_shards(conn, &store, model).unwrap()
|
||||
);
|
||||
time("face_shard::export_to_shards (steady)", 5, || {
|
||||
dr_catalog::face_shard::export_to_shards(conn, &mut store, model).unwrap();
|
||||
});
|
||||
time("face_shard::import_from_shards (steady)", 5, || {
|
||||
dr_catalog::face_shard::import_from_shards(conn, &store, model).unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Run `f` a few times and print the best wall-clock, the median, and the
|
||||
/// best CPU time — the figure to compare across runs on a busy machine.
|
||||
fn time(label: &str, runs: usize, mut f: impl FnMut()) {
|
||||
let mut wall: Vec<Duration> = Vec::with_capacity(runs);
|
||||
let mut cpu: Vec<Duration> = Vec::with_capacity(runs);
|
||||
for _ in 0..runs {
|
||||
let c = cpu_now();
|
||||
let t = Instant::now();
|
||||
f();
|
||||
wall.push(t.elapsed());
|
||||
cpu.push(cpu_now().saturating_sub(c));
|
||||
}
|
||||
wall.sort();
|
||||
cpu.sort();
|
||||
println!(
|
||||
"{label:42} best {:8.2} ms median {:8.2} ms cpu {:8.2} ms",
|
||||
wall[0].as_secs_f64() * 1e3,
|
||||
wall[runs / 2].as_secs_f64() * 1e3,
|
||||
cpu[0].as_secs_f64() * 1e3
|
||||
);
|
||||
}
|
||||
|
||||
/// This thread's time on a CPU so far, from `/proc/self/schedstat`; zero where
|
||||
/// the file is missing, which only makes the CPU column useless.
|
||||
fn cpu_now() -> Duration {
|
||||
std::fs::read_to_string("/proc/self/schedstat")
|
||||
.ok()
|
||||
.and_then(|s| s.split_whitespace().next()?.parse::<u64>().ok())
|
||||
.map(Duration::from_nanos)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
Reference in New Issue
Block a user