use std::time::{Duration, Instant};
use super::ann::{bench_params, synth_query, Sq8Nsw};
use super::engine::EngineKind;
use super::hnsw::{brute_force_topk, recall_at_k, DEFAULT_EF_SEARCH};
use super::memory::{plan_under_2gb_cgroup, LOCAL_SURVIVAL_CAP_BYTES};
use super::simd::{detect_simd, dot_i8, SimdKind};
use super::tier2::{quantize_sq8, Sq8Cache};
#[derive(Debug, Clone)]
pub struct QueryBenchResult {
pub n_vectors: usize,
pub p95: Duration,
pub simd: SimdKind,
}
pub const TARGET_P95_MS: u128 = 50;
pub const BENCH_Q_CORPUS: usize = 1_000_000;
pub fn bench_query_p95(cache: &Sq8Cache, query: &[i8], iters: usize) -> QueryBenchResult {
let simd = detect_simd();
let dim = cache.dim();
let data = cache.as_bytes();
let mut samples = Vec::with_capacity(iters);
for _ in 0..iters {
let t0 = Instant::now();
let mut best = i32::MIN;
for row in data.chunks_exact(dim) {
best = best.max(dot_i8(row, query, simd));
}
let _ = best;
samples.push(t0.elapsed());
}
samples.sort();
let idx = ((samples.len() as f64) * 0.95).floor() as usize;
let p95 = samples[idx.min(samples.len().saturating_sub(1))];
QueryBenchResult {
n_vectors: cache.len(),
p95,
simd,
}
}
pub fn bench_ann_query_p95(graph: &Sq8Nsw, query: &[i8], iters: usize) -> QueryBenchResult {
let ef = graph.params().ef_search.max(DEFAULT_EF_SEARCH);
let k = 10.min(graph.len().max(1));
let mut samples = Vec::with_capacity(iters);
for _ in 0..iters {
let t0 = Instant::now();
let hits = graph.search(query, k, ef);
let _ = hits;
samples.push(t0.elapsed());
}
samples.sort();
let idx = ((samples.len() as f64) * 0.95).floor() as usize;
let p95 = samples[idx.min(samples.len().saturating_sub(1))];
QueryBenchResult {
n_vectors: graph.len(),
p95,
simd: detect_simd(),
}
}
pub fn bench_ann_p95_at(n: usize, dim: usize, iters: usize) -> QueryBenchResult {
let graph = Sq8Nsw::synth_for_bench(n, dim, bench_params());
let query = synth_query(dim, 42);
bench_ann_query_p95(&graph, &query, iters)
}
pub fn ann_p95_meets_1m_floor(iters: usize) -> (QueryBenchResult, bool) {
let res = bench_ann_p95_at(BENCH_Q_CORPUS, super::engine::DEFAULT_VECTOR_DIM, iters);
let ok = res.p95.as_millis() < TARGET_P95_MS;
(res, ok)
}
pub fn synth_sq8_cache(n: usize, dim: usize) -> (Sq8Cache, Vec<i8>) {
let mut cache = Sq8Cache::new(dim);
let mut query_fp = vec![0.0f32; dim];
for (i, slot) in query_fp.iter_mut().enumerate() {
*slot = ((i % 7) as f32) * 0.1 - 0.3;
}
let (query, _) = quantize_sq8(&query_fp);
for id in 0..n as u64 {
let mut fp = vec![0.0f32; dim];
for (j, slot) in fp.iter_mut().enumerate() {
*slot = (((id as usize + j) % 11) as f32) * 0.05 - 0.2;
}
cache.push_fp32(id, &fp).unwrap();
}
(cache, query)
}
pub fn io_reduction_vs_mmap(n_vectors: usize, pages_touched_hot: usize) -> f64 {
if n_vectors == 0 {
return 1.0;
}
1.0 - (pages_touched_hot as f64 / n_vectors as f64)
}
pub const TARGET_IO_REDUCTION: f64 = 0.80;
#[derive(Debug, Clone, Copy)]
pub struct IoReductionReport {
pub n_vectors: usize,
pub legacy_disk_touches: usize,
pub hot_disk_touches: usize,
pub reduction: f64,
}
impl IoReductionReport {
pub fn meets_floor(self) -> bool {
self.reduction >= TARGET_IO_REDUCTION
}
}
pub fn measure_io_reduction(n_vectors: usize, k_post_filter: usize) -> IoReductionReport {
let legacy_disk_touches = n_vectors;
let hot_disk_touches = k_post_filter.min(n_vectors);
let reduction = io_reduction_vs_mmap(legacy_disk_touches, hot_disk_touches);
IoReductionReport {
n_vectors,
legacy_disk_touches,
hot_disk_touches,
reduction,
}
}
pub fn recall_sq8_vs_fp32(fp32_rows: &[(u64, Vec<f32>)], query: &[f32], k: usize) -> f32 {
let fp_scores: Vec<(u64, f32)> = fp32_rows
.iter()
.map(|(id, v)| {
let s: f32 = v.iter().zip(query.iter()).map(|(a, b)| a * b).sum();
(*id, s)
})
.collect();
let truth = brute_force_topk(&fp_scores, k);
let (q8, _) = quantize_sq8(query);
let sq_scores: Vec<(u64, f32)> = fp32_rows
.iter()
.map(|(id, v)| {
let (row, _) = quantize_sq8(v);
let s = dot_i8(&row, &q8, SimdKind::Scalar) as f32;
(*id, s)
})
.collect();
let approx = brute_force_topk(&sq_scores, k);
recall_at_k(&truth, &approx)
}
pub const TARGET_RECALL: f32 = 0.90;
pub fn recall_sq8_at_ef_search(n: usize, dim: usize, ef_search: usize) -> f32 {
let k = ef_search.min(n).max(1);
let mut rows = Vec::with_capacity(n);
for id in 0..n as u64 {
let mut v = vec![0.0f32; dim];
let peak = (id as usize) % dim;
v[peak] = 1.0;
v[(peak + 3) % dim] = 0.05;
rows.push((id, v));
}
let mut sum = 0.0f32;
let trials = 8usize;
for t in 0..trials {
let mut q = vec![0.0f32; dim];
let peak = (t * 7) % dim;
q[peak] = 1.0;
sum += recall_sq8_vs_fp32(&rows, &q, k.min(10));
}
sum / trials as f32
}
pub fn recall_meets_ef50_floor() -> (f32, bool) {
let r = recall_sq8_at_ef_search(256, 32, DEFAULT_EF_SEARCH);
(r, r >= TARGET_RECALL)
}
pub fn oom_plan_within_cap() -> bool {
let plan = plan_under_2gb_cgroup(EngineKind::Local);
plan.block_cache_bytes <= LOCAL_SURVIVAL_CAP_BYTES
&& plan.survival_cap_bytes <= LOCAL_SURVIVAL_CAP_BYTES
}
pub fn estimate_local_engine_heap_bytes(n_vectors: usize, dim: usize, m: usize) -> u64 {
let sq8 = (n_vectors as u64).saturating_mul(dim as u64);
let ids = (n_vectors as u64).saturating_mul(8);
let scales = (n_vectors as u64).saturating_mul(4);
let adjacency = (n_vectors as u64)
.saturating_mul(m as u64)
.saturating_mul(8);
let block_cache = plan_under_2gb_cgroup(EngineKind::Local).block_cache_bytes;
sq8 + ids + scales + adjacency + block_cache
}
pub fn oom_1m_corpus_within_2gb() -> (u64, bool) {
let bytes =
estimate_local_engine_heap_bytes(BENCH_Q_CORPUS, super::engine::DEFAULT_VECTOR_DIM, 16);
(
bytes,
bytes <= LOCAL_SURVIVAL_CAP_BYTES && oom_plan_within_cap(),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vector_engine::ann::{bench_params, synth_query, Sq8Nsw};
use crate::vector_engine::engine::DEFAULT_VECTOR_DIM;
use crate::vector_engine::hnsw::DEFAULT_EF_SEARCH;
#[test]
fn bench_query_small_n_completes() {
let (cache, query) = synth_sq8_cache(200, DEFAULT_VECTOR_DIM);
let res = bench_query_p95(&cache, &query, 20);
assert_eq!(res.n_vectors, 200);
assert!(res.p95 < Duration::from_secs(1));
let _ = TARGET_P95_MS;
}
#[test]
fn ann_query_mid_scale_under_p95_floor() {
let res = bench_ann_p95_at(20_000, DEFAULT_VECTOR_DIM, 30);
assert_eq!(res.n_vectors, 20_000);
assert!(
res.p95.as_millis() < TARGET_P95_MS,
"ANN P95 {}ms exceeds {}ms at 20k",
res.p95.as_millis(),
TARGET_P95_MS
);
}
#[test]
#[ignore = "full 1M corpus; run explicitly for FR-VE-BENCH-Q sign-off"]
fn ann_p95_1m_under_floor() {
let (res, ok) = ann_p95_meets_1m_floor(40);
eprintln!(
"[FR-VE-BENCH-Q] n={} P95={:.3}ms simd={:?} ok={}",
res.n_vectors,
res.p95.as_secs_f64() * 1000.0,
res.simd,
ok
);
assert!(
ok,
"P95 {}ms exceeds {}ms at 1M",
res.p95.as_millis(),
TARGET_P95_MS
);
}
#[test]
fn io_reduction_meets_floor_when_hot_path_ram_only() {
let report = measure_io_reduction(1_000_000, 10);
assert!(
report.meets_floor(),
"I/O reduction {:.1}% below floor",
report.reduction * 100.0
);
assert_eq!(report.legacy_disk_touches, 1_000_000);
assert_eq!(report.hot_disk_touches, 10);
}
#[test]
fn recall_sq8_exceeds_90_at_ef_search_50() {
let (r, ok) = recall_meets_ef50_floor();
assert!(
ok,
"SQ8 recall@efSearch=50 was {r:.3}, need >{TARGET_RECALL}"
);
}
#[test]
fn oom_2gb_cgroup_plan_safe() {
assert!(oom_plan_within_cap());
let (bytes, ok) = oom_1m_corpus_within_2gb();
assert!(
ok,
"estimated 1M LocalEngine heap {bytes} exceeds 2GB survival cap"
);
}
#[test]
#[ignore = "allocates ~400MB+; run explicitly for OOM sign-off"]
fn oom_live_1m_alloc_under_2gb() {
use sysinfo::{Pid, System};
let mut sys = System::new();
sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
let pid = Pid::from_u32(std::process::id());
let before = sys.process(pid).map(|p| p.memory()).unwrap_or(0);
let graph = Sq8Nsw::synth_for_bench(BENCH_Q_CORPUS, DEFAULT_VECTOR_DIM, bench_params());
assert_eq!(graph.len(), BENCH_Q_CORPUS);
let _ = graph.search(&synth_query(DEFAULT_VECTOR_DIM, 1), 10, DEFAULT_EF_SEARCH);
sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
let after = sys.process(pid).map(|p| p.memory()).unwrap_or(0);
let delta = after.saturating_sub(before);
eprintln!(
"[FR-VE-BENCH-OOM] rss_before={before} rss_after={after} delta={delta} cap={}",
LOCAL_SURVIVAL_CAP_BYTES
);
assert!(
after <= LOCAL_SURVIVAL_CAP_BYTES,
"RSS {after} exceeds 2GB survival cap"
);
let _ = delta;
}
}