Skip to content

Blazor - rendering metrics #61516

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

Merged
merged 4 commits into from
Apr 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<Compile Include="$(SharedSourceRoot)Debugger\DictionaryItemDebugView.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)Debugger\DictionaryDebugView.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)UrlDecoder\UrlDecoder.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)Metrics\MetricsConstants.cs" LinkBase="Shared" />
</ItemGroup>

<Import Project="Microsoft.AspNetCore.Components.Routing.targets" />
Expand Down
9 changes: 9 additions & 0 deletions src/Components/Components/src/RenderTree/Renderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Metrics;
using System.Linq;
using Microsoft.AspNetCore.Components.HotReload;
using Microsoft.AspNetCore.Components.Reflection;
Expand Down Expand Up @@ -33,6 +34,7 @@ public abstract partial class Renderer : IDisposable, IAsyncDisposable
private readonly Dictionary<ulong, ulong> _eventHandlerIdReplacements = new Dictionary<ulong, ulong>();
private readonly ILogger _logger;
private readonly ComponentFactory _componentFactory;
private readonly RenderingMetrics? _renderingMetrics;
private Dictionary<int, ParameterView>? _rootComponentsLatestParameters;
private Task? _ongoingQuiescenceTask;

Expand Down Expand Up @@ -90,6 +92,10 @@ public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory,
_logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Components.RenderTree.Renderer");
_componentFactory = new ComponentFactory(componentActivator, this);

// TODO register RenderingMetrics as singleton in DI
Copy link
Member Author

Choose a reason for hiding this comment

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

It would be good to register RenderingMetrics as singleton. But I think it should be done in one of the "Extensions" helpers and I'm not sure which. This could be done in next PR when @javiercn is back.

Copy link
Member

Choose a reason for hiding this comment

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

var meterFactory = serviceProvider.GetService<IMeterFactory>();
_renderingMetrics = meterFactory != null ? new RenderingMetrics(meterFactory) : null;

ServiceProviderCascadingValueSuppliers = serviceProvider.GetService<ICascadingValueSupplier>() is null
? Array.Empty<ICascadingValueSupplier>()
: serviceProvider.GetServices<ICascadingValueSupplier>().ToArray();
Expand Down Expand Up @@ -926,12 +932,15 @@ private void RenderInExistingBatch(RenderQueueEntry renderQueueEntry)
{
var componentState = renderQueueEntry.ComponentState;
Log.RenderingComponent(_logger, componentState);
var startTime = (_renderingMetrics != null && _renderingMetrics.IsDurationEnabled()) ? Stopwatch.GetTimestamp() : 0;
_renderingMetrics?.RenderStart(componentState.Component.GetType().FullName);
componentState.RenderIntoBatch(_batchBuilder, renderQueueEntry.RenderFragment, out var renderFragmentException);
if (renderFragmentException != null)
{
// If this returns, the error was handled by an error boundary. Otherwise it throws.
HandleExceptionViaErrorBoundary(renderFragmentException, componentState);
}
_renderingMetrics?.RenderEnd(componentState.Component.GetType().FullName, renderFragmentException, startTime, Stopwatch.GetTimestamp());

// Process disposal queue now in case it causes further component renders to be enqueued
ProcessDisposalQueueInExistingBatch();
Expand Down
106 changes: 106 additions & 0 deletions src/Components/Components/src/Rendering/RenderingMetrics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Diagnostics.Metrics;
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Components.Rendering;

internal sealed class RenderingMetrics : IDisposable
{
public const string MeterName = "Microsoft.AspNetCore.Components.Rendering";

private readonly Meter _meter;
private readonly Counter<long> _renderTotalCounter;
private readonly UpDownCounter<long> _renderActiveCounter;
private readonly Histogram<double> _renderDuration;

public RenderingMetrics(IMeterFactory meterFactory)
{
Debug.Assert(meterFactory != null);

_meter = meterFactory.Create(MeterName);

_renderTotalCounter = _meter.CreateCounter<long>(
"aspnetcore.components.rendering.count",
unit: "{renders}",
description: "Number of component renders performed.");

_renderActiveCounter = _meter.CreateUpDownCounter<long>(
"aspnetcore.components.rendering.active_renders",
unit: "{renders}",
description: "Number of component renders performed.");

_renderDuration = _meter.CreateHistogram<double>(
"aspnetcore.components.rendering.duration",
unit: "ms",
description: "Duration of component rendering operations per component.",
advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
}

public void RenderStart(string componentType)
{
var tags = new TagList();
tags = InitializeRequestTags(componentType, tags);

if (_renderActiveCounter.Enabled)
{
_renderActiveCounter.Add(1, tags);
}
if (_renderTotalCounter.Enabled)
{
_renderTotalCounter.Add(1, tags);
}
}

public void RenderEnd(string componentType, Exception? exception, long startTimestamp, long currentTimestamp)
{
// Tags must match request start.
var tags = new TagList();
tags = InitializeRequestTags(componentType, tags);

if (_renderActiveCounter.Enabled)
{
_renderActiveCounter.Add(-1, tags);
}

if (_renderDuration.Enabled)
{
if (exception != null)
{
TryAddTag(ref tags, "error.type", exception.GetType().FullName);
}

var duration = Stopwatch.GetElapsedTime(startTimestamp, currentTimestamp);
_renderDuration.Record(duration.TotalMilliseconds, tags);
}
}

private static TagList InitializeRequestTags(string componentType, TagList tags)
{
tags.Add("component.type", componentType);
return tags;
}

public bool IsDurationEnabled() => _renderDuration.Enabled;

public void Dispose()
{
_meter.Dispose();
}

private static bool TryAddTag(ref TagList tags, string name, object? value)
{
for (var i = 0; i < tags.Count; i++)
{
if (tags[i].Key == name)
{
return false;
}
}

tags.Add(new KeyValuePair<string, object?>(name, value));
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Components" />
<Reference Include="Microsoft.Extensions.DependencyInjection" />
<Reference Include="Microsoft.Extensions.Diagnostics.Testing" />
</ItemGroup>

<ItemGroup>
<Compile Include="$(SharedSourceRoot)Metrics\TestMeterFactory.cs" LinkBase="shared" />
<Compile Include="$(ComponentsSharedSourceRoot)test\**\*.cs" LinkBase="Helpers" />
</ItemGroup>

Expand Down
Loading
Loading