-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Changing Translation of an element causes Maui in iOS to constantly run Measure & ArrangeChildren (likely cause of brutal iOS Layout performance issues) #24996
Comments
@jonmdev a follow-up from my comment on the other issue. You can find me on Discord in case you wanna chat. |
Also, keeping the ScrollView, may you try to install this version of MAUI from my NuGet GitHub feed and see if the issue is gone? Note: This custom build supports Android and iOS only. |
I am not sure what you are referring to. I am not using any ScrollView. Just translating an AbsoluteLayout. This should not cause these remeasure and rearranges. There is a fundamental Layout failure. Do you see what I mean? You can see as I linked above the only minimal project code is here:
The "scroll window" and "scroll content" of my project are just AbsoluteLayouts with a custom LayoutManager so we can see all the bizarre behavior debugged out from it:
I am not sure if you would call AbsoluteLayout a "legacy layout" but it is still very much needed. We need a simple method for laying out objects where we are controlling their arrangement or translation and need to add multiple children to it and arrange them for example in layers without something like "VerticalStackLayout" taking over and forcing a position. I updated my original post for better clarity on the object types (only AbsoluteLayout and Border in this project at all at any point). Thanks for any help as always. |
I'm sorry, I got tricked by the name "scrollWindow". |
@jonmdev here's what I tried to do within the MAUI repo to reproduce: namespace Maui.Controls.Sample.Issues;
[Issue(IssueTracker.Github, 24996, "Translation causes multiple layout passes", PlatformAffected.All)]
public partial class Issue24996 : ContentPage
{
public Issue24996()
{
InitializeComponent();
}
public async void OnTapped(object sender, EventArgs e)
{
Lvl2.TranslationY = Random.Shared.Next(0, 400);
await Task.Delay(500);
Stats.Text = $"Root: {Root.MeasurePasses}->{Root.ArrangePasses} / Lvl1: {Lvl1.MeasurePasses}->{Lvl1.ArrangePasses} / Lvl2: {Lvl2.MeasurePasses}->{Lvl2.ArrangePasses} / Lvl3: {Lvl3.MeasurePasses}->{Lvl3.ArrangePasses}";
}
}
public class AbsoluteLayout24996 : AbsoluteLayout
{
public int MeasurePasses { get; private set; }
public int ArrangePasses { get; private set; }
protected override Size MeasureOverride(double widthConstraint, double heightConstraint)
{
MeasurePasses++;
return base.MeasureOverride(widthConstraint, heightConstraint);
}
protected override Size ArrangeOverride(Rect bounds)
{
ArrangePasses++;
return base.ArrangeOverride(bounds);
}
} <?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:issues="clr-namespace:Maui.Controls.Sample.Issues"
x:Class="Maui.Controls.Sample.Issues.Issue24996"
Title="Issue24996">
<issues:AbsoluteLayout24996 x:Name="Root">
<issues:AbsoluteLayout24996.GestureRecognizers>
<TapGestureRecognizer Tapped="OnTapped" />
</issues:AbsoluteLayout24996.GestureRecognizers>
<issues:AbsoluteLayout24996 x:Name="Lvl1" BackgroundColor="DarkSlateBlue">
<issues:AbsoluteLayout24996 x:Name="Lvl2" BackgroundColor="AliceBlue">
<issues:AbsoluteLayout24996 x:Name="Lvl3" HeightRequest="100" WidthRequest="100" BackgroundColor="Aqua">
<Border BackgroundColor="GreenYellow" HeightRequest="100" WidthRequest="100" />
</issues:AbsoluteLayout24996>
</issues:AbsoluteLayout24996>
</issues:AbsoluteLayout24996>
<Label x:Name="Stats"
issues:AbsoluteLayout24996.LayoutFlags="PositionProportional"
issues:AbsoluteLayout24996.LayoutBounds="0,1,-1,-1"
BackgroundColor="#222222"
TextColor="White"/>
</issues:AbsoluteLayout24996>
</ContentPage> As you can see from this recording, the number of measure / arrange passes matches the number of taps (translations). Why are you doing this? mainPage.SizeChanged += delegate {
if (mainPage.Width > 0) {
scrollWindow.WidthRequest = mainPage.Width;
scrollWindow.HeightRequest = mainPage.Height;
scrollContent.WidthRequest = mainPage.Width;
scrollContent.HeightRequest = mainPage.Height * 0.78;
}
}; Anyway, even if I add this after |
This small snippet is just a simple way to resize the objects when the ContentPage changes size to full screen automatically. It runs once on initial project load. You can change it to:
You will see it only runs once. It is not the culprit or any problem.
This is likely the same abnormal behavior, based on my project and expectations compared to Android/Windows and any other system I have worked in. If you load my project in Windows/Android and run it, you will see there are ZERO measure or arrange passes being debugged out after the very initial project load, despite it continuously translating the object infinitely over time. This is the correct behavior. Translation should NOT trigger measure/arrange passes. That is the whole point of the translation function. It is meant to be a cheap and simple way to move an object relative to another WITHOUT re-measuring or arranging anything. It only does these extra measures and arranges in iOS. It is a bug that is making smooth translation in iOS near impossible when there is a complex hierarchy. If you have a very complex hierarchy and try to translate it, you can do this smoothly with ease in Windows/Android because they are not remeasuring/arranging anything every time (as expected). But in iOS it becomes crushing to remeasure/rearrange on every translation change which shouldn't be happening. The concept of Translation is common to many UI systems and the Windows/Android behavior is the expected behavior. The iOS behavior is not. Give my project a try in Android/Windows and you will see what I mean. I don't actually work at all in xaml (only C#) so I am not sure if there would be any other difference. But I suspect you will see the same thing in your project if you test it in Windows/Android to compare. Thanks very much for your attention to this or any further ideas. |
@jonmdev It took me a while to understand your point, I'm sorry! :D So, this is what's happening:
On other platforms this "bubble up" mechanism happens natively, while on iOS it does not. I have a PR which moves this mechanism at handler mapper level maui/src/Core/src/Platform/iOS/ViewExtensions.cs Lines 283 to 316 in 50358af
This way, even if the transformation happens at native level, it would only invalidate that native view, and not the entire chain. Now, even if I wanted to create a PR targeting .NET8, there is no chance it'd be included as an SR10 because I've been told that only very small and very important PRs could be included there. On the other hand I can propose a fix for this targeting .NET9, or I may even fix this and release it within my custom .NET8 MAUI build. |
@jonmdev let me add that this is only happening in a precise situation, I've updated my test code That said, my message above is still valid, but it's limited to this scenario. SizeChanged += delegate {
if (Width > 0) {
// For some reason, constraining Lvl1 layout to a fixed size causes a `SetNeedsLayout` to be called
// when translating the Lvl2 view (its child) outside the bottom boundary.
// This causes a layout pass to be called on the Root, Lvl1, Lvl2, and Lvl3.
Lvl1.WidthRequest = Width;
Lvl1.HeightRequest = Height;
}
}; |
This comment has been minimized.
This comment has been minimized.
@OvrBtn may you contact me via Discord (I have the same username there) so that we don't spam this issue? Thanks! |
I've prepared a branch (for now it's based on .NET8 so I'll have to rebase) with the fix. |
Hey, thanks so much for looking into this and trying to solve it. I would love to test this to see if it fixes my real world terrible iOS performance scenario I am facing which I believe is due to this bug. But I am not sure how. I have never built Maui and I am not sure how or what to do with it even if I do. I usually just add the nuget packages. To me it is no different whether it is .NET 9 or .NET 8 as I am not in production yet (can't be, too many Maui issues) and we will all have to go to .NET 9 anyway at some point. Any thoughts or suggestions? Thanks. |
Once I find the time to rebase onto net9 and create a PR, we can then run AZP and generate packages from there. :) |
@jonmdev you can find packages with my fix in artifacts here: https://dev.azure.com/xamarin/public/_build/results?buildId=126601&view=artifacts&pathAsName=false&type=publishedArtifacts Obviously we're talking about MAUI 9 packages. |
Alby! I have to say - Thank you so much! You have single handedly fixed so many of the terrible iOS problems now. Now this appears fixed as well. I tested with your custom nuget package you sent me. Before, I was getting ridiculous slow downs at certain points of translation due to all the extra measurements and layouts and arranges. Now that is smooth. This appears to have fixed what was driving me crazy. It looked horrible before. Now it looks quite normal. I still have slow performance on my custom CollectionView type object. But that is I think a separate matter. Likely more over-draw or other issues like that again that will need to be proven separately. It seems evident at this point that all these performance issues we are seeing are not just one issue but many issues. It will be easier to figure out the remaining ones now that this one is out of the way. With each problem fixed it is easier to see the remaining ones. Hopefully this fix you made will be added soon to the official package for .NET 9. Then once that is the case, I will work on trying to narrow down the remaining iOS performance issues and let you know if I find anything else significant. Thanks again for all your work. All the best. |
Description
Maui provides very poor Layout performance on iOS during translations (eg. custom animations, scrolling, or collection view type systems), and I believe I have now found the cause. Hopefully there is an easy workaround or solution. I am open to any suggestions.
This is likely also the cause of the generally poor scrolling and CollectionView performance in iOS as both will also likely depend on translation changes to function, and thus also be subject to these endless re-measures and re-arranges.
PROBLEM
In a working system, adjusting the translation of an element should NEVER provoke a re-measure/re-layout/re-arrange of the object or all its children. That is the whole point of translation. It is supposed to be a fast and efficient way to adjust the position of something WITHOUT re-doing every measurement and layout.
This is working in Android/Windows. Adjusting the translation of an element does NOT provoke a re-layout/re-arrange of the object or all children. No re-measure or arrange occurs on translation change.
However, in iOS
ArrangeChildren
is being run on every single translation change. AndMeasure
is being run extremely often as well provoked by only translation changes (though not quite as often).This is brutalizing the performance in iOS as any translation change leads to continuous re-layouts and re-measures of all children of the translated object starting from its parent and propagating down through the entire child hierarchy.
DEMONSTRATION PROJECT
This is demonstrated through the bug project here: https://github.com/jonmdev/iOSTranslationArrangeBug
The project is created in only two files:
CAbsoluteLayout.cs - This is a derived class of AbsoluteLayout to implement a custom layout manager so we can see the layouts on debug when they happen through the overrides of
Measure
andArrangeChildren
. The custom layout manager is just a simplified copy and paste from Maui's AbsoluteLayoutManager.cs. No special changes.App.xaml.cs - This is just a simple C# project that adds a few of these Custom Absolute Layout elements to the screen and starts oscillating the position of one. It creates a hierarchy of, by styleId names:
"Scroll Content" is the moved object. In iOS, we will see how arranges and measures are spammed as its translation changes from its parent ("Scroll Window") all the way down through the hierarchy.
RESULT
One can see if you run this in Windows/Android there are NO ongoing lines showing
Measure
orArrangeChildren
are being run (as expected). You will get on project load only once or twice something like:This is normal behavior in Android/Windows.
However, in iOS, these outputs are spammed on a continuous basis as the translation keeps changing. You will get an infinite number of such re-measurements and re-arranges. The arranges occur on literally every single Translation change while the measures occur only at certain points of the oscillation (unclear why).
In general, manipulating the translations in iOS of anything in the real world with complex hierarchies is extremely costly and nothing runs as smoothly as it should even on high end iOS devices. This is likely why.
We cannot afford to be re-arranging the whole hierarchy every time a translation occurs and this should not be happening.
CAUSE / SOLUTION?
Is there any obvious cause or solution that comes to mind?
Any workaround or solution (or ideas) would be very appreciated.
Steps to Reproduce
ArrangeChildren
orMeasure
being run on an ongoing basis.Link to public reproduction project repository
https://github.com/jonmdev/iOSTranslationArrangeBug
Version with bug
8.0.91 SR9.1
Is this a regression from previous behavior?
No, this is something new
Affected platforms
iOS
The text was updated successfully, but these errors were encountered: