Skip to content

Conversation

mattleibow
Copy link
Member

@mattleibow mattleibow commented Aug 7, 2025

Note

Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!

Description of Change

Note

This is an alternate version #30130 and #31049 with just the core tracking features.

This PR introduces comprehensive diagnostics and metrics tracking for .NET MAUI applications, focusing on layout performance monitoring with an extensible architecture for future observability needs.

image image

🎯 What This Adds

Core Diagnostics Infrastructure:

  • ActivitySource: "Microsoft.Maui" - Tracks layout operations with detailed timing
  • Metrics: "Microsoft.Maui" - Records counters and histograms for performance analysis
  • Feature Switch: System.Diagnostics.Metrics.Meter.IsSupported - Runtime enable/disable for AOT/trimming

Layout Performance Tracking:

  • Instruments IView.Measure() and IView.Arrange() operations
  • Records timing data and operation counts with rich contextual tags
  • Zero-allocation struct-based instrumentation using using pattern

📊 Metrics Collected

Metric Name Type Description
maui.layout.measure_count Counter Number of measure operations
maui.layout.measure_duration Histogram Time spent measuring (ns)
maui.layout.arrange_count Counter Number of arrange operations
maui.layout.arrange_duration Histogram Time spent arranging (ns)

🏷️ Diagnostic Tags

All Views:

  • element.type - Full type name

Controls (Element/VisualElement):

  • element.id, element.automation_id, element.class_id, element.style_id
  • element.class, element.frame

🔌 Extensibility Interfaces

The diagnostics system provides three key interfaces for extensibility:

IDiagnosticTagger

Purpose: Add contextual tags to activities and metrics

internal interface IDiagnosticTagger
{
    void AddTags(object? source, ref TagList tagList);
}
  • Called for every diagnostic event to enrich with contextual information
  • Multiple taggers can contribute tags for the same event
  • Examples: element properties, navigation state, performance characteristics

IDiagnosticMetrics

Purpose: Define domain-specific metrics collections

internal interface IDiagnosticMetrics
{
    void Create(Meter meter);
}
  • Called once during startup to create all metrics for a specific domain
  • Enables type-safe access via MauiDiagnostics.GetMetrics<T>()
  • Examples: layout metrics, navigation metrics, data binding metrics

IDiagnosticInstrumentation

Purpose: Represent a single diagnostic measurement session

internal interface IDiagnosticInstrumentation : IDisposable
{
    void Stopped(MauiDiagnostics diag, in TagList tagList);
}
  • Automatically starts activities in constructor, records metrics when disposed
  • Designed for using statement pattern for zero-overhead tracking
  • Called when instrumentation completes to record final metrics

🔧 Architecture

Extensible Plugin System:

// Add custom taggers
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IDiagnosticTagger, MyTagger>());

// Add custom metrics
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IDiagnosticMetrics, MyMetrics>());

Usage in VisualElement:

Size IView.Measure(double widthConstraint, double heightConstraint)
{
    using var _ = DiagnosticInstrumentation.StartLayoutMeasure(this);
    DesiredSize = MeasureOverride(widthConstraint, heightConstraint);
    return DesiredSize;
}

📝 Custom Instrumentation Example

Here's how to extend the system with custom navigation metrics:

// 1. Create custom metrics
internal class NavigationDiagnosticMetrics : IDiagnosticMetrics
{
    public Counter<int>? NavigationCounter { get; private set; }

    public void Create(Meter meter)
    {
        NavigationCounter = meter.CreateCounter<int>(
            "maui.navigation.count", 
            "{navigations}", 
            "Number of navigation operations");
    }

    public void RecordNavigation(string navigationType, in TagList tagList)
    {
        NavigationCounter?.Add(1, tagList);
    }
}

// 2. Create custom instrumentation
readonly struct NavigationInstrumentation(Page page, string navigationType) : IDiagnosticInstrumentation
{
    readonly Activity? _activity = page.StartDiagnosticActivity($"Navigate.{navigationType}");

    public void Dispose() => page.StopDiagnostics(_activity, this);

    public void Stopped(MauiDiagnostics diag, in TagList tagList) =>
        diag.GetMetrics<NavigationDiagnosticMetrics>()?.RecordNavigation(navigationType, in tagList);
}

// 3. Register metrics with DI
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IDiagnosticMetrics, NavigationDiagnosticMetrics>());

// 4. Use in navigation code
public async Task<bool> PushAsync(Page page)
{
    using var _ = new NavigationInstrumentation(page, "Push");
    return await base.PushAsync(page);
}

This pattern enables teams to add domain-specific observability (navigation, data binding, network requests, etc.) while leveraging the shared infrastructure.

Other Examples

Simple Activities

We can just fire off activities without complex things:

using var _ = this.GetMauiDiagnostics()?.ActivitySource?.StartActivity("Button.Click");

Or, if we want the default tags:

using var _ = this.StartDiagnosticActivity("Button.Click");

Simple Meters

// 1. Metrics
internal class ButtonDiagnosticMetrics : IDiagnosticMetrics
{
	internal Counter<int>? ClickCounter { get; private set; }

	public void Create(Meter meter) => ClickCounter = meter.CreateCounter<int>("maui.button.click_count");

	public void RecordClick() => ClickCounter?.Add(1);
}

// 2. Registered in the builder
mauiAppBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IDiagnosticMetrics, LayoutDiagnosticMetrics>(_ => new LayoutDiagnosticMetrics()));

// 3. Usage
this.GetMauiDiagnostics()?.GetMetrics<ButtonDiagnosticMetrics>()?.RecordClick(in tagList);

Very Simple Metrics (Not Great)

Because the meter instance caches the particular counter, we can always just fetch. Not great, but it will work.

this.GetMauiDiagnostics()?.Meter?.CreateCounter<int>("maui.button.click_count").Add(1);

Instrumentation without Activity but persisted Meter (Bit Heavy)

// Helper method
public static ButtonClickInstrumentation? StartButtonClick(IView view) =>
	RuntimeFeature.IsMeterSupported
		? new ButtonClickInstrumentation(view)
		: null;

// Instrumentation
internal struct ButtonClickInstrumentation(IView view) : IDiagnosticInstrumentation
{
	public void Dispose() =>
		view.StopDiagnostics(null, this);

	public void Stopped(MauiDiagnostics diag, in TagList tagList) =>
		diag.GetMetrics<ButtonDiagnosticMetrics>()?.RecordClick(in tagList);
}

// Metrics
internal class ButtonDiagnosticMetrics : IDiagnosticMetrics
{
	internal Counter<int>? ClickCounter { get; private set; }

	public void Create(Meter meter)
	{
		ClickCounter = meter.CreateCounter<int>("maui.button.click_count");
	}

	public void RecordClick(in TagList tagList)
	{
		ClickCounter?.Add(1, tagList);
	}
}

// Registered in the builder
mauiAppBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IDiagnosticMetrics, LayoutDiagnosticMetrics>(_ => new LayoutDiagnosticMetrics()));

Instrumentation with Activity (Bit Heavy)

// Helper method
public static ButtonClickInstrumentation? StartButtonClick(IView view) =>
	RuntimeFeature.IsMeterSupported
		? new ButtonClickInstrumentation(view)
		: null;

// Instrumentation
internal struct ButtonClickInstrumentation(IView view)
{
	readonly Activity? _activity = view.StartDiagnosticActivity("Button.Click");

	public void Dispose() =>
		view.StopDiagnostics(_activity);
}

⚡ Performance

  • Minimal overhead when disabled via feature switches
  • Zero allocation struct-based instrumentation
  • Lazy initialization - only creates objects when listeners are registered
  • AOT/Trimming friendly with proper feature switches

🗂️ Files Added

Core Infrastructure:

  • MauiDiagnostics.cs - Central diagnostics hub
  • src/Core/src/Diagnostics/IDiagnostic*.cs - Plugin interfaces
  • MauiDiagnosticsExtensions.cs - Extension methods

Layout Instrumentation:

  • src/Core/src/Diagnostics/Instrumentation/Layout*.cs - Layout-specific tracking
  • DiagnosticInstrumentation.cs - Factory

Tagging System:

  • ViewDiagnosticTagger.cs - Base view tags
  • ControlsViewDiagnosticTagger.cs - Controls-specific tags

This provides a solid foundation for MAUI observability that teams can extend for their specific monitoring needs while maintaining excellent performance characteristics.

Issues Fixed

Fixes #28091

@Copilot Copilot AI review requested due to automatic review settings August 7, 2025 00:55
@mattleibow mattleibow requested a review from a team as a code owner August 7, 2025 00:55
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR introduces a comprehensive diagnostics and metrics tracking system for .NET MAUI applications, focusing on layout performance monitoring with an extensible architecture for future observability needs. The implementation provides zero-allocation instrumentation for layout operations with proper feature switches for AOT/trimming scenarios.

Key changes include:

  • Core diagnostics infrastructure with ActivitySource and Metrics tracking for "Microsoft.Maui"
  • Layout performance instrumentation for IView.Measure() and IView.Arrange() operations
  • Extensible plugin system through IDiagnosticTagger, IDiagnosticMetrics, and IDiagnosticInstrumentation interfaces

Reviewed Changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
RuntimeFeature.cs Adds feature switch for System.Diagnostics.Metrics.Meter support
MauiAppBuilder.cs Integrates diagnostics configuration into MAUI app startup
MauiDiagnostics.cs Core diagnostics hub managing ActivitySource, Meter, and plugin coordination
MauiDiagnosticsExtensions.cs Extension methods for diagnostics registration and IView integration
ViewDiagnosticTagger.cs Base tagger adding element.type tags for all IView instances
ControlsViewDiagnosticTagger.cs Controls-specific tagger adding Element and VisualElement property tags
Layout*Instrumentation.cs Zero-allocation struct instrumentation for measure/arrange operations
LayoutDiagnosticMetrics.cs Layout-specific metrics definitions with counters and histograms
DiagnosticInstrumentation.cs Factory for creating layout instrumentation instances
IDiagnostic*.cs Plugin interfaces for taggers, metrics, and instrumentation
VisualElement.cs Adds instrumentation calls to IView.Measure() and IView.Arrange() implementations
AppHostBuilderExtensions.cs Registers Controls-specific diagnostics during setup
MauiControlsDiagnosticsExtensions.cs Controls layer diagnostics configuration
MauiProgram.cs Sample implementation showing ActivityListener and MeterListener usage

@mattleibow

This comment was marked as outdated.

@mattleibow mattleibow moved this from Todo to Ready To Review in MAUI SDK Ongoing Aug 7, 2025
PureWeen
PureWeen previously approved these changes Aug 7, 2025
@github-project-automation github-project-automation bot moved this from Ready To Review to Approved in MAUI SDK Ongoing Aug 7, 2025
jonathanpeppers added a commit to dotnet/android that referenced this pull request Aug 7, 2025
Context: https://github.com/jonathanpeppers/MauiAspireWithTelemetry/tree/peppers
Context: dotnet/maui#31058

We are in the process of adding `Metrics` to .NET MAUI to enable
`dotnet-counters` such as:

    > dotnet-counters monitor --dsrouter android --counters Microsoft.Maui
    Press p to pause, r to resume, q to quit.
    Status: Running

    Name                                      Current Value
    [Microsoft.Maui]
        maui.layout.arrange_count ({times})               6
        maui.layout.arrange_duration (ns)
            Percentile
            50                                      518,656
            95                                    2,367,488
            99                                    2,367,488
        maui.layout.measure_count ({times})              11
        maui.layout.measure_duration (ns)
            Percentile
            50                                      389,632
            95                                   28,475,392
            99                                   28,475,392

For this to work, you'd unfortunately need to build the app with:

    dotnet build -c Release -p:EnableDiagnostics=true -p:MetricsSupport=true -p:EventSourceSupport=true

That's way too many properties to mess with!

So, we should make the defaults:

* For `Release` mode, `$(Optimize)` is `true`.

* Only if `$(AndroidEnableProfiler)` and `$(EnableDiagnostics)` are
  not `true`, then:

  * `$(MetricsSupport)` is `false`.

  * `$(EventSourceSupport)` is `false`.

I also refactored other properties that default to `false` in Release
mode by checking the `$(Optimize)` property.
@PureWeen PureWeen added the p/0 Work that we can't release without label Aug 7, 2025
@PureWeen
Copy link
Member

PureWeen commented Aug 8, 2025

  • failing tests unrelated

jonathanpeppers added a commit to dotnet/android that referenced this pull request Aug 8, 2025
Context: https://github.com/jonathanpeppers/MauiAspireWithTelemetry/tree/peppers
Context: dotnet/maui#31058

We are in the process of adding `Metrics` to .NET MAUI to enable
`dotnet-counters` such as:

    > dotnet-counters monitor --dsrouter android --counters Microsoft.Maui
    Press p to pause, r to resume, q to quit.
    Status: Running

    Name                                      Current Value
    [Microsoft.Maui]
        maui.layout.arrange_count ({times})               6
        maui.layout.arrange_duration (ns)
            Percentile
            50                                      518,656
            95                                    2,367,488
            99                                    2,367,488
        maui.layout.measure_count ({times})              11
        maui.layout.measure_duration (ns)
            Percentile
            50                                      389,632
            95                                   28,475,392
            99                                   28,475,392

For this to work, you'd unfortunately need to build the app with:

    dotnet build -c Release -p:EnableDiagnostics=true -p:MetricsSupport=true -p:EventSourceSupport=true

That's way too many properties to mess with!

So, we should make the defaults:

* For `Release` mode, `$(Optimize)` is `true`.

* Only if `$(AndroidEnableProfiler)` and `$(EnableDiagnostics)` are
  not `true`, then:

  * `$(MetricsSupport)` is `false`.

  * `$(EventSourceSupport)` is `false`.

I also refactored other properties that default to `false` in Release
mode by checking the `$(Optimize)` property.
@PureWeen PureWeen merged commit 6538806 into net10.0 Aug 8, 2025
126 of 129 checks passed
@PureWeen PureWeen deleted the dev/metrics_v3 branch August 8, 2025 20:30
@github-project-automation github-project-automation bot moved this from Approved to Done in MAUI SDK Ongoing Aug 8, 2025
jonathanpeppers added a commit to dotnet/android that referenced this pull request Aug 8, 2025
Context: https://github.com/jonathanpeppers/MauiAspireWithTelemetry/tree/peppers
Context: dotnet/maui#31058

We are in the process of adding `Metrics` to .NET MAUI to enable
`dotnet-counters` such as:

    > dotnet-counters monitor --dsrouter android --counters Microsoft.Maui
    Press p to pause, r to resume, q to quit.
    Status: Running

    Name                                      Current Value
    [Microsoft.Maui]
        maui.layout.arrange_count ({times})               6
        maui.layout.arrange_duration (ns)
            Percentile
            50                                      518,656
            95                                    2,367,488
            99                                    2,367,488
        maui.layout.measure_count ({times})              11
        maui.layout.measure_duration (ns)
            Percentile
            50                                      389,632
            95                                   28,475,392
            99                                   28,475,392

For this to work, you'd unfortunately need to build the app with:

    dotnet build -c Release -p:EnableDiagnostics=true -p:MetricsSupport=true -p:EventSourceSupport=true

That's way too many properties to mess with!

So, we should make the defaults:

* For `Release` mode, `$(Optimize)` is `true`.

* Only if `$(AndroidEnableProfiler)` and `$(EnableDiagnostics)` are
  not `true`, then:

  * `$(MetricsSupport)` is `false`.

  * `$(EventSourceSupport)` is `false`.

I also refactored other properties that default to `false` in Release
mode by checking the `$(Optimize)` property.
rolfbjarne added a commit to dotnet/macios that referenced this pull request Aug 11, 2025
…icsSupport'.

Context: dotnet/maui#31058
Context: dotnet/android#10388

We are in the process of adding `Metrics` to .NET MAUI to enable `dotnet-counters` such as:

    > dotnet-counters monitor --dsrouter ios --counters Microsoft.Maui
    Press p to pause, r to resume, q to quit.
    Status: Running

    Name                                      Current Value
    [Microsoft.Maui]
        maui.layout.arrange_count ({times})               6
        maui.layout.arrange_duration (ns)
            Percentile
            50                                      518,656
            95                                    2,367,488
            99                                    2,367,488
        maui.layout.measure_count ({times})              11
        maui.layout.measure_duration (ns)
            Percentile
            50                                      389,632
            95                                   28,475,392
            99                                   28,475,392

For this to work, you'd unfortunately need to build the app with:

    dotnet build -c Release -p:EnableDiagnostics=true -p:MetricsSupport=true -p:EventSourceSupport=true

That's way too many properties to mess with!

So, we should make the defaults:

* For `Release` mode, `$(Optimize)` is `true`.

* Only if `$(EnableDiagnostics)` is not `true`, then:

  * `$(MetricsSupport)` is `false`.

  * `$(EventSourceSupport)` is `false`.
rolfbjarne added a commit to dotnet/macios that referenced this pull request Aug 12, 2025
…icsSupport'. (#23543)

Context: dotnet/maui#31058
Context: dotnet/android#10388

We are in the process of adding `Metrics` to .NET MAUI to enable `dotnet-counters` such as:

    > dotnet-counters monitor --dsrouter ios --counters Microsoft.Maui
    Press p to pause, r to resume, q to quit.
    Status: Running

    Name                                      Current Value
    [Microsoft.Maui]
        maui.layout.arrange_count ({times})               6
        maui.layout.arrange_duration (ns)
            Percentile
            50                                      518,656
            95                                    2,367,488
            99                                    2,367,488
        maui.layout.measure_count ({times})              11
        maui.layout.measure_duration (ns)
            Percentile
            50                                      389,632
            95                                   28,475,392
            99                                   28,475,392

For this to work, you'd unfortunately need to build the app with:

    dotnet build -c Release -p:EnableDiagnostics=true -p:MetricsSupport=true -p:EventSourceSupport=true

That's way too many properties to mess with!

So, we should make the defaults:

* For `Release` mode, `$(Optimize)` is `true`.

* Only if `$(EnableDiagnostics)` is not `true`, then:

  * `$(MetricsSupport)` is `false`.

  * `$(EventSourceSupport)` is `false`.
@github-actions github-actions bot locked and limited conversation to collaborators Sep 8, 2025
@PureWeen PureWeen added the area-architecture Issues with code structure, SDK structure, implementation details label Sep 9, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-architecture Issues with code structure, SDK structure, implementation details p/0 Work that we can't release without

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants