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

Reduce viewport updates in LazyList for improved perf with Zipline #1471

Closed
wants to merge 1 commit into from

Conversation

veyndan
Copy link
Contributor

@veyndan veyndan commented Sep 11, 2023

Closes #1256.

@veyndan veyndan force-pushed the veyndan/2023-09-11/paginate-viewport-changes branch 3 times, most recently from 95f3864 to 5b29665 Compare September 11, 2023 15:09
@veyndan veyndan force-pushed the veyndan/2023-09-11/paginate-viewport-changes branch from 5b29665 to f5fe930 Compare September 12, 2023 12:12
// the serialization cost between the host-guest bridge can be expensive.
val viewportItemCount = lastVisibleItemIndex - firstVisibleItemIndex + 1
val firstPagedItemIndex = (((firstVisibleItemIndex / viewportItemCount) * viewportItemCount) - viewportItemCount).coerceAtLeast(0)
val lastPagedItemIndex = ((lastVisibleItemIndex / viewportItemCount) * viewportItemCount) + viewportItemCount * 2
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this need a coerceAtMost of the list size? Or do we not know the upper bound?

Copy link
Contributor Author

@veyndan veyndan Sep 12, 2023

Choose a reason for hiding this comment

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

We know the upper bound that the LazyList is aware of, though that upper bound can dynamically increase (e.g., in the case of Paging). I don't think we necessarily need coerceAtMost (or even coerceAtLeast), but it'll marginally reduce the number of unnecessary onViewportChanged lambda invocations for small lists.

@veyndan veyndan force-pushed the veyndan/2023-09-11/paginate-viewport-changes branch from f5fe930 to c5eafbf Compare September 12, 2023 14:53
@veyndan veyndan enabled auto-merge (squash) September 12, 2023 14:56
@veyndan veyndan force-pushed the veyndan/2023-09-11/paginate-viewport-changes branch from c5eafbf to 7f230a4 Compare September 12, 2023 15:24
// particularly important when using `LazyList` in Treehouse (compared to plain Redwood), as
// the serialization cost between the host-guest bridge can be expensive.
val viewportItemCount = lastVisibleItemIndex - firstVisibleItemIndex + 1
val firstPagedItemIndex = (((firstVisibleItemIndex / viewportItemCount) * viewportItemCount) - viewportItemCount).coerceAtLeast(0)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why subtract a page?

Suggested change
val firstPagedItemIndex = (((firstVisibleItemIndex / viewportItemCount) * viewportItemCount) - viewportItemCount).coerceAtLeast(0)
val firstPagedItemIndex = (firstVisibleItemIndex / viewportItemCount) * viewportItemCount

// the serialization cost between the host-guest bridge can be expensive.
val viewportItemCount = lastVisibleItemIndex - firstVisibleItemIndex + 1
val firstPagedItemIndex = (((firstVisibleItemIndex / viewportItemCount) * viewportItemCount) - viewportItemCount).coerceAtLeast(0)
val lastPagedItemIndex = (((lastVisibleItemIndex / viewportItemCount) * viewportItemCount) + viewportItemCount * 2).coerceAtMost(items.items.size)
Copy link
Collaborator

Choose a reason for hiding this comment

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

To round up, do this?

Suggested change
val lastPagedItemIndex = (((lastVisibleItemIndex / viewportItemCount) * viewportItemCount) + viewportItemCount * 2).coerceAtMost(items.items.size)
val lastPagedItemIndex = (lastVisibleItemIndex + viewportItemCount - 1) / viewportItemCount * viewportItemCount

// Paginate the results so `onViewportChanged` isn't blasted with a bunch of updates. This is
// particularly important when using `LazyList` in Treehouse (compared to plain Redwood), as
// the serialization cost between the host-guest bridge can be expensive.
val viewportItemCount = lastVisibleItemIndex - firstVisibleItemIndex + 1
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would you consider coerceAtLeast() instead of + 1 to make sure it's nonzero?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Replacing + 1 with coerceAtLeast() would mean that we are no longer calculating the number of items in the viewport. The + 1 is to ensure that that is what we're calculating, and the fact that it produces a nonzero is happy happenstance.

Copy link
Collaborator

Choose a reason for hiding this comment

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

What does the + 1 do? Inclusive to exclusive?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm kinda confused by what you're asking, as we're not dealing with a range here. As Jake pointed (#1471 (comment)), we need to do a + 1 to ensure we're calculating the number of items in the viewport.

@veyndan veyndan disabled auto-merge September 12, 2023 15:29
Copy link
Collaborator

@swankjesse swankjesse left a comment

Choose a reason for hiding this comment

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

I wanna nail down exactly what the calls to onViewportChanged() should be, so the guest code has the maximum amount of control over preloading...

I assume that the bounds on the viewport are inclusive, and the two arguments to onViewportChanged() are both inclusive. To make the math easier I’m making the visible end index exclusive below.

Suppose the page size is 10. I think my preference is:

  • scrolls to [40..50), we call onViewportChanged(40, 49) to show 10 elements.
  • scrolls to [41..51), we call onViewportChanged(40, 59) to show 20 elements
  • scrolls to [42, 52), we do nothing
  • scrolls to [49, 59), we do nothing
  • scrolls to [50, 60), we call onViewportChanged(50, 59) to show 10 elements

Alternately we could subtract 1 off at the end to adjust back to inclusive/inclusive?

@veyndan veyndan force-pushed the veyndan/2023-09-11/paginate-viewport-changes branch from 7f230a4 to fb06b95 Compare September 12, 2023 16:11
@veyndan
Copy link
Contributor Author

veyndan commented Sep 12, 2023

I just updated the tests to showcase what the current solution does. In the current case, the "page size" is identical to the "viewport item count". Therefore, we request a page above and below what where we're currently at. With your proposed solution, hard coding a page size of 10 would be okay in most cases, but if you have very large items (think Instagram posts, where you can only fit one full post on a mobile screen), a page size of 10 is much too large.

@veyndan veyndan force-pushed the veyndan/2023-09-11/paginate-viewport-changes branch from fb06b95 to 028306a Compare September 12, 2023 16:29
@veyndan veyndan enabled auto-merge (squash) September 12, 2023 16:32
@swankjesse
Copy link
Collaborator

Sorry, I didn’t mean to imply a hardcoded page size. I just used examples where the page size as always 10.

@JakeWharton
Copy link
Collaborator

I would not let the viewport get within 1 row of needing placeholders. I would think at minimum we should probably keep at least half a viewport's amount of row's offscreen in either direction.

windowedLazyList.updateViewport(49, 58)
windowedLazyList.updateViewport(59, 68)
windowedLazyList.viewportChanges.test {
assertThat(awaitItem()).isEqualTo(30..70)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Expanding it by one page on either side feels like you’re doing the guest code’s work on the host side. Would you consider changing these to 40..60 and 50..70 ?

Are these inclusive or exclusive? 40..60 seems like it’s 10 elements too large for 41..50 as input

@veyndan veyndan disabled auto-merge September 12, 2023 18:19
val proposedPlaceholderPoolSize = visibleItemCount + visibleItemCount / 2
// We only ever want to increase the pool size.
if (placeholderPoolSize < proposedPlaceholderPoolSize) {
placeholderPoolSize = proposedPlaceholderPoolSize
}
state.firstVisibleItemIndex = localFirstVisibleItemIndex
lastVisibleItemIndex = localLastVisibleItemIndex
state.firstVisibleItemIndex = localFirstPagedItemIndex
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@swankjesse @JakeWharton I didn't notice it before, but such a paginated viewport introduces a bug here. This PR means we can no longer reliably expose to the caller of LazyList what the first visible item index is.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we add a third parameter to the lambda for the actual first index?

Copy link
Contributor Author

@veyndan veyndan Sep 13, 2023

Choose a reason for hiding this comment

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

If we did that and didn't update the frequency of the lambda invocation on every scroll, then the first visible item index would still be incorrect due to the new deduplication logic. If the new deduplication logic took into account the actual first index, then we're serializing and deserializing the viewport updates at the same frequency as before.

@veyndan
Copy link
Contributor Author

veyndan commented Dec 7, 2023

Closing because of #1471 (comment).

@veyndan veyndan closed this Dec 7, 2023
@veyndan veyndan deleted the veyndan/2023-09-11/paginate-viewport-changes branch December 7, 2023 12:46
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.

Optimize this for less operations
3 participants