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

Reduce the number of calls to invalidate measure #21801

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

mattleibow
Copy link
Member

@mattleibow mattleibow commented Apr 12, 2024

Description of Change

While investigating layout calls with Label, we noticed a few extra calls to SetNeedsLayout than is particularly necessary. On iOS at least, multiple calls to SetNeedsLayout does not actually do anything as it just "sets a flag" for the next layout pass.

But. Sometimes we do not need to invalidate the layout. For example:

  • if the label has a fixed size, then invalidating the layout will do nothing as the size is fixed
  • if the label has a fixed width, and the text is a single line, then the text can only cause a horizontal change which will be prevented

@mattleibow mattleibow requested a review from a team as a code owner April 12, 2024 15:20
Copy link
Member Author

@mattleibow mattleibow left a comment

Choose a reason for hiding this comment

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

This is a "small and simple" change, but as we all know this has HUGE knock-on effects. iOS is smarter with layouts, but I worry about the other platforms. I will need to add a large bunch of UI tests and things like that.

However, this will at least get the ball rolling.

Comment on lines -79 to +80
label.InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
label.InvalidateMeasureIfLabelSizeable();
Copy link
Member Author

Choose a reason for hiding this comment

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

This is the case for all calls to invalidate, first check the conditions.

Comment on lines -365 to -369
LineBreakMode breakMode = label.LineBreakMode;
bool isVerticallyFixed = (label.Constraint & LayoutConstraint.VerticallyFixed) != 0;
bool isSingleLine = !(breakMode == LineBreakMode.CharacterWrap || breakMode == LineBreakMode.WordWrap);
if (!isVerticallyFixed || !isSingleLine)
((Label)bindable).InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
Copy link
Member Author

Choose a reason for hiding this comment

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

This is a "major" change where I think the calls will prevent a valid measure from taking place.

Assume you have a label:

var label = new Label {
  HeightRequest = 100,
  MinimumHeightRequest = 10,
  LineBreakMode = LineBreakMode.NoWrap,
}

This condition will evaluate as follows:

bool isVerticallyFixed = true; // the constraint evals to fixed as there is a height constraint
bool isSingleLine = true; // the break mode is NoWrap and thus is NOT NOT wrapping
if (!isVerticallyFixed || !isSingleLine) // becomes if (!true || !true) or false

However, if you look at the label definition, it is not a wrapping label and it has a fixed height. However, it does NOT have a fixed with. This means updating the text WILL require a measure since the width has changed. But here we do not call it.

Comment on lines +474 to +475
if (!isHorizontallySizeable && isSingleLine)
return false;
Copy link
Member Author

Choose a reason for hiding this comment

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

This condition is the new condition. Besides the check to make sure that if the width AND height are fixed, this check makes sure to only avoid a layout if the WIDTH is constrained. Previously the height was the constraint that was used however it was not correct. And even if it was correct in some cases, we cannot assume vertical and horizontal are mutually exclusive.

Comment on lines -1631 to +1635
if (element.WidthRequest >= 0 && element.MinimumWidthRequest >= 0)
if (element.WidthRequest >= 0)
{
constraint |= LayoutConstraint.HorizontallyFixed;
}
if (element.HeightRequest >= 0 && element.MinimumHeightRequest >= 0)
if (element.HeightRequest >= 0)
Copy link
Member Author

Choose a reason for hiding this comment

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

This is very interesting, and I am not sure I agree with the logic before. Why is the fixed status determined by the minimum bounds? If I have a view that has a height of say 100, why is it dependent on the min height? This is an AND operation, so BOTH need to be set, but setting height is all that is needed. If I set a Height of 100 and a MinHeight of 0, 10, 100, 200 it makes no difference.

Copy link
Member

Choose a reason for hiding this comment

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

This check was added before Xamarin.Forms was even public. I couldn't find a historical rationalization for why this was here.

Comment on lines -69 to -85
public override void InvalidateIntrinsicContentSize()
{
base.InvalidateIntrinsicContentSize();

if (Frame.Width == 0 && Frame.Height == 0)
{
// The Label hasn't actually been laid out on screen yet; no reason to request a layout
return;
}

if (!Frame.Size.IsCloseTo(AddInsets(IntrinsicContentSize), (nfloat)0.001))
{
// The text or its attributes have changed enough that the size no longer matches the set Frame. It's possible
// that the Label needs to be laid out again at a different size, so we request that the parent do so.
Superview?.SetNeedsLayout();
}
}
Copy link
Member Author

Choose a reason for hiding this comment

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

I have not seen any real cases where this call should/does/needs to invalidate the layout. We are already calling invalidate in the Label class when we change the properties (Text, Font, CharSpacing) so this is redundant.

Also, comparing Frame to IntrinsicContentSize is ALWAYS incorrect. Assume you have a label with text "Hello" and the width of that text is say 100, then you set the label width of 200. The Frame now has a width of 200, but IntrinsicContentSize has a width of 100... This means it will ALWAYS trigger a layout.

@mattleibow mattleibow marked this pull request as draft April 12, 2024 15:37
@jsuarezruiz jsuarezruiz added area-layout StackLayout, GridLayout, ContentView, AbsoluteLayout, FlexLayout, ContentPresenter legacy-area-perf Startup / Runtime performance labels Apr 12, 2024
@AlleSchonWeg
Copy link

Hi @mattleibow ,
i had created an issue with similar behaviour. #20444
Perhaps you can also test when the label is animated.
Thank you

@jsuarezruiz
Copy link
Contributor

@mattleibow Let's chat about this, to prepare a good list of UITests covering all the possible impacted scenarios.

@Eilon Eilon added t/perf The issue affects performance (runtime speed, memory usage, startup time, etc.) and removed legacy-area-perf Startup / Runtime performance labels May 10, 2024
@mattleibow mattleibow closed this May 13, 2024
@mattleibow mattleibow reopened this May 13, 2024
@mattleibow mattleibow marked this pull request as ready for review May 13, 2024 15:08
@mattleibow
Copy link
Member Author

/rebase

@mattleibow mattleibow added this to the .NET 8 SR6 milestone May 13, 2024
@PureWeen PureWeen self-assigned this May 14, 2024
@PureWeen
Copy link
Member

/rebase

@PureWeen
Copy link
Member

/rebase

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-layout StackLayout, GridLayout, ContentView, AbsoluteLayout, FlexLayout, ContentPresenter t/perf The issue affects performance (runtime speed, memory usage, startup time, etc.)
Projects
Status: In Progress
Development

Successfully merging this pull request may close these issues.

None yet

5 participants