Skip to content

fix(🤖): stop a TextureView canvas from freezing the UI thread on Android 11 - #543

Draft
YevheniiKotyrlo wants to merge 2 commits into
wcandillon:mainfrom
YevheniiKotyrlo:fix/android-11-textureview-acquire
Draft

YevheniiKotyrlo wants to merge 2 commits into
wcandillon:mainfrom
YevheniiKotyrlo:fix/android-11-textureview-acquire

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Problem

On Android 11, a canvas backed by a TextureView (the default backing view) freezes the UI thread for 4.3 s at a time when it renders from the UI thread. Android 11's Vulkan loader passes the swapchain's dequeue timeout as an int where the window reads an int64_t (swapchain.cpp, Surface.cpp), so the TextureView's buffer queue waits up to 4.29 s for a free buffer instead of failing at once. A frame acquired before the window has drawn the previous one waits for that draw, which only the UI thread can run. react-native-skia's TextureView canvas has the same freeze; wcandillon/react-native-skia#4251 fixes it there.

Solution

On Android 11, a TextureView canvas acquires its next texture only once the window has drawn the frame presented last. A frame that comes sooner renders into the offscreen texture and is copied onto the surface as soon as the window has drawn, so the latest frame always shows and nothing waits:

  • WebGPUView.acquiresAfterDraw decides it (a TextureView on Android 11) and passes it with the surface.
  • SurfaceInfo counts the presents and the presents the window has drawn; getCurrentTexture() defers a frame while the two differ, and the deferred frame is copied onto the surface with the attach blit's copy.
  • WebGPUTextureView.onSurfaceTextureUpdated reads the present count while the window draws and posts it behind that draw, so a draw counts only the presents it took.
  • With the gate in place, a present made on the UI thread waits behind the window's pending draw on Android 11 too, as fix(🤖): show every frame a worklet presents on a TextureView canvas #542 has it on every other release (WebGPUView.presentsBehindPendingDraw).

Every other release and backing view is unchanged. The canvas docs mention the Android 11 copy.

Verification

  • TextureViewPresent now presents from the UI thread and from the JS thread, and fails when the last frame does not show or when a frame stalls the thread for 2 s. On the Android 11 emulator the UI-thread case fails with fix(🤖): show every frame a worklet presents on a TextureView canvas #542 alone, in 2 of 2 runs (a 4339 ms stall, and a 13 s run whose readback is then aborted), and passes with this change in 2 of 2; on the Android 15 emulator both cases pass either way.
  • The Reanimated UI-thread screen on its default TextureView, two 30 s windows on the Android 11 emulator: eight 4.3 s freezes in each window on main and with fix(🤖): show every frame a worklet presents on a TextureView canvas #542 alone, none with this change (the window drew 43–46 frames per window on main, 43–44 with fix(🤖): show every frame a worklet presents on a TextureView canvas #542 alone and 986–1004 with this change) (measurement). Other releases are unchanged: only a TextureView on Android 11 acquires after the draw.
  • CI's Android e2e step on an API 35 emulator: 222 of 224, main's 220 and the two TextureViewPresent cases; the two failures, the YUV cases of ExternalTextureColorSpace and NativeBufferWrapper, fail on main there too.
  • On a Galaxy S23 (Android 14) the whole suite passes, 224 of 224; main c080934e passes 222 of 222. On an iPad Pro 12.9-inch (4th generation, iOS 27.0.1), built and run as CI's iOS job does on a simulator (Debug build, Metro with CI=true), it passed 223 of 223 before the rebase onto feat(🤖): name the Android backing view explicitly #574 (main 8f664fca: 221 of 221).
  • yarn lint · yarn tsc · the package and web-template builds · yarn test:plugin · yarn check-dawn · yarn build:docs · cpplint · clang-format on the changed lines · the example's iOS build for the simulator SDK, on eef933cf, whose iOS-compiled files this head keeps.

Base

Stacked on #542, which posts a present made on the UI thread behind the window's pending draw everywhere but Android 11; this change brings it to Android 11 with the acquire gate. Both are on main c080934e.

@YevheniiKotyrlo

YevheniiKotyrlo commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor Author

Device measurement — before / after

main — on Android 11, the Reanimated UI-thread screen on its default TextureView canvas freezes the UI thread eight times in each 30 s window, 4.3 s each (dequeueBuffer timed out).

This PR — no freeze: the window draws 986–1004 frames in each window. #542 alone leaves Android 11 as main has it.

Android 11 emulator (API 30), per 30 s window main c080934e #542 0f8007bd this PR e0673609
dequeueBuffer timeouts, 4.3 s UI-thread freezes each 8 / 8 8 / 8 0 / 0
ANRs 0 / 0 0 / 0 0 / 0
HWUI frames rendered 46 / 43 44 / 43 1004 / 986

TextureViewPresent on the same emulator: its UI-thread case fails with #542's natives in 2 of 2 runs (a 4339 ms stall against its 2 s limit, and a 13 s run whose readback is then aborted) and passes with this PR's in 2 of 2.

Release builds of the example opening Reanimated's UI-thread screen on its default TextureView canvas, two 30 s windows per build, timeouts and ANRs read from logcat and frames from dumpsys gfxinfo; the e2e spec ran in debug builds with Metro. The emulator's janky share comes from its GL translation, so phones are the measure of jank; outside Android 11 this PR changes nothing.

A canvas drawn from Reanimated's UI thread presents inside the main
thread's Choreographer frame, ahead of the window's draw. A TextureView
takes its next buffer only when the window draws, and its buffer queue
replaces a buffer it has not taken yet, so the next present replaced
every other frame and the window showed half of them.

A frame presented on the main thread to a TextureView surface now waits
for a present the view posts to the main looper, behind the draw the
window has pending, as react-native-skia's FrameScheduler does. The next
frame boundary presents a frame still waiting first, a detach drops it
with its surface, and frames presented on other threads, or to a
SurfaceView or HardwareBufferView, are presented as before.

Android 11 keeps presenting at once: its Vulkan loader gives a
TextureView's buffer queue a 4.3 s dequeue timeout, and a present held
behind the draw stretched those waits into 14 s freezes and an ANR on
the Android 11 emulator.

The weak handle the hardware buffer view's waker kept on its view is
shared by both calls from native, the canvas docs say when a UI-thread
frame reaches a TextureView, and the e2e suite presents frames from a
worklet on a TextureView canvas on the tests screen.
…oid 11

Android 11's Vulkan loader gives a TextureView's queue a 4.29 s dequeue
timeout where it means none, and a canvas presenting on the UI thread
then waits there for a buffer only that thread's next draw gives back.
On Android 11 a TextureView canvas acquires only once its window has
drawn the frame presented last, rendering into the offscreen texture
meanwhile, so a frame presented on the UI thread waits behind the
window's pending draw there too. The TextureViewPresent spec runs on
the UI thread as well.
@YevheniiKotyrlo
YevheniiKotyrlo force-pushed the fix/android-11-textureview-acquire branch from eef933c to e067360 Compare October 18, 2026 19:08

This branch has not been deployed

No deployments
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.

1 participant