use anyhow::Result;
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
use fcb_core::{FcbReader, GeometryType};
use prettytable::{format, Cell, Row, Table};
use std::{
collections::HashMap,
fs::File,
io::BufReader,
time::{Duration, Instant},
};
fn read_fcb_with_cur_feature(path: &str) -> Result<(u64, u64, u64)> {
let input_file = File::open(path)?;
let inputreader = BufReader::new(input_file);
let mut reader = FcbReader::open(inputreader)?.select_all()?;
let header = reader.header();
let feat_count = header.features_count();
let mut solid_count = 0;
let mut multi_surface_count = 0;
let mut other_count = 0;
let mut feat_num = 0;
while let Some(feat_buf) = reader.next()? {
let feature = feat_buf.cur_feature();
feature
.objects()
.into_iter()
.flatten()
.flat_map(|city_object| city_object.geometry().unwrap_or_default())
.for_each(|geometry| match geometry.type_() {
GeometryType::Solid => solid_count += 1,
GeometryType::MultiSurface => multi_surface_count += 1,
_ => other_count += 1,
});
feat_num += 1;
if feat_num == feat_count {
break;
}
}
Ok((solid_count, multi_surface_count, other_count))
}
fn read_fcb_with_cur_cj_feature(path: &str) -> Result<(u64, u64, u64)> {
let input_file = File::open(path)?;
let inputreader = BufReader::new(input_file);
let mut reader = FcbReader::open(inputreader)?.select_all()?;
let header = reader.header();
let feat_count = header.features_count();
let mut solid_count = 0;
let mut multi_surface_count = 0;
let mut other_count = 0;
let mut feat_num = 0;
while let Some(feat_buf) = reader.next()? {
let feature = feat_buf.cur_cj_feature()?;
feature.city_objects.iter().for_each(|(_, co)| {
if let Some(geometries) = &co.geometry {
for geometry in geometries {
match geometry.thetype {
cjseq::GeometryType::Solid => solid_count += 1,
cjseq::GeometryType::MultiSurface => multi_surface_count += 1,
_ => other_count += 1,
}
}
}
});
feat_num += 1;
if feat_num == feat_count {
break;
}
}
Ok((solid_count, multi_surface_count, other_count))
}
#[derive(Debug)]
struct BenchResult {
function: String,
duration: Duration,
}
fn format_duration(d: Duration) -> String {
if d.as_secs() > 0 {
format!("{:.2}s", d.as_secs_f64())
} else {
format!("{:.2}ms", d.as_secs_f64() * 1000.0)
}
}
fn benchmark_read_fn<F>(
iterations: u32,
function_name: &str,
path: &str,
read_fn: F,
) -> Result<BenchResult>
where
F: Fn(&str) -> Result<(u64, u64, u64)>,
{
let mut total_duration = Duration::new(0, 0);
for i in 0..iterations {
let iter_start = Instant::now();
let _ = read_fn(black_box(path))?;
let iter_duration = iter_start.elapsed();
total_duration += iter_duration;
if iterations > 1 && (i + 1) % (iterations / 10).max(1) == 0 {
println!(
"progress: {}/{} iterations for {} - {}",
i + 1,
iterations,
function_name,
path
);
}
}
let avg_duration = if iterations > 0 {
total_duration / iterations
} else {
Duration::new(0, 0)
};
Ok(BenchResult {
function: function_name.to_string(),
duration: avg_duration,
})
}
fn print_benchmark_results(results: &HashMap<String, HashMap<String, BenchResult>>) {
let mut table = Table::new();
table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
let mut header_cells = vec![Cell::new("Function")];
let datasets: Vec<&String> = results.keys().collect();
datasets.iter().for_each(|dataset| {
header_cells.push(Cell::new(dataset));
});
table.add_row(Row::new(header_cells));
let mut all_functions = Vec::new();
for dataset_results in results.values() {
for function_name in dataset_results.keys() {
if !all_functions.contains(function_name) {
all_functions.push(function_name.clone());
}
}
}
all_functions.sort();
for function_name in &all_functions {
let mut row_cells = vec![Cell::new(function_name)];
for dataset in &datasets {
if let Some(result) = results.get(*dataset).and_then(|dr| dr.get(function_name)) {
row_cells.push(Cell::new(&format_duration(result.duration)));
} else {
row_cells.push(Cell::new("N/A"));
}
}
table.add_row(Row::new(row_cells));
}
println!("\nFunction Benchmark Results:");
table.printstd();
let mut comparison_table = Table::new();
comparison_table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
let mut comp_header_cells = vec![Cell::new("Dataset")];
for function_name in &all_functions {
comp_header_cells.push(Cell::new(function_name));
}
comparison_table.add_row(Row::new(comp_header_cells));
for dataset in &datasets {
let mut row_cells = vec![Cell::new(dataset)];
let mut baseline_duration = Duration::from_secs(u64::MAX);
if let Some(dataset_results) = results.get(*dataset) {
for result in dataset_results.values() {
if result.duration < baseline_duration {
baseline_duration = result.duration;
}
}
}
for function_name in &all_functions {
if let Some(result) = results.get(*dataset).and_then(|dr| dr.get(function_name)) {
let relative = result.duration.as_secs_f64() / baseline_duration.as_secs_f64();
row_cells.push(Cell::new(&format!("{relative:.2}x")));
} else {
row_cells.push(Cell::new("N/A"));
}
}
comparison_table.add_row(Row::new(row_cells));
}
println!("\nRelative Performance (lower is better):");
comparison_table.printstd();
}
const DATASETS: &[(&str, &str)] = &[
("Ingolstadt", "benchmark_data/Ingolstadt.city.fcb"),
];
pub fn read_func_benchmark(c: &mut Criterion) {
let mut group = c.benchmark_group("read_func");
let iterations: u32 = 10;
group
.sample_size(iterations as usize)
.warm_up_time(Duration::from_secs(5));
let mut all_results: HashMap<String, HashMap<String, BenchResult>> = HashMap::new();
let mut real_time_table = Table::new();
real_time_table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
real_time_table.add_row(Row::new(vec![
Cell::new("Dataset"),
Cell::new("Function"),
Cell::new("Duration"),
]));
println!("\nFunction Benchmark Results (Real-time):");
real_time_table.printstd();
for (dataset_name, fcb_path) in DATASETS {
let mut dataset_results = HashMap::new();
let mut dataset_table = Table::new();
dataset_table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
dataset_table.add_row(Row::new(vec![Cell::new("Function"), Cell::new("Duration")]));
println!("benchmarking cur_feature for dataset: {dataset_name}");
let result = benchmark_read_fn(
iterations,
"cur_feature",
fcb_path,
read_fcb_with_cur_feature,
)
.unwrap_or_else(|e| {
println!("error in cur_feature benchmark: {e:?}");
BenchResult {
function: "cur_feature".to_string(),
duration: Duration::new(0, 0),
}
});
dataset_table.add_row(Row::new(vec![
Cell::new(&result.function),
Cell::new(&format_duration(result.duration)),
]));
group.bench_with_input(
BenchmarkId::new("cur_feature", dataset_name),
fcb_path,
|b, path| b.iter(|| read_fcb_with_cur_feature(black_box(path))),
);
dataset_results.insert("cur_feature".to_string(), result);
println!("benchmarking cur_cj_feature for dataset: {dataset_name}");
let result = benchmark_read_fn(
iterations,
"cur_cj_feature",
fcb_path,
read_fcb_with_cur_cj_feature,
)
.unwrap_or_else(|e| {
println!("error in cur_cj_feature benchmark: {e:?}");
BenchResult {
function: "cur_cj_feature".to_string(),
duration: Duration::new(0, 0),
}
});
dataset_table.add_row(Row::new(vec![
Cell::new(&result.function),
Cell::new(&format_duration(result.duration)),
]));
group.bench_with_input(
BenchmarkId::new("cur_cj_feature", dataset_name),
fcb_path,
|b, path| b.iter(|| read_fcb_with_cur_cj_feature(black_box(path))),
);
dataset_results.insert("cur_cj_feature".to_string(), result);
println!("\nResults for dataset: {dataset_name}");
dataset_table.printstd();
all_results.insert(dataset_name.to_string(), dataset_results);
}
group.finish();
print_benchmark_results(&all_results);
}
criterion_group!(benches, read_func_benchmark);
criterion_main!(benches);