-
Notifications
You must be signed in to change notification settings - Fork 74
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
Conversation
95f3864
to
5b29665
Compare
...zylayout-widget/src/commonMain/kotlin/app/cash/redwood/lazylayout/widget/WindowedLazyList.kt
Outdated
Show resolved
Hide resolved
...zylayout-widget/src/commonMain/kotlin/app/cash/redwood/lazylayout/widget/WindowedLazyList.kt
Outdated
Show resolved
Hide resolved
5b29665
to
f5fe930
Compare
// 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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
f5fe930
to
c5eafbf
Compare
c5eafbf
to
7f230a4
Compare
// 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why subtract a page?
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) |
There was a problem hiding this comment.
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?
val lastPagedItemIndex = (((lastVisibleItemIndex / viewportItemCount) * viewportItemCount) + viewportItemCount * 2).coerceAtMost(items.items.size) | |
val lastPagedItemIndex = (lastVisibleItemIndex + viewportItemCount - 1) / viewportItemCount * viewportItemCount |
...yout-widget/src/commonTest/kotlin/app/cash/redwood/lazylayout/widget/WindowedLazyListTest.kt
Outdated
Show resolved
Hide resolved
// 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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
...yout-widget/src/commonTest/kotlin/app/cash/redwood/lazylayout/widget/WindowedLazyListTest.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this 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 callonViewportChanged(40, 49)
to show 10 elements. - scrolls to
[41..51)
, we callonViewportChanged(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 callonViewportChanged(50, 59)
to show 10 elements
Alternately we could subtract 1 off at the end to adjust back to inclusive/inclusive?
7f230a4
to
fb06b95
Compare
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. |
fb06b95
to
028306a
Compare
Sorry, I didn’t mean to imply a hardcoded page size. I just used examples where the page size as always 10. |
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) |
There was a problem hiding this comment.
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
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
Closing because of #1471 (comment). |
Closes #1256.