From 35b575fefd3e1fd6645f4363aee2ff19fff4ab21 Mon Sep 17 00:00:00 2001 From: Andrei Shikov Date: Wed, 24 Mar 2021 13:53:54 -0700 Subject: [PATCH] Refactor mount logic into MountItemDispatcher Summary: Changelog: [Internal] FabricUIManager contains a lot of logic related to mounting items and manipulating dispatch queues, which can be safely extracted outside. Apart from decreased logical complexity, this change enables easier modification of the queuing behavior later in this stack. Majority of the changes is caused by moving existing logic to a different class, no change in behavior expected. Reviewed By: JoshuaGross Differential Revision: D27271116 fbshipit-source-id: 86fe45b19bb839f96fde8ba607f72006f6401cc7 --- .../react/fabric/FabricUIManager.java | 428 +++--------------- .../fabric/mounting/MountItemDispatcher.java | 358 +++++++++++++++ 2 files changed, 422 insertions(+), 364 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountItemDispatcher.java 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(); + } +}