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

Measure invalidation performance and fixes (batching version) #24823

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions src/Controls/src/Core/BindableObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -361,11 +361,16 @@ public static void SetInheritedBindingContext(BindableObject bindable, object va
else
{
bindable._inheritedContext = new WeakReference(value);
bindable.ApplyBindings(fromBindingContextChanged: true);
bindable.OnBindingContextChanged();
bindable.ApplyBindingsFromBindingContextChanged();
}
}

private protected virtual void ApplyBindingsFromBindingContextChanged()
{
ApplyBindings(fromBindingContextChanged: true);
OnBindingContextChanged();
}

/// <summary>
/// Applies all the current bindings to <see cref="BindingContext" />.
/// </summary>
Expand Down Expand Up @@ -717,8 +722,7 @@ static void BindingContextPropertyBindingChanging(BindableObject bindable, Bindi
static void BindingContextPropertyChanged(BindableObject bindable, object oldvalue, object newvalue)
{
bindable._inheritedContext = null;
bindable.ApplyBindings(fromBindingContextChanged: true);
bindable.OnBindingContextChanged();
bindable.ApplyBindingsFromBindingContextChanged();
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

namespace Microsoft.Maui.Controls.Handlers.Compatibility
{
public class ListViewRenderer : ViewRenderer<ListView, UITableView>
public class ListViewRenderer : ViewRenderer<ListView, UITableView>, IPropagatesSetNeedsLayout
{
public static PropertyMapper<ListView, ListViewRenderer> Mapper =
new PropertyMapper<ListView, ListViewRenderer>(VisualElementRendererMapper);
Expand Down Expand Up @@ -62,6 +62,33 @@ public ListViewRenderer() : base(Mapper, CommandMapper)
AutoPackage = false;
}

bool _pendingSuperViewSetNeedsLayout;

public override void SetNeedsLayout()
{
base.SetNeedsLayout();

if (Window is not null)
{
_pendingSuperViewSetNeedsLayout = false;
Superview?.SetNeedsLayout();
}
else {
_pendingSuperViewSetNeedsLayout = true;
}
}

public override void MovedToWindow()
{
base.MovedToWindow();
if (_pendingSuperViewSetNeedsLayout)
{
Superview?.SetNeedsLayout();
}

_pendingSuperViewSetNeedsLayout = false;
}

public override void LayoutSubviews()
{
base.LayoutSubviews();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

namespace Microsoft.Maui.Controls.Handlers.Compatibility
{
public class TableViewRenderer : ViewRenderer<TableView, UITableView>
public class TableViewRenderer : ViewRenderer<TableView, UITableView>, IPropagatesSetNeedsLayout
{
const int DefaultRowHeight = 44;
UIView _originalBackgroundView;
Expand All @@ -33,6 +33,33 @@ public override void LayoutSubviews()
_previousFrame = Frame;
}

bool _pendingSuperViewSetNeedsLayout;

public override void SetNeedsLayout()
{
base.SetNeedsLayout();

if (Window is not null)
{
_pendingSuperViewSetNeedsLayout = false;
Superview?.SetNeedsLayout();
}
else {
_pendingSuperViewSetNeedsLayout = true;
}
}

public override void MovedToWindow()
{
base.MovedToWindow();
if (_pendingSuperViewSetNeedsLayout)
{
Superview?.SetNeedsLayout();
}

_pendingSuperViewSetNeedsLayout = false;
}

protected override void Dispose(bool disposing)
{
if (disposing)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace Microsoft.Maui.Controls.Handlers.Compatibility
{
public class FrameRenderer : VisualElementRenderer<Frame>
public class FrameRenderer : VisualElementRenderer<Frame>, IPropagatesSetNeedsLayout
{
public static IPropertyMapper<Frame, FrameRenderer> Mapper
= new PropertyMapper<Frame, FrameRenderer>(VisualElementRendererMapper);
Expand Down
43 changes: 43 additions & 0 deletions src/Controls/src/Core/Internals/InvalidationTriggerFlags.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;

namespace Microsoft.Maui.Controls.Internals;

[Flags]
internal enum InvalidationTriggerFlags : ushort
{
None = 0,
ApplyingBindingContext = 1 << 0,
WillNotifyParentMeasureInvalidated = 1 << 1,
WillTriggerHorizontalOptionsChanged = 1 << 2,
WillTriggerVerticalOptionsChanged = 1 << 3,
WillTriggerMarginChanged = 1 << 4,
WillTriggerSizeRequestChanged = 1 << 5,
WillTriggerMeasureChanged = 1 << 6,
WillTriggerRendererReady = 1 << 7,
WillTriggerUndefined = 1 << 8,
}

internal static class InvalidationTriggerFlagsExtensions {
public static InvalidationTriggerFlags ToInvalidationTriggerFlags(this InvalidationTrigger trigger) {
return trigger switch {
InvalidationTrigger.MeasureChanged => InvalidationTriggerFlags.WillTriggerMeasureChanged,
InvalidationTrigger.HorizontalOptionsChanged => InvalidationTriggerFlags.WillTriggerHorizontalOptionsChanged,
InvalidationTrigger.VerticalOptionsChanged => InvalidationTriggerFlags.WillTriggerVerticalOptionsChanged,
InvalidationTrigger.SizeRequestChanged => InvalidationTriggerFlags.WillTriggerSizeRequestChanged,
InvalidationTrigger.RendererReady => InvalidationTriggerFlags.WillTriggerRendererReady,
InvalidationTrigger.MarginChanged => InvalidationTriggerFlags.WillTriggerMarginChanged,
_ => InvalidationTriggerFlags.WillTriggerUndefined,
};
}

public static InvalidationTrigger ToInvalidationTrigger(this InvalidationTriggerFlags flags) {
if ((flags & InvalidationTriggerFlags.WillTriggerUndefined) != 0) return InvalidationTrigger.Undefined;
if ((flags & InvalidationTriggerFlags.WillTriggerRendererReady) != 0) return InvalidationTrigger.RendererReady;
if ((flags & InvalidationTriggerFlags.WillTriggerMeasureChanged) != 0) return InvalidationTrigger.MeasureChanged;
if ((flags & InvalidationTriggerFlags.WillTriggerSizeRequestChanged) != 0) return InvalidationTrigger.SizeRequestChanged;
if ((flags & InvalidationTriggerFlags.WillTriggerMarginChanged) != 0) return InvalidationTrigger.MarginChanged;
if ((flags & InvalidationTriggerFlags.WillTriggerVerticalOptionsChanged) != 0) return InvalidationTrigger.VerticalOptionsChanged;
if ((flags & InvalidationTriggerFlags.WillTriggerHorizontalOptionsChanged) != 0) return InvalidationTrigger.HorizontalOptionsChanged;
return InvalidationTrigger.Undefined;
}
}
75 changes: 23 additions & 52 deletions src/Controls/src/Core/LegacyLayouts/Layout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,12 @@ private protected override IList<Element> LogicalChildrenInternalBackingStore
public override SizeRequest Measure(double widthConstraint, double heightConstraint, MeasureFlags flags = MeasureFlags.None)
{
SizeRequest size = base.Measure(widthConstraint - Padding.HorizontalThickness, heightConstraint - Padding.VerticalThickness, flags);
return new SizeRequest(new Size(size.Request.Width + Padding.HorizontalThickness, size.Request.Height + Padding.VerticalThickness),
new Size(size.Minimum.Width + Padding.HorizontalThickness, size.Minimum.Height + Padding.VerticalThickness));
var request = new Size(size.Request.Width + Padding.HorizontalThickness, size.Request.Height + Padding.VerticalThickness);
var minimum = new Size(size.Minimum.Width + Padding.HorizontalThickness, size.Minimum.Height + Padding.VerticalThickness);

DesiredSize = request;

return new SizeRequest(request, minimum);
}

/// <summary>
Expand Down Expand Up @@ -294,13 +298,19 @@ public void RaiseChild(View view)
OnChildrenReordered();
}

internal virtual void InvalidateLayoutInternal()
{
_hasDoneLayout = false;
InvalidateMeasureCacheInternal();
}

/// <summary>
/// Invalidates the current layout.
/// </summary>
/// <remarks>Calling this method will invalidate the measure and triggers a new layout cycle.</remarks>
protected virtual void InvalidateLayout()
{
_hasDoneLayout = false;
InvalidateLayoutInternal();
InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
if (!_hasDoneLayout)
{
Expand All @@ -319,10 +329,15 @@ protected virtual void InvalidateLayout()
/// It is suggested to still call the base method and modify its calculated results.</remarks>
protected abstract void LayoutChildren(double x, double y, double width, double height);

internal override void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger)
internal override bool OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger)
{
// TODO: once we remove old Xamarin public signatures we can invoke `OnChildMeasureInvalidated(VisualElement, InvalidationTrigger)` directly
OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger));
if (base.OnChildMeasureInvalidatedInternal(child, trigger))
{
OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger));
return true;
}

return false;
}

/// <summary>
Expand All @@ -334,8 +349,7 @@ internal override void OnChildMeasureInvalidatedInternal(VisualElement child, In
/// <remarks>This method has a default implementation and application developers must call the base implementation.</remarks>
protected void OnChildMeasureInvalidated(object sender, EventArgs e)
{
InvalidationTrigger trigger = (e as InvalidationEventArgs)?.Trigger ?? InvalidationTrigger.Undefined;
OnChildMeasureInvalidated((VisualElement)sender, trigger);
InvalidateLayoutInternal();
OnChildMeasureInvalidated();
}

Expand Down Expand Up @@ -497,42 +511,6 @@ internal static void LayoutChildIntoBoundingRegion(View child, Rect region, Size
child.Layout(region);
}

internal virtual void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger)
{
IReadOnlyList<Element> children = LogicalChildrenInternal;
int count = children.Count;
for (var index = 0; index < count; index++)
{
if (LogicalChildrenInternal[index] is VisualElement v && v.IsVisible && (!v.IsPlatformEnabled || !v.IsPlatformStateConsistent))
{
return;
}
}

if (child is View view)
{
// we can ignore the request if we are either fully constrained or when the size request changes and we were already fully constrained
if ((trigger == InvalidationTrigger.MeasureChanged && view.Constraint == LayoutConstraint.Fixed) ||
(trigger == InvalidationTrigger.SizeRequestChanged && view.ComputedConstraint == LayoutConstraint.Fixed))
{
return;
}
if (trigger == InvalidationTrigger.HorizontalOptionsChanged || trigger == InvalidationTrigger.VerticalOptionsChanged)
{
ComputeConstraintForView(view);
}
}

if (trigger == InvalidationTrigger.RendererReady)
{
InvalidateMeasureInternal(InvalidationTrigger.RendererReady);
}
else
{
InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
}
}

internal override void OnIsVisibleChanged(bool oldValue, bool newValue)
{
base.OnIsVisibleChanged(oldValue, newValue);
Expand Down Expand Up @@ -645,15 +623,8 @@ bool ShouldLayoutChildren()

protected override void InvalidateMeasureOverride()
{
InvalidateLayoutInternal();
base.InvalidateMeasureOverride();

foreach (var child in ((IElementController)this).LogicalChildren)
{
if (child is IView fe)
{
fe.InvalidateMeasure();
}
}
}

protected override Size ArrangeOverride(Rect bounds)
Expand Down
10 changes: 8 additions & 2 deletions src/Controls/src/Core/LegacyLayouts/StackLayout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,16 @@ internal override void ComputeConstraintForView(View view)
ComputeConstraintForView(view, false);
}

internal override void InvalidateMeasureInternal(InvalidationTrigger trigger)
internal override void InvalidateLayoutInternal()
{
base.InvalidateLayoutInternal();
_layoutInformation = new LayoutInformation();
base.InvalidateMeasureInternal(trigger);
}

internal override void InvalidateMeasureInternal(InvalidationTriggerFlags flags)
{
InvalidateLayoutInternal();
base.InvalidateMeasureInternal(flags);
}

void AlignOffAxis(LayoutInformation layout, StackOrientation orientation, double widthConstraint, double heightConstraint)
Expand Down
64 changes: 37 additions & 27 deletions src/Controls/src/Core/Page/Page.cs
Original file line number Diff line number Diff line change
Expand Up @@ -497,10 +497,44 @@ protected override void OnBindingContextChanged()
SetInheritedBindingContext(TitleView, BindingContext);
}

internal override void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger)
internal override bool OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger)
{
// TODO: once we remove old Xamarin public signatures we can invoke `OnChildMeasureInvalidated(VisualElement, InvalidationTrigger)` directly
// Behave like `VisualElement` except for propagation to parent
switch (trigger)
{
case InvalidationTrigger.Undefined:
if (IsApplyingBindings)
{
// If we're applying bindings, we need to wait until it's done to invalidate the measure
MeasureInvalidationStatus |= InvalidationTriggerFlags.WillNotifyParentMeasureInvalidated;
return false;
}

InvokeMeasureInvalidated(InvalidationTrigger.MeasureChanged);
break;

default:
// When visibility changes `InvalidationTrigger.Undefined` is used,
// so here we're sure that visibility didn't change
if (child.IsVisible)
{
if (IsApplyingBindings)
{
// If we're applying bindings, we need to wait until it's done to invalidate the measure
MeasureInvalidationStatus |= InvalidationTriggerFlags.WillNotifyParentMeasureInvalidated;
return false;
}

// We need to invalidate measures only if child is actually visible
InvokeMeasureInvalidated(InvalidationTrigger.MeasureChanged);
}
break;
}

// We still need to call the legacy OnChildMeasureInvalidated to keep the compatibility.
OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger));

return true;
}

/// <summary>
Expand All @@ -510,8 +544,7 @@ internal override void OnChildMeasureInvalidatedInternal(VisualElement child, In
/// <param name="e">The event arguments.</param>
protected virtual void OnChildMeasureInvalidated(object sender, EventArgs e)
{
InvalidationTrigger trigger = (e as InvalidationEventArgs)?.Trigger ?? InvalidationTrigger.Undefined;
OnChildMeasureInvalidated((VisualElement)sender, trigger);
// Nothing to do here: platform will take care of arranging the children if needed on the next layout pass
}

/// <summary>
Expand Down Expand Up @@ -583,29 +616,6 @@ protected void UpdateChildrenLayout()
}
}

internal virtual void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger)
{
var container = this as IPageContainer<Page>;
if (container != null)
{
Page page = container.CurrentPage;
if (page != null && page.IsVisible && (!page.IsPlatformEnabled || !page.IsPlatformStateConsistent))
return;
}
else
{
var logicalChildren = this.InternalChildren;
for (var i = 0; i < logicalChildren.Count; i++)
{
var v = logicalChildren[i] as VisualElement;
if (v != null && v.IsVisible && (!v.IsPlatformEnabled || !v.IsPlatformStateConsistent))
return;
}
}

InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
}

internal void OnAppearing(Action action)
{
if (_hasAppeared)
Expand Down
Loading
Loading