#![allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
use contract_bridge::Strain;
use contract_bridge::deck::full_deal;
use core::hint::black_box;
use pons_dds::Solver;
use rand::SeedableRng;
use rand::rngs::SmallRng;
use std::cell::RefCell;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Instant;
const STRAIN_LABEL: [&str; 5] = ["C", "D", "H", "S", "NT"];
fn percentile(sorted: &[u128], p: f64) -> u128 {
if sorted.is_empty() {
return 0;
}
let last = sorted.len() - 1;
let idx = ((p / 100.0) * last as f64).round() as usize;
sorted[idx.min(last)]
}
fn main() {
let n: usize = std::env::args()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(500);
let mut rng = SmallRng::seed_from_u64(0);
let deals: Vec<_> = (0..n).map(|_| full_deal(&mut rng)).collect();
let strains = Strain::ASC;
let tasks: Vec<(usize, usize)> = (0..deals.len())
.flat_map(|d| (0..strains.len()).map(move |s| (d, s)))
.collect();
thread_local! {
static SOLVER: RefCell<Option<Solver>> = const { RefCell::new(None) };
}
let pool = rayon::ThreadPoolBuilder::new()
.stack_size(16 * 1024 * 1024)
.build()
.expect("failed to build thread pool");
let threads = pool.current_num_threads();
let cursor = AtomicUsize::new(0);
let start = Instant::now();
let timings: Vec<(usize, u128)> = pool
.broadcast(|_| {
SOLVER.with(|cell| {
let mut slot = cell.borrow_mut();
let solver = slot.get_or_insert_with(|| Solver::new(Strain::Notrump));
let mut local = Vec::new();
loop {
let i = cursor.fetch_add(1, Ordering::Relaxed);
let Some(&(d, s)) = tasks.get(i) else { break };
solver.set_strain(strains[s]);
let t0 = Instant::now();
black_box(solver.solve(deals[d]));
local.push((s, t0.elapsed().as_nanos()));
}
local
})
})
.into_iter()
.flatten()
.collect();
let makespan = start.elapsed();
let sum_nanos: u128 = timings.iter().map(|&(_, ns)| ns).sum();
let ideal_nanos = sum_nanos as f64 / threads as f64;
let tail_ratio = makespan.as_nanos() as f64 / ideal_nanos;
let ms = |nanos: f64| nanos / 1.0e6;
println!(
"deals: {n} tasks: {} threads: {threads} (seed 0)\n",
tasks.len()
);
println!(
"makespan {:>9.2} ms",
ms(makespan.as_nanos() as f64)
);
println!(
"ms/deal {:>9.3} ms",
ms(makespan.as_nanos() as f64) / n as f64
);
println!(
"Σ task time {:>9.2} ms (total compute)",
ms(sum_nanos as f64)
);
println!("ideal (Σ/thr) {:>9.2} ms", ms(ideal_nanos));
println!("tail ratio {tail_ratio:>9.2}x (1.00 = perfect balance)\n");
println!("per-strain task cost (ms):");
println!(
"{:>6} {:>8} {:>8} {:>8} {:>8}",
"strain", "mean", "median", "p95", "share%"
);
for (s, label) in STRAIN_LABEL.iter().enumerate() {
let mut v: Vec<u128> = timings
.iter()
.filter(|&&(ts, _)| ts == s)
.map(|&(_, ns)| ns)
.collect();
v.sort_unstable();
let strain_sum: u128 = v.iter().sum();
let mean = strain_sum as f64 / v.len() as f64;
let median = percentile(&v, 50.0) as f64;
let p95 = percentile(&v, 95.0) as f64;
let share = 100.0 * strain_sum as f64 / sum_nanos as f64;
println!(
"{label:>6} {:>8.2} {:>8.2} {:>8.2} {:>7.1}%",
ms(mean),
ms(median),
ms(p95),
share
);
}
}