diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/JSTimersExecution.java b/ReactAndroid/src/main/java/com/facebook/react/modules/core/JSTimersExecution.java index 67f5ca231..9e0ba2a3d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/core/JSTimersExecution.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/JSTimersExecution.java @@ -10,8 +10,10 @@ package com.facebook.react.modules.core; import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.SupportsWebWorkers; import com.facebook.react.bridge.WritableArray; +@SupportsWebWorkers public interface JSTimersExecution extends JavaScriptModule { public void callTimers(WritableArray timerIDs); diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/Timing.java b/ReactAndroid/src/main/java/com/facebook/react/modules/core/Timing.java index 36954a125..b0bf332da 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/core/Timing.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/Timing.java @@ -12,6 +12,8 @@ package com.facebook.react.modules.core; import javax.annotation.Nullable; import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; import java.util.PriorityQueue; import java.util.concurrent.atomic.AtomicBoolean; @@ -20,7 +22,9 @@ import android.view.Choreographer; import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ExecutorToken; import com.facebook.react.bridge.LifecycleEventListener; +import com.facebook.react.bridge.OnExecutorUnregisteredListener; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; @@ -31,16 +35,24 @@ import com.facebook.react.uimanager.ReactChoreographer; /** * Native module for JS timer execution. Timers fire on frame boundaries. */ -public final class Timing extends ReactContextBaseJavaModule implements LifecycleEventListener { +public final class Timing extends ReactContextBaseJavaModule implements LifecycleEventListener, + OnExecutorUnregisteredListener { private static class Timer { + private final ExecutorToken mExecutorToken; 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) { + private Timer( + ExecutorToken executorToken, + int callbackID, + long initialTargetTime, + int duration, + boolean repeat) { + mExecutorToken = executorToken; mCallbackID = callbackID; mTargetTime = initialTargetTime; mInterval = duration; @@ -50,6 +62,9 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl private class FrameCallback implements Choreographer.FrameCallback { + // Temporary map for constructing the individual arrays of timers per ExecutorToken + private final HashMap mTimersToCall = new HashMap<>(); + /** * Calls all timers that have expired since the last time this frame callback was called. */ @@ -60,14 +75,15 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl } long frameTimeMillis = frameTimeNanos / 1000000; - WritableArray timersToCall = null; synchronized (mTimerGuard) { while (!mTimers.isEmpty() && mTimers.peek().mTargetTime < frameTimeMillis) { Timer timer = mTimers.poll(); - if (timersToCall == null) { - timersToCall = Arguments.createArray(); + WritableArray timersForContext = mTimersToCall.get(timer.mExecutorToken); + if (timersForContext == null) { + timersForContext = Arguments.createArray(); + mTimersToCall.put(timer.mExecutorToken, timersForContext); } - timersToCall.pushInt(timer.mCallbackID); + timersForContext.pushInt(timer.mCallbackID); if (timer.mRepeat) { timer.mTargetTime = frameTimeMillis + timer.mInterval; mTimers.add(timer); @@ -77,9 +93,11 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl } } - if (timersToCall != null) { - Assertions.assertNotNull(mJSTimersModule).callTimers(timersToCall); + for (Map.Entry entry : mTimersToCall.entrySet()) { + getReactApplicationContext().getJSModule(entry.getKey(), JSTimersExecution.class) + .callTimers(entry.getValue()); } + mTimersToCall.clear(); Assertions.assertNotNull(mReactChoreographer) .postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, this); @@ -88,11 +106,10 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl private final Object mTimerGuard = new Object(); private final PriorityQueue mTimers; - private final SparseArray mTimerIdsToTimers; + private final HashMap> mTimerIdsToTimers; private final AtomicBoolean isPaused = new AtomicBoolean(true); private final FrameCallback mFrameCallback = new FrameCallback(); private @Nullable ReactChoreographer mReactChoreographer; - private @Nullable JSTimersExecution mJSTimersModule; private boolean mFrameCallbackPosted = false; public Timing(ReactApplicationContext reactContext) { @@ -113,15 +130,13 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl } } }); - mTimerIdsToTimers = new SparseArray(); + mTimerIdsToTimers = new HashMap<>(); } @Override public void initialize() { // Safe to acquire choreographer here, as initialize() is invoked from UI thread. mReactChoreographer = ReactChoreographer.getInstance(); - mJSTimersModule = getReactApplicationContext().getCatalystInstance() - .getJSModule(JSTimersExecution.class); getReactApplicationContext().addLifecycleEventListener(this); } @@ -172,8 +187,28 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl return "RKTiming"; } + @Override + public boolean supportsWebWorkers() { + return true; + } + + @Override + public void onExecutorDestroyed(ExecutorToken executorToken) { + synchronized (mTimerGuard) { + SparseArray timersForContext = mTimerIdsToTimers.remove(executorToken); + if (timersForContext == null) { + return; + } + for (int i = 0; i < timersForContext.size(); i++) { + Timer timer = timersForContext.get(timersForContext.keyAt(i)); + mTimers.remove(timer); + } + } + } + @ReactMethod public void createTimer( + ExecutorToken executorToken, final int callbackID, final int duration, final double jsSchedulingTime, @@ -183,17 +218,22 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl 0, jsSchedulingTime - SystemClock.currentTimeMillis() + duration); long initialTargetTime = SystemClock.nanoTime() / 1000000 + adjustedDuration; - Timer timer = new Timer(callbackID, initialTargetTime, duration, repeat); + Timer timer = new Timer(executorToken, callbackID, initialTargetTime, duration, repeat); synchronized (mTimerGuard) { mTimers.add(timer); - mTimerIdsToTimers.put(callbackID, timer); + SparseArray timersForContext = mTimerIdsToTimers.get(executorToken); + if (timersForContext == null) { + timersForContext = new SparseArray<>(); + mTimerIdsToTimers.put(executorToken, timersForContext); + } + timersForContext.put(callbackID, timer); } } @ReactMethod - public void deleteTimer(int timerId) { + public void deleteTimer(ExecutorToken executorToken, int timerId) { synchronized (mTimerGuard) { - Timer timer = mTimerIdsToTimers.get(timerId); + Timer timer = mTimerIdsToTimers.get(executorToken).get(timerId); if (timer != null) { // We may have already called/removed it mTimerIdsToTimers.remove(timerId); diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/timing/TimingModuleTest.java b/ReactAndroid/src/test/java/com/facebook/react/modules/timing/TimingModuleTest.java index 423480cda..1597d84e3 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/modules/timing/TimingModuleTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/modules/timing/TimingModuleTest.java @@ -12,6 +12,7 @@ package com.facebook.react.modules.timing; import android.view.Choreographer; import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ExecutorToken; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.CatalystInstance; import com.facebook.react.bridge.JavaOnlyArray; @@ -54,6 +55,7 @@ public class TimingModuleTest { private PostFrameCallbackHandler mPostFrameCallbackHandler; private long mCurrentTimeNs; private JSTimersExecution mJSTimersMock; + private ExecutorToken mExecutorTokenMock; @Rule public PowerMockRule rule = new PowerMockRule(); @@ -92,7 +94,8 @@ public class TimingModuleTest { mTiming = new Timing(reactContext); mJSTimersMock = mock(JSTimersExecution.class); - when(reactInstance.getJSModule(JSTimersExecution.class)).thenReturn(mJSTimersMock); + mExecutorTokenMock = mock(ExecutorToken.class); + when(reactContext.getJSModule(mExecutorTokenMock, JSTimersExecution.class)).thenReturn(mJSTimersMock); mTiming.initialize(); } @@ -107,7 +110,7 @@ public class TimingModuleTest { @Test public void testSimpleTimer() { mTiming.onHostResume(); - mTiming.createTimer(1, 0, 0, false); + mTiming.createTimer(mExecutorTokenMock, 1, 0, 0, false); stepChoreographerFrame(); verify(mJSTimersMock).callTimers(JavaOnlyArray.of(1)); reset(mJSTimersMock); @@ -117,7 +120,7 @@ public class TimingModuleTest { @Test public void testSimpleRecurringTimer() { - mTiming.createTimer(100, 0, 0, true); + mTiming.createTimer(mExecutorTokenMock, 100, 0, 0, true); mTiming.onHostResume(); stepChoreographerFrame(); verify(mJSTimersMock).callTimers(JavaOnlyArray.of(100)); @@ -130,13 +133,13 @@ public class TimingModuleTest { @Test public void testCancelRecurringTimer() { mTiming.onHostResume(); - mTiming.createTimer(105, 0, 0, true); + mTiming.createTimer(mExecutorTokenMock, 105, 0, 0, true); stepChoreographerFrame(); verify(mJSTimersMock).callTimers(JavaOnlyArray.of(105)); reset(mJSTimersMock); - mTiming.deleteTimer(105); + mTiming.deleteTimer(mExecutorTokenMock, 105); stepChoreographerFrame(); verifyNoMoreInteractions(mJSTimersMock); } @@ -144,7 +147,7 @@ public class TimingModuleTest { @Test public void testPausingAndResuming() { mTiming.onHostResume(); - mTiming.createTimer(41, 0, 0, true); + mTiming.createTimer(mExecutorTokenMock, 41, 0, 0, true); stepChoreographerFrame(); verify(mJSTimersMock).callTimers(JavaOnlyArray.of(41));