Skip to content

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

Open
capt-muji wants to merge 1 commit 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 1 commit 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)

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.

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