From 88cd5fbabe24947efee1ecbfa3e21d14d2e78a7e Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 16 Jul 2025 16:05:06 +0200 Subject: [PATCH 1/5] - split aspnetcore.components.render_diff into aspnetcore.components.render_diff.duration and aspnetcore.components.render_diff.size - rename aspnetcore.components.navigation to aspnetcore.components.navigate to match the trace name - rename aspnetcore.components.event_handler to aspnetcore.components.handle_event.duration to match the trace name - rename aspnetcore.components.update_parameters to aspnetcore.components.update_parameters.duration - update unit tests --- .../Components/src/ComponentsMetrics.cs | 40 ++++++----------- .../Components/test/ComponentsMetricsTest.cs | 45 ++++++++++--------- src/Shared/Metrics/MetricsConstants.cs | 3 ++ 3 files changed, 39 insertions(+), 49 deletions(-) diff --git a/src/Components/Components/src/ComponentsMetrics.cs b/src/Components/Components/src/ComponentsMetrics.cs index 712d9e395b4c..684776729ccd 100644 --- a/src/Components/Components/src/ComponentsMetrics.cs +++ b/src/Components/Components/src/ComponentsMetrics.cs @@ -20,6 +20,7 @@ internal sealed class ComponentsMetrics : IDisposable private readonly Histogram _eventDuration; private readonly Histogram _parametersDuration; private readonly Histogram _batchDuration; + private readonly Histogram _batchSize; public bool IsNavigationEnabled => _navigationCount.Enabled; @@ -37,27 +38,33 @@ public ComponentsMetrics(IMeterFactory meterFactory) _lifeCycleMeter = meterFactory.Create(LifecycleMeterName); _navigationCount = _meter.CreateCounter( - "aspnetcore.components.navigation", + "aspnetcore.components.navigate", unit: "{route}", description: "Total number of route changes."); _eventDuration = _meter.CreateHistogram( - "aspnetcore.components.event_handler", + "aspnetcore.components.handle_event.duration", unit: "s", description: "Duration of processing browser event. It includes business logic of the component but not affected child components.", advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); _parametersDuration = _lifeCycleMeter.CreateHistogram( - "aspnetcore.components.update_parameters", + "aspnetcore.components.update_parameters.duration", unit: "s", description: "Duration of processing component parameters. It includes business logic of the component.", advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.BlazorRenderingSecondsBucketBoundaries }); _batchDuration = _lifeCycleMeter.CreateHistogram( - "aspnetcore.components.render_diff", + "aspnetcore.components.render_diff.duration", unit: "s", description: "Duration of rendering component tree and producing HTML diff. It includes business logic of the changed components.", advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.BlazorRenderingSecondsBucketBoundaries }); + + _batchSize = _lifeCycleMeter.CreateHistogram( + "aspnetcore.components.render_diff.size", + unit: "{elements}", + description: "Number of HTML elements modified during rendering of batch of changes.", + advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.BlazorRenderingDiffLengthBucketBoundaries }); } public void Navigation(string componentType, string route) @@ -137,10 +144,7 @@ public void FailParametersSync(Exception ex, long startTimestamp, string? compon public async Task CaptureBatchDuration(Task task, long startTimestamp, int diffLength) { - var tags = new TagList - { - { "aspnetcore.components.diff.length", BucketDiffLength(diffLength) } - }; + var tags = new TagList(); try { @@ -152,6 +156,7 @@ public async Task CaptureBatchDuration(Task task, long startTimestamp, int diffL } var duration = Stopwatch.GetElapsedTime(startTimestamp); _batchDuration.Record(duration.TotalSeconds, tags); + _batchSize.Record(diffLength); } public void FailBatchSync(Exception ex, long startTimestamp) @@ -159,30 +164,11 @@ public void FailBatchSync(Exception ex, long startTimestamp) var duration = Stopwatch.GetElapsedTime(startTimestamp); var tags = new TagList { - { "aspnetcore.components.diff.length", 0 }, { "error.type", ex.GetType().FullName ?? "unknown" } }; _batchDuration.Record(duration.TotalSeconds, tags); } - private static int BucketDiffLength(int diffLength) - { - return diffLength switch - { - <= 1 => 1, - <= 2 => 2, - <= 5 => 5, - <= 10 => 10, - <= 20 => 20, - <= 50 => 50, - <= 100 => 100, - <= 500 => 500, - <= 1000 => 1000, - <= 10000 => 10000, - _ => 10001, - }; - } - public void Dispose() { _meter.Dispose(); diff --git a/src/Components/Components/test/ComponentsMetricsTest.cs b/src/Components/Components/test/ComponentsMetricsTest.cs index e8b466d5e45d..8293874cab14 100644 --- a/src/Components/Components/test/ComponentsMetricsTest.cs +++ b/src/Components/Components/test/ComponentsMetricsTest.cs @@ -39,7 +39,7 @@ public void Navigation_RecordsMetric() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var navigationCounter = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.navigation"); + ComponentsMetrics.MeterName, "aspnetcore.components.navigate"); // Act componentsMetrics.Navigation("TestComponent", "/test-route"); @@ -61,7 +61,7 @@ public void IsNavigationEnabled_ReturnsCorrectState() // Create a collector to ensure the meter is enabled using var navigationCounter = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.navigation"); + ComponentsMetrics.MeterName, "aspnetcore.components.navigate"); // Act & Assert Assert.True(componentsMetrics.IsNavigationEnabled); @@ -73,7 +73,7 @@ public async Task CaptureEventDuration_RecordsSuccessMetric() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var eventDurationHistogram = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.event_handler"); + ComponentsMetrics.MeterName, "aspnetcore.components.handle_event.duration"); // Act var startTimestamp = Stopwatch.GetTimestamp(); @@ -98,7 +98,7 @@ public async Task CaptureEventDuration_RecordsErrorMetric() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var eventDurationHistogram = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.event_handler"); + ComponentsMetrics.MeterName, "aspnetcore.components.handle_event.duration"); // Act var startTimestamp = Stopwatch.GetTimestamp(); @@ -123,7 +123,7 @@ public void FailEventSync_RecordsErrorMetric() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var eventDurationHistogram = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.event_handler"); + ComponentsMetrics.MeterName, "aspnetcore.components.handle_event.duration"); var exception = new InvalidOperationException(); // Act @@ -150,7 +150,7 @@ public void IsEventEnabled_ReturnsCorrectState() // Create a collector to ensure the meter is enabled using var eventDurationHistogram = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.event_handler"); + ComponentsMetrics.MeterName, "aspnetcore.components.handle_event.duration"); // Act & Assert Assert.True(componentsMetrics.IsEventEnabled); @@ -162,7 +162,7 @@ public async Task CaptureParametersDuration_RecordsSuccessMetric() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var parametersDurationHistogram = new MetricCollector(_meterFactory, - ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters"); + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters.duration"); // Act var startTimestamp = Stopwatch.GetTimestamp(); @@ -184,7 +184,7 @@ public async Task CaptureParametersDuration_RecordsErrorMetric() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var parametersDurationHistogram = new MetricCollector(_meterFactory, - ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters"); + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters.duration"); // Act var startTimestamp = Stopwatch.GetTimestamp(); @@ -207,7 +207,7 @@ public void FailParametersSync_RecordsErrorMetric() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var parametersDurationHistogram = new MetricCollector(_meterFactory, - ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters"); + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters.duration"); var exception = new InvalidOperationException(); // Act @@ -231,7 +231,7 @@ public void IsParametersEnabled_ReturnsCorrectState() // Create a collector to ensure the meter is enabled using var parametersDurationHistogram = new MetricCollector(_meterFactory, - ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters"); + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters.duration"); // Act & Assert Assert.True(componentsMetrics.IsParametersEnabled); @@ -243,7 +243,7 @@ public async Task CaptureBatchDuration_RecordsSuccessMetric() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var batchDurationHistogram = new MetricCollector(_meterFactory, - ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff"); + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff.duration"); // Act var startTimestamp = Stopwatch.GetTimestamp(); @@ -255,7 +255,6 @@ public async Task CaptureBatchDuration_RecordsSuccessMetric() Assert.Single(measurements); Assert.True(measurements[0].Value > 0); - Assert.Equal(50, Assert.Contains("aspnetcore.components.diff.length", measurements[0].Tags)); Assert.DoesNotContain("error.type", measurements[0].Tags); } @@ -265,7 +264,7 @@ public async Task CaptureBatchDuration_RecordsErrorMetric() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var batchDurationHistogram = new MetricCollector(_meterFactory, - ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff"); + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff.duration"); // Act var startTimestamp = Stopwatch.GetTimestamp(); @@ -278,7 +277,6 @@ await componentsMetrics.CaptureBatchDuration(Task.FromException(new InvalidOpera Assert.Single(measurements); Assert.True(measurements[0].Value > 0); - Assert.Equal(50, Assert.Contains("aspnetcore.components.diff.length", measurements[0].Tags)); Assert.Equal("System.InvalidOperationException", Assert.Contains("error.type", measurements[0].Tags)); } @@ -288,7 +286,7 @@ public void FailBatchSync_RecordsErrorMetric() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var batchDurationHistogram = new MetricCollector(_meterFactory, - ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff"); + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff.duration"); var exception = new InvalidOperationException(); // Act @@ -300,7 +298,6 @@ public void FailBatchSync_RecordsErrorMetric() Assert.Single(measurements); Assert.True(measurements[0].Value > 0); - Assert.Equal(0, Assert.Contains("aspnetcore.components.diff.length", measurements[0].Tags)); Assert.Equal("System.InvalidOperationException", Assert.Contains("error.type", measurements[0].Tags)); } @@ -312,7 +309,7 @@ public void IsBatchEnabled_ReturnsCorrectState() // Create a collector to ensure the meter is enabled using var batchDurationHistogram = new MetricCollector(_meterFactory, - ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff"); + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff.duration"); // Act & Assert Assert.True(componentsMetrics.IsBatchEnabled); @@ -324,13 +321,15 @@ public async Task ComponentLifecycle_RecordsAllMetricsCorrectly() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var navigationCounter = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.navigation"); + ComponentsMetrics.MeterName, "aspnetcore.components.navigate"); using var eventDurationHistogram = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.event_handler"); + ComponentsMetrics.MeterName, "aspnetcore.components.handle_event.duration"); using var parametersDurationHistogram = new MetricCollector(_meterFactory, - ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters"); + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters.duration"); using var batchDurationHistogram = new MetricCollector(_meterFactory, - ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff"); + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff.duration"); + using var batchSizeHistogram = new MetricCollector(_meterFactory, + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff.size"); // Act - Simulate a component lifecycle // 1. Navigation @@ -357,11 +356,13 @@ await componentsMetrics.CaptureEventDuration(Task.CompletedTask, startTimestamp2 var eventMeasurements = eventDurationHistogram.GetMeasurementSnapshot(); var parametersMeasurements = parametersDurationHistogram.GetMeasurementSnapshot(); var batchMeasurements = batchDurationHistogram.GetMeasurementSnapshot(); + var batchSizeMeasurements = batchSizeHistogram.GetMeasurementSnapshot(); Assert.Single(navigationMeasurements); Assert.Single(eventMeasurements); Assert.Single(parametersMeasurements); Assert.Single(batchMeasurements); + Assert.Single(batchSizeMeasurements); // Check navigation Assert.Equal(1, navigationMeasurements[0].Value); @@ -380,7 +381,7 @@ await componentsMetrics.CaptureEventDuration(Task.CompletedTask, startTimestamp2 // Check batch duration Assert.True(batchMeasurements[0].Value > 0); - Assert.Equal(20, Assert.Contains("aspnetcore.components.diff.length", batchMeasurements[0].Tags)); + Assert.True(batchSizeMeasurements[0].Value > 0); } [Fact] diff --git a/src/Shared/Metrics/MetricsConstants.cs b/src/Shared/Metrics/MetricsConstants.cs index cebbaba0e6a9..c4716669458b 100644 --- a/src/Shared/Metrics/MetricsConstants.cs +++ b/src/Shared/Metrics/MetricsConstants.cs @@ -14,6 +14,9 @@ internal static class MetricsConstants // For blazor rendering, which should be very fast. public static readonly IReadOnlyList BlazorRenderingSecondsBucketBoundaries = [0.000001, 0.00001, 0.0001, 0.001, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]; + // For blazor rendering, which should be very fast. + public static readonly IReadOnlyList BlazorRenderingDiffLengthBucketBoundaries = [1, 2, 5, 10, 20, 50, 100, 500, 1000, 10000]; + // For blazor circuit sessions, which can last a long time. public static readonly IReadOnlyList BlazorCircuitSecondsBucketBoundaries = [1, 3, 10, 30, 1 * 60, 3 * 60, 10 * 60, 30 * 60, 1 * 60 * 60, 3 * 60 * 60, 10 * 60 * 60, 24 * 60 * 60]; } From 3a9725e1257636247ce1dbad470c103e7997fff9 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Thu, 17 Jul 2025 09:41:24 +0200 Subject: [PATCH 2/5] Update src/Components/Components/src/ComponentsMetrics.cs Co-authored-by: James Newton-King --- src/Components/Components/src/ComponentsMetrics.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Components/src/ComponentsMetrics.cs b/src/Components/Components/src/ComponentsMetrics.cs index 684776729ccd..063a559b5cd9 100644 --- a/src/Components/Components/src/ComponentsMetrics.cs +++ b/src/Components/Components/src/ComponentsMetrics.cs @@ -63,7 +63,7 @@ public ComponentsMetrics(IMeterFactory meterFactory) _batchSize = _lifeCycleMeter.CreateHistogram( "aspnetcore.components.render_diff.size", unit: "{elements}", - description: "Number of HTML elements modified during rendering of batch of changes.", + description: "Number of HTML elements modified during a rendering batch.", advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.BlazorRenderingDiffLengthBucketBoundaries }); } From 073cce320fc4495cedf17b4e3d4771a15fadc155 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Thu, 17 Jul 2025 09:41:34 +0200 Subject: [PATCH 3/5] Update src/Shared/Metrics/MetricsConstants.cs Co-authored-by: James Newton-King --- src/Shared/Metrics/MetricsConstants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Shared/Metrics/MetricsConstants.cs b/src/Shared/Metrics/MetricsConstants.cs index c4716669458b..9af080724638 100644 --- a/src/Shared/Metrics/MetricsConstants.cs +++ b/src/Shared/Metrics/MetricsConstants.cs @@ -14,7 +14,6 @@ internal static class MetricsConstants // For blazor rendering, which should be very fast. public static readonly IReadOnlyList BlazorRenderingSecondsBucketBoundaries = [0.000001, 0.00001, 0.0001, 0.001, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]; - // For blazor rendering, which should be very fast. public static readonly IReadOnlyList BlazorRenderingDiffLengthBucketBoundaries = [1, 2, 5, 10, 20, 50, 100, 500, 1000, 10000]; // For blazor circuit sessions, which can last a long time. From 771329d99c80f020e2031bb2083b4de099228bdf Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Thu, 17 Jul 2025 09:42:20 +0200 Subject: [PATCH 4/5] Update src/Components/Components/src/ComponentsMetrics.cs Co-authored-by: James Newton-King --- src/Components/Components/src/ComponentsMetrics.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Components/src/ComponentsMetrics.cs b/src/Components/Components/src/ComponentsMetrics.cs index 063a559b5cd9..089696e301ce 100644 --- a/src/Components/Components/src/ComponentsMetrics.cs +++ b/src/Components/Components/src/ComponentsMetrics.cs @@ -156,7 +156,7 @@ public async Task CaptureBatchDuration(Task task, long startTimestamp, int diffL } var duration = Stopwatch.GetElapsedTime(startTimestamp); _batchDuration.Record(duration.TotalSeconds, tags); - _batchSize.Record(diffLength); + _batchSize.Record(diffLength, tags); } public void FailBatchSync(Exception ex, long startTimestamp) From 83086d34c99192092e58d36ff12823919a233841 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 17 Jul 2025 16:44:46 +0200 Subject: [PATCH 5/5] feedback --- src/Shared/Metrics/MetricsConstants.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Shared/Metrics/MetricsConstants.cs b/src/Shared/Metrics/MetricsConstants.cs index 9af080724638..d62983f9bba4 100644 --- a/src/Shared/Metrics/MetricsConstants.cs +++ b/src/Shared/Metrics/MetricsConstants.cs @@ -11,11 +11,12 @@ internal static class MetricsConstants // Not based on a standard. Larger bucket sizes for longer lasting operations, e.g. HTTP connection duration. See https://github.com/open-telemetry/semantic-conventions/issues/336 public static readonly IReadOnlyList LongSecondsBucketBoundaries = [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 30, 60, 120, 300]; - // For blazor rendering, which should be very fast. + // For Blazor rendering, which should be very fast. public static readonly IReadOnlyList BlazorRenderingSecondsBucketBoundaries = [0.000001, 0.00001, 0.0001, 0.001, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]; - public static readonly IReadOnlyList BlazorRenderingDiffLengthBucketBoundaries = [1, 2, 5, 10, 20, 50, 100, 500, 1000, 10000]; + // For measuring the length of HTML diff in Blazor rendering. + public static readonly IReadOnlyList BlazorRenderingDiffLengthBucketBoundaries = [0, 1, 2, 4, 6, 8, 10, 20, 30, 40, 50, 100]; - // For blazor circuit sessions, which can last a long time. + // For Blazor circuit sessions, which can last a long time. public static readonly IReadOnlyList BlazorCircuitSecondsBucketBoundaries = [1, 3, 10, 30, 1 * 60, 3 * 60, 10 * 60, 30 * 60, 1 * 60 * 60, 3 * 60 * 60, 10 * 60 * 60, 24 * 60 * 60]; }