Move lifecycle listener inside JavaTimerManager (#43338)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/43338

Reafactor JavaTimerManager so more code is shared between bridge and bridgeless.

Note that HeadlessJSTaskContext is not currently configured when using bridgeless.

Changelog: [Internal]

Reviewed By: rshest

Differential Revision: D54496604

fbshipit-source-id: 2a61294267df372e69f8316dd8f8059625d0a2bd
This commit is contained in:
Pieter De Baets
2024-03-11 09:09:52 -07:00
committed by Facebook GitHub Bot
parent 7c5a014c0d
commit 641a9c468f
5 changed files with 122 additions and 155 deletions
@@ -3032,7 +3032,7 @@ public abstract interface class com/facebook/react/modules/core/JavaScriptTimerE
public abstract fun emitTimeDriftWarning (Ljava/lang/String;)V
}
public class com/facebook/react/modules/core/JavaTimerManager {
public class com/facebook/react/modules/core/JavaTimerManager : com/facebook/react/bridge/LifecycleEventListener, com/facebook/react/jstasks/HeadlessJsTaskEventListener {
public fun <init> (Lcom/facebook/react/bridge/ReactApplicationContext;Lcom/facebook/react/modules/core/JavaScriptTimerExecutor;Lcom/facebook/react/modules/core/ReactChoreographer;Lcom/facebook/react/devsupport/interfaces/DevSupportManager;)V
public fun createAndMaybeCallTimer (IIDZ)V
public fun createTimer (IJZ)V
@@ -3078,25 +3078,16 @@ public final class com/facebook/react/modules/core/ReactChoreographer$CallbackTy
public static fun values ()[Lcom/facebook/react/modules/core/ReactChoreographer$CallbackType;
}
public final class com/facebook/react/modules/core/TimingModule : com/facebook/fbreact/specs/NativeTimingSpec, com/facebook/react/bridge/LifecycleEventListener, com/facebook/react/jstasks/HeadlessJsTaskEventListener {
public final class com/facebook/react/modules/core/TimingModule : com/facebook/fbreact/specs/NativeTimingSpec, com/facebook/react/modules/core/JavaScriptTimerExecutor {
public fun <init> (Lcom/facebook/react/bridge/ReactApplicationContext;Lcom/facebook/react/devsupport/interfaces/DevSupportManager;)V
public fun createTimer (DDDZ)V
public fun deleteTimer (D)V
public fun initialize ()V
public fun invalidate ()V
public fun onHeadlessJsTaskFinish (I)V
public fun onHeadlessJsTaskStart (I)V
public fun onHostDestroy ()V
public fun onHostPause ()V
public fun onHostResume ()V
public fun setSendIdleEvents (Z)V
}
public class com/facebook/react/modules/core/TimingModule$BridgeTimerExecutor : com/facebook/react/modules/core/JavaScriptTimerExecutor {
public fun <init> (Lcom/facebook/react/modules/core/TimingModule;)V
public fun callIdleCallbacks (D)V
public fun callTimers (Lcom/facebook/react/bridge/WritableArray;)V
public fun createTimer (DDDZ)V
public fun deleteTimer (D)V
public fun emitTimeDriftWarning (Ljava/lang/String;)V
public fun initialize ()V
public fun invalidate ()V
public fun setSendIdleEvents (Z)V
}
public class com/facebook/react/modules/debug/DevSettingsModule : com/facebook/fbreact/specs/NativeDevSettingsSpec {
@@ -12,12 +12,14 @@ 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;
@@ -28,7 +30,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
*
* <p>This is used by the NativeModule {@link TimingModule}.
*/
public class JavaTimerManager {
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.
@@ -196,19 +198,24 @@ public class JavaTimerManager {
}
});
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
@@ -217,6 +224,7 @@ public class JavaTimerManager {
maybeSetChoreographerIdleCallback();
}
@Override
public void onHeadlessJsTaskStart(int taskId) {
if (!isRunningTasks.getAndSet(true)) {
setChoreographerCallback();
@@ -224,6 +232,7 @@ public class JavaTimerManager {
}
}
@Override
public void onHeadlessJsTaskFinish(int taskId) {
HeadlessJsTaskContext headlessJsTaskContext =
HeadlessJsTaskContext.getInstance(mReactApplicationContext);
@@ -235,6 +244,8 @@ public class JavaTimerManager {
}
public void onInstanceDestroy() {
mReactApplicationContext.removeLifecycleEventListener(this);
clearFrameCallback();
clearChoreographerIdleCallback();
}
@@ -8,49 +8,16 @@
package com.facebook.react.modules.core;
import com.facebook.fbreact.specs.NativeTimingSpec;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.devsupport.interfaces.DevSupportManager;
import com.facebook.react.jstasks.HeadlessJsTaskContext;
import com.facebook.react.jstasks.HeadlessJsTaskEventListener;
import com.facebook.react.module.annotations.ReactModule;
/** Native module for JS timer execution. Timers fire on frame boundaries. */
@ReactModule(name = NativeTimingSpec.NAME)
public final class TimingModule extends NativeTimingSpec
implements LifecycleEventListener, HeadlessJsTaskEventListener {
public class BridgeTimerExecutor implements JavaScriptTimerExecutor {
@Override
public void callTimers(WritableArray timerIDs) {
ReactApplicationContext reactApplicationContext = getReactApplicationContextIfActiveOrWarn();
if (reactApplicationContext != null) {
reactApplicationContext.getJSModule(JSTimers.class).callTimers(timerIDs);
}
}
@Override
public void callIdleCallbacks(double frameTime) {
ReactApplicationContext reactApplicationContext = getReactApplicationContextIfActiveOrWarn();
if (reactApplicationContext != null) {
reactApplicationContext.getJSModule(JSTimers.class).callIdleCallbacks(frameTime);
}
}
@Override
public void emitTimeDriftWarning(String warningMessage) {
ReactApplicationContext reactApplicationContext = getReactApplicationContextIfActiveOrWarn();
if (reactApplicationContext != null) {
reactApplicationContext.getJSModule(JSTimers.class).emitTimeDriftWarning(warningMessage);
}
}
}
public final class TimingModule extends NativeTimingSpec implements JavaScriptTimerExecutor {
private final JavaTimerManager mJavaTimerManager;
public TimingModule(ReactApplicationContext reactContext, DevSupportManager devSupportManager) {
@@ -58,18 +25,14 @@ public final class TimingModule extends NativeTimingSpec
mJavaTimerManager =
new JavaTimerManager(
reactContext,
new BridgeTimerExecutor(),
ReactChoreographer.getInstance(),
devSupportManager);
reactContext, this, ReactChoreographer.getInstance(), devSupportManager);
}
@Override
public void initialize() {
getReactApplicationContext().addLifecycleEventListener(this);
HeadlessJsTaskContext headlessJsTaskContext =
HeadlessJsTaskContext.getInstance(getReactApplicationContext());
headlessJsTaskContext.addTaskEventListener(this);
headlessJsTaskContext.addTaskEventListener(mJavaTimerManager);
}
@Override
@@ -97,28 +60,30 @@ public final class TimingModule extends NativeTimingSpec
}
@Override
public void onHostResume() {
mJavaTimerManager.onHostResume();
public void callTimers(WritableArray timerIDs) {
ReactApplicationContext reactApplicationContext = getReactApplicationContextIfActiveOrWarn();
if (reactApplicationContext != null) {
reactApplicationContext.getJSModule(JSTimers.class).callTimers(timerIDs);
}
}
@Override
public void onHostPause() {
mJavaTimerManager.onHostPause();
public void callIdleCallbacks(double frameTime) {
ReactApplicationContext reactApplicationContext = getReactApplicationContextIfActiveOrWarn();
if (reactApplicationContext != null) {
reactApplicationContext.getJSModule(JSTimers.class).callIdleCallbacks(frameTime);
}
}
@Override
public void onHostDestroy() {
mJavaTimerManager.onHostDestroy();
}
public void emitTimeDriftWarning(String warningMessage) {
ReactApplicationContext reactApplicationContext = getReactApplicationContextIfActiveOrWarn();
@Override
public void onHeadlessJsTaskStart(int taskId) {
mJavaTimerManager.onHeadlessJsTaskStart(taskId);
}
@Override
public void onHeadlessJsTaskFinish(int taskId) {
mJavaTimerManager.onHeadlessJsTaskFinish(taskId);
if (reactApplicationContext != null) {
reactApplicationContext.getJSModule(JSTimers.class).emitTimeDriftWarning(warningMessage);
}
}
@Override
@@ -127,9 +92,9 @@ public final class TimingModule extends NativeTimingSpec
HeadlessJsTaskContext headlessJsTaskContext =
HeadlessJsTaskContext.getInstance(reactApplicationContext);
headlessJsTaskContext.removeTaskEventListener(this);
headlessJsTaskContext.removeTaskEventListener(mJavaTimerManager);
mJavaTimerManager.onInstanceDestroy();
reactApplicationContext.removeLifecycleEventListener(this);
}
@VisibleForTesting
@@ -21,7 +21,6 @@ import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.JSBundleLoader;
import com.facebook.react.bridge.JSBundleLoaderDelegate;
import com.facebook.react.bridge.JavaScriptContextHolder;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.NativeArray;
import com.facebook.react.bridge.NativeMap;
import com.facebook.react.bridge.NativeModule;
@@ -143,6 +142,7 @@ final class ReactInstance {
if (useDevSupport) {
devSupportManager.startInspector();
}
JSTimerExecutor jsTimerExecutor = createJSTimerExecutor();
mJavaTimerManager =
new JavaTimerManager(
@@ -151,24 +151,6 @@ final class ReactInstance {
ReactChoreographer.getInstance(),
devSupportManager);
mBridgelessReactContext.addLifecycleEventListener(
new LifecycleEventListener() {
@Override
public void onHostResume() {
mJavaTimerManager.onHostResume();
}
@Override
public void onHostPause() {
mJavaTimerManager.onHostPause();
}
@Override
public void onHostDestroy() {
mJavaTimerManager.onHostDestroy();
}
});
JSRuntimeFactory jsRuntimeFactory = mDelegate.getJsRuntimeFactory();
BindingsInstaller bindingsInstaller = mDelegate.getBindingsInstaller();
// Notify JS if profiling is enabled
@@ -448,6 +430,7 @@ final class ReactInstance {
mQueueConfiguration.destroy();
mTurboModuleManager.invalidate();
mFabricUIManager.invalidate();
mJavaTimerManager.onInstanceDestroy();
mHybridData.resetNative();
mComponentNameResolverManager = null;
mUIConstantsProviderManager = null;
@@ -7,14 +7,19 @@
package com.facebook.react.modules.timing
import android.content.Context
import android.view.Choreographer.FrameCallback
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.CatalystInstance
import com.facebook.react.bridge.JavaOnlyArray
import com.facebook.react.bridge.JavaOnlyMap
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.WritableArray
import com.facebook.react.common.SystemClock
import com.facebook.react.devsupport.interfaces.DevSupportManager
import com.facebook.react.jstasks.HeadlessJsTaskConfig
import com.facebook.react.jstasks.HeadlessJsTaskContext
import com.facebook.react.modules.appregistry.AppRegistry
import com.facebook.react.modules.core.JSTimers
import com.facebook.react.modules.core.ReactChoreographer
import com.facebook.react.modules.core.ReactChoreographer.CallbackType
@@ -27,9 +32,12 @@ import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.eq
import org.mockito.MockedStatic
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
import org.mockito.Mockito.mockStatic
import org.mockito.Mockito.reset
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.`when` as whenever
@@ -43,12 +51,14 @@ class TimingModuleTest {
const val FRAME_TIME_NS = 17 * 1000 * 1000
}
private lateinit var reactContext: ReactApplicationContext
private lateinit var headlessContext: HeadlessJsTaskContext
private lateinit var timingModule: TimingModule
private lateinit var reactChoreographerMock: ReactChoreographer
private lateinit var postFrameCallbackHandler: PostFrameCallbackHandler
private lateinit var idlePostFrameCallbackHandler: PostFrameCallbackHandler
private var currentTimeNs = 0L
private lateinit var jSTimersMock: JSTimers
private lateinit var jsTimersMock: JSTimers
private lateinit var arguments: MockedStatic<Arguments>
private lateinit var systemClock: MockedStatic<SystemClock>
private lateinit var reactChoreographer: MockedStatic<ReactChoreographer>
@@ -82,9 +92,11 @@ class TimingModuleTest {
.thenAnswer { reactChoreographerMock }
val reactInstance = mock(CatalystInstance::class.java)
val reactContext = mock(ReactApplicationContext::class.java)
whenever(reactContext.catalystInstance).thenReturn(reactInstance)
whenever(reactContext.hasActiveReactInstance()).thenReturn(true)
reactContext = spy(ReactApplicationContext(mock(Context::class.java)))
doReturn(reactInstance).`when`(reactContext).catalystInstance
doReturn(true).`when`(reactContext).hasActiveReactInstance()
headlessContext = HeadlessJsTaskContext.getInstance(reactContext)
postFrameCallbackHandler = PostFrameCallbackHandler()
idlePostFrameCallbackHandler = PostFrameCallbackHandler()
@@ -95,7 +107,6 @@ class TimingModuleTest {
.thenAnswer {
return@thenAnswer postFrameCallbackHandler.answer(it)
}
whenever(
reactChoreographerMock.postFrameCallback(
eq(CallbackType.IDLE_EVENT), any(FrameCallback::class.java)))
@@ -104,12 +115,17 @@ class TimingModuleTest {
}
timingModule = TimingModule(reactContext, mock(DevSupportManager::class.java))
jSTimersMock = mock(JSTimers::class.java)
whenever(reactContext.getJSModule(JSTimers::class.java)).thenReturn(jSTimersMock)
whenever(reactContext.runOnJSQueueThread(any(Runnable::class.java))).thenAnswer { invocation ->
(invocation.arguments[0] as Runnable).run()
return@thenAnswer true
}
jsTimersMock = mock(JSTimers::class.java)
doReturn(jsTimersMock).`when`(reactContext).getJSModule(JSTimers::class.java)
doReturn(mock(AppRegistry::class.java))
.`when`(reactContext)
.getJSModule(AppRegistry::class.java)
doAnswer({ invocation ->
(invocation.arguments[0] as Runnable).run()
return@doAnswer true
})
.`when`(reactContext)
.runOnJSQueueThread(any(Runnable::class.java))
timingModule.initialize()
}
@@ -134,111 +150,113 @@ class TimingModuleTest {
@Test
fun testSimpleTimer() {
timingModule.onHostResume()
reactContext.onHostResume(null)
timingModule.createTimer(1.0, 1.0, 0.0, false)
stepChoreographerFrame()
verify(jSTimersMock).callTimers(JavaOnlyArray.of(1.0))
reset(jSTimersMock)
verify(jsTimersMock).callTimers(JavaOnlyArray.of(1.0))
reset(jsTimersMock)
stepChoreographerFrame()
verifyNoMoreInteractions(jSTimersMock)
verifyNoMoreInteractions(jsTimersMock)
}
@Test
fun testSimpleRecurringTimer() {
timingModule.createTimer(100.0, 1.0, 0.0, true)
timingModule.onHostResume()
reactContext.onHostResume(null)
stepChoreographerFrame()
verify(jSTimersMock).callTimers(JavaOnlyArray.of(100.0))
reset(jSTimersMock)
verify(jsTimersMock).callTimers(JavaOnlyArray.of(100.0))
reset(jsTimersMock)
stepChoreographerFrame()
verify(jSTimersMock).callTimers(JavaOnlyArray.of(100.0))
verify(jsTimersMock).callTimers(JavaOnlyArray.of(100.0))
}
@Test
fun testCancelRecurringTimer() {
timingModule.onHostResume()
reactContext.onHostResume(null)
timingModule.createTimer(105.0, 1.0, 0.0, true)
stepChoreographerFrame()
verify(jSTimersMock).callTimers(JavaOnlyArray.of(105.0))
reset(jSTimersMock)
verify(jsTimersMock).callTimers(JavaOnlyArray.of(105.0))
reset(jsTimersMock)
timingModule.deleteTimer(105.0)
stepChoreographerFrame()
verifyNoMoreInteractions(jSTimersMock)
verifyNoMoreInteractions(jsTimersMock)
}
@Test
fun testPausingAndResuming() {
timingModule.onHostResume()
reactContext.onHostResume(null)
timingModule.createTimer(41.0, 1.0, 0.0, true)
stepChoreographerFrame()
verify(jSTimersMock).callTimers(JavaOnlyArray.of(41.0))
reset(jSTimersMock)
timingModule.onHostPause()
verify(jsTimersMock).callTimers(JavaOnlyArray.of(41.0))
reset(jsTimersMock)
reactContext.onHostPause()
stepChoreographerFrame()
verifyNoMoreInteractions(jSTimersMock)
reset(jSTimersMock)
timingModule.onHostResume()
verifyNoMoreInteractions(jsTimersMock)
reset(jsTimersMock)
reactContext.onHostResume(null)
stepChoreographerFrame()
verify(jSTimersMock).callTimers(JavaOnlyArray.of(41.0))
verify(jsTimersMock).callTimers(JavaOnlyArray.of(41.0))
}
@Test
fun testHeadlessJsTaskInBackground() {
timingModule.onHostPause()
timingModule.onHeadlessJsTaskStart(42)
reactContext.onHostPause()
val taskConfig = HeadlessJsTaskConfig("foo", JavaOnlyMap())
val taskId = headlessContext.startTask(taskConfig)
timingModule.createTimer(41.0, 1.0, 0.0, true)
stepChoreographerFrame()
verify(jSTimersMock).callTimers(JavaOnlyArray.of(41.0))
reset(jSTimersMock)
timingModule.onHeadlessJsTaskFinish(42)
verify(jsTimersMock).callTimers(JavaOnlyArray.of(41.0))
reset(jsTimersMock)
headlessContext.finishTask(taskId)
stepChoreographerFrame()
verifyNoMoreInteractions(jSTimersMock)
verifyNoMoreInteractions(jsTimersMock)
}
@Test
fun testHeadlessJsTaskInForeground() {
timingModule.onHostResume()
timingModule.onHeadlessJsTaskStart(42)
val taskConfig = HeadlessJsTaskConfig("foo", JavaOnlyMap())
val taskId = headlessContext.startTask(taskConfig)
reactContext.onHostResume(null)
timingModule.createTimer(41.0, 1.0, 0.0, true)
stepChoreographerFrame()
verify(jSTimersMock).callTimers(JavaOnlyArray.of(41.0))
reset(jSTimersMock)
timingModule.onHeadlessJsTaskFinish(42)
verify(jsTimersMock).callTimers(JavaOnlyArray.of(41.0))
reset(jsTimersMock)
headlessContext.finishTask(taskId)
stepChoreographerFrame()
verify(jSTimersMock).callTimers(JavaOnlyArray.of(41.0))
reset(jSTimersMock)
timingModule.onHostPause()
verifyNoMoreInteractions(jSTimersMock)
verify(jsTimersMock).callTimers(JavaOnlyArray.of(41.0))
reset(jsTimersMock)
reactContext.onHostPause()
verifyNoMoreInteractions(jsTimersMock)
}
@Test
fun testHeadlessJsTaskIntertwine() {
timingModule.onHostResume()
timingModule.onHeadlessJsTaskStart(42)
timingModule.createTimer(41.0, 1.0, 0.0, true)
timingModule.onHostPause()
reactContext.onHostPause()
val taskConfig = HeadlessJsTaskConfig("foo", JavaOnlyMap())
val taskId = headlessContext.startTask(taskConfig)
stepChoreographerFrame()
verify(jSTimersMock).callTimers(JavaOnlyArray.of(41.0))
reset(jSTimersMock)
timingModule.onHostResume()
timingModule.onHeadlessJsTaskFinish(42)
verify(jsTimersMock).callTimers(JavaOnlyArray.of(41.0))
reset(jsTimersMock)
reactContext.onHostResume(null)
headlessContext.finishTask(taskId)
stepChoreographerFrame()
verify(jSTimersMock).callTimers(JavaOnlyArray.of(41.0))
reset(jSTimersMock)
timingModule.onHostPause()
verify(jsTimersMock).callTimers(JavaOnlyArray.of(41.0))
reset(jsTimersMock)
reactContext.onHostPause()
stepChoreographerFrame()
verifyNoMoreInteractions(jSTimersMock)
verifyNoMoreInteractions(jsTimersMock)
}
@Test
fun testSetTimeoutZero() {
timingModule.createTimer(100.0, 0.0, 0.0, false)
verify(jSTimersMock).callTimers(JavaOnlyArray.of(100.0))
verify(jsTimersMock).callTimers(JavaOnlyArray.of(100.0))
}
@Test
fun testActiveTimersInRange() {
timingModule.onHostResume()
reactContext.onHostResume(null)
assertThat(timingModule.hasActiveTimersInRange(100)).isFalse
timingModule.createTimer(41.0, 1.0, 0.0, true)
assertThat(timingModule.hasActiveTimersInRange(100)).isFalse // Repeating
@@ -250,13 +268,12 @@ class TimingModuleTest {
@Test
fun testIdleCallback() {
timingModule.setSendIdleEvents(true)
timingModule.onHostResume()
reactContext.onHostResume(null)
stepChoreographerFrame()
verify(jSTimersMock).callIdleCallbacks(SystemClock.currentTimeMillis().toDouble())
verify(jsTimersMock).callIdleCallbacks(SystemClock.currentTimeMillis().toDouble())
}
private class PostFrameCallbackHandler : Answer<Unit> {
private var frameCallback: FrameCallback? = null
override fun answer(invocation: InvocationOnMock) {