Proposed fix for ScrollView race condition between C++ state update and onScroll

Summary:
FlatList relies heavily on onScroll events + the measure API. In Fabric, usage of `measure` relies on C++ having an accurate view of the current scroll position of the ScrollView.

We already have a mechanism for updating the scroll position in C++ using UpdateState. But, it is only used currently at the /beginning/ and /end/ of scrolling, and UpdateState is not called /during/ scrolling at all.

This means that we will see a series of events like this while scrolling:

```
Scrolling begins
UPDATE C++ STATE: scrollLeft = 0, scrollTop = 0
JS event: onScroll x=0, y=0
JS event: onScroll x=0, y=100
JS event: onScroll x=0, y=200
...
JS event: onScroll x=0, y=1000
UPDATE C++ STATE: scrollLeft = 0, scrollTop = 1000
```

Notably, not many C++ state updates are queued; and the last one is queued AFTER the JS event is sent. The last JS event and UpdateState will race, which means that sometimes the C++ update will /lose/ and C++ will have an inaccurate view of the world when FlatList receives the onScroll event and calls `measure`.

My proposed solution, gated behind a feature flag, is to delay /some/ onScroll events until the C++ UpdateState has made its way back to Java, and send UpdateStates more frequently. The balance here is that UpdateState is a relatively expensive operation, so we probably still want to call it /less/ than we call onScroll. This means that `measure` will still return some incorrect results but will return correct results more frequently. Win?

Changelog: [Internal[

Reviewed By: mdvacca

Differential Revision: D28558380

fbshipit-source-id: 11c7cd714fae67ee5a94c4413be988481413ec03
This commit is contained in:
Joshua Gross
2021-05-20 12:53:39 -07:00
committed by Facebook GitHub Bot
parent fa4045e4dd
commit b161241db2
3 changed files with 77 additions and 13 deletions
@@ -19,6 +19,11 @@ import com.facebook.proguard.annotations.DoNotStripAny;
@DoNotStripAny
public class ReactFeatureFlags {
/** An interface used to compute flags on demand. */
public interface FlagProvider {
boolean get();
}
/**
* Should this application use TurboModules? If yes, then any module that inherits {@link
* com.facebook.react.turbomodule.core.interfaces.TurboModule} will NOT be passed in to C++
@@ -63,11 +68,6 @@ public class ReactFeatureFlags {
/** Feature flag to configure eager initialization of MapBuffer So file */
public static boolean enableEagerInitializeMapBufferSoFile = false;
/** An interface used to compute flags on demand. */
public interface FlagProvider {
boolean get();
}
/** Should the RuntimeExecutor call JSIExecutor::flush()? */
private static FlagProvider enableRuntimeExecutorFlushingProvider = null;
@@ -96,4 +96,17 @@ public class ReactFeatureFlags {
/** Enables Fabric for LogBox */
public static boolean enableFabricInLogBox = false;
//
// ScrollView C++ UpdateState vs onScroll race fixes
//
/* Enables a "state race condition fix" for ScrollViews StateUpdate + onScroll event emitter */
public static boolean enableScrollViewStateEventRaceFix = false;
/* Enables another "state race condition fix" for ScrollViews StateUpdate + onScroll event emitter. Races a StateUpdate with every onScroll event. */
public static boolean enableScrollViewStateEventAlwaysRace = false;
/* Configure a min scroll delta for UpdateState to be called while still actively scrolling. */
public static int scrollViewUpdateStateMinScrollDelta = 0;
}