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 2 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.

Copy link
Member

Choose a reason for hiding this comment

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

This is going to be "painful" because it lives in the Microsoft.AspNetCore.Components. Typically, what we do in these cases is expose a helper method that is then called by the different hosts to register it on DI.

There's no way around not introducing a bit of public API for this. If I were to do something, I would go with creating an additional extension method here (look at AddValueProvider for a sample pattern) and have that called in the places that @maraf pointed out.

Later on, we can decide if we prefer to introduce a single extension method that we can pile up things in the future. The pattern MVC follows has AddMvcCore for this reason, we could have something like AddComponentsCore that is meant to be called out by the individual hosts, but for now let's just stick with what we've been doing so far (that would be my recommendation).

var meterFactory = serviceProvider.GetService<IMeterFactory>();
_renderingMetrics = meterFactory != null ? new RenderingMetrics(meterFactory) : null;
Comment on lines +96 to +97
Copy link
Member

Choose a reason for hiding this comment

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

Is there a case where IMeterFactory is not ever on 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.

In unit tests I think. Also I can imagine that somebody would like to disable it in non-cloud environments.

Copy link
Member

Choose a reason for hiding this comment

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

For the unit tests we would simply update them. For production workloads our hosts should just call AddMetrics and rely on it being there. I don't think this dependency should be optional.

Copy link
Member Author

Choose a reason for hiding this comment

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

What about WASM, do you think it should always have metrics in DI ? That would make impossible to disable/trim metrics for Blazor

Copy link
Member

Choose a reason for hiding this comment

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

I think that's fine, if we are concerned, we can look at the size before and after the change to understand the delta. In any case we could put it behind and app compat switch so that it gets trimmed by default if not enabled?

Copy link
Member

Choose a reason for hiding this comment

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

There is a fair bit of code involved with metrics. I think you'll want a way to toggle it on and off in WASM.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm thinking [FeatureSwitchDefinition("System.Diagnostics.Metrics.Meter.IsSupported")]


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 = ((bool)_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
104 changes: 104 additions & 0 deletions src/Components/Components/src/Rendering/RenderingMetrics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// 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)
{
_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;
}
}
6 changes: 6 additions & 0 deletions src/Components/Server/src/Circuits/CircuitFactory.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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.Metrics;

Check failure on line 4 in src/Components/Server/src/Circuits/CircuitFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: Linux ARM)

src/Components/Server/src/Circuits/CircuitFactory.cs#L4

src/Components/Server/src/Circuits/CircuitFactory.cs(4,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 4 in src/Components/Server/src/Circuits/CircuitFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: Linux Musl x64)

src/Components/Server/src/Circuits/CircuitFactory.cs#L4

src/Components/Server/src/Circuits/CircuitFactory.cs(4,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 4 in src/Components/Server/src/Circuits/CircuitFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: Linux ARM64)

src/Components/Server/src/Circuits/CircuitFactory.cs#L4

src/Components/Server/src/Circuits/CircuitFactory.cs(4,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 4 in src/Components/Server/src/Circuits/CircuitFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: macOS x64)

src/Components/Server/src/Circuits/CircuitFactory.cs#L4

src/Components/Server/src/Circuits/CircuitFactory.cs(4,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 4 in src/Components/Server/src/Circuits/CircuitFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: Linux x64)

src/Components/Server/src/Circuits/CircuitFactory.cs#L4

src/Components/Server/src/Circuits/CircuitFactory.cs(4,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 4 in src/Components/Server/src/Circuits/CircuitFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: macOS arm64)

src/Components/Server/src/Circuits/CircuitFactory.cs#L4

src/Components/Server/src/Circuits/CircuitFactory.cs(4,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 4 in src/Components/Server/src/Circuits/CircuitFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: Linux Musl ARM64)

src/Components/Server/src/Circuits/CircuitFactory.cs#L4

src/Components/Server/src/Circuits/CircuitFactory.cs(4,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 4 in src/Components/Server/src/Circuits/CircuitFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: Linux Musl ARM)

src/Components/Server/src/Circuits/CircuitFactory.cs#L4

src/Components/Server/src/Circuits/CircuitFactory.cs(4,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 4 in src/Components/Server/src/Circuits/CircuitFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-quarantined-pr (Tests: Ubuntu x64)

src/Components/Server/src/Circuits/CircuitFactory.cs#L4

src/Components/Server/src/Circuits/CircuitFactory.cs(4,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 4 in src/Components/Server/src/Circuits/CircuitFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-quarantined-pr (Tests: macOS)

src/Components/Server/src/Circuits/CircuitFactory.cs#L4

src/Components/Server/src/Circuits/CircuitFactory.cs(4,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 4 in src/Components/Server/src/Circuits/CircuitFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Test: macOS)

src/Components/Server/src/Circuits/CircuitFactory.cs#L4

src/Components/Server/src/Circuits/CircuitFactory.cs(4,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 4 in src/Components/Server/src/Circuits/CircuitFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Test: Ubuntu x64)

src/Components/Server/src/Circuits/CircuitFactory.cs#L4

src/Components/Server/src/Circuits/CircuitFactory.cs(4,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 4 in src/Components/Server/src/Circuits/CircuitFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-quarantined-pr

src/Components/Server/src/Circuits/CircuitFactory.cs#L4

src/Components/Server/src/Circuits/CircuitFactory.cs(4,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 4 in src/Components/Server/src/Circuits/CircuitFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-quarantined-pr

src/Components/Server/src/Circuits/CircuitFactory.cs#L4

src/Components/Server/src/Circuits/CircuitFactory.cs(4,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 4 in src/Components/Server/src/Circuits/CircuitFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci

src/Components/Server/src/Circuits/CircuitFactory.cs#L4

src/Components/Server/src/Circuits/CircuitFactory.cs(4,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 4 in src/Components/Server/src/Circuits/CircuitFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci

src/Components/Server/src/Circuits/CircuitFactory.cs#L4

src/Components/Server/src/Circuits/CircuitFactory.cs(4,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)
using System.Linq;
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Infrastructure;
Expand All @@ -20,18 +21,22 @@
private readonly CircuitIdFactory _circuitIdFactory;
private readonly CircuitOptions _options;
private readonly ILogger _logger;
private readonly CircuitMetrics? _circuitMetrics;

public CircuitFactory(
IServiceScopeFactory scopeFactory,
ILoggerFactory loggerFactory,
CircuitIdFactory circuitIdFactory,
CircuitMetrics? circuitMetrics,
IOptions<CircuitOptions> options)
{
_scopeFactory = scopeFactory;
_loggerFactory = loggerFactory;
_circuitIdFactory = circuitIdFactory;
_options = options.Value;
_logger = _loggerFactory.CreateLogger<CircuitFactory>();

_circuitMetrics = circuitMetrics;
}

public async ValueTask<CircuitHost> CreateCircuitHostAsync(
Expand Down Expand Up @@ -104,6 +109,7 @@
jsRuntime,
navigationManager,
circuitHandlers,
_circuitMetrics,
_loggerFactory.CreateLogger<CircuitHost>());
Log.CreatedCircuit(_logger, circuitHost);

Expand Down
11 changes: 11 additions & 0 deletions src/Components/Server/src/Circuits/CircuitHost.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// 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;

Check failure on line 5 in src/Components/Server/src/Circuits/CircuitHost.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: Linux ARM)

src/Components/Server/src/Circuits/CircuitHost.cs#L5

src/Components/Server/src/Circuits/CircuitHost.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 5 in src/Components/Server/src/Circuits/CircuitHost.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: Linux Musl x64)

src/Components/Server/src/Circuits/CircuitHost.cs#L5

src/Components/Server/src/Circuits/CircuitHost.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 5 in src/Components/Server/src/Circuits/CircuitHost.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: Linux ARM64)

src/Components/Server/src/Circuits/CircuitHost.cs#L5

src/Components/Server/src/Circuits/CircuitHost.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 5 in src/Components/Server/src/Circuits/CircuitHost.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: macOS x64)

src/Components/Server/src/Circuits/CircuitHost.cs#L5

src/Components/Server/src/Circuits/CircuitHost.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 5 in src/Components/Server/src/Circuits/CircuitHost.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: Linux x64)

src/Components/Server/src/Circuits/CircuitHost.cs#L5

src/Components/Server/src/Circuits/CircuitHost.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 5 in src/Components/Server/src/Circuits/CircuitHost.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: macOS arm64)

src/Components/Server/src/Circuits/CircuitHost.cs#L5

src/Components/Server/src/Circuits/CircuitHost.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 5 in src/Components/Server/src/Circuits/CircuitHost.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: Linux Musl ARM64)

src/Components/Server/src/Circuits/CircuitHost.cs#L5

src/Components/Server/src/Circuits/CircuitHost.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 5 in src/Components/Server/src/Circuits/CircuitHost.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: Linux Musl ARM)

src/Components/Server/src/Circuits/CircuitHost.cs#L5

src/Components/Server/src/Circuits/CircuitHost.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 5 in src/Components/Server/src/Circuits/CircuitHost.cs

View check run for this annotation

Azure Pipelines / aspnetcore-quarantined-pr (Tests: Ubuntu x64)

src/Components/Server/src/Circuits/CircuitHost.cs#L5

src/Components/Server/src/Circuits/CircuitHost.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 5 in src/Components/Server/src/Circuits/CircuitHost.cs

View check run for this annotation

Azure Pipelines / aspnetcore-quarantined-pr (Tests: macOS)

src/Components/Server/src/Circuits/CircuitHost.cs#L5

src/Components/Server/src/Circuits/CircuitHost.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 5 in src/Components/Server/src/Circuits/CircuitHost.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Test: macOS)

src/Components/Server/src/Circuits/CircuitHost.cs#L5

src/Components/Server/src/Circuits/CircuitHost.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 5 in src/Components/Server/src/Circuits/CircuitHost.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Test: Ubuntu x64)

src/Components/Server/src/Circuits/CircuitHost.cs#L5

src/Components/Server/src/Circuits/CircuitHost.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 5 in src/Components/Server/src/Circuits/CircuitHost.cs

View check run for this annotation

Azure Pipelines / aspnetcore-quarantined-pr

src/Components/Server/src/Circuits/CircuitHost.cs#L5

src/Components/Server/src/Circuits/CircuitHost.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 5 in src/Components/Server/src/Circuits/CircuitHost.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci

src/Components/Server/src/Circuits/CircuitHost.cs#L5

src/Components/Server/src/Circuits/CircuitHost.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 5 in src/Components/Server/src/Circuits/CircuitHost.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci

src/Components/Server/src/Circuits/CircuitHost.cs#L5

src/Components/Server/src/Circuits/CircuitHost.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)
using System.Globalization;
using System.Linq;
using System.Security.Claims;
Expand All @@ -23,11 +25,13 @@
private readonly CircuitOptions _options;
private readonly RemoteNavigationManager _navigationManager;
private readonly ILogger _logger;
private readonly CircuitMetrics? _circuitMetrics;
private Func<Func<Task>, Task> _dispatchInboundActivity;
private CircuitHandler[] _circuitHandlers;
private bool _initialized;
private bool _isFirstUpdate = true;
private bool _disposed;
private long _startTime;

// This event is fired when there's an unrecoverable exception coming from the circuit, and
// it need so be torn down. The registry listens to this even so that the circuit can
Expand All @@ -47,6 +51,7 @@
RemoteJSRuntime jsRuntime,
RemoteNavigationManager navigationManager,
CircuitHandler[] circuitHandlers,
CircuitMetrics circuitMetrics,
ILogger logger)
{
CircuitId = circuitId;
Expand All @@ -64,6 +69,7 @@
JSRuntime = jsRuntime ?? throw new ArgumentNullException(nameof(jsRuntime));
_navigationManager = navigationManager ?? throw new ArgumentNullException(nameof(navigationManager));
_circuitHandlers = circuitHandlers ?? throw new ArgumentNullException(nameof(circuitHandlers));
_circuitMetrics = circuitMetrics;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));

Services = scope.ServiceProvider;
Expand Down Expand Up @@ -230,6 +236,8 @@
private async Task OnCircuitOpenedAsync(CancellationToken cancellationToken)
{
Log.CircuitOpened(_logger, CircuitId);
_startTime = ((bool)_circuitMetrics?.IsDurationEnabled()) ? Stopwatch.GetTimestamp() : 0;
_circuitMetrics?.OnCircuitOpened();

Renderer.Dispatcher.AssertAccess();

Expand Down Expand Up @@ -259,6 +267,7 @@
public async Task OnConnectionUpAsync(CancellationToken cancellationToken)
{
Log.ConnectionUp(_logger, CircuitId, Client.ConnectionId);
_circuitMetrics?.OnConnectionUp();

Renderer.Dispatcher.AssertAccess();

Expand Down Expand Up @@ -288,6 +297,7 @@
public async Task OnConnectionDownAsync(CancellationToken cancellationToken)
{
Log.ConnectionDown(_logger, CircuitId, Client.ConnectionId);
_circuitMetrics?.OnConnectionDown();

Renderer.Dispatcher.AssertAccess();

Expand Down Expand Up @@ -317,6 +327,7 @@
private async Task OnCircuitDownAsync(CancellationToken cancellationToken)
{
Log.CircuitClosed(_logger, CircuitId);
_circuitMetrics?.OnCircuitDown(_startTime, Stopwatch.GetTimestamp());

List<Exception> exceptions = null;

Expand Down
108 changes: 108 additions & 0 deletions src/Components/Server/src/Circuits/CircuitMetrics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// 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.Server.Circuits;

internal sealed class CircuitMetrics : IDisposable
{
public const string MeterName = "Microsoft.AspNetCore.Components.Server.Circuits";

private readonly Meter _meter;
private readonly Counter<long> _circuitTotalCounter;
private readonly UpDownCounter<long> _circuitActiveCounter;
private readonly UpDownCounter<long> _circuitConnectedCounter;
private readonly Histogram<double> _circuitDuration;

public CircuitMetrics(IMeterFactory meterFactory)
{
_meter = meterFactory.Create(MeterName);

_circuitTotalCounter = _meter.CreateCounter<long>(
"aspnetcore.components.circuits.count",
unit: "{circuits}",
description: "Number of active circuits.");

_circuitActiveCounter = _meter.CreateUpDownCounter<long>(
"aspnetcore.components.circuits.active_circuits",
unit: "{circuits}",
description: "Number of active circuits.");

_circuitConnectedCounter = _meter.CreateUpDownCounter<long>(
"aspnetcore.components.circuits.connected_circuits",
unit: "{circuits}",
description: "Number of disconnected circuits.");

_circuitDuration = _meter.CreateHistogram<double>(
"aspnetcore.components.circuits.duration",
unit: "s",
description: "Duration of circuit.",
advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.VeryLongSecondsBucketBoundaries });
Copy link
Member

@JamesNK JamesNK Apr 21, 2025

Choose a reason for hiding this comment

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

What is wrong with the existing LongSecondsBucketBoundaries? That's already used for Kestrel connection duration and signalr connection duration.

I don't like having 3 different durations.

Copy link
Member

Choose a reason for hiding this comment

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

I think this is used to bucketize the circuit duration, which I think naturally spans longer than the SignalR/Http one. If we were to use those, doesn't that mean that most data will end up in a single bucket? Ideally the buckets should be representative of the "durations" that we expect to track, isn't it?

Copy link
Member

Choose a reason for hiding this comment

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

The biggest value in th existing buckets is 5 minutes. Do you think most circuits last longer than 5 minutes?

Copy link
Member

Choose a reason for hiding this comment

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

I think so. Circuits are associated with the "session" (a browser tab), so it's very feasible that they remain open while the user is looking at the tab. As for how long they can last, it's very app specific, but I think we want to have enough granularity to track whether sessions are very short (1-10 minutes) or last significantly longer (10 minutes, hours if the users leave the browser tab open and don't have energy saving settings)

Copy link
Member

Choose a reason for hiding this comment

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

Is there a specific problem with having a different set of numbers? Is there a recommended number of values that we should strive for?

Copy link
Member

Choose a reason for hiding this comment

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

No there isn't a specific problem. Just there is an extra choice for people to choose from, and good values for the buckets need to be decided.

Copy link
Member

Choose a reason for hiding this comment

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

When you mean people, do you mean us, or do you mean developers consuming the metrics. I assume you mean us?

Copy link
Member

Choose a reason for hiding this comment

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

I mean us.

The comment on the new buckets doesn't seem right. SignalR connection duration goes up to 300 seconds.

Copy link
Member Author

Choose a reason for hiding this comment

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

I thought that Blazor circuit could be alive for days. And since circuit keeps the state of the blazor session it could be memory hungry. So understanding the histogram of how long your users keep the tab open matters for sizing your cluster.

I thought that it could happen that 80% of your circuits live over 5 minutes but with the previous buckets you don't know if that's 6 minutes or 6 days.

The comment on the new buckets doesn't seem right. SignalR connection duration goes up to 300 seconds.

Could you please be more specific? I will fix it on next PR. Thanks.

}

public void OnCircuitOpened()
{
var tags = new TagList();

if (_circuitActiveCounter.Enabled)
{
_circuitActiveCounter.Add(1, tags);
}
if (_circuitTotalCounter.Enabled)
{
_circuitTotalCounter.Add(1, tags);
}
}

public void OnConnectionUp()
{
var tags = new TagList();

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

public void OnConnectionDown()
{
var tags = new TagList();

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

public void OnCircuitDown(long startTimestamp, long currentTimestamp)
{
// Tags must match request start.
var tags = new TagList();

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

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

if (_circuitDuration.Enabled)
{
var duration = Stopwatch.GetElapsedTime(startTimestamp, currentTimestamp);
_circuitDuration.Record(duration.TotalSeconds, tags);
}
}

public bool IsDurationEnabled() => _circuitDuration.Enabled;

public void Dispose()
{
_meter.Dispose();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Metrics;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Forms;
Expand Down Expand Up @@ -60,6 +61,12 @@ public static IServerSideBlazorBuilder AddServerSideBlazor(this IServiceCollecti
// Here we add a bunch of services that don't vary in any way based on the
// user's configuration. So even if the user has multiple independent server-side
// Components entrypoints, this lot is the same and repeated registrations are a no-op.

services.TryAddSingleton(s =>
{
var meterFactory = s.GetService<IMeterFactory>();
return meterFactory != null ? new CircuitMetrics(meterFactory) : null;
});
services.TryAddSingleton<ICircuitFactory, CircuitFactory>();
services.TryAddSingleton<ICircuitHandleRegistry, CircuitHandleRegistry>();
services.TryAddSingleton<RootTypeCache>();
Expand Down
Loading
Loading