use rpki::repository::cert::Cert; use rpki::repository::crl::Crl; use rpki::repository::manifest::Manifest; use rpki::repository::roa::Roa; use rpki::repository::aspa::Aspa; use rpki::repository::resources::{AsResources, IpResources}; use std::hint::black_box; use std::path::{Path, PathBuf}; use std::time::Instant; #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] enum ObjType { Cer, Crl, Manifest, Roa, Aspa, } impl ObjType { fn parse(s: &str) -> Result { match s { "cer" => Ok(Self::Cer), "crl" => Ok(Self::Crl), "manifest" => Ok(Self::Manifest), "roa" => Ok(Self::Roa), "aspa" => Ok(Self::Aspa), _ => Err("type must be one of: cer, crl, manifest, roa, aspa".into()), } } fn as_str(self) -> &'static str { match self { ObjType::Cer => "cer", ObjType::Crl => "crl", ObjType::Manifest => "manifest", ObjType::Roa => "roa", ObjType::Aspa => "aspa", } } fn ext(self) -> &'static str { match self { ObjType::Cer => "cer", ObjType::Crl => "crl", ObjType::Manifest => "mft", ObjType::Roa => "roa", ObjType::Aspa => "asa", } } } #[derive(Clone, Debug)] struct Sample { obj_type: ObjType, name: String, path: PathBuf, } #[derive(Clone, Debug)] struct Config { dir: PathBuf, type_filter: Option, sample_filter: Option, fixed_iters: Option, warmup_iters: u64, rounds: u64, min_round_ms: u64, max_adaptive_iters: u64, strict: bool, cert_inspect: bool, out_csv: Option, out_md: Option, } fn usage_and_exit(err: Option<&str>) -> ! { if let Some(err) = err { eprintln!("error: {err}"); eprintln!(); } eprintln!( "Usage:\n\ cargo run --release --manifest-path rpki/benchmark/routinator_object_bench/Cargo.toml -- [OPTIONS]\n\ \n\ Options:\n\ --dir Fixtures root dir (default: ../../tests/benchmark/selected_der_v2)\n\ --type Filter by type\n\ --sample Filter by sample name (e.g. p50)\n\ --iters Fixed iterations per round (optional; otherwise adaptive)\n\ --warmup-iters Warmup iterations (default: 50)\n\ --rounds Rounds (default: 5)\n\ --min-round-ms Adaptive: minimum round time (default: 200)\n\ --max-iters Adaptive: maximum iters (default: 1_000_000)\n\ --strict Strict DER where applicable (default: true)\n\ --cert-inspect Also run Cert::inspect_ca/inspect_ee where applicable (default: false)\n\ --out-csv Write CSV output\n\ --out-md Write Markdown output\n\ " ); std::process::exit(2); } fn parse_bool(s: &str, name: &str) -> bool { match s { "1" | "true" | "TRUE" | "yes" | "YES" => true, "0" | "false" | "FALSE" | "no" | "NO" => false, _ => usage_and_exit(Some(&format!("{name} must be true/false"))), } } fn parse_u64(s: &str, name: &str) -> u64 { s.parse::() .unwrap_or_else(|_| usage_and_exit(Some(&format!("{name} must be an integer")))) } fn default_samples_dir() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/benchmark/selected_der_v2") } fn parse_args() -> Config { let mut dir: PathBuf = default_samples_dir(); let mut type_filter: Option = None; let mut sample_filter: Option = None; let mut fixed_iters: Option = None; let mut warmup_iters: u64 = 50; let mut rounds: u64 = 5; let mut min_round_ms: u64 = 200; let mut max_adaptive_iters: u64 = 1_000_000; let mut strict: bool = true; let mut cert_inspect: bool = false; let mut out_csv: Option = None; let mut out_md: Option = None; let mut args = std::env::args().skip(1); while let Some(arg) = args.next() { match arg.as_str() { "--dir" => dir = PathBuf::from(args.next().unwrap_or_else(|| usage_and_exit(None))), "--type" => { type_filter = Some(ObjType::parse( &args.next().unwrap_or_else(|| usage_and_exit(None)), ) .unwrap_or_else(|e| usage_and_exit(Some(&e)))) } "--sample" => { sample_filter = Some(args.next().unwrap_or_else(|| usage_and_exit(None))) } "--iters" => { fixed_iters = Some(parse_u64( &args.next().unwrap_or_else(|| usage_and_exit(None)), "--iters", )) } "--warmup-iters" => { warmup_iters = parse_u64( &args.next().unwrap_or_else(|| usage_and_exit(None)), "--warmup-iters", ) } "--rounds" => { rounds = parse_u64(&args.next().unwrap_or_else(|| usage_and_exit(None)), "--rounds") } "--min-round-ms" => { min_round_ms = parse_u64( &args.next().unwrap_or_else(|| usage_and_exit(None)), "--min-round-ms", ) } "--max-iters" => { max_adaptive_iters = parse_u64( &args.next().unwrap_or_else(|| usage_and_exit(None)), "--max-iters", ) } "--strict" => { strict = parse_bool( &args.next().unwrap_or_else(|| usage_and_exit(None)), "--strict", ) } "--cert-inspect" => cert_inspect = true, "--out-csv" => out_csv = Some(PathBuf::from(args.next().unwrap_or_else(|| usage_and_exit(None)))), "--out-md" => out_md = Some(PathBuf::from(args.next().unwrap_or_else(|| usage_and_exit(None)))), "-h" | "--help" => usage_and_exit(None), _ => usage_and_exit(Some(&format!("unknown argument: {arg}"))), } } if warmup_iters == 0 { usage_and_exit(Some("--warmup-iters must be > 0")); } if rounds == 0 { usage_and_exit(Some("--rounds must be > 0")); } if min_round_ms == 0 { usage_and_exit(Some("--min-round-ms must be > 0")); } if max_adaptive_iters == 0 { usage_and_exit(Some("--max-iters must be > 0")); } if let Some(n) = fixed_iters { if n == 0 { usage_and_exit(Some("--iters must be > 0")); } } Config { dir, type_filter, sample_filter, fixed_iters, warmup_iters, rounds, min_round_ms, max_adaptive_iters, strict, cert_inspect, out_csv, out_md, } } fn read_samples(root: &Path) -> Vec { let mut out = Vec::new(); for obj_type in [ ObjType::Cer, ObjType::Crl, ObjType::Manifest, ObjType::Roa, ObjType::Aspa, ] { let dir = root.join(obj_type.as_str()); let rd = match std::fs::read_dir(&dir) { Ok(rd) => rd, Err(_) => continue, }; for ent in rd.flatten() { let path = ent.path(); if path.extension().and_then(|s| s.to_str()) != Some(obj_type.ext()) { continue; } let name = path .file_stem() .and_then(|s| s.to_str()) .unwrap_or("unknown") .to_string(); out.push(Sample { obj_type, name, path }); } } out.sort_by(|a, b| a.obj_type.cmp(&b.obj_type).then_with(|| a.name.cmp(&b.name))); out } fn choose_iters_adaptive(mut op: F, min_round_ms: u64, max_iters: u64) -> u64 { let min_secs = (min_round_ms as f64) / 1e3; let mut iters: u64 = 1; loop { let start = Instant::now(); for _ in 0..iters { op(); } let elapsed = start.elapsed().as_secs_f64(); if elapsed >= min_secs { return iters; } if iters >= max_iters { return iters; } iters = (iters.saturating_mul(2)).min(max_iters); } } fn count_ip(res: &IpResources) -> u64 { if res.is_inherited() { return 1; } let Ok(blocks) = res.to_blocks() else { return 0; }; blocks.iter().count() as u64 } fn count_as(res: &AsResources) -> u64 { if res.is_inherited() { return 1; } let Ok(blocks) = res.to_blocks() else { return 0; }; blocks.iter().count() as u64 } fn complexity(obj_type: ObjType, bytes: &[u8], strict: bool, cert_inspect: bool) -> u64 { match obj_type { ObjType::Cer => { let cert = Cert::decode(bytes).expect("decode cert"); if cert_inspect { if cert.is_ca() { cert.inspect_ca(strict).expect("inspect ca"); } else { cert.inspect_ee(strict).expect("inspect ee"); } } count_ip(cert.v4_resources()) .saturating_add(count_ip(cert.v6_resources())) .saturating_add(count_as(cert.as_resources())) } ObjType::Crl => { let crl = Crl::decode(bytes).expect("decode crl"); crl.revoked_certs().iter().count() as u64 } ObjType::Manifest => { let mft = Manifest::decode(bytes, strict).expect("decode manifest"); if cert_inspect { mft.cert().inspect_ee(strict).expect("inspect ee"); } mft.content().len() as u64 } ObjType::Roa => { let roa = Roa::decode(bytes, strict).expect("decode roa"); if cert_inspect { roa.cert().inspect_ee(strict).expect("inspect ee"); } roa.content().iter().count() as u64 } ObjType::Aspa => { let asa = Aspa::decode(bytes, strict).expect("decode aspa"); if cert_inspect { asa.cert().inspect_ee(strict).expect("inspect ee"); } asa.content().provider_as_set().len() as u64 } } } fn decode_profile(obj_type: ObjType, bytes: &[u8], strict: bool, cert_inspect: bool) { match obj_type { ObjType::Cer => { let cert = Cert::decode(black_box(bytes)).expect("decode cert"); if cert_inspect { if cert.is_ca() { cert.inspect_ca(strict).expect("inspect ca"); } else { cert.inspect_ee(strict).expect("inspect ee"); } } black_box(cert); } ObjType::Crl => { let crl = Crl::decode(black_box(bytes)).expect("decode crl"); black_box(crl); } ObjType::Manifest => { let mft = Manifest::decode(black_box(bytes), strict).expect("decode manifest"); if cert_inspect { mft.cert().inspect_ee(strict).expect("inspect ee"); } black_box(mft); } ObjType::Roa => { let roa = Roa::decode(black_box(bytes), strict).expect("decode roa"); if cert_inspect { roa.cert().inspect_ee(strict).expect("inspect ee"); } black_box(roa); } ObjType::Aspa => { let asa = Aspa::decode(black_box(bytes), strict).expect("decode aspa"); if cert_inspect { asa.cert().inspect_ee(strict).expect("inspect ee"); } black_box(asa); } } } #[derive(Clone, Debug)] struct ResultRow { obj_type: String, sample: String, size_bytes: usize, complexity: u64, avg_ns_per_op: f64, ops_per_sec: f64, } fn render_markdown(title: &str, rows: &[ResultRow]) -> String { let mut out = String::new(); out.push_str(&format!("# {title}\n\n")); out.push_str("| type | sample | size_bytes | complexity | avg ns/op | ops/s |\n"); out.push_str("|---|---|---:|---:|---:|---:|\n"); for r in rows { out.push_str(&format!( "| {} | {} | {} | {} | {:.2} | {:.2} |\n", r.obj_type, r.sample, r.size_bytes, r.complexity, r.avg_ns_per_op, r.ops_per_sec )); } out } fn render_csv(rows: &[ResultRow]) -> String { let mut out = String::new(); out.push_str("type,sample,size_bytes,complexity,avg_ns_per_op,ops_per_sec\n"); for r in rows { let sample = r.sample.replace('"', "\"\""); out.push_str(&format!( "{},{},{},{},{:.6},{:.6}\n", r.obj_type, format!("\"{}\"", sample), r.size_bytes, r.complexity, r.avg_ns_per_op, r.ops_per_sec )); } out } fn create_parent_dirs(path: &Path) { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent).unwrap_or_else(|e| { panic!("create_dir_all {}: {e}", parent.display()); }); } } fn write_text_file(path: &Path, content: &str) { create_parent_dirs(path); std::fs::write(path, content).unwrap_or_else(|e| panic!("write {}: {e}", path.display())); } fn main() { let cfg = parse_args(); let mut samples = read_samples(&cfg.dir); if samples.is_empty() { usage_and_exit(Some(&format!( "no samples found under: {}", cfg.dir.display() ))); } if let Some(t) = cfg.type_filter { samples.retain(|s| s.obj_type == t); if samples.is_empty() { usage_and_exit(Some(&format!("no sample matched --type {}", t.as_str()))); } } if let Some(filter) = cfg.sample_filter.as_deref() { samples.retain(|s| s.name == filter); if samples.is_empty() { usage_and_exit(Some(&format!("no sample matched --sample {filter}"))); } } println!("# Routinator baseline (rpki crate) decode benchmark (selected_der_v2)"); println!(); println!("- dir: {}", cfg.dir.display()); println!("- strict: {}", cfg.strict); println!("- cert_inspect: {}", cfg.cert_inspect); if let Some(t) = cfg.type_filter { println!("- type: {}", t.as_str()); } if let Some(s) = cfg.sample_filter.as_deref() { println!("- sample: {}", s); } if let Some(n) = cfg.fixed_iters { println!("- iters: {} (fixed)", n); } else { println!( "- warmup: {} iters, rounds: {}, min_round: {}ms (adaptive iters, max {})", cfg.warmup_iters, cfg.rounds, cfg.min_round_ms, cfg.max_adaptive_iters ); } if let Some(p) = cfg.out_csv.as_ref() { println!("- out_csv: {}", p.display()); } if let Some(p) = cfg.out_md.as_ref() { println!("- out_md: {}", p.display()); } println!(); println!("| type | sample | size_bytes | complexity | avg ns/op | ops/s |"); println!("|---|---|---:|---:|---:|---:|"); let mut rows: Vec = Vec::with_capacity(samples.len()); for sample in &samples { let bytes = std::fs::read(&sample.path) .unwrap_or_else(|e| panic!("read {}: {e}", sample.path.display())); let size_bytes = bytes.len(); let complexity = complexity(sample.obj_type, bytes.as_slice(), cfg.strict, cfg.cert_inspect); for _ in 0..cfg.warmup_iters { decode_profile(sample.obj_type, bytes.as_slice(), cfg.strict, cfg.cert_inspect); } let mut per_round_ns_per_op = Vec::with_capacity(cfg.rounds as usize); for _round in 0..cfg.rounds { let iters = if let Some(n) = cfg.fixed_iters { n } else { choose_iters_adaptive( || decode_profile(sample.obj_type, bytes.as_slice(), cfg.strict, cfg.cert_inspect), cfg.min_round_ms, cfg.max_adaptive_iters, ) }; let start = Instant::now(); for _ in 0..iters { decode_profile(sample.obj_type, bytes.as_slice(), cfg.strict, cfg.cert_inspect); } let elapsed = start.elapsed(); let total_ns = elapsed.as_secs_f64() * 1e9; per_round_ns_per_op.push(total_ns / (iters as f64)); } let avg_ns = per_round_ns_per_op.iter().sum::() / (per_round_ns_per_op.len() as f64); let ops_per_sec = 1e9_f64 / avg_ns; println!( "| {} | {} | {} | {} | {:.2} | {:.2} |", sample.obj_type.as_str(), sample.name, size_bytes, complexity, avg_ns, ops_per_sec ); rows.push(ResultRow { obj_type: sample.obj_type.as_str().to_string(), sample: sample.name.clone(), size_bytes, complexity, avg_ns_per_op: avg_ns, ops_per_sec, }); } if let Some(path) = cfg.out_md.as_ref() { let md = render_markdown( "Routinator baseline (rpki crate) decode+inspect (selected_der_v2)", &rows, ); write_text_file(path, &md); eprintln!("Wrote {}", path.display()); } if let Some(path) = cfg.out_csv.as_ref() { let csv = render_csv(&rows); write_text_file(path, &csv); eprintln!("Wrote {}", path.display()); } }