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

@ -21,6 +21,7 @@
"__fbBatchedBridgeConfig": false, "__fbBatchedBridgeConfig": false,
"alert": false, "alert": false,
"cancelAnimationFrame": false, "cancelAnimationFrame": false,
"cancelIdleCallback": false,
"clearImmediate": true, "clearImmediate": true,
"clearInterval": false, "clearInterval": false,
"clearTimeout": false, "clearTimeout": false,
@ -40,6 +41,7 @@
"process": false, "process": false,
"Promise": true, "Promise": true,
"requestAnimationFrame": true, "requestAnimationFrame": true,
"requestIdleCallback": true,
"require": false, "require": false,
"Set": true, "Set": true,
"setImmediate": true, "setImmediate": true,

View File

@ -28,10 +28,101 @@ var {
AlertIOS, AlertIOS,
Platform, Platform,
ToastAndroid, ToastAndroid,
Text,
View, View,
} = ReactNative; } = ReactNative;
var TimerMixin = require('react-timer-mixin'); var TimerMixin = require('react-timer-mixin');
var UIExplorerButton = require('./UIExplorerButton'); var UIExplorerButton = require('./UIExplorerButton');
var performanceNow = require('fbjs/lib/performanceNow');
function burnCPU(milliseconds) {
const start = performanceNow();
while (performanceNow() < (start + milliseconds)) {}
}
var RequestIdleCallbackTester = React.createClass({
_idleTimer: (null: any),
_iters: 0,
getInitialState() {
return {
message: '-',
};
},
componentWillUnmount() {
cancelIdleCallback(this._idleTimer);
},
render() {
return (
<View>
{Platform.OS === 'ios' ? this._renderIOS() : this._renderAndroid()}
</View>
);
},
_renderIOS() {
return (
<Text>Not implemented on iOS, falls back to requestAnimationFrame</Text>
);
},
_renderAndroid() {
return (
<View>
<UIExplorerButton onPress={this._run.bind(this, false)}>
Run requestIdleCallback
</UIExplorerButton>
<UIExplorerButton onPress={this._run.bind(this, true)}>
Burn CPU inside of requestIdleCallback
</UIExplorerButton>
<UIExplorerButton onPress={this._runBackground}>
Run background task
</UIExplorerButton>
<UIExplorerButton onPress={this._stopBackground}>
Stop background task
</UIExplorerButton>
<Text>{this.state.message}</Text>
</View>
);
},
_run(shouldBurnCPU) {
cancelIdleCallback(this._idleTimer);
this._idleTimer = requestIdleCallback((deadline) => {
let message = '';
if (shouldBurnCPU) {
burnCPU(10);
message = 'Burned CPU for 10ms,';
}
this.setState({message: `${message} ${deadline.timeRemaining()}ms remaining in frame`});
});
},
_runBackground() {
cancelIdleCallback(this._idleTimer);
const handler = (deadline) => {
while (deadline.timeRemaining() > 5) {
burnCPU(5);
this.setState({message: `Burned CPU for 5ms ${this._iters++} times, ${deadline.timeRemaining()}ms remaining in frame`});
}
this._idleTimer = requestIdleCallback(handler);
};
this._idleTimer = requestIdleCallback(handler);
},
_stopBackground() {
this._iters = 0;
cancelIdleCallback(this._idleTimer);
}
});
var TimerTester = React.createClass({ var TimerTester = React.createClass({
mixins: [TimerMixin], mixins: [TimerMixin],
@ -141,6 +232,17 @@ exports.examples = [
); );
}, },
}, },
{
title: 'this.requestIdleCallback(fn)',
description: 'Execute function fn on the next JS frame that has idle time',
render: function() {
return (
<View>
<RequestIdleCallbackTester />
</View>
);
},
},
{ {
title: 'this.setImmediate(fn)', title: 'this.setImmediate(fn)',
description: 'Execute function fn at the end of the current JS event loop.', description: 'Execute function fn at the end of the current JS event loop.',

View File

@ -144,6 +144,8 @@ function setUpTimers(): void {
defineLazyTimer('clearImmediate'); defineLazyTimer('clearImmediate');
defineLazyTimer('requestAnimationFrame'); defineLazyTimer('requestAnimationFrame');
defineLazyTimer('cancelAnimationFrame'); defineLazyTimer('cancelAnimationFrame');
defineLazyTimer('requestIdleCallback');
defineLazyTimer('cancelIdleCallback');
} }
function setUpAlert(): void { function setUpAlert(): void {

View File

@ -96,6 +96,41 @@ var JSTimers = {
return newID; return newID;
}, },
/**
* @param {function} func Callback to be invoked every frame and provided
* with time remaining in frame.
*/
requestIdleCallback: function(func) {
if (!RCTTiming.setSendIdleEvents) {
console.warn('requestIdleCallback is not currently supported on this platform');
return requestAnimationFrame(func);
}
if (JSTimersExecution.requestIdleCallbacks.length === 0) {
RCTTiming.setSendIdleEvents(true);
}
var newID = JSTimersExecution.GUID++;
var freeIndex = JSTimers._getFreeIndex();
JSTimersExecution.timerIDs[freeIndex] = newID;
JSTimersExecution.callbacks[freeIndex] = func;
JSTimersExecution.types[freeIndex] = JSTimersExecution.Type.requestIdleCallback;
JSTimersExecution.requestIdleCallbacks.push(newID);
return newID;
},
cancelIdleCallback: function(timerID) {
JSTimers._clearTimerID(timerID);
var index = JSTimersExecution.requestIdleCallbacks.indexOf(timerID);
if (index !== -1) {
JSTimersExecution.requestIdleCallbacks.splice(index, 1);
}
if (JSTimersExecution.requestIdleCallbacks.length === 0) {
RCTTiming.setSendIdleEvents(false);
}
},
clearTimeout: function(timerID) { clearTimeout: function(timerID) {
JSTimers._clearTimerID(timerID); JSTimers._clearTimerID(timerID);
}, },
@ -127,7 +162,9 @@ var JSTimers = {
// See corresponding comment in `callTimers` for reasoning behind this // See corresponding comment in `callTimers` for reasoning behind this
if (index !== -1) { if (index !== -1) {
JSTimersExecution._clearIndex(index); JSTimersExecution._clearIndex(index);
if (JSTimersExecution.types[index] !== JSTimersExecution.Type.setImmediate) { var type = JSTimersExecution.types[index];
if (type !== JSTimersExecution.Type.setImmediate &&
type !== JSTimersExecution.Type.requestIdleCallback) {
RCTTiming.deleteTimer(timerID); RCTTiming.deleteTimer(timerID);
} }
} }

View File

@ -31,6 +31,7 @@ const JSTimersExecution = {
setInterval: null, setInterval: null,
requestAnimationFrame: null, requestAnimationFrame: null,
setImmediate: null, setImmediate: null,
requestIdleCallback: null,
}), }),
// Parallel arrays: // Parallel arrays:
@ -38,13 +39,14 @@ const JSTimersExecution = {
types: [], types: [],
timerIDs: [], timerIDs: [],
immediates: [], immediates: [],
requestIdleCallbacks: [],
/** /**
* Calls the callback associated with the ID. Also unregister that callback * Calls the callback associated with the ID. Also unregister that callback
* if it was a one time timer (setTimeout), and not unregister it if it was * if it was a one time timer (setTimeout), and not unregister it if it was
* recurring (setInterval). * recurring (setInterval).
*/ */
callTimer(timerID) { callTimer(timerID, frameTime) {
warning( warning(
timerID <= JSTimersExecution.GUID, timerID <= JSTimersExecution.GUID,
'Tried to call timer with ID %s but no such timer exists.', 'Tried to call timer with ID %s but no such timer exists.',
@ -65,7 +67,8 @@ const JSTimersExecution = {
// Clear the metadata // Clear the metadata
if (type === JSTimersExecution.Type.setTimeout || if (type === JSTimersExecution.Type.setTimeout ||
type === JSTimersExecution.Type.setImmediate || type === JSTimersExecution.Type.setImmediate ||
type === JSTimersExecution.Type.requestAnimationFrame) { type === JSTimersExecution.Type.requestAnimationFrame ||
type === JSTimersExecution.Type.requestIdleCallback) {
JSTimersExecution._clearIndex(timerIndex); JSTimersExecution._clearIndex(timerIndex);
} }
@ -77,6 +80,16 @@ const JSTimersExecution = {
} else if (type === JSTimersExecution.Type.requestAnimationFrame) { } else if (type === JSTimersExecution.Type.requestAnimationFrame) {
const currentTime = performanceNow(); const currentTime = performanceNow();
callback(currentTime); callback(currentTime);
} else if (type === JSTimersExecution.Type.requestIdleCallback) {
const { Timing } = require('NativeModules');
callback({
timeRemaining: function() {
// TODO: Optimisation: allow running for longer than one frame if
// there are no pending JS calls on the bridge from native. This
// would require a way to check the bridge queue synchronously.
return Math.max(0, Timing.frameDuration - (performanceNow() - frameTime));
},
});
} else { } else {
console.error('Tried to call a callback with invalid type: ' + type); console.error('Tried to call a callback with invalid type: ' + type);
return; return;
@ -99,7 +112,7 @@ const JSTimersExecution = {
); );
JSTimersExecution.errors = null; JSTimersExecution.errors = null;
timerIDs.forEach(JSTimersExecution.callTimer); timerIDs.forEach((id) => { JSTimersExecution.callTimer(id); });
const errors = JSTimersExecution.errors; const errors = JSTimersExecution.errors;
if (errors) { if (errors) {
@ -118,6 +131,35 @@ const JSTimersExecution = {
} }
}, },
callIdleCallbacks: function(frameTime) {
const { Timing } = require('NativeModules');
if (Timing.frameDuration - (performanceNow() - frameTime) < Timing.idleCallbackFrameDeadline) {
return;
}
JSTimersExecution.errors = null;
if (JSTimersExecution.requestIdleCallbacks.length > 0) {
const passIdleCallbacks = JSTimersExecution.requestIdleCallbacks.slice();
JSTimersExecution.requestIdleCallbacks = [];
for (let i = 0; i < passIdleCallbacks.length; ++i) {
JSTimersExecution.callTimer(passIdleCallbacks[i], frameTime);
}
}
if (JSTimersExecution.requestIdleCallbacks.length === 0) {
Timing.setSendIdleEvents(false);
}
if (JSTimersExecution.errors) {
JSTimersExecution.errors.forEach((error) =>
require('JSTimers').setTimeout(() => { throw error; }, 0)
);
}
},
/** /**
* Performs a single pass over the enqueued immediates. Returns whether * Performs a single pass over the enqueued immediates. Returns whether
* more immediates are queued up (can be used as a condition a while loop). * more immediates are queued up (can be used as a condition a while loop).

View File

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

View File

@ -22,4 +22,9 @@ public class SystemClock {
public static long nanoTime() { public static long nanoTime() {
return System.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 @SupportsWebWorkers
public interface JSTimersExecution extends JavaScriptModule { public interface JSTimersExecution extends JavaScriptModule {
void callTimers(WritableArray timerIDs);
public void callTimers(WritableArray timerIDs); void callIdleCallbacks(double frameTime);
void emitTimeDriftWarning(String warningMessage);
public void emitTimeDriftWarning(String warningMessage);
} }

View File

@ -9,14 +9,6 @@
package com.facebook.react.modules.core; 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.util.SparseArray;
import android.view.Choreographer; 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.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;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableArray;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.common.SystemClock; import com.facebook.react.common.SystemClock;
import com.facebook.react.devsupport.DevSupportManager; import com.facebook.react.devsupport.DevSupportManager;
import com.facebook.react.uimanager.ReactChoreographer; 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. * 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 { 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 final DevSupportManager mDevSupportManager;
private static class Timer { 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 // Temporary map for constructing the individual arrays of timers per ExecutorToken
private final HashMap<ExecutorToken, WritableArray> mTimersToCall = new HashMap<>(); 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 mTimerGuard = new Object();
private final Object mIdleCallbackGuard = new Object();
private final PriorityQueue<Timer> mTimers; 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 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 @Nullable ReactChoreographer mReactChoreographer;
private boolean mFrameCallbackPosted = false; 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) { public Timing(ReactApplicationContext reactContext, DevSupportManager devSupportManager) {
super(reactContext); super(reactContext);
@ -135,6 +219,8 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
} }
}); });
mTimerIdsToTimers = new HashMap<>(); mTimerIdsToTimers = new HashMap<>();
mSendIdleEventsExecutorTokens = new HashSet<>();
mIdleCallbackContextsToCall = new ArrayList<>();
} }
@Override @Override
@ -148,11 +234,13 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
public void onHostPause() { public void onHostPause() {
isPaused.set(true); isPaused.set(true);
clearChoreographerCallback(); clearChoreographerCallback();
clearChoreographerIdleCallback();
} }
@Override @Override
public void onHostDestroy() { public void onHostDestroy() {
clearChoreographerCallback(); clearChoreographerCallback();
clearChoreographerIdleCallback();
} }
@Override @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 // TODO(5195192) Investigate possible problems related to restarting all tasks at the same
// moment // moment
setChoreographerCallback(); setChoreographerCallback();
synchronized (mIdleCallbackGuard) {
if (mSendIdleEventsExecutorTokens.size() > 0) {
setChoreographerIdleCallback();
}
}
} }
@Override @Override
public void onCatalystInstanceDestroy() { public void onCatalystInstanceDestroy() {
clearChoreographerCallback(); clearChoreographerCallback();
clearChoreographerIdleCallback();
} }
private void setChoreographerCallback() { private void setChoreographerCallback() {
if (!mFrameCallbackPosted) { if (!mFrameCallbackPosted) {
Assertions.assertNotNull(mReactChoreographer).postFrameCallback( Assertions.assertNotNull(mReactChoreographer).postFrameCallback(
ReactChoreographer.CallbackType.TIMERS_EVENTS, ReactChoreographer.CallbackType.TIMERS_EVENTS,
mFrameCallback); mTimerFrameCallback);
mFrameCallbackPosted = true; mFrameCallbackPosted = true;
} }
} }
@ -181,14 +276,32 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
if (mFrameCallbackPosted) { if (mFrameCallbackPosted) {
Assertions.assertNotNull(mReactChoreographer).removeFrameCallback( Assertions.assertNotNull(mReactChoreographer).removeFrameCallback(
ReactChoreographer.CallbackType.TIMERS_EVENTS, ReactChoreographer.CallbackType.TIMERS_EVENTS,
mFrameCallback); mTimerFrameCallback);
mFrameCallbackPosted = false; 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 @Override
public String getName() { public String getName() {
return "RKTiming"; return "RCTTiming";
} }
@Override @Override
@ -196,6 +309,13 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
return true; return true;
} }
@Override
public Map<String, Object> getConstants() {
return MapBuilder.<String, Object>of(
"frameDuration", FRAME_DURATION_MS,
"idleCallbackFrameDeadline", IDLE_CALLBACK_FRAME_DEADLINE_MS);
}
@Override @Override
public void onExecutorDestroyed(ExecutorToken executorToken) { public void onExecutorDestroyed(ExecutorToken executorToken) {
synchronized (mTimerGuard) { synchronized (mTimerGuard) {
@ -208,6 +328,10 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
mTimers.remove(timer); mTimers.remove(timer);
} }
} }
synchronized (mIdleCallbackGuard) {
mSendIdleEventsExecutorTokens.remove(executorToken);
}
} }
@ReactMethod @ReactMethod
@ -272,4 +396,28 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
mTimers.remove(timer); 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. * Events that make JS do things.
*/ */
TIMERS_EVENTS(3), 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; private final int mOrder;

View File

@ -52,8 +52,9 @@ public class TimingModuleTest {
private static final long FRAME_TIME_NS = 17 * 1000 * 1000; // 17 ms private static final long FRAME_TIME_NS = 17 * 1000 * 1000; // 17 ms
private Timing mTiming; private Timing mTiming;
private ReactChoreographer mChoreographerMock; private ReactChoreographer mReactChoreographerMock;
private PostFrameCallbackHandler mPostFrameCallbackHandler; private PostFrameCallbackHandler mPostFrameCallbackHandler;
private PostFrameIdleCallbackHandler mIdlePostFrameCallbackHandler;
private long mCurrentTimeNs; private long mCurrentTimeNs;
private JSTimersExecution mJSTimersMock; private JSTimersExecution mJSTimersMock;
private ExecutorToken mExecutorTokenMock; private ExecutorToken mExecutorTokenMock;
@ -73,12 +74,13 @@ public class TimingModuleTest {
}); });
PowerMockito.mockStatic(SystemClock.class); PowerMockito.mockStatic(SystemClock.class);
when(SystemClock.uptimeMillis()).thenReturn(mCurrentTimeNs / 1000000);
when(SystemClock.currentTimeMillis()).thenReturn(mCurrentTimeNs / 1000000); when(SystemClock.currentTimeMillis()).thenReturn(mCurrentTimeNs / 1000000);
when(SystemClock.nanoTime()).thenReturn(mCurrentTimeNs); when(SystemClock.nanoTime()).thenReturn(mCurrentTimeNs);
mChoreographerMock = mock(ReactChoreographer.class); mReactChoreographerMock = mock(ReactChoreographer.class);
PowerMockito.mockStatic(ReactChoreographer.class); PowerMockito.mockStatic(ReactChoreographer.class);
when(ReactChoreographer.getInstance()).thenReturn(mChoreographerMock); when(ReactChoreographer.getInstance()).thenReturn(mReactChoreographerMock);
CatalystInstance reactInstance = mock(CatalystInstance.class); CatalystInstance reactInstance = mock(CatalystInstance.class);
ReactApplicationContext reactContext = mock(ReactApplicationContext.class); ReactApplicationContext reactContext = mock(ReactApplicationContext.class);
@ -86,26 +88,49 @@ public class TimingModuleTest {
mCurrentTimeNs = 0; mCurrentTimeNs = 0;
mPostFrameCallbackHandler = new PostFrameCallbackHandler(); mPostFrameCallbackHandler = new PostFrameCallbackHandler();
mIdlePostFrameCallbackHandler = new PostFrameIdleCallbackHandler();
doAnswer(mPostFrameCallbackHandler) doAnswer(mPostFrameCallbackHandler)
.when(mChoreographerMock) .when(mReactChoreographerMock)
.postFrameCallback( .postFrameCallback(
eq(ReactChoreographer.CallbackType.TIMERS_EVENTS), eq(ReactChoreographer.CallbackType.TIMERS_EVENTS),
any(Choreographer.FrameCallback.class)); any(Choreographer.FrameCallback.class));
doAnswer(mIdlePostFrameCallbackHandler)
.when(mReactChoreographerMock)
.postFrameCallback(
eq(ReactChoreographer.CallbackType.IDLE_EVENT),
any(Choreographer.FrameCallback.class));
mTiming = new Timing(reactContext, mock(DevSupportManager.class)); mTiming = new Timing(reactContext, mock(DevSupportManager.class));
mJSTimersMock = mock(JSTimersExecution.class); mJSTimersMock = mock(JSTimersExecution.class);
mExecutorTokenMock = mock(ExecutorToken.class); mExecutorTokenMock = mock(ExecutorToken.class);
when(reactContext.getJSModule(mExecutorTokenMock, JSTimersExecution.class)).thenReturn(mJSTimersMock); when(reactContext.getJSModule(mExecutorTokenMock, JSTimersExecution.class)).thenReturn(mJSTimersMock);
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
((Runnable)invocation.getArguments()[0]).run();
return null;
}
}).when(reactContext).runOnJSQueueThread(any(Runnable.class));
mTiming.initialize(); mTiming.initialize();
} }
private void stepChoreographerFrame() { private void stepChoreographerFrame() {
Choreographer.FrameCallback callback = mPostFrameCallbackHandler.getAndResetFrameCallback(); Choreographer.FrameCallback callback = mPostFrameCallbackHandler.getAndResetFrameCallback();
Choreographer.FrameCallback idleCallback = mIdlePostFrameCallbackHandler.getAndResetFrameCallback();
mCurrentTimeNs += FRAME_TIME_NS; mCurrentTimeNs += FRAME_TIME_NS;
when(SystemClock.uptimeMillis()).thenReturn(mCurrentTimeNs / 1000000);
if (callback != null) { if (callback != null) {
callback.doFrame(mCurrentTimeNs); callback.doFrame(mCurrentTimeNs);
} }
if (idleCallback != null) {
idleCallback.doFrame(mCurrentTimeNs);
}
} }
@Test @Test
@ -170,6 +195,33 @@ public class TimingModuleTest {
verify(mJSTimersMock).callTimers(JavaOnlyArray.of(100)); verify(mJSTimersMock).callTimers(JavaOnlyArray.of(100));
} }
@Test
public void testIdleCallback() {
mTiming.onHostResume();
mTiming.setSendIdleEvents(mExecutorTokenMock, true);
stepChoreographerFrame();
verify(mJSTimersMock).callIdleCallbacks(SystemClock.currentTimeMillis());
}
private static class PostFrameIdleCallbackHandler implements Answer<Void> {
private Choreographer.FrameCallback mFrameCallback;
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
mFrameCallback = (Choreographer.FrameCallback) args[1];
return null;
}
public Choreographer.FrameCallback getAndResetFrameCallback() {
Choreographer.FrameCallback callback = mFrameCallback;
mFrameCallback = null;
return callback;
}
}
private static class PostFrameCallbackHandler implements Answer<Void> { private static class PostFrameCallbackHandler implements Answer<Void> {
private Choreographer.FrameCallback mFrameCallback; private Choreographer.FrameCallback mFrameCallback;

13
flow/global.js Normal file
View File

@ -0,0 +1,13 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
/* eslint-disable */
// Add missing flow definitions, can be removed when included in flow.
declare function requestIdleCallback(callback: any): number;
declare function cancelIdleCallback(idleCallbackId: ?number): void;