grit-core 0.2.4

Embedded, bi-temporal property graph for agent memory: one SQLite file, in-process, deterministic
Documentation
//! Read-path benchmark against an existing quickbench database (fresh
//! connection, checkpointed WAL — the normal "open your memory file and
//! query" shape). Usage:
//!
//! ```sh
//! cargo run --release -p grit-core --example readbench -- <path/to/bench.db>
//! ```

use std::time::Instant;

use grit_core::{Budget, 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",
];

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 main() -> Result<(), Box<dyn std::error::Error>> {
    let path = std::env::args()
        .nth(1)
        .expect("usage: readbench <bench.db>");
    let mut rng = Rng(0xDEAD_BEEF_CAFE_F00D);

    // Grab seed ids directly (grit has no "list all nodes" API — this is a
    // bench harness, not a product path).
    let node_ids: Vec<Uuid> = {
        let conn = rusqlite::Connection::open_with_flags(
            &path,
            rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
        )?;
        let mut stmt = conn.prepare("SELECT id FROM nodes")?;
        stmt.query_map([], |r| r.get::<_, String>(0))?
            .filter_map(|r| r.ok().and_then(|s| Uuid::parse_str(&s).ok()))
            .collect::<Vec<_>>()
    };
    println!("{} nodes available as seeds", node_ids.len());

    let g = Grit::open(&path, Options::new("readbench"))?;
    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(node_ids.len())];
        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; target p95 ≤ 10ms)",
        reached / ITERS
    );

    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(node_ids.len())];
        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");
    Ok(())
}