diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
deleted file mode 100644
index c155e7614dd495..00000000000000
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
+++ /dev/null
@@ -1,428 +0,0 @@
-/*
- * Copyright (c) Meta Platforms, Inc. and affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- */
-
-package com.facebook.react.modules.core;
-
-import android.util.SparseArray;
-import android.view.Choreographer;
-import androidx.annotation.Nullable;
-import com.facebook.proguard.annotations.DoNotStrip;
-import com.facebook.react.bridge.Arguments;
-import com.facebook.react.bridge.LifecycleEventListener;
-import com.facebook.react.bridge.ReactApplicationContext;
-import com.facebook.react.bridge.UiThreadUtil;
-import com.facebook.react.bridge.WritableArray;
-import com.facebook.react.common.SystemClock;
-import com.facebook.react.devsupport.interfaces.DevSupportManager;
-import com.facebook.react.jstasks.HeadlessJsTaskContext;
-import com.facebook.react.jstasks.HeadlessJsTaskEventListener;
-import java.util.Comparator;
-import java.util.PriorityQueue;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * This class is the native implementation for JS timer execution on Android. It schedules JS timers
- * to be invoked on frame boundaries using {@link ReactChoreographer}.
- *
- *
This is used by the NativeModule {@link TimingModule}.
- */
-public class JavaTimerManager implements LifecycleEventListener, HeadlessJsTaskEventListener {
-
- // These timing constants should be kept in sync with the ones in `JSTimers.js`.
- // The minimum time in milliseconds left in the frame to call idle callbacks.
- private static final float IDLE_CALLBACK_FRAME_DEADLINE_MS = 1.f;
- // The total duration of a frame in milliseconds, this assumes that devices run at 60 fps.
- // TODO: Lower frame duration on devices that are too slow to run consistently
- // at 60 fps.
- private static final float FRAME_DURATION_MS = 1000.f / 60.f;
-
- private static class Timer {
- private final int mCallbackID;
- private final boolean mRepeat;
- private final int mInterval;
- private long mTargetTime;
-
- private Timer(int callbackID, long initialTargetTime, int duration, boolean repeat) {
- mCallbackID = callbackID;
- mTargetTime = initialTargetTime;
- mInterval = duration;
- mRepeat = repeat;
- }
- }
-
- private class TimerFrameCallback implements Choreographer.FrameCallback {
-
- // Temporary map for constructing the individual arrays of timers to call
- private @Nullable WritableArray mTimersToCall = null;
-
- /** Calls all timers that have expired since the last time this frame callback was called. */
- @Override
- public void doFrame(long frameTimeNanos) {
- if (isPaused.get() && !isRunningTasks.get()) {
- return;
- }
-
- long frameTimeMillis = frameTimeNanos / 1000000;
- synchronized (mTimerGuard) {
- while (!mTimers.isEmpty() && mTimers.peek().mTargetTime < frameTimeMillis) {
- Timer timer = mTimers.poll();
- if (mTimersToCall == null) {
- mTimersToCall = Arguments.createArray();
- }
- mTimersToCall.pushInt(timer.mCallbackID);
- if (timer.mRepeat) {
- timer.mTargetTime = frameTimeMillis + timer.mInterval;
- mTimers.add(timer);
- } else {
- mTimerIdsToTimers.remove(timer.mCallbackID);
- }
- }
- }
-
- if (mTimersToCall != null) {
- mJavaScriptTimerExecutor.callTimers(mTimersToCall);
- mTimersToCall = null;
- }
-
- mReactChoreographer.postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, this);
- }
- }
-
- private class IdleFrameCallback implements Choreographer.FrameCallback {
-
- @Override
- public void doFrame(long frameTimeNanos) {
- if (isPaused.get() && !isRunningTasks.get()) {
- return;
- }
-
- // If the JS thread is busy for multiple frames we cancel any other pending runnable.
- // We also capture the idleCallbackRunnable to tentatively fix:
- // https://github.com/facebook/react-native/issues/44842
- IdleCallbackRunnable idleCallbackRunnable = mCurrentIdleCallbackRunnable;
- if (idleCallbackRunnable != null) {
- idleCallbackRunnable.cancel();
- }
-
- mCurrentIdleCallbackRunnable = new IdleCallbackRunnable(frameTimeNanos);
- mReactApplicationContext.runOnJSQueueThread(mCurrentIdleCallbackRunnable);
-
- mReactChoreographer.postFrameCallback(ReactChoreographer.CallbackType.IDLE_EVENT, this);
- }
- }
-
- private class IdleCallbackRunnable implements Runnable {
- private volatile boolean mCancelled = false;
- private final long mFrameStartTime;
-
- public IdleCallbackRunnable(long frameStartTime) {
- mFrameStartTime = frameStartTime;
- }
-
- @Override
- public void run() {
- if (mCancelled) {
- return;
- }
-
- long frameTimeMillis = mFrameStartTime / 1000000;
- long timeSinceBoot = SystemClock.uptimeMillis();
- long frameTimeElapsed = timeSinceBoot - frameTimeMillis;
- long time = SystemClock.currentTimeMillis();
- long absoluteFrameStartTime = time - frameTimeElapsed;
-
- if (FRAME_DURATION_MS - (float) frameTimeElapsed < IDLE_CALLBACK_FRAME_DEADLINE_MS) {
- return;
- }
-
- boolean sendIdleEvents;
- synchronized (mIdleCallbackGuard) {
- sendIdleEvents = mSendIdleEvents;
- }
-
- if (sendIdleEvents) {
- mJavaScriptTimerExecutor.callIdleCallbacks(absoluteFrameStartTime);
- }
-
- mCurrentIdleCallbackRunnable = null;
- }
-
- public void cancel() {
- mCancelled = true;
- }
- }
-
- private final ReactApplicationContext mReactApplicationContext;
- private final JavaScriptTimerExecutor mJavaScriptTimerExecutor;
- private final ReactChoreographer mReactChoreographer;
- private final DevSupportManager mDevSupportManager;
- private final Object mTimerGuard = new Object();
- private final Object mIdleCallbackGuard = new Object();
- private final PriorityQueue mTimers;
- private final SparseArray mTimerIdsToTimers;
- private final AtomicBoolean isPaused = new AtomicBoolean(true);
- private final AtomicBoolean isRunningTasks = new AtomicBoolean(false);
- private final TimerFrameCallback mTimerFrameCallback = new TimerFrameCallback();
- private final IdleFrameCallback mIdleFrameCallback = new IdleFrameCallback();
- private @Nullable IdleCallbackRunnable mCurrentIdleCallbackRunnable;
- private boolean mFrameCallbackPosted = false;
- private boolean mFrameIdleCallbackPosted = false;
- private boolean mSendIdleEvents = false;
-
- public JavaTimerManager(
- ReactApplicationContext reactContext,
- JavaScriptTimerExecutor javaScriptTimerManager,
- ReactChoreographer reactChoreographer,
- DevSupportManager devSupportManager) {
- mReactApplicationContext = reactContext;
- mJavaScriptTimerExecutor = javaScriptTimerManager;
- mReactChoreographer = reactChoreographer;
- mDevSupportManager = devSupportManager;
-
- // We store timers sorted by finish time.
- mTimers =
- new PriorityQueue(
- 11, // Default capacity: for some reason they don't expose a (Comparator) constructor
- new Comparator() {
- @Override
- public int compare(Timer lhs, Timer rhs) {
- long diff = lhs.mTargetTime - rhs.mTargetTime;
- if (diff == 0) {
- return 0;
- } else if (diff < 0) {
- return -1;
- } else {
- return 1;
- }
- }
- });
- mTimerIdsToTimers = new SparseArray<>();
-
- mReactApplicationContext.addLifecycleEventListener(this);
- }
-
- @Override
- public void onHostPause() {
- isPaused.set(true);
- clearFrameCallback();
- maybeIdleCallback();
- }
-
- @Override
- public void onHostDestroy() {
- clearFrameCallback();
- maybeIdleCallback();
- }
-
- @Override
- public void onHostResume() {
- isPaused.set(false);
- // TODO(5195192) Investigate possible problems related to restarting all tasks at the same
- // moment
- setChoreographerCallback();
- maybeSetChoreographerIdleCallback();
- }
-
- @Override
- public void onHeadlessJsTaskStart(int taskId) {
- if (!isRunningTasks.getAndSet(true)) {
- setChoreographerCallback();
- maybeSetChoreographerIdleCallback();
- }
- }
-
- @Override
- public void onHeadlessJsTaskFinish(int taskId) {
- HeadlessJsTaskContext headlessJsTaskContext =
- HeadlessJsTaskContext.getInstance(mReactApplicationContext);
- if (!headlessJsTaskContext.hasActiveTasks()) {
- isRunningTasks.set(false);
- clearFrameCallback();
- maybeIdleCallback();
- }
- }
-
- public void onInstanceDestroy() {
- mReactApplicationContext.removeLifecycleEventListener(this);
-
- clearFrameCallback();
- clearChoreographerIdleCallback();
- }
-
- private void maybeSetChoreographerIdleCallback() {
- synchronized (mIdleCallbackGuard) {
- if (mSendIdleEvents) {
- setChoreographerIdleCallback();
- }
- }
- }
-
- private void maybeIdleCallback() {
- if (isPaused.get() && !isRunningTasks.get()) {
- clearFrameCallback();
- }
- }
-
- private void setChoreographerCallback() {
- if (!mFrameCallbackPosted) {
- mReactChoreographer.postFrameCallback(
- ReactChoreographer.CallbackType.TIMERS_EVENTS, mTimerFrameCallback);
- mFrameCallbackPosted = true;
- }
- }
-
- private void clearFrameCallback() {
- HeadlessJsTaskContext headlessJsTaskContext =
- HeadlessJsTaskContext.getInstance(mReactApplicationContext);
- if (mFrameCallbackPosted && isPaused.get() && !headlessJsTaskContext.hasActiveTasks()) {
- mReactChoreographer.removeFrameCallback(
- ReactChoreographer.CallbackType.TIMERS_EVENTS, mTimerFrameCallback);
- mFrameCallbackPosted = false;
- }
- }
-
- private void setChoreographerIdleCallback() {
- if (!mFrameIdleCallbackPosted) {
- mReactChoreographer.postFrameCallback(
- ReactChoreographer.CallbackType.IDLE_EVENT, mIdleFrameCallback);
- mFrameIdleCallbackPosted = true;
- }
- }
-
- private void clearChoreographerIdleCallback() {
- if (mFrameIdleCallbackPosted) {
- mReactChoreographer.removeFrameCallback(
- ReactChoreographer.CallbackType.IDLE_EVENT, mIdleFrameCallback);
- mFrameIdleCallbackPosted = false;
- }
- }
-
- /**
- * A method to be used for synchronously creating a timer. The timer will not be invoked until the
- * next frame, regardless of whether it has already expired (i.e. the delay is 0).
- *
- * @param callbackID An identifier for the callback that can be passed to JS or C++ to invoke it.
- * @param delay The time in ms before the callback should be invoked.
- * @param repeat Whether the timer should be repeated (used for setInterval).
- */
- @DoNotStrip
- public void createTimer(final int callbackID, final long delay, final boolean repeat) {
- long initialTargetTime = SystemClock.nanoTime() / 1000000 + delay;
- Timer timer = new Timer(callbackID, initialTargetTime, (int) delay, repeat);
- synchronized (mTimerGuard) {
- mTimers.add(timer);
- mTimerIdsToTimers.put(callbackID, timer);
- }
- }
-
- /**
- * A method to be used for asynchronously creating a timer. If the timer has already expired,
- * (based on the provided jsSchedulingTime) then it will be immediately invoked.
- *
- * @param callbackID An identifier that can be passed back to JS to invoke the callback.
- * @param duration The time in ms before the callback should be invoked.
- * @param jsSchedulingTime The time (ms since epoch) when this timer was created in JS.
- * @param repeat Whether the timer should be repeated (used for setInterval)
- */
- public void createAndMaybeCallTimer(
- final int callbackID,
- final int duration,
- final double jsSchedulingTime,
- final boolean repeat) {
- long deviceTime = SystemClock.currentTimeMillis();
- long remoteTime = (long) jsSchedulingTime;
-
- // If the times on the server and device have drifted throw an exception to warn the developer
- // that things might not work or results may not be accurate. This is required only for
- // developer builds.
- if (mDevSupportManager.getDevSupportEnabled()) {
- long driftTime = Math.abs(remoteTime - deviceTime);
- if (driftTime > 60000) {
- mJavaScriptTimerExecutor.emitTimeDriftWarning(
- "Debugger and device times have drifted by more than 60s. Please correct this by "
- + "running adb shell \"date `date +%m%d%H%M%Y.%S`\" on your debugger machine.");
- }
- }
-
- // Adjust for the amount of time it took for native to receive the timer registration call
- long adjustedDuration = Math.max(0, remoteTime - deviceTime + duration);
- if (duration == 0 && !repeat) {
- WritableArray timerToCall = Arguments.createArray();
- timerToCall.pushInt(callbackID);
- mJavaScriptTimerExecutor.callTimers(timerToCall);
- return;
- }
-
- createTimer(callbackID, adjustedDuration, repeat);
- }
-
- @DoNotStrip
- public void deleteTimer(int timerId) {
- synchronized (mTimerGuard) {
- Timer timer = mTimerIdsToTimers.get(timerId);
- if (timer == null) {
- return;
- }
- mTimerIdsToTimers.remove(timerId);
- mTimers.remove(timer);
- }
- }
-
- @DoNotStrip
- public void setSendIdleEvents(final boolean sendIdleEvents) {
- synchronized (mIdleCallbackGuard) {
- mSendIdleEvents = sendIdleEvents;
- }
-
- UiThreadUtil.runOnUiThread(
- new Runnable() {
- @Override
- public void run() {
- synchronized (mIdleCallbackGuard) {
- if (sendIdleEvents) {
- setChoreographerIdleCallback();
- } else {
- clearChoreographerIdleCallback();
- }
- }
- }
- });
- }
-
- /**
- * Returns a bool representing whether there are any active timers that will be fired within a
- * certain period of time. Disregards repeating timers (setInterval). Used for testing to
- * determine if RN is idle.
- *
- * @param rangeMs The time range, in ms, to check
- * @return True if there are pending timers within the given range; false otherwise
- */
- /* package */ boolean hasActiveTimersInRange(long rangeMs) {
- synchronized (mTimerGuard) {
- Timer nextTimer = mTimers.peek();
- if (nextTimer == null) {
- // Timers queue is empty
- return false;
- }
- if (isTimerInRange(nextTimer, rangeMs)) {
- // First check the next timer, so we can avoid iterating over the entire queue if it's
- // already within range.
- return true;
- }
- for (Timer timer : mTimers) {
- if (isTimerInRange(timer, rangeMs)) {
- return true;
- }
- }
- }
- return false;
- }
-
- private static boolean isTimerInRange(Timer timer, long rangeMs) {
- return !timer.mRepeat && timer.mInterval < rangeMs;
- }
-}
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.kt
new file mode 100644
index 00000000000000..8ce57c8babec7c
--- /dev/null
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.kt
@@ -0,0 +1,367 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.react.modules.core
+
+import android.util.SparseArray
+import android.view.Choreographer
+import com.facebook.proguard.annotations.DoNotStrip
+import com.facebook.react.bridge.Arguments
+import com.facebook.react.bridge.LifecycleEventListener
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.UiThreadUtil
+import com.facebook.react.bridge.WritableArray
+import com.facebook.react.common.SystemClock.currentTimeMillis
+import com.facebook.react.common.SystemClock.nanoTime
+import com.facebook.react.common.SystemClock.uptimeMillis
+import com.facebook.react.devsupport.interfaces.DevSupportManager
+import com.facebook.react.jstasks.HeadlessJsTaskContext
+import com.facebook.react.jstasks.HeadlessJsTaskEventListener
+import java.util.PriorityQueue
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.concurrent.Volatile
+
+/**
+ * This class is the native implementation for JS timer execution on Android. It schedules JS timers
+ * to be invoked on frame boundaries using [ReactChoreographer].
+ *
+ * This is used by the NativeModule [TimingModule].
+ */
+public open class JavaTimerManager(
+ private val reactApplicationContext: ReactApplicationContext,
+ private val javaScriptTimerExecutor: JavaScriptTimerExecutor,
+ private val reactChoreographer: ReactChoreographer,
+ private val devSupportManager: DevSupportManager
+) : LifecycleEventListener, HeadlessJsTaskEventListener {
+ private data class Timer(
+ val callbackID: Int,
+ val targetTime: Long,
+ val interval: Int,
+ val repeat: Boolean
+ )
+
+ private val timerGuard = Any()
+ private val idleCallbackGuard = Any()
+ private val timerIdsToTimers: SparseArray = SparseArray()
+ private val isPaused = AtomicBoolean(true)
+ private val isRunningTasks = AtomicBoolean(false)
+ private val timerFrameCallback = TimerFrameCallback()
+ private val idleFrameCallback = IdleFrameCallback()
+ private var currentIdleCallbackRunnable: IdleCallbackRunnable? = null
+ private var frameCallbackPosted = false
+ private var frameIdleCallbackPosted = false
+ private var sendIdleEvents = false
+
+ // We store timers sorted by finish time.
+ private val timers: PriorityQueue =
+ PriorityQueue(
+ 11) // Default capacity: for some reason they don't expose a (Comparator) constructor
+ { lhs, rhs ->
+ val diff = lhs.targetTime - rhs.targetTime
+ if (diff == 0L) {
+ 0
+ } else if (diff < 0) {
+ -1
+ } else {
+ 1
+ }
+ }
+
+ init {
+ reactApplicationContext.addLifecycleEventListener(this)
+ }
+
+ override fun onHostPause() {
+ isPaused.set(true)
+ clearFrameCallback()
+ maybeIdleCallback()
+ }
+
+ override fun onHostDestroy() {
+ clearFrameCallback()
+ maybeIdleCallback()
+ }
+
+ override fun onHostResume() {
+ isPaused.set(false)
+ // TODO(5195192) Investigate possible problems related to restarting all tasks at the same
+ // moment
+ setChoreographerCallback()
+ maybeSetChoreographerIdleCallback()
+ }
+
+ override fun onHeadlessJsTaskStart(taskId: Int) {
+ if (!isRunningTasks.getAndSet(true)) {
+ setChoreographerCallback()
+ maybeSetChoreographerIdleCallback()
+ }
+ }
+
+ override fun onHeadlessJsTaskFinish(taskId: Int) {
+ val headlessJsTaskContext = HeadlessJsTaskContext.getInstance(reactApplicationContext)
+ if (!headlessJsTaskContext.hasActiveTasks()) {
+ isRunningTasks.set(false)
+ clearFrameCallback()
+ maybeIdleCallback()
+ }
+ }
+
+ public open fun onInstanceDestroy() {
+ reactApplicationContext.removeLifecycleEventListener(this)
+ clearFrameCallback()
+ clearChoreographerIdleCallback()
+ }
+
+ private fun maybeSetChoreographerIdleCallback() {
+ synchronized(idleCallbackGuard) {
+ if (sendIdleEvents) {
+ setChoreographerIdleCallback()
+ }
+ }
+ }
+
+ private fun maybeIdleCallback() {
+ if (isPaused.get() && !isRunningTasks.get()) {
+ clearFrameCallback()
+ }
+ }
+
+ private fun setChoreographerCallback() {
+ if (!frameCallbackPosted) {
+ reactChoreographer.postFrameCallback(
+ ReactChoreographer.CallbackType.TIMERS_EVENTS, timerFrameCallback)
+ frameCallbackPosted = true
+ }
+ }
+
+ private fun clearFrameCallback() {
+ val headlessJsTaskContext = HeadlessJsTaskContext.getInstance(reactApplicationContext)
+ if (frameCallbackPosted && isPaused.get() && !headlessJsTaskContext.hasActiveTasks()) {
+ reactChoreographer.removeFrameCallback(
+ ReactChoreographer.CallbackType.TIMERS_EVENTS, timerFrameCallback)
+ frameCallbackPosted = false
+ }
+ }
+
+ private fun setChoreographerIdleCallback() {
+ if (!frameIdleCallbackPosted) {
+ reactChoreographer.postFrameCallback(
+ ReactChoreographer.CallbackType.IDLE_EVENT, idleFrameCallback)
+ frameIdleCallbackPosted = true
+ }
+ }
+
+ private fun clearChoreographerIdleCallback() {
+ if (frameIdleCallbackPosted) {
+ reactChoreographer.removeFrameCallback(
+ ReactChoreographer.CallbackType.IDLE_EVENT, idleFrameCallback)
+ frameIdleCallbackPosted = false
+ }
+ }
+
+ /**
+ * A method to be used for synchronously creating a timer. The timer will not be invoked until the
+ * next frame, regardless of whether it has already expired (i.e. the delay is 0).
+ *
+ * @param callbackID An identifier for the callback that can be passed to JS or C++ to invoke it.
+ * @param delay The time in ms before the callback should be invoked.
+ * @param repeat Whether the timer should be repeated (used for setInterval).
+ */
+ @DoNotStrip
+ public open fun createTimer(callbackID: Int, delay: Long, repeat: Boolean) {
+ val initialTargetTime = nanoTime() / 1000000 + delay
+ val timer = Timer(callbackID, initialTargetTime, delay.toInt(), repeat)
+ synchronized(timerGuard) {
+ timers.add(timer)
+ timerIdsToTimers.put(callbackID, timer)
+ }
+ }
+
+ /**
+ * A method to be used for asynchronously creating a timer. If the timer has already expired,
+ * (based on the provided jsSchedulingTime) then it will be immediately invoked.
+ *
+ * @param callbackID An identifier that can be passed back to JS to invoke the callback.
+ * @param duration The time in ms before the callback should be invoked.
+ * @param jsSchedulingTime The time (ms since epoch) when this timer was created in JS.
+ * @param repeat Whether the timer should be repeated (used for setInterval)
+ */
+ public open fun createAndMaybeCallTimer(
+ callbackID: Int,
+ duration: Int,
+ jsSchedulingTime: Double,
+ repeat: Boolean
+ ) {
+ val deviceTime = currentTimeMillis()
+ val remoteTime = jsSchedulingTime.toLong()
+
+ // If the times on the server and device have drifted throw an exception to warn the developer
+ // that things might not work or results may not be accurate. This is required only for
+ // developer builds.
+ if (devSupportManager.devSupportEnabled) {
+ val driftTime = Math.abs(remoteTime - deviceTime)
+ if (driftTime > 60000) {
+ javaScriptTimerExecutor.emitTimeDriftWarning(
+ "Debugger and device times have drifted by more than 60s. Please correct this by " +
+ "running adb shell \"date `date +%m%d%H%M%Y.%S`\" on your debugger machine.")
+ }
+ }
+
+ // Adjust for the amount of time it took for native to receive the timer registration call
+ val adjustedDuration = Math.max(0, remoteTime - deviceTime + duration)
+ if (duration == 0 && !repeat) {
+ val timerToCall = Arguments.createArray()
+ timerToCall.pushInt(callbackID)
+ javaScriptTimerExecutor.callTimers(timerToCall)
+ return
+ }
+ createTimer(callbackID, adjustedDuration, repeat)
+ }
+
+ @DoNotStrip
+ public open fun deleteTimer(timerId: Int) {
+ synchronized(timerGuard) {
+ val timer = timerIdsToTimers[timerId] ?: return
+ timerIdsToTimers.remove(timerId)
+ timers.remove(timer)
+ }
+ }
+
+ @DoNotStrip
+ public open fun setSendIdleEvents(sendIdleEvents: Boolean) {
+ synchronized(idleCallbackGuard) { this.sendIdleEvents = sendIdleEvents }
+ UiThreadUtil.runOnUiThread {
+ synchronized(idleCallbackGuard) {
+ if (sendIdleEvents) {
+ setChoreographerIdleCallback()
+ } else {
+ clearChoreographerIdleCallback()
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns a bool representing whether there are any active timers that will be fired within a
+ * certain period of time. Disregards repeating timers (setInterval). Used for testing to
+ * determine if RN is idle.
+ *
+ * @param rangeMs The time range, in ms, to check
+ * @return True if there are pending timers within the given range; false otherwise
+ */
+ internal fun hasActiveTimersInRange(rangeMs: Long): Boolean {
+ synchronized(timerGuard) {
+ val nextTimer =
+ timers.peek()
+ ?: // Timers queue is empty
+ return false
+ if (isTimerInRange(nextTimer, rangeMs)) {
+ // First check the next timer, so we can avoid iterating over the entire queue if it's
+ // already within range.
+ return true
+ }
+ for (timer in timers) {
+ if (isTimerInRange(timer, rangeMs)) {
+ return true
+ }
+ }
+ }
+ return false
+ }
+
+ private inner class TimerFrameCallback : Choreographer.FrameCallback {
+ // Temporary map for constructing the individual arrays of timers to call
+ private var timersToCall: WritableArray? = null
+
+ /** Calls all timers that have expired since the last time this frame callback was called. */
+ override fun doFrame(frameTimeNanos: Long) {
+ if (isPaused.get() && !isRunningTasks.get()) {
+ return
+ }
+ val frameTimeMillis = frameTimeNanos / 1000000
+ synchronized(timerGuard) {
+ while (!timers.isEmpty() && timers.peek()!!.targetTime < frameTimeMillis) {
+ val timer = timers.poll()
+ if (timer == null) {
+ break
+ }
+ if (timersToCall == null) {
+ timersToCall = Arguments.createArray()
+ }
+ timersToCall?.pushInt(timer.callbackID)
+ if (timer.repeat) {
+ timers.add(timer.copy(targetTime = frameTimeMillis + timer.interval))
+ } else {
+ timerIdsToTimers.remove(timer.callbackID)
+ }
+ }
+ }
+ timersToCall?.let {
+ javaScriptTimerExecutor.callTimers(it)
+ timersToCall = null
+ }
+ reactChoreographer.postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, this)
+ }
+ }
+
+ private inner class IdleFrameCallback : Choreographer.FrameCallback {
+ override fun doFrame(frameTimeNanos: Long) {
+ if (isPaused.get() && !isRunningTasks.get()) {
+ return
+ }
+
+ // If the JS thread is busy for multiple frames we cancel any other pending runnable.
+ // We also capture the idleCallbackRunnable to tentatively fix:
+ // https://github.com/facebook/react-native/issues/44842
+ currentIdleCallbackRunnable?.cancel()
+ currentIdleCallbackRunnable = IdleCallbackRunnable(frameTimeNanos)
+ reactApplicationContext.runOnJSQueueThread(currentIdleCallbackRunnable)
+ reactChoreographer.postFrameCallback(ReactChoreographer.CallbackType.IDLE_EVENT, this)
+ }
+ }
+
+ private inner class IdleCallbackRunnable(private val frameStartTime: Long) : Runnable {
+ @Volatile private var isCancelled = false
+
+ override fun run() {
+ if (isCancelled) {
+ return
+ }
+ val frameTimeMillis = frameStartTime / 1000000
+ val timeSinceBoot = uptimeMillis()
+ val frameTimeElapsed = timeSinceBoot - frameTimeMillis
+ val time = currentTimeMillis()
+ val absoluteFrameStartTime = time - frameTimeElapsed
+ if (FRAME_DURATION_MS - frameTimeElapsed.toFloat() < IDLE_CALLBACK_FRAME_DEADLINE_MS) {
+ return
+ }
+ var sendIdleEvents: Boolean
+ synchronized(idleCallbackGuard) { sendIdleEvents = this@JavaTimerManager.sendIdleEvents }
+ if (sendIdleEvents) {
+ javaScriptTimerExecutor.callIdleCallbacks(absoluteFrameStartTime.toDouble())
+ }
+ currentIdleCallbackRunnable = null
+ }
+
+ fun cancel() {
+ isCancelled = true
+ }
+ }
+
+ private companion object {
+ // These timing constants should be kept in sync with the ones in `JSTimers.js`.
+ // The minimum time in milliseconds left in the frame to call idle callbacks.
+ private const val IDLE_CALLBACK_FRAME_DEADLINE_MS = 1f
+ // The total duration of a frame in milliseconds, this assumes that devices run at 60 fps.
+ // TODO: Lower frame duration on devices that are too slow to run consistently
+ // at 60 fps.
+ private const val FRAME_DURATION_MS = 1000f / 60f
+
+ private fun isTimerInRange(timer: Timer, rangeMs: Long): Boolean =
+ !timer.repeat && timer.interval < rangeMs
+ }
+}
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/TimingModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/TimingModule.kt
index 5f797fdf7fcabe..bfa0219acf2454 100644
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/TimingModule.kt
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/TimingModule.kt
@@ -18,8 +18,8 @@ import com.facebook.react.module.annotations.ReactModule
/** Native module for JS timer execution. Timers fire on frame boundaries. */
@ReactModule(name = NativeTimingSpec.NAME)
public class TimingModule(
- reactContext: ReactApplicationContext?,
- devSupportManager: DevSupportManager?
+ reactContext: ReactApplicationContext,
+ devSupportManager: DevSupportManager
) : com.facebook.fbreact.specs.NativeTimingSpec(reactContext), JavaScriptTimerExecutor {
private val javaTimerManager: JavaTimerManager =
JavaTimerManager(reactContext, this, ReactChoreographer.getInstance(), devSupportManager)