use std::collections::BTreeSet;
use std::time::{Duration, Instant};
use rete_core::parallel::{
batch_reach_parallel, count_predicate_parallel, count_predicate_serial,
count_predicate_tiles_serial, out_degree_parallel, out_degree_serial,
};
use rete_core::{
batch_reach_serial, build_adjacency, build_dendrogram, build_pyramid_meta,
choose_round_for_budget, project_graph, tile_by_community, write_dataset, Dictionary,
DictionaryBuilder, GraphIndexBuilder, Rete, DEFAULT_TILE_BUDGET,
};
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
fn below(&mut self, n: usize) -> usize {
(self.next() % n as u64) as usize
}
fn unit(&mut self) -> f64 {
(self.next() >> 11) as f64 / (1u64 << 53) as f64
}
}
fn gen_triples(
people: usize,
knows_per: usize,
comm_size: usize,
) -> (Dictionary, Vec<(u32, u32, u32)>) {
let mut db = DictionaryBuilder::new();
let p = |i: usize| format!("http://ex/p{i}");
let age = |i: usize| format!("{}", 18 + (i % 60));
let mut rng = Rng(0x9E3779B97F4A7C15);
for i in 0..people {
db.observe(&p(i), "http://ex/age", &age(i));
db.observe(&p(i), "http://ex/name", &format!("Person {i}"));
}
let mut edges: Vec<(usize, usize)> = Vec::new();
for i in 0..people {
let c = i / comm_size;
let lo = c * comm_size;
let hi = ((c + 1) * comm_size).min(people);
for _ in 0..knows_per {
let j = if rng.unit() < 0.9 && hi - lo > 1 {
lo + rng.below(hi - lo)
} else {
rng.below(people)
};
if j != i {
db.observe(&p(i), "http://ex/knows", &p(j));
edges.push((i, j));
}
}
}
let dict = db.build();
let mut triples = Vec::new();
for i in 0..people {
triples.push(dict.encode(&p(i), "http://ex/age", &age(i)).unwrap());
triples.push(
dict.encode(&p(i), "http://ex/name", &format!("Person {i}"))
.unwrap(),
);
}
for (i, j) in edges {
triples.push(dict.encode(&p(i), "http://ex/knows", &p(j)).unwrap());
}
triples.sort_unstable();
triples.dedup();
(dict, triples)
}
fn best_of<T>(reps: usize, mut f: impl FnMut() -> T) -> (Duration, T) {
let mut out = f(); let mut best = Duration::MAX;
for _ in 0..reps {
let t = Instant::now();
out = f();
best = best.min(t.elapsed());
}
(best, out)
}
fn ms(d: Duration) -> f64 {
d.as_secs_f64() * 1000.0
}
fn report(name: &str, serial: Duration, parallel: Duration, matched: bool) {
let speedup = serial.as_secs_f64() / parallel.as_secs_f64();
println!("\n## {name}");
println!(" serial : {:>9.3} ms", ms(serial));
println!(" parallel : {:>9.3} ms", ms(parallel));
println!(" speedup : {speedup:>9.2}x");
if matched {
println!(" RESULTS MATCH");
} else {
println!(" RESULTS MISMATCH");
panic!("benchmark integrity violation: {name} serial != parallel");
}
}
fn main() {
let cores = std::thread::available_parallelism().map_or(0, |n| n.get());
println!("=== rete parallel evaluator benchmark ===");
println!("available_parallelism (cores seen): {cores}");
println!("rayon threads: {}", rayon::current_num_threads());
let people: usize = std::env::var("BENCH_PEOPLE")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(20_000);
let knows_per = 5;
let comm_size = 100;
println!(
"\nbuilding graph: {people} people, {knows_per} knows/each, communities of {comm_size}"
);
let t0 = Instant::now();
let (dict, triples) = gen_triples(people, knows_per, comm_size);
println!(
"generated {} triples in {:.0} ms",
triples.len(),
ms(t0.elapsed())
);
let t0 = Instant::now();
let mut def = GraphIndexBuilder::new();
for &t in &triples {
def.push(t);
}
let (meta, levels) = build_pyramid_meta(&dict, &triples, DEFAULT_TILE_BUDGET);
let bytes = write_dataset(&dict, &def.build(), &[], false, &meta, levels);
let rete = Rete::open(&bytes).expect("open .rete");
println!(
"built + opened .rete ({} bytes, {} pyramid levels) in {:.0} ms",
bytes.len(),
levels,
ms(t0.elapsed())
);
let g = project_graph(&dict, &triples);
let dend = build_dendrogram(&g);
let round = choose_round_for_budget(&dict, &triples, &dend, DEFAULT_TILE_BUDGET);
let tiles = tile_by_community(&dict, &triples, &dend, round);
println!("tiled into {} communities at round {round}", tiles.len());
let knows_pid = dict.predicate_id("http://ex/knows").unwrap();
let (s, sv) = best_of(20, || count_predicate_serial(&rete, "http://ex/knows"));
let (pn, pv) = best_of(20, || count_predicate_parallel(&tiles, knows_pid));
assert_eq!(sv, count_predicate_tiles_serial(&tiles, knows_pid));
report(
"Workload A — predicate count (cheap scan; parallel expected to lose)",
s,
pn,
sv == pv,
);
println!(" (count = {sv} knows edges)");
let (s, sv) = best_of(10, || out_degree_serial(&tiles));
let (pn, pv) = best_of(10, || out_degree_parallel(&tiles));
report(
"Workload B — per-subject out-degree (per-community + BTreeMap merge)",
s,
pn,
sv == pv,
);
println!(" (distinct subjects = {})", sv.len());
let adj = build_adjacency(&rete, "http://ex/knows");
let n_seeds = 512.min(people);
let stride = (people / n_seeds).max(1);
let seeds: Vec<u32> = (0..people)
.step_by(stride)
.filter_map(|i| dict.node_of_term(&format!("http://ex/p{i}")))
.take(n_seeds)
.collect();
println!("\nbatch reachability over {} seeds", seeds.len());
let (s, sv) = best_of(5, || batch_reach_serial(&adj, &seeds));
let (pn, pv) = best_of(5, || batch_reach_parallel(&adj, &seeds));
let total_reached: usize = sv.iter().map(BTreeSet::len).sum();
report(
"Workload C — batch reachability (embarrassingly parallel)",
s,
pn,
sv == pv,
);
println!(
" (sum of per-seed reach set sizes = {total_reached}, avg {:.0}/seed)",
total_reached as f64 / seeds.len().max(1) as f64
);
println!("\n=== done; all RESULTS MATCH ===");
}