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

Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -1332,7 +1332,7 @@ public void TestEnd()
[Fact]
public void TestDefaultRowSpacing()
{
var layout = new Grid();
var layout = new Grid { IsPlatformEnabled = true };

bool preferredSizeChanged = false;
layout.MeasureInvalidated += (sender, args) =>
Expand All @@ -1352,7 +1352,7 @@ public void TestDefaultRowSpacing()
[Fact]
public void TestDefaultColumnSpacing()
{
var layout = new Grid();
var layout = new Grid { IsPlatformEnabled = true };

bool preferredSizeChanged = false;
layout.MeasureInvalidated += (sender, args) =>
Expand All @@ -1372,7 +1372,7 @@ public void TestDefaultColumnSpacing()
[Fact]
public void TestAddCell()
{
var layout = new Grid();
var layout = new Grid { IsPlatformEnabled = true };
bool preferredSizeChanged = false;
layout.MeasureInvalidated += (sender, args) => preferredSizeChanged = true;

Expand All @@ -1386,7 +1386,7 @@ public void TestAddCell()
[Fact]
public void TestMoveCell()
{
var layout = new Grid();
var layout = new Grid { IsPlatformEnabled = true };
var label = new Label();
layout.Children.Add(label, 0, 0);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,8 +352,7 @@ public void TestVisibility()

child1.IsVisible = false;

// Verify that the visibility change invalidated the layout, and simulate a native layout update
AssertInvalidated(handler);
// Simulate a native layout update
stack.ForceLayout();

Assert.False(child1.IsVisible);
Expand Down Expand Up @@ -668,7 +667,6 @@ public void PaddingResizeTest()
innerStack.Padding = new Thickness(30);

// Verify that the Padding change invalidated the layout, and simulate a native layout update
AssertInvalidated(handler);
outerLayout.ForceLayout();

var afterSize = innerStack.Bounds.Size;
Expand Down
12 changes: 8 additions & 4 deletions src/Controls/src/Core/BindableObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -364,11 +364,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 @@ -730,8 +735,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
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
Copy link
Contributor Author

Choose a reason for hiding this comment

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

InvalidationTrigger is a public API but at the same time it says

For internal use by platform renderers.

So I wonder if I could simply change that to:

[Flags]
public enum InvalidationTrigger : ushort
{
	None = 0,
	BatchingBindingContext = 1 << 0,
    // leaving empty space here in case we want to add other types of batching
 	NotifyOnChildMeasureInvalidated = 1 << 4,
	HorizontalOptionsChanged = 1 << 5,
	VerticalOptionsChanged = 1 << 6,
	MarginChanged = 1 << 7,
	SizeRequestChanged = 1 << 8,
	MeasureChanged = 1 << 9,
	RendererReady = 1 << 10,
	Undefined = 1 << 11,
}

{
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;
}
}
73 changes: 21 additions & 52 deletions src/Controls/src/Core/LegacyLayouts/Layout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,11 @@ public override SizeRequest Measure(double widthConstraint, double heightConstra
#pragma warning disable CS0618 // Type or member is obsolete
SizeRequest size = base.Measure(widthConstraint - Padding.HorizontalThickness, heightConstraint - Padding.VerticalThickness, flags);
#pragma warning restore CS0618 // Type or member is obsolete
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);
#pragma warning disable CS0618 // Type or member is obsolete
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));
DesiredSize = request;
return new SizeRequest(request, minimum);
#pragma warning restore CS0618 // Type or member is obsolete
}
#pragma warning restore CS0672 // Member overrides obsolete member
Expand Down Expand Up @@ -313,14 +315,20 @@ 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>
[Obsolete("Use InvalidateMeasure depending on your scenario")]
protected virtual void InvalidateLayout()
{
_hasDoneLayout = false;
InvalidateLayoutInternal();
InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
if (!_hasDoneLayout)
{
Expand All @@ -341,10 +349,15 @@ protected virtual void InvalidateLayout()
[Obsolete("Use ArrangeOverride")]
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 @@ -356,8 +369,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 @@ -531,42 +543,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 @@ -679,15 +655,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 @@ -97,10 +97,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
Loading