Improve dictation hot-path performance#172
Conversation
|
Additional loop update:
Validation rerun after this loop: ./gradlew.bat :app:testDebugUnitTest --tests "dev.patrickgold.florisboard.dictate.*" :app:compileDebugKotlin :lib:dictate-core:compileDebugKotlin :wear:compileDebugKotlin
git diff --check origin/main...HEADBoth passed locally. |
|
Additional high-performance loop with Oracle/GPT-5.5 Pro:
Why this matters:
Validation rerun after this loop: ./gradlew.bat :app:testDebugUnitTest --tests "dev.patrickgold.florisboard.dictate.*" :app:compileDebugKotlin :lib:dictate-core:compileDebugKotlin :wear:compileDebugKotlin
git diff --check origin/main...HEADBoth passed locally. Oracle output was saved locally as |
|
Additional Oracle/GPT-5.5 Pro high-performance loop:
What changed:
Expected value:
Validation rerun after this loop: ./gradlew.bat :app:testDebugUnitTest --tests "dev.patrickgold.florisboard.dictate.*" :app:compileDebugKotlin :lib:dictate-core:compileDebugKotlin :wear:compileDebugKotlin
git diff --check origin/main...HEADBoth passed locally. The Gradle run exited with code 0 and all Dictate tests passed. Oracle output was saved locally as |
|
Additional Oracle/GPT-5.5 Pro high-performance loop:
What changed:
Expected value:
Validation rerun after this loop: ./gradlew.bat :app:testDebugUnitTest --tests "dev.patrickgold.florisboard.dictate.*" :app:compileDebugKotlin :lib:dictate-core:compileDebugKotlin :wear:compileDebugKotlin
git diff --check origin/main...HEADBoth passed locally. The focused Oracle output was saved locally as |
High-performance loop 6: coalesce realtime IME preview updatesOracle / GPT-5.5 Pro reviewed the current PR state and picked realtime preview UI coalescing/throttling for the IME path as the next highest-impact target. The recommendation is saved locally at Why this adds performance valueBefore this commit, every realtime provider partial could become an immediate editor preview write:
That is expensive in exactly the hot path where providers can emit many small partial revisions per second. Even when the text only changes by a few characters, the keyboard still does main-thread editor work, common-prefix comparisons, InputConnection commits/replacements, and preview-state rewrites at provider callback frequency. This commit bounds that cost for the IME path by coalescing preview writes to about one update every 66 ms (~15 fps). The latest transcript remains visible quickly enough for the user, but editor writes are no longer tied to token/partial callback frequency. On a fast partial stream, that turns potentially dozens or hundreds of editor writes per second into a small, predictable number of writes per second. What changedCommit:
Expected user-visible impactRealtime dictation should feel smoother in text fields under high partial-update rates:
Risk controlsThe main correctness risk was stale state: after coalescing, the latest transcript and the visible preview are no longer always identical. The patch handles that explicitly:
ValidationRan successfully: .\gradlew.bat :app:testDebugUnitTest --tests "dev.patrickgold.florisboard.dictate.*" :app:compileDebugKotlin :lib:dictate-core:compileDebugKotlin :wear:compileDebugKotlin
git diff --check origin/main...HEADResult: |
Review follow-up: realtime final callback orderingI reviewed the PR after the latest realtime preview coalescing change and found one subtle lifecycle risk in the controller:
Commit
This keeps the performance benefit from coalescing editor preview writes, but removes the ordering race between final transcript receipt and session-close finalization. Validation rerun after the fix: .\gradlew.bat :app:testDebugUnitTest --tests "dev.patrickgold.florisboard.dictate.*" :app:compileDebugKotlin :lib:dictate-core:compileDebugKotlin :wear:compileDebugKotlin
git diff --check origin/main...HEADResult: |
Conflict resolution and review follow-upMerged the latest Conflict resolution
Additional fixes found during review
I also re-reviewed the remaining performance changes against the current ValidationPassed locally after resolving the conflicts: .\gradlew.bat :app:testDebugUnitTest --tests "dev.patrickgold.florisboard.dictate.*" :app:compileDebugKotlin :lib:dictate-core:compileDebugKotlin :wear:compileDebugKotlin
.\gradlew.bat :app:testDebugUnitTest
git diff --check origin/main...HEADResults:
|
|
@DevEmperor : Please review and merge. |
Resolve DictateController audio-level/preview scheduling and preserve realtime handshake buffering with optimized audio framing.
85965e5 to
973b653
Compare
Summary
This PR reduces avoidable CPU work and allocation pressure in the dictation hot paths, with a focus on realtime streaming, audio encoding, lightweight preference parsing, local transcription reuse, PCM resampling, and Wear recording shutdown behavior.
The changes are intentionally split into small commits so each optimization has a narrow behavioral surface:
kotlinx.serializationJSON object construction for realtime base64 audio payloads.StringBuilder.appendcalls.split()list allocations.numThreadsin the local recognizer cache key so changing thread count actually rebuilds the native recognizer.AudioRecordbefore joining the recorder thread, propagate read/write failures, and make waveform peak tracking atomic.Why this adds value
Dictation performance is dominated by work that happens repeatedly while the microphone is active or while audio is being uploaded. Even small allocations in those paths matter on Android because they happen many times per second, compete with audio/UI work, and increase garbage collection pressure. This is especially relevant for realtime transcription and Wear devices, where CPU headroom, battery, and thermal budget are tighter than on a desktop.
The biggest improvements are in repeated audio handling:
Base64OutputStreamwith a 64 KiB buffer and append encoded chunks through a reusableCharArray. This avoids a large amount of tiny append work while preserving the same multipart payload format.doneand capturewsbefore encoding/sending. Closed sessions no longer pay Base64 or ByteString allocation cost for frames that will be dropped anyway.The smaller parser changes reduce background churn in frequently touched UI/accounting code:
day:words;...store directly and preserves malformed-entry tolerance.time:<value>orcount:<value>.There are also correctness fixes that improve practical performance by preventing stale or slow behavior:
numThreads. Without this, a user-visible thread-count change could keep reusing a recognizer built with the old parallelism, making performance tuning appear ineffective until the model/language changed.AudioRecord.stop()before joining the recorder thread, which avoids waiting on a blockingread()during stop/cancel. It also surfaces read/write failures instead of silently producing a misleading WAV file and uses an atomic peak value for live waveform polling.Review process
I used an Oracle/GPT-5.5 Pro review pass to identify hot-path opportunities and risk boundaries, then implemented the items that were low-risk and directly measurable by code inspection/compile-time validation. The review specifically highlighted realtime JSON builders, Base64 string output, split-heavy helpers, PCM16 resampling, local recognizer cache invalidation, and Wear recorder shutdown/error handling. The resulting changes keep provider configuration serialization unchanged and only specialize the stable high-frequency payloads.
Validation
Validated locally on the current
mainbaseline plus this branch:./gradlew.bat :app:testDebugUnitTest --tests "dev.patrickgold.florisboard.dictate.*" :app:compileDebugKotlin :lib:dictate-core:compileDebugKotlin :wear:compileDebugKotlinResult:
BUILD SUCCESSFUL.Also ran:
Result: clean.
Focused tests covered:
DictateAutoFormattingTestDictateLanguageMatchTestAudioWavTestDictateStatsTestPcm16ResamplerTestNew/updated test coverage verifies malformed daily-stat tolerance, fixed-window activity parsing, and odd trailing-byte handling in PCM16 resampling.
Risk and compatibility notes
Base64.NO_WRAPoutput. Provider setup/config JSON still uses structured serialization.