Skip to content

Commit 183c128

Browse files
authored
Blazor - rendering metrics (#61516)
1 parent 6842853 commit 183c128

14 files changed

+706
-3
lines changed

src/Components/Components/src/Microsoft.AspNetCore.Components.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<Compile Include="$(SharedSourceRoot)Debugger\DictionaryItemDebugView.cs" LinkBase="Shared" />
2424
<Compile Include="$(SharedSourceRoot)Debugger\DictionaryDebugView.cs" LinkBase="Shared" />
2525
<Compile Include="$(SharedSourceRoot)UrlDecoder\UrlDecoder.cs" LinkBase="Shared" />
26+
<Compile Include="$(SharedSourceRoot)Metrics\MetricsConstants.cs" LinkBase="Shared" />
2627
</ItemGroup>
2728

2829
<Import Project="Microsoft.AspNetCore.Components.Routing.targets" />

src/Components/Components/src/RenderTree/Renderer.cs

+9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
using System.Diagnostics;
77
using System.Diagnostics.CodeAnalysis;
8+
using System.Diagnostics.Metrics;
89
using System.Linq;
910
using Microsoft.AspNetCore.Components.HotReload;
1011
using Microsoft.AspNetCore.Components.Reflection;
@@ -33,6 +34,7 @@ public abstract partial class Renderer : IDisposable, IAsyncDisposable
3334
private readonly Dictionary<ulong, ulong> _eventHandlerIdReplacements = new Dictionary<ulong, ulong>();
3435
private readonly ILogger _logger;
3536
private readonly ComponentFactory _componentFactory;
37+
private readonly RenderingMetrics? _renderingMetrics;
3638
private Dictionary<int, ParameterView>? _rootComponentsLatestParameters;
3739
private Task? _ongoingQuiescenceTask;
3840

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

95+
// TODO register RenderingMetrics as singleton in DI
96+
var meterFactory = serviceProvider.GetService<IMeterFactory>();
97+
_renderingMetrics = meterFactory != null ? new RenderingMetrics(meterFactory) : null;
98+
9399
ServiceProviderCascadingValueSuppliers = serviceProvider.GetService<ICascadingValueSupplier>() is null
94100
? Array.Empty<ICascadingValueSupplier>()
95101
: serviceProvider.GetServices<ICascadingValueSupplier>().ToArray();
@@ -926,12 +932,15 @@ private void RenderInExistingBatch(RenderQueueEntry renderQueueEntry)
926932
{
927933
var componentState = renderQueueEntry.ComponentState;
928934
Log.RenderingComponent(_logger, componentState);
935+
var startTime = (_renderingMetrics != null && _renderingMetrics.IsDurationEnabled()) ? Stopwatch.GetTimestamp() : 0;
936+
_renderingMetrics?.RenderStart(componentState.Component.GetType().FullName);
929937
componentState.RenderIntoBatch(_batchBuilder, renderQueueEntry.RenderFragment, out var renderFragmentException);
930938
if (renderFragmentException != null)
931939
{
932940
// If this returns, the error was handled by an error boundary. Otherwise it throws.
933941
HandleExceptionViaErrorBoundary(renderFragmentException, componentState);
934942
}
943+
_renderingMetrics?.RenderEnd(componentState.Component.GetType().FullName, renderFragmentException, startTime, Stopwatch.GetTimestamp());
935944

936945
// Process disposal queue now in case it causes further component renders to be enqueued
937946
ProcessDisposalQueueInExistingBatch();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
using System.Diagnostics.Metrics;
6+
using Microsoft.AspNetCore.Http;
7+
8+
namespace Microsoft.AspNetCore.Components.Rendering;
9+
10+
internal sealed class RenderingMetrics : IDisposable
11+
{
12+
public const string MeterName = "Microsoft.AspNetCore.Components.Rendering";
13+
14+
private readonly Meter _meter;
15+
private readonly Counter<long> _renderTotalCounter;
16+
private readonly UpDownCounter<long> _renderActiveCounter;
17+
private readonly Histogram<double> _renderDuration;
18+
19+
public RenderingMetrics(IMeterFactory meterFactory)
20+
{
21+
Debug.Assert(meterFactory != null);
22+
23+
_meter = meterFactory.Create(MeterName);
24+
25+
_renderTotalCounter = _meter.CreateCounter<long>(
26+
"aspnetcore.components.rendering.count",
27+
unit: "{renders}",
28+
description: "Number of component renders performed.");
29+
30+
_renderActiveCounter = _meter.CreateUpDownCounter<long>(
31+
"aspnetcore.components.rendering.active_renders",
32+
unit: "{renders}",
33+
description: "Number of component renders performed.");
34+
35+
_renderDuration = _meter.CreateHistogram<double>(
36+
"aspnetcore.components.rendering.duration",
37+
unit: "ms",
38+
description: "Duration of component rendering operations per component.",
39+
advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
40+
}
41+
42+
public void RenderStart(string componentType)
43+
{
44+
var tags = new TagList();
45+
tags = InitializeRequestTags(componentType, tags);
46+
47+
if (_renderActiveCounter.Enabled)
48+
{
49+
_renderActiveCounter.Add(1, tags);
50+
}
51+
if (_renderTotalCounter.Enabled)
52+
{
53+
_renderTotalCounter.Add(1, tags);
54+
}
55+
}
56+
57+
public void RenderEnd(string componentType, Exception? exception, long startTimestamp, long currentTimestamp)
58+
{
59+
// Tags must match request start.
60+
var tags = new TagList();
61+
tags = InitializeRequestTags(componentType, tags);
62+
63+
if (_renderActiveCounter.Enabled)
64+
{
65+
_renderActiveCounter.Add(-1, tags);
66+
}
67+
68+
if (_renderDuration.Enabled)
69+
{
70+
if (exception != null)
71+
{
72+
TryAddTag(ref tags, "error.type", exception.GetType().FullName);
73+
}
74+
75+
var duration = Stopwatch.GetElapsedTime(startTimestamp, currentTimestamp);
76+
_renderDuration.Record(duration.TotalMilliseconds, tags);
77+
}
78+
}
79+
80+
private static TagList InitializeRequestTags(string componentType, TagList tags)
81+
{
82+
tags.Add("component.type", componentType);
83+
return tags;
84+
}
85+
86+
public bool IsDurationEnabled() => _renderDuration.Enabled;
87+
88+
public void Dispose()
89+
{
90+
_meter.Dispose();
91+
}
92+
93+
private static bool TryAddTag(ref TagList tags, string name, object? value)
94+
{
95+
for (var i = 0; i < tags.Count; i++)
96+
{
97+
if (tags[i].Key == name)
98+
{
99+
return false;
100+
}
101+
}
102+
103+
tags.Add(new KeyValuePair<string, object?>(name, value));
104+
return true;
105+
}
106+
}

src/Components/Components/test/Microsoft.AspNetCore.Components.Tests.csproj

+2
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
<ItemGroup>
99
<Reference Include="Microsoft.AspNetCore.Components" />
1010
<Reference Include="Microsoft.Extensions.DependencyInjection" />
11+
<Reference Include="Microsoft.Extensions.Diagnostics.Testing" />
1112
</ItemGroup>
1213

1314
<ItemGroup>
15+
<Compile Include="$(SharedSourceRoot)Metrics\TestMeterFactory.cs" LinkBase="shared" />
1416
<Compile Include="$(ComponentsSharedSourceRoot)test\**\*.cs" LinkBase="Helpers" />
1517
</ItemGroup>
1618

0 commit comments

Comments
 (0)