Skip to content

Commit caf0f3f

Browse files
authored
Blazor - rendering metrics and tracing (#61609)
1 parent 60d068e commit caf0f3f

26 files changed

+1271
-444
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
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+
6+
namespace Microsoft.AspNetCore.Components;
7+
8+
/// <summary>
9+
/// This is instance scoped per renderer
10+
/// </summary>
11+
internal class ComponentsActivitySource
12+
{
13+
internal const string Name = "Microsoft.AspNetCore.Components";
14+
internal const string OnCircuitName = $"{Name}.CircuitStart";
15+
internal const string OnRouteName = $"{Name}.RouteChange";
16+
internal const string OnEventName = $"{Name}.HandleEvent";
17+
18+
private ActivityContext _httpContext;
19+
private ActivityContext _circuitContext;
20+
private string? _circuitId;
21+
private ActivityContext _routeContext;
22+
23+
private ActivitySource ActivitySource { get; } = new ActivitySource(Name);
24+
25+
public static ActivityContext CaptureHttpContext()
26+
{
27+
var parentActivity = Activity.Current;
28+
if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn" && parentActivity.Recorded)
29+
{
30+
return parentActivity.Context;
31+
}
32+
return default;
33+
}
34+
35+
public Activity? StartCircuitActivity(string circuitId, ActivityContext httpContext)
36+
{
37+
_circuitId = circuitId;
38+
39+
var activity = ActivitySource.CreateActivity(OnCircuitName, ActivityKind.Internal, parentId: null, null, null);
40+
if (activity is not null)
41+
{
42+
if (activity.IsAllDataRequested)
43+
{
44+
if (_circuitId != null)
45+
{
46+
activity.SetTag("aspnetcore.components.circuit.id", _circuitId);
47+
}
48+
if (httpContext != default)
49+
{
50+
activity.AddLink(new ActivityLink(httpContext));
51+
}
52+
}
53+
activity.DisplayName = $"Circuit {circuitId ?? ""}";
54+
activity.Start();
55+
_circuitContext = activity.Context;
56+
}
57+
return activity;
58+
}
59+
60+
public void FailCircuitActivity(Activity? activity, Exception ex)
61+
{
62+
_circuitContext = default;
63+
if (activity != null && !activity.IsStopped)
64+
{
65+
activity.SetTag("error.type", ex.GetType().FullName);
66+
activity.SetStatus(ActivityStatusCode.Error);
67+
activity.Stop();
68+
}
69+
}
70+
71+
public Activity? StartRouteActivity(string componentType, string route)
72+
{
73+
if (_httpContext == default)
74+
{
75+
_httpContext = CaptureHttpContext();
76+
}
77+
78+
var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Internal, parentId: null, null, null);
79+
if (activity is not null)
80+
{
81+
if (activity.IsAllDataRequested)
82+
{
83+
if (_circuitId != null)
84+
{
85+
activity.SetTag("aspnetcore.components.circuit.id", _circuitId);
86+
}
87+
if (componentType != null)
88+
{
89+
activity.SetTag("aspnetcore.components.type", componentType);
90+
}
91+
if (route != null)
92+
{
93+
activity.SetTag("aspnetcore.components.route", route);
94+
}
95+
if (_httpContext != default)
96+
{
97+
activity.AddLink(new ActivityLink(_httpContext));
98+
}
99+
if (_circuitContext != default)
100+
{
101+
activity.AddLink(new ActivityLink(_circuitContext));
102+
}
103+
}
104+
105+
activity.DisplayName = $"Route {route ?? "[unknown path]"} -> {componentType ?? "[unknown component]"}";
106+
activity.Start();
107+
_routeContext = activity.Context;
108+
}
109+
return activity;
110+
}
111+
112+
public Activity? StartEventActivity(string? componentType, string? methodName, string? attributeName)
113+
{
114+
var activity = ActivitySource.CreateActivity(OnEventName, ActivityKind.Internal, parentId: null, null, null);
115+
if (activity is not null)
116+
{
117+
if (activity.IsAllDataRequested)
118+
{
119+
if (_circuitId != null)
120+
{
121+
activity.SetTag("aspnetcore.components.circuit.id", _circuitId);
122+
}
123+
if (componentType != null)
124+
{
125+
activity.SetTag("aspnetcore.components.type", componentType);
126+
}
127+
if (methodName != null)
128+
{
129+
activity.SetTag("aspnetcore.components.method", methodName);
130+
}
131+
if (attributeName != null)
132+
{
133+
activity.SetTag("aspnetcore.components.attribute.name", attributeName);
134+
}
135+
if (_httpContext != default)
136+
{
137+
activity.AddLink(new ActivityLink(_httpContext));
138+
}
139+
if (_circuitContext != default)
140+
{
141+
activity.AddLink(new ActivityLink(_circuitContext));
142+
}
143+
if (_routeContext != default)
144+
{
145+
activity.AddLink(new ActivityLink(_routeContext));
146+
}
147+
}
148+
149+
activity.DisplayName = $"Event {attributeName ?? "[unknown attribute]"} -> {componentType ?? "[unknown component]"}.{methodName ?? "[unknown method]"}";
150+
activity.Start();
151+
}
152+
return activity;
153+
}
154+
155+
public static void FailEventActivity(Activity? activity, Exception ex)
156+
{
157+
if (activity != null && !activity.IsStopped)
158+
{
159+
activity.SetTag("error.type", ex.GetType().FullName);
160+
activity.SetStatus(ActivityStatusCode.Error);
161+
activity.Stop();
162+
}
163+
}
164+
165+
public static async Task CaptureEventStopAsync(Task task, Activity? activity)
166+
{
167+
try
168+
{
169+
await task;
170+
activity?.Stop();
171+
}
172+
catch (Exception ex)
173+
{
174+
FailEventActivity(activity, ex);
175+
}
176+
}
177+
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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.Components;
7+
using Microsoft.AspNetCore.Http;
8+
9+
namespace Microsoft.AspNetCore.Components;
10+
11+
internal sealed class ComponentsMetrics : IDisposable
12+
{
13+
public const string MeterName = "Microsoft.AspNetCore.Components";
14+
public const string LifecycleMeterName = "Microsoft.AspNetCore.Components.Lifecycle";
15+
private readonly Meter _meter;
16+
private readonly Meter _lifeCycleMeter;
17+
18+
private readonly Counter<long> _navigationCount;
19+
20+
private readonly Histogram<double> _eventDuration;
21+
private readonly Histogram<double> _parametersDuration;
22+
private readonly Histogram<double> _batchDuration;
23+
24+
public bool IsNavigationEnabled => _navigationCount.Enabled;
25+
26+
public bool IsEventEnabled => _eventDuration.Enabled;
27+
28+
public bool IsParametersEnabled => _parametersDuration.Enabled;
29+
30+
public bool IsBatchEnabled => _batchDuration.Enabled;
31+
32+
public ComponentsMetrics(IMeterFactory meterFactory)
33+
{
34+
Debug.Assert(meterFactory != null);
35+
36+
_meter = meterFactory.Create(MeterName);
37+
_lifeCycleMeter = meterFactory.Create(LifecycleMeterName);
38+
39+
_navigationCount = _meter.CreateCounter<long>(
40+
"aspnetcore.components.navigation",
41+
unit: "{route}",
42+
description: "Total number of route changes.");
43+
44+
_eventDuration = _meter.CreateHistogram(
45+
"aspnetcore.components.event_handler",
46+
unit: "s",
47+
description: "Duration of processing browser event. It includes business logic of the component but not affected child components.",
48+
advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
49+
50+
_parametersDuration = _lifeCycleMeter.CreateHistogram(
51+
"aspnetcore.components.update_parameters",
52+
unit: "s",
53+
description: "Duration of processing component parameters. It includes business logic of the component.",
54+
advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.BlazorRenderingSecondsBucketBoundaries });
55+
56+
_batchDuration = _lifeCycleMeter.CreateHistogram(
57+
"aspnetcore.components.render_diff",
58+
unit: "s",
59+
description: "Duration of rendering component tree and producing HTML diff. It includes business logic of the changed components.",
60+
advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.BlazorRenderingSecondsBucketBoundaries });
61+
}
62+
63+
public void Navigation(string componentType, string route)
64+
{
65+
var tags = new TagList
66+
{
67+
{ "aspnetcore.components.type", componentType ?? "unknown" },
68+
{ "aspnetcore.components.route", route ?? "unknown" },
69+
};
70+
71+
_navigationCount.Add(1, tags);
72+
}
73+
74+
public async Task CaptureEventDuration(Task task, long startTimestamp, string? componentType, string? methodName, string? attributeName)
75+
{
76+
var tags = new TagList
77+
{
78+
{ "aspnetcore.components.type", componentType ?? "unknown" },
79+
{ "aspnetcore.components.method", methodName ?? "unknown" },
80+
{ "aspnetcore.components.attribute.name", attributeName ?? "unknown" }
81+
};
82+
83+
try
84+
{
85+
await task;
86+
}
87+
catch (Exception ex)
88+
{
89+
tags.Add("error.type", ex.GetType().FullName ?? "unknown");
90+
}
91+
var duration = Stopwatch.GetElapsedTime(startTimestamp);
92+
_eventDuration.Record(duration.TotalSeconds, tags);
93+
}
94+
95+
public void FailEventSync(Exception ex, long startTimestamp, string? componentType, string? methodName, string? attributeName)
96+
{
97+
var tags = new TagList
98+
{
99+
{ "aspnetcore.components.type", componentType ?? "unknown" },
100+
{ "aspnetcore.components.method", methodName ?? "unknown" },
101+
{ "aspnetcore.components.attribute.name", attributeName ?? "unknown" },
102+
{ "error.type", ex.GetType().FullName ?? "unknown" }
103+
};
104+
var duration = Stopwatch.GetElapsedTime(startTimestamp);
105+
_eventDuration.Record(duration.TotalSeconds, tags);
106+
}
107+
108+
public async Task CaptureParametersDuration(Task task, long startTimestamp, string? componentType)
109+
{
110+
var tags = new TagList
111+
{
112+
{ "aspnetcore.components.type", componentType ?? "unknown" },
113+
};
114+
115+
try
116+
{
117+
await task;
118+
}
119+
catch(Exception ex)
120+
{
121+
tags.Add("error.type", ex.GetType().FullName ?? "unknown");
122+
}
123+
var duration = Stopwatch.GetElapsedTime(startTimestamp);
124+
_parametersDuration.Record(duration.TotalSeconds, tags);
125+
}
126+
127+
public void FailParametersSync(Exception ex, long startTimestamp, string? componentType)
128+
{
129+
var duration = Stopwatch.GetElapsedTime(startTimestamp);
130+
var tags = new TagList
131+
{
132+
{ "aspnetcore.components.type", componentType ?? "unknown" },
133+
{ "error.type", ex.GetType().FullName ?? "unknown" }
134+
};
135+
_parametersDuration.Record(duration.TotalSeconds, tags);
136+
}
137+
138+
public async Task CaptureBatchDuration(Task task, long startTimestamp, int diffLength)
139+
{
140+
var tags = new TagList
141+
{
142+
{ "aspnetcore.components.diff.length", BucketDiffLength(diffLength) }
143+
};
144+
145+
try
146+
{
147+
await task;
148+
}
149+
catch (Exception ex)
150+
{
151+
tags.Add("error.type", ex.GetType().FullName ?? "unknown");
152+
}
153+
var duration = Stopwatch.GetElapsedTime(startTimestamp);
154+
_batchDuration.Record(duration.TotalSeconds, tags);
155+
}
156+
157+
public void FailBatchSync(Exception ex, long startTimestamp)
158+
{
159+
var duration = Stopwatch.GetElapsedTime(startTimestamp);
160+
var tags = new TagList
161+
{
162+
{ "aspnetcore.components.diff.length", 0 },
163+
{ "error.type", ex.GetType().FullName ?? "unknown" }
164+
};
165+
_batchDuration.Record(duration.TotalSeconds, tags);
166+
}
167+
168+
private static int BucketDiffLength(int diffLength)
169+
{
170+
return diffLength switch
171+
{
172+
<= 1 => 1,
173+
<= 2 => 2,
174+
<= 5 => 5,
175+
<= 10 => 10,
176+
<= 20 => 20,
177+
<= 50 => 50,
178+
<= 100 => 100,
179+
<= 500 => 500,
180+
<= 1000 => 1000,
181+
<= 10000 => 10000,
182+
_ => 10001,
183+
};
184+
}
185+
186+
public void Dispose()
187+
{
188+
_meter.Dispose();
189+
_lifeCycleMeter.Dispose();
190+
}
191+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979

8080
<ItemGroup>
8181
<InternalsVisibleTo Include="Microsoft.AspNetCore.Components.Web" />
82+
<InternalsVisibleTo Include="Microsoft.AspNetCore.Components.Server" />
8283
<InternalsVisibleTo Include="Microsoft.AspNetCore.Blazor.Build.Tests" />
8384
<InternalsVisibleTo Include="Microsoft.AspNetCore.Components.Authorization.Tests" />
8485
<InternalsVisibleTo Include="Microsoft.AspNetCore.Components.Forms.Tests" />

src/Components/Components/src/PublicAPI.Unshipped.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#nullable enable
2-
32
Microsoft.AspNetCore.Components.Routing.Router.NotFoundPage.get -> System.Type!
43
Microsoft.AspNetCore.Components.Routing.Router.NotFoundPage.set -> void
4+
Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions
55
Microsoft.AspNetCore.Components.NavigationManager.OnNotFound -> System.EventHandler<Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs!>!
66
Microsoft.AspNetCore.Components.NavigationManager.NotFound() -> void
77
Microsoft.AspNetCore.Components.Routing.IHostEnvironmentNavigationManager.Initialize(string! baseUri, string! uri, System.Func<string!, System.Threading.Tasks.Task!>! onNavigateTo) -> void
@@ -14,5 +14,7 @@ Microsoft.AspNetCore.Components.SupplyParameterFromPersistentComponentStateAttri
1414
Microsoft.AspNetCore.Components.SupplyParameterFromPersistentComponentStateAttribute.SupplyParameterFromPersistentComponentStateAttribute() -> void
1515
Microsoft.Extensions.DependencyInjection.SupplyParameterFromPersistentComponentStateProviderServiceCollectionExtensions
1616
static Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponentStateServiceCollectionExtensions.AddPersistentServiceRegistration<TService>(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.AspNetCore.Components.IComponentRenderMode! componentRenderMode) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
17+
static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsMetrics(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
18+
static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsTracing(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
1719
static Microsoft.Extensions.DependencyInjection.SupplyParameterFromPersistentComponentStateProviderServiceCollectionExtensions.AddSupplyValueFromPersistentComponentStateProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
1820
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.SignalRendererToFinishRendering() -> void

0 commit comments

Comments
 (0)