23
23
import com .facebook .react .bridge .UIManager ;
24
24
import com .facebook .react .bridge .UiThreadUtil ;
25
25
import com .facebook .react .bridge .WritableMap ;
26
- import com .facebook .react .uimanager .IllegalViewOperationException ;
27
26
import com .facebook .react .uimanager .UIManagerHelper ;
28
27
import com .facebook .react .uimanager .common .UIManagerType ;
29
28
import com .facebook .react .uimanager .events .Event ;
54
53
/*package*/ class NativeAnimatedNodesManager implements EventDispatcherListener {
55
54
56
55
private static final String TAG = "NativeAnimatedNodesManager" ;
57
- private static final int MAX_INCONSISTENT_FRAMES = 64 ;
58
56
59
57
private final SparseArray <AnimatedNode > mAnimatedNodes = new SparseArray <>();
60
58
private final SparseArray <AnimationDriver > mActiveAnimations = new SparseArray <>();
64
62
private final Map <String , List <EventAnimationDriver >> mEventDrivers = new HashMap <>();
65
63
private final ReactApplicationContext mReactApplicationContext ;
66
64
private int mAnimatedGraphBFSColor = 0 ;
67
- private int mNumInconsistentFrames = 0 ;
68
65
// Used to avoid allocating a new array on every frame in `runUpdates` and `onEventDispatch`.
69
66
private final List <AnimatedNode > mRunUpdateNodeList = new LinkedList <>();
70
67
71
68
private boolean mEventListenerInitializedForFabric = false ;
72
69
private boolean mEventListenerInitializedForNonFabric = false ;
73
70
71
+ private boolean mWarnedAboutGraphTraversal = false ;
72
+
74
73
public NativeAnimatedNodesManager (ReactApplicationContext reactApplicationContext ) {
75
74
mReactApplicationContext = reactApplicationContext ;
76
75
}
@@ -646,7 +645,7 @@ private void updateNodes(List<AnimatedNode> nodes) {
646
645
}
647
646
648
647
// Run main "update" loop
649
- boolean errorsCaught = false ;
648
+ int cyclesDetected = 0 ;
650
649
while (!nodesQueue .isEmpty ()) {
651
650
AnimatedNode nextNode = nodesQueue .poll ();
652
651
try {
@@ -655,36 +654,15 @@ private void updateNodes(List<AnimatedNode> nodes) {
655
654
// Send property updates to native view manager
656
655
((PropsAnimatedNode ) nextNode ).updateView ();
657
656
}
658
- } catch (IllegalViewOperationException e ) {
657
+ } catch (JSApplicationCausedNativeException e ) {
659
658
// 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.
664
662
//
665
663
// 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.
668
665
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
- }
688
666
}
689
667
if (nextNode instanceof ValueAnimatedNode ) {
690
668
// Potentially send events to JS when the node's value is updated
@@ -698,31 +676,54 @@ private void updateNodes(List<AnimatedNode> nodes) {
698
676
child .mBFSColor = mAnimatedGraphBFSColor ;
699
677
updatedNodesCount ++;
700
678
nodesQueue .add (child );
679
+ } else if (child .mBFSColor == mAnimatedGraphBFSColor ) {
680
+ cyclesDetected ++;
701
681
}
702
682
}
703
683
}
704
684
}
705
685
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.
710
690
// In Fabric there can be race conditions between the JS thread setting up or tearing down
711
691
// 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.
713
693
if (activeNodesCount != updatedNodesCount ) {
714
- if (! errorsCaught ) {
715
- mNumInconsistentFrames ++ ;
694
+ if (mWarnedAboutGraphTraversal ) {
695
+ return ;
716
696
}
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 () );
723
703
}
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 ;
726
727
}
727
728
}
728
729
}
0 commit comments