Skip to content

[.NET 10] Add layout performance logging #30130

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

Open
wants to merge 27 commits into
base: net10.0
Choose a base branch
from
Open

Conversation

jsuarezruiz
Copy link
Contributor

Description of Change

This PR includes changes to add an initial and basic performance monitoring system for the .NET MAUI framework, focusing on performance layout metrics. The system is integrated directly into the MAUI framework, instruments from the System.Diagnostics.Metrics API.

Based on #29972

@jsuarezruiz jsuarezruiz added t/enhancement ☀️ New feature or request perf/general The issue affects performance (runtime speed, memory usage, startup time, etc.) (sub: perf) labels Jun 23, 2025
@jsuarezruiz jsuarezruiz changed the title Fix 28091 ii [.NET 10] Add layout performance logging Jun 23, 2025
@jsuarezruiz jsuarezruiz marked this pull request as ready for review July 2, 2025 10:55
@jsuarezruiz jsuarezruiz requested a review from a team as a code owner July 2, 2025 10:55
@PureWeen PureWeen added this to the .NET 10.0-preview7 milestone Jul 9, 2025
@PureWeen PureWeen added the p/0 Work that we can't release without label Jul 9, 2025
Copy link
Member

@PureWeen PureWeen left a comment

Choose a reason for hiding this comment

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

  • Add Tests
  • Let's look at moving the tracker calls inside the arrange/measure pass to an extension method also account for if something null is registered as a tracker (which applies to flag checking)

@github-project-automation github-project-automation bot moved this from Todo to Changes Requested in MAUI SDK Ongoing Jul 9, 2025
Comment on lines +87 to +96


#if NET9_0_OR_GREATER
[FeatureSwitchDefinition("System.Diagnostics.Metrics.Meter.IsSupported")]
#endif
internal static bool IsMetricsSupported { get; } =
!AppContext.TryGetSwitch(
"System.Diagnostics.Metrics.Meter.IsSupported",
out bool isSupported)
|| isSupported;
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 use the same pattern as the other switches in this file?

IsHybridWebViewSupportedByDefault, for example shows what the default value is. It would be good if all the switches read the same.

For example:

AppContext.TryGetSwitch("System.Diagnostics.Metrics.Meter.IsSupported", out bool isSupported)
				? isSupported
				: IsMeterSupportedByDefault;

Copy link
Member

@jonathanpeppers jonathanpeppers Jul 10, 2025

Choose a reason for hiding this comment

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

I would also use the => to just make them all exactly the same.

return Untracked;

var sw = Stopwatch.StartNew();
return new ActionDisposable(() =>
Copy link
Member

Choose a reason for hiding this comment

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

Perf nit: This is no better than before :-)

This allocates:

  • A closure (since the action needs to access the sw and element local variables)
  • Action instance (the instance of the delegate)
  • ActionDisposable instance (that is what is returned)

You could get rid of the closure by passing the necessary parameters to the ctor of the ActionDisposable and then pass them as parameters to the action - that will remove the closure allocation at least.

But you would still be left with the creation of the delegate and the creation of the disposable object.

I'm torn between - let's use the original simple approach because "Code complexity" and fix it for real by having explicit start stop calls on a readonly structure. Such that StartTrackMeasure returns a readonly struct which contains the stop watch. And then you have to call Stop on the struct which will stop the stopwatch and report it. You would probably need one structure type per type of measure... That would not allocate anything.

BTW: Stopwatch.StartNew also allocates.... but that's probably a lost cause.

Maybe we should just do this simply... for now - too much complexity for unknown gain.

Copy link
Member

Choose a reason for hiding this comment

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

For context, this is what an IL of a similar method would look like:

        IL_0000: newobj instance void C/'<>c__DisplayClass0_0'::.ctor()
        IL_0005: dup
        IL_0006: ldarg.1
        IL_0007: stfld string C/'<>c__DisplayClass0_0'::e
        IL_000c: dup
        IL_000d: call class [System.Runtime]System.Diagnostics.Stopwatch [System.Runtime]System.Diagnostics.Stopwatch::StartNew()
        IL_0012: stfld class [System.Runtime]System.Diagnostics.Stopwatch C/'<>c__DisplayClass0_0'::sw
        IL_0017: ldftn instance void C/'<>c__DisplayClass0_0'::'<M>b__0'()
        IL_001d: newobj instance void [System.Runtime]System.Action::.ctor(object, native int)
        IL_0022: newobj instance void ActionDisposable::.ctor(class [System.Runtime]System.Action)
        IL_0027: ret

All three newobj are over a class so true allocations (not a new struct)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the detailed feedback! You're absolutely right. After thinking it over, I realized an explicit Start/Stop pattern is both simpler and can be more allocation-free.

I changed the implementation from using ActionDisposable to use Start/Stop methods. I created a PerformanceTracker class that is a read-only struct. No closures, delegates, or disposable objects are created. Then, we use for tracking the Start method that returns a PerformanceTracker struct. No allocations occur. Then, the Stop method avoids using Stopwatch.StartNew(). Unlike Stopwatch.StartNew() (which allocates a Stopwatch object), GetTimestamp() avoids object creation.

I think it's more efficient now, although I'll review it by adding more related tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
p/0 Work that we can't release without perf/general The issue affects performance (runtime speed, memory usage, startup time, etc.) (sub: perf) t/enhancement ☀️ New feature or request
Projects
Status: Changes Requested
Development

Successfully merging this pull request may close these issues.

4 participants