diff --git a/Cargo.toml b/Cargo.toml index d4e6f04..2c4604f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,10 @@ tempfile = "3.10" name = "staging_bench" harness = false +[[bench]] +name = "app_bench" +harness = false + # cargo-dist configuration [workspace.metadata.dist] # CI backends diff --git a/benches/app_bench.rs b/benches/app_bench.rs new file mode 100644 index 0000000..e98dcfa --- /dev/null +++ b/benches/app_bench.rs @@ -0,0 +1,181 @@ +//! Benchmarks for git-twig application operations +//! +//! These benchmarks measure the performance of core operations: +//! - Tree flattening (Node::flatten) +//! - Filter nodes (App::filter_nodes) +//! - ANSI code stripping (strip_ansi_codes) +//! - Full refresh cycle + +use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; +use std::collections::HashSet; + +use git_twig::node::{Node, NodeType}; +use git_twig::theme::{Theme, ThemeType}; +use git_twig::tui::app::{strip_ansi_codes, App}; + +/// Create a test tree with the specified depth and breadth +fn create_test_tree(depth: usize, breadth: usize) -> Node { + fn build_level(current_depth: usize, max_depth: usize, breadth: usize) -> Node { + if current_depth >= max_depth { + // Create a file node using the constructor + Node::new_file( + format!("file_{}.rs", current_depth), + format!("path/to/file_{}.rs", current_depth), + "M".to_string(), + Some((10, 5)), // (added, deleted) + ) + } else { + // Create a directory with children + let mut children = Vec::new(); + for _ in 0..breadth { + let child = build_level(current_depth + 1, max_depth, breadth); + children.push(child); + } + // Add some file nodes at each level + for i in 0..3 { + children.push(Node::new_file( + format!("file_{}.rs", i), + format!("level_{}/file_{}.rs", current_depth, i), + "M".to_string(), + Some((i + 1, i)), + )); + } + Node::new_dir( + format!("dir_{}", current_depth), + format!("path/to/dir_{}", current_depth), + children, + ) + } + } + + build_level(0, depth, breadth) +} + +/// Benchmark tree flattening with various tree sizes +fn bench_tree_flatten(c: &mut Criterion) { + let mut group = c.benchmark_group("tree_flatten"); + let theme = Theme::new(ThemeType::Unicode); + let collapsed_paths: HashSet = HashSet::new(); + + // Test with different tree configurations (depth, breadth) + for (depth, breadth) in [(3, 3), (4, 4), (5, 3), (3, 10)].iter() { + let tree = create_test_tree(*depth, *breadth); + let node_count = count_nodes(&tree); + + group.bench_with_input(BenchmarkId::new("nodes", node_count), &tree, |b, tree| { + b.iter(|| { + black_box(tree.flatten(2, false, &theme, &collapsed_paths)); + }); + }); + } + + group.finish(); +} + +/// Count total nodes in a tree +fn count_nodes(node: &Node) -> usize { + match &node.node_type { + NodeType::File { .. } => 1, + NodeType::Directory { children } => 1 + children.iter().map(count_nodes).sum::(), + } +} + +/// Benchmark filter_nodes with various query lengths and node counts +fn bench_filter_nodes(c: &mut Criterion) { + let mut group = c.benchmark_group("filter_nodes"); + let theme = Theme::new(ThemeType::Unicode); + let collapsed_paths: HashSet = HashSet::new(); + + // Create trees of different sizes + for (depth, breadth) in [(3, 5), (4, 4), (5, 3)].iter() { + let tree = create_test_tree(*depth, *breadth); + let flat_nodes = tree.flatten(2, false, &theme, &collapsed_paths); + let node_count = flat_nodes.len(); + + // Test with different query lengths + for query in ["", "file", "dir_2", "nonexistent"].iter() { + let id = format!("{}_nodes_query_{}", node_count, query.len()); + group.bench_with_input(BenchmarkId::new("filter", &id), &flat_nodes, |b, nodes| { + b.iter(|| { + black_box(App::filter_nodes(nodes, query)); + }); + }); + } + } + + group.finish(); +} + +/// Benchmark ANSI code stripping +fn bench_strip_ansi_codes(c: &mut Criterion) { + let mut group = c.benchmark_group("strip_ansi_codes"); + + // Sample strings with ANSI codes (simulating diff output) + let test_strings: Vec = vec![ + // No ANSI codes + "plain text without any formatting".to_string(), + // Single ANSI code + "\x1B[32m+added line\x1B[0m".to_string(), + // Multiple ANSI codes + "\x1B[31m-\x1B[0m\x1B[31mdeleted line with \x1B[1mbold\x1B[0m text\x1B[0m".to_string(), + // Long line with many codes + (0..50) + .map(|i| { + if i % 2 == 0 { + format!("\x1B[32m+line {}\x1B[0m", i) + } else { + format!("\x1B[31m-line {}\x1B[0m", i) + } + }) + .collect::>() + .join("\n"), + ]; + + for (idx, s) in test_strings.iter().enumerate() { + let id = format!("len_{}_idx_{}", s.len(), idx); + group.bench_with_input(BenchmarkId::new("strip", &id), s, |b, s| { + b.iter(|| { + black_box(strip_ansi_codes(s)); + }); + }); + } + + group.finish(); +} + +/// Benchmark multiple strip_ansi_codes calls (simulating diff search) +fn bench_strip_ansi_batch(c: &mut Criterion) { + let mut group = c.benchmark_group("strip_ansi_batch"); + + // Simulate a diff with many lines + let diff_lines: Vec = (0..500) + .map(|i| { + if i % 3 == 0 { + format!("\x1B[32m+added line {} with some content\x1B[0m", i) + } else if i % 3 == 1 { + format!("\x1B[31m-deleted line {} with content\x1B[0m", i) + } else { + format!(" context line {} unchanged", i) + } + }) + .collect(); + + group.bench_function("500_lines", |b| { + b.iter(|| { + for line in &diff_lines { + black_box(strip_ansi_codes(line)); + } + }); + }); + + group.finish(); +} + +criterion_group!( + benches, + bench_tree_flatten, + bench_filter_nodes, + bench_strip_ansi_codes, + bench_strip_ansi_batch, +); +criterion_main!(benches); diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f9c640e --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,12 @@ +//! git-twig library for benchmarking and testing +//! +//! This module exposes internal types for benchmarking purposes. + +pub mod cache; +pub mod config; +pub mod git; +pub mod icons; +pub mod node; +pub mod parser; +pub mod theme; +pub mod tui; diff --git a/src/tui/app.rs b/src/tui/app.rs index 999ae45..c0fa888 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -1199,7 +1199,8 @@ impl App { static ANSI_RE: LazyLock = LazyLock::new(|| regex::Regex::new(r"\x1B\[[0-9;]*[mK]").unwrap()); -fn strip_ansi_codes(s: &str) -> String { +/// Strip ANSI escape codes from a string +pub fn strip_ansi_codes(s: &str) -> String { ANSI_RE.replace_all(s, "").to_string() } diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 85d9dad..7d3963b 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -6,7 +6,7 @@ use crossterm::{ use ratatui::{backend::CrosstermBackend, Terminal}; use std::io; -mod app; +pub mod app; mod event; mod history; mod ui;