Open eth-ellis opened 6 months ago
I updated the projects to add similar iOS label update debugging. Maui in iOS does not go into the same circular update loop on start up like Android. However, it demonstrates another major problem.
I suspect we would see the same in Android if it were equally testable (ie. if the feedback loop was removed from load first so we can see better).
Again, this goes back to what has seemed blatantly obvious - Maui is overdoing simple work due to the layout system continuously spamming unnecessary updates.
Here we can objectively and reproduceably measure that Maui draws labels in iOS 42% more than Xamarin for the same workload.
Run the project in Maui/Xamarin iOS (iPhone) Debug mode. Let it load. Then click to select in the menu "CardWithComplexContent". Start scrolling down the screen at your own pace until you hit the bottom.
Look at the count of label draws incremented in the Debug output. I did this twice in Xamarin and twice in Maui and got identical results both times on my test device.
Here we are monitoring with:
public override void Draw(CGRect rect) {
base.Draw(rect);
UpdateCounter.addDrawUpdate();
}
Maui Initial Load:
[0:] DRAW UPDATE 1
[0:] DRAW UPDATE 2
[0:] DRAW UPDATE 3
[0:] DRAW UPDATE 4
[0:] DRAW UPDATE 5
[0:] DRAW UPDATE 6
[0:] DRAW UPDATE 7
[0:] DRAW UPDATE 8
[0:] DRAW UPDATE 9
[0:] DRAW UPDATE 10
[0:] DRAW UPDATE 11
[0:] DRAW UPDATE 12
[0:] DRAW UPDATE 13
[0:] DRAW UPDATE 14
[0:] DRAW UPDATE 15
[0:] DRAW UPDATE 16
[0:] DRAW UPDATE 17
Maui Final Label Draw Count:
TRIAL 1:
[0:] DRAW UPDATE 1324 //final update
TRIAL 2:
[0:] DRAW UPDATE 1324 //final update
Xamarin Initial Load:
[0:] DRAW UPDATE 1
[0:] DRAW UPDATE 2
[0:] DRAW UPDATE 3
[0:] DRAW UPDATE 4
[0:] DRAW UPDATE 5
[0:] DRAW UPDATE 6
[0:] DRAW UPDATE 7
[0:] DRAW UPDATE 8
[0:] DRAW UPDATE 9
[0:] DRAW UPDATE 10
[0:] DRAW UPDATE 11
[0:] DRAW UPDATE 12
[0:] DRAW UPDATE 13
[0:] DRAW UPDATE 14
[0:] DRAW UPDATE 15
Xamarin Final Label Draw Count:
TRIAL 1:
[0:] DRAW UPDATE 933 //final update
TRIAL 2:
[0:] DRAW UPDATE 933 //final update
Thus we see consistently in both trials Xamarin needed precisely 933 label draws to initially load, switch page, and then scroll to the bottom, while in both trials Maui needed 1324 label draws to do the exact same thing.
That is 42% more label draws to accomplish the same thing on the same device. A massive deterioration.
I updated the test projects once more to add a Reset Counters button so I could test Android's performance. The outcome was even worse than iOS in the parameter that stood out.
Run the project in Android debug to device. Then click Change Template > CardWIthComplexContent.
Scroll down until the feedback loop stops (it stops spamming feedback loop updates and goes quiet), then scroll back to the top. Click Reset Counters to go back to zero on the update counters. Clear your debug output also perhaps. Then scroll down through the full list with this as your "starting point".
This will not be quite as perfectly consistent as we are not starting from identical conditions each time given the need to escape the initial feedback loop. However, the data is still just as stark as you will see.
Here we are testing with:
public CustomAndroidLabelRenderer() {
this.AddOnLayoutChangeListener(new LayoutListener());
}
class LayoutListener : Java.Lang.Object, IOnLayoutChangeListener {
public void OnLayoutChange(Android.Views.View? v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
UpdateCounter.addLayoutUpdate();
}
};
protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) {
UpdateCounter.addMeasureUpdate(this.GetHashCode().ToString());
base.OnMeasure(widthMeasureSpec, heightMeasureSpec);
}
Maui:
TRIAL 1:
[0:] MEASURE UPDATE 3006
[0:] LAYOUT UPDATE 1224
TRIAL 2:
[0:] MEASURE UPDATE 2994
[0:] LAYOUT UPDATE 1222
Xamarin:
TRIAL 1:
[0:] MEASURE UPDATE 1882
[0:] LAYOUT UPDATE 1264
TRIAL 2:
[0:] MEASURE UPDATE 1824
[0:] LAYOUT UPDATE 1248
Thus we see an average of 3000 Label Measure commands in Maui vs. just 1853 in Xamarin.
This represents 62% more Label measurements performed in Maui vs. Xamarin to accomplish the same thing.
Is this stark enough? Is it still any mystery why we are suffering for performance, compared to Xamarin or other systems, both in Android and iOS?
Any thoughts?
I have also always felt like the Image performance in Maui has also been quite poor. Loading simple images in a "CollectionView" type situation (ie. while scrolling) always leads to stutters and seems far too arduous no matter how much you cache or create platform Bitmaps first.
So I extended the project to add an Image View. I just copied and pasted the "ComplexCard" of the project into "CardWithPhoto" and threw some random cat photos in there.
The outcomes are absolutely disastrous and shocking. I don't usually use XAML (I code in C#) so unless there is something I missed here, while Xamarin handled the change fine for both Android and iOS, it's an absolute wreck in Maui across the board.
I added a random photo into the XAML like so with some code elsewhere to select and add one of 14 cat photos at that spot:
<StackLayout
Margin="10"
Spacing="10">
<Label
Text="{Binding Source.RestaurantName, Source={x:Reference this}}"
FontSize="Title" />
<Label
Text="{Binding Source.RestaurantDescription, Source={x:Reference this}}"
FontSize="Body" />
<Image
Source="{Binding Source.PhotoImageSource, Source= {x:Reference this}}"
/>
<Label
Text="{Binding Source.RestaurantAddress, Source={x:Reference this}}"
TextColor="DodgerBlue"
FontAttributes="Bold"
FontSize="Body" />
It looked quite nice and laid out just right in Xamarin iOS and Android:
Xamarin iOS & Android: Lays out perfectly
However we get very different results on Maui iOS and Android. Android was an abomination where there was endless space above and below the images. iOS just truncated the bottom of the views. Both were grotesque by comparison to Xamarin.
Android Maui: Empty space all over
iOS Maui: Truncates everything
I am not sure again on XAML and how much might have changed to Maui, but to me there is no reason this should fail so differently on iOS and Android. Clearly something is broken.
Though this is cosmetic. The abomination goes deeper and it gets weirder.
OnDraw
at least 21x for each Image State UpdateThe Android Image data set was difficult to test in Maui because Maui has some terrifying problems. First of all, it was very hard to get out of the startup infinite update loop once the images were added into the mix. Furthermore, scrolling back upwards frequently led to errors and crashes as I will point out in a minute.
However, one of the most strikingly strange things I encountered in all this is the following.
With Android images, we have available to monitor:
protected override void DrawableStateChanged() {
UpdateCounter.addImageDrawStateUpdate();
base.DrawableStateChanged();
}
protected override void OnDraw(Canvas canvas) {
UpdateCounter.addImageDrawUpdate(); //doesn't run on xamarin at all
base.OnDraw(canvas);
}
protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) {
UpdateCounter.addImageMeasureUpdate(this.GetHashCode().ToString());
base.OnMeasure(widthMeasureSpec, heightMeasureSpec);
}
protected override void OnLayout(bool changed, int left, int top, int right, int bottom) {
UpdateCounter.addImageLayoutUpdate();
base.OnLayout(changed, left, top, right, bottom);
}
What was fascinating and bizarre is the Xamarin manages to render the photos without once calling OnDraw
. It only calls DrawableStateChanged
and roughly in proportion to how many photos it must display.
Xamarin:
Here is the output from Xamarin Android when I load app, clear counters, select CardWithPhoto, and then scroll to the bottom:
TRIAL 1:
[0:] IMAGE STATE UPDATE 119
[0:] IMAGE MEASURE UPDATE 177
[0:] IMAGE LAYOUT UPDATE 177
TRIAL 2:
[0:] IMAGE STATE UPDATE 121
[0:] IMAGE MEASURE UPDATE 177
[0:] IMAGE LAYOUT UPDATE 177
For reference there should be 100 images (100 entries, 1 image per entry), so it is only running image state changed slightly more than the total count of images. It doesn't run OnDraw
even once.
Maui:
By contrast, in Maui, it is an absolute disaster. I was not able to scroll through the page due to the infinite loop and crashes, but I could gather a bit of data in that by one point I had before crashing:
[0:] IMAGE STATE UPDATE 21
[0:] IMAGE DRAW UPDATE 455
This suggests that by the 21st photo display or so, it had run OnDraw
455 times (drawn 455 photos?), which is absolutely surreal. How is Xamarin not running OnDraw
at all? While Maui is running it 21x more often than there are image state changes???
It blows my mind.
It seems clear then that Image is now drawing photos in a completely different way from Xamarin and this is not favorable at all. The component seems deeply broken compared to the Xamarin version.
As you scroll you will encounter endless errors of:
[View] ContentViewGroup not displayed because it is too large to fit into a software layer (or drawing cache), needs 12932556 bytes, only 10368000 available
(note that Xamarin handles the exact same photos without a single complaint.)
And eventually if you scroll back up you will crash with:
[Bitmap] Called getConfig() on a recycle()'d bitmap! This is undefined behavior!
**Java.Lang.RuntimeException:** 'Canvas: trying to use a recycled bitmap android.graphics.Bitmap@236f184'
Where to begin? 😬
Any thoughts?
With iOS the image behavior is at least not so broken as to be unquantifiable, but just like everything else it is far poorer than Xamarin.
Loading the app, clearing the counters, clicking the CardWithPhoto option, and then scrolling top to bottom I get:
Maui:
TRIAL 1:
[0:] IMAGE LAYOUT UPDATE 406
TRIAL 2:
[0:] IMAGE LAYOUT UPDATE 396
Xamarin:
TRIAL 1:
[0:] IMAGE LAYOUT UPDATE 138
TRIAL 2:
[0:] IMAGE LAYOUT UPDATE 154
Thus we get an average of 146 image layouts in Xamarin, and 401 image layouts in Maui, meaning Maui lays out Images 2.7x as many times as Xamarin for the same workload.
What more can be said?
😬
I am shocked at the severity but also happy the problems are so easily quantified. I think a few things may need to be done if getting the same performance as Xamarin is expected, as there are basically two fundamental issues:
OnDraw
20+ times in Android for each photo update, as Xamarin certainly wasn't (it didn't call it at all).Did I miss anything? Feel free to fork or download my projects as I forked them from @eth-ellis if you want to experience it for yourself. 😬
Great work @jonmdev , seems reasonably obvious that there is an issue, regarding overdrawing that will affect all applications. Hope this gets the right attention asap
@jonmdev You can just look at their commits, to understand if they need mobile framework: tests, tests, tests, ui tests, windows windows windows, snapshots, tests tests, windows... And NO, Absolutely nothing related to layout problems on ios and android.
Who the f**k even needs that windows? people needs MOBILE framework firstly, and then, maybe when all the problems on mobile platforms are gone, then and only then think about desktop. By the way, if you need desktop, I'd rather use Avalonia, but 100% not MAUI
This should be number one prio (top of tops). The layout system was not the fastest in XF but Maui is a lot slower. Perhaps the team should concentrate to mobile and analyse why layouting, measuring and drawing cycles are increasing.
The reactions on @jonmdev 's tests clearly showing that a lot of people observe similar behaviors. I know this is difficult but we (and our customers) need a performant app.
I was curious and wanted to see if (1) the measuring/layout/drawing issues can be reproduced by a very simple layout configuration and (2) if problems would be amplified by increasing depth of nesting layout objects.
I edited my projects here:
By changing App.xaml.cs to allow an alternative application build. This is switched by the bool which remains defaulted to the original CollectionView project. If set true, you will run the simple new project instead.
This project just adds a single or nest of AbsoluteLayout (or StackLayout or VerticalStackLayout) elements and then a Label at the end in the deepest of the hierarchy. Then it clears the update counter on startup and issues one resize command to the hierarchy of layouts.
A perfectly efficient system should only give one label layout and draw after the resize.
Android:
iOS:
Nesting:
Layout problems in Maui can be very easily reproduced without any "Bindings" or CollectionView both in Android and iOS. It only takes around 10-30 lines of C# code to reproduce.
Maui Android inefficiencies are seen equally with both AbsoluteLayout and StackLayout/VerticalStackLayout in this simple test.
Maui iOS inefficiencies are also reproduced with VerticalStackLayout/StackLayout. I believe there are AbsoluteLayout inefficiencies and problems in iOS as well (as I use them regularly) though we are seeing equivalent Maui and Xamarin inefficiency in this simple test for iOS. I will need to think and test more about this.
If I was you guys (Microsoft staff), I would take a simple project like this one, add some Debug outs inside the Maui measuring/layout system at the various steps and see which steps are being run too many times in each OS and why.
I would guess this would be the easiest way to figure out the problems. Using simple C# like here you can ramp up the complexity of the layouts as things get solved and compare. Then go to the CollectionView project for eventual testing of real world performance.
Perfect CollectionView performance can be measured by counting the number of labels/images in each entry and multiplying by the number of elements (100 by default). So if 8 labels per element, once counter is cleared, then CollectionView is loaded from menu, then scrolled to bottom, perfect performance would be 800 label layouts/measures/draws.
This is all quantifiable in such simple terms. Perhaps there are other ways to approach this. That is just the best I can think of.
App.xaml.cs is updated in each to:
public App() {
//===================
//toggle projects
//===================
bool testSimpleLayout = false;
//============================================
//1) ORIGINAL PROJECT USING MAUI SYSTEM
//============================================
if (!testSimpleLayout) {
InitializeComponent();
MainPage = new AppShell();
}
//============================================
//2) SIMPLE PROJECT USING BASIC LAYOUT
//============================================
else {
ContentPage mainPage = new();
MainPage = mainPage;
int numNested = 10; //set number of extra layouts to nest from 0-n, does not change result
List<VerticalStackLayout> layoutList = new(); //can try alternative layout types here
VerticalStackLayout layout = new(); //can try alternative layout types here
mainPage.Content = layout;
layoutList.Add(layout);
for (int i=0; i < numNested; i++) {
layout = new();
layoutList[layoutList.Count - 1].Add(layout);
layoutList.Add(layout);
}
Label label = new();
label.Text = "HELLO HELLO HELLO HELLO HELLO HELLO HELLO HELLO HELLO HELLO HELLO HELLO";
layoutList[layoutList.Count-1].Add(label);
mainPage.SizeChanged += delegate {
UpdateCounter.resetCounters();
Debug.WriteLine("====================RESET COUNTERS ON RESIZE");
if (mainPage.Width > 0) {
for (int i = 0; i < layoutList.Count; i++) {
layoutList[i].WidthRequest = mainPage.Width;
layoutList[i].HeightRequest = mainPage.Height;
}
}
};
//ABSOLUTE LAYOUT:
//== android maui - measure 3x, layout 2x | android xamarin - measure 1x, layout 1x
//== iOS maui - layout 2x, draw 1x | iOS xamarin - layout 2x, draw 1x
//VERTICAL STACK LAYOUT & STACK LAYOUT:
//== android maui - measure 3x, layout 2x | android xamarin - measure 1x, layout 1x
//== iOS maui - layout 3x, draw 1x | iOS xamarin - layout 2x, draw 1x
}
}
Thanks for your ongoing efforts.
Improving CollectionView performance is the one single most convincing MAUI enhancement that will produce the highest short-term reward as literally anybody creating professional apps with complex layouts will benefit from it: I see this card keeps switching across product increments but it's worth considering for prioritization.
I am glad this one has been assigned but I would love to see the p/1
tag on it.
With all the open verified performance issues for android we are very hesitant on releasing our app to production. We were on a time crunch to migrate from Xamarin Forms to MAUI support Android 15. Since Android 15 is expected to be released today, there will be customers we will be unable to support.
Looks like @brentpbc posted another good example of the problem for iOS here:
https://github.com/dotnet/maui/issues/24224
The original title for this current SR9 issue was "[iOS/Android] CollectionView scrolling performance worse in .NET MAUI when compared to Xamarin.Forms (lag/stutter)"
I notice the current issue title for this thread was edited to remove iOS by @PureWeen in June seemingly by @davidortinau 's suggestion.
I don't mean to rub it in or again be rude or abrasive, but as @PureWeen is the current assignee and @davidortinau is the Microsoft head of Maui, is it currently recognized by the Maui team that an issue exists with both Android and iOS? Will both be fixed?
I hope performance in both will be corrected. No one is using Maui with the intention of only developing mobile for Android. In my own application I have worse performance in iOS actually at this point.
@davidortinau you also asked for more examples. Looks like another very good one there in that linked report for iOS showing the same issue including again videos. Perhaps that will help? Thanks again for your efforts.
No news on this subject?
.NET 9 Preview 7 has CollectionView & CarouselView improvements with a new opt-in handler for iOS and Mac Catalyst
This release introduces two new handlers for developers to try that bring sweeping performance and stability improvements for both CollectionView and CarouselView. These new implementations are based on newer UICollectionView APIs. Opt-in by adding the following into your Program.cs:
#if IOS || MACCATALYST appBuilder.ConfigureMauiHandlers(handlers => { handlers.AddHandler<Microsoft.Maui.Controls.CollectionView, Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2>(); handlers.AddHandler<Microsoft.Maui.Controls.CarouselView, Microsoft.Maui.Controls.Handlers.Items2.CarouselViewHandler2>(); }); #endif
.NET 9 Preview 7 has CollectionView & CarouselView improvements with a new opt-in handler for iOS and Mac Catalyst
This release introduces two new handlers for developers to try that bring sweeping performance and stability improvements for both CollectionView and CarouselView. These new implementations are based on newer UICollectionView APIs. Opt-in by adding the following into your Program.cs:
#if IOS || MACCATALYST appBuilder.ConfigureMauiHandlers(handlers => { handlers.AddHandler<Microsoft.Maui.Controls.CollectionView, Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2>(); handlers.AddHandler<Microsoft.Maui.Controls.CarouselView, Microsoft.Maui.Controls.Handlers.Items2.CarouselViewHandler2>(); }); #endif
That is well in good for iOS but those of us needing an immediate stable release for Android are still stuck.
.NET 9 Preview 7 has CollectionView & CarouselView improvements with a new opt-in handler for iOS and Mac Catalyst
This release introduces two new handlers for developers to try that bring sweeping performance and stability improvements for both CollectionView and CarouselView. These new implementations are based on newer UICollectionView APIs. Opt-in by adding the following into your Program.cs:
#if IOS || MACCATALYST appBuilder.ConfigureMauiHandlers(handlers => { handlers.AddHandler<Microsoft.Maui.Controls.CollectionView, Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2>(); handlers.AddHandler<Microsoft.Maui.Controls.CarouselView, Microsoft.Maui.Controls.Handlers.Items2.CarouselViewHandler2>(); }); #endif
Thanks. That's interesting but not of any particular benefit to me. I do not use CollectionView but rather my own custom systems. Again, the problem is not CollectionView. The problem is Maui's layout system.
Xamarin was fast using the old API. As proven by numerous projects and reports. Which again shows that the problem is in the layout system of Maui, not the API.
There are numerous projects and tests showing that Maui's layout system is defective. I spent almost a week straight doing tests and experiments to try to illustrate it here.
Can you acknowledge whether you guys recognize this issue as such and are working on trying to fix the basic layout issues in both iOS and Android and attain the same performance as Xamarin had with the same code?
I think we all understand this is hard work but we would all very much appreciate it. It is demoralizing to be working in a system like Maui where we can be micromanaging our own code optimizations but in the background Maui is still sluggish because the fundamental layouts are doubling and tripling the OS's workload.
Thanks again for your help and interest in the problem.
Hi @jonmdev perhaps you should create a new issue with your tests. Hopefully the new issue will get more attention than the comments in this issue.
@Alex-Dobrynin thanks a lot !!!
I believe I have found the primary cause of the poor scrolling/collectionview/translation performance in iOS Maui.
Every time you change the translation of any objects in iOS, Maui is causing the whole hierarchy to re-arrange itself and re-measure itself in iOS. This is not similarly happening in Android/Windows.
This matches my experience where the poorest performing systems I now have are in iOS. It makes sense as any scrolling or translation our systems perform will therefore spam massively expensive re-measurements and re-arrangements which permeate down through the hierarchy on every frame.
I have shared my bug report here which demonstrates the issue: https://github.com/dotnet/maui/issues/24996
As far as a solution, @PureWeen (or anyone here), any thoughts?
Description
When migrating our app from Xamarin.Forms to .NET MAUI we noticed that for most of our CollectionViews the performance when scrolling was worse.
When new views appeared the CollectionView would stutter/jitter resulting in an unpleasant user experience.
Our item templates are somewhat complex but still worked great in Xamarin.Forms.
In the linked repro apps, we have a template called CardWithComplexContent which is somewhat similar to the template in our app.
Note
Android (Samsung A73) - Xamarin.Forms
https://github.com/dotnet/maui/assets/13865151/25eac924-a499-4908-afd9-ab7108d71437
Android (Samsung A73) - MAUI
https://github.com/dotnet/maui/assets/13865151/cd402208-674f-4cd4-8a67-b3038e1e8f88
iOS (iPhone 6s) - Xamarin.Forms
https://github.com/dotnet/maui/assets/13865151/13a64912-3daf-4325-9d0e-4c15d4cee2eb
iOS (iPhone 6s) - MAUI
https://github.com/dotnet/maui/assets/13865151/d46972ec-5e51-479d-aa53-81351874db9e
Steps to Reproduce
The apps contain other templates for attempting to isolate elements which impact performance the most.
Accepting PRs or suggestions on the repro app repos for templates to add for comparison.
Acceptance Criteria
The scrolling experience on .NET MAUI performs as well as or better than Xamarin.Forms for all affected platforms.
Link to public reproduction project repository
https://github.com/eth-ellis/Issue-Repro/tree/main/CollectionViewPerformanceXamarin and https://github.com/eth-ellis/Issue-Repro/tree/main/CollectionViewPerformanceMaui
Version with bug
Nightly / CI build (Please specify exact version)
Is this a regression from previous behavior?
Yes, this used to work in Xamarin.Forms
Last version that worked well
Unknown/Other
Affected platforms
iOS, Android
Affected platform versions
No response
Did you find any workaround?
No response
Relevant log output
No response