Skip to content

Android: stop re-arming the idle DISPATCH_UI Choreographer frame callback (behind feature flag) - #58378

Open
capt-muji wants to merge 2 commits into
react:mainfrom
capt-muji:fix/android-idle-mount-items-choreographer
Open

Android: stop re-arming the idle DISPATCH_UI Choreographer frame callback (behind feature flag)#58378
capt-muji wants to merge 2 commits into
react:mainfrom
capt-muji:fix/android-idle-mount-items-choreographer

Conversation

@capt-muji

@capt-muji capt-muji commented Sep 7, 2026

Copy link
Copy Markdown

Stop re-arming the idle DISPATCH_UI Choreographer frame callback (Android)

Part of #58367. Split from #58368 per review feedback (one PR per pump, each behind a feature flag).

Summary

FabricUIManager.DispatchUIFrameCallback.doFrameGuarded re-schedules itself unconditionally in its finally block, so the DISPATCH_UI Choreographer callback runs at vsync rate forever — including while no mount items, pre-mount items or view commands are pending.

This PR makes the re-schedule demand-gated, behind ReactNativeFeatureFlags.disableIdleMountItemFrameCallbackRearmAndroid (default false = current behavior; ossReleaseStage: 'experimental'):

  • doFrameGuarded's finally only re-schedules while mMountItemDispatcher.hasPendingItems() (new helper on MountItemDispatcher).
  • Re-arm on queueing: items can be queued from any thread (view commands via dispatchCommand are @AnyThread; JNI mount-item scheduling also queues cross-thread) and previously relied on the always-armed callback for eventual dispatch. MountItemDispatcher now notifies its listener (onItemsQueued, called from addMountItem / addViewCommandMountItem / addPreAllocateMountItem, flag-gated) and FabricUIManager re-arms by hopping to the UI queue — schedule() is idempotent (mIsScheduled guard) and stays UI-confined. Without this, off-UI-thread items could starve at idle; this is the re-arm-trigger completeness point called out in the review of fix(android): stop re-arming idle Choreographer frame callbacks at vsync rate #58368.

Foregrounding / backgrounding

onHostResumeresume() (re-schedules) and onHostPausepause() (mShouldSchedule = false; schedule() no-ops) are untouched. Items queued while paused are dispatched after resume, as on main. Items arriving on the UI thread during a commit keep the existing inline dispatch path (scheduleMountItem runs tryDispatchMountItems in a guarded runnable); items added during a dispatch leave hasPendingItems() true, so the finally re-arms.

Rollout

Flag: disableIdleMountItemFrameCallbackRearmAndroiddefaultValue: false, expectedReleaseValue: true, ossReleaseStage: 'experimental'. With the flag off, behavior is identical to current main (onItemsQueued is never invoked; the finally re-schedules unconditionally).

Test plan

  • Flag off (defaultValue): both new paths are inert.
  • Compiles: ./gradlew :packages:react-native:ReactAndroid:compileReleaseKotlin :packages:react-native:ReactAndroid:compileReleaseJavaWithJavac.
  • Flag on, real app on device (OnePlus 3T, Android 9): idle adb shell atrace shows DISPATCH_UI doFrames drop to ~0 with a static UI; any render (navigation, state updates) re-arms and commits land the same frame or the next; dispatchCommand from JS (e.g. scroll-to flows) still executes promptly at idle; background/foreground round-trips keep the UI responsive.
  • Flag on: RNTester rendering integration tests pass.

Changelog:

[Android] [Fixed] - Stop re-arming the DISPATCH_UI Choreographer frame callback at vsync rate while no mount items are pending; queueing items re-arms it (behind disableIdleMountItemFrameCallbackRearmAndroid)

Prior art

FabricUIManager carried an on-demand DISPATCH_UI schedule (postChoreographerCallbackIfNecessary) removed by #43044 with the rest of enableOnDemandReactChoreographer. Mechanism differences: #58367.

DispatchUIFrameCallback.doFrameGuarded re-schedules itself unconditionally in
its finally block, running at vsync rate while no mount items are pending.
Behind disableIdleMountItemFrameCallbackRearmAndroid (default off): the finally
re-schedules only while items remain pending, and MountItemDispatcher notifies
its listener when items are queued (any thread) so FabricUIManager can re-arm
the callback - covering off-UI-thread view commands that previously relied on
the always-armed pump.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Sep 7, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Sep 7, 2026
@meta-codesync

meta-codesync Bot commented Sep 7, 2026

Copy link
Copy Markdown

@javache has imported this pull request. If you are a Meta employee, you can view this in D119071289.

@capt-muji

capt-muji commented Sep 7, 2026

Copy link
Copy Markdown
Author

On-device validation (OnePlus 3T, Android 9 / API 28, bridgeless Release)

Backported onto v0.86.3 (see the #58375 comment for why) with disableIdleMountItemFrameCallbackRearmAndroid forced ON, substituted into a real production app.

Result: DISPATCH_UI posts once (the initial mount) and then 0 at idle — the finally-block re-schedule only fires with pending items, and the MountItemDispatcher.onItemsQueued re-arm path works. All render commits and view commands functional (pager swipes, sheet open/close, overlay open/close) across the interaction smoke battery. No starvation.

@capt-muji

capt-muji commented Sep 7, 2026

Copy link
Copy Markdown
Author

Prior-art note for reviewers landing here: FabricUIManager carried an on-demand DISPATCH_UI schedule (postChoreographerCallbackIfNecessary with mIsFrameCallbackScheduled and MountItemDispatcher.hasMountItems()), armed from the addMountItem wrapper methods, onAnimationStarted, onHostResume and per-frame. #43044 removed it in February 2024 together with the rest of enableOnDemandReactChoreographer. Full analysis on the parent issue: #58367.

Correction after self-review against current main

An earlier revision of this comment claimed C++ animation work reaches the surface through mount items and is therefore covered. That was wrong. DispatchUIFrameCallback itself drives C++ animation sessions every frame:

  • driveCxxAnimations() when mDriveCxxAnimations is set (via onAnimationStarted from Binding.cpp, e.g. LayoutAnimations) or cxxNativeAnimatedEnabled() is on
  • driveAnimationBackend() when useSharedAnimatedBackend() is on

and a session can be active on a frame with no pending mount items. The old code's hasMountItems() || mDriveCxxAnimations condition existed for exactly this reason, and our initial port dropped it.

d45be0e restores that protection: the re-arm condition now also keeps the callback armed while mDriveCxxAnimations is set or either C++ animation backend flag is enabled (both default off), and onAnimationStarted re-arms the callback so a session beginning at idle gets its first tick. The LayoutAnimations single-schedule invariant is unchanged (schedule() is idempotent).

One soft edge documented for reviewers

drainPreallocateViewsQueue drains a C++-side preallocation queue from this callback. If a preallocate request ever lands with no Java-side mount items pending, it waits for the next armed frame. Preallocation is a prefetch optimization, so the degradation is a late preallocate rather than a stall, but we could not find a cheap Java-visible query for that queue's size and would welcome a reviewer's take.

Follow-up to the demand-gated re-arm. DispatchUIFrameCallback also drives
C++ animation work every frame: driveCxxAnimations (armed by onAnimationStarted
from Binding.cpp, e.g. LayoutAnimations), the shared animation backend behind
useSharedAnimatedBackend, and cxxNativeAnimatedEnabled. The previous condition
could disarm the callback while an animation session was active but no mount
items were pending, starving the session of ticks. The re-arm condition now
also keeps the callback armed while mDriveCxxAnimations is set or either C++
animation flag is enabled, and onAnimationStarted re-arms the callback so a
session that begins at idle gets its first tick. This mirrors the
hasMountItems() || mDriveCxxAnimations condition the old
enableOnDemandReactChoreographer experiment used before its removal in react#43044.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant