use std::path::Path;
use std::time::Instant;
use mrrc::boundary_scanner::RecordBoundaryScanner;
use mrrc::rayon_parser_pool::parse_batch_parallel;
use mrrc::{MarcReader, MarcWriter, RecordHelpers};
type Op = fn(&Path) -> (usize, f64);
fn op_read(path: &Path) -> (usize, f64) {
let start = Instant::now();
let mut reader = MarcReader::from_path(path).expect("open fixture");
let mut count = 0usize;
while let Some(_record) = reader.read_record().expect("read record") {
count += 1;
}
(count, start.elapsed().as_secs_f64())
}
fn op_read_bulk(path: &Path) -> (usize, f64) {
let start = Instant::now();
let buffer = std::fs::read(path).expect("read fixture");
let mut scanner = RecordBoundaryScanner::new();
let boundaries = scanner.scan(&buffer).expect("scan boundaries");
let records = parse_batch_parallel(&boundaries, &buffer).expect("parse batch");
(records.len(), start.elapsed().as_secs_f64())
}
fn op_extract(path: &Path) -> (usize, f64) {
let start = Instant::now();
let mut reader = MarcReader::from_path(path).expect("open fixture");
let mut count = 0usize;
let mut acc = 0usize;
while let Some(record) = reader.read_record().expect("read record") {
count += 1;
if let Some(title) = record.title() {
acc += title.len();
}
for values in record.control_fields.values() {
for value in values {
acc += value.len();
}
}
for field in record.fields() {
acc += field.value().len();
}
}
std::hint::black_box(acc);
(count, start.elapsed().as_secs_f64())
}
fn op_roundtrip(path: &Path) -> (usize, f64) {
let start = Instant::now();
let mut reader = MarcReader::from_path(path).expect("open fixture");
let mut count = 0usize;
let mut acc = 0usize;
while let Some(record) = reader.read_record().expect("read record") {
count += 1;
let mut buffer = Vec::new();
{
let mut writer = MarcWriter::new(&mut buffer);
writer.write_record(&record).expect("serialize record");
}
acc += buffer.len();
}
std::hint::black_box(acc);
(count, start.elapsed().as_secs_f64())
}
fn op_by_name(name: &str) -> Option<Op> {
match name {
"read" => Some(op_read),
"read_bulk" => Some(op_read_bulk),
"extract" => Some(op_extract),
"roundtrip" => Some(op_roundtrip),
_ => None,
}
}
#[allow(clippy::cast_precision_loss)]
fn measure(op: Op, path: &Path, repeat: usize) -> (usize, f64) {
let (warm_count, _) = op(path); let mut count = warm_count;
let mut rates: Vec<f64> = Vec::with_capacity(repeat);
for _ in 0..repeat {
let (n, elapsed) = op(path);
if n != count {
eprintln!("record count changed between runs ({count} vs {n}); aborting");
std::process::exit(1);
}
count = n;
rates.push(if elapsed > 0.0 {
n as f64 / elapsed
} else {
0.0
});
}
rates.sort_by(|a, b| a.partial_cmp(b).expect("no NaN rates"));
let mid = rates.len() / 2;
let median = if rates.len() % 2 == 1 {
rates[mid]
} else {
f64::midpoint(rates[mid - 1], rates[mid])
};
(count, median)
}
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let mut path: Option<String> = None;
let mut repeat = 9usize;
let mut ops_arg = "read,read_bulk,extract,roundtrip".to_string();
let mut json = false;
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--repeat" => {
i += 1;
repeat = args.get(i).and_then(|v| v.parse().ok()).unwrap_or(repeat);
},
"--ops" => {
i += 1;
if let Some(v) = args.get(i) {
ops_arg.clone_from(v);
}
},
"--json" => json = true,
other if !other.starts_with("--") => path = Some(other.to_string()),
other => {
eprintln!("unknown argument: {other}");
std::process::exit(2);
},
}
i += 1;
}
let path = path.unwrap_or_else(|| {
eprintln!(
"usage: benchmark_native <fixture.mrc> [--repeat N] \
[--ops read,read_bulk,extract,roundtrip] [--json]"
);
std::process::exit(2);
});
let path = Path::new(&path);
if !path.exists() {
eprintln!("no such file: {}", path.display());
std::process::exit(2);
}
let op_names: Vec<&str> = ops_arg
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.collect();
let mut results: Vec<(String, usize, f64)> = Vec::new();
for name in &op_names {
let op = op_by_name(name).unwrap_or_else(|| {
eprintln!("unknown operation: {name}");
std::process::exit(2);
});
let (count, rate) = measure(op, path, repeat);
eprintln!(" {name:<10} mrrc (Rust) {rate:>14.0} rec/s ({count} records)");
results.push((name.to_string(), count, rate));
}
if json {
let body: Vec<String> = results
.iter()
.map(|(name, count, rate)| {
format!("\"{name}\":{{\"count\":{count},\"rec_s\":{rate:.1}}}")
})
.collect();
println!("{{{}}}", body.join(","));
}
}