Skip to content

Commit 1378ae2

Browse files
[layout] avoid IView[] creation in LayoutExtensions
Context: https://github.com/unoplatform/performance/tree/master/src/dopes/DopeTestMaui Reviewing `dotnet trace` output of the "dopes" test sample, 7% of CPU time is spent in `LayoutExtensions.OrderByZIndex()`: 3.42s (16%) microsoft.maui!Microsoft.Maui.Handlers.LayoutHandler.Add(Microsoft.Maui.IView) 1.52s (7.3%) microsoft.maui!Microsoft.Maui.Handlers.LayoutExtensions.GetLayoutHandlerIndex(Microsoft.Maui.ILayout,Microsoft.Maui.IVie... 1.50s (7.2%) microsoft.maui!Microsoft.Maui.Handlers.LayoutExtensions.OrderByZIndex(Microsoft.Maui.ILayout) Reviewing the code in `OrderByZIndex()`: * Makes a `record ViewAndIndex(IView View, int Index)` for each child * Makes a `ViewAndIndex[]` the size of the number of children * Call `Array.Sort()` * Makes a `IView[]` the size of the number of children * Iterating twice over the arrays in the process Then if you look at the `GetLayoutHandlerIndex()` method, it does all the same work to create an `IView[]`, get the index, then throws the array away. I removed the above code and used a System.Linq `OrderBy()` with faster paths for 0 or 1 children. I also made an `internal` `EnumerateByZIndex()` method for use by `foreach` loops. This avoids creating arrays in those cases. The `ZIndexTests` still pass for me, which proves out `OrderBy()`'s stable sort: https://docs.microsoft.com/dotnet/api/system.linq.enumerable.orderby After this change, the % time spent in these methods went down: 1.84s (13%) microsoft.maui!Microsoft.Maui.Handlers.LayoutHandler.Add(Microsoft.Maui.IView) 352.24ms (2.50%) microsoft.maui!Microsoft.Maui.Handlers.LayoutExtensions.GetLayoutHandlerIndex(Microsoft.Maui.ILayout,Microsoft.Maui.IVie... 181.27ms (1.30%) microsoft.maui!Microsoft.Maui.Handlers.LayoutExtensions.<>c.<EnumerateByZIndex>b__0_0(Microsoft.Maui.IView) 2.78ms (0.02%) microsoft.maui!Microsoft.Maui.Handlers.LayoutExtensions.EnumerateByZIndex(Microsoft.Maui.ILayout) This kind of goes against the guidance "avoid System.Linq". But `OrderBy()` generally seems to be fine? ~~ Results ~~ A `Release` build on a Pixel 5 device, I was getting: Before: 103.04 Dopes/s After: 230.87 Dopes/s I suspect that was a lot of `IView[]`'s before!
1 parent 0b6976e commit 1378ae2

5 files changed

+25
-45
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,39 @@
11
using System;
2-
using System.Collections.Generic;
2+
using System.Linq;
33

44
namespace Microsoft.Maui.Handlers
55
{
66
internal static class LayoutExtensions
77
{
8-
record ViewAndIndex(IView View, int Index);
9-
10-
class ZIndexComparer : IComparer<ViewAndIndex>
11-
{
12-
public int Compare(ViewAndIndex? x, ViewAndIndex? y)
13-
{
14-
if (x == null || y == null)
15-
{
16-
return 0;
17-
}
18-
19-
var zIndexCompare = x.View.ZIndex.CompareTo(y.View.ZIndex);
20-
21-
if (zIndexCompare == 0)
22-
{
23-
return x.Index.CompareTo(y.Index);
24-
}
25-
26-
return zIndexCompare;
27-
}
28-
}
29-
30-
static ZIndexComparer s_comparer = new();
8+
// <summary>
9+
// Use this overload if you want to `foreach` over it
10+
// </summary>
11+
internal static IOrderedEnumerable<IView> EnumerateByZIndex(this ILayout layout) => layout.OrderBy(v => v.ZIndex);
3112

3213
public static IView[] OrderByZIndex(this ILayout layout)
3314
{
34-
var count = layout.Count;
35-
var indexedViews = new ViewAndIndex[count];
36-
37-
for (int n = 0; n < count; n++)
15+
switch (layout.Count)
3816
{
39-
indexedViews[n] = new ViewAndIndex(layout[n], n);
17+
case 0:
18+
return Array.Empty<IView>();
19+
case 1:
20+
return new[] { layout[0] };
21+
default:
22+
return layout.EnumerateByZIndex().ToArray();
4023
}
41-
42-
Array.Sort(indexedViews, s_comparer);
43-
44-
var ordered = new IView[count];
45-
46-
for (int n = 0; n < count; n++)
47-
{
48-
ordered[n] = indexedViews[n].View;
49-
}
50-
51-
return ordered;
5224
}
5325

5426
public static int GetLayoutHandlerIndex(this ILayout layout, IView view)
5527
{
56-
return layout.OrderByZIndex().IndexOf(view);
28+
switch (layout.Count)
29+
{
30+
case 0:
31+
return -1;
32+
case 1:
33+
return view == layout[0] ? 0 : -1;
34+
default:
35+
return layout.EnumerateByZIndex().IndexOf(view);
36+
}
5737
}
5838
}
5939
}

src/Core/src/Handlers/Layout/LayoutHandler.Android.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public override void SetVirtualView(IView view)
3838

3939
PlatformView.RemoveAllViews();
4040

41-
foreach (var child in VirtualView.OrderByZIndex())
41+
foreach (var child in VirtualView.EnumerateByZIndex())
4242
{
4343
PlatformView.AddView(child.ToPlatform(MauiContext));
4444
}

src/Core/src/Handlers/Layout/LayoutHandler.Tizen.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public override void SetVirtualView(IView view)
4545

4646
PlatformView.Children.Clear();
4747

48-
foreach (var child in VirtualView.OrderByZIndex())
48+
foreach (var child in VirtualView.EnumerateByZIndex())
4949
{
5050
PlatformView.Children.Add(child.ToPlatform(MauiContext));
5151
if (child.Handler is IPlatformViewHandler thandler)

src/Core/src/Handlers/Layout/LayoutHandler.Windows.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public override void SetVirtualView(IView view)
2929

3030
PlatformView.Children.Clear();
3131

32-
foreach (var child in VirtualView.OrderByZIndex())
32+
foreach (var child in VirtualView.EnumerateByZIndex())
3333
{
3434
PlatformView.Children.Add(child.ToPlatform(MauiContext));
3535
}

src/Core/src/Handlers/Layout/LayoutHandler.iOS.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public override void SetVirtualView(IView view)
3838
// Remove any previous children
3939
PlatformView.ClearSubviews();
4040

41-
foreach (var child in VirtualView.OrderByZIndex())
41+
foreach (var child in VirtualView.EnumerateByZIndex())
4242
{
4343
PlatformView.AddSubview(child.ToPlatform(MauiContext));
4444
}

0 commit comments

Comments
 (0)