Skip to content

Commit f9996a8

Browse files
zeyapmeta-codesync[bot]
authored andcommitted
Add feature flag to keep Android timers running while the host is paused (#59371)
Summary: Pull Request resolved: #59371 On Android, `JavaTimerManager` stops JS timers on `onHostPause` and only restarts them on `onHostResume` or while a headless JS task is active. When the app keeps using the JS runtime without a resumed React screen, pending `setTimeout`s and Promise-driven work that depends on them stall until a React screen resumes. On iOS, `RCTTiming` does not depend on host lifecycle: timers keep firing until the instance is invalidated. Add `keepTimersRunningWhileHostPausedAndroid` (default `false`). When enabled, `JavaTimerManager`: - starts the timer frame callback when it is created, since the host may never resume; - stops timers only once the instance is destroyed (tracked by a new `isDestroyed`), instead of when the host is paused. Independently of the flag, instance destroy now stops timers even while a headless JS task is still marked active. The instance's JS thread and runtime are gone by then, so such a task can no longer run JS; previously the timer frame callback kept re-posting itself every frame and retained the destroyed `JavaTimerManager`. Changelog: [Internal] Reviewed By: christophpurrer Differential Revision: D126948111 fbshipit-source-id: ae4422c07ebdcf01498a6284686e44344207fa28
1 parent b217ecd commit f9996a8

22 files changed

Lines changed: 334 additions & 53 deletions

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<f8fad51e8b31224b04d7933da8affb3a>>
7+
* @generated SignedSource<<f308cce94b6675fb99b66792ba49347a>>
88
*/
99

1010
/**
@@ -444,6 +444,12 @@ public object ReactNativeFeatureFlags {
444444
@JvmStatic
445445
public fun fuseboxWebSocketEventsEnabled(): Boolean = accessor.fuseboxWebSocketEventsEnabled()
446446

447+
/**
448+
* Keep JS timers running on Android while the React host is paused, stopping them only when the React instance is destroyed. Matches iOS, where timers do not depend on host lifecycle.
449+
*/
450+
@JvmStatic
451+
public fun keepTimersRunningWhileHostPausedAndroid(): Boolean = accessor.keepTimersRunningWhileHostPausedAndroid()
452+
447453
/**
448454
* When enabled, uses optimized platform-specific paths to apply animated props synchronously. On Android, this uses a batched int/double buffer protocol with a single JNI call. Other platforms apply the props of each view through the existing synchronous update.
449455
*/

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<846a8685f662d66dbcdaa4324c772d51>>
7+
* @generated SignedSource<<1cefe829b93cd661df195cbf3a3663a7>>
88
*/
99

1010
/**
@@ -89,6 +89,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
8989
private var fuseboxFrameRecordingEnabledCache: Boolean? = null
9090
private var fuseboxScreenshotCaptureEnabledCache: Boolean? = null
9191
private var fuseboxWebSocketEventsEnabledCache: Boolean? = null
92+
private var keepTimersRunningWhileHostPausedAndroidCache: Boolean? = null
9293
private var optimizedAnimatedPropUpdatesCache: Boolean? = null
9394
private var overrideBySynchronousMountPropsAtMountingAndroidCache: Boolean? = null
9495
private var perfIssuesEnabledCache: Boolean? = null
@@ -739,6 +740,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
739740
return cached
740741
}
741742

743+
override fun keepTimersRunningWhileHostPausedAndroid(): Boolean {
744+
var cached = keepTimersRunningWhileHostPausedAndroidCache
745+
if (cached == null) {
746+
cached = ReactNativeFeatureFlagsCxxInterop.keepTimersRunningWhileHostPausedAndroid()
747+
keepTimersRunningWhileHostPausedAndroidCache = cached
748+
}
749+
return cached
750+
}
751+
742752
override fun optimizedAnimatedPropUpdates(): Boolean {
743753
var cached = optimizedAnimatedPropUpdatesCache
744754
if (cached == null) {

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<9ccf3958e1db2db295ba73dc371b5185>>
7+
* @generated SignedSource<<e984b654c4b8fc17f99046e60306f3f6>>
88
*/
99

1010
/**
@@ -166,6 +166,8 @@ public object ReactNativeFeatureFlagsCxxInterop {
166166

167167
@DoNotStrip @JvmStatic public external fun fuseboxWebSocketEventsEnabled(): Boolean
168168

169+
@DoNotStrip @JvmStatic public external fun keepTimersRunningWhileHostPausedAndroid(): Boolean
170+
169171
@DoNotStrip @JvmStatic public external fun optimizedAnimatedPropUpdates(): Boolean
170172

171173
@DoNotStrip @JvmStatic public external fun overrideBySynchronousMountPropsAtMountingAndroid(): Boolean

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<0a04c5e0a0b418f2efd4b253646ae7c1>>
7+
* @generated SignedSource<<c0f98773f7b83272d69132d1a3e2f45a>>
88
*/
99

1010
/**
@@ -161,6 +161,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
161161

162162
override fun fuseboxWebSocketEventsEnabled(): Boolean = false
163163

164+
override fun keepTimersRunningWhileHostPausedAndroid(): Boolean = false
165+
164166
override fun optimizedAnimatedPropUpdates(): Boolean = false
165167

166168
override fun overrideBySynchronousMountPropsAtMountingAndroid(): Boolean = true

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<861063ef7d217992d3cecd410e25d885>>
7+
* @generated SignedSource<<f18d16c52745abc62a0b9ccad5358a0b>>
88
*/
99

1010
/**
@@ -93,6 +93,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
9393
private var fuseboxFrameRecordingEnabledCache: Boolean? = null
9494
private var fuseboxScreenshotCaptureEnabledCache: Boolean? = null
9595
private var fuseboxWebSocketEventsEnabledCache: Boolean? = null
96+
private var keepTimersRunningWhileHostPausedAndroidCache: Boolean? = null
9697
private var optimizedAnimatedPropUpdatesCache: Boolean? = null
9798
private var overrideBySynchronousMountPropsAtMountingAndroidCache: Boolean? = null
9899
private var perfIssuesEnabledCache: Boolean? = null
@@ -812,6 +813,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
812813
return cached
813814
}
814815

816+
override fun keepTimersRunningWhileHostPausedAndroid(): Boolean {
817+
var cached = keepTimersRunningWhileHostPausedAndroidCache
818+
if (cached == null) {
819+
cached = currentProvider.keepTimersRunningWhileHostPausedAndroid()
820+
accessedFeatureFlags.add("keepTimersRunningWhileHostPausedAndroid")
821+
keepTimersRunningWhileHostPausedAndroidCache = cached
822+
}
823+
return cached
824+
}
825+
815826
override fun optimizedAnimatedPropUpdates(): Boolean {
816827
var cached = optimizedAnimatedPropUpdatesCache
817828
if (cached == null) {

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<5fa21c808df68be4c007469040fbca2a>>
7+
* @generated SignedSource<<0a088b4c798dd44274c63a4aa6cba312>>
88
*/
99

1010
/**
@@ -161,6 +161,8 @@ public interface ReactNativeFeatureFlagsProvider {
161161

162162
@DoNotStrip public fun fuseboxWebSocketEventsEnabled(): Boolean
163163

164+
@DoNotStrip public fun keepTimersRunningWhileHostPausedAndroid(): Boolean
165+
164166
@DoNotStrip public fun optimizedAnimatedPropUpdates(): Boolean
165167

166168
@DoNotStrip public fun overrideBySynchronousMountPropsAtMountingAndroid(): Boolean

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.kt‎

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.facebook.react.common.SystemClock.nanoTime
2020
import com.facebook.react.common.SystemClock.uptimeMillis
2121
import com.facebook.react.common.annotations.internal.LegacyArchitecture
2222
import com.facebook.react.devsupport.interfaces.DevSupportManager
23+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
2324
import com.facebook.react.jstasks.HeadlessJsTaskContext
2425
import com.facebook.react.jstasks.HeadlessJsTaskEventListener
2526
import java.util.PriorityQueue
@@ -50,6 +51,7 @@ public open class JavaTimerManager(
5051
private val idleCallbackGuard = Any()
5152
private val timerIdsToTimers: SparseArray<Timer> = SparseArray()
5253
private val isPaused = AtomicBoolean(true)
54+
private val isDestroyed = AtomicBoolean(false)
5355
private val isRunningTasks = AtomicBoolean(false)
5456
private val timerFrameCallback = TimerFrameCallback()
5557
private val idleFrameCallback = IdleFrameCallback()
@@ -65,6 +67,15 @@ public open class JavaTimerManager(
6567
init {
6668
reactApplicationContext.addLifecycleEventListener(this)
6769
HeadlessJsTaskContext.getInstance(reactApplicationContext).addTaskEventListener(this)
70+
if (ReactNativeFeatureFlags.keepTimersRunningWhileHostPausedAndroid()) {
71+
// The host may never resume (e.g. no React screen is shown), so start timers here.
72+
UiThreadUtil.runOnUiThread {
73+
if (!isDestroyed.get()) {
74+
setChoreographerCallback()
75+
maybeSetChoreographerIdleCallback()
76+
}
77+
}
78+
}
6879
}
6980

7081
override fun onHostPause() {
@@ -105,10 +116,21 @@ public open class JavaTimerManager(
105116
public open fun onInstanceDestroy() {
106117
HeadlessJsTaskContext.getInstance(reactApplicationContext).removeTaskEventListener(this)
107118
reactApplicationContext.removeLifecycleEventListener(this)
119+
isDestroyed.set(true)
108120
clearFrameCallback()
109121
clearChoreographerIdleCallback()
110122
}
111123

124+
// Timers always stop once the instance is destroyed: its JS thread and runtime are gone, so
125+
// headless tasks left active on its context can no longer run JS. With the flag, timers
126+
// otherwise follow the instance lifetime like iOS, so JS work keeps progressing while no React
127+
// screen is resumed.
128+
private fun isStopped(): Boolean =
129+
isDestroyed.get() ||
130+
(!ReactNativeFeatureFlags.keepTimersRunningWhileHostPausedAndroid() &&
131+
isPaused.get() &&
132+
!isRunningTasks.get())
133+
112134
@LegacyArchitecture
113135
private fun maybeSetChoreographerIdleCallback() {
114136
synchronized(idleCallbackGuard) {
@@ -120,7 +142,7 @@ public open class JavaTimerManager(
120142

121143
@LegacyArchitecture
122144
private fun maybeIdleCallback() {
123-
if (isPaused.get() && !isRunningTasks.get()) {
145+
if (isStopped()) {
124146
clearFrameCallback()
125147
}
126148
}
@@ -136,8 +158,7 @@ public open class JavaTimerManager(
136158
}
137159

138160
private fun clearFrameCallback() {
139-
val headlessJsTaskContext = HeadlessJsTaskContext.getInstance(reactApplicationContext)
140-
if (frameCallbackPosted && isPaused.get() && !headlessJsTaskContext.hasActiveTasks()) {
161+
if (frameCallbackPosted && isStopped()) {
141162
reactChoreographer.removeFrameCallback(
142163
ReactChoreographer.CallbackType.TIMERS_EVENTS,
143164
timerFrameCallback,
@@ -286,7 +307,7 @@ public open class JavaTimerManager(
286307

287308
/** Calls all timers that have expired since the last time this frame callback was called. */
288309
override fun doFrame(frameTimeNanos: Long) {
289-
if (isPaused.get() && !isRunningTasks.get()) {
310+
if (isStopped()) {
290311
return
291312
}
292313
val frameTimeMillis = frameTimeNanos / 1000000
@@ -318,7 +339,7 @@ public open class JavaTimerManager(
318339

319340
private inner class IdleFrameCallback : Choreographer.FrameCallback {
320341
override fun doFrame(frameTimeNanos: Long) {
321-
if (isPaused.get() && !isRunningTasks.get()) {
342+
if (isStopped()) {
322343
return
323344
}
324345

‎packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<7e430d5b575ea983d53ecde87c4dc9f0>>
7+
* @generated SignedSource<<19e1daeb6cb72249eb279268fc7122e5>>
88
*/
99

1010
/**
@@ -453,6 +453,12 @@ class ReactNativeFeatureFlagsJavaProvider
453453
return method(javaProvider_);
454454
}
455455

456+
bool keepTimersRunningWhileHostPausedAndroid() override {
457+
static const auto method =
458+
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("keepTimersRunningWhileHostPausedAndroid");
459+
return method(javaProvider_);
460+
}
461+
456462
bool optimizedAnimatedPropUpdates() override {
457463
static const auto method =
458464
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("optimizedAnimatedPropUpdates");
@@ -970,6 +976,11 @@ bool JReactNativeFeatureFlagsCxxInterop::fuseboxWebSocketEventsEnabled(
970976
return ReactNativeFeatureFlags::fuseboxWebSocketEventsEnabled();
971977
}
972978

979+
bool JReactNativeFeatureFlagsCxxInterop::keepTimersRunningWhileHostPausedAndroid(
980+
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
981+
return ReactNativeFeatureFlags::keepTimersRunningWhileHostPausedAndroid();
982+
}
983+
973984
bool JReactNativeFeatureFlagsCxxInterop::optimizedAnimatedPropUpdates(
974985
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
975986
return ReactNativeFeatureFlags::optimizedAnimatedPropUpdates();
@@ -1348,6 +1359,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() {
13481359
makeNativeMethod(
13491360
"fuseboxWebSocketEventsEnabled",
13501361
JReactNativeFeatureFlagsCxxInterop::fuseboxWebSocketEventsEnabled),
1362+
makeNativeMethod(
1363+
"keepTimersRunningWhileHostPausedAndroid",
1364+
JReactNativeFeatureFlagsCxxInterop::keepTimersRunningWhileHostPausedAndroid),
13511365
makeNativeMethod(
13521366
"optimizedAnimatedPropUpdates",
13531367
JReactNativeFeatureFlagsCxxInterop::optimizedAnimatedPropUpdates),

‎packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<6b96c286c25d3c9fcb66d39d67092787>>
7+
* @generated SignedSource<<d3d1a8ea9d6f94f1e069a3f43a79bd68>>
88
*/
99

1010
/**
@@ -237,6 +237,9 @@ class JReactNativeFeatureFlagsCxxInterop
237237
static bool fuseboxWebSocketEventsEnabled(
238238
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
239239

240+
static bool keepTimersRunningWhileHostPausedAndroid(
241+
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
242+
240243
static bool optimizedAnimatedPropUpdates(
241244
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
242245

0 commit comments

Comments
 (0)