Skip to content

Wear OS 6 (Galaxy Watch 5 Pro): first utterance after a service restart 0.06-1.2 s late, and display-state work blocks the main thread up to 1.6 s #152

Closed

Description

@devinprater

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, appId fyi.quin.backtalk, the only enabled accessibility service on the watch
  • TTS engine org.evvdroid (the watch's default synth too)
  • Method: adb shell input gestures 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:

  1. Samsung's FreecessController freezes 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.

  1. 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 requestAudioFocus for fyi.quin.backtalk: 595, 668, 432, 542, 551, 539 ms
  • requestAudioFocus to audio_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, then AudioTrack: 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.backtalk and AudioHardening 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

  1. trypsynth commented on Oct 11, 2026

    @trypsynth
    Owner

    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.restoreLastFocusedNode also 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.
    • The late first utterance after a restart: as you saw, it isn't lost, it waits in the queue until the speech engine is connected. The restarts themselves (the watch stopping Backtalk under memory pressure) and the engine being frozen between phrases by Samsung's FreecessController are outside Backtalk, and each restart has to reconnect to the engine, so we can't remove that wait. We'll keep in mind your point that anything clearing the queue in that window would drop the announcement.
    • The AudioHardening notices, where One UI Watch treats screen reader audio as background playback, are worth a closer look, but we don't have a Galaxy watch to test on, and speech still plays.

    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.

  2. trypsynth commented on Oct 12, 2026

    @trypsynth
    Owner

    @devinprater how does the latest backtalk feel?

  3. devinprater commented on Oct 14, 2026

    @devinprater
    SponsorAuthor

    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, then Waking up from Dozing ... reason=WAKE_REASON_WAKE_KEY). The watchdog is the same code in 272 and 318, so the counts compare.

    • Screen off, 12 windows / 110 s: 0 blocks of 100 ms or more.
    • Screen on, 12 windows / 90 s: 13 blocks, 101 to 275 ms, 1.87 s in total.
    • Shade open, 6 gestures: 14 blocks, 109 to 376 ms, 3.0 s. Shade close, 6 gestures: 6 blocks, 107 to 190 ms, 0.84 s.
    • Whole run: 33 blocks, 5.51 s blocked, median 151 ms, worst 376 ms.
    • A second run with the display cycles only: 18 blocks, 4.15 s, median 172 ms, worst 809 ms — and that worst one is the first screen-on after the install, while the service was starting.

    Your 272 capture has 4 blocks inside DirectTouchController and 3 in the display listener on the same path, 112 to 251 ms. Neither new run contains a single DirectTouchController frame.

    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 one getMatchingDescendant frame 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

  4. trypsynth commented on Oct 26, 2026

    @trypsynth
    Owner

    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.restoreLastFocusedNode re-reads the last focused item from the app on the main thread, and that refresh() 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. The WindowEventDelayer blocks 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.

  5. trypsynth commented on Oct 27, 2026

    @trypsynth
    Owner

    @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:

    1. Whether the restoreLastFocusedNode blocks are gone from the screen-on stalls.
    2. Whether focus still comes back to the item you were on when the watch wakes, and how soon it's spoken.

    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.

  6. devinprater commented on Oct 27, 2026

    @devinprater
    SponsorAuthor

    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, goToSleep and WAKE_REASON_WAKE_KEY at each mark, there is not one BacktalkStall block, and restoreLastFocusedNode appears in no stack. Build 318 had 13 blocks of 101 to 275 ms at screen on, six of them restoreLastFocusedNode, so that work looks off the main thread now.

    Focus and speech:

    • Real wrist raise at 04:48:21.249, WAKE_REASON_WAKE_MOTION at 04:48:22.787, speech at 04:48:23.502, so 0.72 s after the wake.
    • Wake with Settings in front at 04:54:52.085, WAKE_REASON_WAKE_KEY, speech at 04:54:52.627, 0.54 s after the wake, and Settings was still the resumed activity afterwards.
    • Wake with the watch face in front: silent, with no focused item to restore.

    Caveat: the 8 cycles were injected with KEYCODE_SLEEP and KEYCODE_WAKEUP, so they report WAKE_REASON_WAKE_KEY rather 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.

  7. trypsynth commented on Nov 2, 2026

    @trypsynth
    Owner

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions