From e4766b79797119015caa68f0641645829fbd9591 Mon Sep 17 00:00:00 2001 From: Andy Street Date: Thu, 3 Mar 2016 08:50:52 -0800 Subject: [PATCH] WebWorkers: Update Timing module to support web workers Summary: Example of a conversion to web worker support using the ExecutionContext API changes made in the last set of web worker diffs. WebWorkerSample now creates timers to show that we can dispatch timer calls to multiple JS contexts. Reviewed By: lexs Differential Revision: D2928657 fb-gh-sync-id: 17c5f8cd7c63624da43383da7c4160dc48482fe5 shipit-source-id: 17c5f8cd7c63624da43383da7c4160dc48482fe5 --- .../react/modules/core/JSTimersExecution.java | 2 + .../facebook/react/modules/core/Timing.java | 74 ++++++++++++++----- .../modules/timing/TimingModuleTest.java | 15 ++-- 3 files changed, 68 insertions(+), 23 deletions(-) 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));