diff --git a/cli/src/native/diff.rs b/cli/src/native/diff.rs index c35a18b..251a39c 100644 --- a/cli/src/native/diff.rs +++ b/cli/src/native/diff.rs @@ -101,6 +101,21 @@ pub fn diff_screenshot( /// Compute a snapshot diff using the Myers algorithm via the `similar` crate. pub fn diff_snapshots(before: &str, after: &str) -> SnapshotDiffResult { + // Fast path: identical inputs. + // This avoids constructing the `similar` TextDiff object and running the diff + // iteration when agents compare a snapshot to itself (common in retry/loop + // workloads). + if before == after { + let unchanged = before.lines().count(); + return SnapshotDiffResult { + diff: String::new(), + additions: 0, + removals: 0, + unchanged, + changed: false, + }; + } + let text_diff = TextDiff::from_lines(before, after); let mut additions = 0usize; @@ -192,4 +207,68 @@ mod tests { assert_eq!(result.unchanged, 1); assert!(!result.diff.is_empty()); } + + #[test] + fn test_diff_snapshots_identical_fast_path() { + let input = "hello\nworld\n"; + let result = diff_snapshots(input, input); + assert!(!result.changed); + assert_eq!(result.additions, 0); + assert_eq!(result.removals, 0); + assert_eq!(result.unchanged, input.lines().count()); + assert!(result.diff.is_empty()); + } + + #[test] + #[ignore] + fn bench_diff_snapshots_identical_and_changed() { + use std::hint::black_box; + use std::time::Instant; + + let identical_a = (0..200) + .map(|i| format!("line {i}")) + .collect::>() + .join("\n"); + let identical_b = identical_a.clone(); + + let changed_a = identical_a.clone(); + let changed_b = (0..200) + .map(|i| { + if i == 123 { + format!("line {i} changed") + } else { + format!("line {i}") + } + }) + .collect::>() + .join("\n"); + + // Keep the iteration count high enough to measure, but low enough + // to avoid long CI times when someone runs `--ignored`. + let iters = 50_000usize; + + let start = Instant::now(); + let mut acc_changed = 0usize; + for _ in 0..iters { + let r = diff_snapshots(black_box(&identical_a), black_box(&identical_b)); + acc_changed ^= r.unchanged; + } + let identical_ms = start.elapsed().as_secs_f64() * 1000.0; + + let start = Instant::now(); + let mut acc_changed2 = 0usize; + for _ in 0..iters { + let r = diff_snapshots(black_box(&changed_a), black_box(&changed_b)); + acc_changed2 ^= r.additions; + } + let changed_ms = start.elapsed().as_secs_f64() * 1000.0; + + // Prevent the compiler from optimizing everything away. + black_box(acc_changed); + black_box(acc_changed2); + + println!( + "bench_diff_snapshots_identical_and_changed: iters={iters} identical_ms={identical_ms:.2} changed_ms={changed_ms:.2}" + ); + } }