luff 0.2.1

Print files with formatting
Documentation
//! Performance benchmarks for luff
//!
//! Run with: cargo bench

use clap_builder::Parser;
use criterion::{
    BatchSize, BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main,
};
use luff::{
    cli::Args,
    config::Config,
    printer::{MarkdownPrinter, SkipPatterns},
    walker::{Walker, WalkerEntry},
};
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;

/// Helper to create a Config rooted at a specific path
///
/// This temporarily changes CWD to ensure Config::from_args picks up the correct root.
fn config_for_root(root: &std::path::Path, args: &[&str]) -> Config {
    let original_cwd = std::env::current_dir().expect("Failed to get CWD");
    std::env::set_current_dir(root).expect("Failed to set CWD");

    let parsed_args = Args::parse_from(args);
    let config = Config::from_args(&parsed_args).expect("Failed to create config");

    std::env::set_current_dir(original_cwd).expect("Failed to restore CWD");
    config
}

/// Create a test directory structure with specified depth and breadth
fn create_test_structure(depth: usize, files_per_dir: usize) -> TempDir {
    let temp = TempDir::new().unwrap();
    create_recursive(temp.path(), depth, files_per_dir);
    temp
}

fn create_recursive(path: &std::path::Path, depth: usize, files_per_dir: usize) {
    if depth == 0 {
        return;
    }

    // Create files
    for i in 0..files_per_dir {
        let file_path = path.join(format!("file_{i}.txt"));
        fs::write(&file_path, format!("Content {i} - some data to read")).unwrap();
    }

    // Create subdirectories
    for i in 0..2 {
        let dir_path = path.join(format!("dir_{i}"));
        fs::create_dir(&dir_path).unwrap();
        create_recursive(&dir_path, depth - 1, files_per_dir);
    }
}

/// Benchmark the DirectoryWalker (scanning the filesystem)
fn bench_walker_traversal(c: &mut Criterion) {
    let mut group = c.benchmark_group("walker_traversal");

    for depth in [1, 3] {
        let files_per_dir = 5;
        // Calculate total items for throughput reporting
        // This is a rough geometric series calculation, but sufficient for estimates
        let total_items = (files_per_dir * 2_usize.pow(depth as u32 + 1)) as u64;

        group.throughput(Throughput::Elements(total_items));

        group.bench_with_input(BenchmarkId::from_parameter(depth), &depth, |b, &d| {
            // Setup: Create directory structure ONCE per batch
            b.iter_batched(
                || {
                    let temp = create_test_structure(d, files_per_dir);
                    // Create config rooted at the temp dir
                    let config = config_for_root(temp.path(), &["luff", "--max-files", "10000"]);
                    (temp, config)
                },
                |(_temp, config)| {
                    // Measure: Walker creation + Iteration
                    let walker = Walker::from_dir(&config).unwrap();
                    let count = walker.count();
                    black_box(count);
                },
                BatchSize::SmallInput,
            );
        });
    }
    group.finish();
}

/// Benchmark the FileListWalker (explicit file list)
fn bench_file_list_walker(c: &mut Criterion) {
    let mut group = c.benchmark_group("file_list_walker");

    // Setup a static set of files
    let temp = TempDir::new().unwrap();
    let file_count = 50;
    let files: Vec<PathBuf> = (0..file_count)
        .map(|i| {
            let path = temp.path().join(format!("file_{i}.txt"));
            fs::write(&path, format!("Content {i}")).unwrap();
            path
        })
        .collect();

    // Create config rooted at the temp dir so validation passes
    let config = config_for_root(temp.path(), &["luff"]);

    group.throughput(Throughput::Elements(file_count as u64));

    // Benchmark 1: Construction cost (Validation + Canonicalization)
    group.bench_function("construction", |b| {
        b.iter(|| {
            // We clone the file list to simulate passing a new Vec each time
            let input_files = files.clone();
            let walker = Walker::from_file_list(&input_files, &config).unwrap();
            black_box(walker);
        });
    });

    // Benchmark 2: Iteration cost (Pure in-memory iteration)
    group.bench_function("iteration", |b| {
        b.iter_batched(
            || Walker::from_file_list(&files, &config).unwrap(),
            |walker| {
                let count = walker.count();
                black_box(count);
            },
            BatchSize::SmallInput,
        );
    });

    group.finish();
}

/// Benchmark the Printer (Markdown formatting)
/// Note: This includes I/O cost as MarkdownPrinter reads the file
fn bench_printer_markdown(c: &mut Criterion) {
    let temp = TempDir::new().unwrap();
    let file_path = temp.path().join("bench_test.rs");
    let content = "fn main() { println!(\"Hello Benchmark\"); }".repeat(10);
    fs::write(&file_path, &content).unwrap();

    let entry = WalkerEntry {
        path: file_path.clone(),
        relative_path: PathBuf::from("bench_test.rs"),
        is_dir: false,
    };

    // Config doesn't need to be rooted at temp here since we are calling printer directly
    // and printer just reads the path in entry.path (which is absolute)
    let args = Args::parse_from(["luff"]);
    let config = Config::from_args(&args).unwrap();
    let options = config.printer_options();

    let mut group = c.benchmark_group("printer");
    group.throughput(Throughput::Bytes(content.len() as u64));

    group.bench_function("markdown_format_into_memory", |b| {
        b.iter(|| {
            let mut buffer = String::with_capacity(1024);
            let _ = MarkdownPrinter::format_entry_into(
                black_box(&entry),
                &mut buffer,
                &options.patterns,
                SkipPatterns::ENABLED,
            );
            black_box(buffer);
        });
    });

    group.finish();
}

/// Benchmark the Tree Printer formatting
fn bench_printer_tree(c: &mut Criterion) {
    let temp = TempDir::new().unwrap();
    let root = temp.path();

    // Create a simulated list of entries representing a tree
    let entries: Vec<WalkerEntry> = (0..100)
        .map(|i| {
            let rel = PathBuf::from(format!("src/component_{}/mod.rs", i % 10));
            WalkerEntry {
                path: root.join(&rel),
                relative_path: rel,
                is_dir: false,
            }
        })
        .collect();

    let mut group = c.benchmark_group("printer");
    group.throughput(Throughput::Elements(entries.len() as u64));

    group.bench_function("tree_format", |b| {
        b.iter(|| {
            let output = luff::printer::format_tree(black_box(&entries), root).unwrap();
            black_box(output);
        });
    });

    group.finish();
}

criterion_group!(
    benches,
    bench_walker_traversal,
    bench_file_list_walker,
    bench_printer_markdown,
    bench_printer_tree
);
criterion_main!(benches);