fix(android): stop re-arming idle Choreographer frame callbacks at vsync rate - #58368
fix(android): stop re-arming idle Choreographer frame callbacks at vsync rate#58368capt-muji wants to merge 1 commit into
Conversation
…ync rate Four Android frame callbacks re-post themselves unconditionally on every doFrame, keeping the Choreographer armed at ~60fps while an app is foreground-idle with zero pending work (zero timers, zero animations, zero mount items, zero frames rendered): - JavaTimerManager.TimerFrameCallback (JavaTimerManager.kt) - FabricEventDispatcher.ScheduleDispatchFrameCallback - NativeAnimatedModule.animatedFrameCallback - FabricUIManager.DispatchUIFrameCallback Each now re-arms only when it has work, and re-arms lazily from its registration/posting path (createTimer / didDispatchMountItems / schedule). Measured on a stock RN 0.86.3 template app, idle foreground: ~600 doFrames/10s at 0.2-2ms each and 3.5-22.5% process CPU across API 28-36 devices; after this change: 0 doFrames, ~0% CPU, app fully functional.
|
Hi @capt-muji! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
|
We looked at improving some of these in the past, but it's very easy to cause regressions here, especially when foregrounding/background. Can you split this PR into the different changes, and add feature flags so we can control the rollout? |
|
Thanks @javache — that makes sense. Plan: split this into per-pump PRs (JavaTimerManager timers, FabricEventDispatcher, NativeAnimatedModule, FabricUIManager/MountItemDispatcher), each introducing its own |
|
Split complete per review — closing in favor of: #58375 (JavaTimerManager / TIMERS_EVENTS), #58376 (FabricEventDispatcher one-shot), #58377 (NativeAnimatedModule / NATIVE_ANIMATED_MODULE), #58378 (FabricUIManager / DISPATCH_UI + re-arm on item queue). Each is behind its own ReactNativeFeatureFlags flag defaulting to current behavior (ossReleaseStage: experimental). Beyond the original diff, the split versions add the re-arm-trigger completeness points identified during the split: timers re-arm covers headless-JS execution while the host is paused (#58375), native-animated re-arms on operation enqueue so imperative JS animation starts can't starve without pending mount items (#58377), and mount-item queueing from any thread re-arms DISPATCH_UI so off-UI-thread view commands can't starve at idle (#58378). |
Summary
Resolves #58367.
Four Android frame callbacks re-post themselves unconditionally at the end of their own
doFrame(armed at host resume), keeping the main-thread Choreographer running at ~60fps while an app is idle in the foreground with zero pending work and zero frames rendered. Measured on a stock 0.86.3 template app: ~600 doFrames/10s at 0.2–2ms each, ~3.5–22.5% process CPU across API 28–36 devices,Total frames rendered: 0(full data in the issue).This makes each pump re-arm only when it has work, re-arming lazily from its registration/posting path:
JavaTimerManager.TimerFrameCallback— re-post only while the timer queue is non-empty;createTimerre-arms when a new timer arrives (posting is thread-safe;ReactChoreographerdocuments any-thread use, and the flag dance stays undertimerGuard).FabricEventDispatcher.ScheduleDispatchFrameCallback— one-shot per schedule request. Events are dispatched synchronously indispatchEvent; this callback only notifiesBatchEventDispatchedListeners, sodoFramehas nothing to re-post.stop()now simply skips listener notification (the stop/resume of the re-post chain is no longer needed).NativeAnimatedModule.animatedFrameCallback—enqueueFrameCallback()moves inside the existinghasActiveAnimations()branch;didDispatchMountItemsre-arms after operation batches execute (wherestartAnimatingNode-style operations fliphasActiveAnimations()true).FabricUIManager.DispatchUIFrameCallback— thefinally { schedule(); }re-schedules only whileMountItemDispatcherstill has pending items (newhasPendingItems(); all three queues checked). Posting new items already callsschedule()directly, and the pre-mount "wait until next frame" continuation case is covered because the items are still queued.Test plan
pthread.hnot found #2 keeps the app fully functional (timers fire, events dispatch) but the loop persists via docs(README): fix quickstart instruction #3/Set UIStatusBarStyleLightContent #4; gating all four → 0 doFrames at idle, ~0% CPU, app alive, rendering and interaction normal — each pump was keeping only itself armed (one dropped re-post per pump collapses it permanently).requestIdleCallbackusage, andsetIntervalbehavior before/after.Fresh template +
atrace/dumpsys gfxinforeproduction commands are in the linked issue.Impact
Every RN Android app currently burns main-thread CPU at vsync rate whenever its screen is visible but idle (measured up to ~22.5% of a core on a 2016 SoC, ~3.5% on a 2025 flagship). After this change idle RN apps are Choreographer-silent like native apps. No behavior change while work is pending — each callback keeps its exact per-frame dispatch whenever it has work.
Changelog:
[Android] [Fixed] - Stop re-arming idle Choreographer frame callbacks at vsync rate — idle-foreground apps no longer burn main-thread CPU at ~60 doFrames/s with zero pending work (timers, event dispatch, native animations, mount dispatch each re-arm only when they have work).