Skip to content

Fix crash in draw() when a multi-preview surface is removed concurrently - #2225

Open
x270880x wants to merge 1 commit into
pedroSG94:masterfrom
x270880x:fix/multi-preview-snapshot
Open

x270880x wants to merge 1 commit into
pedroSG94:masterfrom
x270880x:fix/multi-preview-snapshot

Conversation

@x270880x

Copy link
Copy Markdown
Contributor

Problem

GlStreamInterface.draw() takes a snapshot of the extra preview surfaces with multiPreviewSurfaceManagers.values.toList(). For a collection of exactly one element, Kotlin's toList() reads size and then calls iterator().next() as a separate step. If that surface is removed from another thread in between (in our app removeMultiPreviewSurface() is called from the main thread, while draw() runs on the GL thread), the iterator is already empty and next() throws NoSuchElementException. Without a RenderErrorCallback the exception is rethrown on the GL thread and the app crashes.

With two or more surfaces toList() already copies through an ArrayList, so only the common case of one extra preview is affected.

We hit this in production with 2.8.0 (the same line, 346 there): first as a crash on a Samsung Galaxy S24 Ultra (Android 16), and, after we set a RenderErrorCallback that skips the frame, again on a Vivo phone (Android 16). In both cases the exception came from the ConcurrentHashMap iterator inside toList() in draw().

Change

Take the snapshot with ArrayList(multiPreviewSurfaceManagers.values). ArrayList copies through ConcurrentHashMap's toArray(), which tolerates concurrent changes, so a surface removed meanwhile is simply in or out of this frame's snapshot, as already happens with two or more surfaces. One line, plus a comment so it isn't changed back to toList().

Testing

  • ./gradlew :library:compileReleaseKotlin and ./gradlew :library:testDebugUnitTest (22 tests) pass locally.
  • A throwaway JVM loop, not included in the PR: one thread keeps removing and re-adding the only entry of a ConcurrentHashMap, another snapshots values the way draw() does. This is an artificial worst case, only to show the mechanism: toList() threw NoSuchElementException 1,839,677 times in 6,835,364 snapshots in 5 s; ArrayList(values) threw 0 times in 29,150,687.
  • Not run on a device: the window is tiny, and on a phone it only shows up occasionally.

🤖 Generated with Claude Code

draw() snapshots multiPreviewSurfaceManagers with values.toList().
For a single-element collection Kotlin's toList() reads size and then
calls iterator().next() as a separate step. If that surface is removed
from another thread in between (removeMultiPreviewSurface() on the
main thread while draw() runs on the GL thread), next() throws
NoSuchElementException on the GL thread, and without a render error
callback the app crashes.

ArrayList(values) copies through ConcurrentHashMap's toArray(), which
tolerates concurrent changes, so a surface removed meanwhile is simply
in or out of this frame's snapshot, as it already is with two or more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

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