Fix multi-second typing lag from suggestion pileup and redundant search - #854
Open
lexuschris wants to merge 1 commit into
Open
Fix multi-second typing lag from suggestion pileup and redundant search#854lexuschris wants to merge 1 commit into
lexuschris wants to merge 1 commit into
Conversation
Three compounding issues in the live-suggestion/autocorrect pipeline caused typing to freeze for several seconds, worst on long or fast-typed words: 1. SUGGESTION_DEBOUNCE_MS was 10ms — well under any human typing cadence, so it fired a full suggestion computation on nearly every keystroke instead of coalescing bursts of typing. Raised to 150ms. 2. UrikDictionary's fuzzy-match DFS had no cooperative cancellation checkpoints, so cancelling a superseded suggestion job did nothing until that DFS finished on its own. Combined with (1), rapid typing queued many uncancellable searches that piled up on the shared Dispatchers.Default pool. Threads the calling coroutine's Job through getCandidates() and checks it every 512 visited nodes so a cancelled search actually stops. 3. SpellCheckManager additionally ran a separate "fat-finger" dictionary search for every (character position x adjacent key) variant of the typed word — tens of extra full searches for a longer word. This was redundant: the primary search is already an exhaustive edit-distance-2 search, which by construction finds every single-substitution typo (adjacent-key or not) as a special case. Confirmed via the dedup path, which recomputes true edit distance from the original word regardless of which search found a candidate. Removed the fat-finger expansion and its now-dead adjacent-key-map plumbing. Verified via on-device logcat timing across iterations: worst-case decide() latency went from 7751ms to 422ms (on a 36-character stress input; typical long words are well under 100ms). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #853 — typing could freeze for multiple seconds (worst case ~7.75s observed) on longer words or fast typing, with keystrokes and spaces appearing to "arrive all at once" after the freeze clears. Thank you for the work you all put into this project — it's a pleasure to use, and I wanted to give this one a thorough writeup since it took a few passes to fully track down.
Three separate, compounding issues in the suggestion pipeline:
SuggestionPipeline.SUGGESTION_DEBOUNCE_MSwas10L— well under any human typing cadence, so a full suggestion computation fired on nearly every keystroke instead of only after a pause in typing.UrikDictionary's fuzzy-match DFS (the on-device Levenshtein-automaton dictionary search) had no cooperative cancellation checkpoints, sojob.cancel()on a superseded suggestion request did nothing until that search finished on its own. Combined with the debounce issue, normal typing speed queued many uncancellable searches that piled up competing for CPU.SpellCheckManager.queryUrikSuggestions()additionally ran a separate full dictionary search for every (character position × adjacent QWERTY key) variant of the typed word — tens of extra searches for a longer word. This turned out to be redundant: the primary search is already an exhaustive edit-distance-2 search, which by construction already finds every single-substitution typo (adjacent-key or not) as a special case. Confirmed via the dedup/scoring path, which recomputes true edit distance from the original word regardless of which search found a given candidate — fat-finger-sourced results never got different treatment.Changes
SuggestionPipeline.kt:SUGGESTION_DEBOUNCE_MSraised from10Lto150Lso rapid typing actually coalesces into one suggestion request per pause, instead of firing on every keystroke.UrikDictionary.kt:getCandidates()/dfs()now optionally take the calling coroutine'sJoband checkensureActive()every 512 visited trie nodes, so a cancelled search actually stops instead of running to completion regardless.SpellCheckManager.kt:queryUrikSuggestions()now threads thatJobthrough togetCandidates(), and the fat-finger variant expansion (and its now-unusedcachedAdjacentKeyMapplumbing) has been removed as redundant with the primary search.KeyboardModule.kt: dropped the now-unusedfatFingerExpanderparameter fromprovideSpellCheckManager().SpellCheckManagerTest.kt: updated the 4 tests that referenced the removed constructor parameter — 3 renamed to reflect that the primary edit-distance search alone (not a dedicated fat-finger path) is what's being verified, 1 removed as an exact duplicate of another after the change.FatFingerExpanderitself is untouched and still independently tested (FatFingerExpanderTest.kt) — it's just no longer wired into the suggestion pipeline, since nothing needs it there anymore. Happy to remove the class entirely too if you'd rather not carry unused code, just didn't want to make that call unilaterally.Root cause discovery
Found via a layered on-device logcat timing investigation (temporary, since-removed instrumentation) rather than static reading alone — the first hypothesis (dictionary eviction under memory pressure) didn't hold up against the data, and each subsequent layer of timing (space-press level → per-language fan-out → per-search breakdown → per-search node-visit counts) narrowed it down further, first to the debounce/cancellation issue, then to the fat-finger expansion once that pileup was fixed and the individual search cost for long words was still visibly too high.
Test plan
./gradlew compileDebugKotlin/compileDebugUnitTestKotlin— clean build./gradlew ktlintCheck— passes./gradlew detekt— passes./gradlew test— full suite, 1815 tests, 0 failures