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

[WinUI] Allocate less when updating gestures #21450

Conversation

MartyIX
Copy link
Contributor

@MartyIX MartyIX commented Mar 26, 2024

Description of Change

This PR adds a few very basic optimizations to improve performance of GesturePlatformManager.UpdateDragAndDropGestureRecognizers and gestures in general.

Measurements

Using commit e984695, I did:

cd src/Controls/samples/Controls.Sample.Sandbox
dotnet publish -f net8.0-windows10.0.19041.0 -c Release -p:PublishReadyToRun=false -p:WindowsPackageType=None
dotnet trace collect --format speedscope -- .\bin\Release\net8.0-windows10.0.19041.0\win10-x64\publish\Maui.Controls.Sample.Sandbox.exe

Speedscope files:
Maui.Controls.Sample.Sandbox.exe_20240326_114219.speedscope.MAIN.json
Maui.Controls.Sample.Sandbox.exe_20240326_114435.speedscope.PR.json

Results

main:
image

PR:
image

Unfortunately, the results are not stable. So the results are not meaningful. I think one would have to start the application and then create gestures in code to measure it better. Any other ideas?

Issues Fixed

Contributes to #21449
See also: #21377

@MartyIX MartyIX requested a review from a team as a code owner March 26, 2024 10:53
@dotnet-policy-service dotnet-policy-service bot added the community ✨ Community Contribution label Mar 26, 2024
Copy link
Contributor

Hey there @MartyIX! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

@@ -60,5 +59,25 @@ static class EnumerableExtensions
}
}
}

public static bool HasAnyGesturesFor<T>(this IEnumerable<IGestureRecognizer>? gestures, Func<T, bool>? predicate = null) where T : GestureRecognizer
Copy link
Contributor Author

@MartyIX MartyIX Mar 26, 2024

Choose a reason for hiding this comment

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

This is to avoid allocation on L53.


_container.CanDrag = gestures.GetGesturesFor<DragGestureRecognizer>()
.FirstOrDefault()?.CanDrag ?? false;
bool canDrag = gestures.GetGesturesFor<DragGestureRecognizer>().FirstOrDefault()?.CanDrag ?? false;
Copy link
Contributor Author

@MartyIX MartyIX Mar 26, 2024

Choose a reason for hiding this comment

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

Store value in a local variable, so we save 1 interop call on L692 (i.e. if (_container.CanDrag) vs. if (canDrag)) and L698.

Copy link
Member

Choose a reason for hiding this comment

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

Can this use your new HasAnyGesturesFor() method? And check CanDrag inside the predicate?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have come up with acadf48. Not really sure if that is what you had in mind.

@@ -714,7 +718,7 @@ void UpdatingGestureRecognizers()
IList<TapGestureRecognizer>? childGestures =
children?.GetChildGesturesFor<TapGestureRecognizer>().ToList();

if (gestures.GetGesturesFor<TapGestureRecognizer>(g => g.NumberOfTapsRequired == 1).Any()
if (gestures.HasAnyGesturesFor<TapGestureRecognizer>(g => g.NumberOfTapsRequired == 1)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This should save 1 allocation.

bool hasPanGesture = gestures.GetGesturesFor<PanGestureRecognizer>().GetEnumerator().MoveNext();
bool hasSwipeGesture = gestures.HasAnyGesturesFor<SwipeGestureRecognizer>();
bool hasPinchGesture = gestures.HasAnyGesturesFor<PinchGestureRecognizer>();
bool hasPanGesture = gestures.HasAnyGesturesFor<PanGestureRecognizer>();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

3 allocations here.

Copy link
Member

Choose a reason for hiding this comment

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

So one thing to consider is this would still iterate over the gestures list three times. How many are normally in this list? If it's usually like 1-5, that might be OK.

Copy link
Contributor Author

@MartyIX MartyIX Mar 26, 2024

Choose a reason for hiding this comment

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

I think the list contains only a handful of items as you said.

@MartyIX
Copy link
Contributor Author

MartyIX commented Mar 26, 2024

cc @jonathanpeppers

@jsuarezruiz
Copy link
Contributor

/azp run

@jsuarezruiz jsuarezruiz added legacy-area-perf Startup / Runtime performance area-gestures Gesture types labels Mar 26, 2024
Copy link

Azure Pipelines successfully started running 3 pipeline(s).

bool hasPanGesture = gestures.GetGesturesFor<PanGestureRecognizer>().GetEnumerator().MoveNext();
bool hasSwipeGesture = gestures.HasAnyGesturesFor<SwipeGestureRecognizer>();
bool hasPinchGesture = gestures.HasAnyGesturesFor<PinchGestureRecognizer>();
bool hasPanGesture = gestures.HasAnyGesturesFor<PanGestureRecognizer>();
Copy link
Member

Choose a reason for hiding this comment

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

So one thing to consider is this would still iterate over the gestures list three times. How many are normally in this list? If it's usually like 1-5, that might be OK.

@@ -60,5 +59,25 @@ static class EnumerableExtensions
}
}
}

public static bool HasAnyGesturesFor<T>(this IEnumerable<IGestureRecognizer>? gestures, Func<T, bool>? predicate = null) where T : GestureRecognizer
Copy link
Member

Choose a reason for hiding this comment

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

Can we make the new method(s) internal?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in edd66ef.


_container.CanDrag = gestures.GetGesturesFor<DragGestureRecognizer>()
.FirstOrDefault()?.CanDrag ?? false;
bool canDrag = gestures.GetGesturesFor<DragGestureRecognizer>().FirstOrDefault()?.CanDrag ?? false;
Copy link
Member

Choose a reason for hiding this comment

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

Can this use your new HasAnyGesturesFor() method? And check CanDrag inside the predicate?

return false;
}

predicate ??= x => true;
Copy link
Member

Choose a reason for hiding this comment

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

Could we just do a null check inside the foreach?

if (item is T gesture && (predicate is null || predicate(gesture)))
{
	return true;
}

This way we aren't creating a lambda like x => true.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 39b7ec6.

@MartyIX
Copy link
Contributor Author

MartyIX commented Mar 26, 2024

Main: Maui.Controls.Sample.Sandbox.exe_20240326_202255.speedscope.json

-> UNMANAGED_CODE_TIME: 1:26 (1 minute 26 seconds)

PR: Maui.Controls.Sample.Sandbox.exe_20240326_202543.speedscope.json

-> UNMANAGED_CODE_TIME: 54.21s

On sample code:

MainPage.xaml

<ContentPage
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    x:Class="Maui.Controls.Sample.MainPage"
    x:DataType="local:MainPage"
    xmlns:local="clr-namespace:Maui.Controls.Sample">
    <VerticalStackLayout x:Name="myLayout">

        <Button Text="Add" Clicked="Button_Clicked"/>

        <Label Text="Drag me">
            <Label.GestureRecognizers>
                <DragGestureRecognizer CanDrag="True" />
            </Label.GestureRecognizers>
        </Label>

    </VerticalStackLayout>
</ContentPage>

MainPage.xaml.cs

using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;

namespace Maui.Controls.Sample
{
	public partial class MainPage : ContentPage
	{
		public MainPage()
		{
			InitializeComponent();
		}

		private void DragGestureRecognizer_DragStarting(object? sender, DragStartingEventArgs e)
		{

		}

		private void Button_Clicked(object sender, EventArgs e)
		{
			for (int i = 0; i < 10000; i++)
			{
				// Add the following XAML.
				/*
					<Rectangle Stroke="Red" Fill="DarkBlue" StrokeThickness="4" HeightRequest="200" WidthRequest="200">
						<Rectangle.GestureRecognizers>
							<DragGestureRecognizer DragStarting="DragGestureRecognizer_DragStarting" />
						</Rectangle.GestureRecognizers>
					</Rectangle>
				 */

				Microsoft.Maui.Controls.Shapes.Rectangle rectangle = new()
				{
					Fill = Colors.DarkBlue,
					Stroke = Colors.Red,
					StrokeThickness = 4,
					WidthRequest = 200,
					HeightRequest = 200,
				};

				DragGestureRecognizer recognizer = new();
				recognizer.DragStarting += DragGestureRecognizer_DragStarting;

				rectangle.GestureRecognizers.Add(recognizer);

				myLayout.Add(rectangle);
			}
		}
	}
}

@jonathanpeppers
Copy link
Member

/azp run

Copy link

Azure Pipelines successfully started running 3 pipeline(s).

Copy link
Member

@jonathanpeppers jonathanpeppers left a comment

Choose a reason for hiding this comment

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

The changes make sense to me. 👍

But I think someone most familiar with Windows gestures on the MAUI team should also review.

@MartyIX
Copy link
Contributor Author

MartyIX commented Mar 29, 2024

@mattleibow Could you please take a look?

@rmarinho rmarinho merged commit 0c02e4b into dotnet:main Apr 6, 2024
47 checks passed
@github-actions github-actions bot locked and limited conversation to collaborators May 9, 2024
@Eilon Eilon added t/perf 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
@MartyIX MartyIX deleted the feature/2024-03-26-gestures-less-allocations-final branch May 24, 2024 08:43
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-gestures Gesture types community ✨ Community Contribution fixed-in-8.0.40 fixed-in-9.0.0-preview.4.10690 t/perf 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.

6 participants