#![allow(missing_docs)]
use std::path::Path;
use std::time::Instant;
use cbvault_format::cbh::record::{GameHeader, RECORD_SIZE};
use cbvault_format::cbh::{Entities, Headers};
use rayon::prelude::*;
const CHUNK: u32 = 8192;
fn build_pool(threads: usize) -> rayon::ThreadPool {
rayon::ThreadPoolBuilder::new().num_threads(threads).build().expect("pool")
}
fn scan(headers: &Headers, first: u32, last: u32, threads: usize, min_elo: u16) -> (f64, Vec<u32>, u64) {
let pool: Vec<u32> = (first..=last).step_by(CHUNK as usize).collect();
let t = Instant::now();
let out: Vec<(Vec<u32>, u64)> = build_pool(threads).install(|| {
pool.par_iter()
.map(|&f| {
let count = (last + 1 - f).min(CHUNK);
let mut buf = vec![0u8; count as usize * RECORD_SIZE];
let read = headers.read_records(f, count, &mut buf).expect("records");
let mut hits = Vec::new();
let mut seen = 0u64;
for i in 0..read as usize {
let b: &[u8; RECORD_SIZE] = buf[i * 46..i * 46 + 46].try_into().expect("46");
let h = GameHeader::from_bytes(f + i as u32, b);
if h.is_deleted() {
continue;
}
seen += 1;
if h.white_elo() >= min_elo && h.black_elo() >= min_elo {
hits.push(h.id());
}
}
(hits, seen)
})
.collect()
});
let secs = t.elapsed().as_secs_f64();
let mut hits = Vec::new();
let mut seen = 0;
for (h, s) in out {
hits.extend(h);
seen += s;
}
(secs, hits, seen)
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let base = Path::new(&args[1]);
let min_elo: u16 = args.get(2).and_then(|v| v.parse().ok()).unwrap_or(2700);
let headers = Headers::open(base).expect("headers");
let last = headers.records();
println!("database: {}\nrecords: {last} header file: {} bytes", base.display(), last as u64 * 46);
println!("filter: white_elo >= {min_elo} AND black_elo >= {min_elo} (pure integer predicates)\n");
let (cold, _, _) = scan(&headers, 1, last, 1, min_elo);
println!("cold (first touch, from disk): {cold:6.2} s {:>10.0} records/s\n", last as f64 / cold);
let mut base_secs = 0.0;
let mut hits = Vec::new();
for threads in [1usize, 2, 4, 10] {
let _ = scan(&headers, 1, last, threads, min_elo); let (secs, h, _) = scan(&headers, 1, last, threads, min_elo);
if threads == 1 {
base_secs = secs;
hits = h;
}
let mib = (last as f64 * 46.0) / (1024.0 * 1024.0);
println!(
"{threads:>2} thread(s): {secs:6.3} s {:>10.0} records/s {:>6.0} MB/s speedup {:4.2}x",
last as f64 / secs,
mib / secs,
base_secs / secs
);
}
println!("\nmatched games: {}", hits.len());
let entities = Entities::open(base).expect("entities");
let sample = hits.len().min(2000);
let t = Instant::now();
let mut bytes = 0usize;
for id in hits.iter().take(sample) {
let h = headers.record(*id).expect("record");
if let Ok(Some(p)) = entities.player(h.white()) {
bytes += p.first.len() + p.last.len();
}
if let Ok(Some(p)) = entities.player(h.black()) {
bytes += p.first.len() + p.last.len();
}
}
let per_ns = t.elapsed().as_secs_f64() / sample.max(1) as f64 * 1e9;
println!(
"resolving both player names of a hit: {per_ns:.0} ns/game ({} names, {:.1} KiB, first {sample} hits)",
sample * 2,
bytes as f64 / 1024.0
);
println!(
"=> a 100-game result page: {:.0} us of name resolution, zero bytes copied (the names are slices of the mapped .cbe)",
per_ns * 100.0 / 1000.0
);
}