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

Calculate average item size #296

Merged
merged 23 commits into from
Apr 8, 2022
Merged

Calculate average item size #296

merged 23 commits into from
Apr 8, 2022

Conversation

naqvitalha
Copy link
Collaborator

@naqvitalha naqvitalha commented Apr 6, 2022

Description

resolves #297

FlashList will compute average sizes from onLayout callbacks. I've introduced an average window so that the list reacts quickly to size changes and we can avoid worrying about cleaning up average based on prop changes. This is needed because average for a large number of items can become very rigid. Swapping out total count for a smaller number was another way to achieve the same thing but deciding where to do it was a problem.

I'm not changing estimatedItemSize requirement as part of this. This is a behind the scene optimization for v0.5 milestone. I'd want to discuss and make the prop optional as part of OSS milestone.

Reviewers’ hat-rack 🎩

  • Open GridLayoutProviderWithProps.ts file
  • Log averageItemSize at the end of reportItemLayout method and see it updating in realtime

Check if the values look correct and constantly update with changes.

Checklist

@naqvitalha
Copy link
Collaborator Author

I'll walk you folks through the changes in our next sync up.

Copy link
Contributor

@fortmarek fortmarek left a comment

Choose a reason for hiding this comment

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

Thanks for making all those tests 👏 I have a couple of smaller things but mostly looks good 💯

documentation/docs/fundamentals/usage.md Outdated Show resolved Hide resolved
documentation/docs/fundamentals/usage.md Outdated Show resolved Hide resolved
documentation/docs/fundamentals/usage.md Outdated Show resolved Hide resolved
src/GridLayoutProviderWithProps.ts Outdated Show resolved Hide resolved
src/GridLayoutProviderWithProps.ts Show resolved Hide resolved
this.currentCount = newCount;
}

protected getStoredValues() {
Copy link
Contributor

Choose a reason for hiding this comment

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

could be a getter method if we rename the underlying variable to have underscore.

import { AverageWindow } from "../utils/AverageWindow";

class AverageWindowTest extends AverageWindow {
public get storedValues(): (number | undefined)[] {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't really see why this is necessary? As commented in AverageWindow, let's make this method part of AverageWindow itself and remove getStoredValues

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Point was to make sure storedValues isn't a public API. I don't want people to ever have access unless it's for tests. I just saw this issue in TS: microsoft/TypeScript#19335
It seems like string accessor is an escape hatch for testing :) I'll try and use that

Comment on lines +39 to +48
const reduceObj = inputValues.reduce(
(obj, val) => {
if (val !== undefined) {
obj.sum += val;
obj.count++;
}
return obj;
},
{ sum: 0, count: 0 }
);
Copy link
Contributor

Choose a reason for hiding this comment

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

can't we compute the value outside the test and then use absolute value? I don't really like adding non-trivial logic to a test just for the sake of using it as an assert.

Copy link
Collaborator Author

@naqvitalha naqvitalha Apr 7, 2022

Choose a reason for hiding this comment

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

This logic only belongs to this test and totally irrelevant outside. We wouldn't want this code to be in average window. This is verifying if running average is same as what we'd get from explicit calculation.

Copy link
Contributor

@fortmarek fortmarek Apr 8, 2022

Choose a reason for hiding this comment

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

I understand but I still think it would be better for the assert to rather work with hard values instead of computing them on the go. But it's not a big deal, so we can keep it.

src/__tests__/FlashList.test.tsx Outdated Show resolved Hide resolved
src/__tests__/FlashList.test.tsx Show resolved Hide resolved
naqvitalha and others added 7 commits April 7, 2022 10:35
Co-authored-by: Marek Fořt <marek.fort@shopify.com>
Co-authored-by: Marek Fořt <marek.fort@shopify.com>
Co-authored-by: Marek Fořt <marek.fort@shopify.com>
@naqvitalha
Copy link
Collaborator Author

@fortmarek I've addressed the comments. Please check again when you have time.

Copy link
Contributor

@fortmarek fortmarek left a comment

Choose a reason for hiding this comment

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

There are some discussions but most of them low prio, so nothing holding back to merge this from my side.

/**
* Can be used to get the current average value
*/
public get currentValue(): number {
Copy link
Contributor

Choose a reason for hiding this comment

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

+1 to Marek, I'd rather have a longer, but more obvious meaning.
averageWindow.current doesn't mean currentAverage imho, it could be an instance of averageWindow for example

Comment on lines +29 to +40
const target = this.getNextIndex();
const oldValue = this.inputValues[target];
const newCount =
oldValue === undefined ? this.currentCount + 1 : this.currentCount;

this.inputValues[target] = value;

this.currentAverage =
this.currentAverage * (this.currentCount / newCount) +
(value - (oldValue ?? 0)) / newCount;

this.currentCount = newCount;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const target = this.getNextIndex();
const oldValue = this.inputValues[target];
const newCount =
oldValue === undefined ? this.currentCount + 1 : this.currentCount;
this.inputValues[target] = value;
this.currentAverage =
this.currentAverage * (this.currentCount / newCount) +
(value - (oldValue ?? 0)) / newCount;
this.currentCount = newCount;
// The index of the next value to be added
const targetIndex = this.getNextIndex();
// Current value at the target index. can be undefined if we are adding the first value
const oldValue = this.inputValues[targetIndex];
// If no value was previously at the target index, we need to increase the number of items
const newCount =
oldValue === undefined ? this.numberOfItems + 1 : this.numberOfItems;
// Set the new value at the target index
this.inputValues[targetIndex] = value;
const targetValueDiff = value - (oldValue ?? 0);
const newAverage =
(this.currentAverage * this.numberOfItems + targetValueDiff) / newCount;
this.currentAverage = newAverage;
this.numberOfItems = newCount;

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In running average it's better to divide number of items with new count before multiplying. This way the output of the multiplication can never overshoot number's max size.

e.g, currentAverage * numberOfItems can be very big if numbers you're averaging are large and can go over what the variable can handle.

public newLayoutManager(
renderWindowSize: Dimension,
isHorizontal?: boolean,
cachedLayouts?: Layout[]
): LayoutManager {
// Average window is updated whenever a new layout manager is created. This is because old values are not relevant anymore.
const estimatedItemCount = Math.max(
Copy link
Contributor

Choose a reason for hiding this comment

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

What does 3 mean here?

@@ -698,6 +705,12 @@ class FlashList<T> extends React.PureComponent<
return currentOffset >= this.distanceFromWindow;
}

private onItemLayout = (index: number) => {
// Informing the layout provider about change to an item's layout. It already knows the dimensions so there's not need to pass them.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Informing the layout provider about change to an item's layout. It already knows the dimensions so there's not need to pass them.
// Informing the layout provider about change to an item's layout. It already knows the dimensions so there's no need to pass them.

*
* @param value Add new value to the average window and updated current average
*/
public addValue(value: number): void {
Copy link
Contributor

Choose a reason for hiding this comment

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

+1, I first thought it's related to UI window. However, reference to Sliding Window is helpful to understand the meaning of what we do here. AverageSlidingWindow? AverageValueTracker is good too.

@naqvitalha naqvitalha merged commit 7e16f0d into main Apr 8, 2022
@naqvitalha naqvitalha deleted the automaticAverage branch April 8, 2022 19:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Calculate average item sizes internally
3 participants