use kernel::graph::{Graph, Metric};
use kernel::io::IoMode;
use kernel::store::{Config, Store, SyncMode};
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};
const VF: u64 = 1;
static LIVE: AtomicUsize = AtomicUsize::new(0);
static PEAK: AtomicUsize = AtomicUsize::new(0);
struct Counting;
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, l: Layout) -> *mut u8 {
let p = unsafe { System.alloc(l) };
if !p.is_null() {
let now = LIVE.fetch_add(l.size(), Relaxed) + l.size();
PEAK.fetch_max(now, Relaxed);
}
p
}
unsafe fn dealloc(&self, p: *mut u8, l: Layout) {
LIVE.fetch_sub(l.size(), Relaxed);
unsafe { System.dealloc(p, l) }
}
unsafe fn realloc(&self, p: *mut u8, l: Layout, n: usize) -> *mut u8 {
let q = unsafe { System.realloc(p, l, n) };
if !q.is_null() { LIVE.fetch_add(n, Relaxed); LIVE.fetch_sub(l.size(), Relaxed); }
q
}
}
#[global_allocator]
static A: Counting = Counting;
const DIM: usize = 1536;
fn vec_for(i: u64) -> Vec<f32> {
let s = i.wrapping_mul(0x9E37_79B9_7F4A_7C15);
(0..DIM).map(|j| ((s.wrapping_add(j as u64) % 1000) as f32) / 500.0 - 1.0).collect()
}
fn ingest(n: u64) -> (f64, f64) {
let d = tempfile::TempDir::new().unwrap();
let cfg = Config { budget_bytes: 8 << 20, io: IoMode::Buffered, sync: SyncMode::Off };
let mut g = Graph::new(Store::create(d.path(), cfg).unwrap()).unwrap();
let base = LIVE.load(Relaxed);
let mut peak = 0usize;
for i in 1..=n {
g.set_vec(VF, i, &vec_for(i)).unwrap();
if i % 512 == 0 { peak = peak.max(LIVE.load(Relaxed).saturating_sub(base)); }
}
g.commit().unwrap();
peak = peak.max(LIVE.load(Relaxed).saturating_sub(base));
let cands: Vec<u64> = (1..=5_000.min(n)).collect();
let q = vec_for(999_983);
let before = LIVE.load(Relaxed);
PEAK.store(before, Relaxed);
let mut rpeak = 0usize;
for _ in 0..3 {
let top = g.rescore(VF, &cands, &q, Metric::Cosine, 10).unwrap();
assert_eq!(top.len(), 10);
rpeak = rpeak.max(PEAK.load(Relaxed).saturating_sub(before));
}
let mib = |b: usize| b as f64 / (1 << 20) as f64;
(mib(peak), mib(rpeak))
}
#[test]
fn vector_ingest_and_rescore_hold_law_1() {
let (w1, r1) = ingest(25_000);
let (w2, r2) = ingest(100_000);
eprintln!("ingest heap: 25k={w1:.2} MiB 100k={w2:.2} MiB (data 150MB -> 600MB)");
eprintln!("rescore heap: {r1:.3}/{r2:.3} MiB for 5K candidates, k=10");
assert!(
w2 < w1 * 2.0 + 1.0,
"the vector-ingest RAM curve is back: ingest heap {w1:.2} -> {w2:.2} MiB over 4x vectors"
);
assert!(r1 < 1.0 && r2 < 1.0, "rescore heap {r1:.3}/{r2:.3} MiB is not ∝ k");
}
#[test]
fn fold_wal_is_bounded_by_the_checkpoint_interval() {
use kernel::graph::Graph;
use kernel::store::{Config, Store};
let wal_after = |every: u64| -> u64 {
let d = tempfile::TempDir::new().unwrap();
let mut g = Graph::new(Store::create(d.path(), Config::default()).unwrap()).unwrap();
let vecf = |i: u64| -> Vec<f32> {
let s = i.wrapping_mul(0x9E37_79B9_7F4A_7C15);
(0..16).map(|j| ((s ^ (j as u64).wrapping_mul(0xD1B5_4A32_D192_ED03)) % 1000) as f32 / 500.0 - 1.0).collect()
};
for i in 1..=1200u64 { g.set_vec(VF, i, &vecf(i)).unwrap(); }
g.commit().unwrap();
g.checkpoint().unwrap();
g.set_nav_fold_interval(every);
g.fold_nav(VF).unwrap();
std::fs::metadata(d.path().join("wal")).map(|m| m.len()).unwrap_or(0)
};
let unbounded = wal_after(u64::MAX);
let bounded = wal_after(300);
assert!(bounded * 2 < unbounded,
"bounded fold WAL {bounded}B must be well under unbounded {unbounded}B");
}