Android: stop re-arming the idle NATIVE_ANIMATED_MODULE Choreographer frame callback (behind feature flag) - #58377
Conversation
… on Android The animated frame callback re-posts itself unconditionally every frame, running at vsync rate while no animations are active. Behind disableIdleNativeAnimatedFrameCallbackRearmAndroid (default off): the callback arms only while animations run; didDispatchMountItems and the operation-enqueue helpers re-arm it when new animation operations arrive (covering imperative JS starts with no pending mount items).
|
cc @zeyap - is this worth doing when C++ Animated is close to rolling out? Is the idle callback problem solved for C++ animated? |
|
For @zeyap's question: when the C++ animated backend is enabled ( |
On-device validation (OnePlus 3T, Android 9 / API 28, bridgeless Release)Backported onto Result: the legacy One thing worth flagging for @zeyap / @javache: Reanimated ( |
|
Prior-art note for reviewers landing here: #41606 landed essentially this same gate for Mechanism difference
Known remaining riskThis is the question @zeyap was cc'd for above: if the C++ shared animated backend drives animations that never surface through the Java |
Audit note: the event-driven path (the gap class that plausibly broke #41606)Finding: event dispatch does not depend on this frame callback.
What still needs the frame callback: time-driven progression only, and every timed animation start routes through the operations queue ( The remaining open question is the C++ shared animated backend above. |
Stop re-arming the idle
NATIVE_ANIMATED_MODULEChoreographer frame callback (Android)Part of #58367. Split from #58368 per review feedback (one PR per pump, each behind a feature flag).
Summary
NativeAnimatedModule's frame callback re-posts itself unconditionally at the end of everydoFrameGuarded, so theNATIVE_ANIMATED_MODULEChoreographer callback runs at vsync rate forever — including while no animations are active.This PR makes the re-post demand-gated, behind
ReactNativeFeatureFlags.disableIdleNativeAnimatedFrameCallbackRearmAndroid(default false = current behavior;ossReleaseStage: 'experimental'). The callback stays armed only whilehasActiveAnimations()is true, with two explicit re-arm points for animations being started:didDispatchMountItems— animation operations (e.g.startAnimatingNode) execute in batches piggy-backed on mount-item dispatch; after executing a batch we re-arm unconditionally (the callback itself disarms on the next frame if nothing is actually active).addOperation/addUnbatchedOperation/addPreOperation) — imperative JS animation calls can arrive from the native module thread when no mount items are pending at all; enqueueing re-arms the callback.ReactChoreographer.postFrameCallbackhops to the UI thread internally, so this is safe from the module thread;enqueuedAnimationOnFrameis now@Volatile(it is written from both threads; the worst pre-existing race outcome is a redundant post).Foregrounding / backgrounding
onHostResume→enqueueFrameCallback()andonHostPause/onHostDestroy→clearFrameCallback()are untouched. With the flag on, a resumed-but-idle app disarms after one frame with no active animations; the next animation operation re-arms.Rollout
Flag:
disableIdleNativeAnimatedFrameCallbackRearmAndroid—defaultValue: false,expectedReleaseValue: true,ossReleaseStage: 'experimental'. With the flag off, behavior is identical to currentmain.Test plan
defaultValue): the unconditionalenqueueFrameCallback()path is taken verbatim../gradlew :packages:react-native:ReactAndroid:compileReleaseKotlin.adb shell atraceshows theNATIVE_ANIMATED_MODULEdoFrames drop to ~0 while no animations run; starting an animation (Reanimated/Animated) arms it again and the animation completes;Animated.loop/repeat animations keep it armed continuously; pause/resume does not lose running animations.Changelog:
[Android] [Fixed] - Stop re-arming the NATIVE_ANIMATED_MODULE Choreographer frame callback at vsync rate while no animations are active (behind
disableIdleNativeAnimatedFrameCallbackRearmAndroid)Prior art
The same gate was landed by #41606 (November 2023) and backed out by #41671 as "caused performace problems with react app". Mechanism differences and analysis: #58367.