Skip to content

Commit c2de996

Browse files
JoshuaGrossfacebook-github-bot
authored andcommitted
Prevent reentrant dispatchMountItems calls
Summary: Turns out that dispatchMountItems was reentrant, meaning that something (in particular, updateState) could cause mount items to be queued up which would then be executed synchronously, out-of-order, in the middle of the previous dispatchMountItems call. We will continue to monitor this and see how often we're reentering: T63181639 and via any soft exceptions that are logged. For context, there are currently three ways dispatchMountItems gets called: 1) On every UI Tick in the UI thread, in a loop; 2) via animations, via synchronouslyUpdateViewOnUIThread, which happens to fail a *lot* currently; 3) when there is a commit on the UI thread, like with a Java->C++ state update. We must account for reentrance and failure in all three cases and make sure the `mInDispatch` flag is reset after success or failure in all of those situations. Changelog: [Internal] Reviewed By: mdvacca Differential Revision: D20170160 fbshipit-source-id: 834f1d9b65000caa7f2eea4849e5e7951d2b6be6
1 parent 11948bb commit c2de996

File tree

2 files changed

+89
-5
lines changed

2 files changed

+89
-5
lines changed

ReactAndroid/src/main/java/com/facebook/react/bridge/ReactNoCrashSoftException.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,8 @@ public class ReactNoCrashSoftException extends RuntimeException {
1616
public ReactNoCrashSoftException(String detailMessage) {
1717
super(detailMessage);
1818
}
19+
20+
public ReactNoCrashSoftException(String detailMessage, Throwable ex) {
21+
super(detailMessage, ex);
22+
}
1923
}

ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import com.facebook.react.bridge.ReactContext;
3636
import com.facebook.react.bridge.ReactMarker;
3737
import com.facebook.react.bridge.ReactMarkerConstants;
38+
import com.facebook.react.bridge.ReactNoCrashSoftException;
3839
import com.facebook.react.bridge.ReactSoftException;
3940
import com.facebook.react.bridge.ReadableArray;
4041
import 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

Comments
 (0)