Skip to content

Load uitableviews faster? #211

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

Closed
crrobinson14 opened this issue Nov 29, 2016 · 21 comments
Closed

Load uitableviews faster? #211

crrobinson14 opened this issue Nov 29, 2016 · 21 comments

Comments

@crrobinson14
Copy link

Is there anything I can do to speed up UITableView loads? Even when using persistent/cached data (and I've confirmed this via Airplane Mode) it takes a long time to populate a UITableView with its first cells. Here's a quick/dirty NSLog dump population a table with a few rows:

2016-11-29 11:41:32.332 News Rush Local[872:625725] TIMER1: VIEWDIDLOAD
2016-11-29 11:41:32.333 News Rush Local[872:625725] TIMER2: VIEWWILLAPPEAR
2016-11-29 11:41:32.335 News Rush Local[872:625725] TIMER3: SET DATASOURCE
...
2016-11-29 11:41:33.067 News Rush Local[872:625725] TIMER4: POPULATE CELL
2016-11-29 11:41:33.105 News Rush Local[872:625725] TIMER4: POPULATE CELL
2016-11-29 11:41:33.121 News Rush Local[872:625725] TIMER4: POPULATE CELL
2016-11-29 11:41:33.141 News Rush Local[872:625725] TIMER4: POPULATE CELL
2016-11-29 11:41:33.159 News Rush Local[872:625725] TIMER4: POPULATE CELL

I set up the query in viewWillAppear, hence the tracking for those. After setting up the query and datasource, there's about a 700ms wait before rows begin to populate. This delay is VERY visually noticeable particularly because we have a "You have not yet..." empty message for this table encouraging users to interact with the app to get data to appear.

This delay occurs EVERY time the view appears, not just the first time, and as I said, it happens even while in Airplane Mode using offline data, so it's not network-related.

I don't mind if the solution is some sort of hack - I like the workflow here enough that a hack would be fine. Does anybody have any idea how to speed this up? It would be really ideal if we could get this into the <200ms range.

@morganchen12
Copy link
Contributor

Can you provide an instruments (time profiler) trace for what's taking the most time? If it's not something in FirebaseUI I'll file an internal issue so the core Firebase team can take a look.

It's possible an instruments trace won't be that useful if it's all asynchronous waiting, but it's worth a try nonetheless.

@crrobinson14
Copy link
Author

Sure, I just took one. Do you want me to attach it here? I didn't see anything compelling in it but am happy to try any/all messing around to improve this. FirebaseUI is a really great code pattern for us!

@morganchen12
Copy link
Contributor

If it's got symbols of your project that you'd like to keep hidden from public view, you can email it to me at morganchen@google.

@crrobinson14
Copy link
Author

This @morganchen12, I emailed you the trace. Let me know if anything else would be helpful - I'll try to do some more debugging on this side as well. We can make a data set available publicly if it helps at all.

@morganchen12
Copy link
Contributor

Looks like FirebaseUI is causing some slowdown here, probably since we begin and end updates after every insert. It's still not the majority of the time spent here, but should make your app feel more responsive if we were to get rid of it.

Having some formal initialization for FUIArray has been proposed in the past but not implemented for reasons I can't remember. For now you can work around it by wrapping FUIArray in your own data source and using reloadData instead of beginUpdates/endUpdates.

@crrobinson14
Copy link
Author

I'm coming out of Realm, and wonder if the technique used there would work here. Basically, they get one main data set immediately when loading the view, and render that in the tableview with a traditional, non-animated reloadData. Then they subscribe to CHANGES to that result set. The change list nets together adds/updates/deletes and is handled in one begin/end update block.

I'm not really sure how to go about that here, though. I'm not too worried about netting together the updates because in practice, most updates tend to be a single operation anyway (once the table is rendered the first time). But how would one go about the first piece, doing a traditional reloadData for the first rendering pass?

@morganchen12
Copy link
Contributor

morganchen12 commented Nov 29, 2016

If your query is limited to some number of objects (which it should be if your collection in Firebase Database may be unbounded), you can track insertions until you reach that number, then call reloadData once, then set some state that says the initialization has finished.

It's gross because you have to attach state to your objects but otherwise should be relatively simple.

Having initialization from the Firebase queries themselves is possibly something work looking at.

@crrobinson14
Copy link
Author

That makes sense, but it sounds like it precludes using FirebaseUI and doing the data source management manually, no?

@morganchen12
Copy link
Contributor

Yes, to an extent--you'd only have to write your own data source, but you could reuse FUIArray.

@crrobinson14
Copy link
Author

It seems from reviewing the developer docs that FIRDataEventTypeValue might be useful here, in that one could listen for an entire-list event at first, render the table from that result set, then start listening to change events from there. I'm not sure what the best way to de-dupe the "Insert" events might be, and would welcome any input on that score...

If I find a clean solution to this would you entertain a PR to enhance FUITableViewDataSource to do this, or is this an edge case you'd prefer to leave to clients?

@morganchen12
Copy link
Contributor

I think we should provide initialization and behavior for changing the query to accommodate the relatively common case of having an unbounded list that a user scrolls through that only loads some content at a time.

Say you have a Twitter-like app that loads the first ~50 items when launched, but as the user scrolls you want to load more. Our goals here should be:

  • Make it easy to append/load extra stuff by modifying the limit on the existing query.
  • Don't re-download stuff we've already downloaded.
  • Handle batch updates (including initial download) separately from individual insertions/changes.

This would solve your problem while still being adaptable to other use cases.

@crrobinson14
Copy link
Author

Is there anything I can do to facilitate that?

FYI I just did some more testing with a combination of persistence settings, attempts to pre-subscribe to the data set, moving the population work to viewDidLoad and so on. It's definitely what you think it is in terms of the performance issue. If I just subscribe to FIRDataEventTypeValue to get an immediate result set, that query itself returns in 81ms:

2016-11-29 16:45:29.139 News Rush Local[935:673199] TIMER3: SET DATASOURCE
2016-11-29 16:45:29.220 News Rush Local[935:673199] TIMER4: Got value set with 214 rows

My particular application is more interested in value updates than live insert/delete operations so a combination of FIRDataEventTypeValue to populate the table plus FIRDataEventTypeChildChanged to detect updates is fine for me. I'm making a custom version of FUITableViewDataSource that just does this, but am happy to help wherever possible with what you described above.

@morganchen12
Copy link
Contributor

PRs are always welcome!

@crrobinson14
Copy link
Author

crrobinson14 commented Dec 3, 2016

One thing I'm experimenting with right now is using FUIArray directly, but adding a FIRDataEventTypeValue observer to the query to determine the SIZE of the initial data set (just getting the row count), then setting this in a class-local variable. I also keep a BOOL loaded variable.

I then evaluate against this in my FUIArrayDelegate -didAddObject: callback. If loaded is NO and the entry count is less than the expected data set size, I do nothing. Once the entry count >= the data set size, I set loaded to YES, call reloadData on the tableView, then do incremental/animated updates from then on.

This doesn't seem to solve the entire problem but it does seem to be a lot faster. I'd appreciate your thoughts on this pattern and how it aligns with how you mean this library to be used...

In particular, are there holes/edge cases where FIRDataEventTypeValue could possibly return a different childCount value, particularly a LARGER one, than the number of times didAddObject gets called? So far I haven't seen that happen, but obviously this logic would break in that case...

@morganchen12
Copy link
Contributor

It's reasonable and does solve your particular problem, but lacks flexibility. I think an ideal solution in terms of speed would be to only load data through .value and diff the data on the client, which would align more nicely with UIKit's batch update methods without sacrificing flexibility.

Alternatively, a simpler solution if you don't mind discarding animations would be to create a dumber array that only sends events on any update and call reloadData on every .value event.

@crrobinson14
Copy link
Author

crrobinson14 commented Dec 5, 2016

Yeah, I'm trying to marry these two things together and it's a little tricky. It seems to me that iOS really expects you to be providing an initial, already-loaded dataset by or before viewWillAppear, to provide the best visual experience. There's a dataSource call to ...rowsInSection just after that, and although I obv. don't have the source for this I can imagine the Apple core devs wired up tableviews such that the initial data load automatically doesn't have animation.

For a lot of my data sets I do actually know what I'm going to need and when ahead of time. I wonder if the best thing to do would be to pre-subscribe to these things, not the "normal Firebase way" of

FIRDatabaseReference *scoresRef = [[FIRDatabase database] referenceWithPath:@"scores"];
[scoresRef keepSynced:YES];

but rather having a database service singleton that set up these FUIArrays when the app started and just provided references to them when the views needed them. This would keep a lot of data in memory so I need to figure out whether that's practicable - my data isn't huge, but I don't want to be too crazy here. But for 6-8 data sets it's probably manageable, and would let the views themselves load almost instantly, no?

Does this sound crazy/stupid? Is there any better way? I feel like I'm overbuilding it here but remember, there's just one simple initial goal: user complaints that on every view appearance, the tables animate-reload with data they already saw. Any way of eliminating that is problem-solved...

@morganchen12
Copy link
Contributor

Table views should behave fine when empty.

If you'd like to preload stuff, I'd suggest you use Reachability to check if you're on wifi first and preload content more aggressively if you are. There's nothing wrong with loading content beforehand, especially if it's content you can guarantee your user will see and it's content that's valuable. Try to avoid doing things like preloading ads before actual content on mobile data.

Preloading with a singleton can sometimes add massive complexity to your app/tests.

@crrobinson14
Copy link
Author

No, the pre-loading is just a goose chase. The only important goal is to eliminate the initial on-load animation when the first records are being displayed. In iOS, tableviews aren't supposed to animate-load - animations are usually for updates, not load events. It's disconcerting to the user because the 700ms or so it takes this to get through makes the views look very "slow".

If there's a quick way to do this it would be ideal, even if it's a hack, that would be great. If not, manually managing the FUIArray, using Value to get the initial child count, and inhibiting animations in didAddObject until that number is reached seems to be working ok.

@morganchen12
Copy link
Contributor

morganchen12 commented Jan 3, 2017

FYI batching methods are added in #186 in 4cf3b1e, but not merged yet

@morganchen12
Copy link
Contributor

#186 has been merged. Release coming soon.

@morganchen12
Copy link
Contributor

v2.0.0 has been released. This behavior should be fixed automatically for UITableView if you're using FUITableViewDataSource.

Feel free to reopen if you run into any issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants
@crrobinson14 @morganchen12 and others