use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use rayon::prelude::*;
use std::{hint::black_box, sync::Arc};
#[path = "bench_common.rs"]
mod bench_common;
use bench_common::*;
const REPEAT: usize = 100;
fn make_pool(threads: usize) -> rayon::ThreadPool {
rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build()
.expect("thread pool")
}
fn parallel_run(html: &str, pool: &rayon::ThreadPool) -> Vec<String> {
let inputs: Vec<&str> = std::iter::repeat(html).take(REPEAT).collect();
pool.install(|| {
inputs
.par_iter()
.map(|h| mdka::html_to_markdown(black_box(h)))
.collect()
})
}
fn bench_parallel(c: &mut Criterion) {
let datasets = load_datasets();
let html = Arc::new(
datasets
.iter()
.find(|(n, _)| *n == "medium")
.map(|(_, h)| h.clone())
.expect("medium dataset"),
);
let max = rayon::current_num_threads().max(1);
let counts: Vec<usize> = [1usize, 2, 4, max]
.iter()
.copied()
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect();
{
let html = Arc::clone(&html);
let mut group = c.benchmark_group("parallel/sequential");
group.sample_size(10);
group.throughput(Throughput::Bytes((html.len() * REPEAT) as u64));
group.bench_function("baseline", move |b| {
let html = Arc::clone(&html);
b.iter(|| {
(0..REPEAT)
.map(|_| mdka::html_to_markdown(black_box(&*html)))
.collect::<Vec<_>>()
})
});
group.finish();
}
let mut group = c.benchmark_group("parallel/rayon");
group.sample_size(10);
group.throughput(Throughput::Bytes((html.len() * REPEAT) as u64));
for &threads in &counts {
let pool = Arc::new(make_pool(threads));
let html_ref = Arc::clone(&html);
let pool_ref = Arc::clone(&pool);
group.bench_with_input(
BenchmarkId::new("threads", threads),
&threads,
move |b, _| {
let html = Arc::clone(&html_ref);
let pool = Arc::clone(&pool_ref);
b.iter(|| parallel_run(&html, &pool))
},
);
}
group.finish();
}
fn bench_files_to_disk(c: &mut Criterion) {
use std::fs;
use std::path::PathBuf;
let tmp = std::env::temp_dir().join("mdka_parallel_bench");
fs::create_dir_all(&tmp).unwrap();
let out = tmp.join("out");
fs::create_dir_all(&out).unwrap();
let manifest = std::env!("CARGO_MANIFEST_DIR");
let src = fs::read_to_string(format!("{}/benches/benchdata/medium.html", manifest)).unwrap();
let paths: Vec<PathBuf> = (0..20)
.map(|i| {
let p = tmp.join(format!("input_{:02}.html", i));
fs::write(&p, &src).unwrap();
p
})
.collect();
let max = rayon::current_num_threads().max(1);
let counts: Vec<usize> = [1usize, 2, max]
.iter()
.copied()
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect();
for &threads in &counts {
let pool = make_pool(threads);
let mut group = c.benchmark_group(format!("files_to_disk/threads/{}", threads));
group.sample_size(10);
group.throughput(Throughput::Bytes((src.len() * 20) as u64));
group.bench_function("convert", |b| {
b.iter(|| {
pool.install(|| {
paths.par_iter().for_each(|p| {
let html = std::fs::read_to_string(p).unwrap();
let md = mdka::html_to_markdown(black_box(&html));
let out_p = out.join(p.file_stem().unwrap()).with_extension("md");
std::fs::write(out_p, md).unwrap();
});
});
})
});
group.finish();
}
let _ = fs::remove_dir_all(&tmp);
}
fn emit_speedup_report() {
let datasets = load_datasets();
let html = datasets
.iter()
.find(|(n, _)| *n == "medium")
.map(|(_, h)| h.as_str())
.expect("medium dataset");
let max = rayon::current_num_threads().max(1);
let counts: Vec<usize> = [1usize, 2, 4, max]
.iter()
.copied()
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect();
eprintln!(
"\n=== Parallel Speedup (medium × {} iterations, {} cores) ===",
REPEAT, max
);
eprintln!(
"{:<10} {:>12} {:>14} {:>10}",
"threads", "time_ms", "throughput", "speedup"
);
eprintln!("{}", "─".repeat(50));
let mut records = Vec::new();
let mut baseline_ns: Option<u64> = None;
for &threads in &counts {
let pool = make_pool(threads);
parallel_run(html, &pool);
let ns = wall_median_ns(
|| {
parallel_run(html, &pool);
},
5,
);
let ms = ns as f64 / 1e6;
let mbps = (html.len() * REPEAT) as f64 / 1_048_576.0 / (ns as f64 / 1e9);
let base = *baseline_ns.get_or_insert(ns);
let speedup = base as f64 / ns as f64;
eprintln!(
"{:<10} {:>12.1} {:>12.1} MB/s {:>8.2}x",
threads, ms, mbps, speedup
);
records.push(CsvRecord {
benchmark_name: format!("parallel/rayon/threads/{}", threads),
input_size: html.len() * REPEAT,
threads,
time_ns: ns,
memory_bytes: 0,
});
}
eprintln!();
append_csv("target/bench_results.csv", &records);
eprintln!(
"[parallel] CSV → target/bench_results.csv ({} rows)",
records.len()
);
}
fn setup(c: &mut Criterion) {
emit_speedup_report();
bench_parallel(c);
bench_files_to_disk(c);
}
criterion_group!(benches, setup);
criterion_main!(benches);