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 89ea427748d..fc5516258f9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -40,14 +40,12 @@ import com.facebook.react.bridge.NativeArray; import com.facebook.react.bridge.NativeMap; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContext; -import com.facebook.react.bridge.ReactIgnorableMountingException; import com.facebook.react.bridge.ReactMarker; import com.facebook.react.bridge.ReactMarkerConstants; import com.facebook.react.bridge.ReactNoCrashSoftException; import com.facebook.react.bridge.ReactSoftException; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.RetryableMountingLayerException; import com.facebook.react.bridge.UIManager; import com.facebook.react.bridge.UIManagerListener; import com.facebook.react.bridge.UiThreadUtil; @@ -58,9 +56,9 @@ import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.fabric.events.EventBeatManager; import com.facebook.react.fabric.events.EventEmitterWrapper; import com.facebook.react.fabric.events.FabricEventEmitter; +import com.facebook.react.fabric.mounting.MountItemDispatcher; import com.facebook.react.fabric.mounting.MountingManager; import com.facebook.react.fabric.mounting.SurfaceMountingManager; -import com.facebook.react.fabric.mounting.mountitems.DispatchCommandMountItem; import com.facebook.react.fabric.mounting.mountitems.DispatchIntCommandMountItem; import com.facebook.react.fabric.mounting.mountitems.DispatchStringCommandMountItem; import com.facebook.react.fabric.mounting.mountitems.IntBufferBatchMountItem; @@ -83,13 +81,9 @@ import com.facebook.react.uimanager.events.EventDispatcher; import com.facebook.react.uimanager.events.EventDispatcherImpl; import com.facebook.react.views.text.TextLayoutManager; import com.facebook.react.views.text.TextLayoutManagerMapBuffer; -import com.facebook.systrace.Systrace; -import java.util.ArrayList; -import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CopyOnWriteArrayList; @SuppressLint("MissingNativeLoadLibrary") @@ -103,9 +97,6 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { ReactFeatureFlags.enableFabricLogs || PrinterHolder.getPrinter() .shouldDisplayLogMessage(ReactDebugOverlayTags.FABRIC_UI_MANAGER); - private static final int FRAME_TIME_MS = 16; - private static final int MAX_TIME_IN_FRAME_FOR_NON_BATCHED_OPERATIONS_MS = 8; - private static final int PRE_MOUNT_ITEMS_INITIAL_SIZE_ARRAY = 250; static { FabricSoLoader.staticInit(); @@ -115,27 +106,13 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { @NonNull private final ReactApplicationContext mReactApplicationContext; @NonNull private final MountingManager mMountingManager; @NonNull private final EventDispatcher mEventDispatcher; + @NonNull private final MountItemDispatcher mMountItemDispatcher; @NonNull private final EventBeatManager mEventBeatManager; - private boolean mInDispatch = false; - private int mReDispatchCounter = 0; - @NonNull private final CopyOnWriteArrayList mListeners = new CopyOnWriteArrayList<>(); - @NonNull - private final ConcurrentLinkedQueue mViewCommandMountItemsConcurrent = - new ConcurrentLinkedQueue<>(); - - @NonNull - private final ConcurrentLinkedQueue mMountItemsConcurrent = - new ConcurrentLinkedQueue<>(); - - @NonNull - private final ConcurrentLinkedQueue mPreMountItemsConcurrent = - new ConcurrentLinkedQueue<>(); - @ThreadConfined(UI) @NonNull private final DispatchUIFrameCallback mDispatchUIFrameCallback; @@ -151,8 +128,6 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { private boolean mDriveCxxAnimations = false; - private long mRunStartTime = 0l; - private long mBatchedExecutionTime = 0l; private long mDispatchViewUpdatesTime = 0l; private long mCommitStartTime = 0l; private long mLayoutTime = 0l; @@ -175,6 +150,8 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { mDispatchUIFrameCallback = new DispatchUIFrameCallback(reactContext); mReactApplicationContext = reactContext; mMountingManager = new MountingManager(viewManagerRegistry); + mMountItemDispatcher = + new MountItemDispatcher(mMountingManager, new MountItemDispatchListener()); mEventDispatcher = eventDispatcher; mShouldDeallocateEventDispatcher = false; mEventBeatManager = eventBeatManager; @@ -188,6 +165,8 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { mDispatchUIFrameCallback = new DispatchUIFrameCallback(reactContext); mReactApplicationContext = reactContext; mMountingManager = new MountingManager(viewManagerRegistry); + mMountItemDispatcher = + new MountItemDispatcher(mMountingManager, new MountItemDispatchListener()); mEventDispatcher = new EventDispatcherImpl(reactContext); mShouldDeallocateEventDispatcher = true; mEventBeatManager = eventBeatManager; @@ -377,37 +356,6 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { } } - @DoNotStrip - @SuppressWarnings("unused") - @AnyThread - @ThreadConfined(ANY) - private void preallocateView( - int rootTag, - int reactTag, - final String componentName, - @Nullable ReadableMap props, - @Nullable Object stateWrapper, - boolean isLayoutable) { - - addPreAllocateMountItem( - new PreAllocateViewMountItem( - rootTag, - reactTag, - getFabricComponentName(componentName), - props, - (StateWrapper) stateWrapper, - isLayoutable)); - } - - @DoNotStrip - @SuppressWarnings("unused") - @AnyThread - @ThreadConfined(ANY) - private MountItem createIntBufferBatchMountItem( - int rootTag, int[] intBuffer, Object[] objBuffer, int commitNumber) { - return new IntBufferBatchMountItem(rootTag, intBuffer, objBuffer, commitNumber); - } - @DoNotStrip @SuppressWarnings("unused") private NativeArray measureLines( @@ -557,6 +505,14 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { return true; } + public void addUIManagerEventListener(UIManagerListener listener) { + mListeners.add(listener); + } + + public void removeUIManagerEventListener(UIManagerListener listener) { + mListeners.remove(listener); + } + @Override @UiThread @ThreadConfined(UI) @@ -613,7 +569,7 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { // If the reactTag exists, we assume that it might at the end of the next // batch of MountItems. Otherwise, we try to execute immediately. if (!mMountingManager.getViewExists(reactTag)) { - addMountItem(synchronousMountItem); + mMountItemDispatcher.addMountItem(synchronousMountItem); return; } @@ -634,17 +590,40 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { ReactMarkerConstants.FABRIC_UPDATE_UI_MAIN_THREAD_END, null, commitNumber); } - public void addUIManagerEventListener(UIManagerListener listener) { - mListeners.add(listener); + @DoNotStrip + @SuppressWarnings("unused") + @AnyThread + @ThreadConfined(ANY) + private void preallocateView( + int rootTag, + int reactTag, + final String componentName, + @Nullable ReadableMap props, + @Nullable Object stateWrapper, + boolean isLayoutable) { + + mMountItemDispatcher.addPreAllocateMountItem( + new PreAllocateViewMountItem( + rootTag, + reactTag, + getFabricComponentName(componentName), + props, + (StateWrapper) stateWrapper, + isLayoutable)); } - public void removeUIManagerEventListener(UIManagerListener listener) { - mListeners.remove(listener); + @DoNotStrip + @SuppressWarnings("unused") + @AnyThread + @ThreadConfined(ANY) + private MountItem createIntBufferBatchMountItem( + int rootTag, int[] intBuffer, Object[] objBuffer, int commitNumber) { + return new IntBufferBatchMountItem(rootTag, intBuffer, objBuffer, commitNumber); } /** * This method enqueues UI operations directly to the UI thread. This might change in the future - * to enforce execution order using {@link ReactChoreographer#CallbackType}. This method should + * to enforce execution order using {@link ReactChoreographer.CallbackType}. This method should * only be called as the result of a new tree being committed. */ @DoNotStrip @@ -684,10 +663,10 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { } if (shouldSchedule) { - addMountItem(mountItem); + mMountItemDispatcher.addMountItem(mountItem); if (UiThreadUtil.isOnUiThread()) { // We only read these flags on the UI thread. - tryDispatchMountItems(); + mMountItemDispatcher.tryDispatchMountItems(); } } @@ -717,265 +696,6 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { } } - /** - * Try to dispatch MountItems. Returns true if any items were dispatched, false otherwise. A - * `false` return value doesn't indicate errors, it may just indicate there was no work to be - * done. - * - * @return - */ - @UiThread - @ThreadConfined(UI) - private boolean tryDispatchMountItems() { - // If we're already dispatching, don't reenter. - // Reentrance can potentially happen a lot on Android in Fabric because - // `updateState` from the - // mounting layer causes mount items to be dispatched synchronously. We want to 1) make sure - // we don't reenter in those cases, but 2) still execute those queued instructions - // synchronously. - // This is a pretty blunt tool, but we might not have better options since we really don't want - // to execute anything out-of-order. - if (mInDispatch) { - return false; - } - - final boolean didDispatchItems; - try { - didDispatchItems = dispatchMountItems(); - } catch (Throwable e) { - mReDispatchCounter = 0; - throw e; - } finally { - // Clean up after running dispatchMountItems - even if an exception was thrown - 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. - if (mReDispatchCounter > 2) { - ReactSoftException.logSoftException( - TAG, - new ReactNoCrashSoftException( - "Re-dispatched " - + mReDispatchCounter - + " times. This indicates setState (?) is likely being called too many times during mounting.")); - } - - mReDispatchCounter++; - tryDispatchMountItems(); - } - mReDispatchCounter = 0; - return didDispatchItems; - } - - @Nullable - private List drainConcurrentItemQueue(ConcurrentLinkedQueue queue) { - List result = new ArrayList<>(); - while (!queue.isEmpty()) { - E item = queue.poll(); - if (item != null) { - result.add(item); - } - } - if (result.size() == 0) { - return null; - } - return result; - } - - @UiThread - @ThreadConfined(UI) - private List getAndResetViewCommandMountItems() { - return drainConcurrentItemQueue(mViewCommandMountItemsConcurrent); - } - - @UiThread - @ThreadConfined(UI) - private List getAndResetMountItems() { - return drainConcurrentItemQueue(mMountItemsConcurrent); - } - - private Collection getAndResetPreMountItems() { - return drainConcurrentItemQueue(mPreMountItemsConcurrent); - } - - private static void printMountItem(MountItem mountItem, String prefix) { - // If a MountItem description is split across multiple lines, it's because it's a - // compound MountItem. Log each line separately. - String[] mountItemLines = mountItem.toString().split("\n"); - for (String m : mountItemLines) { - FLog.e(TAG, prefix + ": " + m); - } - } - - @UiThread - @ThreadConfined(UI) - /** Nothing should call this directly except for `tryDispatchMountItems`. */ - private boolean dispatchMountItems() { - if (mReDispatchCounter == 0) { - mBatchedExecutionTime = 0; - } - - mRunStartTime = SystemClock.uptimeMillis(); - - List viewCommandMountItemsToDispatch = - getAndResetViewCommandMountItems(); - List mountItemsToDispatch = getAndResetMountItems(); - - if (mountItemsToDispatch == null && viewCommandMountItemsToDispatch == null) { - return false; - } - - // As an optimization, execute all ViewCommands first - // This should be: - // 1) Performant: ViewCommands are often a replacement for SetNativeProps, which we've always - // wanted to be as "synchronous" as possible. - // 2) Safer: ViewCommands are inherently disconnected from the tree commit/diff/mount process. - // JS imperatively queues these commands. - // If JS has queued a command, it's reasonable to assume that the more time passes, the more - // likely it is that the view disappears. - // Thus, by executing ViewCommands early, we should actually avoid a category of - // errors/glitches. - if (viewCommandMountItemsToDispatch != null) { - Systrace.beginSection( - Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, - "FabricUIManager::mountViews viewCommandMountItems to execute: " - + viewCommandMountItemsToDispatch.size()); - for (DispatchCommandMountItem command : viewCommandMountItemsToDispatch) { - if (ENABLE_FABRIC_LOGS) { - printMountItem(command, "dispatchMountItems: Executing viewCommandMountItem"); - } - try { - command.execute(mMountingManager); - } catch (RetryableMountingLayerException e) { - // If the exception is marked as Retryable, we retry the viewcommand exactly once, after - // the current batch of mount items has finished executing. - if (command.getRetries() == 0) { - command.incrementRetries(); - dispatchCommandMountItem(command); - } else { - // It's very common for commands to be executed on views that no longer exist - for - // example, a blur event on TextInput being fired because of a navigation event away - // from the current screen. If the exception is marked as Retryable, we log a soft - // exception but never crash in debug. - // It's not clear that logging this is even useful, because these events are very - // common, mundane, and there's not much we can do about them currently. - ReactSoftException.logSoftException( - TAG, - new ReactNoCrashSoftException( - "Caught exception executing ViewCommand: " + command.toString(), e)); - } - } catch (Throwable e) { - // Non-Retryable exceptions are logged as soft exceptions in prod, but crash in Debug. - ReactSoftException.logSoftException( - TAG, - new RuntimeException( - "Caught exception executing ViewCommand: " + command.toString(), e)); - } - } - - Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); - } - - // If there are MountItems to dispatch, we make sure all the "pre mount items" are executed - // first - Collection preMountItemsToDispatch = getAndResetPreMountItems(); - - if (preMountItemsToDispatch != null) { - Systrace.beginSection( - Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, - "FabricUIManager::mountViews preMountItems to execute: " - + preMountItemsToDispatch.size()); - - for (PreAllocateViewMountItem preMountItem : preMountItemsToDispatch) { - preMountItem.execute(mMountingManager); - } - - Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); - } - - if (mountItemsToDispatch != null) { - Systrace.beginSection( - Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, - "FabricUIManager::mountViews mountItems to execute: " + mountItemsToDispatch.size()); - - long batchedExecutionStartTime = SystemClock.uptimeMillis(); - - for (MountItem mountItem : mountItemsToDispatch) { - if (ENABLE_FABRIC_LOGS) { - printMountItem(mountItem, "dispatchMountItems: Executing mountItem"); - } - - try { - mountItem.execute(mMountingManager); - } catch (Throwable e) { - // If there's an exception, we want to log diagnostics in prod and rethrow. - FLog.e(TAG, "dispatchMountItems: caught exception, displaying all MountItems", e); - for (MountItem m : mountItemsToDispatch) { - printMountItem(m, "dispatchMountItems: mountItem"); - } - - if (ReactIgnorableMountingException.isIgnorable(e)) { - ReactSoftException.logSoftException(TAG, e); - } else { - throw e; - } - } - } - mBatchedExecutionTime += SystemClock.uptimeMillis() - batchedExecutionStartTime; - } - Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); - - return true; - } - - /** - * Detect if we still have processing time left in this frame. - * - * @param frameTimeNanos - * @return - */ - private boolean haveExceededNonBatchedFrameTime(long frameTimeNanos) { - long timeLeftInFrame = FRAME_TIME_MS - ((System.nanoTime() - frameTimeNanos) / 1000000); - return timeLeftInFrame < MAX_TIME_IN_FRAME_FOR_NON_BATCHED_OPERATIONS_MS; - } - - @UiThread - @ThreadConfined(UI) - private void dispatchPreMountItems(long frameTimeNanos) { - Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "FabricUIManager::premountViews"); - - // dispatchPreMountItems cannot be reentrant, but we want to prevent dispatchMountItems from - // reentering during dispatchPreMountItems - mInDispatch = true; - - try { - while (true) { - if (haveExceededNonBatchedFrameTime(frameTimeNanos)) { - break; - } - - PreAllocateViewMountItem preMountItemToDispatch = mPreMountItemsConcurrent.poll(); - - // If list is empty, `poll` will return null, or var will never be set - if (preMountItemToDispatch == null) { - break; - } - - preMountItemToDispatch.execute(mMountingManager); - } - } finally { - mInDispatch = false; - } - - Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); - } - public void setBinding(Binding binding) { mBinding = binding; } @@ -1101,7 +821,7 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { final int reactTag, final int commandId, @Nullable final ReadableArray commandArgs) { - dispatchCommandMountItem( + mMountItemDispatcher.dispatchCommandMountItem( new DispatchIntCommandMountItem(surfaceId, reactTag, commandId, commandArgs)); } @@ -1113,23 +833,17 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { final int reactTag, final String commandId, @Nullable final ReadableArray commandArgs) { - dispatchCommandMountItem( + mMountItemDispatcher.dispatchCommandMountItem( new DispatchStringCommandMountItem(surfaceId, reactTag, commandId, commandArgs)); } - @AnyThread - @ThreadConfined(ANY) - private void dispatchCommandMountItem(DispatchCommandMountItem command) { - addViewCommandMountItem(command); - } - @Override @AnyThread @ThreadConfined(ANY) public void sendAccessibilityEvent(int reactTag, int eventType) { // Can be called from native, not just JS - we need to migrate the native callsites // before removing this entirely. - addMountItem(new SendAccessibilityEvent(View.NO_ID, reactTag, eventType)); + mMountItemDispatcher.addMountItem(new SendAccessibilityEvent(View.NO_ID, reactTag, eventType)); } @DoNotStrip @@ -1147,7 +861,7 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { throw new IllegalArgumentException( "sendAccessibilityEventFromJS: invalid eventType " + eventTypeJS); } - addMountItem(new SendAccessibilityEvent(surfaceId, reactTag, eventType)); + mMountItemDispatcher.addMountItem(new SendAccessibilityEvent(surfaceId, reactTag, eventType)); } /** @@ -1164,7 +878,7 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { final int initialReactTag, final boolean blockNativeResponder) { if (ReactFeatureFlags.enableJSResponder) { - addMountItem( + mMountItemDispatcher.addMountItem( new MountItem() { @Override public void execute(MountingManager mountingManager) { @@ -1188,13 +902,13 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { } /** - * Clears the JS Responder specified by {@link #setJSResponder(int, int, boolean)}. After this - * method is called, all the touch events are going to be handled by JS. + * Clears the JS Responder specified by {@link #setJSResponder}. After this method is called, all + * the touch events are going to be handled by JS. */ @DoNotStrip public void clearJSResponder() { if (ReactFeatureFlags.enableJSResponder) { - addMountItem( + mMountItemDispatcher.addMountItem( new MountItem() { @Override public void execute(MountingManager mountingManager) { @@ -1221,7 +935,7 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { if (eventName == null) { return null; } - if (eventName.substring(0, 3).equals("top")) { + if (eventName.startsWith("top")) { return "on" + eventName.substring(3); } return eventName; @@ -1247,36 +961,22 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { performanceCounters.put("CommitStartTime", mCommitStartTime); performanceCounters.put("LayoutTime", mLayoutTime); performanceCounters.put("DispatchViewUpdatesTime", mDispatchViewUpdatesTime); - performanceCounters.put("RunStartTime", mRunStartTime); - performanceCounters.put("BatchedExecutionTime", mBatchedExecutionTime); + performanceCounters.put("RunStartTime", mMountItemDispatcher.getRunStartTime()); + performanceCounters.put("BatchedExecutionTime", mMountItemDispatcher.getBatchedExecutionTime()); performanceCounters.put("FinishFabricTransactionTime", mFinishTransactionTime); performanceCounters.put("FinishFabricTransactionCPPTime", mFinishTransactionCPPTime); return performanceCounters; } - private void addMountItem(MountItem mountItem) { - mMountItemsConcurrent.add(mountItem); - } - - private void addPreAllocateMountItem(PreAllocateViewMountItem mountItem) { - // We do this check only for PreAllocateViewMountItem - and not DispatchMountItem or regular - // MountItem - because PreAllocateViewMountItem is not batched, and is relatively more expensive - // both to queue, to drain, and to execute. - if (!mMountingManager.surfaceIsStopped(mountItem.getSurfaceId())) { - mPreMountItemsConcurrent.add(mountItem); - } else if (IS_DEVELOPMENT_ENVIRONMENT) { - FLog.e( - TAG, - "Not queueing PreAllocateMountItem: surfaceId stopped: [%d] - %s", - mountItem.getSurfaceId(), - mountItem.toString()); + private class MountItemDispatchListener implements MountItemDispatcher.ItemDispatchListener { + @Override + public void didDispatchMountItems() { + for (UIManagerListener listener : mListeners) { + listener.didDispatchMountItems(FabricUIManager.this); + } } } - private void addViewCommandMountItem(DispatchCommandMountItem mountItem) { - mViewCommandMountItemsConcurrent.add(mountItem); - } - private class DispatchUIFrameCallback extends GuardedFrameCallback { private volatile boolean mIsMountingEnabled = true; @@ -1308,8 +1008,8 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { } try { - dispatchPreMountItems(frameTimeNanos); - tryDispatchMountItems(); + mMountItemDispatcher.dispatchPreMountItems(frameTimeNanos); + mMountItemDispatcher.tryDispatchMountItems(); } catch (Exception ex) { FLog.e(TAG, "Exception thrown when executing UIFrameGuarded", ex); stop(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountItemDispatcher.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountItemDispatcher.java new file mode 100644 index 00000000000..68ec64ac5cc --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountItemDispatcher.java @@ -0,0 +1,358 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.fabric.mounting; + +import static com.facebook.infer.annotation.ThreadConfined.ANY; +import static com.facebook.infer.annotation.ThreadConfined.UI; +import static com.facebook.react.fabric.FabricUIManager.ENABLE_FABRIC_LOGS; +import static com.facebook.react.fabric.FabricUIManager.IS_DEVELOPMENT_ENVIRONMENT; + +import android.os.SystemClock; +import androidx.annotation.AnyThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; +import com.facebook.common.logging.FLog; +import com.facebook.infer.annotation.ThreadConfined; +import com.facebook.react.bridge.ReactIgnorableMountingException; +import com.facebook.react.bridge.ReactNoCrashSoftException; +import com.facebook.react.bridge.ReactSoftException; +import com.facebook.react.bridge.RetryableMountingLayerException; +import com.facebook.react.fabric.mounting.mountitems.DispatchCommandMountItem; +import com.facebook.react.fabric.mounting.mountitems.MountItem; +import com.facebook.react.fabric.mounting.mountitems.PreAllocateViewMountItem; +import com.facebook.systrace.Systrace; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + +public class MountItemDispatcher { + + private static final String TAG = "MountItemDispatcher"; + private static final int FRAME_TIME_MS = 16; + private static final int MAX_TIME_IN_FRAME_FOR_NON_BATCHED_OPERATIONS_MS = 8; + + private final MountingManager mMountingManager; + private final ItemDispatchListener mItemDispatchListener; + + @NonNull + private final ConcurrentLinkedQueue mViewCommandMountItems = + new ConcurrentLinkedQueue<>(); + + @NonNull + private final ConcurrentLinkedQueue mMountItems = new ConcurrentLinkedQueue<>(); + + @NonNull + private final ConcurrentLinkedQueue mPreMountItems = + new ConcurrentLinkedQueue<>(); + + private boolean mInDispatch = false; + private int mReDispatchCounter = 0; + private long mBatchedExecutionTime = 0L; + private long mRunStartTime = 0L; + + public MountItemDispatcher(MountingManager mountingManager, ItemDispatchListener listener) { + mMountingManager = mountingManager; + mItemDispatchListener = listener; + } + + @AnyThread + @ThreadConfined(ANY) + public void dispatchCommandMountItem(DispatchCommandMountItem command) { + addViewCommandMountItem(command); + } + + public void addMountItem(MountItem mountItem) { + mMountItems.add(mountItem); + } + + public void addPreAllocateMountItem(PreAllocateViewMountItem mountItem) { + // We do this check only for PreAllocateViewMountItem - and not DispatchMountItem or regular + // MountItem - because PreAllocateViewMountItem is not batched, and is relatively more expensive + // both to queue, to drain, and to execute. + if (!mMountingManager.surfaceIsStopped(mountItem.getSurfaceId())) { + mPreMountItems.add(mountItem); + } else if (IS_DEVELOPMENT_ENVIRONMENT) { + FLog.e( + TAG, + "Not queueing PreAllocateMountItem: surfaceId stopped: [%d] - %s", + mountItem.getSurfaceId(), + mountItem.toString()); + } + } + + public void addViewCommandMountItem(DispatchCommandMountItem mountItem) { + mViewCommandMountItems.add(mountItem); + } + + /** + * Try to dispatch MountItems. Returns true if any items were dispatched, false otherwise. A + * `false` return value doesn't indicate errors, it may just indicate there was no work to be + * done. + * + * @return + */ + @UiThread + @ThreadConfined(UI) + public boolean tryDispatchMountItems() { + // If we're already dispatching, don't reenter. + // Reentrance can potentially happen a lot on Android in Fabric because + // `updateState` from the + // mounting layer causes mount items to be dispatched synchronously. We want to 1) make sure + // we don't reenter in those cases, but 2) still execute those queued instructions + // synchronously. + // This is a pretty blunt tool, but we might not have better options since we really don't want + // to execute anything out-of-order. + if (mInDispatch) { + return false; + } + + final boolean didDispatchItems; + try { + didDispatchItems = dispatchMountItems(); + } catch (Throwable e) { + mReDispatchCounter = 0; + throw e; + } finally { + // Clean up after running dispatchMountItems - even if an exception was thrown + mInDispatch = false; + } + + mItemDispatchListener.didDispatchMountItems(); + + // Decide if we want to try reentering + if (mReDispatchCounter < 10 && didDispatchItems) { + // Executing twice in a row is normal. Only log after that point. + if (mReDispatchCounter > 2) { + ReactSoftException.logSoftException( + TAG, + new ReactNoCrashSoftException( + "Re-dispatched " + + mReDispatchCounter + + " times. This indicates setState (?) is likely being called too many times during mounting.")); + } + + mReDispatchCounter++; + tryDispatchMountItems(); + } + mReDispatchCounter = 0; + return didDispatchItems; + } + + @UiThread + @ThreadConfined(UI) + /** Nothing should call this directly except for `tryDispatchMountItems`. */ + private boolean dispatchMountItems() { + if (mReDispatchCounter == 0) { + mBatchedExecutionTime = 0; + } + + mRunStartTime = SystemClock.uptimeMillis(); + + List viewCommandMountItemsToDispatch = + getAndResetViewCommandMountItems(); + List mountItemsToDispatch = getAndResetMountItems(); + + if (mountItemsToDispatch == null && viewCommandMountItemsToDispatch == null) { + return false; + } + + // As an optimization, execute all ViewCommands first + // This should be: + // 1) Performant: ViewCommands are often a replacement for SetNativeProps, which we've always + // wanted to be as "synchronous" as possible. + // 2) Safer: ViewCommands are inherently disconnected from the tree commit/diff/mount process. + // JS imperatively queues these commands. + // If JS has queued a command, it's reasonable to assume that the more time passes, the more + // likely it is that the view disappears. + // Thus, by executing ViewCommands early, we should actually avoid a category of + // errors/glitches. + if (viewCommandMountItemsToDispatch != null) { + Systrace.beginSection( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, + "FabricUIManager::mountViews viewCommandMountItems to execute: " + + viewCommandMountItemsToDispatch.size()); + for (DispatchCommandMountItem command : viewCommandMountItemsToDispatch) { + if (ENABLE_FABRIC_LOGS) { + printMountItem(command, "dispatchMountItems: Executing viewCommandMountItem"); + } + try { + command.execute(mMountingManager); + } catch (RetryableMountingLayerException e) { + // If the exception is marked as Retryable, we retry the viewcommand exactly once, after + // the current batch of mount items has finished executing. + if (command.getRetries() == 0) { + command.incrementRetries(); + dispatchCommandMountItem(command); + } else { + // It's very common for commands to be executed on views that no longer exist - for + // example, a blur event on TextInput being fired because of a navigation event away + // from the current screen. If the exception is marked as Retryable, we log a soft + // exception but never crash in debug. + // It's not clear that logging this is even useful, because these events are very + // common, mundane, and there's not much we can do about them currently. + ReactSoftException.logSoftException( + TAG, + new ReactNoCrashSoftException( + "Caught exception executing ViewCommand: " + command.toString(), e)); + } + } catch (Throwable e) { + // Non-Retryable exceptions are logged as soft exceptions in prod, but crash in Debug. + ReactSoftException.logSoftException( + TAG, + new RuntimeException( + "Caught exception executing ViewCommand: " + command.toString(), e)); + } + } + + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + + // If there are MountItems to dispatch, we make sure all the "pre mount items" are executed + // first + Collection preMountItemsToDispatch = getAndResetPreMountItems(); + + if (preMountItemsToDispatch != null) { + Systrace.beginSection( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, + "FabricUIManager::mountViews preMountItems to execute: " + + preMountItemsToDispatch.size()); + + for (PreAllocateViewMountItem preMountItem : preMountItemsToDispatch) { + preMountItem.execute(mMountingManager); + } + + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + + if (mountItemsToDispatch != null) { + Systrace.beginSection( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, + "FabricUIManager::mountViews mountItems to execute: " + mountItemsToDispatch.size()); + + long batchedExecutionStartTime = SystemClock.uptimeMillis(); + + for (MountItem mountItem : mountItemsToDispatch) { + if (ENABLE_FABRIC_LOGS) { + printMountItem(mountItem, "dispatchMountItems: Executing mountItem"); + } + + try { + mountItem.execute(mMountingManager); + } catch (Throwable e) { + // If there's an exception, we want to log diagnostics in prod and rethrow. + FLog.e(TAG, "dispatchMountItems: caught exception, displaying all MountItems", e); + for (MountItem m : mountItemsToDispatch) { + printMountItem(m, "dispatchMountItems: mountItem"); + } + + if (ReactIgnorableMountingException.isIgnorable(e)) { + ReactSoftException.logSoftException(TAG, e); + } else { + throw e; + } + } + } + mBatchedExecutionTime += SystemClock.uptimeMillis() - batchedExecutionStartTime; + } + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + + return true; + } + + @UiThread + @ThreadConfined(UI) + public void dispatchPreMountItems(long frameTimeNanos) { + Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "FabricUIManager::premountViews"); + + // dispatchPreMountItems cannot be reentrant, but we want to prevent dispatchMountItems from + // reentering during dispatchPreMountItems + mInDispatch = true; + + try { + while (true) { + if (haveExceededNonBatchedFrameTime(frameTimeNanos)) { + break; + } + + PreAllocateViewMountItem preMountItemToDispatch = mPreMountItems.poll(); + + // If list is empty, `poll` will return null, or var will never be set + if (preMountItemToDispatch == null) { + break; + } + + preMountItemToDispatch.execute(mMountingManager); + } + } finally { + mInDispatch = false; + } + + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + + @Nullable + private static List drainConcurrentItemQueue( + ConcurrentLinkedQueue queue) { + List result = new ArrayList<>(); + while (!queue.isEmpty()) { + E item = queue.poll(); + if (item != null) { + result.add(item); + } + } + if (result.size() == 0) { + return null; + } + return result; + } + + /** Detect if we still have processing time left in this frame. */ + private static boolean haveExceededNonBatchedFrameTime(long frameTimeNanos) { + long timeLeftInFrame = FRAME_TIME_MS - ((System.nanoTime() - frameTimeNanos) / 1000000); + return timeLeftInFrame < MAX_TIME_IN_FRAME_FOR_NON_BATCHED_OPERATIONS_MS; + } + + @UiThread + @ThreadConfined(UI) + private List getAndResetViewCommandMountItems() { + return drainConcurrentItemQueue(mViewCommandMountItems); + } + + @UiThread + @ThreadConfined(UI) + private List getAndResetMountItems() { + return drainConcurrentItemQueue(mMountItems); + } + + private Collection getAndResetPreMountItems() { + return drainConcurrentItemQueue(mPreMountItems); + } + + public long getBatchedExecutionTime() { + return mBatchedExecutionTime; + } + + public long getRunStartTime() { + return mRunStartTime; + } + + private static void printMountItem(MountItem mountItem, String prefix) { + // If a MountItem description is split across multiple lines, it's because it's a + // compound MountItem. Log each line separately. + String[] mountItemLines = mountItem.toString().split("\n"); + for (String m : mountItemLines) { + FLog.e(TAG, prefix + ": " + m); + } + } + + public interface ItemDispatchListener { + void didDispatchMountItems(); + } +}