#![cfg(not(debug_assertions))]
#![allow(
clippy::cast_lossless,
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::doc_markdown,
clippy::useless_conversion,
clippy::similar_names,
clippy::unreadable_literal,
clippy::items_after_statements
)]
use std::sync::{Mutex, MutexGuard, OnceLock};
use std::thread;
use std::time::{Duration, Instant};
#[cfg(unix)]
#[allow(unsafe_code)]
fn thread_cpu_now() -> Duration {
let mut ts = libc::timespec {
tv_sec: 0,
tv_nsec: 0,
};
let rc = unsafe { libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, &raw mut ts) };
assert_eq!(rc, 0, "clock_gettime(CLOCK_THREAD_CPUTIME_ID) failed");
Duration::new(ts.tv_sec as u64, ts.tv_nsec as u32)
}
#[cfg(not(unix))]
fn thread_cpu_now() -> Duration {
static START: OnceLock<Instant> = OnceLock::new();
START.get_or_init(Instant::now).elapsed()
}
fn perf_lock() -> MutexGuard<'static, ()> {
static L: OnceLock<Mutex<()>> = OnceLock::new();
let guard = L
.get_or_init(Mutex::default)
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
thread::sleep(Duration::from_millis(500));
guard
}
use spg_storage::{
BloomFilter, Catalog, ColumnSchema, DataType, NSW_DEFAULT_M, NswMetric, Row,
SEGMENT_PAGE_BYTES, SegmentReader, TableSchema, Value, VecEncoding, encode_segment, nsw_query,
persistent::PersistentVec,
persistent_btree::PersistentBTreeMap,
quantize::{Sq8Vector, quantize, sq8_l2_distance_sq, sq8_l2_distance_sq_asymmetric},
};
fn build_catalog(n_rows: i32) -> Catalog {
let mut cat = Catalog::new();
cat.create_table(TableSchema::new(
"users",
vec![
ColumnSchema::new("id", DataType::Int, false),
ColumnSchema::new("name", DataType::Text, false),
ColumnSchema::new("score", DataType::Float, true),
],
))
.unwrap();
let t = cat.get_mut("users").unwrap();
for i in 0..n_rows {
t.insert(Row::new(vec![
Value::Int(i),
Value::text(format!("user-{i}")),
Value::Float(f64::from(i) * 0.1),
]))
.unwrap();
}
cat
}
#[test]
fn catalog_roundtrip_100rows_under_budget() {
let _perf = perf_lock();
let cat = build_catalog(100);
let bytes = cat.serialize();
let iters: u32 = 100;
let start = Instant::now();
for _ in 0..iters {
let out = cat.serialize();
let restored = Catalog::deserialize(std::hint::black_box(&out)).expect("deserialize ok");
std::hint::black_box(restored);
}
let mean_secs = start.elapsed().as_secs_f64() / f64::from(iters);
let budget_secs = 5e-3;
assert!(
mean_secs < budget_secs,
"catalog_roundtrip_100rows mean {mean_secs:.6} s exceeds budget {budget_secs:.6} s"
);
let _ = bytes;
}
#[test]
fn hnsw_search_under_budget() {
let _perf = perf_lock();
let mut cat = Catalog::new();
cat.create_table(TableSchema::new(
"vecs",
vec![
ColumnSchema::new("id", DataType::Int, false),
ColumnSchema::new(
"v",
DataType::Vector {
dim: 8,
encoding: VecEncoding::F32,
},
false,
),
],
))
.unwrap();
let t = cat.get_mut("vecs").unwrap();
for i in 0_i32..200 {
#[allow(clippy::cast_precision_loss)]
let f = i as f32;
t.insert(Row::new(vec![
Value::Int(i),
Value::vector(vec![
f * 0.01,
f * 0.02,
f * 0.03,
f * 0.04,
f * 0.05,
f * 0.06,
f * 0.07,
f * 0.08,
]),
]))
.unwrap();
}
t.add_nsw_index("v_idx".into(), "v", NSW_DEFAULT_M).unwrap();
let table = cat.get("vecs").unwrap();
let query = vec![1.5_f32, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0];
let iters: u32 = 200;
let start = Instant::now();
for _ in 0..iters {
let hits = nsw_query(
std::hint::black_box(table),
"v_idx",
std::hint::black_box(&query),
10,
NswMetric::L2,
);
std::hint::black_box(hits);
}
let mean_secs = start.elapsed().as_secs_f64() / f64::from(iters);
let budget_secs = 1e-3;
assert!(
mean_secs < budget_secs,
"hnsw_search_top10_dim8_n200 mean {mean_secs:.6} s exceeds budget {budget_secs:.6} s"
);
}
#[test]
fn pv_push_1m_under_200ms() {
let _perf = perf_lock();
let start = thread_cpu_now();
let mut pv: PersistentVec<u64> = PersistentVec::new();
for i in 0..1_000_000_u64 {
pv = pv.push(i);
}
let elapsed = thread_cpu_now().saturating_sub(start);
std::hint::black_box(&pv);
let budget_ms: u128 = 200;
let elapsed_ms = elapsed.as_millis();
assert!(
elapsed_ms < budget_ms,
"pv_push_1m elapsed {elapsed_ms} ms exceeds budget {budget_ms} ms"
);
assert_eq!(pv.len(), 1_000_000);
}
#[test]
fn pb_insert_mut_100k_under_50ms() {
let _perf = perf_lock();
let mut pb: PersistentBTreeMap<i64, i64> = PersistentBTreeMap::new();
let start = thread_cpu_now();
for i in 0..100_000_i64 {
pb.insert_mut(i, i.wrapping_mul(0x9E37_79B9));
}
let elapsed = thread_cpu_now().saturating_sub(start);
std::hint::black_box(&pb);
let budget_ms: u128 = 50;
let elapsed_ms = elapsed.as_millis();
assert!(
elapsed_ms < budget_ms,
"pb_insert_mut_100k elapsed {elapsed_ms} ms exceeds budget {budget_ms} ms"
);
assert_eq!(pb.len(), 100_000);
}
#[test]
fn pv_push_mut_1m_under_50ms() {
let _perf = perf_lock();
let start = thread_cpu_now();
let mut pv: PersistentVec<u64> = PersistentVec::new();
for i in 0..1_000_000_u64 {
pv.push_mut(i);
}
let elapsed = thread_cpu_now().saturating_sub(start);
std::hint::black_box(&pv);
let budget_ms: u128 = 75;
let elapsed_ms = elapsed.as_millis();
assert!(
elapsed_ms < budget_ms,
"pv_push_mut_1m elapsed {elapsed_ms} ms exceeds budget {budget_ms} ms"
);
assert_eq!(pv.len(), 1_000_000);
}
#[test]
fn pv_get_random_under_100ns_avg() {
let _perf = perf_lock();
let mut pv: PersistentVec<u64> = PersistentVec::new();
for i in 0..1_000_000_u64 {
pv = pv.push(i);
}
let mut state: u64 = 0xC0FFEE_u64;
const N_PROBES: usize = 100_000;
let start = Instant::now();
let mut acc: u64 = 0;
for _ in 0..N_PROBES {
state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut x = state;
x = (x ^ (x >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
x = (x ^ (x >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
x ^= x >> 31;
let idx = (x as usize) % 1_000_000;
let v = pv.get(std::hint::black_box(idx)).unwrap();
acc = acc.wrapping_add(*v);
}
let elapsed = start.elapsed();
std::hint::black_box(acc);
let avg_ns = elapsed.as_nanos() / (N_PROBES as u128);
let budget_ns: u128 = 100;
assert!(
avg_ns < budget_ns,
"pv_get_random avg {avg_ns} ns exceeds budget {budget_ns} ns"
);
}
fn splitmix64(mut x: u64) -> u64 {
x = x.wrapping_add(0x9e37_79b9_7f4a_7c15);
x = (x ^ (x >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
x = (x ^ (x >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
x ^ (x >> 31)
}
#[test]
fn bloom_fp_rate_under_1pct() {
let _perf = perf_lock();
const TARGET_FP: f64 = 0.01;
const CEILING_FP: f64 = TARGET_FP * 1.1;
const N: usize = 100_000;
let mut bf = BloomFilter::with_target_fp_rate(N, TARGET_FP);
let mut s = 0xfeed_beef_u64;
let mut inserted = Vec::with_capacity(N);
for _ in 0..N {
s = splitmix64(s.wrapping_add(1));
inserted.push(s);
bf.insert(&s.to_le_bytes());
}
let inserted_set: std::collections::BTreeSet<u64> = inserted.iter().copied().collect();
let mut s2 = 0xbeef_feed_u64;
let mut fp = 0u64;
let mut tested = 0u64;
for _ in 0..N {
s2 = splitmix64(s2.wrapping_add(1));
if inserted_set.contains(&s2) {
continue;
}
tested += 1;
if bf.contains(&s2.to_le_bytes()) {
fp += 1;
}
}
let observed = fp as f64 / tested as f64;
eprintln!(
"bloom_fp_rate: {fp} fp / {tested} tested = {observed:.5} (target {TARGET_FP:.3}, ceiling {CEILING_FP:.3})"
);
assert!(
observed <= CEILING_FP,
"observed FP {observed:.5} exceeded ceiling {CEILING_FP:.3} (target {TARGET_FP:.3})"
);
}
#[test]
fn bloom_insert_1m_under_100ms() {
let _perf = perf_lock();
const N: usize = 1_000_000;
let mut bf = BloomFilter::with_target_fp_rate(N, 0.01);
let mut s = 0xdead_beef_u64;
let start = Instant::now();
for _ in 0..N {
s = splitmix64(s.wrapping_add(1));
bf.insert(&s.to_le_bytes());
}
let elapsed = start.elapsed();
eprintln!(
"bloom_insert_1m: {N} inserts in {:.3} ms",
elapsed.as_secs_f64() * 1000.0
);
let budget_ms: u128 = 100;
assert!(
elapsed.as_millis() <= budget_ms,
"bloom_insert_1m took {} ms, budget {budget_ms} ms",
elapsed.as_millis()
);
}
#[test]
fn segment_write_1m_under_2s() {
let _perf = perf_lock();
const N: u64 = 1_000_000;
let rows: Vec<(u64, Vec<u8>)> = (0..N)
.map(|i| (i, vec![u8::try_from(i & 0xff).unwrap(); 32]))
.collect();
let start = Instant::now();
let (bytes, meta) =
encode_segment(rows.into_iter(), 0.01, SEGMENT_PAGE_BYTES).expect("encode succeeds");
let elapsed = start.elapsed();
eprintln!(
"segment_write_1m: {} rows in {:.3} s ({} bytes, {} pages)",
meta.num_rows,
elapsed.as_secs_f64(),
bytes.len(),
meta.num_pages,
);
assert_eq!(meta.num_rows, N);
let budget_ms: u128 = 2000;
assert!(
elapsed.as_millis() <= budget_ms,
"segment_write_1m took {} ms, budget {budget_ms} ms",
elapsed.as_millis()
);
}
#[test]
fn segment_lookup_p99_under_500us() {
let _perf = perf_lock();
const N: u64 = 1_000_000;
let rows: Vec<(u64, Vec<u8>)> = (0..N)
.map(|i| (i, vec![u8::try_from(i & 0xff).unwrap(); 32]))
.collect();
let (bytes, _) =
encode_segment(rows.into_iter(), 0.01, SEGMENT_PAGE_BYTES).expect("encode succeeds");
let reader = SegmentReader::open(&bytes).expect("open succeeds");
let mut state: u64 = 0xc0ffee;
const N_PROBES: usize = 1000;
let mut samples_us = Vec::with_capacity(N_PROBES);
for _ in 0..N_PROBES {
state = state.wrapping_add(0x9e37_79b9_7f4a_7c15);
let mut x = state;
x = (x ^ (x >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
x = (x ^ (x >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
x ^= x >> 31;
let key = x % N;
let t = Instant::now();
let got = reader.lookup(std::hint::black_box(key));
samples_us.push(t.elapsed().as_micros());
assert!(got.is_some(), "lookup({key}) returned None");
}
samples_us.sort_unstable();
let p99_idx = (N_PROBES * 99) / 100;
let p99 = samples_us[p99_idx];
eprintln!("segment_lookup_p99: {p99} µs (over {N_PROBES} warm-RAM probes)");
let budget_us: u128 = 500;
assert!(
p99 <= budget_us,
"segment_lookup p99 {p99} µs exceeds budget {budget_us} µs"
);
}
fn random_unit_vec_f32(rng_state: &mut u64, dim: usize) -> Vec<f32> {
let mut out = Vec::with_capacity(dim);
for _ in 0..dim {
*rng_state = splitmix64(*rng_state);
let bits = (*rng_state >> 40) as u32;
let u = (bits as f32) / ((1u32 << 24) as f32);
out.push(u * 2.0 - 1.0);
}
out
}
#[test]
fn sq8_quantize_1m_under_500ms() {
let _perf = perf_lock();
const N: usize = 1_000_000;
const DIM: usize = 128;
let mut rng: u64 = 0xC0DE_C0DE_5BAD_5BAD;
let corpus: Vec<Vec<f32>> = (0..N).map(|_| random_unit_vec_f32(&mut rng, DIM)).collect();
let t = Instant::now();
let mut sink: u32 = 0;
for v in &corpus {
let q = quantize(std::hint::black_box(v));
sink = sink
.wrapping_add(q.bytes[0] as u32)
.wrapping_add(q.bytes[DIM / 2] as u32);
}
let elapsed = t.elapsed();
std::hint::black_box(sink);
eprintln!(
"sq8_quantize_1m_dim128: {} ms ({} ns/vec)",
elapsed.as_millis(),
elapsed.as_nanos() / N as u128
);
let budget_ms: u128 = 500;
assert!(
elapsed.as_millis() <= budget_ms,
"sq8_quantize_1m_dim128 took {} ms, budget {budget_ms} ms",
elapsed.as_millis()
);
}
#[test]
fn sq8_adc_l2_under_200ns_per_pair() {
let _perf = perf_lock();
const DIM: usize = 128;
const N_PAIRS: usize = 1_000_000;
let mut rng: u64 = 0xADC0_ADC0_ADC0_ADC0;
let pool: Vec<Sq8Vector> = (0..1024)
.map(|_| quantize(&random_unit_vec_f32(&mut rng, DIM)))
.collect();
let t = Instant::now();
let mut acc: f32 = 0.0;
for i in 0..N_PAIRS {
let a = &pool[i & 1023];
let b = &pool[(i.wrapping_mul(2654435761)) & 1023]; acc += std::hint::black_box(sq8_l2_distance_sq(a, b));
}
let elapsed = t.elapsed();
std::hint::black_box(acc);
let per_call_ns = elapsed.as_nanos() / N_PAIRS as u128;
eprintln!("sq8_adc_l2_dim128: {per_call_ns} ns/pair (over {N_PAIRS} pairs)");
let budget_ns: u128 = 250;
assert!(
per_call_ns <= budget_ns,
"sq8_adc_l2_dim128 per-pair {per_call_ns} ns exceeds budget {budget_ns} ns"
);
}
#[test]
fn sq8_adc_l2_asymmetric_under_200ns_per_pair() {
let _perf = perf_lock();
const DIM: usize = 128;
const N_PAIRS: usize = 1_000_000;
let mut rng: u64 = 0xADC1_BEEF_ADC1_BEEF;
let pool: Vec<Sq8Vector> = (0..1024)
.map(|_| quantize(&random_unit_vec_f32(&mut rng, DIM)))
.collect();
let queries: Vec<Vec<f32>> = (0..1024)
.map(|_| random_unit_vec_f32(&mut rng, DIM))
.collect();
let t = Instant::now();
let mut acc: f32 = 0.0;
for i in 0..N_PAIRS {
let a = &pool[i & 1023];
let q = &queries[(i.wrapping_mul(2654435761)) & 1023];
acc += std::hint::black_box(sq8_l2_distance_sq_asymmetric(a, q));
}
let elapsed = t.elapsed();
std::hint::black_box(acc);
let per_call_ns = elapsed.as_nanos() / N_PAIRS as u128;
eprintln!("sq8_adc_l2_asym_dim128: {per_call_ns} ns/pair (over {N_PAIRS} pairs)");
let budget_ns: u128 = 200;
assert!(
per_call_ns <= budget_ns,
"sq8_adc_l2_asym_dim128 per-pair {per_call_ns} ns exceeds budget {budget_ns} ns"
);
}
#[test]
fn cosine_dim128_under_50ns() {
let _perf = perf_lock();
const DIM: usize = 128;
const N_PAIRS: usize = 1_000_000;
let mut rng: u64 = 0xC051_C051_DEAD_BEEF;
let pool: Vec<Vec<f32>> = (0..1024)
.map(|_| random_unit_vec_f32(&mut rng, DIM))
.collect();
let mut warm: f32 = 0.0;
for i in 0..10_000 {
let a = &pool[i & 1023];
let b = &pool[(i.wrapping_mul(2_654_435_761)) & 1023];
warm += std::hint::black_box(metric_dispatch_cos(a, b));
}
std::hint::black_box(warm);
let t = Instant::now();
let mut acc: f32 = 0.0;
for i in 0..N_PAIRS {
let a = &pool[i & 1023];
let b = &pool[(i.wrapping_mul(2_654_435_761)) & 1023];
acc += std::hint::black_box(metric_dispatch_cos(a, b));
}
let elapsed = t.elapsed();
std::hint::black_box(acc);
let per_call_ns = elapsed.as_nanos() / N_PAIRS as u128;
eprintln!("cosine_dim128: {per_call_ns} ns/pair (over {N_PAIRS} pairs)");
#[cfg(target_arch = "aarch64")]
{
let budget_ns: u128 = 60;
assert!(
per_call_ns <= budget_ns,
"cosine_dim128 per-pair {per_call_ns} ns exceeds budget {budget_ns} ns — \
check NEON dispatch in `metric_distance(Cosine, ...)`"
);
}
}
#[test]
fn inner_product_dim128_under_50ns() {
let _perf = perf_lock();
const DIM: usize = 128;
const N_PAIRS: usize = 1_000_000;
let mut rng: u64 = 0xBEEF_FEED_FACE_C001;
let pool: Vec<Vec<f32>> = (0..1024)
.map(|_| random_unit_vec_f32(&mut rng, DIM))
.collect();
let mut warm: f32 = 0.0;
for i in 0..10_000 {
let a = &pool[i & 1023];
let b = &pool[(i.wrapping_mul(2_654_435_761)) & 1023];
warm += std::hint::black_box(metric_dispatch_ip(a, b));
}
std::hint::black_box(warm);
let t = Instant::now();
let mut acc: f32 = 0.0;
for i in 0..N_PAIRS {
let a = &pool[i & 1023];
let b = &pool[(i.wrapping_mul(2_654_435_761)) & 1023];
acc += std::hint::black_box(metric_dispatch_ip(a, b));
}
let elapsed = t.elapsed();
std::hint::black_box(acc);
let per_call_ns = elapsed.as_nanos() / N_PAIRS as u128;
eprintln!("inner_product_dim128: {per_call_ns} ns/pair (over {N_PAIRS} pairs)");
#[cfg(target_arch = "aarch64")]
{
let budget_ns: u128 = 50;
assert!(
per_call_ns <= budget_ns,
"inner_product_dim128 per-pair {per_call_ns} ns exceeds budget {budget_ns} ns"
);
}
}
#[test]
fn sq8_adc_l2_asymmetric_neon_dim128_under_50ns() {
let _perf = perf_lock();
const DIM: usize = 128;
const N_PAIRS: usize = 1_000_000;
let mut rng: u64 = 0x5A8_5A8_5A8_5A8;
let pool: Vec<Sq8Vector> = (0..1024)
.map(|_| quantize(&random_unit_vec_f32(&mut rng, DIM)))
.collect();
let queries: Vec<Vec<f32>> = (0..1024)
.map(|_| random_unit_vec_f32(&mut rng, DIM))
.collect();
let mut warm: f32 = 0.0;
for i in 0..10_000 {
let a = &pool[i & 1023];
let q = &queries[(i.wrapping_mul(2_654_435_761)) & 1023];
warm += std::hint::black_box(sq8_l2_distance_sq_asymmetric(a, q));
}
std::hint::black_box(warm);
let t = Instant::now();
let mut acc: f32 = 0.0;
for i in 0..N_PAIRS {
let a = &pool[i & 1023];
let q = &queries[(i.wrapping_mul(2_654_435_761)) & 1023];
acc += std::hint::black_box(sq8_l2_distance_sq_asymmetric(a, q));
}
let elapsed = t.elapsed();
std::hint::black_box(acc);
let per_call_ns = elapsed.as_nanos() / N_PAIRS as u128;
eprintln!("sq8_adc_l2_asym_neon_dim128: {per_call_ns} ns/pair (over {N_PAIRS} pairs)");
#[cfg(target_arch = "aarch64")]
{
let budget_ns: u128 = 50;
assert!(
per_call_ns <= budget_ns,
"sq8_adc_l2_asym_neon_dim128 per-pair {per_call_ns} ns exceeds budget {budget_ns} ns \
— check NEON dispatch in `sq8_l2_distance_sq_asymmetric`"
);
}
}
fn metric_dispatch_cos(a: &[f32], b: &[f32]) -> f32 {
let (dot, na, nb) = spg_storage::cosine_dot_norms_f32(a, b);
if na == 0.0 || nb == 0.0 {
return f32::INFINITY;
}
1.0 - dot / (na.sqrt() * nb.sqrt())
}
fn metric_dispatch_ip(a: &[f32], b: &[f32]) -> f32 {
-spg_storage::inner_product_f32(a, b)
}
#[test]
fn sq8_recall_at_10_above_0_95_perf_gate() {
let _perf = perf_lock();
const N: usize = 10_000;
const Q: usize = 100;
const K: usize = 10;
const DIM: usize = 128;
let mut rng: u64 = 0xCAFE_BABE_5EED_1234;
let corpus_f32: Vec<Vec<f32>> = (0..N).map(|_| random_unit_vec_f32(&mut rng, DIM)).collect();
let corpus_sq8: Vec<Sq8Vector> = corpus_f32.iter().map(|v| quantize(v)).collect();
let mut total_recall: f32 = 0.0;
for _ in 0..Q {
let query = random_unit_vec_f32(&mut rng, DIM);
let mut scored_f32: Vec<(f32, usize)> = corpus_f32
.iter()
.enumerate()
.map(|(i, v)| {
let mut d = 0.0_f32;
for (x, y) in v.iter().zip(query.iter()) {
let dd = x - y;
d += dd * dd;
}
(d, i)
})
.collect();
scored_f32.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
let truth: Vec<usize> = scored_f32.into_iter().take(K).map(|(_, i)| i).collect();
let mut scored_sq8: Vec<(f32, usize)> = corpus_sq8
.iter()
.enumerate()
.map(|(i, qv)| (sq8_l2_distance_sq_asymmetric(qv, &query), i))
.collect();
scored_sq8.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
let sq8_top: Vec<usize> = scored_sq8.into_iter().take(K).map(|(_, i)| i).collect();
let mut hits = 0;
for x in &truth {
if sq8_top.contains(x) {
hits += 1;
}
}
total_recall += hits as f32 / K as f32;
}
let avg = total_recall / Q as f32;
eprintln!("sq8_recall_at_10_unit_sphere_dim128: {avg:.4}");
assert!(avg >= 0.95, "sq8 recall@10 average = {avg} (need ≥ 0.95)");
}
#[test]
fn gin_trgm_append_does_not_recopy_the_posting_list() {
let _perf = perf_lock();
const BATCH: i64 = 1500;
const BATCHES: i64 = 8;
let mut cat = Catalog::new();
cat.create_table(TableSchema::new(
"docs",
vec![
ColumnSchema::new("id", DataType::Int, false),
ColumnSchema::new("body", DataType::Text, false),
],
))
.unwrap();
let t = cat.get_mut("docs").unwrap();
t.add_gin_trgm_index("docs_body_trgm".into(), "body")
.unwrap();
let shared = "the quick brown fox jumps over the lazy dog while the \
rain in spain stays mainly on the plain and the cat sat";
let mut cost = Vec::with_capacity(BATCHES as usize);
for b in 0..BATCHES {
let start = thread_cpu_now();
for i in (b * BATCH)..((b + 1) * BATCH) {
let t = cat.get_mut("docs").unwrap();
t.insert(Row::new(vec![
Value::Int(i as i32),
Value::text(format!("{shared} {i}")),
]))
.unwrap();
}
cost.push(thread_cpu_now() - start);
}
for (b, c) in cost.iter().enumerate() {
eprintln!("batch {b}: {:.4}s", c.as_secs_f64());
}
let first = cost[1].as_secs_f64();
let last = cost[(BATCHES - 1) as usize].as_secs_f64();
assert!(
first > 0.0,
"the reference batch measured zero CPU time — the clock, not the code"
);
let ratio = last / first;
assert!(
ratio < 2.0,
"the last batch of {BATCH} rows cost {ratio:.1}x the first \
({last:.4}s vs {first:.4}s) — posting-list append is scaling with \
the rows already indexed"
);
}