diff --git a/.jules/bolt.md b/.jules/bolt.md index 8513f7ad5..0a1774476 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -27,3 +27,7 @@ ## 2024-05-18 - Optimize Regex pattern detection in DictionaryFacilitatorImpl **Learning:** `DictionaryFacilitatorImpl.isRegexPattern` used chained `String.contains()` and `String.startsWith()` calls, which internally execute full loops over the string (or delegate to regex). Replacing these chained calls with a single manual O(N) iteration reduces overhead and prevents repeatedly traversing the string. **Action:** Replace chained string detection methods (`contains`, `startsWith`, `endsWith`) with a single manual loop over the string's indices when checking multiple characters in performance hot paths. + +## 2024-05-18 - Optimize Regex split overhead +**Learning:** `LanguageDetector.detectByHeuristics` used `text.lowercase().split(Regex("[^\\p{L}]+")).filter { it.length >= 2 }`. While the Regex was pre-compiled, the split and filter operations still allocated intermediate lists and required regex engine execution, causing GC pressure in a frequently invoked text processing path. +**Action:** Replace `Regex.split` combined with `.filter` in hot paths with manual O(N) string index iteration using character checks (like `char.isLetter()`) and a `StringBuilder` to eliminate regex execution and avoid allocating intermediate collections. diff --git a/app/src/main/java/helium314/keyboard/latin/utils/LanguageDetector.kt b/app/src/main/java/helium314/keyboard/latin/utils/LanguageDetector.kt index 2be7e5f72..1419fcaa0 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/LanguageDetector.kt +++ b/app/src/main/java/helium314/keyboard/latin/utils/LanguageDetector.kt @@ -148,8 +148,6 @@ object LanguageDetector { return null } - private val NON_LETTER_REGEX = Regex("[^\\p{L}]+") - private fun detectByHeuristics(text: String, targetLangCode: String?): String? { val scores = mutableMapOf() @@ -175,18 +173,35 @@ object LanguageDetector { } } - val words = text.lowercase().split(NON_LETTER_REGEX).filter { it.length >= 2 } - for (word in words) { - if (SPANISH_WORDS.contains(word)) addScore("es", 2) - if (FRENCH_WORDS.contains(word)) addScore("fr", 2) - if (GERMAN_WORDS.contains(word)) addScore("de", 2) - if (ITALIAN_WORDS.contains(word)) addScore("it", 2) - if (PORTUGUESE_WORDS.contains(word)) addScore("pt", 2) - if (DUTCH_WORDS.contains(word)) addScore("nl", 2) - if (TURKISH_WORDS.contains(word)) addScore("tr", 2) - if (POLISH_WORDS.contains(word)) addScore("pl", 2) - if (ENGLISH_WORDS.contains(word)) addScore("en", 2) + val textLower = text.lowercase() + val len = textLower.length + val wordBuilder = StringBuilder() + + fun processWord() { + if (wordBuilder.length >= 2) { + val word = wordBuilder.toString() + if (SPANISH_WORDS.contains(word)) addScore("es", 2) + if (FRENCH_WORDS.contains(word)) addScore("fr", 2) + if (GERMAN_WORDS.contains(word)) addScore("de", 2) + if (ITALIAN_WORDS.contains(word)) addScore("it", 2) + if (PORTUGUESE_WORDS.contains(word)) addScore("pt", 2) + if (DUTCH_WORDS.contains(word)) addScore("nl", 2) + if (TURKISH_WORDS.contains(word)) addScore("tr", 2) + if (POLISH_WORDS.contains(word)) addScore("pl", 2) + if (ENGLISH_WORDS.contains(word)) addScore("en", 2) + } + wordBuilder.clear() + } + + for (i in 0 until len) { + val char = textLower[i] + if (char.isLetter()) { + wordBuilder.append(char) + } else { + processWord() + } } + processWord() if (scores.isEmpty()) return null