use ifc_lite_geometry::csg::ClippingProcessor;
use ifc_lite_geometry::csg_capture::{drain, CapturedCsgJob};
use ifc_lite_geometry::mesh::Mesh;
use ifc_lite_processing::process_geometry;
use rayon::prelude::*;
use std::time::Instant;
fn replay(job: &CapturedCsgJob) -> usize {
let csg = ClippingProcessor::new();
match job {
CapturedCsgJob::Single { host, cutter } => {
csg.subtract_mesh(host, cutter).map(|m| m.indices.len()).unwrap_or(0)
}
CapturedCsgJob::Many { host, cutters } => {
let refs: Vec<&Mesh> = cutters.iter().collect();
csg.subtract_mesh_many(host, &refs).map(|m| m.indices.len()).unwrap_or(0)
}
}
}
fn run_pool(jobs: &[CapturedCsgJob], threads: usize) -> (u128, usize) {
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build()
.expect("build rayon pool");
let t = Instant::now();
let fingerprint = pool.install(|| jobs.par_iter().map(replay).sum::<usize>());
(t.elapsed().as_micros(), fingerprint)
}
fn main() {
let fixture = std::env::args()
.nth(1)
.or_else(|| std::env::var("CSG_BENCH_FIXTURE").ok())
.unwrap_or_else(|| "tests/models/ara3d/C20-Institute-Var-2.ifc".to_string());
let iters: usize = std::env::var("CSG_BENCH_ITERS").ok().and_then(|s| s.parse().ok()).unwrap_or(4);
let content = match std::fs::read_to_string(&fixture) {
Ok(c) => c,
Err(e) => {
eprintln!("skip: cannot read fixture {fixture}: {e}");
eprintln!(" (run `pnpm fixtures` or pass a path / CSG_BENCH_FIXTURE)");
return;
}
};
let max_threads = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(8);
println!("fixture: {fixture} ({:.1} MB)", content.len() as f64 / 1e6);
println!("host cores (available_parallelism): {max_threads}");
println!("\n[capture] driving native pipeline once to record real void-cut jobs…");
let cap_t = Instant::now();
ifc_lite_geometry::csg_capture::set_enabled(true);
let _ = process_geometry(&content);
ifc_lite_geometry::csg_capture::set_enabled(false);
let cap_ms = cap_t.elapsed().as_millis();
let jobs = drain();
if jobs.is_empty() {
eprintln!("skip: 0 CSG jobs captured — this model has no void cuts. Try a model with openings.");
return;
}
let (mut n_single, mut n_many, mut n_cutters, mut host_tris, mut cutter_tris) = (0usize, 0usize, 0usize, 0usize, 0usize);
for j in &jobs {
match j {
CapturedCsgJob::Single { host, cutter } => {
n_single += 1;
n_cutters += 1;
host_tris += host.indices.len() / 3;
cutter_tris += cutter.indices.len() / 3;
}
CapturedCsgJob::Many { host, cutters } => {
n_many += 1;
n_cutters += cutters.len();
host_tris += host.indices.len() / 3;
cutter_tris += cutters.iter().map(|c| c.indices.len() / 3).sum::<usize>();
}
}
}
let working_set_mb = jobs.iter().map(|j| match j {
CapturedCsgJob::Single { host, cutter } => mesh_bytes(host) + mesh_bytes(cutter),
CapturedCsgJob::Many { host, cutters } => mesh_bytes(host) + cutters.iter().map(mesh_bytes).sum::<usize>(),
}).sum::<usize>() as f64 / 1e6;
println!("[capture] full pipeline ran in {cap_ms} ms (incl. parse+decode+extrude+CSG)");
if let Ok(path) = std::env::var("CSG_BENCH_DUMP") {
let blob = ifc_lite_geometry::csg_capture::serialize(&jobs);
match std::fs::write(&path, &blob) {
Ok(()) => println!("[dump] wrote {} jobs -> {path} ({:.1} MB)", jobs.len(), blob.len() as f64 / 1e6),
Err(e) => eprintln!("[dump] failed to write {path}: {e}"),
}
}
println!("\n=== CSG corpus (the timed working set, decode EXCLUDED) ===");
println!(" jobs: {} ({} batched 'many', {} single)", jobs.len(), n_many, n_single);
println!(" total cutters: {n_cutters}");
println!(" host triangles: {host_tris}");
println!(" cutter triangles:{cutter_tris}");
println!(" input working set: {working_set_mb:.1} MB (L2/L3 cache caveat: a small set understates the memory cost wasm's imported-memory tax would expose)");
println!("\n[warmup] one untimed serial pass…");
let (_, baseline_fp) = run_pool(&jobs, 1);
let mut thread_counts: Vec<usize> = vec![1, 2, 4, 8, max_threads];
thread_counts.retain(|&n| n <= max_threads);
thread_counts.sort_unstable();
thread_counts.dedup();
println!("\n=== scaling sweep ({iters} timed iters each, reporting MIN wall) ===");
println!("{:>8} | {:>11} | {:>8} | {:>10} | {}", "threads", "min ms", "speedup", "efficiency", "parity");
let mut serial_ms: Option<f64> = None;
for &n in &thread_counts {
let _ = run_pool(&jobs, n);
let mut best = u128::MAX;
let mut ok = true;
for _ in 0..iters {
let (us, fp) = run_pool(&jobs, n);
if fp != baseline_fp {
ok = false;
}
best = best.min(us);
}
let ms = best as f64 / 1000.0;
if n == 1 {
serial_ms = Some(ms);
}
let speedup = serial_ms.map(|s| s / ms).unwrap_or(1.0);
let eff = speedup / n as f64 * 100.0;
println!(
"{:>8} | {:>11.1} | {:>7.2}x | {:>9.0}% | {}",
n, ms, speedup, eff, if ok { "ok" } else { "MISMATCH!" }
);
}
println!("\nReading: efficiency near 100% at high thread counts ⇒ exact CSG is core-scalable ⇒");
println!("rung 2 (wasm-threaded build) is worth measuring. Efficiency collapsing toward 1/N ⇒");
println!("the work is memory/bandwidth-bound even on native ⇒ wasm threading will not help. Gate: >=2x at max threads.");
}
fn mesh_bytes(m: &Mesh) -> usize {
m.positions.len() * 4 + m.normals.len() * 4 + m.indices.len() * 4
}