-
Notifications
You must be signed in to change notification settings - Fork 520
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
[Xamarin.iOS] UICollectionViewDelegate -> GetSizeForItem() will freeze app #4923
Comments
Wow thanks for the test case this is great I could reproduce the issue right away with this environment: https://gist.github.com/VincentDondain/56e267066c8896b310a683219e292814 It's very precise Will report back after more investigation. |
There should not be reflection in the hot path. @VincentDondain will profile it to double check this. The sample is a bit hard to measure. Try changing
when comparing times. |
@projectgoav did you get something similar to: when profiling? Can you point me to what made you think the issue was due to reflexion? |
Mmh your performance issues seem to have as much or more to do with: Commenting the random delay when getting the cells does fix the performance issues though. |
Here is a more accurate time profiling view. Now the questions about what you think are our reflexion issues remain but I can give you a clearer overview of what's happening with However please note that no matter how fast we make The While this operation is "relatively fast", it adds up quickly on a very large number of calls (huge number of cells needing to be refreshed). In particular we, in some cases, need reflexion to convert arguments from native to managed and that's a variable cost we can address. One trick you can use is the following: [Export ("collectionView:layout:sizeForItemAtIndexPath:")]
CGSize FastGetSizeForItem (IntPtr collectionView, IntPtr layout, IntPtr indexPath)
{
callCount++;
// Uncomment line below to track call count (WILL IMPACT PERFORMANCE EVEN MORE)
//Console.WriteLine("GetSizeForItem() called: {0}time(s)", callCount);
return new CGSize (150, 150);
} Here because we're using arguments with IntPtr (no need to change the return type as Here's the same profiling view with the hack in place: 15.12s without the hack VS 184ms with the hack (: Let me know if this helps and answers your specific |
Edit: FastGetSizeForItem was never called in my first set of runs. I've updated this comment to reflect that. Thanks for looking into this! Firstly, I must appologise about the reflection comment. Looking back I realised I was profiling our own application and not my test app... Ooops!
I re-ran my test app with instruments and I got a different stack-trace. I couldn't find anything related to 'GetSizeForItem()' but it was obvious that something was taking a long time. Due to this I then ran my test app in the following conditions with instruments to see the effects. I've attached the file incase it's any use to you? In all cases it was run on my Mac via an iPad Pro (9.7 inch) simulator running iOS 10.3.1 in Debug configuration. For the first 3 runs I disabled that dataRefreshTask. The second 3, I re-enabled it and gave the app a small scroll. Run 1
It took about 5-6seconds of 100% CPU usage before anything was to show in the collection and the app become responsive. As expected the scroll performance was near perfect as nothing else was happening after the initial load. Run 2
It took around 1second for data to appear and the app to become responsive. There is a really noticable difference in the CPU usage from the graph in instruments. Run 3
Takes around 2seconds for the data to appear and the app to become responsive. Run 4
Again, a large amount of CPU usuage for 4-5seconds during initial load and again when the dataRefreshTask ReloadItems() kicks in. Run 5
Largely similiar to Run 2 Run 6
Largely similiar to run 3 with a much shorter freeze when ReloadItems() from the dataRefreshTask kicks in. It certainly looks like it was this the 'trampolining' that was causing the slowdown. |
I had to update my previous comment as FastGetSizeForItem wasn't actually being called. While freezing is less than ideal I can appreciate that calling native to/from Xamarin does incurr a cost and when multiplied by 100s of thousands of times it certainly does add up. However I don't believe I'm seeing the same about of speedup that you have seen with the FastGetSizeForItem |
The call to Class.LookupClass is slower than it need to be, so I've filed #4936 to see if we can fix that. |
@projectgoav you should run/profile on device using release builds for performance profiling. The code generated for simulator + JIT + debug is, for various reasons (different architecture, less optimization by the JIT, less optimization from |
I had a look at your test project, and I have a few comments:
Updated ViewController.cs with all my changes Could you have a look and see if this is a viable solution for your app as well? |
Thanks looking into this and your detailed response! I looked at your changes and tried to port them into our original application. I then deployed them to an iPad Air running iOS 12.1 in Debug mode, to allow me to see some timing information in the console. I'm running VS Mac 7.6.11 (build 9) with Xamarin iOS 12.1.0.15 and XCode 10.1 Changing the Converting to use an exported GetCell method, like in your example above, was a little more difficult. We use the constructed cell instances to do some configuration. One of the libraries we use provides some helpers for the UICollectionViewSource which I worried would interfere with my tests. I managed to strip away as much of that as possible and replicate the helpers we used within tests below. I suspected that since we use the cell instances resolving them from their handle would negate any speedup we might see. By results below largely follow this pattern.
I then removed a similiar LINQ query to the one you commented on in my example. Looping the indexes and manually constructing NSIndexPaths + dispose knocked about 25ms from the overall ReloadItems time. I finally I tried a different method to update the contents in our cells. Instead of calling
If you couple these changes with the I'm sure once the improvements you've made since my inital report are released this may become even faster! |
Hi, glad this helped (: Rolf made some optimizations, see: #4936 (comment) and there's a request for comment issue #5025 for even more gains 🎉 I'm closing this issue since I believe we provided as much info as we could but feel free to report any improvements here or file more performance related issues and we'll be happy to take a look. |
Steps to Reproduce
We have a music application that allows users to browse their own collection or the contents from a number of streaming services. On many screens we display a grid of albums or playlists with an alphamap down the right-hand side. Some of these may have 10s or even 100s of thousands of items.
We virtualise the data we get from external services. We tell the collection view the total number of items in the collection but only load into memory the actual content as the user scrolls (about every 100-200 items). We only reload the cells whose data we've fetched.
Recently we wanted to add the ability for the user to adjust the size of the cells in the grid. To do this we used the
GetSizeForItem()
method as part ofUICollectionViewDelegateFlowLayout
. This is the recommended way in Xamarin and Apple docs.What we've found is on the larger collection sizes the application grinds to a halt, both when first displayed and then when scrolling.
GetSizeForItem()
is called for every item when the layout has updated. A quick profile in Instruments suggests that most of the time is the internals of mono/Xamarin trying to figure out, via reflection(?), if the method has been implemented(?). I believe this is what's causing the application to freeze.If this is indeed the case, I would have maybe expected Xamarin to check when the delegate is first assigned and keep a reference to the method or something similiar after the first call to it.
I've created a small sample app with the following steps:
GetSizeForItem()
inUICollectionViewDelegateFlowLayout
and set as delegate for collection viewUICollectionViewSource
with an item count of:Expected Behavior
Application will scroll without freezing the entire app with large number of cells in a collection.
Actual Behavior
Anything over 1000 items will start to impact the performance when scrolling. At around 10,000 items the app will freeze for multiple seconds everytime you alter a cell or group of cells.
I've seen our app running on a device freeze for 20seconds or more.
Environment
Build Logs
Example Project (If Possible)
I've attached a small sample app with a simulated virtualised collection.
GSFIPerformance_Minimal.zip
The text was updated successfully, but these errors were encountered: