use std::time::Instant;
use grit_core::{Budget, GraphOp, Grit, Options, Query, Traversal};
use uuid::Uuid;
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
fn below(&mut self, n: usize) -> usize {
(self.next() % n as u64) as usize
}
}
const WORDS: &[&str] = &[
"yoneda",
"functor",
"kernel",
"image",
"exact",
"sequence",
"lemma",
"sheaf",
"topos",
"adjoint",
"limit",
"colimit",
"monad",
"algebra",
"module",
"tensor",
"homology",
"cohomology",
"spectral",
"fibration",
"bundle",
"manifold",
"scheme",
"variety",
"ideal",
"ring",
"field",
"group",
"torsion",
"lattice",
"category",
"morphism",
"natural",
"transformation",
"presheaf",
"stalk",
"germ",
"local",
"global",
"section",
"derived",
"abelian",
"simplicial",
"chain",
"complex",
"boundary",
"cycle",
"closed",
"open",
"dense",
"compact",
"hausdorff",
"metric",
"norm",
"banach",
"hilbert",
"operator",
"spectrum",
"eigenvalue",
"duality",
];
fn percentiles(mut samples: Vec<f64>) -> (f64, f64, f64) {
samples.sort_by(f64::total_cmp);
let pick = |q: f64| samples[((samples.len() - 1) as f64 * q) as usize];
(pick(0.50), pick(0.95), *samples.last().unwrap())
}
fn fill_embedding(rng: &mut Rng, out: &mut [f32]) {
for x in out.iter_mut() {
*x = ((rng.next() >> 40) as f32 / (1u32 << 24) as f32) * 2.0 - 1.0;
}
}
fn f32s_as_bytes(v: &[f32]) -> Vec<u8> {
let mut out = Vec::with_capacity(v.len() * 4);
for x in v {
out.extend_from_slice(&x.to_le_bytes());
}
out
}
fn arg(n: usize, default: usize) -> usize {
std::env::args()
.nth(n)
.and_then(|s| s.parse().ok())
.unwrap_or(default)
}
fn main() -> Result<(), grit_core::Error> {
let n_nodes = arg(1, 100_000);
let n_edges = arg(2, 300_000);
let n_episodes = arg(3, 10_000);
let dim = arg(4, 1024);
let n_invalidations = n_edges / 20;
let dir = std::env::temp_dir().join(format!("grit-quickbench-{}", std::process::id()));
std::fs::create_dir_all(&dir)?;
let path = dir.join("bench.db");
let _ = std::fs::remove_file(&path);
let g = Grit::open(&path, Options::new("bench"))?;
let mut rng = Rng(0x9E37_79B9_7F4A_7C15);
println!(
"fixture: {n_nodes} nodes, {n_edges} edges, {n_episodes} episodes, {n_invalidations} invalidations, dim {dim}"
);
println!("db: {}\n", path.display());
let t = Instant::now();
let mut node_ids: Vec<Uuid> = Vec::with_capacity(n_nodes);
for i in 0..n_nodes {
let id = g.new_id();
let (w1, w2, w3, w4) = (
WORDS[rng.below(WORDS.len())],
WORDS[rng.below(WORDS.len())],
WORDS[rng.below(WORDS.len())],
WORDS[rng.below(WORDS.len())],
);
g.apply(GraphOp::AddNode {
id,
kind: "concept".into(),
name: format!("{w1} {w2} {i}"),
summary: format!("the {w3} of the {w4} in context {i}"),
attrs: serde_json::json!({}),
group_id: format!("g{}", i % 8),
})?;
node_ids.push(id);
}
let dt = t.elapsed().as_secs_f64();
println!(
"insert nodes: {:>9.0} ops/s ({dt:.1}s)",
n_nodes as f64 / dt
);
let now_ms = 1_752_000_000_000i64; let t = Instant::now();
let mut edge_ids: Vec<Uuid> = Vec::with_capacity(n_edges);
for i in 0..n_edges {
let id = g.new_id();
let src = node_ids[rng.below(n_nodes)];
let dst = node_ids[rng.below(n_nodes)];
let (w1, w2) = (WORDS[rng.below(WORDS.len())], WORDS[rng.below(WORDS.len())]);
g.apply(GraphOp::AddEdge {
id,
src,
dst,
rel: "RELATES".into(),
fact: format!("the {w1} constrains the {w2} ({i})"),
attrs: serde_json::json!({}),
group_id: format!("g{}", i % 8),
valid_at: Some(now_ms - (rng.below(1_000_000_000) as i64)),
invalid_at: None,
})?;
edge_ids.push(id);
}
let dt = t.elapsed().as_secs_f64();
println!(
"insert edges: {:>9.0} ops/s ({dt:.1}s)",
n_edges as f64 / dt
);
let t = Instant::now();
for i in 0..n_episodes {
let (w1, w2, w3) = (
WORDS[rng.below(WORDS.len())],
WORDS[rng.below(WORDS.len())],
WORDS[rng.below(WORDS.len())],
);
g.apply(GraphOp::AddEpisode {
id: g.new_id(),
source: "bench".into(),
kind: String::new(),
content: format!("today we discussed the {w1} and how the {w2} affects the {w3}"),
occurred_at: now_ms - i as i64,
group_id: format!("g{}", i % 8),
mentions: vec![node_ids[rng.below(n_nodes)], edge_ids[rng.below(n_edges)]],
})?;
}
let dt = t.elapsed().as_secs_f64();
println!(
"insert episodes: {:>9.0} ops/s ({dt:.1}s)",
n_episodes as f64 / dt
);
let t = Instant::now();
for _ in 0..n_invalidations {
g.apply(GraphOp::InvalidateEdge {
edge_id: edge_ids[rng.below(n_edges)],
invalid_at: now_ms - (rng.below(500_000_000) as i64),
})?;
}
let dt = t.elapsed().as_secs_f64();
println!(
"invalidate edges: {:>9.0} ops/s ({dt:.1}s)\n",
n_invalidations as f64 / dt
);
let mut embed_rng = Rng(0xA5A5_5A5A_D00D_F00D);
let mut embedding = vec![0f32; dim];
if dim > 0 {
g.register_embedding_model("bench-model", dim, "1")?;
let t = Instant::now();
for id in &node_ids {
fill_embedding(&mut embed_rng, &mut embedding);
g.set_node_embedding(*id, embedding.clone())?;
}
let dt = t.elapsed().as_secs_f64();
println!(
"embed nodes: {:>9.0} ops/s ({dt:.1}s)",
n_nodes as f64 / dt
);
let t = Instant::now();
for id in &edge_ids {
fill_embedding(&mut embed_rng, &mut embedding);
g.set_edge_embedding(*id, embedding.clone())?;
}
let dt = t.elapsed().as_secs_f64();
println!(
"embed edges: {:>9.0} ops/s ({dt:.1}s)\n",
n_edges as f64 / dt
);
}
const ITERS: usize = 200;
const VEC_ITERS: usize = 60;
let mut samples = Vec::with_capacity(ITERS);
let mut reached = 0usize;
for _ in 0..ITERS {
let seed = node_ids[rng.below(n_nodes)];
let t = Instant::now();
let sub = g.traverse(&[seed], &Traversal::default().depth(3))?;
samples.push(t.elapsed().as_secs_f64() * 1e3);
reached += sub.nodes.len();
}
let (p50, p95, max) = percentiles(samples);
println!(
"traverse depth-3: p50 {p50:7.2}ms p95 {p95:7.2}ms max {max:7.2}ms \
(avg {} nodes reached, default 256 budget; target p95 ≤ 10ms)",
reached / ITERS
);
let mut samples = Vec::with_capacity(ITERS);
let mut reached = 0usize;
for _ in 0..ITERS {
let seed = node_ids[rng.below(n_nodes)];
let t = Instant::now();
let sub = g.traverse(
&[seed],
&Traversal::default().depth(3).max_nodes(usize::MAX),
)?;
samples.push(t.elapsed().as_secs_f64() * 1e3);
reached += sub.nodes.len();
}
let (p50, p95, max) = percentiles(samples);
println!(
"traverse (no cap): p50 {p50:7.2}ms p95 {p95:7.2}ms max {max:7.2}ms \
(avg {} nodes reached, unbounded)",
reached / ITERS
);
let mut samples = Vec::with_capacity(ITERS);
for _ in 0..ITERS {
let seed = node_ids[rng.below(n_nodes)];
let t = Instant::now();
g.traverse(
&[seed],
&Traversal::default().depth(3).as_at(now_ms - 250_000_000),
)?;
samples.push(t.elapsed().as_secs_f64() * 1e3);
}
let (p50, p95, max) = percentiles(samples);
println!("traverse (as_at): p50 {p50:7.2}ms p95 {p95:7.2}ms max {max:7.2}ms (time travel)");
let mut samples = Vec::with_capacity(ITERS);
let mut hits_total = 0usize;
for _ in 0..ITERS {
let (w1, w2) = (WORDS[rng.below(WORDS.len())], WORDS[rng.below(WORDS.len())]);
let t = Instant::now();
let hits = g.search(Query::text(format!("{w1} {w2}")).budget(Budget::items(20)))?;
samples.push(t.elapsed().as_secs_f64() * 1e3);
hits_total += hits.len();
}
let (p50, p95, max) = percentiles(samples);
println!(
"hybrid search: p50 {p50:7.2}ms p95 {p95:7.2}ms max {max:7.2}ms \
(avg {} hits; FTS only — no query vector; target p95 ≤ 50ms)",
hits_total / ITERS
);
if dim > 0 {
let mut samples = Vec::with_capacity(VEC_ITERS);
let mut hits_total = 0usize;
for _ in 0..VEC_ITERS {
let (w1, w2) = (WORDS[rng.below(WORDS.len())], WORDS[rng.below(WORDS.len())]);
fill_embedding(&mut embed_rng, &mut embedding);
let t = Instant::now();
let hits = g.search(
Query::text(format!("{w1} {w2}"))
.vector(embedding.clone())
.budget(Budget::items(20)),
)?;
samples.push(t.elapsed().as_secs_f64() * 1e3);
hits_total += hits.len();
}
let (p50, p95, max) = percentiles(samples);
println!(
"hybrid+vec (all): p50 {p50:7.2}ms p95 {p95:7.2}ms max {max:7.2}ms \
(avg {} hits; global vec scan; target p95 ≤ 50ms)",
hits_total / VEC_ITERS
);
let mut samples = Vec::with_capacity(VEC_ITERS);
let mut hits_total = 0usize;
for _ in 0..VEC_ITERS {
let (w1, w2) = (WORDS[rng.below(WORDS.len())], WORDS[rng.below(WORDS.len())]);
fill_embedding(&mut embed_rng, &mut embedding);
let t = Instant::now();
let hits = g.search(
Query::text(format!("{w1} {w2}"))
.vector(embedding.clone())
.group(format!("g{}", rng.below(8)))
.budget(Budget::items(20)),
)?;
samples.push(t.elapsed().as_secs_f64() * 1e3);
hits_total += hits.len();
}
let (p50, p95, max) = percentiles(samples);
println!(
"hybrid+vec (grp): p50 {p50:7.2}ms p95 {p95:7.2}ms max {max:7.2}ms \
(avg {} hits; 1/8 partition; target p95 ≤ 50ms)",
hits_total / VEC_ITERS
);
let mut samples = Vec::with_capacity(VEC_ITERS);
for _ in 0..VEC_ITERS {
fill_embedding(&mut embed_rng, &mut embedding);
let t = Instant::now();
g.search(
Query::text("")
.vector(embedding.clone())
.budget(Budget::items(20)),
)?;
samples.push(t.elapsed().as_secs_f64() * 1e3);
}
let (p50, p95, max) = percentiles(samples);
println!("vec legs only: p50 {p50:7.2}ms p95 {p95:7.2}ms max {max:7.2}ms");
let raw = rusqlite::Connection::open_with_flags(
&path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
)
.expect("read-only connection");
for (label, table) in [("nodes", "vec_nodes"), ("edges", "vec_edges")] {
let mut stmt = raw
.prepare(&format!(
"SELECT id FROM {table} WHERE embedding MATCH ?1 AND k = ?2 ORDER BY distance"
))
.expect("prepare knn");
let mut samples = Vec::with_capacity(VEC_ITERS);
for _ in 0..VEC_ITERS {
fill_embedding(&mut embed_rng, &mut embedding);
let blob = f32s_as_bytes(&embedding);
let t = Instant::now();
let n = stmt
.query_map(rusqlite::params![blob, 50i64], |r| r.get::<_, String>(0))
.expect("knn query")
.count();
samples.push(t.elapsed().as_secs_f64() * 1e3);
assert!(n <= 50);
}
let (p50, p95, max) = percentiles(samples);
println!(
"raw knn {label} all: p50 {p50:7.2}ms p95 {p95:7.2}ms max {max:7.2}ms (k=50)"
);
let mut stmt = raw
.prepare(&format!(
"SELECT id FROM {table}
WHERE embedding MATCH ?1 AND k = ?2 AND group_id = ?3
ORDER BY distance"
))
.expect("prepare grouped knn");
let mut samples = Vec::with_capacity(VEC_ITERS);
for _ in 0..VEC_ITERS {
fill_embedding(&mut embed_rng, &mut embedding);
let blob = f32s_as_bytes(&embedding);
let group = format!("g{}", rng.below(8));
let t = Instant::now();
let n = stmt
.query_map(rusqlite::params![blob, 50i64, group], |r| {
r.get::<_, String>(0)
})
.expect("grouped knn query")
.count();
samples.push(t.elapsed().as_secs_f64() * 1e3);
assert!(n <= 50);
}
let (p50, p95, max) = percentiles(samples);
println!(
"raw knn {label} grp: p50 {p50:7.2}ms p95 {p95:7.2}ms max {max:7.2}ms (k=50, 1 of 8 partitions)"
);
}
}
let mut samples = Vec::with_capacity(ITERS);
for _ in 0..ITERS {
let id = node_ids[rng.below(n_nodes)];
let t = Instant::now();
g.node_history(id)?;
samples.push(t.elapsed().as_secs_f64() * 1e3);
}
let (p50, p95, max) = percentiles(samples);
println!("node_history: p50 {p50:7.2}ms p95 {p95:7.2}ms max {max:7.2}ms");
let stats = g.stats()?;
drop(g); let size = std::fs::metadata(&path)?.len() as f64 / 1e6;
let vec_mb = if dim > 0 {
((n_nodes + n_edges) * dim * 4) as f64 / 1e6
} else {
0.0
};
println!(
"\nfinal: {} nodes, {} edges, {} episodes, {} oplog entries, {size:.0} MB on disk \
(~{vec_mb:.0} MB of that is raw vector data)",
stats.nodes, stats.edges, stats.episodes, stats.oplog
);
Ok(())
}