// Comparison benchmark: stumpalo vs bumpalo vs blink-alloc.
//
// No external harness (harness = false in Cargo.toml). Measures raw allocation
// throughput using std::time::Instant and prints a relative-performance table
// to stdout.
//
use libc::{close, fork, pipe, read, waitpid, write};
use mimalloc::MiMalloc;
use std::io::{self, Write};

#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;

use std::ffi::c_void;
use std::hint::black_box;
use std::time::{Duration, Instant};

const NUM_ALLOCS: usize = 10_000;
const WARMUP: usize = 10;
const SAMPLES: usize = 60;
const OUTLIERS: usize = 6;

// --- Static data for alloc_slice_lit_copy / alloc_str_lit benchmarks ---

const SLICE_LIT_U8_8: [u8; 8] = [0u8; 8];
const SLICE_LIT_U8_32: [u8; 32] = [0u8; 32];
const SLICE_LIT_U8_64: [u8; 64] = [0u8; 64];
const SLICE_LIT_U8_128: [u8; 128] = [0u8; 128];

// NOTE: these use &[u8; N] rather than &str so that the const generic N
// carries compile-time size information through to alloc_slice_lit_copy.
// In practice, users call alloc_str_lit("literal") directly and the
// compiler sees the string length at compile time without this workaround.
const STR_LIT_8: &[u8; 8] = b"aaaaaaaa";
const STR_LIT_16: &[u8; 16] = b"aaaaaaaaaaaaaaaa";
const STR_LIT_32: &[u8; 32] = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const STR_LIT_40: &[u8; 40] = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const STR_LIT_48: &[u8; 48] = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const STR_LIT_64: &[u8; 64] = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const STR_LIT_72: &[u8; 72] =
    b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const STR_LIT_80: &[u8; 80] =
    b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const STR_LIT_128: &[u8; 128] = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";

/// Run `f` in a forked child process and return the resulting `Duration`.
/// This ensures a pristine heap for each measurement, eliminating heap
/// fragmentation interference between benchmarks.
fn fork_measure(f: impl FnOnce() -> Duration) -> Option<Duration> {
    let mut fds = [0i32; 2];
    unsafe {
        if pipe(fds.as_mut_ptr()) != 0 {
            panic!("pipe failed");
        }
    }

    match unsafe { fork() } {
        -1 => panic!("fork failed"),
        0 => {
            // Child: run the measurement, send duration, exit.
            unsafe { close(fds[0]) };
            let ns = f().as_nanos() as u64;
            unsafe {
                write(
                    fds[1],
                    &ns as *const u64 as *const c_void,
                    std::mem::size_of::<u64>(),
                );
                close(fds[1]);
            }
            std::process::exit(0);
        }
        pid => {
            // Parent: wait for child and read the duration.
            unsafe { close(fds[1]) };
            let mut status = 0;
            unsafe { waitpid(pid, &mut status, 0) };
            let mut ns: u64 = 0;
            unsafe {
                read(
                    fds[0],
                    &mut ns as *mut u64 as *mut c_void,
                    std::mem::size_of::<u64>(),
                );
                close(fds[0]);
            }
            Some(Duration::from_nanos(ns))
        }
    }
}

// --- Conditional bench runner: skips execution when filtered out ---

struct BenchRunner {
    filter: Option<String>,
    results: Vec<(&'static str, BenchResult)>,
    bench_num: usize,
}

impl BenchRunner {
    fn new(filter: Option<String>) -> Self {
        BenchRunner {
            filter,
            results: Vec::new(),
            bench_num: 1,
        }
    }

    fn bench(&mut self, name: &'static str, f: impl FnOnce() -> BenchResult) {
        if self
            .filter
            .as_ref()
            .is_none_or(|flt| name.contains(flt.as_str()))
        {
            let mut stdout = io::stdout().lock();
            // Print dots + current bench name, overwriting the previous line.
            let _ = write!(
                stdout,
                "\x1b[1K\rRunning bench {} - {}",
                self.bench_num, name
            );
            let _ = stdout.flush();
            self.bench_num += 1;

            self.results.push((name, f()));
        }
    }
}

fn main() {
    println!("Num allocs: {}", NUM_ALLOCS);
    println!("Warmup: {}", WARMUP);
    println!("Samples: {}", SAMPLES);
    println!("Discarded samples: {}", OUTLIERS);

    // Optional BENCH_FILTER env var: only benchmarks whose names contain
    // the filter string are executed.  Non-matching ones are skipped entirely.
    let filter = std::env::var("BENCH_FILTER").ok();
    if let Some(ref f) = filter {
        println!("Filter: {}", f);
    }

    // Warm up the CPU frequency governor + cache before any measurements.
    // Modern CPUs take several hundred ms to ramp up turbo.
    warmup_system();

    let mut runner = BenchRunner::new(filter);

    // --- alloc scalar ---
    runner.bench("alloc_u8", bench_alloc::<u8>);
    runner.bench("alloc_u16", bench_alloc::<u16>);
    runner.bench("alloc_u32", bench_alloc::<u32>);
    runner.bench("alloc_u64", bench_alloc::<u64>);
    runner.bench("alloc_u128", bench_alloc::<u128>);

    // --- alloc multiple scalars ---
    runner.bench("alloc_multiple_u8", bench_alloc_multiple::<u8>);
    runner.bench("alloc_multiple_u16", bench_alloc_multiple::<u16>);
    runner.bench("alloc_multiple_u32", bench_alloc_multiple::<u32>);
    runner.bench("alloc_multiple_u64", bench_alloc_multiple::<u64>);
    runner.bench("alloc_multiple_u128", bench_alloc_multiple::<u128>);

    // --- alloc array ---
    // Use alloc_with to avoid the intermediate stack-copy artifact:
    //   a.alloc([T::default(); N])                 ←  creates + zeros stack array, then copies in
    //   a.alloc_with(|| [T::default(); N])          ←  closure is called inside, no intermediate copy
    runner.bench("alloc_array_u8_8", bench_alloc_array::<u8, 8>);
    runner.bench("alloc_array_u8_32", bench_alloc_array::<u8, 32>);
    runner.bench("alloc_array_u8_64", bench_alloc_array::<u8, 64>);
    runner.bench("alloc_array_u8_128", bench_alloc_array::<u8, 128>);

    // --- alloc_slice_copy ---
    runner.bench("alloc_slice_u8_8", bench_alloc_slice::<u8, 8>);
    runner.bench("alloc_slice_u8_32", bench_alloc_slice::<u8, 32>);
    runner.bench("alloc_slice_u8_64", bench_alloc_slice::<u8, 64>);
    runner.bench("alloc_slice_u8_128", bench_alloc_slice::<u8, 128>);

    runner.bench("alloc_slice_u16_8", bench_alloc_slice::<u16, 8>);
    runner.bench("alloc_slice_u16_32", bench_alloc_slice::<u16, 32>);
    runner.bench("alloc_slice_u16_64", bench_alloc_slice::<u16, 64>);
    runner.bench("alloc_slice_u16_128", bench_alloc_slice::<u16, 128>);

    runner.bench("alloc_slice_u32_8", bench_alloc_slice::<u32, 8>);
    runner.bench("alloc_slice_u32_32", bench_alloc_slice::<u32, 32>);
    runner.bench("alloc_slice_u32_64", bench_alloc_slice::<u32, 64>);
    runner.bench("alloc_slice_u32_128", bench_alloc_slice::<u32, 128>);

    runner.bench("alloc_slice_u64_8", bench_alloc_slice::<u64, 8>);
    runner.bench("alloc_slice_u64_32", bench_alloc_slice::<u64, 32>);
    runner.bench("alloc_slice_u64_64", bench_alloc_slice::<u64, 64>);
    runner.bench("alloc_slice_u64_128", bench_alloc_slice::<u64, 128>);

    runner.bench("alloc_slice_u128_8", bench_alloc_slice::<u128, 8>);
    runner.bench("alloc_slice_u128_32", bench_alloc_slice::<u128, 32>);
    runner.bench("alloc_slice_u128_64", bench_alloc_slice::<u128, 64>);
    runner.bench("alloc_slice_u128_128", bench_alloc_slice::<u128, 128>);

    // --- alloc_sized_slice_copy ---
    runner.bench("alloc_sized_slice_u8_8", bench_alloc_sized_slice::<u8, 8>);
    runner.bench("alloc_sized_slice_u8_32", bench_alloc_sized_slice::<u8, 32>);
    runner.bench("alloc_sized_slice_u8_64", bench_alloc_sized_slice::<u8, 64>);
    runner.bench(
        "alloc_sized_slice_u8_128",
        bench_alloc_sized_slice::<u8, 128>,
    );

    runner.bench("alloc_sized_slice_u16_8", bench_alloc_sized_slice::<u16, 8>);
    runner.bench(
        "alloc_sized_slice_u16_32",
        bench_alloc_sized_slice::<u16, 32>,
    );
    runner.bench(
        "alloc_sized_slice_u16_64",
        bench_alloc_sized_slice::<u16, 64>,
    );
    runner.bench(
        "alloc_sized_slice_u16_128",
        bench_alloc_sized_slice::<u16, 128>,
    );

    runner.bench("alloc_sized_slice_u32_8", bench_alloc_sized_slice::<u32, 8>);
    runner.bench(
        "alloc_sized_slice_u32_32",
        bench_alloc_sized_slice::<u32, 32>,
    );
    runner.bench(
        "alloc_sized_slice_u32_64",
        bench_alloc_sized_slice::<u32, 64>,
    );
    runner.bench(
        "alloc_sized_slice_u32_128",
        bench_alloc_sized_slice::<u32, 128>,
    );

    runner.bench("alloc_sized_slice_u64_8", bench_alloc_sized_slice::<u64, 8>);
    runner.bench(
        "alloc_sized_slice_u64_32",
        bench_alloc_sized_slice::<u64, 32>,
    );
    runner.bench(
        "alloc_sized_slice_u64_64",
        bench_alloc_sized_slice::<u64, 64>,
    );
    runner.bench(
        "alloc_sized_slice_u64_128",
        bench_alloc_sized_slice::<u64, 128>,
    );

    runner.bench(
        "alloc_sized_slice_u128_8",
        bench_alloc_sized_slice::<u128, 8>,
    );
    runner.bench(
        "alloc_sized_slice_u128_32",
        bench_alloc_sized_slice::<u128, 32>,
    );
    runner.bench(
        "alloc_sized_slice_u128_64",
        bench_alloc_sized_slice::<u128, 64>,
    );
    runner.bench(
        "alloc_sized_slice_u128_128",
        bench_alloc_sized_slice::<u128, 128>,
    );

    // --- alloc_slice_fill_with ---
    runner.bench("alloc_fill_with_u32_8", bench_fill_with::<8>);
    runner.bench("alloc_fill_with_u32_32", bench_fill_with::<32>);
    runner.bench("alloc_fill_with_u32_512", bench_fill_with::<512>);
    runner.bench("alloc_fill_with_u32_4096", bench_fill_with::<4096>);

    // --- alloc_slice_fill_iter ---
    runner.bench(
        "alloc_fill_iter_exact_vec_u32_8",
        bench_fill_iter_vec_exact::<8>,
    );
    runner.bench(
        "alloc_fill_iter_exact_vec_u32_32",
        bench_fill_iter_vec_exact::<32>,
    );
    runner.bench(
        "alloc_fill_iter_exact_vec_u32_512",
        bench_fill_iter_vec_exact::<512>,
    );
    runner.bench(
        "alloc_fill_iter_exact_vec_u32_4096",
        bench_fill_iter_vec_exact::<4096>,
    );

    #[cfg(nightly)]
    {
        runner.bench(
            "alloc_fill_iter_trusted_vec_u32_8",
            bench_fill_iter_vec_trusted::<8>,
        );
        runner.bench(
            "alloc_fill_iter_trusted_vec_u32_32",
            bench_fill_iter_vec_trusted::<32>,
        );
        runner.bench(
            "alloc_fill_iter_trusted_vec_u32_512",
            bench_fill_iter_vec_trusted::<512>,
        );
        runner.bench(
            "alloc_fill_iter_trusted_vec_u32_4096",
            bench_fill_iter_vec_trusted::<4096>,
        );
    }

    runner.bench(
        "alloc_fill_iter_exact_repeat_u32_8",
        bench_fill_iter_repeat_exact::<8>,
    );
    runner.bench(
        "alloc_fill_iter_exact_repeat_u32_32",
        bench_fill_iter_repeat_exact::<32>,
    );
    runner.bench(
        "alloc_fill_iter_exact_repeat_u32_512",
        bench_fill_iter_repeat_exact::<512>,
    );
    runner.bench(
        "alloc_fill_iter_exact_repeat_u32_4096",
        bench_fill_iter_repeat_exact::<4096>,
    );

    #[cfg(nightly)]
    {
        runner.bench(
            "alloc_fill_iter_trusted_repeat_u32_8",
            bench_fill_iter_repeat_trusted::<8>,
        );
        runner.bench(
            "alloc_fill_iter_trusted_repeat_u32_32",
            bench_fill_iter_repeat_trusted::<32>,
        );
        runner.bench(
            "alloc_fill_iter_trusted_repeat_u32_512",
            bench_fill_iter_repeat_trusted::<512>,
        );
        runner.bench(
            "alloc_fill_iter_trusted_repeat_u32_4096",
            bench_fill_iter_repeat_trusted::<4096>,
        );
    }

    // --- alloc big struct ---
    // Use round sizes and sizes relative to INITIAL_CHUNK_CAPACITY to exercise
    // the various allocation paths (normal chunk, > half-chunk, full chunk, etc.).
    runner.bench("alloc_struct_13", bench_alloc_struct::<13>);
    runner.bench("alloc_struct_24", bench_alloc_struct::<24>);
    runner.bench("alloc_struct_26", bench_alloc_struct::<26>);
    runner.bench("alloc_struct_30", bench_alloc_struct::<30>);
    runner.bench("alloc_struct_32", bench_alloc_struct::<32>);
    runner.bench("alloc_struct_64", bench_alloc_struct::<64>);
    runner.bench("alloc_struct_96", bench_alloc_struct::<96>);
    runner.bench("alloc_struct_128", bench_alloc_struct::<128>);
    runner.bench("alloc_struct_192", bench_alloc_struct::<192>);
    runner.bench("alloc_struct_256", bench_alloc_struct::<256>);
    runner.bench("alloc_struct_512", bench_alloc_struct::<512>);
    runner.bench("alloc_struct_1k", bench_alloc_struct::<1024>);
    // Sizes relative to INITIAL_CHUNK_CAPACITY.
    runner.bench(
        "alloc_struct_half_chunk_minus_1",
        bench_alloc_struct::<{ stumpalo::INITIAL_CHUNK_CAPACITY / 2 - 1 }>,
    );
    runner.bench(
        "alloc_struct_half_chunk",
        bench_alloc_struct::<{ stumpalo::INITIAL_CHUNK_CAPACITY / 2 }>,
    );
    runner.bench(
        "alloc_struct_half_chunk_plus_1",
        bench_alloc_struct::<{ stumpalo::INITIAL_CHUNK_CAPACITY / 2 + 1 }>,
    );
    runner.bench(
        "alloc_struct_one_chunk_minus_1",
        bench_alloc_struct::<{ stumpalo::INITIAL_CHUNK_CAPACITY - 1 }>,
    );
    runner.bench(
        "alloc_struct_one_chunk",
        bench_alloc_struct::<{ stumpalo::INITIAL_CHUNK_CAPACITY }>,
    );
    runner.bench(
        "alloc_struct_one_chunk_plus_1",
        bench_alloc_struct::<{ stumpalo::INITIAL_CHUNK_CAPACITY + 1 }>,
    );
    runner.bench(
        "alloc_struct_two_chunks",
        bench_alloc_struct::<{ stumpalo::INITIAL_CHUNK_CAPACITY * 2 }>,
    );

    // --- alloc_str ---
    runner.bench("alloc_str_8", || bench_alloc_str(STR_LIT_8));
    runner.bench("alloc_str_16", || bench_alloc_str(STR_LIT_16));
    runner.bench("alloc_str_32", || bench_alloc_str(STR_LIT_32));
    runner.bench("alloc_str_40", || bench_alloc_str(STR_LIT_40));
    runner.bench("alloc_str_48", || bench_alloc_str(STR_LIT_48));
    runner.bench("alloc_str_64", || bench_alloc_str(STR_LIT_64));
    runner.bench("alloc_str_72", || bench_alloc_str(STR_LIT_72));
    runner.bench("alloc_str_80", || bench_alloc_str(STR_LIT_80));
    runner.bench("alloc_str_128", || bench_alloc_str(STR_LIT_128));

    // --- alloc_slice_lit_copy ---
    runner.bench("alloc_slice_lit_u8_8", || {
        bench_alloc_slice_lit_u8::<8>(&SLICE_LIT_U8_8)
    });
    runner.bench("alloc_slice_lit_u8_32", || {
        bench_alloc_slice_lit_u8::<32>(&SLICE_LIT_U8_32)
    });
    runner.bench("alloc_slice_lit_u8_64", || {
        bench_alloc_slice_lit_u8::<64>(&SLICE_LIT_U8_64)
    });
    runner.bench("alloc_slice_lit_u8_128", || {
        bench_alloc_slice_lit_u8::<128>(&SLICE_LIT_U8_128)
    });

    // --- alloc_str_lit ---
    runner.bench("alloc_str_lit_8", || bench_alloc_str_lit::<8>(STR_LIT_8));
    runner.bench("alloc_str_lit_16", || bench_alloc_str_lit::<16>(STR_LIT_16));
    runner.bench("alloc_str_lit_32", || bench_alloc_str_lit::<32>(STR_LIT_32));
    runner.bench("alloc_str_lit_40", || bench_alloc_str_lit::<40>(STR_LIT_40));
    runner.bench("alloc_str_lit_48", || bench_alloc_str_lit::<48>(STR_LIT_48));
    runner.bench("alloc_str_lit_64", || bench_alloc_str_lit::<64>(STR_LIT_64));
    runner.bench("alloc_str_lit_72", || bench_alloc_str_lit::<72>(STR_LIT_72));
    runner.bench("alloc_str_lit_80", || bench_alloc_str_lit::<80>(STR_LIT_80));
    runner.bench("alloc_str_lit_128", || {
        bench_alloc_str_lit::<128>(STR_LIT_128)
    });

    // --- alloc_vec ---
    runner.bench("alloc_vec_u32_8", bench_alloc_vec::<8>);
    runner.bench("alloc_vec_u32_32", bench_alloc_vec::<32>);
    runner.bench("alloc_vec_u32_512", bench_alloc_vec::<512>);
    runner.bench("alloc_vec_u32_4096", bench_alloc_vec::<4096>);

    // --- alloc_boxed_slice ---
    runner.bench("alloc_boxed_slice_u32_8", bench_alloc_boxed_slice::<8>);
    runner.bench("alloc_boxed_slice_u32_32", bench_alloc_boxed_slice::<32>);
    runner.bench("alloc_boxed_slice_u32_512", bench_alloc_boxed_slice::<512>);
    runner.bench(
        "alloc_boxed_slice_u32_4096",
        bench_alloc_boxed_slice::<4096>,
    );

    // --- clear ---
    runner.bench("clear", bench_clear);

    // --- clear_and_reuse ---
    runner.bench("clear_and_reuse", bench_clear_and_reuse);

    // --- output ---
    if !runner.results.is_empty() {
        // Clear the current bench name, show final dots on their own line.
        println!("\r");
    }
    let tsv = tsv_table(&runner.results);
    let formatted = pipe_through_column(&tsv);
    print!("{formatted}");
}

// ---------------------------------------------------------------------------
// System warmup — drive the CPU to max turbo before measuring
// ---------------------------------------------------------------------------

/// Run a tight loop for ~1s to stabilise CPU frequency governor + cache state.
fn warmup_system() {
    let deadline = Instant::now() + Duration::from_secs(1);
    let mut x: u64 = 0;
    while Instant::now() < deadline {
        x = x
            .wrapping_mul(6364136223846793005)
            .wrapping_add(1442695040888963407);
        black_box(x);
    }
}

// ---------------------------------------------------------------------------
// Measurement helpers
// ---------------------------------------------------------------------------

/// Run `f` once and return the elapsed wall time.
fn once<F: FnOnce()>(f: F) -> Duration {
    let start = Instant::now();
    f();
    start.elapsed()
}

/// Run warmup iterations, then collect `n` measured samples, returning a sorted copy.
fn sample<F: FnMut()>(n: usize, mut f: F) -> Vec<Duration> {
    for _ in 0..WARMUP {
        f();
    }
    let mut out: Vec<Duration> = (0..n).map(|_| once(&mut f)).collect();
    out.sort();
    out
}

/// Return the mean of the pre-sorted slice, dropping the worst N outliers.
fn mean_without_outliers(s: &[Duration]) -> Duration {
    let keep = s.len().saturating_sub(OUTLIERS);
    if keep == 0 {
        return Duration::ZERO;
    }
    let sum_ns: u128 = s[..keep].iter().map(|d| d.as_nanos()).sum();
    Duration::from_nanos((sum_ns / keep as u128) as u64)
}

// ---------------------------------------------------------------------------
// Per-library measurement functions
// ---------------------------------------------------------------------------

fn measure_stump_alloc<T: Default>() -> Duration {
    mean_without_outliers(&sample(SAMPLES, || {
        let a = stumpalo::Arena::new();
        for _ in 0..NUM_ALLOCS {
            black_box(a.alloc(T::default()));
        }
    }))
}

fn measure_bump_alloc<T: Default>() -> Duration {
    mean_without_outliers(&sample(SAMPLES, || {
        let b = bumpalo::Bump::new();
        for _ in 0..NUM_ALLOCS {
            black_box(b.alloc(T::default()));
        }
    }))
}

fn measure_blink_alloc<T: Default + 'static>() -> Duration {
    mean_without_outliers(&sample(SAMPLES, || {
        let b = blink_alloc::Blink::new();
        for _ in 0..NUM_ALLOCS {
            black_box(b.put(T::default()));
        }
    }))
}

fn measure_stump_alloc_multiple<T: Default>() -> Duration {
    mean_without_outliers(&sample(SAMPLES, || {
        let a = stumpalo::Arena::new();
        for _ in 0..NUM_ALLOCS {
            black_box(a.alloc(T::default()));
            black_box(a.alloc(T::default()));
            black_box(a.alloc(T::default()));
            black_box(a.alloc(T::default()));
            black_box(a.alloc(T::default()));
            black_box(a.alloc(T::default()));
            black_box(a.alloc(T::default()));
            black_box(a.alloc(T::default()));
        }
    }))
}

fn measure_bump_alloc_multiple<T: Default>() -> Duration {
    mean_without_outliers(&sample(SAMPLES, || {
        let b = bumpalo::Bump::new();
        for _ in 0..NUM_ALLOCS {
            black_box(b.alloc(T::default()));
            black_box(b.alloc(T::default()));
            black_box(b.alloc(T::default()));
            black_box(b.alloc(T::default()));
            black_box(b.alloc(T::default()));
            black_box(b.alloc(T::default()));
            black_box(b.alloc(T::default()));
            black_box(b.alloc(T::default()));
        }
    }))
}

fn measure_blink_alloc_multiple<T: Default + 'static>() -> Duration {
    mean_without_outliers(&sample(SAMPLES, || {
        let b = blink_alloc::Blink::new();
        for _ in 0..NUM_ALLOCS {
            black_box(b.put(T::default()));
            black_box(b.put(T::default()));
            black_box(b.put(T::default()));
            black_box(b.put(T::default()));
            black_box(b.put(T::default()));
            black_box(b.put(T::default()));
            black_box(b.put(T::default()));
            black_box(b.put(T::default()));
        }
    }))
}

/// Measure array alloc using `alloc_with` to avoid the intermediate stack-copy
/// artifact: `a.alloc_with(|| [T::default(); N])` creates the array inside
/// the allocator, avoiding a separate stack zero + copy.
fn measure_stump_alloc_array<T: Default + Copy, const N: usize>() -> Duration {
    mean_without_outliers(&sample(SAMPLES, || {
        let a = stumpalo::Arena::new();
        for _ in 0..NUM_ALLOCS {
            black_box(a.alloc_sized_slice_copy(&[T::default(); N]));
        }
    }))
}

fn measure_bump_alloc_array<T: Default + Copy, const N: usize>() -> Duration {
    mean_without_outliers(&sample(SAMPLES, || {
        let b = bumpalo::Bump::new();
        for _ in 0..NUM_ALLOCS {
            black_box(b.alloc_with(|| [T::default(); N]));
        }
    }))
}

fn measure_blink_alloc_array<T: Default + Copy + 'static, const N: usize>() -> Duration {
    mean_without_outliers(&sample(SAMPLES, || {
        let b = blink_alloc::Blink::new();
        for _ in 0..NUM_ALLOCS {
            // blink-alloc doesn't have alloc_with; put(T) takes ownership.
            // We create the value outside, like the original bench.
            black_box(b.put([T::default(); N]));
        }
    }))
}

fn measure_stump_alloc_slice<T: Copy + Default, const N: usize>() -> Duration {
    let data: &[T] = &[T::default(); N];
    mean_without_outliers(&sample(SAMPLES, || {
        let a = stumpalo::Arena::new();
        for _ in 0..NUM_ALLOCS {
            black_box(a.alloc_slice_copy(data));
        }
    }))
}

fn measure_bump_alloc_slice<T: Copy + Default, const N: usize>() -> Duration {
    let data: &[T] = &[T::default(); N];
    mean_without_outliers(&sample(SAMPLES, || {
        let b = bumpalo::Bump::new();
        for _ in 0..NUM_ALLOCS {
            black_box(b.alloc_slice_copy(data));
        }
    }))
}

fn measure_blink_alloc_slice<T: Copy + Default, const N: usize>() -> Duration {
    let data: &[T] = &[T::default(); N];
    mean_without_outliers(&sample(SAMPLES, || {
        let b = blink_alloc::Blink::new();
        for _ in 0..NUM_ALLOCS {
            black_box(b.copy_slice(data));
        }
    }))
}

fn measure_stump_alloc_sized_slice<T: Copy + Default, const N: usize>() -> Duration {
    let data: &[T; N] = &[T::default(); N];
    mean_without_outliers(&sample(SAMPLES, || {
        let a = stumpalo::Arena::new();
        for _ in 0..NUM_ALLOCS {
            black_box(a.alloc_sized_slice_copy(data));
        }
    }))
}

fn measure_bump_alloc_sized_slice<T: Copy + Default, const N: usize>() -> Duration {
    let data: &[T; N] = &[T::default(); N];
    mean_without_outliers(&sample(SAMPLES, || {
        let b = bumpalo::Bump::new();
        for _ in 0..NUM_ALLOCS {
            black_box(b.alloc_slice_copy(data));
        }
    }))
}

fn measure_blink_alloc_sized_slice<T: Copy + Default, const N: usize>() -> Duration {
    let data: &[T; N] = &[T::default(); N];
    mean_without_outliers(&sample(SAMPLES, || {
        let b = blink_alloc::Blink::new();
        for _ in 0..NUM_ALLOCS {
            black_box(b.copy_slice(data));
        }
    }))
}

fn measure_stump_alloc_str(s: &str) -> Duration {
    mean_without_outliers(&sample(SAMPLES, || {
        let a = stumpalo::Arena::new();
        for _ in 0..NUM_ALLOCS {
            black_box(a.alloc_str(s));
        }
    }))
}

fn measure_bump_alloc_str(s: &str) -> Duration {
    mean_without_outliers(&sample(SAMPLES, || {
        let b = bumpalo::Bump::new();
        for _ in 0..NUM_ALLOCS {
            black_box(b.alloc_str(s));
        }
    }))
}

fn measure_blink_alloc_str(s: &str) -> Duration {
    mean_without_outliers(&sample(SAMPLES, || {
        let b = blink_alloc::Blink::new();
        for _ in 0..NUM_ALLOCS {
            black_box(b.copy_str(s));
        }
    }))
}

// --- alloc_slice_lit_copy (stumpalo only — no direct equivalents in other libs) ---

fn measure_stump_alloc_slice_lit<const N: usize>(data: &'static [u8; N]) -> Duration {
    mean_without_outliers(&sample(SAMPLES, || {
        let a = stumpalo::Arena::new();
        for _ in 0..NUM_ALLOCS {
            black_box(a.alloc_slice_lit_copy(data));
        }
    }))
}

// --- alloc_str_lit (stumpalo only — use alloc_str/copy_str as comparison baseline) ---

// Workaround: manually inline alloc_str_lit's body so that the compile-time
// size N propagates into alloc_slice_lit_copy. In real code, calling
// alloc_str_lit("literal") directly lets the compiler see the string length;
// this is only needed because we pass data through a generic function boundary.
fn measure_stump_alloc_str_lit<const N: usize>(data: &'static [u8; N]) -> Duration {
    mean_without_outliers(&sample(SAMPLES, || {
        let a = stumpalo::Arena::new();
        for _ in 0..NUM_ALLOCS {
            let slice = a.alloc_slice_lit_copy(data);
            // Safety: data is ASCII, so from_utf8_unchecked_mut is sound.
            // This mirrors what alloc_str_lit does internally.
            black_box(unsafe { core::str::from_utf8_unchecked_mut(slice) });
        }
    }))
}

fn measure_bump_alloc_str_lit(n: usize) -> Duration {
    let buf = vec![b'a'; n];
    let s: &str = std::str::from_utf8(&buf).unwrap();
    mean_without_outliers(&sample(SAMPLES, || {
        let b = bumpalo::Bump::new();
        for _ in 0..NUM_ALLOCS {
            black_box(b.alloc_str(s));
        }
    }))
}

fn measure_blink_alloc_str_lit(n: usize) -> Duration {
    let buf = vec![b'a'; n];
    let s: &str = std::str::from_utf8(&buf).unwrap();
    mean_without_outliers(&sample(SAMPLES, || {
        let b = blink_alloc::Blink::new();
        for _ in 0..NUM_ALLOCS {
            black_box(b.copy_str(s));
        }
    }))
}

// --- alloc big struct (sized via const generic, covers large-allocation paths) ---

fn measure_stump_alloc_big<const N: usize>() -> Duration {
    mean_without_outliers(&sample(SAMPLES, || {
        let a = stumpalo::Arena::new();
        for _ in 0..NUM_ALLOCS {
            black_box(a.alloc_with(|| [0u8; N]));
        }
    }))
}

fn measure_bump_alloc_big<const N: usize>() -> Duration {
    mean_without_outliers(&sample(SAMPLES, || {
        let b = bumpalo::Bump::new();
        for _ in 0..NUM_ALLOCS {
            black_box(b.alloc_with(|| [0u8; N]));
        }
    }))
}

fn measure_blink_alloc_big<const N: usize>() -> Duration {
    mean_without_outliers(&sample(SAMPLES, || {
        let b = blink_alloc::Blink::new();
        for _ in 0..NUM_ALLOCS {
            black_box(b.put([0u8; N]));
        }
    }))
}

// --- alloc_slice_fill_with ---

/// Iterations for fill benchmarks, scaled inversely with N so N=1 element
/// would use NUM_ALLOCS full iterations; larger N uses proportionally fewer.
fn fill_iters<const N: usize>() -> usize {
    /*
    let iters = NUM_ALLOCS * std::mem::size_of::<u32>() / (N / 5).max(1);
    iters.min(NUM_ALLOCS)
    */
    NUM_ALLOCS / 5
}

fn measure_stump_fill_with<const N: usize>() -> Duration {
    mean_without_outliers(&sample(SAMPLES, || {
        let a = stumpalo::Arena::new();
        for _ in 0..fill_iters::<N>() {
            black_box(a.alloc_slice_fill_with(N, |i| i as u32));
        }
    }))
}

fn measure_bump_fill_with<const N: usize>() -> Duration {
    mean_without_outliers(&sample(SAMPLES, || {
        let b = bumpalo::Bump::new();
        for _ in 0..fill_iters::<N>() {
            black_box(b.alloc_slice_fill_with(N, |i| i as u32));
        }
    }))
}

// --- alloc_vec ---

fn measure_stump_alloc_vec<const N: usize>() -> Duration {
    let mut source: Vec<u32> = vec![42u32; N];
    let ptr = source.as_mut_ptr();
    let len = source.len();
    let cap = source.capacity();
    // Leak the source buffer. alloc_vec wraps each reconstructed Vec in
    // ManuallyDrop, so the buffer is never freed by any consumer.
    std::mem::forget(source);
    mean_without_outliers(&sample(SAMPLES, || {
        let a = stumpalo::Arena::new();
        for _ in 0..fill_iters::<N>() {
            let v = unsafe { Vec::from_raw_parts(ptr, len, cap) };
            black_box(a.alloc_vec(v));
        }
    }))
}

// A version of Vec::IntoIterator that never drops the allocation.
// ManuallyDrop on the field prevents automatic field-drop at end of scope.
struct ClonedVecIter<T> {
    data: std::mem::ManuallyDrop<<Vec<T> as IntoIterator>::IntoIter>,
}

impl<T> Iterator for ClonedVecIter<T> {
    type Item = T;

    fn next(&mut self) -> Option<Self::Item> {
        self.data.next()
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        self.data.size_hint()
    }
}

impl<T> ExactSizeIterator for ClonedVecIter<T> {}

impl<T> ClonedVecIter<T> {
    fn new(data: <Vec<T> as IntoIterator>::IntoIter) -> Self {
        Self {
            data: std::mem::ManuallyDrop::new(data),
        }
    }
}

fn measure_bump_alloc_vec<const N: usize>() -> Duration {
    let mut source: Vec<u32> = vec![42u32; N];
    let ptr = source.as_mut_ptr();
    let len = source.len();
    let cap = source.capacity();
    // Leak the source buffer. alloc_vec wraps each reconstructed Vec in
    // ManuallyDrop, so the buffer is never freed by any consumer.
    std::mem::forget(source);
    mean_without_outliers(&sample(SAMPLES, || {
        let b = bumpalo::Bump::new();
        for _ in 0..fill_iters::<N>() {
            let v = unsafe { Vec::from_raw_parts(ptr, len, cap) };
            let iter = ClonedVecIter::new(v.into_iter());
            black_box(b.alloc_slice_fill_iter(iter));
        }
    }))
}

fn bench_alloc_vec<const N: usize>() -> BenchResult {
    BenchResult {
        stumpalo: fork_measure(measure_stump_alloc_vec::<N>),
        bumpalo: fork_measure(measure_bump_alloc_vec::<N>),
        blink: None,
    }
}

// --- alloc_boxed_slice ---

fn measure_stump_alloc_boxed_slice<const N: usize>() -> Duration {
    let source: Box<[u32]> = vec![42u32; N].into_boxed_slice();
    let ptr = Box::into_raw(source);
    mean_without_outliers(&sample(SAMPLES, || {
        let a = stumpalo::Arena::new();
        for _ in 0..fill_iters::<N>() {
            let b = unsafe { Box::from_raw(ptr) };
            black_box(a.alloc_boxed_slice(b));
        }
    }))
}

fn measure_bump_alloc_boxed_slice<const N: usize>() -> Duration {
    let data: Box<[u32]> = vec![42u32; N].into_boxed_slice();
    mean_without_outliers(&sample(SAMPLES, || {
        let b = bumpalo::Bump::new();
        for _ in 0..fill_iters::<N>() {
            black_box(b.alloc_slice_copy(&*data));
        }
    }))
}

fn measure_blink_alloc_boxed_slice<const N: usize>() -> Duration {
    let data: Box<[u32]> = vec![42u32; N].into_boxed_slice();
    mean_without_outliers(&sample(SAMPLES, || {
        let b = blink_alloc::Blink::new();
        for _ in 0..fill_iters::<N>() {
            black_box(b.copy_slice(&*data));
        }
    }))
}

fn bench_alloc_boxed_slice<const N: usize>() -> BenchResult {
    BenchResult {
        stumpalo: fork_measure(measure_stump_alloc_boxed_slice::<N>),
        bumpalo: fork_measure(measure_bump_alloc_boxed_slice::<N>),
        blink: fork_measure(measure_blink_alloc_boxed_slice::<N>),
    }
}

fn bench_fill_with<const N: usize>() -> BenchResult {
    BenchResult {
        stumpalo: fork_measure(measure_stump_fill_with::<N>),
        bumpalo: fork_measure(measure_bump_fill_with::<N>),
        blink: None,
    }
}

// --- alloc_slice_fill_iter ---

fn measure_stump_fill_iter_repeat_exact<const N: usize>() -> Duration {
    mean_without_outliers(&sample(SAMPLES, || {
        let a = stumpalo::Arena::new();
        for _ in 0..fill_iters::<N>() {
            black_box(a.alloc_slice_fill_iter(std::iter::repeat_n(42u32, N)));
        }
    }))
}

fn measure_bump_fill_iter_repeat_exact<const N: usize>() -> Duration {
    mean_without_outliers(&sample(SAMPLES, || {
        let b = bumpalo::Bump::new();
        for _ in 0..fill_iters::<N>() {
            black_box(b.alloc_slice_fill_iter(std::iter::repeat_n(42u32, N)));
        }
    }))
}

#[cfg(nightly)]
fn measure_stump_fill_iter_repeat_trusted<const N: usize>() -> Duration {
    mean_without_outliers(&sample(SAMPLES, || {
        let a = stumpalo::Arena::new();
        for _ in 0..fill_iters::<N>() {
            black_box(a.alloc_slice_fill_iter_trusted(std::iter::repeat_n(42u32, N)));
        }
    }))
}

fn measure_stump_fill_iter_vec_exact<const N: usize>() -> Duration {
    let data: Vec<u32> = (0..N as u32).collect();
    mean_without_outliers(&sample(SAMPLES, || {
        let a = stumpalo::Arena::new();
        for _ in 0..fill_iters::<N>() {
            black_box(a.alloc_slice_fill_iter(data.iter().copied()));
        }
    }))
}

fn measure_bump_fill_iter_vec_exact<const N: usize>() -> Duration {
    let data: Vec<u32> = (0..N as u32).collect();
    mean_without_outliers(&sample(SAMPLES, || {
        let b = bumpalo::Bump::new();
        for _ in 0..fill_iters::<N>() {
            black_box(b.alloc_slice_fill_iter(data.iter().copied()));
        }
    }))
}

#[cfg(nightly)]
fn measure_stump_fill_iter_vec_trusted<const N: usize>() -> Duration {
    let data: Vec<u32> = (0..N as u32).collect();
    mean_without_outliers(&sample(SAMPLES, || {
        let a = stumpalo::Arena::new();
        for _ in 0..fill_iters::<N>() {
            black_box(a.alloc_slice_fill_iter_trusted(data.iter().copied()));
        }
    }))
}

fn bench_fill_iter_vec_exact<const N: usize>() -> BenchResult {
    BenchResult {
        stumpalo: fork_measure(measure_stump_fill_iter_vec_exact::<N>),
        bumpalo: fork_measure(measure_bump_fill_iter_vec_exact::<N>),
        blink: None,
    }
}

#[cfg(nightly)]
fn bench_fill_iter_vec_trusted<const N: usize>() -> BenchResult {
    BenchResult {
        stumpalo: fork_measure(measure_stump_fill_iter_vec_trusted::<N>),
        bumpalo: fork_measure(measure_bump_fill_iter_vec_exact::<N>),
        blink: None,
    }
}

fn bench_fill_iter_repeat_exact<const N: usize>() -> BenchResult {
    BenchResult {
        stumpalo: fork_measure(measure_stump_fill_iter_repeat_exact::<N>),
        bumpalo: fork_measure(measure_bump_fill_iter_repeat_exact::<N>),
        blink: None,
    }
}

#[cfg(nightly)]
fn bench_fill_iter_repeat_trusted<const N: usize>() -> BenchResult {
    BenchResult {
        stumpalo: fork_measure(measure_stump_fill_iter_repeat_trusted::<N>),
        bumpalo: fork_measure(measure_bump_fill_iter_repeat_exact::<N>),
        blink: None,
    }
}

fn bench_alloc_struct<const N: usize>() -> BenchResult {
    BenchResult {
        stumpalo: fork_measure(measure_stump_alloc_big::<N>),
        bumpalo: fork_measure(measure_bump_alloc_big::<N>),
        blink: fork_measure(measure_blink_alloc_big::<N>),
    }
}

// ---------------------------------------------------------------------------
// Bench functions returning BenchResult
// ---------------------------------------------------------------------------

#[derive(Clone, Copy)]
struct BenchResult {
    stumpalo: Option<Duration>,
    bumpalo: Option<Duration>,
    blink: Option<Duration>,
}

fn bench_alloc<T: Default + 'static>() -> BenchResult {
    BenchResult {
        stumpalo: fork_measure(|| measure_stump_alloc::<T>()),
        bumpalo: fork_measure(|| measure_bump_alloc::<T>()),
        blink: fork_measure(|| measure_blink_alloc::<T>()),
    }
}

fn bench_alloc_multiple<T: Default + 'static>() -> BenchResult {
    BenchResult {
        stumpalo: fork_measure(|| measure_stump_alloc_multiple::<T>()),
        bumpalo: fork_measure(|| measure_bump_alloc_multiple::<T>()),
        blink: fork_measure(|| measure_blink_alloc_multiple::<T>()),
    }
}

fn bench_alloc_array<T: Default + Copy + 'static, const N: usize>() -> BenchResult {
    BenchResult {
        stumpalo: fork_measure(|| measure_stump_alloc_array::<T, N>()),
        bumpalo: fork_measure(|| measure_bump_alloc_array::<T, N>()),
        blink: fork_measure(|| measure_blink_alloc_array::<T, N>()),
    }
}

fn bench_alloc_slice<T: Copy + Default, const N: usize>() -> BenchResult {
    BenchResult {
        stumpalo: fork_measure(|| measure_stump_alloc_slice::<T, N>()),
        bumpalo: fork_measure(|| measure_bump_alloc_slice::<T, N>()),
        blink: fork_measure(|| measure_blink_alloc_slice::<T, N>()),
    }
}

fn bench_alloc_sized_slice<T: Copy + Default, const N: usize>() -> BenchResult {
    BenchResult {
        stumpalo: fork_measure(|| measure_stump_alloc_sized_slice::<T, N>()),
        bumpalo: fork_measure(|| measure_bump_alloc_sized_slice::<T, N>()),
        blink: fork_measure(|| measure_blink_alloc_sized_slice::<T, N>()),
    }
}

fn bench_alloc_str(s: &[u8]) -> BenchResult {
    let s = unsafe { std::str::from_utf8_unchecked(s) };
    BenchResult {
        stumpalo: fork_measure(|| measure_stump_alloc_str(s)),
        bumpalo: fork_measure(|| measure_bump_alloc_str(s)),
        blink: fork_measure(|| measure_blink_alloc_str(s)),
    }
}

fn bench_alloc_slice_lit_u8<const N: usize>(data: &'static [u8; N]) -> BenchResult {
    // blink-alloc and bumpalo don't have slice_lit equivalents —
    // use alloc_slice_copy / copy_slice as comparison baselines.
    BenchResult {
        stumpalo: fork_measure(|| measure_stump_alloc_slice_lit::<N>(data)),
        bumpalo: fork_measure(measure_bump_alloc_slice::<u8, N>),
        blink: fork_measure(measure_blink_alloc_slice::<u8, N>),
    }
}

fn bench_alloc_str_lit<const N: usize>(data: &'static [u8; N]) -> BenchResult {
    // bumpalo and blink-alloc don't have str_lit equivalents —
    // use alloc_str / copy_str as comparison baselines.
    BenchResult {
        stumpalo: fork_measure(|| measure_stump_alloc_str_lit::<N>(data)),
        bumpalo: fork_measure(|| measure_bump_alloc_str_lit(N)),
        blink: fork_measure(|| measure_blink_alloc_str_lit(N)),
    }
}

fn bench_clear() -> BenchResult {
    fn prepare_bump() -> bumpalo::Bump {
        let b = bumpalo::Bump::new();
        for _ in 0..NUM_ALLOCS {
            black_box(b.alloc(0u64));
        }
        b
    }
    fn prepare_blink() -> blink_alloc::Blink {
        let b = blink_alloc::Blink::new();
        for _ in 0..NUM_ALLOCS {
            black_box(b.put(0u64));
        }
        b
    }

    BenchResult {
        stumpalo: fork_measure(|| {
            mean_without_outliers(&sample(SAMPLES, || {
                let mut a = stumpalo::Arena::new();
                for _ in 0..NUM_ALLOCS {
                    black_box(a.alloc(0u64));
                }
                a.clear();
                black_box(a.chunk_capacity());
            }))
        }),
        bumpalo: fork_measure(|| {
            mean_without_outliers(&sample(SAMPLES, || {
                let mut b = prepare_bump();
                b.reset();
                black_box(());
            }))
        }),
        blink: fork_measure(|| {
            mean_without_outliers(&sample(SAMPLES, || {
                let mut b = prepare_blink();
                b.reset();
                black_box(());
            }))
        }),
    }
}

fn bench_clear_and_reuse() -> BenchResult {
    fn prepare_bump() -> bumpalo::Bump {
        let b = bumpalo::Bump::new();
        for _ in 0..NUM_ALLOCS {
            black_box(b.alloc(0u64));
        }
        b
    }
    fn prepare_blink() -> blink_alloc::Blink {
        let b = blink_alloc::Blink::new();
        for _ in 0..NUM_ALLOCS {
            black_box(b.put(0u64));
        }
        b
    }

    BenchResult {
        stumpalo: fork_measure(|| {
            mean_without_outliers(&sample(SAMPLES, || {
                let mut a = stumpalo::Arena::new();
                for _ in 0..NUM_ALLOCS {
                    black_box(a.alloc(0u64));
                }
                a.clear();
                black_box(a.alloc(0u64));
            }))
        }),
        bumpalo: fork_measure(|| {
            mean_without_outliers(&sample(SAMPLES, || {
                let mut b = prepare_bump();
                b.reset();
                black_box(b.alloc(0u64));
            }))
        }),
        blink: fork_measure(|| {
            mean_without_outliers(&sample(SAMPLES, || {
                let mut b = prepare_blink();
                b.reset();
                black_box(b.put(0u64));
            }))
        }),
    }
}

// ---------------------------------------------------------------------------
// TSV table generation
// ---------------------------------------------------------------------------

fn tsv_table(results: &[(&'static str, BenchResult)]) -> String {
    let mut tsv = String::new();
    tsv.push_str("operation\tstumpalo\tblink-alloc\tbumpalo\n");
    for (name, r) in results {
        let st = fmt_cell(r.stumpalo, r.stumpalo, r.blink, r.bumpalo);
        let bl = fmt_cell(r.blink, r.stumpalo, r.blink, r.bumpalo);
        let bu = fmt_cell(r.bumpalo, r.stumpalo, r.blink, r.bumpalo);
        tsv.push_str(&format!("{}\t{}\t{}\t{}\n", name, st, bl, bu));
    }
    tsv
}

/// Format a single cell. If `d` is `None`, prints a dash.
/// Ratio is relative to the fastest of the three (self / fastest).
fn fmt_cell(
    d: Option<Duration>,
    stumpalo: Option<Duration>,
    blink: Option<Duration>,
    bumpalo: Option<Duration>,
) -> String {
    let d = match d {
        Some(d) => d,
        None => return String::from("          -"),
    };
    let fastest = stumpalo
        .unwrap_or(Duration::MAX)
        .min(bumpalo.unwrap_or(Duration::MAX))
        .min(blink.unwrap_or(Duration::MAX));
    let ratio = d.as_nanos() as f64 / fastest.as_nanos() as f64;
    let emoji = ratio_emoji(ratio);
    format!(
        "{} {:>5.2}x {:>7.1} µs",
        emoji,
        ratio,
        d.as_nanos() as f64 / 1000.0
    )
}

fn ratio_emoji(ratio: f64) -> &'static str {
    if ratio <= 1.05 {
        "✅" // winner / within noise
    } else if ratio <= 1.15 {
        "🟢" // slight lead
    } else if ratio <= 1.35 {
        "🟡" // close
    } else if ratio <= 1.75 {
        "🟠" // noticeable
    } else if ratio <= 2.5 {
        "🔴" // substantial
    } else {
        "🟥" // blowout
    }
}

// ---------------------------------------------------------------------------
// column(1) formatting — pipe TSV through Unix `column -t -s $'\t'`
// ---------------------------------------------------------------------------

/// Pipe plain TSV through `column -t -s $'\t'` for aligned output.
/// Falls back to raw TSV if column(1) is not available.
fn pipe_through_column(tsv: &str) -> String {
    use std::process::{Command, Stdio};
    let mut child = match Command::new("column")
        .args(["-t", "-s", "\t"])
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .env("LC_CTYPE", "C.utf8")
        .spawn()
    {
        Ok(c) => c,
        Err(_) => return tsv.to_owned(),
    };
    use std::io::Write;
    let _ = child.stdin.take().unwrap().write_all(tsv.as_bytes());
    match child.wait_with_output() {
        Ok(output) => String::from_utf8_lossy(&output.stdout).to_string(),
        Err(_) => tsv.to_owned(),
    }
}
