Skip to content

Fix multi-second typing lag from suggestion pileup and redundant search - #854

Open
lexuschris wants to merge 1 commit into
urikdev:mainfrom
lexuschris:investigate/mms-typing-lag
Open

Fix multi-second typing lag from suggestion pileup and redundant search#854
lexuschris wants to merge 1 commit into
urikdev:mainfrom
lexuschris:investigate/mms-typing-lag

Conversation

@lexuschris

Copy link
Copy Markdown

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_MS was 10L — 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, so job.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_MS raised from 10L to 150L so 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's Job and check ensureActive() every 512 visited trie nodes, so a cancelled search actually stops instead of running to completion regardless.
  • SpellCheckManager.kt: queryUrikSuggestions() now threads that Job through to getCandidates(), and the fat-finger variant expansion (and its now-unused cachedAdjacentKeyMap plumbing) has been removed as redundant with the primary search.
  • KeyboardModule.kt: dropped the now-unused fatFingerExpander parameter from provideSpellCheckManager().
  • 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.

FatFingerExpander itself 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
  • Verified live on a Pixel 6 Pro (GrapheneOS): typed long/fast sentences that reliably froze for multiple seconds before the fix; after the fix, typing kept up with no perceptible lag and spelling corrections/suggestions were noticeably more accurate (since suggestions from a stale, superseded keystroke were no longer occasionally winning the race)
  • On-device logcat timing confirmed the fix at each stage: worst-case suggestion latency went from ~7.75s → ~3.9s (debounce + cancellation fix alone) → ~420ms (after also removing the redundant fat-finger search), the last of those on a 36-character stress-test input well beyond normal word length

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Typing freezes for multiple seconds on longer words / fast typing

1 participant