use std::sync::Arc;
use std::time::{Duration, Instant};
use cel_memory::{
CallerScope, ChunkKind, ChunkSource, MemoryProvider, MemoryQuery, NewMemoryChunk,
RetrievalProfile,
};
use cel_memory_sqlite::{MockEmbedder, SqliteMemoryProvider};
const N_CHUNKS: usize = 1_000;
const N_RETRIEVES: usize = 200;
const K: usize = 8;
#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let db_path = dir.path().join("bench.sqlite");
let embedder = Arc::new(MockEmbedder::new());
let provider = SqliteMemoryProvider::open(&db_path, embedder).await?;
let mut total_write = Duration::ZERO;
let start = Instant::now();
let batch_size = 50;
let mut batch = Vec::with_capacity(batch_size);
for i in 0..N_CHUNKS {
batch.push(NewMemoryChunk {
kind: ChunkKind::Chat,
source: ChunkSource::Embedded,
session_id: None,
project_root: None,
caller_id: "bench".into(),
content: format!(
"chunk {i}: synthetic content. category={} sample={}",
i % 50,
i * 7919 % 10_000
),
metadata: serde_json::json!({"i": i}),
importance: None,
shareable: false,
pinned: false,
});
if batch.len() == batch_size || i + 1 == N_CHUNKS {
let drained: Vec<_> = std::mem::take(&mut batch);
let t = Instant::now();
provider.write_batch(drained).await?;
total_write += t.elapsed();
}
}
let wall_write = start.elapsed();
println!(
"wrote {N_CHUNKS} chunks in {} ms (wall) / {} ms (writer-only)",
wall_write.as_millis(),
total_write.as_millis()
);
let mut samples = Vec::with_capacity(N_RETRIEVES);
for run in 0..N_RETRIEVES {
let q = MemoryQuery {
text: format!("category={} content {}", run % 50, run),
kinds: Some(vec![ChunkKind::Chat]),
since: None,
until: None,
session_id: None,
caller_scope: CallerScope::Global,
project_root_prefix: None,
k: K,
include_rollups: false,
min_importance: None,
profile: RetrievalProfile::AgentChatTurn,
caller_id: "bench".into(),
};
let t = Instant::now();
let hits = provider.retrieve(q).await?;
samples.push(t.elapsed());
if run == 0 {
println!(" first retrieve returned {} hits", hits.len());
}
}
samples.sort();
let p = |q: f64| samples[((samples.len() as f64 * q) as usize).min(samples.len() - 1)];
let mean: Duration = samples.iter().sum::<Duration>() / samples.len() as u32;
println!(
"retrieve k={K} over {N_CHUNKS} chunks, {N_RETRIEVES} runs:\n \
p50: {:>6.1} ms\n p95: {:>6.1} ms\n p99: {:>6.1} ms\n \
mean: {:>5.1} ms",
p(0.50).as_secs_f64() * 1000.0,
p(0.95).as_secs_f64() * 1000.0,
p(0.99).as_secs_f64() * 1000.0,
mean.as_secs_f64() * 1000.0,
);
let p95 = p(0.95);
if p95 > Duration::from_millis(150) {
eprintln!(
"WARN: p95 {} ms exceeds the 150 ms Phase 2 budget",
p95.as_millis()
);
}
Ok(())
}