From 3df32ae015b06c2b27bb4a13cd199da3f81a64d4 Mon Sep 17 00:00:00 2001 From: Ruslan Shestopalyuk Date: Thu, 1 Aug 2024 12:02:42 -0700 Subject: [PATCH] Migrate JavaTimerManager to Kotlin (#45820) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/45820 # Changelog: [Internal] - As in the title. Reviewed By: mdvacca Differential Revision: D60449157 --- .../react/modules/core/JavaTimerManager.java | 428 ------------------ .../react/modules/core/JavaTimerManager.kt | 363 +++++++++++++++ .../react/modules/core/TimingModule.kt | 4 +- 3 files changed, 365 insertions(+), 430 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.kt 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..ecce67478dd9d0 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.kt @@ -0,0 +1,363 @@ +/* + * 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 +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.sign + +/** + * 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 class Timer( + val timerId: Int, + var 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(TIMER_QUEUE_CAPACITY) { lhs, rhs -> (lhs.targetTime - rhs.targetTime).sign } + + 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 timerId 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(timerId: Int, delay: Long, repeat: Boolean) { + val initialTargetTime = nanoTime() / 1000000 + delay + val timer = Timer(timerId, initialTargetTime, delay.toInt(), repeat) + synchronized(timerGuard) { + timers.add(timer) + timerIdsToTimers.put(timerId, 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 timerId 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( + timerId: 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 = 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 = max(0, remoteTime - deviceTime + duration) + if (duration == 0 && !repeat) { + val timerToCall = Arguments.createArray() + timerToCall.pushInt(timerId) + javaScriptTimerExecutor.callTimers(timerToCall) + return + } + createTimer(timerId, 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) { + var timer = timers.poll() + if (timer == null) { + break + } + if (timersToCall == null) { + timersToCall = Arguments.createArray() + } + timersToCall?.pushInt(timer.timerId) + if (timer.repeat) { + timer.targetTime = frameTimeMillis + timer.interval + timers.add(timer) + } else { + timerIdsToTimers.remove(timer.timerId) + } + } + } + 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 const val TIMER_QUEUE_CAPACITY = 11 + + 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)