Skip to content

Commit 41fb336

Browse files
JoshuaGrossfacebook-github-bot
authored andcommitted
NativeAnimated: Fabric constructs partial animation graphs often; warn instead of crashing
Summary: Due to subtle differences in lifecycle on the native side, as well as in JS, Fabric constructs partial graphs more frequently than non-Fabric RN did. We still crash if we detect a cycle, which we check for more explicitly now; and we still always crash in non-Fabric. But if we detect a partial graph in Fabric, we warn instead of crashing. We also print the state of the graph before crashing/warning, to assist in debugging in production. Changelog: [Internal] Reviewed By: shergin Differential Revision: D22752291 fbshipit-source-id: f452892678fbe7b5a49f93644d39d3b6ae5bda75
1 parent ab5e87f commit 41fb336

File tree

1 file changed

+46
-45
lines changed

1 file changed

+46
-45
lines changed

ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java

Lines changed: 46 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import com.facebook.react.bridge.UIManager;
2424
import com.facebook.react.bridge.UiThreadUtil;
2525
import com.facebook.react.bridge.WritableMap;
26-
import com.facebook.react.uimanager.IllegalViewOperationException;
2726
import com.facebook.react.uimanager.UIManagerHelper;
2827
import com.facebook.react.uimanager.common.UIManagerType;
2928
import com.facebook.react.uimanager.events.Event;
@@ -54,7 +53,6 @@
5453
/*package*/ class NativeAnimatedNodesManager implements EventDispatcherListener {
5554

5655
private static final String TAG = "NativeAnimatedNodesManager";
57-
private static final int MAX_INCONSISTENT_FRAMES = 64;
5856

5957
private final SparseArray<AnimatedNode> mAnimatedNodes = new SparseArray<>();
6058
private final SparseArray<AnimationDriver> mActiveAnimations = new SparseArray<>();
@@ -64,13 +62,14 @@
6462
private final Map<String, List<EventAnimationDriver>> mEventDrivers = new HashMap<>();
6563
private final ReactApplicationContext mReactApplicationContext;
6664
private int mAnimatedGraphBFSColor = 0;
67-
private int mNumInconsistentFrames = 0;
6865
// Used to avoid allocating a new array on every frame in `runUpdates` and `onEventDispatch`.
6966
private final List<AnimatedNode> mRunUpdateNodeList = new LinkedList<>();
7067

7168
private boolean mEventListenerInitializedForFabric = false;
7269
private boolean mEventListenerInitializedForNonFabric = false;
7370

71+
private boolean mWarnedAboutGraphTraversal = false;
72+
7473
public NativeAnimatedNodesManager(ReactApplicationContext reactApplicationContext) {
7574
mReactApplicationContext = reactApplicationContext;
7675
}
@@ -646,7 +645,7 @@ private void updateNodes(List<AnimatedNode> nodes) {
646645
}
647646

648647
// Run main "update" loop
649-
boolean errorsCaught = false;
648+
int cyclesDetected = 0;
650649
while (!nodesQueue.isEmpty()) {
651650
AnimatedNode nextNode = nodesQueue.poll();
652651
try {
@@ -655,36 +654,15 @@ private void updateNodes(List<AnimatedNode> nodes) {
655654
// Send property updates to native view manager
656655
((PropsAnimatedNode) nextNode).updateView();
657656
}
658-
} catch (IllegalViewOperationException e) {
657+
} catch (JSApplicationCausedNativeException e) {
659658
// An exception is thrown if the view hasn't been created yet. This can happen because
660-
// views are
661-
// created in batches. If this particular view didn't make it into a batch yet, the view
662-
// won't
663-
// exist and an exception will be thrown when attempting to start an animation on it.
659+
// views are created in batches. If this particular view didn't make it into a batch yet,
660+
// the view won't exist and an exception will be thrown when attempting to start an
661+
// animation on it.
664662
//
665663
// Eat the exception rather than crashing. The impact is that we may drop one or more
666-
// frames of the
667-
// animation.
664+
// frames of the animation.
668665
FLog.e(TAG, "Native animation workaround, frame lost as result of race condition", e);
669-
} catch (JSApplicationCausedNativeException e) {
670-
// In Fabric there can be race conditions between the JS thread setting up or tearing down
671-
// animated nodes, and Fabric executing them on the UI thread, leading to temporary
672-
// inconsistent
673-
// states. We require that the inconsistency last for N frames before throwing these
674-
// exceptions.
675-
if (!errorsCaught) {
676-
errorsCaught = true;
677-
mNumInconsistentFrames++;
678-
}
679-
if (mNumInconsistentFrames > MAX_INCONSISTENT_FRAMES) {
680-
throw new IllegalStateException(e);
681-
} else {
682-
FLog.e(
683-
TAG,
684-
"Swallowing exception due to potential race between JS and UI threads: inconsistent frame counter: "
685-
+ mNumInconsistentFrames,
686-
e);
687-
}
688666
}
689667
if (nextNode instanceof ValueAnimatedNode) {
690668
// Potentially send events to JS when the node's value is updated
@@ -698,31 +676,54 @@ private void updateNodes(List<AnimatedNode> nodes) {
698676
child.mBFSColor = mAnimatedGraphBFSColor;
699677
updatedNodesCount++;
700678
nodesQueue.add(child);
679+
} else if (child.mBFSColor == mAnimatedGraphBFSColor) {
680+
cyclesDetected++;
701681
}
702682
}
703683
}
704684
}
705685

706-
// Verify that we've visited *all* active nodes. Throw otherwise as this would mean there is a
707-
// cycle in animated node graph. We also take advantage of the fact that all active nodes are
708-
// visited in the step above so that all the nodes properties `mActiveIncomingNodes` are set to
709-
// zero.
686+
// Verify that we've visited *all* active nodes. Throw otherwise as this could mean there is a
687+
// cycle in animated node graph, or that the graph is only partially set up. We also take
688+
// advantage of the fact that all active nodes are visited in the step above so that all the
689+
// nodes properties `mActiveIncomingNodes` are set to zero.
710690
// In Fabric there can be race conditions between the JS thread setting up or tearing down
711691
// animated nodes, and Fabric executing them on the UI thread, leading to temporary inconsistent
712-
// states. We require that the inconsistency last for 64 frames before throwing this exception.
692+
// states.
713693
if (activeNodesCount != updatedNodesCount) {
714-
if (!errorsCaught) {
715-
mNumInconsistentFrames++;
694+
if (mWarnedAboutGraphTraversal) {
695+
return;
716696
}
717-
if (mNumInconsistentFrames > MAX_INCONSISTENT_FRAMES) {
718-
throw new IllegalStateException(
719-
"Looks like animated nodes graph has cycles, there are "
720-
+ activeNodesCount
721-
+ " but toposort visited only "
722-
+ updatedNodesCount);
697+
mWarnedAboutGraphTraversal = true;
698+
699+
// Before crashing or logging soft exception, log details about current graph setup
700+
FLog.e(TAG, "Detected animation cycle or disconnected graph. ");
701+
for (AnimatedNode node : nodes) {
702+
FLog.e(TAG, node.prettyPrintWithChildren());
723703
}
724-
} else if (!errorsCaught) {
725-
mNumInconsistentFrames = 0;
704+
705+
// If we're running only in non-Fabric, we still throw an exception.
706+
// In Fabric, it seems that animations enter an inconsistent state fairly often.
707+
// We detect if the inconsistency is due to a cycle (a fatal error for which we must crash)
708+
// or disconnected regions, indicating a partially-set-up animation graph, which is not
709+
// fatal and can stay a warning.
710+
String reason =
711+
cyclesDetected > 0 ? "cycles (" + cyclesDetected + ")" : "disconnected regions";
712+
IllegalStateException ex =
713+
new IllegalStateException(
714+
"Looks like animated nodes graph has "
715+
+ reason
716+
+ ", there are "
717+
+ activeNodesCount
718+
+ " but toposort visited only "
719+
+ updatedNodesCount);
720+
if (mEventListenerInitializedForFabric && cyclesDetected == 0) {
721+
ReactSoftException.logSoftException(TAG, ex);
722+
} else {
723+
throw ex;
724+
}
725+
} else {
726+
mWarnedAboutGraphTraversal = false;
726727
}
727728
}
728729
}

0 commit comments

Comments
 (0)