Skip to content

Commit

Permalink
Optimize performance of Flex.Item (dotnet#20034)
Browse files Browse the repository at this point in the history
* improve exceptions

* make Flex.Item into a real list

* enable nullable types

* add benchmark

---------

Co-authored-by: Edward Miller <symbiogenisis@outlook.com>
  • Loading branch information
symbiogenesis and Edward Miller authored Mar 18, 2024
1 parent 4306566 commit 8569185
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 55 deletions.
23 changes: 1 addition & 22 deletions src/Controls/src/Core/Layout/FlexExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,32 +1,11 @@
#nullable disable
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Layouts;
using Flex = Microsoft.Maui.Layouts.Flex;

namespace Microsoft.Maui.Controls
{
static class FlexExtensions
{
public static int IndexOf(this Flex.Item parent, Flex.Item child)
{
var index = -1;
foreach (var it in parent)
{
index++;
if (it == child)
return index;
}
return -1;
}

public static void Remove(this Flex.Item parent, Flex.Item child)
{
var index = parent.IndexOf(child);
if (index < 0)
return;
parent.RemoveAt((uint)index);
}

public static Rect GetFrame(this Flex.Item item)
{
return new Rect(item.Frame[0], item.Frame[1], item.Frame[2], item.Frame[3]);
Expand Down
48 changes: 15 additions & 33 deletions src/Core/src/Layouts/Flex.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ public Basis(float length, bool isRelative = false)
/// <summary>
/// An item with flexbox properties. Items can also contain other items and be enumerated.
/// </summary>
class Item : IEnumerable<Item>
class Item : List<Item>
{
/// <summary>
/// Gets the frame (x, y, w, h).
Expand All @@ -239,7 +239,6 @@ class Item : IEnumerable<Item>
/// <summary>The parent item.</summary>
/// <value>The parent item, or null if the item is a root item.</value>
public Item? Parent { get; private set; }
List<Item>? Children { get; set; }
bool ShouldOrderChildren { get; set; }

///<summary>This property defines how the layout engine will distribute space between and around child items that have been laid out on multiple lines. This property is ignored if the root item does not have its <see cref="P:Microsoft.Maui.Controls.Flex.Item.Wrap" /> property set to Wrap or WrapReverse.</summary>
Expand Down Expand Up @@ -392,41 +391,30 @@ public Item(float width, float height)
Height = height;
}

public void Add(Item child)
public new void Add(Item child)
{
ValidateChild(child);
(Children ?? (Children = new List<Item>())).Add(child);
base.Add(child);
child.Parent = this;
ShouldOrderChildren |= child.Order != 0;
}

public void InsertAt(int index, Item child)
{
ValidateChild(child);
(Children ?? (Children = new List<Item>())).Insert(index, child);
base.Insert(index, child);
child.Parent = this;
ShouldOrderChildren |= child.Order != 0;
}

public Item RemoveAt(uint index)
{
var child = Children![(int)index];
var child = this[(int)index];
child.Parent = null;
Children.RemoveAt((int)index);
base.RemoveAt((int)index);
return child;
}

public int Count =>
(Children?.Count ?? 0);

public Item ItemAt(int index) =>
Children![index];

public Item this[int index]
{
get => ItemAt(index);
}

public Item Root
{
get
Expand All @@ -453,12 +441,6 @@ public void Layout()

public SelfSizingDelegate? SelfSizing { get; set; }

IEnumerator IEnumerable.GetEnumerator() =>
((IEnumerable<Item>)this).GetEnumerator();

IEnumerator<Item> IEnumerable<Item>.GetEnumerator() =>
(Children ?? global::System.Linq.Enumerable.Empty<Item>()).GetEnumerator();

void ValidateChild(Item child)
{
if (this == child)
Expand All @@ -469,7 +451,7 @@ void ValidateChild(Item child)

static void layout_item(Item item, float width, float height)
{
if (item.Children == null || item.Children.Count == 0)
if (item == null || item.Count == 0)
return;

var layout = new flex_layout();
Expand Down Expand Up @@ -667,7 +649,7 @@ float MarginThickness(bool vertical) =>
static void layout_align(Justify align, float flex_dim, int children_count, ref float pos_p, ref float spacing_p)
{
if (flex_dim < 0)
throw new ArgumentException();
throw new ArgumentException($"{nameof(flex_dim)} must not be negative", nameof(flex_dim));
pos_p = 0;
spacing_p = 0;

Expand Down Expand Up @@ -700,14 +682,14 @@ static void layout_align(Justify align, float flex_dim, int children_count, ref
}
return;
default:
throw new ArgumentException();
throw new ArgumentException($"{nameof(Justify)} option not handled", nameof(align));
}
}

static void layout_align(AlignContent align, float flex_dim, uint children_count, ref float pos_p, ref float spacing_p)
{
if (flex_dim < 0)
throw new ArgumentException();
throw new ArgumentException($"{nameof(flex_dim)} must not be negative", nameof(flex_dim));
pos_p = 0;
spacing_p = 0;

Expand Down Expand Up @@ -743,14 +725,14 @@ static void layout_align(AlignContent align, float flex_dim, uint children_count
spacing_p = flex_dim / children_count;
return;
default:
throw new ArgumentException();
throw new ArgumentException($"{nameof(AlignContent)} option not handled", nameof(align));
}
}

static void layout_items(Item item, int child_begin, int child_end, int children_count, ref flex_layout layout)
{
if (children_count > (child_end - child_begin))
throw new ArgumentException();
throw new ArgumentException($"The {children_count} must not be smaller than the requested range between {child_begin} and {child_end}", nameof(children_count));
if (children_count <= 0)
return;
if (layout.flex_dim > 0 && layout.extra_flex_dim > 0)
Expand Down Expand Up @@ -975,7 +957,7 @@ public void init(Item item, float width, float height)
|| item.PaddingRight < 0
|| item.PaddingTop < 0
|| item.PaddingBottom < 0)
throw new ArgumentException();
throw new ArgumentException($"The padding on {nameof(item)} must not be negative", nameof(item));

width = Math.Max(0, width - item.PaddingLeft + item.PaddingRight);
height = Math.Max(0, height - item.PaddingTop + item.PaddingBottom);
Expand Down Expand Up @@ -1021,7 +1003,7 @@ public void init(Item item, float width, float height)
{
int prev = indices[j - 1];
int curr = indices[j];
if (item.Children![prev].Order <= item.Children[curr].Order)
if (item[prev].Order <= item[curr].Order)
{
break;
}
Expand Down Expand Up @@ -1057,7 +1039,7 @@ public void init(Item item, float width, float height)
}

public Item child_at(Item item, int i) =>
item.Children![(ordered_indices?[i] ?? i)];
item[ordered_indices?[i] ?? i];

public void cleanup()
{
Expand Down
83 changes: 83 additions & 0 deletions src/Core/tests/Benchmarks/Benchmarks/FlexLayoutBenchmarker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System;
using BenchmarkDotNet.Attributes;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Layouts;

namespace Microsoft.Maui.Benchmarks
{
[MemoryDiagnoser]
public class FlexLayoutBenchmarker
{
readonly Border[] _views =
[
new Border(), new Border(), new Border(), new Border(), new Border(), new Border(), new Border(),
new Border(), new Border(), new Border(), new Border(), new Border(), new Border(), new Border()
];

const int Iterations = 100;

[GlobalSetup]
public void Setup()
{
foreach (var view in _views)
{
view.WidthRequest = 100 * Random.Shared.NextDouble();
view.HeightRequest = 100 * Random.Shared.NextDouble();
}
}

[Benchmark]
public void LayoutLotsOfItemsWithWrap()
{
var layout = new FlexLayout { Wrap = FlexWrap.Wrap };

LayoutLotsOfItems(layout);
}

[Benchmark]
public void LayoutLotsOfItemsNoWrap()
{
var layout = new FlexLayout { Wrap = FlexWrap.NoWrap };

LayoutLotsOfItems(layout);
}

private void LayoutLotsOfItems(FlexLayout layout)
{
var parent = new Grid();

layout.Parent = parent;

for (int x = 0; x < Iterations; x++)
{
foreach (var view in _views)
{
layout.Add(view);

// Vary the size of the layout and the views
double layoutWidth = x * 10 * Random.Shared.NextDouble();
double layoutHeight = x * 10 * Random.Shared.NextDouble();
layout.WidthRequest = layoutWidth;
layout.HeightRequest = layoutHeight;
view.WidthRequest = x * Random.Shared.NextDouble();
view.HeightRequest = x * Random.Shared.NextDouble();

// Vary the properties of the views
FlexLayout.SetBasis(view, (x % 2 == 0) ? FlexBasis.Auto : new FlexBasis(1, true));
FlexLayout.SetOrder(view, x % 5);
FlexLayout.SetGrow(view, x % 3);
FlexLayout.SetShrink(view, x % 4);

layout.Layout(new Rect(0, 0, layoutWidth, layoutHeight));

// Remove every 10th view
if (x % 10 == 0)
{
layout.Remove(view);
}
}
}
}
}
}

0 comments on commit 8569185

Please sign in to comment.