mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Batch Animated calls into one JSI call per frame
Summary: We introduce a few optimizations: (1) Previous diff: We defer calling any NativeAnimatedModule methods by waiting 1ms before flushing the queue, and debouncing until no flush is requested. Practically, this just means that we'll call NativeAnimatedModule methods N times at once, at the end of a render loop, instead of N times smeared throughout the render loop. (2) Additionally, instead of calling N methods, we create multi-operation argument buffer and call a single NativeAnimatedModule API, which should essentially throttle NativeAnimatedModule API calls to once-ish per frame. On the native side, this also reduces a lot of overhead associated with scheduling work on the UI thread (we schedule 1 function to run on the UI thread and perform N operations, as opposed to scheduling N functions to run on the UI thread). TODO: - implement stubs for iOS - write gating code so this can be properly tested in VR and in fb4a Changelog: [Internal] Reviewed By: genkikondo Differential Revision: D36338606 fbshipit-source-id: 29ac949b53b874683128a76525586c22def3143b
This commit is contained in:
committed by
Facebook GitHub Bot
parent
29a91babd4
commit
35e2a63b8d
@@ -19,6 +19,7 @@ import com.facebook.react.bridge.Callback;
|
||||
import com.facebook.react.bridge.LifecycleEventListener;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactSoftExceptionLogger;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.UIManager;
|
||||
import com.facebook.react.bridge.UIManagerListener;
|
||||
@@ -92,6 +93,50 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec
|
||||
public static final String NAME = "NativeAnimatedModule";
|
||||
public static final boolean ANIMATED_MODULE_DEBUG = false;
|
||||
|
||||
// For `queueAndExecuteBatchedOperations`
|
||||
private enum BatchExecutionOpCodes {
|
||||
OP_CODE_CREATE_ANIMATED_NODE(1),
|
||||
OP_CODE_UPDATE_ANIMATED_NODE_CONFIG(2),
|
||||
OP_CODE_GET_VALUE(3),
|
||||
OP_START_LISTENING_TO_ANIMATED_NODE_VALUE(4),
|
||||
OP_STOP_LISTENING_TO_ANIMATED_NODE_VALUE(5),
|
||||
OP_CODE_CONNECT_ANIMATED_NODES(6),
|
||||
OP_CODE_DISCONNECT_ANIMATED_NODES(7),
|
||||
OP_CODE_START_ANIMATING_NODE(8),
|
||||
OP_CODE_STOP_ANIMATION(9),
|
||||
OP_CODE_SET_ANIMATED_NODE_VALUE(10),
|
||||
OP_CODE_SET_ANIMATED_NODE_OFFSET(11),
|
||||
OP_CODE_FLATTEN_ANIMATED_NODE_OFFSET(12),
|
||||
OP_CODE_EXTRACT_ANIMATED_NODE_OFFSET(13),
|
||||
OP_CODE_CONNECT_ANIMATED_NODE_TO_VIEW(14),
|
||||
OP_CODE_DISCONNECT_ANIMATED_NODE_FROM_VIEW(15),
|
||||
OP_CODE_RESTORE_DEFAULT_VALUES(16),
|
||||
OP_CODE_DROP_ANIMATED_NODE(17),
|
||||
OP_CODE_ADD_ANIMATED_EVENT_TO_VIEW(18),
|
||||
OP_CODE_REMOVE_ANIMATED_EVENT_FROM_VIEW(19),
|
||||
OP_CODE_ADD_LISTENER(20), // ios only
|
||||
OP_CODE_REMOVE_LISTENERS(21); // ios only
|
||||
|
||||
private static BatchExecutionOpCodes[] valueMap = null;
|
||||
private final int value;
|
||||
|
||||
private BatchExecutionOpCodes(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public static BatchExecutionOpCodes fromId(int id) {
|
||||
if (BatchExecutionOpCodes.valueMap == null) {
|
||||
BatchExecutionOpCodes.valueMap = BatchExecutionOpCodes.values();
|
||||
}
|
||||
// Enum values are 1-indexed, but the value array is 0-indexed
|
||||
return BatchExecutionOpCodes.valueMap[id - 1];
|
||||
}
|
||||
}
|
||||
|
||||
private abstract class UIThreadOperation {
|
||||
abstract void execute(NativeAnimatedNodesManager animatedNodesManager);
|
||||
|
||||
@@ -973,4 +1018,186 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec
|
||||
context.removeLifecycleEventListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a currently-experimental method that allows JS to queue and immediately execute many
|
||||
* instructions at once. Since we make 1 JNI/JSI call instead of N, this should significantly
|
||||
* improve performance.
|
||||
*
|
||||
* <p>The arguments operate as a byte buffer. All integer command IDs and any args are packed into
|
||||
* opsAndArgs.
|
||||
*
|
||||
* <p>For the getValue callback: since this is batched, we accumulate a list of all requested
|
||||
* values, in order, and call the callback once at the end (if present) with the list of requested
|
||||
* values.
|
||||
*/
|
||||
@Override
|
||||
public void queueAndExecuteBatchedOperations(final ReadableArray opsAndArgs) {
|
||||
// This block of code is unfortunate and should be refactored - we just want to
|
||||
// extract the ViewTags in the ReadableArray to mark animations on views as being enabled.
|
||||
// We only do this for initializing animations on views - disabling animations on views
|
||||
// happens later, when the disconnect/stop operations are actually executed.
|
||||
final int opBufferSize = opsAndArgs.size();
|
||||
for (int i = 0; i < opBufferSize; ) {
|
||||
BatchExecutionOpCodes command = BatchExecutionOpCodes.fromId(opsAndArgs.getInt(i++));
|
||||
switch (command) {
|
||||
case OP_CODE_GET_VALUE:
|
||||
case OP_STOP_LISTENING_TO_ANIMATED_NODE_VALUE:
|
||||
case OP_CODE_STOP_ANIMATION:
|
||||
case OP_CODE_FLATTEN_ANIMATED_NODE_OFFSET:
|
||||
case OP_CODE_EXTRACT_ANIMATED_NODE_OFFSET:
|
||||
case OP_CODE_RESTORE_DEFAULT_VALUES:
|
||||
case OP_CODE_DROP_ANIMATED_NODE:
|
||||
case OP_CODE_ADD_LISTENER:
|
||||
case OP_CODE_REMOVE_LISTENERS:
|
||||
i++;
|
||||
break;
|
||||
case OP_CODE_CREATE_ANIMATED_NODE:
|
||||
case OP_CODE_UPDATE_ANIMATED_NODE_CONFIG:
|
||||
case OP_START_LISTENING_TO_ANIMATED_NODE_VALUE:
|
||||
case OP_CODE_CONNECT_ANIMATED_NODES:
|
||||
case OP_CODE_DISCONNECT_ANIMATED_NODES:
|
||||
case OP_CODE_SET_ANIMATED_NODE_VALUE:
|
||||
case OP_CODE_SET_ANIMATED_NODE_OFFSET:
|
||||
case OP_CODE_DISCONNECT_ANIMATED_NODE_FROM_VIEW:
|
||||
i += 2;
|
||||
break;
|
||||
case OP_CODE_START_ANIMATING_NODE:
|
||||
case OP_CODE_REMOVE_ANIMATED_EVENT_FROM_VIEW:
|
||||
i += 3;
|
||||
break;
|
||||
case OP_CODE_CONNECT_ANIMATED_NODE_TO_VIEW:
|
||||
i++; // tag
|
||||
initializeLifecycleEventListenersForViewTag(opsAndArgs.getInt(i++)); // viewTag
|
||||
break;
|
||||
case OP_CODE_ADD_ANIMATED_EVENT_TO_VIEW:
|
||||
initializeLifecycleEventListenersForViewTag(opsAndArgs.getInt(i++)); // viewTag
|
||||
i++; // eventName
|
||||
i++; // eventMapping
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Batch animation execution op: fetching viewTag: unknown op code");
|
||||
}
|
||||
}
|
||||
|
||||
// Batching happens inside this operation - so signal to the thread loop that
|
||||
// this operation should be executed as soon as possible, "unbatched" with other
|
||||
// UIThreadOperations
|
||||
startOperationBatch();
|
||||
addUnbatchedOperation(
|
||||
new UIThreadOperation() {
|
||||
@Override
|
||||
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
|
||||
ReactApplicationContext reactApplicationContext =
|
||||
getReactApplicationContextIfActiveOrWarn();
|
||||
|
||||
int viewTag = -1;
|
||||
for (int i = 0; i < opBufferSize; ) {
|
||||
BatchExecutionOpCodes command = BatchExecutionOpCodes.fromId(opsAndArgs.getInt(i++));
|
||||
|
||||
switch (command) {
|
||||
case OP_CODE_CREATE_ANIMATED_NODE:
|
||||
animatedNodesManager.createAnimatedNode(
|
||||
opsAndArgs.getInt(i++), opsAndArgs.getMap(i++));
|
||||
break;
|
||||
case OP_CODE_UPDATE_ANIMATED_NODE_CONFIG:
|
||||
animatedNodesManager.updateAnimatedNodeConfig(
|
||||
opsAndArgs.getInt(i++), opsAndArgs.getMap(i++));
|
||||
break;
|
||||
case OP_CODE_GET_VALUE:
|
||||
animatedNodesManager.getValue(opsAndArgs.getInt(i++), null);
|
||||
break;
|
||||
case OP_START_LISTENING_TO_ANIMATED_NODE_VALUE:
|
||||
final int tag = opsAndArgs.getInt(i++);
|
||||
final int value = opsAndArgs.getInt(i++);
|
||||
final AnimatedNodeValueListener listener =
|
||||
new AnimatedNodeValueListener() {
|
||||
public void onValueUpdate(double value) {
|
||||
WritableMap onAnimatedValueData = Arguments.createMap();
|
||||
onAnimatedValueData.putInt("tag", tag);
|
||||
onAnimatedValueData.putDouble("value", value);
|
||||
|
||||
ReactApplicationContext reactApplicationContext =
|
||||
getReactApplicationContextIfActiveOrWarn();
|
||||
if (reactApplicationContext != null) {
|
||||
reactApplicationContext
|
||||
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
.emit("onAnimatedValueUpdate", onAnimatedValueData);
|
||||
}
|
||||
}
|
||||
};
|
||||
animatedNodesManager.startListeningToAnimatedNodeValue(tag, listener);
|
||||
break;
|
||||
case OP_STOP_LISTENING_TO_ANIMATED_NODE_VALUE:
|
||||
animatedNodesManager.stopListeningToAnimatedNodeValue(opsAndArgs.getInt(i++));
|
||||
break;
|
||||
case OP_CODE_CONNECT_ANIMATED_NODES:
|
||||
animatedNodesManager.connectAnimatedNodes(
|
||||
opsAndArgs.getInt(i++), opsAndArgs.getInt(i++));
|
||||
break;
|
||||
case OP_CODE_DISCONNECT_ANIMATED_NODES:
|
||||
animatedNodesManager.disconnectAnimatedNodes(
|
||||
opsAndArgs.getInt(i++), opsAndArgs.getInt(i++));
|
||||
break;
|
||||
case OP_CODE_START_ANIMATING_NODE:
|
||||
animatedNodesManager.startAnimatingNode(
|
||||
opsAndArgs.getInt(i++), opsAndArgs.getInt(i++), opsAndArgs.getMap(i++), null);
|
||||
break;
|
||||
case OP_CODE_STOP_ANIMATION:
|
||||
animatedNodesManager.stopAnimation(opsAndArgs.getInt(i++));
|
||||
break;
|
||||
case OP_CODE_SET_ANIMATED_NODE_VALUE:
|
||||
animatedNodesManager.setAnimatedNodeValue(
|
||||
opsAndArgs.getInt(i++), opsAndArgs.getDouble(i++));
|
||||
break;
|
||||
case OP_CODE_SET_ANIMATED_NODE_OFFSET:
|
||||
animatedNodesManager.setAnimatedNodeValue(
|
||||
opsAndArgs.getInt(i++), opsAndArgs.getDouble(i++));
|
||||
break;
|
||||
case OP_CODE_FLATTEN_ANIMATED_NODE_OFFSET:
|
||||
animatedNodesManager.flattenAnimatedNodeOffset(opsAndArgs.getInt(i++));
|
||||
break;
|
||||
case OP_CODE_EXTRACT_ANIMATED_NODE_OFFSET:
|
||||
animatedNodesManager.extractAnimatedNodeOffset(opsAndArgs.getInt(i++));
|
||||
break;
|
||||
case OP_CODE_CONNECT_ANIMATED_NODE_TO_VIEW:
|
||||
animatedNodesManager.connectAnimatedNodeToView(
|
||||
opsAndArgs.getInt(i++), opsAndArgs.getInt(i++));
|
||||
break;
|
||||
case OP_CODE_DISCONNECT_ANIMATED_NODE_FROM_VIEW:
|
||||
int animatedNodeTag = opsAndArgs.getInt(i++);
|
||||
viewTag = opsAndArgs.getInt(i++);
|
||||
decrementInFlightAnimationsForViewTag(viewTag);
|
||||
animatedNodesManager.disconnectAnimatedNodeFromView(animatedNodeTag, viewTag);
|
||||
break;
|
||||
case OP_CODE_RESTORE_DEFAULT_VALUES:
|
||||
animatedNodesManager.restoreDefaultValues(opsAndArgs.getInt(i++));
|
||||
break;
|
||||
case OP_CODE_DROP_ANIMATED_NODE:
|
||||
animatedNodesManager.dropAnimatedNode(opsAndArgs.getInt(i++));
|
||||
break;
|
||||
case OP_CODE_ADD_ANIMATED_EVENT_TO_VIEW:
|
||||
animatedNodesManager.addAnimatedEventToView(
|
||||
opsAndArgs.getInt(i++), opsAndArgs.getString(i++), opsAndArgs.getMap(i++));
|
||||
break;
|
||||
case OP_CODE_REMOVE_ANIMATED_EVENT_FROM_VIEW:
|
||||
viewTag = opsAndArgs.getInt(i++);
|
||||
decrementInFlightAnimationsForViewTag(viewTag);
|
||||
animatedNodesManager.dropAnimatedNode(viewTag);
|
||||
break;
|
||||
case OP_CODE_ADD_LISTENER:
|
||||
case OP_CODE_REMOVE_LISTENERS:
|
||||
i++;
|
||||
// ios only, do nothing on android besides incrementing the arg counter
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Batch animation execution op: unknown op code");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
finishOperationBatch();
|
||||
}
|
||||
}
|
||||
|
||||
+53
-1
@@ -23,6 +23,7 @@ import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.UIManager;
|
||||
import com.facebook.react.bridge.UiThreadUtil;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
import com.facebook.react.uimanager.UIManagerHelper;
|
||||
import com.facebook.react.uimanager.common.UIManagerType;
|
||||
import com.facebook.react.uimanager.events.Event;
|
||||
@@ -302,6 +303,16 @@ import java.util.Queue;
|
||||
WritableMap endCallbackResponse = Arguments.createMap();
|
||||
endCallbackResponse.putBoolean("finished", false);
|
||||
animation.mEndCallback.invoke(endCallbackResponse);
|
||||
} else if (mReactApplicationContext != null) {
|
||||
// If no callback is passed in, this /may/ be an animation set up by the single-op
|
||||
// instruction from JS, meaning that no jsi::functions are passed into native and
|
||||
// we communicate via RCTDeviceEventEmitter instead of callbacks.
|
||||
WritableMap params = Arguments.createMap();
|
||||
params.putInt("animationId", animation.mId);
|
||||
params.putBoolean("finished", false);
|
||||
mReactApplicationContext
|
||||
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
.emit("onNativeAnimatedModuleAnimationFinished", params);
|
||||
}
|
||||
mActiveAnimations.removeAt(i);
|
||||
i--;
|
||||
@@ -323,6 +334,16 @@ import java.util.Queue;
|
||||
WritableMap endCallbackResponse = Arguments.createMap();
|
||||
endCallbackResponse.putBoolean("finished", false);
|
||||
animation.mEndCallback.invoke(endCallbackResponse);
|
||||
} else if (mReactApplicationContext != null) {
|
||||
// If no callback is passed in, this /may/ be an animation set up by the single-op
|
||||
// instruction from JS, meaning that no jsi::functions are passed into native and
|
||||
// we communicate via RCTDeviceEventEmitter instead of callbacks.
|
||||
WritableMap params = Arguments.createMap();
|
||||
params.putInt("animationId", animation.mId);
|
||||
params.putBoolean("finished", false);
|
||||
mReactApplicationContext
|
||||
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
.emit("onNativeAnimatedModuleAnimationFinished", params);
|
||||
}
|
||||
mActiveAnimations.removeAt(i);
|
||||
return;
|
||||
@@ -439,7 +460,25 @@ import java.util.Queue;
|
||||
throw new JSApplicationIllegalArgumentException(
|
||||
"getValue: Animated node with tag [" + tag + "] does not exist or is not a 'value' node");
|
||||
}
|
||||
callback.invoke(((ValueAnimatedNode) node).getValue());
|
||||
double value = ((ValueAnimatedNode) node).getValue();
|
||||
if (callback != null) {
|
||||
callback.invoke(value);
|
||||
return;
|
||||
}
|
||||
|
||||
// If there's no callback, that means that JS is using the single-operation mode, and not
|
||||
// passing any callbacks into Java.
|
||||
// See NativeAnimatedHelper.js for details.
|
||||
// Instead, we use RCTDeviceEventEmitter to pass data back to JS and emulate callbacks.
|
||||
if (mReactApplicationContext == null) {
|
||||
return;
|
||||
}
|
||||
WritableMap params = Arguments.createMap();
|
||||
params.putInt("tag", tag);
|
||||
params.putDouble("value", value);
|
||||
mReactApplicationContext
|
||||
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
.emit("onNativeAnimatedModuleGetValue", params);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@@ -612,6 +651,19 @@ import java.util.Queue;
|
||||
WritableMap endCallbackResponse = Arguments.createMap();
|
||||
endCallbackResponse.putBoolean("finished", true);
|
||||
animation.mEndCallback.invoke(endCallbackResponse);
|
||||
} else if (mReactApplicationContext != null) {
|
||||
// If no callback is passed in, this /may/ be an animation set up by the single-op
|
||||
// instruction from JS, meaning that no jsi::functions are passed into native and
|
||||
// we communicate via RCTDeviceEventEmitter instead of callbacks.
|
||||
WritableMap params = Arguments.createMap();
|
||||
params.putInt("animationId", animation.mId);
|
||||
params.putBoolean("finished", true);
|
||||
DeviceEventManagerModule.RCTDeviceEventEmitter eventEmitter =
|
||||
mReactApplicationContext.getJSModule(
|
||||
DeviceEventManagerModule.RCTDeviceEventEmitter.class);
|
||||
if (eventEmitter != null) {
|
||||
eventEmitter.emit("onNativeAnimatedModuleAnimationFinished", params);
|
||||
}
|
||||
}
|
||||
mActiveAnimations.removeAt(i);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user