index_list 0.3.3

A doubly linked list implemented in safe Rust using vector indexes
Documentation
use comfy_table::{Cell, Row, Table};
use serde::Deserialize;
use std::collections::HashMap;
use std::env;
use std::fs::File;
use std::io::BufReader;
use walkdir::WalkDir;

#[derive(Deserialize, Debug)]
struct Estimate {
    point_estimate: f64,
}

#[derive(Deserialize, Debug)]
struct Benchmark {
    mean: Estimate,
}

fn main() {
    let args: Vec<String> = env::args().collect();
    let mut versions = vec!["new"];

    if args.len() > 1 {
        versions.clear();
        if args.contains(&"--base".to_string()) {
            versions.push("base");
        }
        if args.contains(&"--new".to_string()) {
            versions.push("new");
        }
    }

    for version in versions {
        generate_table(version);
    }
}

fn generate_table(version: &str) {
    let mut results: HashMap<String, HashMap<String, f64>> = HashMap::new();

    for entry in WalkDir::new("target/criterion")
        .into_iter()
        .filter_map(|e| e.ok())
    {
        if entry.file_name().to_str() == Some("estimates.json") {
            let path = entry.path();
            if path.to_str().map_or(false, |s| s.contains(&format!("/{}/", version))) {
                if let Some(parent) = path.parent() {
                    if let Some(benchmark_dir) = parent.parent() {
                        if let Some(benchmark_name) = benchmark_dir.file_name() {
                            if let Some(benchmark_name_str) = benchmark_name.to_str() {
                                let file = File::open(path).unwrap();
                                let reader = BufReader::new(file);
                                let benchmark: Benchmark = serde_json::from_reader(reader).unwrap();

                                let parts: Vec<&str> = benchmark_name_str.split('-').collect();
                                if parts.len() >= 2 {
                                    let implementation = parts[0].to_string();
                                    let operation = parts[1..].join("-");

                                    results
                                        .entry(operation)
                                        .or_default()
                                        .insert(implementation, benchmark.mean.point_estimate);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    if results.is_empty() {
        println!("No benchmark results found for '{}'. Run 'cargo bench' first.", version);
        return;
    }

    println!("\nResults for: {}", version);

    let mut operations: Vec<String> = results.keys().cloned().collect();
    operations.sort();

    let mut implementations: Vec<String> = results
        .values()
        .flat_map(|x| x.keys())
        .map(|s| s.to_string())
        .collect();
    implementations.sort();
    implementations.dedup();

    let mut table = Table::new();
    let mut header = vec![
        Cell::new("Benchmark"),
        Cell::new("Best Time"),
        Cell::new("Worst Time"),
    ];
    for impl_name in &implementations {
        header.push(Cell::new(impl_name));
    }
    table.set_header(header);

    for op in operations {
        let mut row = Row::new();
        row.add_cell(Cell::new(&op));

        let op_results = results.get(&op).unwrap();
        let best_time = op_results.values().cloned().fold(f64::INFINITY, f64::min);
        let worst_time = op_results.values().cloned().fold(f64::NEG_INFINITY, f64::max);

        row.add_cell(Cell::new(format_time(best_time)));
        row.add_cell(Cell::new(format_time(worst_time)));

        for impl_name in &implementations {
            if let Some(time) = op_results.get(impl_name) {
                let relative = time / best_time;
                row.add_cell(Cell::new(format!("{:.2}x", relative)));
            } else {
                row.add_cell(Cell::new("-"));
            }
        }
        table.add_row(row);
    }

    println!("{}", table);
}

fn format_time(time_ns: f64) -> String {
    if time_ns < 1_000.0 {
        format!("{:.2} ns", time_ns)
    } else if time_ns < 1_000_000.0 {
        format!("{:.2} µs", time_ns / 1_000.0)
    } else if time_ns < 1_000_000_000.0 {
        format!("{:.2} ms", time_ns / 1_000_000.0)
    } else {
        format!("{:.2} s", time_ns / 1_000_000_000.0)
    }
}