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 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 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"
);
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
);
const ITERS: usize = 200;
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; target p95 ≤ 50ms)",
hits_total / ITERS
);
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;
println!(
"\nfinal: {} nodes, {} edges, {} episodes, {} oplog entries, {size:.0} MB on disk",
stats.nodes, stats.edges, stats.episodes, stats.oplog
);
Ok(())
}