From 9837b9c1aabae594e719e79f604900e7855c7b89 Mon Sep 17 00:00:00 2001 From: Merlin <46696971+light-merlin-dark@users.noreply.github.com> Date: Thu, 19 Mar 2026 20:02:43 -0500 Subject: [PATCH] perf: fast-path identical snapshots in diff_snapshots (#922) * Update agent-browser optimization plan: PR #916 submitted, Phase 2 complete Co-authored-by: Hermes * perf(snapshot-diff): fast-path identical snapshots Co-authored-by: Hermes * chore(pr): remove unrelated docs from snapshot-diff fast-path --------- Co-authored-by: Merlin Co-authored-by: Hermes --- cli/src/native/diff.rs | 79 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) 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}" + ); + } }