Skip to content
33 changes: 33 additions & 0 deletions src/Controls/samples/Controls.Sample/MauiProgram.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Linq;
using System.Runtime.CompilerServices;
using Maui.Controls.Sample.Controls;
using Maui.Controls.Sample.Pages;
using Maui.Controls.Sample.Services;
Expand Down Expand Up @@ -69,6 +72,36 @@ public static MauiApp CreateMauiApp()
#endif
}

// Diagnostics
{
appBuilder.Services.AddMetrics();

ActivitySource.AddActivityListener(new ActivityListener
{
ShouldListenTo = source => source.Name is "Microsoft.Maui",
Sample = (ref ActivityCreationOptions<ActivityContext> options) => ActivitySamplingResult.AllDataAndRecorded,
ActivityStarted = activity => Console.WriteLine("Started: {0,-15} {1,-60} [{2}]", activity.OperationName, activity.Id, GetTags(activity.TagObjects)),
ActivityStopped = activity => Console.WriteLine("Stopped: {0,-15} {1,-60} {2,-15} [{3}]", activity.OperationName, activity.Id, activity.Duration, GetTags(activity.TagObjects)),
});

MeterListener meterListener = new();
meterListener.InstrumentPublished = (instrument, listener) =>
{
if (instrument.Meter.Name is "Microsoft.Maui")
{
listener.EnableMeasurementEvents(instrument);
}
};
meterListener.SetMeasurementEventCallback<int>((instrument, measurement, tags, state) =>
{
Console.WriteLine($"{instrument.Name} recorded measurement {measurement} [{GetTags(tags.ToArray())}]");
});
meterListener.Start();

static string GetTags(IEnumerable<KeyValuePair<string, object?>> tags) =>
string.Join(", ", tags.Where(t => t.Value is not null).Select(t => t.Key));
}

if (UseMauiGraphicsSkia)
{
/*
Expand Down
24 changes: 24 additions & 0 deletions src/Controls/src/Core/Diagnostics/ControlsViewDiagnosticTagger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Diagnostics;
using Microsoft.Maui.Diagnostics;

namespace Microsoft.Maui.Controls.Diagnostics;

internal class ControlsViewDiagnosticTagger : IDiagnosticTagger
{
public void AddTags(object? source, ref TagList tagList)
{
if (source is Element element)
{
tagList.Add("element.id", element.Id);
tagList.Add("element.automation_id", element.AutomationId);
tagList.Add("element.class_id", element.ClassId);
tagList.Add("element.style_id", element.StyleId);
}

if (source is VisualElement ve)
{
tagList.Add("element.class", ve.@class);
tagList.Add("element.frame", ve.Frame);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Maui.Diagnostics;
using Microsoft.Maui.Hosting;

namespace Microsoft.Maui.Controls.Diagnostics;

internal static class MauiControlsDiagnosticsExtensions
{
public static MauiAppBuilder ConfigureMauiControlsDiagnostics(this MauiAppBuilder builder)
{
if (!RuntimeFeature.IsMeterSupported)
{
return builder;
}

builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IDiagnosticTagger, ControlsViewDiagnosticTagger>(_ => new ControlsViewDiagnosticTagger()));

return builder;
}
}
3 changes: 3 additions & 0 deletions src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Maui.Controls.Diagnostics;
using Microsoft.Maui.Controls.Handlers;
using Microsoft.Maui.Controls.Handlers.Items;
using Microsoft.Maui.Controls.Shapes;
Expand Down Expand Up @@ -215,6 +216,8 @@ static MauiAppBuilder SetupDefaults(this MauiAppBuilder builder)
builder.Services.AddScoped<IHybridWebViewTaskManager>(_ => new HybridWebViewTaskManager());
}

builder.ConfigureMauiControlsDiagnostics();

#if WINDOWS
builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IMauiInitializeService, MauiControlsInitializer>());
#endif
Expand Down
4 changes: 3 additions & 1 deletion src/Controls/src/Core/VisualElement/VisualElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
using System.Globalization;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Controls.Shapes;

using Microsoft.Maui.Diagnostics;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Layouts;
using Geometry = Microsoft.Maui.Controls.Shapes.Geometry;
Expand Down Expand Up @@ -1893,6 +1893,7 @@ public void Arrange(Rect bounds)
/// <inheritdoc/>
Size IView.Arrange(Rect bounds)
{
using var _ = DiagnosticInstrumentation.StartLayoutArrange(this);
return ArrangeOverride(bounds);
}

Expand Down Expand Up @@ -1947,6 +1948,7 @@ void IView.InvalidateArrange()
/// <inheritdoc/>
Size IView.Measure(double widthConstraint, double heightConstraint)
{
using var _ = DiagnosticInstrumentation.StartLayoutMeasure(this);
DesiredSize = MeasureOverride(widthConstraint, heightConstraint);
return DesiredSize;
}
Expand Down
28 changes: 28 additions & 0 deletions src/Core/src/Diagnostics/IDiagnosticInstrumentation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Diagnostics;

namespace Microsoft.Maui.Diagnostics;

/// <summary>
/// Defines a contract for recording diagnostic events for instrumentation purposes.
/// </summary>
/// <remarks>
/// Implementations of this interface enable the collection and tracking of diagnostic information,
/// typically for logging, monitoring, or telemetry scenarios.
///
/// Any activities should be started automatically in the constructor of the implementing class. All
/// started activities should be stopped and disposed of when the implementing class is disposed.
/// </remarks>
internal interface IDiagnosticInstrumentation : IDisposable
{
/// <summary>
/// Called when the instrumentation is stopped.
/// </summary>
/// <remarks>
/// This method is called when the instrumentation is stopped, allowing for any final
/// metrics to be recorded or cleanup to be performed.
/// </remarks>
/// <param name="diag"></param>
/// <param name="tagList"></param>
void Stopped(MauiDiagnostics diag, in TagList tagList);
}
15 changes: 15 additions & 0 deletions src/Core/src/Diagnostics/IDiagnosticMetrics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Diagnostics.Metrics;

namespace Microsoft.Maui.Diagnostics;

/// <summary>
/// Defines a contract for creating diagnostic metrics using a specified <see cref="System.Diagnostics.Metrics.Meter"/>.
/// </summary>
internal interface IDiagnosticMetrics
{
/// <summary>
/// Called when the <see cref="System.Diagnostics.Metrics.Meter"/> is created to initialize the metrics.
/// </summary>
/// <param name="meter">The <see cref="System.Diagnostics.Metrics.Meter"/> instance to use for creating metrics.</param>
void Create(Meter meter);
}
16 changes: 16 additions & 0 deletions src/Core/src/Diagnostics/IDiagnosticTagger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Diagnostics;

namespace Microsoft.Maui.Diagnostics;

/// <summary>
/// Defines a contract for adding diagnostic tags to a <see cref="TagList"/> based on a specific object.
/// </summary>
internal interface IDiagnosticTagger
{
/// <summary>
/// Called to add diagnostic tags to a <see cref="TagList"/> based on the provided source object.
/// </summary>
/// <param name="source">The object to use to generate tags.</param>
/// <param name="tagList">The <see cref="TagList"/> to add tags to.</param>
void AddTags(object? source, ref TagList tagList);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Microsoft.Maui.Diagnostics;

internal static class DiagnosticInstrumentation
{
public static LayoutMeasureInstrumentation? StartLayoutMeasure(IView view) =>
RuntimeFeature.IsMeterSupported
? new LayoutMeasureInstrumentation(view)
: null;

public static LayoutArrangeInstrumentation? StartLayoutArrange(IView view) =>
RuntimeFeature.IsMeterSupported
? new LayoutArrangeInstrumentation(view)
: null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Diagnostics;

namespace Microsoft.Maui.Diagnostics;

readonly struct LayoutArrangeInstrumentation(IView view) : IDiagnosticInstrumentation
{
readonly Activity? _activity = view.StartActivity("Arrange");

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

public void Stopped(MauiDiagnostics diag, in TagList tagList) =>
diag.GetMetrics<LayoutDiagnosticMetrics>()?.RecordArrange(_activity?.Duration, in tagList);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System;
using System.Diagnostics;
using System.Diagnostics.Metrics;

namespace Microsoft.Maui.Diagnostics;

internal class LayoutDiagnosticMetrics : IDiagnosticMetrics
{
public Counter<int>? MeasureCounter { get; private set; }

public Histogram<int>? MeasureHistogram { get; private set; }

internal Counter<int>? ArrangeCounter { get; private set; }

internal Histogram<int>? ArrangeHistogram { get; private set; }

public void Create(Meter meter)
{
MeasureCounter = meter.CreateCounter<int>("maui.layout.measure_count", "{times}", "The number of times a measure happened.");
MeasureHistogram = meter.CreateHistogram<int>("maui.layout.measure_duration", "ns");

ArrangeCounter = meter.CreateCounter<int>("maui.layout.arrange_count", "{times}", "The number of times an arrange happened.");
ArrangeHistogram = meter.CreateHistogram<int>("maui.layout.arrange_duration", "ns");
}

public void RecordMeasure(TimeSpan? duration, in TagList tagList)
{
MeasureCounter?.Add(1, tagList);

if (duration is not null)
{
#if NET9_0_OR_GREATER
MeasureHistogram?.Record((int)duration.Value.TotalNanoseconds, tagList);
#else
MeasureHistogram?.Record((int)(duration.Value.TotalMilliseconds * 1_000_000), tagList);
#endif
}
}

public void RecordArrange(TimeSpan? duration, in TagList tagList)
{
ArrangeCounter?.Add(1, tagList);

if (duration is not null)
{
#if NET9_0_OR_GREATER
ArrangeHistogram?.Record((int)duration.Value.TotalNanoseconds, tagList);
#else
ArrangeHistogram?.Record((int)(duration.Value.TotalMilliseconds * 1_000_000), tagList);
#endif
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Diagnostics;

namespace Microsoft.Maui.Diagnostics;

readonly struct LayoutMeasureInstrumentation(IView view) : IDiagnosticInstrumentation
{
readonly Activity? _activity = view.StartActivity("Measure");

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

public void Stopped(MauiDiagnostics diag, in TagList tagList) =>
diag.GetMetrics<LayoutDiagnosticMetrics>()?.RecordMeasure(_activity?.Duration, in tagList);
}
60 changes: 60 additions & 0 deletions src/Core/src/Diagnostics/MauiDiagnostics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Metrics;

namespace Microsoft.Maui.Diagnostics;

internal class MauiDiagnostics
{
const string DiagnosticsNamespace = "Microsoft.Maui";
const string DiagnosticsVersion = "1.0.0";

readonly IDiagnosticTagger[] _taggers;
readonly IDiagnosticMetrics[] _metrics;
readonly Dictionary<Type, IDiagnosticMetrics> _initializedMetrics = new();

public MauiDiagnostics(IEnumerable<IDiagnosticMetrics> metrics, IEnumerable<IDiagnosticTagger> taggers, IMeterFactory? meterFactory = null)
{
_taggers = [.. taggers];
_metrics = [.. metrics];

ActivitySource = new ActivitySource(DiagnosticsNamespace, DiagnosticsVersion);

Meter = meterFactory?.Create(DiagnosticsNamespace, DiagnosticsVersion);

if (Meter is not null)
{
foreach (var metric in _metrics)
{
metric.Create(Meter);
_initializedMetrics[metric.GetType()] = metric;
}
}
}

internal ActivitySource ActivitySource { get; }

internal Meter? Meter { get; }

internal void GetTags(object source, out TagList tagList)
{
tagList = new TagList();

foreach (var tagger in _taggers)
{
tagger.AddTags(source, ref tagList);
}
}

internal T? GetMetrics<T>()
where T : IDiagnosticMetrics
{
if (!_initializedMetrics.TryGetValue(typeof(T), out var metrics))
{
return default;
}

return (T)metrics;
}
}
Loading
Loading