From 95699baadc7ba330bf7c345f10106de5658f2358 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:45:22 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20[performance=20improvement]?= =?UTF-8?q?=20optimize=20terminal=20search=20case=20folding=20hot=20loop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Lucenx9 <185146821+Lucenx9@users.noreply.github.com> --- .jules/bolt.md | 3 + .../src/gtk_app/terminal_search.rs | 69 ++++++++++++++----- 2 files changed, 55 insertions(+), 17 deletions(-) create mode 100644 .jules/bolt.md diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 00000000..cbc439b9 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2023-10-27 - Terminal Search Hot Loop Case Folding +**Learning:** In the `terminal_search.rs` file, the `chars_eq_ignore_case` fallback invokes two `.to_lowercase()` iterators which is too slow for the hot path of searching a large scrollback. We can optimize it by short-circuiting ASCII characters using pre-calculated lowercase and uppercase bounds. However, a surprising edge case is that some non-ASCII characters (like the Kelvin sign `\u{212A}`) map to an ASCII character (`k`) when lowercased. Therefore, we must NOT reject a haystack character solely based on `h != first_lower && h != first_upper` if it's not pure ASCII. We must fallback to `chars_eq_ignore_case` for any non-ASCII haystack characters to preserve correctness. +**Action:** When implementing ASCII fast-paths for string or character comparisons in Rust, ensure non-ASCII inputs properly fallback to full unicode case folding routines, even if the search query (needle) is purely ASCII, to avoid dropping matches for homoglyphs like Kelvin sign. diff --git a/crates/forktty-ui-gtk/src/gtk_app/terminal_search.rs b/crates/forktty-ui-gtk/src/gtk_app/terminal_search.rs index 90694aa2..46bbd097 100644 --- a/crates/forktty-ui-gtk/src/gtk_app/terminal_search.rs +++ b/crates/forktty-ui-gtk/src/gtk_app/terminal_search.rs @@ -75,25 +75,60 @@ fn for_each_char_match_start( return; } let first_needle = needle[0]; - let mut index = 0; - while index + needle.len() <= haystack.len() { - // Fast-path: short-circuit the full substring check if the first character - // doesn't match, avoiding iterator overhead in the common case. - if !chars_eq_ignore_case(haystack[index], first_needle) { - index += 1; - continue; + + // Fast-path for the most common case: ASCII search queries. + if first_needle.is_ascii() { + let first_lower = first_needle.to_ascii_lowercase(); + let first_upper = first_needle.to_ascii_uppercase(); + + let mut index = 0; + while index + needle.len() <= haystack.len() { + let h = haystack[index]; + + // Check direct ASCII match first to avoid function call overhead + if h != first_lower && h != first_upper { + // If it's pure ASCII and doesn't match, we can safely skip. + // For non-ASCII haystack characters, we must fallback to the full + // unicode-aware check, as characters like Kelvin sign ('\u{212A}') + // uppercase/lowercase to ASCII 'k'. + if h.is_ascii() || !chars_eq_ignore_case(h, first_needle) { + index += 1; + continue; + } + } + + let matched = haystack[index + 1..index + needle.len()] + .iter() + .zip(&needle[1..]) + .all(|(a, b)| chars_eq_ignore_case(*a, *b)); + if matched { + if !visit(index) { + return; + } + index += needle.len(); + } else { + index += 1; + } } - let matched = haystack[index + 1..index + needle.len()] - .iter() - .zip(&needle[1..]) - .all(|(a, b)| chars_eq_ignore_case(*a, *b)); - if matched { - if !visit(index) { - return; + } else { + let mut index = 0; + while index + needle.len() <= haystack.len() { + if !chars_eq_ignore_case(haystack[index], first_needle) { + index += 1; + continue; + } + let matched = haystack[index + 1..index + needle.len()] + .iter() + .zip(&needle[1..]) + .all(|(a, b)| chars_eq_ignore_case(*a, *b)); + if matched { + if !visit(index) { + return; + } + index += needle.len(); + } else { + index += 1; } - index += needle.len(); - } else { - index += 1; } } }