diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/DivisionAnimatedNode.java b/ReactAndroid/src/main/java/com/facebook/react/animated/DivisionAnimatedNode.java index 3c3b595d2fa..550073f7924 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/DivisionAnimatedNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/DivisionAnimatedNode.java @@ -42,12 +42,12 @@ import com.facebook.react.bridge.ReadableMap; } if (value == 0) { throw new JSApplicationCausedNativeException( - "Detected a division by zero in " + "Animated.divide node"); + "Detected a division by zero in Animated.divide node with Animated ID " + mTag); } mValue /= value; } else { throw new JSApplicationCausedNativeException( - "Illegal node ID set as an input for " + "Animated.divide node"); + "Illegal node ID set as an input for Animated.divide node with Animated ID " + mTag); } } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java index 1c05e52b92c..fc3c4efb895 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java @@ -10,6 +10,7 @@ package com.facebook.react.animated; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; +import com.facebook.common.logging.FLog; import com.facebook.fbreact.specs.NativeAnimatedModuleSpec; import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.Arguments; @@ -27,10 +28,13 @@ import com.facebook.react.modules.core.ReactChoreographer; import com.facebook.react.uimanager.GuardedFrameCallback; import com.facebook.react.uimanager.NativeViewHierarchyManager; import com.facebook.react.uimanager.UIBlock; +import com.facebook.react.uimanager.UIManagerHelper; import com.facebook.react.uimanager.UIManagerModule; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; +import com.facebook.react.uimanager.common.UIManagerType; +import com.facebook.react.uimanager.common.ViewUtil; +import java.util.LinkedList; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; /** * Module that exposes interface for creating and managing animated nodes on the "native" side. @@ -81,6 +85,7 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec implements LifecycleEventListener, UIManagerListener { public static final String NAME = "NativeAnimatedModule"; + public static final boolean ANIMATED_MODULE_DEBUG = false; private interface UIThreadOperation { void execute(NativeAnimatedNodesManager animatedNodesManager); @@ -88,14 +93,21 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec @NonNull private final GuardedFrameCallback mAnimatedFrameCallback; private final ReactChoreographer mReactChoreographer; - @NonNull private List mOperations = new ArrayList<>(); - @NonNull private List mPreOperations = new ArrayList<>(); - @NonNull private List mPreOperationsUIBlock = new ArrayList<>(); - @NonNull private List mOperationsUIBlock = new ArrayList<>(); + @NonNull + private ConcurrentLinkedQueue mOperations = new ConcurrentLinkedQueue<>(); + + @NonNull + private ConcurrentLinkedQueue mPreOperations = new ConcurrentLinkedQueue<>(); private @Nullable NativeAnimatedNodesManager mNodesManager; + private volatile boolean mFabricBatchCompleted = false; + private boolean mInitializedForFabric = false; + private @UIManagerType int mUIManagerType = UIManagerType.DEFAULT; + private int mNumFabricAnimations = 0; + private int mNumNonFabricAnimations = 0; + public NativeAnimatedModule(ReactApplicationContext reactContext) { super(reactContext); @@ -147,68 +159,69 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec enqueueFrameCallback(); } - // For FabricUIManager + // For FabricUIManager only + @Override + public void didScheduleMountItems(UIManager uiManager) { + if (mUIManagerType != UIManagerType.FABRIC) { + return; + } + + mFabricBatchCompleted = true; + } + + // For FabricUIManager only @Override @UiThread - public void willDispatchPreMountItems() { - if (mPreOperationsUIBlock.size() != 0) { - List preOperations = mPreOperationsUIBlock; - mPreOperationsUIBlock = new ArrayList<>(); + public void didDispatchMountItems(UIManager uiManager) { + if (mUIManagerType != UIManagerType.FABRIC) { + return; + } - for (UIBlock op : preOperations) { - op.execute(null); - } + if (mFabricBatchCompleted) { + // This will execute all operations and preOperations queued + // since the last time this was run, and will race with anything + // being queued from the JS thread. That is, if the JS thread + // is still queuing operations, we might execute some of them + // at the very end until we exhaust the queue faster than the + // JS thread can queue up new items. + executeAllOperations(mPreOperations); + executeAllOperations(mOperations); + mFabricBatchCompleted = false; } } - // For FabricUIManager - @Override - @UiThread - public void willDispatchMountItems() { - if (mOperationsUIBlock.size() != 0) { - List operations = mOperationsUIBlock; - mOperationsUIBlock = new ArrayList<>(); - - for (UIBlock op : operations) { - op.execute(null); - } + private void executeAllOperations(Queue operationQueue) { + NativeAnimatedNodesManager nodesManager = getNodesManager(); + while (!operationQueue.isEmpty()) { + operationQueue.poll().execute(nodesManager); } } - // For non-FabricUIManager + // For non-FabricUIManager only @Override @UiThread public void willDispatchViewUpdates(final UIManager uiManager) { if (mOperations.isEmpty() && mPreOperations.isEmpty()) { return; } + if (mUIManagerType == UIManagerType.FABRIC) { + return; + } - final AtomicBoolean hasRunPreOperations = new AtomicBoolean(false); - final AtomicBoolean hasRunOperations = new AtomicBoolean(false); - final List preOperations = mPreOperations; - final List operations = mOperations; - mPreOperations = new ArrayList<>(); - mOperations = new ArrayList<>(); - - // This is kind of a hack. Basically UIManagerListener cannot import UIManagerModule - // (that would cause an import cycle) and they're not in the same package. But, - // UIManagerModule is the only thing that calls `willDispatchViewUpdates` so we - // know this is safe. - // This goes away entirely in Fabric/Venice. - UIManagerModule uiManagerModule = (UIManagerModule) uiManager; + final Queue preOperations = new LinkedList<>(); + final Queue operations = new LinkedList<>(); + while (!mPreOperations.isEmpty()) { + preOperations.add(mPreOperations.poll()); + } + while (!mOperations.isEmpty()) { + operations.add(mOperations.poll()); + } UIBlock preOperationsUIBlock = new UIBlock() { @Override public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) { - if (!hasRunPreOperations.compareAndSet(false, true)) { - return; - } - - NativeAnimatedNodesManager nodesManager = getNodesManager(); - for (UIThreadOperation operation : preOperations) { - operation.execute(nodesManager); - } + executeAllOperations(preOperations); } }; @@ -216,23 +229,13 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec new UIBlock() { @Override public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) { - if (!hasRunOperations.compareAndSet(false, true)) { - return; - } - NativeAnimatedNodesManager nodesManager = getNodesManager(); - for (UIThreadOperation operation : operations) { - operation.execute(nodesManager); - } + executeAllOperations(operations); } }; - // Queue up operations for Fabric - mPreOperationsUIBlock.add(preOperationsUIBlock); - mOperationsUIBlock.add(operationsUIBlock); - - // Here we queue up the UI Blocks for the old, non-Fabric UIManager. - // We queue them in both systems, let them race, and see which wins. + assert (uiManager instanceof UIManagerModule); + UIManagerModule uiManagerModule = (UIManagerModule) uiManager; uiManagerModule.prependUIBlock(preOperationsUIBlock); uiManagerModule.addUIBlock(operationsUIBlock); } @@ -265,10 +268,7 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec ReactApplicationContext reactApplicationContext = getReactApplicationContextIfActiveOrWarn(); if (reactApplicationContext != null) { - UIManagerModule uiManager = - Assertions.assertNotNull( - reactApplicationContext.getNativeModule(UIManagerModule.class)); - mNodesManager = new NativeAnimatedNodesManager(uiManager); + mNodesManager = new NativeAnimatedNodesManager(reactApplicationContext); } } @@ -295,11 +295,23 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec @Override public void createAnimatedNode(final double tagDouble, final ReadableMap config) { final int tag = (int) tagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, "queue createAnimatedNode: " + tag + " config: " + config.toHashMap().toString()); + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "execute createAnimatedNode: " + + tag + + " config: " + + config.toHashMap().toString()); + } animatedNodesManager.createAnimatedNode(tag, config); } }); @@ -308,6 +320,9 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec @Override public void startListeningToAnimatedNodeValue(final double tagDouble) { final int tag = (int) tagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "queue startListeningToAnimatedNodeValue: " + tag); + } final AnimatedNodeValueListener listener = new AnimatedNodeValueListener() { @@ -330,6 +345,9 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "execute startListeningToAnimatedNodeValue: " + tag); + } animatedNodesManager.startListeningToAnimatedNodeValue(tag, listener); } }); @@ -338,11 +356,17 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec @Override public void stopListeningToAnimatedNodeValue(final double tagDouble) { final int tag = (int) tagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "queue stopListeningToAnimatedNodeValue: " + tag); + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "execute stopListeningToAnimatedNodeValue: " + tag); + } animatedNodesManager.stopListeningToAnimatedNodeValue(tag); } }); @@ -351,11 +375,17 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec @Override public void dropAnimatedNode(final double tagDouble) { final int tag = (int) tagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "queue dropAnimatedNode: " + tag); + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "execute dropAnimatedNode: " + tag); + } animatedNodesManager.dropAnimatedNode(tag); } }); @@ -364,11 +394,17 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec @Override public void setAnimatedNodeValue(final double tagDouble, final double value) { final int tag = (int) tagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "queue setAnimatedNodeValue: " + tag + " value: " + value); + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "execute setAnimatedNodeValue: " + tag + " value: " + value); + } animatedNodesManager.setAnimatedNodeValue(tag, value); } }); @@ -377,11 +413,17 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec @Override public void setAnimatedNodeOffset(final double tagDouble, final double value) { final int tag = (int) tagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "queue setAnimatedNodeOffset: " + tag + " offset: " + value); + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "execute setAnimatedNodeOffset: " + tag + " offset: " + value); + } animatedNodesManager.setAnimatedNodeOffset(tag, value); } }); @@ -390,11 +432,17 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec @Override public void flattenAnimatedNodeOffset(final double tagDouble) { final int tag = (int) tagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "queue flattenAnimatedNodeOffset: " + tag); + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "execute flattenAnimatedNodeOffset: " + tag); + } animatedNodesManager.flattenAnimatedNodeOffset(tag); } }); @@ -403,11 +451,17 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec @Override public void extractAnimatedNodeOffset(final double tagDouble) { final int tag = (int) tagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "queue extractAnimatedNodeOffset: " + tag); + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "execute extractAnimatedNodeOffset: " + tag); + } animatedNodesManager.extractAnimatedNodeOffset(tag); } }); @@ -421,11 +475,19 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec final Callback endCallback) { final int animationId = (int) animationIdDouble; final int animatedNodeTag = (int) animatedNodeTagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "queue startAnimatingNode: ID: " + animationId + " tag: " + animatedNodeTag); + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "execute startAnimatingNode: ID: " + animationId + " tag: " + animatedNodeTag); + } animatedNodesManager.startAnimatingNode( animationId, animatedNodeTag, animationConfig, endCallback); } @@ -435,11 +497,17 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec @Override public void stopAnimation(final double animationIdDouble) { final int animationId = (int) animationIdDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "queue stopAnimation: ID: " + animationId); + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "execute stopAnimation: ID: " + animationId); + } animatedNodesManager.stopAnimation(animationId); } }); @@ -450,11 +518,23 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec final double parentNodeTagDouble, final double childNodeTagDouble) { final int parentNodeTag = (int) parentNodeTagDouble; final int childNodeTag = (int) childNodeTagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, "queue connectAnimatedNodes: parent: " + parentNodeTag + " child: " + childNodeTag); + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "execute connectAnimatedNodes: parent: " + + parentNodeTag + + " child: " + + childNodeTag); + } animatedNodesManager.connectAnimatedNodes(parentNodeTag, childNodeTag); } }); @@ -465,11 +545,24 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec final double parentNodeTagDouble, final double childNodeTagDouble) { final int parentNodeTag = (int) parentNodeTagDouble; final int childNodeTag = (int) childNodeTagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "queue disconnectAnimatedNodes: parent: " + parentNodeTag + " child: " + childNodeTag); + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "execute disconnectAnimatedNodes: parent: " + + parentNodeTag + + " child: " + + childNodeTag); + } animatedNodesManager.disconnectAnimatedNodes(parentNodeTag, childNodeTag); } }); @@ -480,11 +573,27 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec final double animatedNodeTagDouble, final double viewTagDouble) { final int animatedNodeTag = (int) animatedNodeTagDouble; final int viewTag = (int) viewTagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "queue connectAnimatedNodeToView: animatedNodeTag: " + + animatedNodeTag + + " viewTag: " + + viewTag); + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "execute connectAnimatedNodeToView: animatedNodeTag: " + + animatedNodeTag + + " viewTag: " + + viewTag); + } animatedNodesManager.connectAnimatedNodeToView(animatedNodeTag, viewTag); } }); @@ -495,11 +604,27 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec final double animatedNodeTagDouble, final double viewTagDouble) { final int animatedNodeTag = (int) animatedNodeTagDouble; final int viewTag = (int) viewTagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "queue connectAnimatedNodeToView: disconnectAnimatedNodeFromView: " + + animatedNodeTag + + " viewTag: " + + viewTag); + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "execute connectAnimatedNodeToView: disconnectAnimatedNodeFromView: " + + animatedNodeTag + + " viewTag: " + + viewTag); + } animatedNodesManager.disconnectAnimatedNodeFromView(animatedNodeTag, viewTag); } }); @@ -508,11 +633,21 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec @Override public void restoreDefaultValues(final double animatedNodeTagDouble) { final int animatedNodeTag = (int) animatedNodeTagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, "queue restoreDefaultValues: disconnectAnimatedNodeFromView: " + animatedNodeTag); + } mPreOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "execute restoreDefaultValues: disconnectAnimatedNodeFromView: " + + animatedNodeTag); + } animatedNodesManager.restoreDefaultValues(animatedNodeTag); } }); @@ -522,11 +657,52 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec public void addAnimatedEventToView( final double viewTagDouble, final String eventName, final ReadableMap eventMapping) { final int viewTag = (int) viewTagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "queue addAnimatedEventToView: " + + viewTag + + " eventName: " + + eventName + + " eventMapping: " + + eventMapping.toHashMap().toString()); + } + + mUIManagerType = ViewUtil.getUIManagerType(viewTag); + if (mUIManagerType == UIManagerType.FABRIC) { + mNumFabricAnimations++; + } else { + mNumNonFabricAnimations++; + } + + // Subscribe to FabricUIManager lifecycle events if we haven't yet + if (!mInitializedForFabric && mUIManagerType == UIManagerType.FABRIC) { + ReactApplicationContext reactApplicationContext = getReactApplicationContext(); + if (reactApplicationContext != null) { + @Nullable + UIManager uiManager = + UIManagerHelper.getUIManager(reactApplicationContext, UIManagerType.FABRIC); + if (uiManager != null) { + uiManager.addUIManagerEventListener(this); + mInitializedForFabric = true; + } + } + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "execute addAnimatedEventToView: " + + viewTag + + " eventName: " + + eventName + + " eventMapping: " + + eventMapping.toHashMap().toString()); + } animatedNodesManager.addAnimatedEventToView(viewTag, eventName, eventMapping); } }); @@ -537,11 +713,53 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec final double viewTagDouble, final String eventName, final double animatedValueTagDouble) { final int viewTag = (int) viewTagDouble; final int animatedValueTag = (int) animatedValueTagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "queue addAnimatedEventToView: removeAnimatedEventFromView: " + + viewTag + + " eventName: " + + eventName + + " animatedValueTag: " + + animatedValueTag); + } + + @UIManagerType int animationManagerType = ViewUtil.getUIManagerType(viewTag); + if (animationManagerType == UIManagerType.FABRIC) { + mNumFabricAnimations--; + } else { + mNumNonFabricAnimations--; + } + + // Should we switch to a different animation mode? + // This can be useful when navigating between Fabric and non-Fabric screens: + // If there are ongoing Fabric animations from a previous screen, + // and we tear down the current non-Fabric screen, we should expect + // the animation mode to switch back - and vice-versa. + if (mNumNonFabricAnimations == 0 + && mNumFabricAnimations > 0 + && mUIManagerType != UIManagerType.FABRIC) { + mUIManagerType = UIManagerType.FABRIC; + } else if (mNumFabricAnimations == 0 + && mNumNonFabricAnimations > 0 + && mUIManagerType != UIManagerType.DEFAULT) { + mUIManagerType = UIManagerType.DEFAULT; + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "execute addAnimatedEventToView: removeAnimatedEventFromView: " + + viewTag + + " eventName: " + + eventName + + " animatedValueTag: " + + animatedValueTag); + } animatedNodesManager.removeAnimatedEventFromView(viewTag, eventName, animatedValueTag); } }); diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java index 2c99f40d655..983d1bdde53 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java @@ -10,16 +10,19 @@ package com.facebook.react.animated; import android.util.SparseArray; import androidx.annotation.Nullable; import com.facebook.common.logging.FLog; +import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.JSApplicationCausedNativeException; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReadableArray; 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.common.ReactConstants; import com.facebook.react.uimanager.IllegalViewOperationException; +import com.facebook.react.uimanager.UIManagerHelper; import com.facebook.react.uimanager.UIManagerModule; import com.facebook.react.uimanager.events.Event; import com.facebook.react.uimanager.events.EventDispatcher; @@ -48,6 +51,9 @@ import java.util.Queue; */ /*package*/ class NativeAnimatedNodesManager implements EventDispatcherListener { + private static final String TAG = "NativeAnimatedNodesManager"; + private static final int MAX_INCONSISTENT_FRAMES = 64; + private final SparseArray mAnimatedNodes = new SparseArray<>(); private final SparseArray mActiveAnimations = new SparseArray<>(); private final SparseArray mUpdatedNodes = new SparseArray<>(); @@ -55,14 +61,19 @@ import java.util.Queue; // there will be only one driver per mapping so all code code should be optimized around that. private final Map> mEventDrivers = new HashMap<>(); private final UIManagerModule.CustomEventNamesResolver mCustomEventNamesResolver; - private final UIManager mUIManager; + private final ReactApplicationContext mReactApplicationContext; private int mAnimatedGraphBFSColor = 0; + private int mNumInconsistentFrames = 0; // Used to avoid allocating a new array on every frame in `runUpdates` and `onEventDispatch`. private final List mRunUpdateNodeList = new LinkedList<>(); - public NativeAnimatedNodesManager(UIManagerModule uiManager) { - mUIManager = uiManager; - mUIManager.getEventDispatcher().addListener(this); + public NativeAnimatedNodesManager(ReactApplicationContext reactApplicationContext) { + mReactApplicationContext = reactApplicationContext; + + UIManagerModule uiManager = + Assertions.assertNotNull(reactApplicationContext.getNativeModule(UIManagerModule.class)); + + uiManager.getEventDispatcher().addListener(this); // TODO T64216139 Remove dependency of UIManagerModule when the Constants are not in Native // anymore mCustomEventNamesResolver = uiManager.getDirectEventNamesResolver(); @@ -89,7 +100,7 @@ import java.util.Queue; } else if ("value".equals(type)) { node = new ValueAnimatedNode(config); } else if ("props".equals(type)) { - node = new PropsAnimatedNode(config, this, mUIManager); + node = new PropsAnimatedNode(config, this); } else if ("interpolation".equals(type)) { node = new InterpolationAnimatedNode(config); } else if ("addition".equals(type)) { @@ -301,8 +312,21 @@ import java.util.Queue; + "of type " + PropsAnimatedNode.class.getName()); } + if (mReactApplicationContext == null) { + throw new IllegalStateException( + "Animated node could not be connected, no ReactApplicationContext: " + viewTag); + } + + @Nullable + UIManager uiManager = + UIManagerHelper.getUIManagerForReactTag(mReactApplicationContext, viewTag); + if (uiManager == null) { + throw new IllegalStateException( + "Animated node could not be connected to UIManager: " + viewTag); + } + PropsAnimatedNode propsAnimatedNode = (PropsAnimatedNode) node; - propsAnimatedNode.connectToView(viewTag); + propsAnimatedNode.connectToView(viewTag, uiManager); mUpdatedNodes.put(animatedNodeTag, node); } @@ -543,26 +567,43 @@ import java.util.Queue; } // Run main "update" loop + boolean errorsCaught = false; while (!nodesQueue.isEmpty()) { AnimatedNode nextNode = nodesQueue.poll(); - nextNode.update(); - if (nextNode instanceof PropsAnimatedNode) { - // Send property updates to native view manager - try { + try { + nextNode.update(); + if (nextNode instanceof PropsAnimatedNode) { + // Send property updates to native view manager ((PropsAnimatedNode) nextNode).updateView(); - } catch (IllegalViewOperationException e) { - // An exception is thrown if the view hasn't been created yet. This can happen because - // views are - // created in batches. If this particular view didn't make it into a batch yet, the view - // won't - // exist and an exception will be thrown when attempting to start an animation on it. - // - // Eat the exception rather than crashing. The impact is that we may drop one or more - // frames of the - // animation. + } + } catch (IllegalViewOperationException e) { + // An exception is thrown if the view hasn't been created yet. This can happen because + // views are + // created in batches. If this particular view didn't make it into a batch yet, the view + // won't + // exist and an exception will be thrown when attempting to start an animation on it. + // + // Eat the exception rather than crashing. The impact is that we may drop one or more + // frames of the + // animation. + FLog.e(TAG, "Native animation workaround, frame lost as result of race condition", e); + } catch (JSApplicationCausedNativeException e) { + // In Fabric there can be race conditions between the JS thread setting up or tearing down + // animated nodes, and Fabric executing them on the UI thread, leading to temporary + // inconsistent + // states. We require that the inconsistency last for N frames before throwing these + // exceptions. + if (!errorsCaught) { + errorsCaught = true; + mNumInconsistentFrames++; + } + if (mNumInconsistentFrames > MAX_INCONSISTENT_FRAMES) { + throw new IllegalStateException(e); + } else { FLog.e( - ReactConstants.TAG, - "Native animation workaround, frame lost as result of race condition", + TAG, + "Swallowing exception due to potential race between JS and UI threads: inconsistent frame counter: " + + mNumInconsistentFrames, e); } } @@ -586,13 +627,23 @@ import java.util.Queue; // Verify that we've visited *all* active nodes. Throw otherwise as this would mean there is a // cycle in animated node graph. We also take advantage of the fact that all active nodes are // visited in the step above so that all the nodes properties `mActiveIncomingNodes` are set to - // zero + // zero. + // In Fabric there can be race conditions between the JS thread setting up or tearing down + // animated nodes, and Fabric executing them on the UI thread, leading to temporary inconsistent + // states. We require that the inconsistency last for 64 frames before throwing this exception. if (activeNodesCount != updatedNodesCount) { - throw new IllegalStateException( - "Looks like animated nodes graph has cycles, there are " - + activeNodesCount - + " but toposort visited only " - + updatedNodesCount); + if (!errorsCaught) { + mNumInconsistentFrames++; + } + if (mNumInconsistentFrames > MAX_INCONSISTENT_FRAMES) { + throw new IllegalStateException( + "Looks like animated nodes graph has cycles, there are " + + activeNodesCount + + " but toposort visited only " + + updatedNodesCount); + } + } else if (!errorsCaught) { + mNumInconsistentFrames = 0; } } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java b/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java index 2eea21b4954..4a0293e982b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java @@ -25,14 +25,11 @@ import java.util.Map; private int mConnectedViewTag = -1; private final NativeAnimatedNodesManager mNativeAnimatedNodesManager; - private final UIManager mUIManager; private final Map mPropNodeMapping; private final JavaOnlyMap mPropMap; + @Nullable private UIManager mUIManager; - PropsAnimatedNode( - ReadableMap config, - NativeAnimatedNodesManager nativeAnimatedNodesManager, - UIManager uiManager) { + PropsAnimatedNode(ReadableMap config, NativeAnimatedNodesManager nativeAnimatedNodesManager) { ReadableMap props = config.getMap("props"); ReadableMapKeySetIterator iter = props.keySetIterator(); mPropNodeMapping = new HashMap<>(); @@ -43,15 +40,15 @@ import java.util.Map; } mPropMap = new JavaOnlyMap(); mNativeAnimatedNodesManager = nativeAnimatedNodesManager; - mUIManager = uiManager; } - public void connectToView(int viewTag) { + public void connectToView(int viewTag, UIManager uiManager) { if (mConnectedViewTag != -1) { throw new JSApplicationIllegalArgumentException( "Animated node " + mTag + " is " + "already attached to a view"); } mConnectedViewTag = viewTag; + mUIManager = uiManager; } public void disconnectFromView(int viewTag) { @@ -65,6 +62,11 @@ import java.util.Map; } public void restoreDefaultValues() { + // Cannot restore default values if this view has already been disconnected. + if (mConnectedViewTag == -1) { + return; + } + ReadableMapKeySetIterator it = mPropMap.keySetIterator(); while (it.hasNextKey()) { mPropMap.putNull(it.nextKey()); diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManagerListener.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManagerListener.java index 216d38b00d6..c190e0a8c02 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManagerListener.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManagerListener.java @@ -14,8 +14,8 @@ public interface UIManagerListener { * module needs to add UIBlocks to the queue before it is flushed. */ void willDispatchViewUpdates(UIManager uiManager); - /** Called on the UI thread right before normal mount items are executed. */ - void willDispatchMountItems(); - /** Called on the UI thread right before premount items are executed. */ - void willDispatchPreMountItems(); + /* Called right after view updates are dispatched for a frame. */ + void didDispatchMountItems(UIManager uiManager); + /* Called right after scheduleMountItems is called in Fabric, after a new tree is committed. */ + void didScheduleMountItems(UIManager uiManager); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java index e644fe69e3c..a238799e185 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -512,7 +512,11 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { ReactMarker.logFabricMarker( ReactMarkerConstants.FABRIC_UPDATE_UI_MAIN_THREAD_START, null, commitNumber); if (ENABLE_FABRIC_LOGS) { - FLog.d(TAG, "SynchronouslyUpdateViewOnUIThread for tag %d", reactTag); + FLog.d( + TAG, + "SynchronouslyUpdateViewOnUIThread for tag %d: %s", + reactTag, + (IS_DEVELOPMENT_ENVIRONMENT ? props.toHashMap().toString() : "")); } updatePropsMountItem(reactTag, props).execute(mMountingManager); @@ -617,10 +621,6 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { return; } - for (UIManagerListener listener : mListeners) { - listener.willDispatchMountItems(); - } - final boolean didDispatchItems; try { didDispatchItems = dispatchMountItems(); @@ -632,6 +632,10 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { mInDispatch = false; } + for (UIManagerListener listener : mListeners) { + listener.didDispatchMountItems(this); + } + // Decide if we want to try reentering if (mReDispatchCounter < 10 && didDispatchItems) { // Executing twice in a row is normal. Only log after that point. @@ -880,10 +884,6 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { // reentering during dispatchPreMountItems mInDispatch = true; - for (UIManagerListener listener : mListeners) { - listener.willDispatchPreMountItems(); - } - try { while (true) { long timeLeftInFrame = FRAME_TIME_MS - ((System.nanoTime() - frameTimeNanos) / 1000000); diff --git a/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java b/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java index 0a854969495..4c675ee66eb 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java @@ -20,8 +20,11 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.JSIModuleType; import com.facebook.react.bridge.JavaOnlyArray; import com.facebook.react.bridge.JavaOnlyMap; +import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.common.MapBuilder; import com.facebook.react.uimanager.UIManagerModule; @@ -54,6 +57,8 @@ public class NativeAnimatedNodeTraversalTest { @Rule public PowerMockRule rule = new PowerMockRule(); private long mFrameTimeNanos; + private ReactApplicationContext mReactApplicationContextMock; + private CatalystInstance mCatalystInstanceMock; private UIManagerModule mUIManagerMock; private EventDispatcher mEventDispatcherMock; private NativeAnimatedNodesManager mNativeAnimatedNodesManager; @@ -83,6 +88,59 @@ public class NativeAnimatedNodeTraversalTest { }); mFrameTimeNanos = INITIAL_FRAME_TIME_NANOS; + + mReactApplicationContextMock = mock(ReactApplicationContext.class); + PowerMockito.when(mReactApplicationContextMock.hasActiveCatalystInstance()) + .thenAnswer( + new Answer() { + @Override + public Boolean answer(InvocationOnMock invocationOnMock) throws Throwable { + return true; + } + }); + PowerMockito.when(mReactApplicationContextMock.hasCatalystInstance()) + .thenAnswer( + new Answer() { + @Override + public Boolean answer(InvocationOnMock invocationOnMock) throws Throwable { + return true; + } + }); + PowerMockito.when(mReactApplicationContextMock.getCatalystInstance()) + .thenAnswer( + new Answer() { + @Override + public CatalystInstance answer(InvocationOnMock invocationOnMock) throws Throwable { + return mCatalystInstanceMock; + } + }); + PowerMockito.when(mReactApplicationContextMock.getNativeModule(any(Class.class))) + .thenAnswer( + new Answer() { + @Override + public UIManagerModule answer(InvocationOnMock invocationOnMock) throws Throwable { + return mUIManagerMock; + } + }); + + mCatalystInstanceMock = mock(CatalystInstance.class); + PowerMockito.when(mCatalystInstanceMock.getJSIModule(any(JSIModuleType.class))) + .thenAnswer( + new Answer() { + @Override + public UIManagerModule answer(InvocationOnMock invocationOnMock) throws Throwable { + return mUIManagerMock; + } + }); + PowerMockito.when(mCatalystInstanceMock.getNativeModule(any(Class.class))) + .thenAnswer( + new Answer() { + @Override + public UIManagerModule answer(InvocationOnMock invocationOnMock) throws Throwable { + return mUIManagerMock; + } + }); + mUIManagerMock = mock(UIManagerModule.class); mEventDispatcherMock = mock(EventDispatcher.class); PowerMockito.when(mUIManagerMock.getEventDispatcher()) @@ -125,7 +183,7 @@ public class NativeAnimatedNodeTraversalTest { }; } }); - mNativeAnimatedNodesManager = new NativeAnimatedNodesManager(mUIManagerMock); + mNativeAnimatedNodesManager = new NativeAnimatedNodesManager(mReactApplicationContextMock); } /** @@ -931,7 +989,7 @@ public class NativeAnimatedNodeTraversalTest { MapBuilder.of("topScroll", MapBuilder.of("registrationName", "onScroll"))); } }); - mNativeAnimatedNodesManager = new NativeAnimatedNodesManager(mUIManagerMock); + mNativeAnimatedNodesManager = new NativeAnimatedNodesManager(mReactApplicationContextMock); createSimpleAnimatedViewWithOpacity(viewTag, 0d);