Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix two race conditions in RecyclerBinder. #917

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 69 additions & 6 deletions litho-core/src/main/java/com/facebook/litho/ComponentTree.java
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ RenderUnitIdGenerator getRenderUnitIdGenerator() {
return mRenderUnitIdGenerator;
}

@GuardedBy("this")
private @Nullable WorkingRangeAndPositionHolder mPendingWorkingRangeAndPosition;

public interface MeasureListener {

/**
Expand Down Expand Up @@ -1916,13 +1919,33 @@ public synchronized void checkWorkingRangeAndDispatch(
int lastVisibleIndex,
int firstFullyVisibleIndex,
int lastFullyVisibleIndex) {
if (mCommittedLayoutState != null) {
checkWorkingRangeAndDispatch(
new WorkingRangeAndPositionHolder(
position,
new WorkingRangeHolder(
firstVisibleIndex,
lastVisibleIndex,
firstFullyVisibleIndex,
lastFullyVisibleIndex)));
}

/**
* Check if the any child components stored in {@link LayoutState} have entered/exited the working
* range, and dispatch the event to trigger the corresponding registered methods.
*/
public synchronized void checkWorkingRangeAndDispatch(
WorkingRangeAndPositionHolder workingRangeAndPosition) {
if (mCommittedLayoutState == null) {
if (ComponentsConfiguration.useReliableWorkingRange) {
mPendingWorkingRangeAndPosition = workingRangeAndPosition;
}
} else {
mCommittedLayoutState.checkWorkingRangeAndDispatch(
position,
firstVisibleIndex,
lastVisibleIndex,
firstFullyVisibleIndex,
lastFullyVisibleIndex,
workingRangeAndPosition.position,
workingRangeAndPosition.workingRange.firstVisibleIndex,
workingRangeAndPosition.workingRange.lastVisibleIndex,
workingRangeAndPosition.workingRange.firstFullyVisibleIndex,
workingRangeAndPosition.workingRange.lastFullyVisibleIndex,
mWorkingRangeStatusHandler);
}
}
Expand All @@ -1936,6 +1959,7 @@ private synchronized void clearWorkingRangeStatusHandler() {
}

mWorkingRangeStatusHandler.clear();
mPendingWorkingRangeAndPosition = null;
}

/**
Expand Down Expand Up @@ -2689,6 +2713,15 @@ private synchronized void commitResolutionResult(
if (mTreeState != null) {
mTreeState.unregisterRenderState(resolutionResult.treeState);
}

WorkingRangeAndPositionHolder workingRangeAndPosition = null;
synchronized (this) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This lock seems unnecessary, because commitResolutionResult is synchronized. Can we remove it?

workingRangeAndPosition = mPendingWorkingRangeAndPosition;
mPendingWorkingRangeAndPosition = null;
}
if (workingRangeAndPosition != null) {
checkWorkingRangeAndDispatch(workingRangeAndPosition);
}
}

private void requestLayoutWithSplitFutures(
Expand Down Expand Up @@ -3479,6 +3512,36 @@ public void run() {
}
}

/** Holds an item's position and working range data. */
public static class WorkingRangeAndPositionHolder {
public final int position;
public final WorkingRangeHolder workingRange;

public WorkingRangeAndPositionHolder(int position, WorkingRangeHolder workingRange) {
this.position = position;
this.workingRange = workingRange;
}
}

/** Holds working range data. */
public static class WorkingRangeHolder {
public final int firstVisibleIndex;
public final int lastVisibleIndex;
public final int firstFullyVisibleIndex;
public final int lastFullyVisibleIndex;

public WorkingRangeHolder(
int firstVisibleIndex,
int lastVisibleIndex,
int firstFullyVisibleIndex,
int lastFullyVisibleIndex) {
this.firstVisibleIndex = firstVisibleIndex;
this.lastVisibleIndex = lastVisibleIndex;
this.firstFullyVisibleIndex = firstFullyVisibleIndex;
this.lastFullyVisibleIndex = lastFullyVisibleIndex;
}
}

private @Nullable LayoutState calculateLayoutState(
ComponentContext context,
Component root,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,12 @@ public static boolean isSplitResolveAndLayoutWithSplitHandlers() {

public static boolean enableMountableInFacecast = false;

/**
* Attempts to compute the working range when a collection or its items are not yet laid out will
* save the desired working range, and apply it once layout completes.
*/
public static boolean useReliableWorkingRange = false;

public static boolean enableMountableInStoriesViewerFeelingsSticker = false;

private static boolean sReduceMemorySpikeUserSession = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import com.facebook.litho.ComponentContext;
import com.facebook.litho.ComponentTree;
import com.facebook.litho.ComponentTree.MeasureListener;
import com.facebook.litho.ComponentTree.WorkingRangeAndPositionHolder;
import com.facebook.litho.ComponentTree.WorkingRangeHolder;
import com.facebook.litho.ErrorEventHandler;
import com.facebook.litho.LithoLifecycleListener;
import com.facebook.litho.LithoLifecycleProvider;
Expand Down Expand Up @@ -116,6 +118,9 @@ interface ComponentTreeMeasureListenerFactory {
@GuardedBy("this")
private int mLastRequestedHeightSpec = UNINITIALIZED;

@GuardedBy("this")
private WorkingRangeAndPositionHolder mPendingWorkingRangeAndPosition = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add @nullable to this field to make it Nullsafe?


public static Builder create() {
return new Builder();
}
Expand Down Expand Up @@ -426,19 +431,30 @@ synchronized void setMeasuredHeight(int height) {
mLastMeasuredHeight = height;
}

synchronized void checkWorkingRangeAndDispatch(
void checkWorkingRangeAndDispatch(
int position,
int firstVisibleIndex,
int lastVisibleIndex,
int firstFullyVisibleIndex,
int lastFullyVisibleIndex) {
if (mComponentTree != null) {
mComponentTree.checkWorkingRangeAndDispatch(
position,
firstVisibleIndex,
lastVisibleIndex,
firstFullyVisibleIndex,
lastFullyVisibleIndex);
checkWorkingRangeAndDispatch(
new WorkingRangeAndPositionHolder(
position,
new WorkingRangeHolder(
firstVisibleIndex,
lastVisibleIndex,
firstFullyVisibleIndex,
lastFullyVisibleIndex)));
}

synchronized void checkWorkingRangeAndDispatch(
Copy link
Contributor

@pentiumao pentiumao Dec 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like we broke a test case due to this change, can we fix it? Thanks.

I think we should override this method for TestComponentTreeHolder as well like this.

WorkingRangeAndPositionHolder workingRangeAndPosition) {
if (mComponentTree == null) {
if (ComponentsConfiguration.useReliableWorkingRange) {
mPendingWorkingRangeAndPosition = workingRangeAndPosition;
}
} else {
mComponentTree.checkWorkingRangeAndDispatch(workingRangeAndPosition);
}
}

Expand Down Expand Up @@ -503,6 +519,10 @@ private void ensureComponentTree(ComponentContext context) {
if (mPendingNewLayoutListener != null) {
mComponentTree.setNewLayoutStateReadyListener(mPendingNewLayoutListener);
}
if (mPendingWorkingRangeAndPosition != null) {
mComponentTree.checkWorkingRangeAndDispatch(mPendingWorkingRangeAndPosition);
mPendingWorkingRangeAndPosition = null;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
import com.facebook.litho.ComponentLogParams;
import com.facebook.litho.ComponentTree;
import com.facebook.litho.ComponentTree.MeasureListener;
import com.facebook.litho.ComponentTree.WorkingRangeAndPositionHolder;
import com.facebook.litho.ComponentTree.WorkingRangeHolder;
import com.facebook.litho.ComponentUtils;
import com.facebook.litho.ComponentsLogger;
import com.facebook.litho.ComponentsReporter;
Expand Down Expand Up @@ -221,6 +223,8 @@ public void run() {
private final RunnableHandler mPreallocateMountContentHandler;
private final boolean mPreallocatePerMountSpec;

private @Nullable WorkingRangeHolder mPendingWorkingRange;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This private field is never accessed, can you clarify its purpose? Thanks!


private MeasureListener getMeasureListener(final ComponentTreeHolder holder) {
return new MeasureListener() {
@Override
Expand Down Expand Up @@ -1184,7 +1188,8 @@ private boolean isRecyclerViewTargetComputingLayout() {
public void setSubAdapterModeRecyclerView(RecyclerView recyclerView) {
if (!mIsSubAdapter) {
throw new IllegalStateException(
"Cannot set a subadapter RecyclerView on a RecyclerBinder which is not in subadapter mode.");
"Cannot set a subadapter RecyclerView on a RecyclerBinder which is not in subadapter"
+ " mode.");
}

registerDrawListener(recyclerView);
Expand All @@ -1195,7 +1200,8 @@ public void setSubAdapterModeRecyclerView(RecyclerView recyclerView) {
public void removeSubAdapterModeRecyclerView(RecyclerView recyclerView) {
if (!mIsSubAdapter) {
throw new IllegalStateException(
"Cannot remove a subadapter RecyclerView on a RecyclerBinder which is not in subadapter mode.");
"Cannot remove a subadapter RecyclerView on a RecyclerBinder which is not in subadapter"
+ " mode.");
}

unregisterDrawListener(recyclerView);
Expand Down Expand Up @@ -2335,7 +2341,8 @@ public void measure(
shouldMeasureItemForSize(widthSpec, heightSpec, scrollDirection, canRemeasure);
if (mHasManualEstimatedViewportCount && shouldMeasureItemForSize) {
throw new RuntimeException(
"Cannot use manual estimated viewport count when the RecyclerBinder needs an item to determine its size!");
"Cannot use manual estimated viewport count when the RecyclerBinder needs an item to"
+ " determine its size!");
}

mIsInMeasure.set(true);
Expand Down Expand Up @@ -3330,26 +3337,36 @@ void onNewWorkingRange(
int lastVisibleIndex,
int firstFullyVisibleIndex,
int lastFullyVisibleIndex) {
onNewWorkingRange(
new WorkingRangeHolder(
firstVisibleIndex, lastVisibleIndex, firstFullyVisibleIndex, lastFullyVisibleIndex));
}

private void onNewWorkingRange(WorkingRangeHolder workingRange) {
if (mEstimatedViewportCount == UNSET
|| firstVisibleIndex == RecyclerView.NO_POSITION
|| lastVisibleIndex == RecyclerView.NO_POSITION) {
|| workingRange.firstVisibleIndex == RecyclerView.NO_POSITION
|| workingRange.lastVisibleIndex == RecyclerView.NO_POSITION) {
if (ComponentsConfiguration.useReliableWorkingRange) {
mPendingWorkingRange = workingRange;
}
return;
}

final int rangeSize = Math.max(mEstimatedViewportCount, lastVisibleIndex - firstVisibleIndex);
final int rangeSize =
Math.max(
mEstimatedViewportCount,
workingRange.lastVisibleIndex - workingRange.firstVisibleIndex);
final int layoutRangeSize = (int) (rangeSize * mRangeRatio);
final int rangeStart = Math.max(0, firstVisibleIndex - layoutRangeSize);
final int rangeStart = Math.max(0, workingRange.firstVisibleIndex - layoutRangeSize);
final int rangeEnd =
Math.min(firstVisibleIndex + rangeSize + layoutRangeSize, mComponentTreeHolders.size() - 1);
Math.min(
workingRange.firstVisibleIndex + rangeSize + layoutRangeSize,
mComponentTreeHolders.size() - 1);

for (int position = rangeStart; position <= rangeEnd; position++) {
final ComponentTreeHolder holder = mComponentTreeHolders.get(position);
holder.checkWorkingRangeAndDispatch(
position,
firstVisibleIndex,
lastVisibleIndex,
firstFullyVisibleIndex,
lastFullyVisibleIndex);
new WorkingRangeAndPositionHolder(position, workingRange));
}
}

Expand All @@ -3373,7 +3390,15 @@ private void computeRange(int firstVisible, int lastVisible, RecyclerRangeTraver
final boolean didRangeExtremitiesChange;

synchronized (this) {
if (!isMeasured() || mEstimatedViewportCount == UNSET) {
// When mHasDynamicItemHeight is set, isMeasured() always returns false, to trigger re-measure
// of the RecyclerView whenever a new child item is measured. However, this causes this
// function to return early, preventing layout ranges from being applied.
boolean isMeasured =
ComponentsConfiguration.useReliableWorkingRange && mHasDynamicItemHeight
? mIsMeasured.get()
: isMeasured();

if (!isMeasured || mEstimatedViewportCount == UNSET) {
return;
}

Expand Down