From 64fa81365f53c04bbabd053dc9b9d3065ecb7bdd Mon Sep 17 00:00:00 2001 From: Sephyi Date: Sun, 19 Apr 2026 18:58:26 +0200 Subject: [PATCH] perf(services): use Iterator::eq() for whitespace-stripped comparison Three functions compared two strings with whitespace stripped by first collecting both into full `String` allocations, then comparing equality. Replace each with a streaming `Iterator::eq()` over filtered char iterators. This drops two heap allocations per call and short-circuits on the first differing character, which is the hot path when bodies actually differ. Affected sites: - ContextBuilder::classify_span_change (src/services/context.rs) - ContextBuilder::classify_span_change_rich (src/services/context.rs) - AstDiffer::bodies_semantically_equal (src/services/differ.rs) Behaviour is identical: `Iterator::eq` compares element-by-element and returns true iff both iterators yield the same sequence of chars. Existing tests (context, analyzer, languages suites) cover the semantic-classification paths and continue to pass. Closes audit entry F-021 from #3. --- src/services/context.rs | 30 +++++++++++++++--------------- src/services/differ.rs | 8 ++++++-- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/services/context.rs b/src/services/context.rs index 8c59f91..f682cbf 100644 --- a/src/services/context.rs +++ b/src/services/context.rs @@ -321,18 +321,18 @@ impl ContextBuilder { // Compare non-whitespace character streams. // Correctly handles line wrapping while detecting actual content changes. - let old_text: String = removed_in_span + // Use streaming Iterator::eq to avoid allocating two full Strings and + // to short-circuit on the first differing character. + let old_chars = removed_in_span .iter() .flat_map(|l| l.chars()) - .filter(|c| !c.is_whitespace()) - .collect(); - let new_text: String = added_in_span + .filter(|c| !c.is_whitespace()); + let new_chars = added_in_span .iter() .flat_map(|l| l.chars()) - .filter(|c| !c.is_whitespace()) - .collect(); + .filter(|c| !c.is_whitespace()); - Some(old_text == new_text) + Some(old_chars.eq(new_chars)) } /// Classify changes within a symbol span with doc-vs-code distinction. @@ -393,18 +393,18 @@ impl ContextBuilder { return None; } - // Check whitespace-only first (same logic as classify_span_change) - let old_text: String = removed_in_span + // Check whitespace-only first (same logic as classify_span_change). + // Stream the filtered char iterators through Iterator::eq to avoid + // allocating intermediate Strings and short-circuit on inequality. + let old_chars = removed_in_span .iter() .flat_map(|l| l.chars()) - .filter(|c| !c.is_whitespace()) - .collect(); - let new_text: String = added_in_span + .filter(|c| !c.is_whitespace()); + let new_chars = added_in_span .iter() .flat_map(|l| l.chars()) - .filter(|c| !c.is_whitespace()) - .collect(); - if old_text == new_text { + .filter(|c| !c.is_whitespace()); + if old_chars.eq(new_chars) { return Some(SpanChangeKind::WhitespaceOnly); } diff --git a/src/services/differ.rs b/src/services/differ.rs index 1d04f34..930f434 100644 --- a/src/services/differ.rs +++ b/src/services/differ.rs @@ -283,9 +283,13 @@ impl AstDiffer { } fn bodies_semantically_equal(old_body: Option<&str>, new_body: Option<&str>) -> bool { - let strip = |s: &str| -> String { s.chars().filter(|c| !c.is_whitespace()).collect() }; match (old_body, new_body) { - (Some(o), Some(n)) => strip(o) == strip(n), + // Stream filtered char iterators through Iterator::eq to avoid + // allocating intermediate Strings and short-circuit on inequality. + (Some(o), Some(n)) => o + .chars() + .filter(|c| !c.is_whitespace()) + .eq(n.chars().filter(|c| !c.is_whitespace())), (None, None) => true, _ => false, }