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

Use explicit interval tree query for visibility test #10

Closed
wants to merge 4 commits into from

Conversation

rreusser
Copy link

This is a pretty major change so I don't expect a simple merge, but I thought I'd share my approach. I don't know if the swapping approach is the best since it'd be great to handle this on the objective c/gpu/whatnot level, but until then, I was trying to improve the robustness of the visibility check since it's been a challenge to optimize both robustness and performance (i.e. a small preemptiveLoading number results in cells being hidden accidentally; a large number slows it down a bit).

It wasn't quite clear to me what the built-in check was doing, so I thought I'd try maintaining an interval tree explicitly. When a cell is laid out, it registers or updates its bounds in an interval tree. On each scroll event, the union of the previous visible range and current visible range is queried for intersecting cells in order to determine any visibility changes. I didn't like using onScroll, but a quick check indicates the check comes back under 1ms, so it shouldn't be a bottleneck at least.

It accepts a preemptiveLoadDistance that defines the distance in pixels outside the visible range at which cells become visible.

Final question: I had a hard time following the subtleties of the discussion at facebook/react-native#499 . Is there an approach yet that makes all of this obsolete?

@sghiassy
Copy link
Owner

Interval tree... very intriguing. I'll check out the PR tomorrow and get back to you.... thx for the PR!

@rreusser
Copy link
Author

A fun weekend project, but I'm comparing this against the removeClippedSubviews approach that might be wayyyy better.

@sghiassy
Copy link
Owner

I originally tried using removeClippedSubviews but that still had O(n) memory growth in my testing.

That was back in React Native 0.8 so they might've improved it by now

@rreusser
Copy link
Author

Okay, this is not a very scientific test (should have profiled allocations), but I scrolled through an image feed for three minutes and here's what I have:

ListView without removedClippedSubviews

Memory increase linearly. Scrolling is very choppy.
listview-no-remove-clipped

ListView with removeClippedSubviews

Uses removeClippedSubviews and overflow:hidden as described here. Memory increases linearly and is about the same as without removeClippedSubviews (I accidentally tapped the images a couple times and may not have scrolled at precisely the same rate so there's a margin of error). Scrolling is pretty smooth and when you snap to the top the image is present immediately.
listview-remove-clipped

SGListView with removeClippedSubviews

Memory increases but much less than without SGListView. I should have profiled allocations to see if it's the same as @sghiassy's test in the README. There's some flickering and stuttering because it's rerendering views but pretty smooth for realistic usage.
sglistview-remove-clipped

SGListView with interval tree and removeClippedSubviews

Basically the same as with @sghiassy's implementation except it maybe solves the issue where cells are accidentally removed even when they're onscreen.
interval-tree-remove-clipped-remove-clipped

@rreusser
Copy link
Author

I guess I conclude from this that the memory grown isn't ideal, but maybe it was the lack of removeClippedSubviews that was causing it to be quite so janky. So maybe the answer is removeClippedSubviews if you're okay with some memory growth for ridiculously long scrolling sessions, and use SGListView if you really need to prevent that and a bit of rendering lag is okay with you—although I'm testing now to see if a much larger render-ahead distance helps that.

@rreusser
Copy link
Author

Larger render-ahead distance indeed prevents seeing unrendered rows, but there'll always be swapping going on, so if your constraint is smoothness and memory growth is permissible, then ListView is the smoothest.

@rreusser
Copy link
Author

While walking home I've realized that I've totally failed to handle re-layout correctly. All of the intervals need an update when any one changes. I can fix that. (Edit: need to check. But if things get an onLayout when they move, then this should be handled automagically. I'll confirm either way.)

@robertjpayne
Copy link

@rreusser in your tests did you ensure you added overflow: 'hidden' on each ListView row rather than the ListView itself?

Using react-native 0.11.0 I don't see as explosive of memory growth doing that as that ensures Image/Text nodes are destroyed.

@rreusser
Copy link
Author

@robertjpayne thanks for the suggestion/critique! I'll check this tomorrow. I added it to the rows inside the SGListViewCells but not to the SGListViewCells where it may need to go. The memory management and rendering is moderately opaque to me so I appreciate any suggestions!

@rreusser
Copy link
Author

Interesting. The discrepancy might only be in the profiling tool. removeClippedSubviews with a regular ListView shows linearly increasing memory pretty consistent with what's above (of the presumably bad sort). If I use the Instruments profiler though, the memory increases, but at a rate much more in line with the SGListView approach. Maybe the difference is an image/rendering cache that's not catastrophic. Hard to say. Just guesswork on my part, but maybe removeClippedSubviews is perfectly adequate on its own (much smoother, too).

@sghiassy
Copy link
Owner

@rreusser - Thanks again for the PR.

I really like the interval-tree approach. It's the right data-structure for the job and am committed to bringing it into the repo if you are, but the current PR breaks things.

I didn't do a deep dive on what's breaking, but here's a debugger screenshot showing some early NaN values.

review

// yt1 yt2
//
var scrollProperties = this.refs.nativeListView.scrollProperties
var y1 = scrollProperties.offset
Copy link
Owner

Choose a reason for hiding this comment

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

scrollProperties doesn't have property offset Perhaps something changed between 0.8.0 and current?

Copy link
Author

Choose a reason for hiding this comment

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

Hmm. That may indeed be the case. I'm using react-native 0.11.2.

@rreusser
Copy link
Author

It'll take a few minutes to create a new project with react native 0.8.0 and figure out what's breaking there, but I've committed code style and logic cleanup that should address the other issues.

Yeah, strictly speaking since the cells are adjacent and non-overlapping, this could all be accomplished more 'easily' with an ordered list of cell locations and a binary search, but since the interval tree is already implemented, this approach has the advantage of simplifying the difficult part to like two or three lines of code. 😄

I should be able to give this a once-over for correctness and determine the 0.8 issue in the next day or two.

@rreusser
Copy link
Author

Sorry I haven't made progress on this! We've been in the process of getting our thing out the door and removeClippedSubviews has basically worked perfectly! Would still like to finish this off in case anyone finds it useful though…

@rreusser
Copy link
Author

Okay, I finally added a simple example. Works great, except the cells are initially invisible since they're rendered asynchronously. Works fine as soon as you scroll or if you use a settimeout before testing visibility. Appears though that I was a too confident. 😦

screen shot 2015-11-24 at 11 29 31 am

@sghiassy
Copy link
Owner

So what's your thoughts on the PR? Want to close it out? Or is there a path forward?

@roysG
Copy link

roysG commented Nov 16, 2016

Does it fix the problem for #21 ?

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.

4 participants