Only retry ViewCommand mount items if exception is marked as "Retryable"

Summary:
Instead of just blindly retrying all ViewCommands if they fail - which could be dangerous, since it's arbitrary imperative commands we'd be executing twice, potentially with bad app state - we only retry if the ViewCommand throws a "RetryableMountingLayerException".

Changelog: [Internal] Optimization to ViewCommands

Reviewed By: mdvacca

Differential Revision: D20529985

fbshipit-source-id: 0217b43f4bf92442bcc7ca48c8ae2b9a9e543dc9
This commit is contained in:
Joshua Gross 2020-03-19 22:55:46 -07:00 коммит произвёл Facebook GitHub Bot
Родитель 7561adac77
Коммит 0fe548aa2a
5 изменённых файлов: 61 добавлений и 22 удалений

Просмотреть файл

@ -13,11 +13,15 @@ package com.facebook.react.bridge;
* and not crash, no matter what. * and not crash, no matter what.
*/ */
public class ReactNoCrashSoftException extends RuntimeException { public class ReactNoCrashSoftException extends RuntimeException {
public ReactNoCrashSoftException(String detailMessage) { public ReactNoCrashSoftException(String m) {
super(detailMessage); super(m);
} }
public ReactNoCrashSoftException(String detailMessage, Throwable ex) { public ReactNoCrashSoftException(Throwable e) {
super(detailMessage, ex); super(e);
}
public ReactNoCrashSoftException(String m, Throwable e) {
super(m, e);
} }
} }

Просмотреть файл

@ -0,0 +1,26 @@
/*
* 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.bridge;
/**
* ViewCommands can throw this Exception. If this is caught during the execution of a ViewCommand
* mounting instruction, it indicates that the mount item can be safely retried.
*/
public class RetryableMountingLayerException extends RuntimeException {
public RetryableMountingLayerException(String msg, Throwable e) {
super(msg, e);
}
public RetryableMountingLayerException(Throwable e) {
super(e);
}
public RetryableMountingLayerException(String msg) {
super(msg);
}
}

Просмотреть файл

@ -26,6 +26,7 @@ import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.RetryableMountingLayerException;
import com.facebook.react.bridge.SoftAssertions; import com.facebook.react.bridge.SoftAssertions;
import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.config.ReactFeatureFlags;
@ -762,7 +763,7 @@ public class NativeViewHierarchyManager {
UiThreadUtil.assertOnUiThread(); UiThreadUtil.assertOnUiThread();
View view = mTagsToViews.get(reactTag); View view = mTagsToViews.get(reactTag);
if (view == null) { if (view == null) {
throw new IllegalViewOperationException( throw new RetryableMountingLayerException(
"Trying to send command to a non-existing view with tag [" "Trying to send command to a non-existing view with tag ["
+ reactTag + reactTag
+ "] and command " + "] and command "
@ -777,7 +778,7 @@ public class NativeViewHierarchyManager {
UiThreadUtil.assertOnUiThread(); UiThreadUtil.assertOnUiThread();
View view = mTagsToViews.get(reactTag); View view = mTagsToViews.get(reactTag);
if (view == null) { if (view == null) {
throw new IllegalViewOperationException( throw new RetryableMountingLayerException(
"Trying to send command to a non-existing view with tag [" "Trying to send command to a non-existing view with tag ["
+ reactTag + reactTag
+ "] and command " + "] and command "

Просмотреть файл

@ -17,9 +17,11 @@ import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.GuardedRunnable; import com.facebook.react.bridge.GuardedRunnable;
import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactNoCrashSoftException;
import com.facebook.react.bridge.ReactSoftException; import com.facebook.react.bridge.ReactSoftException;
import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.RetryableMountingLayerException;
import com.facebook.react.bridge.SoftAssertions; import com.facebook.react.bridge.SoftAssertions;
import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.common.ReactConstants; import com.facebook.react.common.ReactConstants;
@ -878,27 +880,26 @@ public class UIViewOperationQueue {
for (DispatchCommandViewOperation op : viewCommandOperations) { for (DispatchCommandViewOperation op : viewCommandOperations) {
try { try {
op.executeWithExceptions(); op.executeWithExceptions();
} catch (Throwable e) { } catch (RetryableMountingLayerException e) {
// Catch errors in DispatchCommands. We allow all commands to be retried // Catch errors in DispatchCommands. We allow all commands to be retried
// exactly // exactly once, after the current batch of other mountitems. If the second
// once, after the current batch of other mountitems. If the second attempt // attempt fails, then we log a soft error. This will still crash only in
// fails, // debug. We do this because it is a ~relatively common pattern to dispatch a
// then we log a soft error. This will still crash only in debug. // command during render, for example, to scroll to the bottom of a ScrollView
// We do this because it is a ~relatively common pattern to dispatch a command // in render. This dispatches the command before that View is even mounted. By
// during render, for example, to scroll to the bottom of a ScrollView in // retrying once, we can still dispatch the vast majority of commands faster,
// render. // avoid errors, and still operate correctly for most commands even when
// This dispatches the command before that View is even mounted. By retrying // they're executed too soon.
// once,
// we can still dispatch the vast majority of commands faster, avoid errors,
// and
// still operate correctly for most commands even when they're executed too
// soon.
if (op.getRetries() == 0) { if (op.getRetries() == 0) {
op.incrementRetries(); op.incrementRetries();
mViewCommandOperations.add(op); mViewCommandOperations.add(op);
} else { } else {
ReactSoftException.logSoftException(TAG, e); // Retryable exceptions should be logged, but never crash in debug.
ReactSoftException.logSoftException(TAG, new ReactNoCrashSoftException(e));
} }
} catch (Throwable e) {
// Non-retryable exceptions should be logged in prod, and crash in Debug.
ReactSoftException.logSoftException(TAG, e);
} }
} }
} }

Просмотреть файл

@ -9,9 +9,11 @@ package com.facebook.react.views.scroll;
import android.graphics.Color; import android.graphics.Color;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.view.View;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat; import androidx.core.view.ViewCompat;
import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.RetryableMountingLayerException;
import com.facebook.react.common.MapBuilder; import com.facebook.react.common.MapBuilder;
import com.facebook.react.module.annotations.ReactModule; import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.DisplayMetricsHolder; import com.facebook.react.uimanager.DisplayMetricsHolder;
@ -272,8 +274,13 @@ public class ReactScrollViewManager extends ViewGroupManager<ReactScrollView>
@Override @Override
public void scrollToEnd( public void scrollToEnd(
ReactScrollView scrollView, ReactScrollViewCommandHelper.ScrollToEndCommandData data) { ReactScrollView scrollView, ReactScrollViewCommandHelper.ScrollToEndCommandData data) {
View child = scrollView.getChildAt(0);
if (child == null) {
throw new RetryableMountingLayerException("scrollToEnd called on ScrollView without child");
}
// ScrollView always has one child - the scrollable area // ScrollView always has one child - the scrollable area
int bottom = scrollView.getChildAt(0).getHeight() + scrollView.getPaddingBottom(); int bottom = child.getHeight() + scrollView.getPaddingBottom();
if (data.mAnimated) { if (data.mAnimated) {
scrollView.reactSmoothScrollTo(scrollView.getScrollX(), bottom); scrollView.reactSmoothScrollTo(scrollView.getScrollX(), bottom);
} else { } else {