Initial implementation of requestIdleCallback on Android

Summary:
This is a follow up of the work by brentvatne in #5052. This addresses the feedback by astreet.

- Uses ReactChoreographer with a new callback type
- Callback dispatch logic moved to JS
- Only calls into JS when needed, when there are pending callbacks, it even removes the Choreographer listener when no JS context listen for idle events.

** Test plan **
Tested by running a background task that burns all remaining idle time (see new UIExplorer example) and made sure that UI and JS fps stayed near 60 on a real device (Nexus 6) with dev mode disabled. Also tried adding a JS driven animation and it stayed smooth.

Tested that native only calls into JS when there are pending idle callbacks.

Also tested that timers are executed before idle callback.
```
requestIdleCallback(() => console.log(1));
setTimeout(() => console.log(2), 100);
burnCPU(1000);
// 2
// 1
```

I did *not* test with webworkers but it should work as I'm using executor tokens.
Closes https://github.com/facebook/react-native/pull/8569

Differential Revision: D3558869

Pulled By: astreet

fbshipit-source-id: 61fa82eb26001d2b8c2ea69c35bf3eb5ce5454ba
This commit is contained in:
Janic Duplessis
2016-07-13 18:43:27 -07:00
committed by Facebook Github Bot 5
parent 22eabe59a2
commit 18394fb179
13 changed files with 437 additions and 29 deletions

View File

@@ -32,7 +32,7 @@ import com.facebook.react.uimanager.UIImplementationProvider;
import com.facebook.react.uimanager.ViewManager;
/**
* This class is managing instances of {@link CatalystInstance}. It expose a way to configure
* This class is managing instances of {@link CatalystInstance}. It exposes a way to configure
* catalyst instance using {@link ReactPackage} and keeps track of the lifecycle of that
* instance. It also sets up connection between the instance and developers support functionality
* of the framework.

View File

@@ -80,8 +80,8 @@ import static com.facebook.react.bridge.ReactMarkerConstants.PROCESS_PACKAGES_ST
import static com.facebook.react.bridge.ReactMarkerConstants.RUN_JS_BUNDLE_START;
/**
* This class is managing instances of {@link CatalystInstance}. It expose a way to configure
* catalyst instance using {@link ReactPackage} and keeps track of the lifecycle of that
* This class manages instances of {@link CatalystInstance}. It exposes a way to configure
* catalyst instances using {@link ReactPackage} and keeps track of the lifecycle of that
* instance. It also sets up connection between the instance and developers support functionality
* of the framework.
*

View File

@@ -22,4 +22,9 @@ public class SystemClock {
public static long nanoTime() {
return System.nanoTime();
}
public static long uptimeMillis() {
return android.os.SystemClock.uptimeMillis();
}
}

View File

@@ -15,8 +15,7 @@ import com.facebook.react.bridge.WritableArray;
@SupportsWebWorkers
public interface JSTimersExecution extends JavaScriptModule {
public void callTimers(WritableArray timerIDs);
public void emitTimeDriftWarning(String warningMessage);
void callTimers(WritableArray timerIDs);
void callIdleCallbacks(double frameTime);
void emitTimeDriftWarning(String warningMessage);
}

View File

@@ -9,14 +9,6 @@
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;
import android.util.SparseArray;
import android.view.Choreographer;
@@ -28,17 +20,38 @@ import com.facebook.react.bridge.OnExecutorUnregisteredListener;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.common.SystemClock;
import com.facebook.react.devsupport.DevSupportManager;
import com.facebook.react.uimanager.ReactChoreographer;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nullable;
/**
* Native module for JS timer execution. Timers fire on frame boundaries.
*/
public final class Timing extends ReactContextBaseJavaModule implements LifecycleEventListener,
OnExecutorUnregisteredListener {
// 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 final DevSupportManager mDevSupportManager;
private static class Timer {
@@ -63,7 +76,7 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
}
}
private class FrameCallback implements Choreographer.FrameCallback {
private class TimerFrameCallback implements Choreographer.FrameCallback {
// Temporary map for constructing the individual arrays of timers per ExecutorToken
private final HashMap<ExecutorToken, WritableArray> mTimersToCall = new HashMap<>();
@@ -107,13 +120,84 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
}
}
private class IdleFrameCallback implements Choreographer.FrameCallback {
@Override
public void doFrame(long frameTimeNanos) {
if (isPaused.get()) {
return;
}
// If the JS thread is busy for multiple frames we cancel any other pending runnable.
if (mCurrentIdleCallbackRunnable != null) {
mCurrentIdleCallbackRunnable.cancel();
}
mCurrentIdleCallbackRunnable = new IdleCallbackRunnable(frameTimeNanos);
getReactApplicationContext().runOnJSQueueThread(mCurrentIdleCallbackRunnable);
Assertions.assertNotNull(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;
}
mIdleCallbackContextsToCall.clear();
synchronized (mIdleCallbackGuard) {
mIdleCallbackContextsToCall.addAll(mSendIdleEventsExecutorTokens);
}
for (ExecutorToken context : mIdleCallbackContextsToCall) {
getReactApplicationContext().getJSModule(context, JSTimersExecution.class)
.callIdleCallbacks(absoluteFrameStartTime);
}
mCurrentIdleCallbackRunnable = null;
}
public void cancel() {
mCancelled = true;
}
}
private final Object mTimerGuard = new Object();
private final Object mIdleCallbackGuard = new Object();
private final PriorityQueue<Timer> mTimers;
private final HashMap<ExecutorToken, SparseArray<Timer>> mTimerIdsToTimers;
private final Map<ExecutorToken, SparseArray<Timer>> mTimerIdsToTimers;
private final AtomicBoolean isPaused = new AtomicBoolean(true);
private final FrameCallback mFrameCallback = new FrameCallback();
private final TimerFrameCallback mTimerFrameCallback = new TimerFrameCallback();
private final IdleFrameCallback mIdleFrameCallback = new IdleFrameCallback();
private @Nullable IdleCallbackRunnable mCurrentIdleCallbackRunnable;
private @Nullable ReactChoreographer mReactChoreographer;
private boolean mFrameCallbackPosted = false;
private boolean mFrameIdleCallbackPosted = false;
private final Set<ExecutorToken> mSendIdleEventsExecutorTokens;
// Temporary array used to dipatch idle callbacks on the JS thread.
private final List<ExecutorToken> mIdleCallbackContextsToCall;
public Timing(ReactApplicationContext reactContext, DevSupportManager devSupportManager) {
super(reactContext);
@@ -135,6 +219,8 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
}
});
mTimerIdsToTimers = new HashMap<>();
mSendIdleEventsExecutorTokens = new HashSet<>();
mIdleCallbackContextsToCall = new ArrayList<>();
}
@Override
@@ -148,11 +234,13 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
public void onHostPause() {
isPaused.set(true);
clearChoreographerCallback();
clearChoreographerIdleCallback();
}
@Override
public void onHostDestroy() {
clearChoreographerCallback();
clearChoreographerIdleCallback();
}
@Override
@@ -161,18 +249,25 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
// TODO(5195192) Investigate possible problems related to restarting all tasks at the same
// moment
setChoreographerCallback();
synchronized (mIdleCallbackGuard) {
if (mSendIdleEventsExecutorTokens.size() > 0) {
setChoreographerIdleCallback();
}
}
}
@Override
public void onCatalystInstanceDestroy() {
clearChoreographerCallback();
clearChoreographerIdleCallback();
}
private void setChoreographerCallback() {
if (!mFrameCallbackPosted) {
Assertions.assertNotNull(mReactChoreographer).postFrameCallback(
ReactChoreographer.CallbackType.TIMERS_EVENTS,
mFrameCallback);
mTimerFrameCallback);
mFrameCallbackPosted = true;
}
}
@@ -181,14 +276,32 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
if (mFrameCallbackPosted) {
Assertions.assertNotNull(mReactChoreographer).removeFrameCallback(
ReactChoreographer.CallbackType.TIMERS_EVENTS,
mFrameCallback);
mTimerFrameCallback);
mFrameCallbackPosted = false;
}
}
private void setChoreographerIdleCallback() {
if (!mFrameIdleCallbackPosted) {
Assertions.assertNotNull(mReactChoreographer).postFrameCallback(
ReactChoreographer.CallbackType.IDLE_EVENT,
mIdleFrameCallback);
mFrameIdleCallbackPosted = true;
}
}
private void clearChoreographerIdleCallback() {
if (mFrameIdleCallbackPosted) {
Assertions.assertNotNull(mReactChoreographer).removeFrameCallback(
ReactChoreographer.CallbackType.IDLE_EVENT,
mIdleFrameCallback);
mFrameIdleCallbackPosted = false;
}
}
@Override
public String getName() {
return "RKTiming";
return "RCTTiming";
}
@Override
@@ -196,6 +309,13 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
return true;
}
@Override
public Map<String, Object> getConstants() {
return MapBuilder.<String, Object>of(
"frameDuration", FRAME_DURATION_MS,
"idleCallbackFrameDeadline", IDLE_CALLBACK_FRAME_DEADLINE_MS);
}
@Override
public void onExecutorDestroyed(ExecutorToken executorToken) {
synchronized (mTimerGuard) {
@@ -208,6 +328,10 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
mTimers.remove(timer);
}
}
synchronized (mIdleCallbackGuard) {
mSendIdleEventsExecutorTokens.remove(executorToken);
}
}
@ReactMethod
@@ -272,4 +396,28 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
mTimers.remove(timer);
}
}
@ReactMethod
public void setSendIdleEvents(ExecutorToken executorToken, boolean sendIdleEvents) {
synchronized (mIdleCallbackGuard) {
if (sendIdleEvents) {
mSendIdleEventsExecutorTokens.add(executorToken);
} else {
mSendIdleEventsExecutorTokens.remove(executorToken);
}
}
UiThreadUtil.runOnUiThread(new Runnable() {
@Override
public void run() {
synchronized (mIdleCallbackGuard) {
if (mSendIdleEventsExecutorTokens.size() > 0) {
setChoreographerIdleCallback();
} else {
clearChoreographerIdleCallback();
}
}
}
});
}
}

View File

@@ -46,6 +46,12 @@ public class ReactChoreographer {
* Events that make JS do things.
*/
TIMERS_EVENTS(3),
/**
* Event used to trigger the idle callback. Called after all UI work has been
* dispatched to JS.
*/
IDLE_EVENT(4),
;
private final int mOrder;