3535import com .facebook .react .bridge .ReactContext ;
3636import com .facebook .react .bridge .ReactMarker ;
3737import com .facebook .react .bridge .ReactMarkerConstants ;
38+ import com .facebook .react .bridge .ReactNoCrashSoftException ;
3839import com .facebook .react .bridge .ReactSoftException ;
3940import com .facebook .react .bridge .ReadableArray ;
4041import com .facebook .react .bridge .ReadableMap ;
@@ -111,6 +112,10 @@ public class FabricUIManager implements UIManager, LifecycleEventListener {
111112 @ NonNull private final Object mMountItemsLock = new Object ();
112113 @ NonNull private final Object mPreMountItemsLock = new Object ();
113114
115+ private boolean mInDispatch = false ;
116+ private boolean mShouldDispatchAgain = false ;
117+ private int mReDispatchCounter = 0 ;
118+
114119 @ GuardedBy ("mMountItemsLock" )
115120 @ NonNull
116121 private List <MountItem > mMountItems = new ArrayList <>();
@@ -486,8 +491,11 @@ public void synchronouslyUpdateViewOnUIThread(int reactTag, @NonNull ReadableMap
486491 scheduleMountItem (
487492 updatePropsMountItem (reactTag , props ), commitNumber , time , 0 , 0 , 0 , 0 , 0 , 0 );
488493 } catch (Exception ex ) {
489- // ignore exceptions for now
490494 // TODO T42943890: Fix animations in Fabric and remove this try/catch
495+ ReactSoftException .logSoftException (
496+ TAG ,
497+ new ReactNoCrashSoftException (
498+ "Caught exception in synchronouslyUpdateViewOnUIThread" , ex ));
491499 } finally {
492500 ReactMarker .logFabricMarker (
493501 ReactMarkerConstants .FABRIC_UPDATE_UI_MAIN_THREAD_END , null , commitNumber );
@@ -547,7 +555,11 @@ private void scheduleMountItem(
547555 !ReactFeatureFlags .allowDisablingImmediateExecutionOfScheduleMountItems
548556 || mImmediatelyExecutedMountItemsOnUI ;
549557 if (immediateExecutionEnabled ) {
550- dispatchMountItems ();
558+ try {
559+ dispatchMountItems ();
560+ } finally {
561+ mInDispatch = false ;
562+ }
551563 }
552564 }
553565
@@ -579,12 +591,30 @@ private void scheduleMountItem(
579591
580592 @ UiThread
581593 @ ThreadConfined (UI )
594+ /**
595+ * Anything that calls dispatchMountItems must call `mInDispatch = false` in a `finally` block
596+ * after calling it. dispatchMountItems will do its best to clean up, but we don't try to recover
597+ * from all failures here.
598+ */
582599 private void dispatchMountItems () {
600+ // Prevent re-dispatching in the middle of another dispatch call - this would cause mount
601+ // items to execute out of order. No need to synchronize, this is all happening on the UI
602+ // thread. TODO T63186801: refactor this
603+ if (mInDispatch ) {
604+ mShouldDispatchAgain = true ;
605+ return ;
606+ }
607+ if (mReDispatchCounter == 0 ) {
608+ mBatchedExecutionTime = 0 ;
609+ }
610+ mInDispatch = true ;
611+
583612 mRunStartTime = SystemClock .uptimeMillis ();
584613
585614 List <MountItem > mountItemsToDispatch ;
586615 synchronized (mMountItemsLock ) {
587616 if (mMountItems .isEmpty ()) {
617+ dispatchMountItemsCleanup ();
588618 return ;
589619 }
590620 mountItemsToDispatch = mMountItems ;
@@ -628,15 +658,57 @@ private void dispatchMountItems() {
628658 }
629659 mountItem .execute (mMountingManager );
630660 }
631- mBatchedExecutionTime = SystemClock .uptimeMillis () - batchedExecutionStartTime ;
661+ mBatchedExecutionTime + = SystemClock .uptimeMillis () - batchedExecutionStartTime ;
632662 Systrace .endSection (Systrace .TRACE_TAG_REACT_JAVA_BRIDGE );
663+
664+ dispatchMountItemsCleanup ();
665+ }
666+
667+ /** Should be called at the end of every dispatchMountItems call. */
668+ @ UiThread
669+ @ ThreadConfined (UI )
670+ private void dispatchMountItemsCleanup () {
671+ // Should we dispatch again? We do this up to 10 times. This is a magic number subject to
672+ // change. TODO T63181639: pick a better magic number.
673+ // Reentrance into dispatchMountItems can potentially happen a lot on Android in Fabric because
674+ // `updateState` from the
675+ // mounting layer causes mount items to be dispatched synchronously. We want to 1) make sure
676+ // we don't reenter in those cases, but 2) still execute those queued instructions
677+ // synchronously.
678+ // This is a pretty blunt tool, but we might not have better options since we really don't want
679+ // to execute anything out-of-order.
680+ mInDispatch = false ;
681+ if (mShouldDispatchAgain ) {
682+ mReDispatchCounter ++;
683+ mShouldDispatchAgain = false ;
684+ ReactSoftException .logSoftException (
685+ TAG ,
686+ new ReactNoCrashSoftException (
687+ "Re-dispatched "
688+ + mReDispatchCounter
689+ + " times. This indicates setState (?) is likely being called too many times during mounting." ));
690+
691+ // If we reach this point, we just wait for the next UI tick to execute mount instructions.
692+ if (mReDispatchCounter < 10 ) {
693+ dispatchMountItems ();
694+ }
695+ }
696+ mReDispatchCounter = 0 ;
633697 }
634698
635699 @ UiThread
636700 @ ThreadConfined (UI )
637701 private void dispatchPreMountItems (long frameTimeNanos ) {
702+ // Just set the flag, don't try to do any retries here. Allow `dispatchMountItems` to handle
703+ // that.
704+ if (mInDispatch ) {
705+ return ;
706+ }
707+
638708 Systrace .beginSection (Systrace .TRACE_TAG_REACT_JAVA_BRIDGE , "FabricUIManager::premountViews" );
639709
710+ mInDispatch = true ;
711+
640712 while (true ) {
641713 long timeLeftInFrame = FRAME_TIME_MS - ((System .nanoTime () - frameTimeNanos ) / 1000000 );
642714 if (timeLeftInFrame < MAX_TIME_IN_FRAME_FOR_NON_BATCHED_OPERATIONS_MS ) {
@@ -653,6 +725,8 @@ private void dispatchPreMountItems(long frameTimeNanos) {
653725
654726 preMountItemsToDispatch .execute (mMountingManager );
655727 }
728+
729+ mInDispatch = false ;
656730 Systrace .endSection (Systrace .TRACE_TAG_REACT_JAVA_BRIDGE );
657731 }
658732
@@ -823,16 +897,22 @@ public void doFrameGuarded(long frameTimeNanos) {
823897 }
824898
825899 try {
826-
827900 dispatchPreMountItems (frameTimeNanos );
828901
829902 dispatchMountItems ();
830903
831904 } catch (Exception ex ) {
832- FLog .i (TAG , "Exception thrown when executing UIFrameGuarded" , ex );
905+ FLog .e (TAG , "Exception thrown when executing UIFrameGuarded" , ex );
833906 stop ();
834907 throw ex ;
835908 } finally {
909+ // In case a catastrophic exception is thrown in either dispatch/preDispatch, and cleanup
910+ // doesn't run. In case of any other cleanup screwup, resetting this flag here will ensure
911+ // that we *never* skip more than a single frame of mount instructions (that would be very
912+ // bad,
913+ // but skipping more than one frame would be even more very bad).
914+ mInDispatch = false ;
915+
836916 ReactChoreographer .getInstance ()
837917 .postFrameCallback (
838918 ReactChoreographer .CallbackType .DISPATCH_UI , mDispatchUIFrameCallback );
0 commit comments