Repository navigation
Description
Summary
Two latency findings from a Wear OS 6 watch, measured with injected gestures while streaming logcat. Both are in the event/service path rather than in any app's screen contents. A trimmed logcat is attached (gist at the bottom).
Environment
- Samsung Galaxy Watch 5 Pro (SM-R920), Wear OS 6 / One UI 8 Watch, Android 16 (API 36), 32-bit userspace only (armeabi-v7a/armeabi)
- Backtalk
TfPu_release_16_2-2026_10_10_1340-wear, versionCode 70000272, appIdfyi.quin.backtalk, the only enabled accessibility service on the watch - TTS engine
org.evvdroid(the watch's default synth too) - Method:
adb shell inputgestures with device-side markers (log -t HERMES_MARK) written into the same logcat stream, so every hand-off shares one clock. About 11 minutes, 47,927 lines.
Finding 1: the first utterance after a service restart is 0.06 to 1.2 s late
The service restarted 3 times in 11 minutes (resumeInfrastructure() appears 8 times in the log). Every restart unbinds and rebinds TTS, which costs 250 to 300 ms:
18:57:49.894 I/TextToSpeech(12645): Disconnected from TTS engine
18:57:50.077 I/TextToSpeech(12645): Sucessfully bound to org.evvdroid
18:57:50.112 I/TextToSpeech(12645): Connected to TTS engine
18:57:50.118 I/TextToSpeech(12645): Setting up the connection to TTS engine...
18:57:50.203 I/TalkBackService(12645): resumeInfrastructure() ... getVersionName=TfPu_release_16_2-2026_10_10_1340-wear
18:57:50.291 E/talkback: SpeechControllerImpl(12645): TTS is not ready. Attempted to speak before TTS was initialized. Item: {utteranceId:"", fragments:[{text:Backtalk on, ...}], uninterruptible:true, ...}
18:57:51.488 I/MediaFocusControl(587): requestAudioFocus() ... fyi.quin.backtalk <-- actually spoken 1197 ms later
The startup utterance lands in SpeechControllerImpl.speak() before TTS finishes initialising, takes the not-ready branch (master ef18fc2, utils/src/main/java/com/google/android/accessibility/utils/output/SpeechControllerImpl.java line 1437) and returns. The item stays in feedbackQueue, and onTtsInitialized() (line 2584) speaks it once TTS is ready. Measured delay from that branch to speech starting, across four restarts: 59, 417, 769 and 1197 ms.
So the announcement is not lost here, it is late on every restart, and anything that clears feedbackQueue in that window would remove it entirely.
Two watch-specific things make this happen on every restart:
- Samsung's
FreecessControllerfreezes the TTS process between phrases:
18:51:19.176 D/MARsmini_FreecessController(587): FZ : org.evvdroid(10001), reason: LEV
18:52:42.277 D/MARsmini_FreecessController(587): UFZ : org.evvdroid(10001), FZ-time:2026/10/10 18:51:19.176 reason: Binder(0)
18:52:42.433 D/MARsmini_FreecessController(587): freezePackage org.evvdroid uid:10001 LEV <-- refrozen 5 ms after the utterance's audio went idle
18:53:03.184 D/MARsmini_FreecessController(587): UFZ : org.evvdroid(10001), FZ-time:2026/10/10 18:52:42.441 reason: screenOn-widget
Frozen for 83 s and 21 s in those two cases, and unfrozen only on demand. So the engine is cold at the start of nearly every utterance, and certainly at every restart.
- The watch restarts the service often under memory pressure (an app launch was enough).
Finding 2: main-thread stalls of 100 to 1578 ms, concentrated on display and window state changes
Over 11 minutes: 120 blocks of 100 ms or more, 31.7 s of blocked main thread in total, median 178 ms, worst 1578 ms. Distribution: 72 blocks in 100-199 ms, 24 in 200-299 ms, 13 in 300-399 ms, plus 700, 1000 and 1400-1578 ms outliers.
The biggest ones are display-state work on the main thread:
at android.os.BinderProxy.transactNative(Native Method)
at android.accessibilityservice.IAccessibilityServiceConnection$Stub$Proxy.findAccessibilityNodeInfoByAccessibilityId(IAccessibilityServiceConnection.java:1233)
at android.view.accessibility.AccessibilityInteractionClient.waitForResultTimedLocked(AccessibilityInteractionClient.java:1437)
at android.view.accessibility.AccessibilityWindowInfo.getRoot(AccessibilityWindowInfo.java:256)
at com.google.android.accessibility.utils.AccessibilityWindowInfoUtils.getRootCompat(AccessibilityWindowInfoUtils.java:234)
at com.google.android.accessibility.talkback.actor.FocusActorForScreenStateChange.restoreLastFocusedNode(FocusActorForScreenStateChange.java:179)
at com.google.android.accessibility.talkback.Pipeline.execute(Pipeline.java:431)
at com.google.android.accessibility.utils.AccessibilityNodeInfoUtils.getMatchingDescendant(AccessibilityNodeInfoUtils.java:1684)
at com.google.android.accessibility.talkback.directtouch.DirectTouchController.evaluate(DirectTouchController.kt:203)
at com.google.android.accessibility.talkback.directtouch.DirectTouchController.onDisplayStateChanged(DirectTouchController.kt:159)
at com.google.android.accessibility.utils.monitor.DisplayMonitor.onDisplayChanged(DisplayMonitor.java:124)
at android.hardware.display.DisplayManagerGlobal$DisplayListenerDelegate.handleDisplayEventInner(DisplayManagerGlobal.java:1595)
WindowEventInterpreter$WindowEventDelayer.handleMessage blocks 174 to 248 ms on the same path, and accessibility events themselves arrive on blocks of 112 to 736 ms (IAccessibilityServiceClientWrapper$$ExternalSyntheticLambda*).
Touch exploration is still useful on the watch, just less so than on a phone, so this path should stay. The cost is the problem: the descendant search and the window-root lookup both run on the main thread, on every display change, and display changes are constant on a watch (raise wrist, screen off, screen on). Moving that work off the main thread, or skipping the descendant search on the way to screen-off, would keep touch exploration and drop the block.
Measured timings, for context
- Notification shade open/close gesture, finger up to
requestAudioFocusforfyi.quin.backtalk: 595, 668, 432, 542, 551, 539 ms requestAudioFocustoaudio_hw_playback ... transited to Playing: 208 to 360 ms- Home key / window change to audible speech: about 1.0 s (with 208 + 131 + 199 ms of main-thread blocks inside that second)
- Launching Settings to speech start: 0.77 s
Other things in the same capture
- One real audio glitch:
AudioFlinger: prepareTracks_l BUFFER TIMEOUT ... due to underrun, thenAudioTrack: restartIfDisabled ... disabled due to previous underrun, restarting. - Every utterance rebuilds the speaker route (about ten mixer-control writes, output goes Ready, Idle, Playing over 120 to 170 ms) and tears it down again at standby.
- 18 x
EventTypeWindowContentChangedFeedbackRule: windowContentChanged: error, node is null. - 88 x Samsung AudioHardening notices about Backtalk's audio, including
AudioHardening focus request for req 3 ignored for fyi.quin.backtalkandAudioHardening background playback would be muted for org.evvdroid, level: full. It still plays, but One UI Watch is treating screen reader audio as background media.
Log
Trimmed logcat, including the markers, the TTS freeze/unfreeze lines, the TTS rebind cycles and three example stall stacks:
https://gist.github.com/devinprater/28b37663f3c13f82939904a26c4b5b54
The full unfiltered capture, or a re-run with different gestures, is available on request.
Activity
trypsynth commentedon Oct 11, 2026
Thank you, this is a really useful report, and the shared-clock markers made it easy to follow.
What we fixed (development build 276): the biggest display-change stall was direct touch. Every time the screen or display turned on or off, it looked at all the windows and asked the app in front for its contents, on the main thread. On a watch that's every turn of the wrist, and direct touch is almost never in use there. Now, when the screen turns off, direct touch only ends if it was on, without looking at any windows. When the screen turns on, it does nothing unless direct touch can actually turn on (an app is turned on in Direct touch, or lift to activate is on for the navigation bar), and then it waits until the screen has settled. So on a typical watch, that work is gone from screen changes entirely.
What we found, but haven't changed yet:
FocusActorForScreenStateChange.restoreLastFocusedNodealso fetches the window root on the main thread when the screen comes back on. That's TalkBack's own code, so we want to measure again with the direct touch work gone before changing it.Could you try build 276 and see whether it's better? A capture like this one would be great: the count and size of the 100 ms+ blocks around raising the wrist and screen on and off should show the difference most clearly.
Let direct touch check its switches before asking apps and the packag…
Fetch the window's root only when there is a focus to restore
trypsynth commentedon Oct 12, 2026
@devinprater how does the latest backtalk feel?
devinprater commentedon Oct 14, 2026
Ran the re-test on today's dev build,
TfPu_release_16_2-2026_10_13_2210-wear(versionCode 70000318, commit cbc1269 — that is 317 plus the speak-permission rename).Method: 12 cycles of screen off then screen on (
KEYCODE_SLEEP/KEYCODE_WAKEUP), then 6 notification shade open/close swipes, 19:50:00 to 19:54:30. Two things made the capture survive the screen sleeping: the log is written to a file on the watch itself, and the gesture script runs on the watch, so the link dying while the screen is off cannot lose it. Every display change is confirmed in the log (Going to sleep due to sleep_button, thenWaking up from Dozing ... reason=WAKE_REASON_WAKE_KEY). The watchdog is the same code in 272 and 318, so the counts compare.Your 272 capture has 4 blocks inside
DirectTouchControllerand 3 in the display listener on the same path, 112 to 251 ms. Neither new run contains a singleDirectTouchControllerframe.What is left on the display path is the piece you said you wanted to measure next: 6 of the 13 screen-on blocks are
FocusActorForScreenStateChange.restoreLastFocusedNode->FocusActionRecord.getFocusableNodeFromFocusRecord->AccessibilityNodeInfo.refresh->AccessibilityInteractionClient.waitForResultTimedLocked, 101 to 298 ms. The rest is the interpreter's own delayed handling (WindowEventInterpreter$WindowEventDelayer, 101 to 275 ms) and the accessibility event callbacks. The onegetMatchingDescendantframe that does still appear comes from TalkBack's own focus record code (NodeDescription.getText), not from direct touch.Two honest limits. One, the injected screen off and on are the same display-state change you get on a wrist raise, but they are not a sensor tilt; I can do a real wrist-raise pass if you want one. Two, this is not a same-build A/B — the watch refuses to install 272 over 318 and the dev APK is not debuggable — so the old numbers are the ones from my first report.
One more thing that works on this build: two-finger swiping to scroll between the notification shade, tiles and all apps. That works now too.
Trimmed log attached, same shape as before: markers, the sleep and wake lines, all 33 stall headlines, and three example stack traces.
https://gist.github.com/devinprater/6e01e61bc439b6ee60098ae5769b8038
Look up windows less often when the screen changes (#164)
trypsynth commentedon Oct 26, 2026
Thanks, Devin, and sorry for the slow reply. This was a really thorough retest, and running the capture on the watch itself was a good fix for the link dying.
Good to see that screen off is clean now, with no direct touch frames at all, and that two-finger scrolling between the shade, tiles and all apps works.
The screen-on blocks you found make sense. When the screen turns on,
FocusActorForScreenStateChange.restoreLastFocusedNodere-reads the last focused item from the app on the main thread, and thatrefresh()is the 101 to 298 ms wait. That's the next thing I'll look at: either doing that lookup off the main thread, or skipping it when the window hasn't changed. TheWindowEventDelayerblocks are the next target after that.A real wrist-raise pass would be great, but it'll be most useful once that fix is in a development build. I'll post here when there's a build to test.
Let the app answer in the background before restoring focus when the …
trypsynth commentedon Oct 27, 2026
@devinprater development build 419 has the screen-on change, from #286. When the screen turns on, Backtalk now asks the app for the last focused item on a background thread, and restores focus once the app has answered, or after half a second at most. That should take the
restoreLastFocusedNode->refresh()blocks off the main thread.If you can do the wrist-raise pass now, it would be good to know:
restoreLastFocusedNodeblocks are gone from the screen-on stalls.Use Check for updates, or the dev release, which has
backtalk-wear.apk. If anything goes wrong, Backup and restore in Backtalk's settings, under More, exports your settings, so you can uninstall, install an older build and import them again.devinprater commentedon Oct 27, 2026
Build 420 on the watch, SM-R920. Two setups: 8 forced sleep and wake cycles with the log written on the watch itself, and one real wrist raise that the watch did register.
Stalls: zero. Across the 8 cycles,
goToSleepandWAKE_REASON_WAKE_KEYat each mark, there is not oneBacktalkStallblock, andrestoreLastFocusedNodeappears in no stack. Build 318 had 13 blocks of 101 to 275 ms at screen on, six of themrestoreLastFocusedNode, so that work looks off the main thread now.Focus and speech:
WAKE_REASON_WAKE_MOTIONat 04:48:22.787, speech at 04:48:23.502, so 0.72 s after the wake.WAKE_REASON_WAKE_KEY, speech at 04:54:52.627, 0.54 s after the wake, and Settings was still the resumed activity afterwards.Caveat: the 8 cycles were injected with KEYCODE_SLEEP and KEYCODE_WAKEUP, so they report
WAKE_REASON_WAKE_KEYrather than a sensor reason. The display did change state at every mark.One thing about my watch rather than the build: wrist-tilt events are unreliable. The sensor logged 10 tilts between 04:21 and 04:48, then none through three deliberate raises, so a raise usually leaves the watch in its dim doze state and silent.
trypsynth commentedon Nov 2, 2026
Closing this, since your retest on build 420 found no stalls around screen on and off, and focus still comes back when the watch wakes. The late first utterance after a restart depends on the watch stopping Backtalk and freezing the speech engine, so that part stays as it is. Thanks for such thorough reports, Devin.