Files
Oxicloud/examples/bench_round14_queries.rs
T
2026-07-19 17:04:24 +02:00

259 lines
9.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Round-14 query-shape pack (needs the dev Postgres up; reads DATABASE_URL
//! from `.env`).
//!
//! Each section is BEFORE (verbatim replica of the shipped query shape) vs
//! AFTER (proposed shape), with an equivalence/safety gate and a `GATE FAIL`
//! rollback check — an AFTER that doesn't beat its BEFORE exits non-zero.
//!
//! [Q1] Lightbox face boxes — `faces_for_file`'s 10-column row (incl. the
//! 2,048-byte `embedding` BYTEA, decoded into a `Vec<f32>` per face)
//! hydrated for a group photo, then filtered `user_id == caller` in
//! Rust, vs a narrow `SELECT id, person_id, bbox … WHERE file_id = $1
//! AND user_id = $2` (embedding + 6 unused columns dropped; the caller
//! filter pushed into SQL). The only consumer, `people_service::
//! faces_for_file`, builds `FaceBoxDto { id, person_id, x,y,w,h }`.
//!
//! Run:
//! cargo run --release --features bench --example bench_round14_queries
//! Tunables (env): BENCH_PASSES (200), BENCH_FACES_PER_FILE (15)
use std::env;
use std::time::Instant;
use sqlx::postgres::PgPoolOptions;
use sqlx::{PgPool, Row};
use uuid::Uuid;
fn env_or<T: std::str::FromStr>(key: &str, default: T) -> T {
env::var(key)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
fn stats(mut s: Vec<f64>) -> (f64, f64, f64) {
s.sort_by(|a, b| a.partial_cmp(b).unwrap());
let n = s.len();
(
s.iter().sum::<f64>() / n as f64,
s[n / 2],
s[((n as f64 * 0.95) as usize).min(n - 1)],
)
}
/// Mirror of `face_pg_repository::bytes_to_embedding` — the per-face
/// `Vec<f32>` decode the BEFORE path pays for a column it never reads.
fn bytes_to_embedding(b: &[u8]) -> Vec<f32> {
b.chunks_exact(4)
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect()
}
// ────────────────────────────────────────────────────────────────────────────
// [Q1] Lightbox face boxes — wide row (incl. embedding) vs narrow projection
// ────────────────────────────────────────────────────────────────────────────
/// BEFORE, verbatim `faces_for_file` + `people_service::faces_for_file`:
/// hydrate the full 10-column row (decoding the 2 KiB embedding like
/// `row_to_face`), then filter `user_id == caller` in Rust and keep only
/// `(id, person_id, bbox)`.
async fn boxes_before(
pool: &PgPool,
file_id: Uuid,
caller: Uuid,
) -> Vec<(Uuid, Option<Uuid>, Vec<f32>)> {
let rows = sqlx::query(
"SELECT id, file_id, user_id, person_id, bbox, det_score, quality, embedding, blob_hash, created_at
FROM faces.faces WHERE file_id = $1",
)
.bind(file_id)
.fetch_all(pool)
.await
.expect("faces wide");
rows.into_iter()
.filter_map(|r| {
let user_id: Uuid = r.get("user_id");
// Decode the embedding exactly as `row_to_face` does (the cost the
// BEFORE path pays even though `FaceBoxDto` never reads it).
let emb_bytes: Vec<u8> = r.get("embedding");
let _embedding = bytes_to_embedding(&emb_bytes);
if user_id != caller {
return None;
}
let bbox: Vec<f32> = r.get("bbox");
Some((r.get("id"), r.get("person_id"), bbox))
})
.collect()
}
/// AFTER: narrow projection, caller filter in SQL.
async fn boxes_after(
pool: &PgPool,
file_id: Uuid,
caller: Uuid,
) -> Vec<(Uuid, Option<Uuid>, Vec<f32>)> {
let rows = sqlx::query(
"SELECT id, person_id, bbox FROM faces.faces WHERE file_id = $1 AND user_id = $2",
)
.bind(file_id)
.bind(caller)
.fetch_all(pool)
.await
.expect("faces narrow");
rows.into_iter()
.map(|r| {
let bbox: Vec<f32> = r.get("bbox");
(r.get("id"), r.get("person_id"), bbox)
})
.collect()
}
async fn section_face_boxes(pool: &PgPool) {
let n: usize = env_or("BENCH_FACES_PER_FILE", 15);
let passes: usize = env_or("BENCH_PASSES", 200);
// Seed: user + drive + folder + one photo file + N faces on it.
let mut tx = pool.begin().await.expect("begin");
let user_id: Uuid = sqlx::query_scalar(
"INSERT INTO auth.users (username, email, role)
VALUES ('bench14_faces', 'bench14_faces@bench.invalid', 'user') RETURNING id",
)
.fetch_one(&mut *tx)
.await
.expect("user");
let drive_id: Uuid = sqlx::query_scalar(
"INSERT INTO storage.drives (kind, default_for_user) VALUES ('personal', $1) RETURNING id",
)
.bind(user_id)
.fetch_one(&mut *tx)
.await
.expect("drive");
let folder_id: Uuid = sqlx::query_scalar(
"INSERT INTO storage.folders (name, path, lpath, drive_id)
VALUES ('bench14', '/bench14', 'bench14', $1) RETURNING id",
)
.bind(drive_id)
.fetch_one(&mut *tx)
.await
.expect("folder");
sqlx::query("UPDATE storage.drives SET root_folder_id = $1 WHERE id = $2")
.bind(folder_id)
.bind(drive_id)
.execute(&mut *tx)
.await
.expect("stamp root");
let file_id: Uuid = sqlx::query_scalar(
"INSERT INTO storage.files (name, folder_id, blob_hash, size, mime_type, drive_id)
VALUES ('group.jpg', $1, 'bench14blob00000000000000000000000000000000000000000000000000', 1024, 'image/jpeg', $2)
RETURNING id",
)
.bind(folder_id)
.bind(drive_id)
.fetch_one(&mut *tx)
.await
.expect("file");
tx.commit().await.expect("commit");
let person_id: Uuid = sqlx::query_scalar(
"INSERT INTO faces.persons (user_id, display_name) VALUES ($1, 'P') RETURNING id",
)
.bind(user_id)
.fetch_one(pool)
.await
.expect("person");
let embedding = vec![7u8; 2048]; // 512 × f32, like the real thing
for i in 0..n {
// Half the faces are named, half unassigned — exercises Option<Uuid>.
let pid = if i % 2 == 0 { Some(person_id) } else { None };
sqlx::query(
"INSERT INTO faces.faces
(file_id, user_id, person_id, bbox, det_score, quality, embedding, blob_hash)
VALUES ($1, $2, $3, ARRAY[0.1,0.2,0.3,0.4]::real[], 0.99, 0.9, $4, NULL)",
)
.bind(file_id)
.bind(user_id)
.bind(pid)
.bind(&embedding)
.execute(pool)
.await
.expect("face");
}
sqlx::query("ANALYZE faces.faces").execute(pool).await.ok();
// Equivalence gate: same (id, person_id, bbox) set both ways, all N present.
let mut b = boxes_before(pool, file_id, user_id).await;
let mut a = boxes_after(pool, file_id, user_id).await;
b.sort_by_key(|x| x.0);
a.sort_by_key(|x| x.0);
assert_eq!(b, a, "face-box projections differ");
assert_eq!(a.len(), n, "expected all faces");
println!("# [Q1] gate: wide/narrow face-box sets identical ({n} faces) — OK");
let mut wide = Vec::with_capacity(passes);
for _ in 0..passes {
let t = Instant::now();
std::hint::black_box(boxes_before(pool, file_id, user_id).await);
wide.push(t.elapsed().as_secs_f64() * 1e3);
}
let mut narrow = Vec::with_capacity(passes);
for _ in 0..passes {
let t = Instant::now();
std::hint::black_box(boxes_after(pool, file_id, user_id).await);
narrow.push(t.elapsed().as_secs_f64() * 1e3);
}
let (wm, wp50, wp95) = stats(wide);
let (nm, np50, np95) = stats(narrow);
let wire_before = n * (2048 + 16 * 4 + 24); // embedding + uuids/bbox + row overhead
let wire_after = n * (16 + 16 + 16 + 8);
println!("\n## [Q1] Lightbox face boxes — group photo, {n} faces");
println!("| arm | mean ms | p50 ms | p95 ms | ~bytes/req |");
println!(
"| BEFORE wide row (incl. embedding) | {wm:>7.3} | {wp50:>6.3} | {wp95:>6.3} | {wire_before:>9} |"
);
println!(
"| AFTER narrow (id,person,bbox) | {nm:>7.3} | {np50:>6.3} | {np95:>6.3} | {wire_after:>9} |"
);
println!(
"# {:.2}x faster; ~{} KiB embedding/columns off the wire per lightbox open (scales with face count)",
wm / nm,
(wire_before - wire_after) / 1024
);
// Cleanup (cascades faces + persons via FKs on drive/user delete).
sqlx::query("DELETE FROM storage.drives WHERE id = $1")
.bind(drive_id)
.execute(pool)
.await
.ok();
sqlx::query("DELETE FROM auth.users WHERE id = $1")
.bind(user_id)
.execute(pool)
.await
.ok();
if nm >= wm {
eprintln!("GATE FAIL [Q1]: narrow projection not faster — rollback");
std::process::exit(1);
}
}
#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
async fn main() {
let _ = dotenvy::dotenv();
let url = std::env::var("DATABASE_URL").expect("DATABASE_URL required (see .env)");
let pool = PgPoolOptions::new()
.max_connections(8)
.connect(&url)
.await
.expect("connect");
println!("#################################################################");
println!("# Round-14 query-shape pack");
println!("#################################################################");
section_face_boxes(&pool).await;
println!("\nGATE PASS (all sections)");
}