Android: stop re-arming the idle TIMERS_EVENTS Choreographer frame callback (behind feature flag) - #58375
Android: stop re-arming the idle TIMERS_EVENTS Choreographer frame callback (behind feature flag)#58375capt-muji wants to merge 2 commits into
Conversation
… Android TimerFrameCallback.doFrame re-posts itself unconditionally every frame, so the TIMERS_EVENTS callback runs at vsync rate even while the timer queue is empty. Make the re-post demand-gated behind disableIdleTimersFrameCallbackRearmAndroid (default off): doFrame disarms when the queue drains and createTimer re-arms lazily (headless-task aware, mirroring clearFrameCallback's guard). Lifecycle arm/disarm points are untouched.
| reactChoreographer.postFrameCallback( | ||
| ReactChoreographer.CallbackType.TIMERS_EVENTS, | ||
| timerFrameCallback, | ||
| ) |
There was a problem hiding this comment.
This is now called off the UI thread.
createTimer can run on the native module thread; ReactChoreographer must be driven from the UI looper (its null-choreographer fallback hop does not cover the initialized case). Route the lazy re-arm through setChoreographerCallback on the UI queue, which is idempotent and already used by onHostResume.
|
Good catch — fixed in 198f5ca: |
On-device validation (OnePlus 3T, Android 9 / API 28, bridgeless Release)Validated by backporting this PR + the three sibling idle-pump PRs onto Result: at idle the TIMERS_EVENTS callback stays armed (~40/s) because the app runs a perpetual per-second countdown timer, so the queue is never empty. This is the intended behavior — the bytecode confirms the flag path ( Background soak: No regressions across the interaction smoke battery (overlay open/close, settings + alert sheets, pager swipes, launch chrome-defer which is rAF+setTimeout). |
Stop re-arming the idle
TIMERS_EVENTSChoreographer frame callback (Android)Part of #58367. Split from #58368 per review feedback (one PR per pump, each behind a feature flag).
Summary
JavaTimerManager.TimerFrameCallback.doFramere-posts itself unconditionally at the end of every frame, so theTIMERS_EVENTSChoreographer callback runs at vsync rate (~60 doFrame/s) forever — including while the app is idle with an empty timer queue. Each idle frame performs no work and renders nothing; on low-end devices this is a measurable constant CPU cost while an RN app sits still (see #58367 for the full matrix: measured on a OnePlus 3T as ~22% process CPU at idle for the four unconditional pumps combined; every looping frame is a no-op).This PR makes the re-post demand-gated, behind
ReactNativeFeatureFlags.disableIdleTimersFrameCallbackRearmAndroid(default false = current behavior;ossReleaseStage: 'experimental'):doFramere-posts only while the timer queue is non-empty; when it drains, the callback disarms itself.createTimerre-arms the callback lazily when a new timer arrives. The guard mirrorsclearFrameCallback's inverse: while the host is paused, only headless JS task execution may re-arm (!isPaused || isRunningTasks), so the paused-app contract is preserved.Foregrounding / backgrounding
The existing lifecycle paths are untouched and remain the authoritative arm/disarm points:
onHostResume/onHeadlessJsTaskStart→setChoreographerCallback()(re-arms if disarmed).onHostPause/onHostDestroy/onHeadlessJsTaskFinish→clearFrameCallback()(unchanged guard: paused + no headless tasks).While the host is paused with headless tasks running, a drained queue disarms the callback; the next
createTimerfrom the still-running headless JS re-arms it (isRunningTasksis true in that state).Rollout
Flag:
disableIdleTimersFrameCallbackRearmAndroid—defaultValue: false,expectedReleaseValue: true,ossReleaseStage: 'experimental'. With the flag off, behavior is identical to currentmain.Test plan
defaultValue): code path is byte-equivalent tomain(shouldRepoststaystrue;createTimerre-arm branch is skipped)../gradlew :packages:react-native:ReactAndroid:compileReleaseKotlin.adb shell atraceshowsTIMERS_EVENTSdoFrames drop to ~0 while no JS timers are scheduled; timers (countdowns,setTimeout/setIntervalapps) still fire; app backgrounded → callback stays disarmed; headless task timers fire while backgrounded; foreground return re-arms.Changelog:
[Android] [Fixed] - Stop re-arming the TIMERS_EVENTS Choreographer frame callback at vsync rate while the timer queue is empty (behind
disableIdleTimersFrameCallbackRearmAndroid)