use std::sync::Arc;
use std::time::Instant;
use grit_core::{Budget, GraphOp, Grit, ManualClock, 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",
];
fn p95(mut samples: Vec<f64>) -> f64 {
samples.sort_by(f64::total_cmp);
samples[(samples.len() - 1) * 95 / 100]
}
#[test]
#[ignore = "several-second fixture build; CI runs it via --release -- --ignored"]
fn latency_envelope_tripwire() {
const N_NODES: usize = 30_000;
const N_EDGES: usize = 90_000;
const TRAVERSE_P95_MS: f64 = 30.0;
const SEARCH_P95_MS: f64 = 150.0;
const HISTORY_P95_MS: f64 = 10.0;
let dir = tempfile::tempdir().unwrap();
let clock = Arc::new(ManualClock::new(1_000_000_000));
let g = Grit::open(dir.path().join("env.db"), Options::new("env").clock(clock)).unwrap();
let mut rng = Rng(0x9E37_79B9_7F4A_7C15);
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) = (
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} in context {i}"),
attrs: serde_json::json!({}),
group_id: format!("g{}", i % 8),
})
.unwrap();
node_ids.push(id);
}
let mut edge_ids: Vec<Uuid> = Vec::with_capacity(N_EDGES);
for i in 0..N_EDGES {
let id = g.new_id();
let (w1, w2) = (WORDS[rng.below(WORDS.len())], WORDS[rng.below(WORDS.len())]);
g.apply(GraphOp::AddEdge {
id,
src: node_ids[rng.below(N_NODES)],
dst: node_ids[rng.below(N_NODES)],
rel: "RELATES".into(),
fact: format!("the {w1} constrains the {w2} ({i})"),
attrs: serde_json::json!({}),
group_id: format!("g{}", i % 8),
valid_at: Some(1_000),
invalid_at: None,
})
.unwrap();
edge_ids.push(id);
}
for _ in 0..N_EDGES / 20 {
g.apply(GraphOp::InvalidateEdge {
edge_id: edge_ids[rng.below(N_EDGES)],
invalid_at: 500_000_000,
})
.unwrap();
}
const ITERS: usize = 100;
let mut traverse_ms = Vec::with_capacity(ITERS);
let mut search_ms = Vec::with_capacity(ITERS);
let mut history_ms = 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)).unwrap();
traverse_ms.push(t.elapsed().as_secs_f64() * 1e3);
let (w1, w2) = (WORDS[rng.below(WORDS.len())], WORDS[rng.below(WORDS.len())]);
let t = Instant::now();
g.search(Query::text(format!("{w1} {w2}")).budget(Budget::items(20)))
.unwrap();
search_ms.push(t.elapsed().as_secs_f64() * 1e3);
let id = node_ids[rng.below(N_NODES)];
let t = Instant::now();
g.node_history(id).unwrap();
history_ms.push(t.elapsed().as_secs_f64() * 1e3);
}
let (t, s, h) = (p95(traverse_ms), p95(search_ms), p95(history_ms));
println!("envelope tripwire: traverse p95 {t:.2}ms, search p95 {s:.2}ms, history p95 {h:.2}ms");
assert!(
t <= TRAVERSE_P95_MS,
"traverse p95 {t:.2}ms > {TRAVERSE_P95_MS}ms tripwire"
);
assert!(
s <= SEARCH_P95_MS,
"search p95 {s:.2}ms > {SEARCH_P95_MS}ms tripwire"
);
assert!(
h <= HISTORY_P95_MS,
"node_history p95 {h:.2}ms > {HISTORY_P95_MS}ms tripwire"
);
}