Repository navigation
Conversation
The UI runtime loop re-arms on every frame, and software-mansion#10196 pauses it in onHostPause. The queue started unpaused, so when React starts without an Activity (a headless JS task), the host never resumes or pauses and the loop runs on every vsync until the process is frozen or killed. Start the queue paused. ReactContext.addLifecycleEventListener calls onHostResume right away when the app is already resumed, so normal launches start the loop as before. JavaTimerManager starts paused the same way.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Priority: ➖ Normal Change: Bug fix · Severity of issue fixed: Low Merge Risk: ⚪ Minimal · up to
|
Note
This pull request was authored by AI on behalf of @hirvesh.
Summary
Follow-up to #10196.
The UI runtime's frame loop requests the next frame on every frame (#8900), and #10196 stops it while the app is in the background by pausing
AnimationFrameQueueinonHostPause. But the queue starts unpaused, andonHostPauseonly comes after anonHostResume. When React starts without an Activity, for example for a headless JS task (HeadlessJsTaskService, or the headless loaderexpo-task-manageruses for background tasks), the host is never resumed, so the queue is never paused either. From the moment the UI runtime is set up, the main thread runsAnimationFrameCallback.onAnimationFrameon every vsync until the process is frozen or killed, with nothing on screen.We found this through a background ANR in production (react-native-worklets 0.13.0, React Native 0.88). The process had no visible Activity, and the main thread was in a Worklets frame callback:
The stack doesn't show that the loop caused the ANR: the thread is waiting for a runtime-wide suspension to end. But a frame callback shouldn't be running in that process at all.
This PR starts
AnimationFrameQueuepaused:WorkletsModule.initialize()registers its lifecycle listener, andReactContext.addLifecycleEventListenercallsonHostResumeright away when the context is already resumed. If the module initializes before the Activity resumes, the loop starts on the firstonHostResume.onHostPause.JavaTimerManagerstarts paused the same way (isPaused = AtomicBoolean(true)).Test plan
AnimationFrameQueue.ktis byte-identical onmainand in 0.13.0.compileReleaseKotlinpasses, and the compiled class initializesmPausedtotrue.expo-background-taskjob (adb shell cmd jobscheduler run -f -n androidx.work.systemjobscheduler <package> <job id>). React starts headless throughexpo-task-manager, runs the task and is torn down 2 s after the task finishes.Choreographer#doFrameon every vsync (60 Hz). This went on from the moment the UI runtime was set up until React was torn down: 241 frames in about 4 s. Sampled stacks showAnimationFrameQueue.executeQueue→AnimationFrameCallback.onAnimationFrame→WorkletRuntime::runSyncAndDiscard. The main thread ran for 344 ms.JavaTimerManager$TimerFrameCallback, not Worklets. The main thread ran for 90 ms.AnimationFrameQueue.executeQueuein 27 samples (638 frames), so the loop starts as before once the Activity resumes.schedand theviewatrace category, pluslinux.perfcallstack sampling. Start sampling after the process exists:traced_perfdoesn't unwind a process that starts after the trace begins.Changelog
yarn changelog:addfor each changed package, or this PR does not changereact-native-reanimatedorreact-native-worklets.