-
Notifications
You must be signed in to change notification settings - Fork 1.9k
[.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
base: net10.0
Are you sure you want to change the base?
Conversation
src/Core/src/PerformanceTracker/Extensions/PerformanceMauiAppBuilderExtensions.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this 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)
|
||
|
||
#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; |
There was a problem hiding this comment.
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;
There was a problem hiding this comment.
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(() => |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
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