From c8f571fdad9d632ab618d53520b4365d4e464423 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Tue, 4 Aug 2020 17:10:41 -0700 Subject: [PATCH] Build ViewGroup mechanism for repeatedly retrying UpdateState until it succeeds Summary: With BackgroundExecutor+State Reconciliation, it is pretty common to have races where View layer UpdateState calls are thrown away. Theoretically this is /always/ possible since C++ doesn't retry failed UpdateStates, we just try once and then bail. This is part 1 of improving our mechanisms for ensuring that State stays up-to-date from the View layer. Changelog: [Internal] Reviewed By: mdvacca Differential Revision: D22933927 fbshipit-source-id: 77b3b87402f772e3f149be0f9200e673bbed7b57 --- .../react/config/ReactFeatureFlags.java | 3 + .../uimanager/FabricStateBaseViewGroup.java | 73 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/FabricStateBaseViewGroup.java diff --git a/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java b/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java index acea75b442f4db..7a6b8a95ae4c41 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java +++ b/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java @@ -68,4 +68,7 @@ public class ReactFeatureFlags { /** Feature flag to use stopSurface when ReactRootView is unmounted. */ public static boolean enableStopSurfaceOnRootViewUnmount = false; + + /** Use experimental SetState retry mechanism in view? */ + public static boolean enableExperimentalStateUpdateRetry = false; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/FabricStateBaseViewGroup.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/FabricStateBaseViewGroup.java new file mode 100644 index 00000000000000..af24da786bce3a --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/FabricStateBaseViewGroup.java @@ -0,0 +1,73 @@ +/* + * 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.uimanager; + +import android.view.ViewGroup; +import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.config.ReactFeatureFlags; + +/** This is a helper base class for ViewGroups that use Fabric State. */ +public abstract class FabricStateBaseViewGroup extends ViewGroup { + public interface StateUpdateCallback { + WritableMap getStateUpdate(); + } + + private StateWrapper mStateWrapper = null; + + public FabricStateBaseViewGroup(ThemedReactContext context) { + super(context); + } + + public void setStateWrapper(StateWrapper stateWrapper) { + mStateWrapper = stateWrapper; + } + + private static void setState( + final FabricStateBaseViewGroup view, + final StateWrapper stateWrapper, + final StateUpdateCallback stateUpdateCallback, + final int numTries) { + // The StateWrapper will change, breaking this loop, whenever the UpdateState MountItem + // is executed. + // The caller is responsible for detecting if data is up-to-date, and doing nothing, or + // detecting if state is stale and calling setState again. + if (stateWrapper != view.mStateWrapper) { + return; + } + // We arbitrarily bail out after a certain number of retries. + // This is a pretty large number: in practice I've seen this number go over 50 + // with minimal/no visual jank. + if (numTries > 5 * 60) { + return; + } + + stateWrapper.updateState(stateUpdateCallback.getStateUpdate()); + + // An `updateState` call can fail, and there's no way to verify if it succeeds besides + // waiting for a corresponding `StateUpdate` MountItem to be executed on some future UI tick. + // So.... to resolve conflicts with updateState, we just keep firing it until it succeeds or + // the View goes away. + if (ReactFeatureFlags.enableExperimentalStateUpdateRetry) { + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + setState(view, stateWrapper, stateUpdateCallback, numTries + 1); + } + }); + } + } + + public static void setState( + final FabricStateBaseViewGroup view, + final StateWrapper stateWrapper, + final StateUpdateCallback stateUpdateCallback) { + setState(view, stateWrapper, stateUpdateCallback, 0); + } +}