Skip to content

Reduce the number of calls to invalidate measure#21801

Merged
PureWeen merged 5 commits into
mainfrom
dev/reduce-invalidates
Jun 4, 2024
Merged

Reduce the number of calls to invalidate measure#21801
PureWeen merged 5 commits into
mainfrom
dev/reduce-invalidates

Conversation

@mattleibow
Copy link
Copy Markdown
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

Issues Fixed

@mattleibow mattleibow requested a review from a team as a code owner April 12, 2024 15:20
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Contributor

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
Copy Markdown
Contributor

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

@Eilon Eilon added perf/general The issue affects performance (runtime speed, memory usage, startup time, etc.) (sub: perf) 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
Copy Markdown
Member Author

/rebase

@mattleibow mattleibow added this to the .NET 8 SR6 milestone May 13, 2024
@github-actions github-actions Bot force-pushed the dev/reduce-invalidates branch from 7af3b6d to d96ee42 Compare May 13, 2024 15:08
@PureWeen PureWeen self-assigned this May 14, 2024
@PureWeen
Copy link
Copy Markdown
Member

/rebase

@github-actions github-actions Bot force-pushed the dev/reduce-invalidates branch from d96ee42 to c7bc495 Compare May 16, 2024 13:28
@PureWeen
Copy link
Copy Markdown
Member

/rebase

@github-actions github-actions Bot force-pushed the dev/reduce-invalidates branch from c7bc495 to 7df8d96 Compare May 16, 2024 15:07
@PureWeen PureWeen force-pushed the dev/reduce-invalidates branch from 7df8d96 to 2a182f6 Compare May 21, 2024 21:39
@PureWeen PureWeen self-requested a review June 4, 2024 14:24
@PureWeen PureWeen merged commit 572b6c6 into main Jun 4, 2024
@PureWeen PureWeen deleted the dev/reduce-invalidates branch June 4, 2024 14:59
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 5, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-layout StackLayout, GridLayout, ContentView, AbsoluteLayout, FlexLayout, ContentPresenter fixed-in-8.0.60 fixed-in-9.0.0-preview.5.24307.10 perf/general The issue affects performance (runtime speed, memory usage, startup time, etc.) (sub: perf)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Reduce the number of calls to invalidate measure

6 participants