use std::sync::Arc;
use std::time::{Duration, Instant};
use bytes::Bytes;
use mnem_core::id::NodeId;
use mnem_core::objects::{Dtype, Embedding, Node};
use mnem_core::repo::ReadonlyRepo;
use mnem_core::retrieve::Retriever;
use mnem_core::store::{Blockstore, MemoryBlockstore, MemoryOpHeadsStore, OpHeadsStore};
const EMBED_MODEL: &str = "perf:fake-384";
const DIM: usize = 384;
const NS: &[usize] = &[1_000, 5_000, 10_000, 25_000];
const RETRIEVE_ROUNDS: usize = 10;
struct Lcg(u64);
impl Lcg {
const fn new(seed: u64) -> Self {
Self(seed.wrapping_add(0x9e37_79b9_7f4a_7c15))
}
fn next_f32(&mut self) -> f32 {
self.0 = self
.0
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
let x = (self.0 >> 33) as u32;
(x as f32 / (u32::MAX as f32 / 2.0)) - 1.0
}
}
fn unit_vec(seed: u64) -> Embedding {
let mut rng = Lcg::new(seed);
let mut raw = Vec::with_capacity(DIM);
for _ in 0..DIM {
raw.push(rng.next_f32());
}
let norm: f32 = raw.iter().map(|x| x * x).sum::<f32>().sqrt().max(1e-9);
for x in raw.iter_mut() {
*x /= norm;
}
let mut bytes = Vec::with_capacity(DIM * 4);
for v in &raw {
bytes.extend_from_slice(&v.to_le_bytes());
}
Embedding {
model: EMBED_MODEL.to_string(),
dtype: Dtype::F32,
dim: DIM as u32,
vector: Bytes::from(bytes),
}
}
fn make_repo() -> Result<ReadonlyRepo, Box<dyn std::error::Error>> {
let bs: Arc<dyn Blockstore> = Arc::new(MemoryBlockstore::new());
let ohs: Arc<dyn OpHeadsStore> = Arc::new(MemoryOpHeadsStore::new());
Ok(ReadonlyRepo::init(bs, ohs)?)
}
fn seed(
repo: &ReadonlyRepo,
n: usize,
from_seed: u64,
) -> Result<ReadonlyRepo, Box<dyn std::error::Error>> {
let mut tx = repo.start_transaction();
for i in 0..n {
let node = Node::new(NodeId::new_v7(), "Doc").with_summary(format!("seed-doc-{i}"));
let cid = tx.add_node(&node)?;
let embed = unit_vec(from_seed + i as u64);
tx.set_embedding(cid, embed.model.clone(), embed)?;
}
Ok(tx.commit("bench", "seed")?)
}
fn measure_fresh_vs_warm(n: usize) -> Result<(), Box<dyn std::error::Error>> {
let repo = seed(&make_repo()?, n, 1)?;
let probe = unit_vec(9_999_999);
let probe_floats: Vec<f32> = probe
.vector
.chunks_exact(4)
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect();
let t = Instant::now();
let idx = Arc::new(repo.build_vector_index(EMBED_MODEL)?);
let build_ms = t.elapsed();
let mut warm_total = Duration::ZERO;
for _ in 0..RETRIEVE_ROUNDS {
let t = Instant::now();
let _ = Retriever::new(&repo)
.vector(EMBED_MODEL, probe_floats.clone())
.with_vector_index(idx.clone())
.limit(10)
.execute()?;
warm_total += t.elapsed();
}
let warm_avg = warm_total / RETRIEVE_ROUNDS as u32;
let mut fresh_total = Duration::ZERO;
for _ in 0..RETRIEVE_ROUNDS {
let t = Instant::now();
let _ = Retriever::new(&repo)
.vector(EMBED_MODEL, probe_floats.clone())
.limit(10)
.execute()?;
fresh_total += t.elapsed();
}
let fresh_avg = fresh_total / RETRIEVE_ROUNDS as u32;
let build_overhead_pct = 100.0 * build_ms.as_secs_f64() / fresh_avg.as_secs_f64();
println!(
"{n:>8} | {build_ms:>12.2?} | {fresh_avg:>12.2?} | {warm_avg:>12.2?} | \
{build_overhead_pct:>6.1}%"
);
Ok(())
}
fn measure_interspersed() -> Result<(), Box<dyn std::error::Error>> {
println!();
println!("# I0c: interspersed ingest-retrieve pattern");
println!("# 50 writes, then 1 retrieve, 10 rounds. Mirrors");
println!("# LongMemEval per-question session ingest + question query.");
println!();
println!(
"{:>6} | {:>12} | {:>12} | {:>12}",
"round", "n_total", "ingest50", "retrieve1"
);
let mut repo = make_repo()?;
let probe = unit_vec(9_999_999);
let probe_floats: Vec<f32> = probe
.vector
.chunks_exact(4)
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect();
for round in 1..=10 {
let t = Instant::now();
let mut tx = repo.start_transaction();
for i in 0..50 {
let node =
Node::new(NodeId::new_v7(), "Doc").with_summary(format!("round{round}-doc{i}"));
let cid = tx.add_node(&node)?;
let embed = unit_vec(round as u64 * 10_000 + i as u64);
tx.set_embedding(cid, embed.model.clone(), embed)?;
}
repo = tx.commit("bench", "batch")?;
let ingest_t = t.elapsed();
let t = Instant::now();
let _ = Retriever::new(&repo)
.vector(EMBED_MODEL, probe_floats.clone())
.limit(10)
.execute()?;
let retr_t = t.elapsed();
println!(
"{:>6} | {:>12} | {:>12.2?} | {:>12.2?}",
round,
round * 50,
ingest_t,
retr_t
);
}
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("# mnem perf layer-attribution bench ");
println!("# Fake 384-dim vectors, MemoryBlockstore, no ONNX, no HTTP.");
println!("# Numbers are for relative comparison only; absolute times");
println!("# will change with real ONNX embed bytes and redb backend.");
println!();
println!("# I0a + I0b: fresh vs warm retrieve at varying N");
println!("# fresh = Retriever::execute() with no override (mnem-core");
println!("# naive path, forces a fresh index build each call).");
println!("# warm = Retriever::execute() with with_vector_index(Arc<..>)");
println!("# override (mimics mnem-http warm IndexCache).");
println!("# build%% = build / fresh ratio (how much of a fresh retrieve");
println!("# is pure index-build overhead).");
println!();
println!(
"{:>8} | {:>12} | {:>12} | {:>12} | {:>7}",
"n", "idx_build", "fresh_avg", "warm_avg", "build%"
);
for &n in NS {
measure_fresh_vs_warm(n)?;
}
measure_interspersed()?;
println!();
println!("# Conclusions (to be filled in by docs/perf-sweep-plan after run):");
println!("# - If build% approaches 100% at N=25k, Fix A is mandatory.");
println!("# - If retrieve1 grows linearly in n_total during the");
println!("# interspersed pattern, that confirms the O(Q^2) hypothesis.");
println!("# - If warm_avg is flat in N, the search path itself is");
println!("# already acceptable; the O(N^2) behaviour lives in the");
println!("# mnem-http cache-invalidation policy, not the algorithm.");
Ok(())
}