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