Repository navigation
fix(android): prevent CameraX executor starvation - #4218
Merged
mrousavy merged 1 commit intoOct 14, 2026
Merged
Conversation
|
@ismaelsousa is attempting to deploy a commit to the Margelo Team on Vercel. A member of the Team first needs to authorize it. |
Member
|
Wow nice catch! Yes i think this is correct thanks |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Prevent long-running Android frame worklets from starving CameraX's internal
ImageAnalysisbackground work.This removes the explicit
setBackgroundExecutor(executor)configuration from both the regular frame output and the depth frame output. Analyzer callbacks and theHybridNativeThreadcontinue to share their existing single-threadIdentifiableExecutor, preserving serialized frame processing, while CameraX can use its own performance-tuned background executor for internal pipeline work.Problem
HybridFrameOutputpreviously used the sameIdentifiableExecutor("com.margelo.camera.frame")for three different responsibilities:ImageAnalysisinternal background work viaImageAnalysis.Builder.setBackgroundExecutor(...).imageAnalysis.setAnalyzer(...).HybridNativeThreadused by the frame worklet runtime.A synchronous frame worklet therefore blocked not only the next analyzer callback, which is expected, but also CameraX's own background tasks on the same single-thread queue.
In a Perfetto trace with an approximately two-second synchronous worklet, CameraX tasks queued behind the worklet for up to approximately 1.95 seconds. This stalled the image-analysis pipeline and caused the camera preview to visibly freeze. Separating CameraX's background work from the analyzer/worklet executor kept the preview smooth under the same workload.
User-visible impact
This executor starvation is the cause of the preview stall: while the synchronous worklet occupies the shared executor, CameraX cannot promptly run the internal tasks needed to receive and discard image-analysis frames. The resulting pipeline delay makes the preview freeze for roughly the duration of the blocking worklet, even though preview rendering itself is not running on the worklet thread.
Video
A before-and-after video demonstrating the preview stall and the behavior after this fix will be added here.
cut.mov
after.mov
How to reproduce on Android
Add a frame output that intentionally blocks its worklet thread for two seconds:
Root cause
CameraX documents that the
KEEP_ONLY_LATESTimage-analysis strategy uses an internal executor to receive and discard stale frames. It also notes that a caller-provided background executor must respond quickly for smooth operation.The executor provided here cannot guarantee that: it intentionally runs analyzer and worklet tasks serially, and user worklets can execute synchronously for an arbitrary amount of time. Reusing it for CameraX's internal work lets application processing starve the camera pipeline.
Changes
HybridFrameOutput.HybridDepthFrameOutput, which had the identical executor coupling.setAnalyzer(executor)unchanged, so analyzer callbacks remain serialized with the worklet runtime as before.clearAnalyzer()during disposal).No tracing or diagnostic instrumentation is included in this change.
Why use CameraX's default executor?
Leaving the background executor unset is the smallest and safest separation:
Before and after
Before:
After:
Verification
./gradlew :react-native-vision-camera:compileDebugKotlin --console=plain./gradlew :react-native-vision-camera:testDebugUnitTest --console=plainNO-SOURCE.git diff --checkImageAnalysisoutput paths: