From cebb68edcfcbdf880050053e66f5627b048110aa Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 23 Apr 2025 20:01:34 +0200 Subject: [PATCH 1/9] rebase --- .../Components/src/PublicAPI.Unshipped.txt | 2 + ...eringMetricsServiceCollectionExtensions.cs | 31 ++ .../Components/src/RenderTree/Renderer.cs | 64 ++- .../src/Rendering/ComponentState.cs | 34 +- .../src/Rendering/RenderingMetrics.cs | 277 +++++++++--- .../test/Rendering/RenderingMetricsTest.cs | 415 ++++++++++++------ ...orComponentsServiceCollectionExtensions.cs | 2 + .../Server/src/Circuits/CircuitHost.cs | 3 +- .../Server/src/Circuits/CircuitMetrics.cs | 25 +- .../ComponentServiceCollectionExtensions.cs | 7 +- .../test/Circuits/CircuitMetricsTest.cs | 4 +- .../src/HtmlRendering/StaticHtmlRenderer.cs | 1 - src/Shared/Metrics/MetricsConstants.cs | 4 +- 13 files changed, 649 insertions(+), 220 deletions(-) create mode 100644 src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 21c0226e2ef5..46b13fa70546 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1,4 +1,5 @@ #nullable enable +Microsoft.AspNetCore.Components.Infrastructure.RenderingMetricsServiceCollectionExtensions Microsoft.AspNetCore.Components.NavigationManager.OnNotFound -> System.EventHandler! Microsoft.AspNetCore.Components.NavigationManager.NotFound() -> void Microsoft.AspNetCore.Components.Routing.IHostEnvironmentNavigationManager.Initialize(string! baseUri, string! uri, System.Func! onNavigateTo) -> void @@ -11,5 +12,6 @@ Microsoft.AspNetCore.Components.SupplyParameterFromPersistentComponentStateAttri Microsoft.AspNetCore.Components.SupplyParameterFromPersistentComponentStateAttribute.SupplyParameterFromPersistentComponentStateAttribute() -> void Microsoft.Extensions.DependencyInjection.SupplyParameterFromPersistentComponentStateProviderServiceCollectionExtensions static Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponentStateServiceCollectionExtensions.AddPersistentServiceRegistration(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.AspNetCore.Components.IComponentRenderMode! componentRenderMode) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.AspNetCore.Components.Infrastructure.RenderingMetricsServiceCollectionExtensions.AddRenderingMetrics(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Extensions.DependencyInjection.SupplyParameterFromPersistentComponentStateProviderServiceCollectionExtensions.AddSupplyValueFromPersistentComponentStateProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.SignalRendererToFinishRendering() -> void \ No newline at end of file diff --git a/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs b/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs new file mode 100644 index 000000000000..da1a539278e9 --- /dev/null +++ b/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Components.Infrastructure; + +/// +/// Infrastructure APIs for registering diagnostic metrics. +/// +public static class RenderingMetricsServiceCollectionExtensions +{ + /// + /// Registers component rendering metrics + /// + /// The . + /// The . + public static IServiceCollection AddRenderingMetrics( + IServiceCollection services) + { + if (RenderingMetrics.IsMetricsSupported) + { + services.AddMetrics(); + services.TryAddSingleton(); + } + + return services; + } +} diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index 5ba977930a46..f616da46b94f 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Metrics; using System.Linq; using Microsoft.AspNetCore.Components.HotReload; using Microsoft.AspNetCore.Components.Reflection; @@ -25,12 +24,14 @@ namespace Microsoft.AspNetCore.Components.RenderTree; // dispatching events to them, and notifying when the user interface is being updated. public abstract partial class Renderer : IDisposable, IAsyncDisposable { + internal static readonly Task CanceledRenderTask = Task.FromCanceled(new CancellationToken(canceled: true)); + private readonly object _lockObject = new(); private readonly IServiceProvider _serviceProvider; private readonly Dictionary _componentStateById = new Dictionary(); private readonly Dictionary _componentStateByComponent = new Dictionary(); private readonly RenderBatchBuilder _batchBuilder = new RenderBatchBuilder(); - private readonly Dictionary _eventBindings = new(); + private readonly Dictionary _eventBindings = new(); private readonly Dictionary _eventHandlerIdReplacements = new Dictionary(); private readonly ILogger _logger; private readonly ComponentFactory _componentFactory; @@ -92,16 +93,18 @@ public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, // logger name in here as a string literal. _logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Components.RenderTree.Renderer"); _componentFactory = new ComponentFactory(componentActivator, this); - - // TODO register RenderingMetrics as singleton in DI - var meterFactory = serviceProvider.GetService(); - _renderingMetrics = meterFactory != null ? new RenderingMetrics(meterFactory) : null; + if (RenderingMetrics.IsMetricsSupported) + { + _renderingMetrics = serviceProvider.GetService(); + } ServiceProviderCascadingValueSuppliers = serviceProvider.GetService() is null ? Array.Empty() : serviceProvider.GetServices().ToArray(); } + internal RenderingMetrics? RenderingMetrics => RenderingMetrics.IsMetricsSupported ? _renderingMetrics : null; + internal ICascadingValueSupplier[] ServiceProviderCascadingValueSuppliers { get; } internal HotReloadManager HotReloadManager { get; set; } = HotReloadManager.Default; @@ -437,12 +440,14 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie { Dispatcher.AssertAccess(); + var eventStartTimestamp = RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsEventDurationEnabled ? Stopwatch.GetTimestamp() : 0; + if (waitForQuiescence) { _pendingTasks ??= new(); } - var (renderedByComponentId, callback) = GetRequiredEventBindingEntry(eventHandlerId); + var (renderedByComponentId, callback, attributeName) = GetRequiredEventBindingEntry(eventHandlerId); // If this event attribute was rendered by a component that's since been disposed, don't dispatch the event at all. // This can occur because event handler disposal is deferred, so event handler IDs can outlive their components. @@ -484,9 +489,25 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie _isBatchInProgress = true; task = callback.InvokeAsync(eventArgs); + + // collect metrics + if (RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsEventDurationEnabled) + { + var receiverName = (callback.Receiver?.GetType() ?? callback.Delegate.Target?.GetType())?.FullName; + RenderingMetrics.EventDurationSync(eventStartTimestamp, receiverName, attributeName); + _ = RenderingMetrics.CaptureEventDurationAsync(task, eventStartTimestamp, receiverName, attributeName); + } + if (RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsEventExceptionEnabled) + { + _ = RenderingMetrics.CaptureEventFailedAsync(task, callback, attributeName); + } } catch (Exception e) { + if (RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsEventExceptionEnabled) + { + RenderingMetrics.EventFailed(e.GetType().FullName, callback, attributeName); + } HandleExceptionViaErrorBoundary(e, receiverComponentState); return Task.CompletedTask; } @@ -497,6 +518,10 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie // Since the task has yielded - process any queued rendering work before we return control // to the caller. ProcessPendingRender(); + + //callback.Receiver + //callback.Delegate.Method. + } // Task completed synchronously or is still running. We already processed all of the rendering @@ -638,7 +663,7 @@ internal void AssignEventHandlerId(int renderedByComponentId, ref RenderTreeFram // // When that happens we intentionally box the EventCallback because we need to hold on to // the receiver. - _eventBindings.Add(id, (renderedByComponentId, callback)); + _eventBindings.Add(id, (renderedByComponentId, callback, frame.AttributeName)); } else if (frame.AttributeValueField is MulticastDelegate @delegate) { @@ -646,7 +671,7 @@ internal void AssignEventHandlerId(int renderedByComponentId, ref RenderTreeFram // is the same as delegate.Target. In this case since the receiver is implicit we can // avoid boxing the EventCallback object and just re-hydrate it on the other side of the // render tree. - _eventBindings.Add(id, (renderedByComponentId, new EventCallback(@delegate.Target as IHandleEvent, @delegate))); + _eventBindings.Add(id, (renderedByComponentId, new EventCallback(@delegate.Target as IHandleEvent, @delegate), frame.AttributeName)); } // NOTE: we do not to handle EventCallback here. EventCallback is only used when passing @@ -696,7 +721,7 @@ internal void TrackReplacedEventHandlerId(ulong oldEventHandlerId, ulong newEven _eventHandlerIdReplacements.Add(oldEventHandlerId, newEventHandlerId); } - private (int RenderedByComponentId, EventCallback Callback) GetRequiredEventBindingEntry(ulong eventHandlerId) + private (int RenderedByComponentId, EventCallback Callback, string? attributeName) GetRequiredEventBindingEntry(ulong eventHandlerId) { if (!_eventBindings.TryGetValue(eventHandlerId, out var entry)) { @@ -770,6 +795,7 @@ private void ProcessRenderQueue() _isBatchInProgress = true; var updateDisplayTask = Task.CompletedTask; + var batchStartTimestamp = RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsBatchDurationEnabled ? Stopwatch.GetTimestamp() : 0; try { @@ -801,9 +827,23 @@ private void ProcessRenderQueue() // Fire off the execution of OnAfterRenderAsync, but don't wait for it // if there is async work to be done. _ = InvokeRenderCompletedCalls(batch.UpdatedComponents, updateDisplayTask); + + if (RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsBatchDurationEnabled) + { + _renderingMetrics.BatchDuration(batchStartTimestamp, batch.UpdatedComponents.Count); + } + if (RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsBatchExceptionEnabled) + { + _ = _renderingMetrics.CaptureBatchFailedAsync(updateDisplayTask); + } } catch (Exception e) { + if (RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsBatchExceptionEnabled) + { + _renderingMetrics.BatchFailed(e.GetType().Name); + } + // Ensure we catch errors while running the render functions of the components. HandleException(e); return; @@ -947,15 +987,13 @@ 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(); diff --git a/src/Components/Components/src/Rendering/ComponentState.cs b/src/Components/Components/src/Rendering/ComponentState.cs index c7be2643edd9..7b80404fb4d3 100644 --- a/src/Components/Components/src/Rendering/ComponentState.cs +++ b/src/Components/Components/src/Rendering/ComponentState.cs @@ -23,6 +23,7 @@ public class ComponentState : IAsyncDisposable private RenderTreeBuilder _nextRenderTree; private ArrayBuilder? _latestDirectParametersSnapshot; // Lazily instantiated private bool _componentWasDisposed; + private readonly string? _componentTypeName; /// /// Constructs an instance of . @@ -51,6 +52,11 @@ public ComponentState(Renderer renderer, int componentId, IComponent component, _hasCascadingParameters = true; _hasAnyCascadingParameterSubscriptions = AddCascadingParameterSubscriptions(); } + + if (RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && (_renderer.RenderingMetrics.IsDiffDurationEnabled || _renderer.RenderingMetrics.IsStateDurationEnabled || _renderer.RenderingMetrics.IsStateExceptionEnabled)) + { + _componentTypeName = component.GetType().FullName; + } } private static ComponentState? GetSectionOutletLogicalParent(Renderer renderer, SectionOutlet sectionOutlet) @@ -102,6 +108,7 @@ internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment re _nextRenderTree.Clear(); + var diffStartTimestamp = RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsDiffDurationEnabled ? Stopwatch.GetTimestamp() : 0; try { renderFragment(_nextRenderTree); @@ -118,6 +125,8 @@ internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment re // We don't want to make errors from this be recoverable, because there's no legitimate reason for them to happen _nextRenderTree.AssertTreeIsValid(Component); + var startCount = batchBuilder.EditsBuffer.Count; + // Swap the old and new tree builders (CurrentRenderTree, _nextRenderTree) = (_nextRenderTree, CurrentRenderTree); @@ -129,6 +138,11 @@ internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment re CurrentRenderTree.GetFrames()); batchBuilder.UpdatedComponentDiffs.Append(diff); batchBuilder.InvalidateParameterViews(); + + if (RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsDiffDurationEnabled) + { + _renderer.RenderingMetrics.DiffDuration(diffStartTimestamp, _componentTypeName, batchBuilder.EditsBuffer.Count - startCount); + } } // Callers expect this method to always return a faulted task. @@ -231,14 +245,32 @@ internal void NotifyCascadingValueChanged(in ParameterViewLifetime lifetime) // a consistent set to the recipient. private void SupplyCombinedParameters(ParameterView directAndCascadingParameters) { - // Normalise sync and async exceptions into a Task + // Normalize sync and async exceptions into a Task Task setParametersAsyncTask; try { + var stateStartTimestamp = RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateDurationEnabled ? Stopwatch.GetTimestamp() : 0; + setParametersAsyncTask = Component.SetParametersAsync(directAndCascadingParameters); + + // collect metrics + if (RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateDurationEnabled) + { + _renderer.RenderingMetrics.ParametersDurationSync(stateStartTimestamp, _componentTypeName); + _ = _renderer.RenderingMetrics.CaptureParametersDurationAsync(setParametersAsyncTask, stateStartTimestamp, _componentTypeName); + } + if (RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateExceptionEnabled) + { + _ = _renderer.RenderingMetrics.CapturePropertiesFailedAsync(setParametersAsyncTask, _componentTypeName); + } } catch (Exception ex) { + if (RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateExceptionEnabled) + { + _renderer.RenderingMetrics.PropertiesFailed(ex.GetType().FullName, _componentTypeName); + } + setParametersAsyncTask = Task.FromException(ex); } diff --git a/src/Components/Components/src/Rendering/RenderingMetrics.cs b/src/Components/Components/src/Rendering/RenderingMetrics.cs index 54b32a793cc7..6c0f9f918053 100644 --- a/src/Components/Components/src/Rendering/RenderingMetrics.cs +++ b/src/Components/Components/src/Rendering/RenderingMetrics.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; using Microsoft.AspNetCore.Http; @@ -10,97 +11,273 @@ 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 _renderTotalCounter; - private readonly UpDownCounter _renderActiveCounter; - private readonly Histogram _renderDuration; + + private readonly Histogram _eventSyncDuration; + private readonly Histogram _eventAsyncDuration; + private readonly Counter _eventException; + + private readonly Histogram _parametersSyncDuration; + private readonly Histogram _parametersAsyncDuration; + private readonly Counter _parametersException; + + private readonly Histogram _diffDuration; + + private readonly Histogram _batchDuration; + private readonly Counter _batchException; + + [FeatureSwitchDefinition("System.Diagnostics.Metrics.Meter.IsSupported")] + public static bool IsMetricsSupported { get; } = InitializeIsMetricsSupported(); + private static bool InitializeIsMetricsSupported() => AppContext.TryGetSwitch("System.Diagnostics.Metrics.Meter.IsSupported", out bool isSupported) ? isSupported : true; + + public bool IsEventDurationEnabled => IsMetricsSupported && (_eventSyncDuration.Enabled || _eventAsyncDuration.Enabled); + public bool IsEventExceptionEnabled => IsMetricsSupported && _eventException.Enabled; + + public bool IsStateDurationEnabled => IsMetricsSupported && (_parametersSyncDuration.Enabled || _parametersAsyncDuration.Enabled); + public bool IsStateExceptionEnabled => IsMetricsSupported && _parametersException.Enabled; + + public bool IsDiffDurationEnabled => IsMetricsSupported && _diffDuration.Enabled; + + public bool IsBatchDurationEnabled => IsMetricsSupported && _batchDuration.Enabled; + public bool IsBatchExceptionEnabled => IsMetricsSupported && _batchException.Enabled; public RenderingMetrics(IMeterFactory meterFactory) { + if (!IsMetricsSupported) + { + // TryAddSingleton prevents trimming constructors, so we trim constructor this way + throw new NotSupportedException("Metrics are not supported in this environment."); + } + Debug.Assert(meterFactory != null); _meter = meterFactory.Create(MeterName); - _renderTotalCounter = _meter.CreateCounter( - "aspnetcore.components.rendering.count", - unit: "{renders}", - description: "Number of component renders performed."); + _eventSyncDuration = _meter.CreateHistogram( + "aspnetcore.components.rendering.event.synchronous.duration", + unit: "s", + description: "Duration of processing browser event synchronously.", + advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); + + _eventAsyncDuration = _meter.CreateHistogram( + "aspnetcore.components.rendering.event.asynchronous.duration", + unit: "s", + description: "Duration of processing browser event asynchronously.", + advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); + + _eventException = _meter.CreateCounter( + "aspnetcore.components.rendering.event.exception", + unit: "{exceptions}", + description: "Total number of exceptions during browser event processing."); + + _parametersSyncDuration = _meter.CreateHistogram( + "aspnetcore.components.rendering.parameters.synchronous.duration", + unit: "s", + description: "Duration of processing component parameters synchronously.", + advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); + + _parametersAsyncDuration = _meter.CreateHistogram( + "aspnetcore.components.rendering.parameters.asynchronous.duration", + unit: "s", + description: "Duration of processing component parameters asynchronously.", + advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); + + _parametersException = _meter.CreateCounter( + "aspnetcore.components.rendering.parameters.exception", + unit: "{exceptions}", + description: "Total number of exceptions during processing component parameters."); - _renderActiveCounter = _meter.CreateUpDownCounter( - "aspnetcore.components.rendering.active_renders", - unit: "{renders}", - description: "Number of component renders performed."); + _diffDuration = _meter.CreateHistogram( + "aspnetcore.components.rendering.diff.duration", + unit: "s", + description: "Duration of rendering component HTML diff.", + advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); - _renderDuration = _meter.CreateHistogram( - "aspnetcore.components.rendering.duration", - unit: "ms", - description: "Duration of component rendering operations per component.", + _batchDuration = _meter.CreateHistogram( + "aspnetcore.components.rendering.batch.duration", + unit: "s", + description: "Duration of rendering batch.", advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); + + _batchException = _meter.CreateCounter( + "aspnetcore.components.rendering.batch.exception", + unit: "{exceptions}", + description: "Total number of exceptions during batch rendering."); } - public void RenderStart(string componentType) + public void EventDurationSync(long startTimestamp, string? componentType, string? attributeName) { - var tags = new TagList(); - tags = InitializeRequestTags(componentType, tags); + var tags = new TagList + { + { "component.type", componentType ?? "unknown" }, + { "attribute.name", attributeName ?? "unknown"} + }; - if (_renderActiveCounter.Enabled) + var duration = Stopwatch.GetElapsedTime(startTimestamp); + _eventSyncDuration.Record(duration.TotalSeconds, tags); + } + + public async Task CaptureEventDurationAsync(Task task, long startTimestamp, string? componentType, string? attributeName) + { + try { - _renderActiveCounter.Add(1, tags); + await task; + + var tags = new TagList + { + { "component.type", componentType ?? "unknown" }, + { "attribute.name", attributeName ?? "unknown" } + }; + + var duration = Stopwatch.GetElapsedTime(startTimestamp); + _eventAsyncDuration.Record(duration.TotalSeconds, tags); } - if (_renderTotalCounter.Enabled) + catch { - _renderTotalCounter.Add(1, tags); + // none } } - public void RenderEnd(string componentType, Exception? exception, long startTimestamp, long currentTimestamp) + public void ParametersDurationSync(long startTimestamp, string? componentType) + { + var tags = new TagList + { + { "component.type", componentType ?? "unknown" }, + }; + + var duration = Stopwatch.GetElapsedTime(startTimestamp); + _parametersSyncDuration.Record(duration.TotalSeconds, tags); + } + + public async Task CaptureParametersDurationAsync(Task task, long startTimestamp, string? componentType) { - // Tags must match request start. - var tags = new TagList(); - tags = InitializeRequestTags(componentType, tags); + try + { + await task; - if (_renderActiveCounter.Enabled) + var tags = new TagList + { + { "component.type", componentType ?? "unknown" }, + }; + + var duration = Stopwatch.GetElapsedTime(startTimestamp); + _parametersAsyncDuration.Record(duration.TotalSeconds, tags); + } + catch { - _renderActiveCounter.Add(-1, tags); + // none } + } - if (_renderDuration.Enabled) + public void DiffDuration(long startTimestamp, string? componentType, int diffLength) + { + var tags = new TagList { - if (exception != null) - { - TryAddTag(ref tags, "error.type", exception.GetType().FullName); - } + { "component.type", componentType ?? "unknown" }, + { "diff.length.bucket", BucketEditLength(diffLength) } + }; + + var duration = Stopwatch.GetElapsedTime(startTimestamp); + _diffDuration.Record(duration.TotalSeconds, tags); + } + + public void BatchDuration(long startTimestamp, int diffLength) + { + var tags = new TagList + { + { "diff.length.bucket", BucketEditLength(diffLength) } + }; + + var duration = Stopwatch.GetElapsedTime(startTimestamp); + _batchDuration.Record(duration.TotalSeconds, tags); + } - var duration = Stopwatch.GetElapsedTime(startTimestamp, currentTimestamp); - _renderDuration.Record(duration.TotalMilliseconds, tags); + public void EventFailed(string? exceptionType, EventCallback callback, string? attributeName) + { + var receiverName = (callback.Receiver?.GetType() ?? callback.Delegate?.Target?.GetType())?.FullName; + var tags = new TagList + { + { "component.type", receiverName ?? "unknown" }, + { "attribute.name", attributeName ?? "unknown"}, + { "error.type", exceptionType ?? "unknown"} + }; + _eventException.Add(1, tags); + } + + public async Task CaptureEventFailedAsync(Task task, EventCallback callback, string? attributeName) + { + try + { + await task; + } + catch (Exception ex) + { + EventFailed(ex.GetType().Name, callback, attributeName); } } - private static TagList InitializeRequestTags(string componentType, TagList tags) + public void PropertiesFailed(string? exceptionType, string? componentType) { - tags.Add("component.type", componentType); - return tags; + var tags = new TagList + { + { "component.type", componentType ?? "unknown" }, + { "error.type", exceptionType ?? "unknown"} + }; + _parametersException.Add(1, tags); } - public bool IsDurationEnabled() => _renderDuration.Enabled; + public async Task CapturePropertiesFailedAsync(Task task, string? componentType) + { + try + { + await task; + } + catch (Exception ex) + { + PropertiesFailed(ex.GetType().Name, componentType); + } + } - public void Dispose() + public void BatchFailed(string? exceptionType) { - _meter.Dispose(); + var tags = new TagList + { + { "error.type", exceptionType ?? "unknown"} + }; + _batchException.Add(1, tags); } - private static bool TryAddTag(ref TagList tags, string name, object? value) + public async Task CaptureBatchFailedAsync(Task task) { - for (var i = 0; i < tags.Count; i++) + try { - if (tags[i].Key == name) - { - return false; - } + await task; } + catch (Exception ex) + { + BatchFailed(ex.GetType().Name); + } + } - tags.Add(new KeyValuePair(name, value)); - return true; + private static int BucketEditLength(int batchLength) + { + return batchLength switch + { + <= 1 => 1, + <= 2 => 2, + <= 5 => 5, + <= 10 => 10, + <= 50 => 50, + <= 100 => 100, + <= 500 => 500, + <= 1000 => 1000, + <= 10000 => 10000, + _ => 10001, + }; + } + + public void Dispose() + { + _meter.Dispose(); } } diff --git a/src/Components/Components/test/Rendering/RenderingMetricsTest.cs b/src/Components/Components/test/Rendering/RenderingMetricsTest.cs index 7339ebbf5dec..be4133ac9a9f 100644 --- a/src/Components/Components/test/Rendering/RenderingMetricsTest.cs +++ b/src/Components/Components/test/Rendering/RenderingMetricsTest.cs @@ -33,206 +33,367 @@ public void Constructor_CreatesMetersCorrectly() } [Fact] - public void RenderStart_IncreasesCounters() + public void EventDurationSync_RecordsDuration() { // Arrange var renderingMetrics = new RenderingMetrics(_meterFactory); - using var totalCounter = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.count"); - using var activeCounter = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.active_renders"); + using var eventSyncDurationCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.synchronous.duration"); - var componentType = "TestComponent"; + // Act + var startTime = Stopwatch.GetTimestamp(); + Thread.Sleep(10); // Add a small delay to ensure a measurable duration + renderingMetrics.EventDurationSync(startTime, "TestComponent", "OnClick"); + + // Assert + var measurements = eventSyncDurationCollector.GetMeasurementSnapshot(); + + Assert.Single(measurements); + Assert.True(measurements[0].Value > 0); + Assert.Equal("TestComponent", measurements[0].Tags["component.type"]); + Assert.Equal("OnClick", measurements[0].Tags["attribute.name"]); + } + + [Fact] + public async Task CaptureEventDurationAsync_RecordsDuration() + { + // Arrange + var renderingMetrics = new RenderingMetrics(_meterFactory); + using var eventAsyncDurationCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.asynchronous.duration"); // Act - renderingMetrics.RenderStart(componentType); + var startTime = Stopwatch.GetTimestamp(); + var task = Task.Delay(10); // Create a delay task + await renderingMetrics.CaptureEventDurationAsync(task, startTime, "TestComponent", "OnClickAsync"); // Assert - var totalMeasurements = totalCounter.GetMeasurementSnapshot(); - var activeMeasurements = activeCounter.GetMeasurementSnapshot(); + var measurements = eventAsyncDurationCollector.GetMeasurementSnapshot(); - Assert.Single(totalMeasurements); - Assert.Equal(1, totalMeasurements[0].Value); - Assert.Equal(componentType, totalMeasurements[0].Tags["component.type"]); + Assert.Single(measurements); + Assert.True(measurements[0].Value > 0); + Assert.Equal("TestComponent", measurements[0].Tags["component.type"]); + Assert.Equal("OnClickAsync", measurements[0].Tags["attribute.name"]); + } - Assert.Single(activeMeasurements); - Assert.Equal(1, activeMeasurements[0].Value); - Assert.Equal(componentType, activeMeasurements[0].Tags["component.type"]); + [Fact] + public void ParametersDurationSync_RecordsDuration() + { + // Arrange + var renderingMetrics = new RenderingMetrics(_meterFactory); + using var parametersSyncDurationCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.synchronous.duration"); + + // Act + var startTime = Stopwatch.GetTimestamp(); + Thread.Sleep(10); // Add a small delay to ensure a measurable duration + renderingMetrics.ParametersDurationSync(startTime, "TestComponent"); + + // Assert + var measurements = parametersSyncDurationCollector.GetMeasurementSnapshot(); + + Assert.Single(measurements); + Assert.True(measurements[0].Value > 0); + Assert.Equal("TestComponent", measurements[0].Tags["component.type"]); } [Fact] - public void RenderEnd_DecreasesActiveCounterAndRecordsDuration() + public async Task CaptureParametersDurationAsync_RecordsDuration() { // Arrange var renderingMetrics = new RenderingMetrics(_meterFactory); - using var activeCounter = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.active_renders"); - using var durationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.duration"); + using var parametersAsyncDurationCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.asynchronous.duration"); + + // Act + var startTime = Stopwatch.GetTimestamp(); + var task = Task.Delay(10); // Create a delay task + await renderingMetrics.CaptureParametersDurationAsync(task, startTime, "TestComponent"); + + // Assert + var measurements = parametersAsyncDurationCollector.GetMeasurementSnapshot(); - var componentType = "TestComponent"; + Assert.Single(measurements); + Assert.True(measurements[0].Value > 0); + Assert.Equal("TestComponent", measurements[0].Tags["component.type"]); + } + + [Fact] + public void DiffDuration_RecordsDuration() + { + // Arrange + var renderingMetrics = new RenderingMetrics(_meterFactory); + using var diffDurationCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.diff.duration"); // Act var startTime = Stopwatch.GetTimestamp(); Thread.Sleep(10); // Add a small delay to ensure a measurable duration - var endTime = Stopwatch.GetTimestamp(); - renderingMetrics.RenderEnd(componentType, null, startTime, endTime); + renderingMetrics.DiffDuration(startTime, "TestComponent", 5); // Assert - var activeMeasurements = activeCounter.GetMeasurementSnapshot(); - var durationMeasurements = durationCollector.GetMeasurementSnapshot(); + var measurements = diffDurationCollector.GetMeasurementSnapshot(); - Assert.Single(activeMeasurements); - Assert.Equal(-1, activeMeasurements[0].Value); - Assert.Equal(componentType, activeMeasurements[0].Tags["component.type"]); + Assert.Single(measurements); + Assert.True(measurements[0].Value > 0); + Assert.Equal("TestComponent", measurements[0].Tags["component.type"]); + Assert.Equal(5, measurements[0].Tags["diff.length.bucket"]); + } - Assert.Single(durationMeasurements); - Assert.True(durationMeasurements[0].Value > 0); - Assert.Equal(componentType, durationMeasurements[0].Tags["component.type"]); + [Fact] + public void BatchDuration_RecordsDuration() + { + // Arrange + var renderingMetrics = new RenderingMetrics(_meterFactory); + using var batchDurationCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.batch.duration"); + + // Act + var startTime = Stopwatch.GetTimestamp(); + Thread.Sleep(10); // Add a small delay to ensure a measurable duration + renderingMetrics.BatchDuration(startTime, 50); + + // Assert + var measurements = batchDurationCollector.GetMeasurementSnapshot(); + + Assert.Single(measurements); + Assert.True(measurements[0].Value > 0); + Assert.Equal(50, measurements[0].Tags["diff.length.bucket"]); } [Fact] - public void RenderEnd_AddsErrorTypeTag_WhenExceptionIsProvided() + public void EventFailed_RecordsException() { // Arrange var renderingMetrics = new RenderingMetrics(_meterFactory); - using var durationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.duration"); + using var eventExceptionCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.exception"); - var componentType = "TestComponent"; - var exception = new InvalidOperationException("Test exception"); + // Create a mock EventCallback + var callback = new EventCallback(new TestComponent(), (Action)(() => { })); // Act - var startTime = Stopwatch.GetTimestamp(); - Thread.Sleep(10); - var endTime = Stopwatch.GetTimestamp(); - renderingMetrics.RenderEnd(componentType, exception, startTime, endTime); + renderingMetrics.EventFailed("ArgumentException", callback, "OnClick"); // Assert - var durationMeasurements = durationCollector.GetMeasurementSnapshot(); + var measurements = eventExceptionCollector.GetMeasurementSnapshot(); - Assert.Single(durationMeasurements); - Assert.True(durationMeasurements[0].Value > 0); - Assert.Equal(componentType, durationMeasurements[0].Tags["component.type"]); - Assert.Equal(exception.GetType().FullName, durationMeasurements[0].Tags["error.type"]); + Assert.Single(measurements); + Assert.Equal(1, measurements[0].Value); + Assert.Equal("ArgumentException", measurements[0].Tags["error.type"]); + Assert.Equal("OnClick", measurements[0].Tags["attribute.name"]); + Assert.Contains("Microsoft.AspNetCore.Components.Rendering.RenderingMetricsTest+TestComponent", (string)measurements[0].Tags["component.type"]); } [Fact] - public void IsDurationEnabled_ReturnsMeterEnabledState() + public async Task CaptureEventFailedAsync_RecordsException() { // Arrange var renderingMetrics = new RenderingMetrics(_meterFactory); + using var eventExceptionCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.exception"); + + // Create a mock EventCallback + var callback = new EventCallback(new TestComponent(), (Action)(() => { })); + + // Create a task that throws an exception + var task = Task.FromException(new InvalidOperationException()); - // Create a collector to ensure the meter is enabled - using var durationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.duration"); + // Act + await renderingMetrics.CaptureEventFailedAsync(task, callback, "OnClickAsync"); + + // Assert + var measurements = eventExceptionCollector.GetMeasurementSnapshot(); - // Act & Assert - Assert.True(renderingMetrics.IsDurationEnabled()); + Assert.Single(measurements); + Assert.Equal(1, measurements[0].Value); + Assert.Equal("InvalidOperationException", measurements[0].Tags["error.type"]); + Assert.Equal("OnClickAsync", measurements[0].Tags["attribute.name"]); + Assert.Contains("Microsoft.AspNetCore.Components.Rendering.RenderingMetricsTest+TestComponent", (string)measurements[0].Tags["component.type"]); } [Fact] - public void FullRenderingLifecycle_RecordsAllMetricsCorrectly() + public void PropertiesFailed_RecordsException() { // Arrange var renderingMetrics = new RenderingMetrics(_meterFactory); - using var totalCounter = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.count"); - using var activeCounter = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.active_renders"); - using var durationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.duration"); + using var parametersExceptionCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.exception"); - var componentType = "TestComponent"; + // Act + renderingMetrics.PropertiesFailed("ArgumentException", "TestComponent"); - // Act - Simulating a full rendering lifecycle - var startTime = Stopwatch.GetTimestamp(); + // Assert + var measurements = parametersExceptionCollector.GetMeasurementSnapshot(); - // 1. Component render starts - renderingMetrics.RenderStart(componentType); + Assert.Single(measurements); + Assert.Equal(1, measurements[0].Value); + Assert.Equal("ArgumentException", measurements[0].Tags["error.type"]); + Assert.Equal("TestComponent", measurements[0].Tags["component.type"]); + } - // 2. Component render ends - Thread.Sleep(10); // Add a small delay to ensure a measurable duration - var endTime = Stopwatch.GetTimestamp(); - renderingMetrics.RenderEnd(componentType, null, startTime, endTime); + [Fact] + public async Task CapturePropertiesFailedAsync_RecordsException() + { + // Arrange + var renderingMetrics = new RenderingMetrics(_meterFactory); + using var parametersExceptionCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.exception"); + + // Create a task that throws an exception + var task = Task.FromException(new InvalidOperationException()); + + // Act + await renderingMetrics.CapturePropertiesFailedAsync(task, "TestComponent"); // Assert - var totalMeasurements = totalCounter.GetMeasurementSnapshot(); - var activeMeasurements = activeCounter.GetMeasurementSnapshot(); - var durationMeasurements = durationCollector.GetMeasurementSnapshot(); + var measurements = parametersExceptionCollector.GetMeasurementSnapshot(); - // Total render count should have 1 measurement with value 1 - Assert.Single(totalMeasurements); - Assert.Equal(1, totalMeasurements[0].Value); - Assert.Equal(componentType, totalMeasurements[0].Tags["component.type"]); + Assert.Single(measurements); + Assert.Equal(1, measurements[0].Value); + Assert.Equal("InvalidOperationException", measurements[0].Tags["error.type"]); + Assert.Equal("TestComponent", measurements[0].Tags["component.type"]); + } - // Active render count should have 2 measurements (1 for start, -1 for end) - Assert.Equal(2, activeMeasurements.Count); - Assert.Equal(1, activeMeasurements[0].Value); - Assert.Equal(-1, activeMeasurements[1].Value); - Assert.Equal(componentType, activeMeasurements[0].Tags["component.type"]); - Assert.Equal(componentType, activeMeasurements[1].Tags["component.type"]); + [Fact] + public void BatchFailed_RecordsException() + { + // Arrange + var renderingMetrics = new RenderingMetrics(_meterFactory); + using var batchExceptionCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.batch.exception"); + + // Act + renderingMetrics.BatchFailed("ArgumentException"); + + // Assert + var measurements = batchExceptionCollector.GetMeasurementSnapshot(); - // Duration should have 1 measurement with a positive value - Assert.Single(durationMeasurements); - Assert.True(durationMeasurements[0].Value > 0); - Assert.Equal(componentType, durationMeasurements[0].Tags["component.type"]); + Assert.Single(measurements); + Assert.Equal(1, measurements[0].Value); + Assert.Equal("ArgumentException", measurements[0].Tags["error.type"]); } [Fact] - public void MultipleRenders_TracksMetricsIndependently() + public async Task CaptureBatchFailedAsync_RecordsException() { // Arrange var renderingMetrics = new RenderingMetrics(_meterFactory); - using var totalCounter = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.count"); - using var activeCounter = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.active_renders"); - using var durationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.duration"); + using var batchExceptionCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.batch.exception"); - var componentType1 = "TestComponent1"; - var componentType2 = "TestComponent2"; + // Create a task that throws an exception + var task = Task.FromException(new InvalidOperationException()); // Act - // First component render - var startTime1 = Stopwatch.GetTimestamp(); - renderingMetrics.RenderStart(componentType1); + await renderingMetrics.CaptureBatchFailedAsync(task); - // Second component render starts while first is still rendering - var startTime2 = Stopwatch.GetTimestamp(); - renderingMetrics.RenderStart(componentType2); + // Assert + var measurements = batchExceptionCollector.GetMeasurementSnapshot(); - // First component render ends - Thread.Sleep(5); - var endTime1 = Stopwatch.GetTimestamp(); - renderingMetrics.RenderEnd(componentType1, null, startTime1, endTime1); + Assert.Single(measurements); + Assert.Equal(1, measurements[0].Value); + Assert.Equal("InvalidOperationException", measurements[0].Tags["error.type"]); + } - // Second component render ends - Thread.Sleep(5); - var endTime2 = Stopwatch.GetTimestamp(); - renderingMetrics.RenderEnd(componentType2, null, startTime2, endTime2); + [Fact] + public void EnabledProperties_ReflectMeterState() + { + // Arrange + var renderingMetrics = new RenderingMetrics(_meterFactory); + + // Create collectors to ensure the meters are enabled + using var eventSyncDurationCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.synchronous.duration"); + using var eventAsyncDurationCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.asynchronous.duration"); + using var eventExceptionCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.exception"); + using var parametersSyncDurationCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.synchronous.duration"); + using var parametersAsyncDurationCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.asynchronous.duration"); + using var parametersExceptionCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.exception"); + using var diffDurationCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.diff.duration"); + using var batchDurationCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.batch.duration"); + using var batchExceptionCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.batch.exception"); // Assert - var totalMeasurements = totalCounter.GetMeasurementSnapshot(); - var activeMeasurements = activeCounter.GetMeasurementSnapshot(); - var durationMeasurements = durationCollector.GetMeasurementSnapshot(); - - // Should have 2 total render counts (one for each component) - Assert.Equal(2, totalMeasurements.Count); - Assert.Contains(totalMeasurements, m => m.Value == 1 && m.Tags["component.type"] as string == componentType1); - Assert.Contains(totalMeasurements, m => m.Value == 1 && m.Tags["component.type"] as string == componentType2); - - // Should have 4 active render counts (start and end for each component) - Assert.Equal(4, activeMeasurements.Count); - Assert.Contains(activeMeasurements, m => m.Value == 1 && m.Tags["component.type"] as string == componentType1); - Assert.Contains(activeMeasurements, m => m.Value == 1 && m.Tags["component.type"] as string == componentType2); - Assert.Contains(activeMeasurements, m => m.Value == -1 && m.Tags["component.type"] as string == componentType1); - Assert.Contains(activeMeasurements, m => m.Value == -1 && m.Tags["component.type"] as string == componentType2); - - // Should have 2 duration measurements (one for each component) - Assert.Equal(2, durationMeasurements.Count); - Assert.Contains(durationMeasurements, m => m.Value > 0 && m.Tags["component.type"] as string == componentType1); - Assert.Contains(durationMeasurements, m => m.Value > 0 && m.Tags["component.type"] as string == componentType2); + Assert.True(renderingMetrics.IsEventDurationEnabled); + Assert.True(renderingMetrics.IsEventExceptionEnabled); + Assert.True(renderingMetrics.IsStateDurationEnabled); + Assert.True(renderingMetrics.IsStateExceptionEnabled); + Assert.True(renderingMetrics.IsDiffDurationEnabled); + Assert.True(renderingMetrics.IsBatchDurationEnabled); + Assert.True(renderingMetrics.IsBatchExceptionEnabled); + } + + [Fact] + public void BucketEditLength_ReturnsCorrectBucket() + { + // Arrange + var renderingMetrics = new RenderingMetrics(_meterFactory); + using var diffDurationCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.diff.duration"); + + // Act & Assert - Test different diff lengths + var startTime = Stopwatch.GetTimestamp(); + + // Test each bucket boundary + renderingMetrics.DiffDuration(startTime, "Component", 1); + renderingMetrics.DiffDuration(startTime, "Component", 2); + renderingMetrics.DiffDuration(startTime, "Component", 5); + renderingMetrics.DiffDuration(startTime, "Component", 10); + renderingMetrics.DiffDuration(startTime, "Component", 50); + renderingMetrics.DiffDuration(startTime, "Component", 100); + renderingMetrics.DiffDuration(startTime, "Component", 500); + renderingMetrics.DiffDuration(startTime, "Component", 1000); + renderingMetrics.DiffDuration(startTime, "Component", 10000); + renderingMetrics.DiffDuration(startTime, "Component", 20000); // Should be 10001 + + // Assert + var measurements = diffDurationCollector.GetMeasurementSnapshot(); + + Assert.Equal(10, measurements.Count); + Assert.Equal(1, measurements[0].Tags["diff.length.bucket"]); + Assert.Equal(2, measurements[1].Tags["diff.length.bucket"]); + Assert.Equal(5, measurements[2].Tags["diff.length.bucket"]); + Assert.Equal(10, measurements[3].Tags["diff.length.bucket"]); + Assert.Equal(50, measurements[4].Tags["diff.length.bucket"]); + Assert.Equal(100, measurements[5].Tags["diff.length.bucket"]); + Assert.Equal(500, measurements[6].Tags["diff.length.bucket"]); + Assert.Equal(1000, measurements[7].Tags["diff.length.bucket"]); + Assert.Equal(10000, measurements[8].Tags["diff.length.bucket"]); + Assert.Equal(10001, measurements[9].Tags["diff.length.bucket"]); + } + + [Fact] + public void Dispose_DisposesUnderlyingMeter() + { + // This test verifies that the meter is disposed when the metrics instance is disposed + // This is a bit tricky to test directly, so we'll use an indirect approach + + // Arrange + var renderingMetrics = new RenderingMetrics(_meterFactory); + + // Act + renderingMetrics.Dispose(); + + // Try to use the disposed meter - this should not throw since TestMeterFactory + // doesn't actually dispose the meter in test contexts + var startTime = Stopwatch.GetTimestamp(); + renderingMetrics.EventDurationSync(startTime, "TestComponent", "OnClick"); + } + + // Helper class for mock components + public class TestComponent : IComponent, IHandleEvent + { + public void Attach(RenderHandle renderHandle) { } + public Task HandleEventAsync(EventCallbackWorkItem item, object arg) => Task.CompletedTask; + public Task SetParametersAsync(ParameterView parameters) => Task.CompletedTask; } } diff --git a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs index 302dec7dcb16..249ee69fccd3 100644 --- a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs +++ b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs @@ -76,6 +76,8 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection services.TryAddScoped(); + RenderingMetricsServiceCollectionExtensions.AddRenderingMetrics(services); + // Form handling services.AddSupplyValueFromFormProvider(); services.TryAddScoped(); diff --git a/src/Components/Server/src/Circuits/CircuitHost.cs b/src/Components/Server/src/Circuits/CircuitHost.cs index b8bc2b05e158..ab461d8b62a2 100644 --- a/src/Components/Server/src/Circuits/CircuitHost.cs +++ b/src/Components/Server/src/Circuits/CircuitHost.cs @@ -120,6 +120,8 @@ public Task InitializeAsync(ProtectedPrerenderComponentApplicationStore store, C { _initialized = true; // We're ready to accept incoming JSInterop calls from here on + _startTime = (_circuitMetrics != null && _circuitMetrics.IsDurationEnabled()) ? Stopwatch.GetTimestamp() : 0; + // We only run the handlers in case we are in a Blazor Server scenario, which renders // the components inmediately during start. // On Blazor Web scenarios we delay running these handlers until the first UpdateRootComponents call @@ -235,7 +237,6 @@ await Renderer.Dispatcher.InvokeAsync(async () => private async Task OnCircuitOpenedAsync(CancellationToken cancellationToken) { Log.CircuitOpened(_logger, CircuitId); - _startTime = (_circuitMetrics != null && _circuitMetrics.IsDurationEnabled()) ? Stopwatch.GetTimestamp() : 0; _circuitMetrics?.OnCircuitOpened(); Renderer.Dispatcher.AssertAccess(); diff --git a/src/Components/Server/src/Circuits/CircuitMetrics.cs b/src/Components/Server/src/Circuits/CircuitMetrics.cs index fb772c119f51..da7b8e9d297b 100644 --- a/src/Components/Server/src/Circuits/CircuitMetrics.cs +++ b/src/Components/Server/src/Circuits/CircuitMetrics.cs @@ -26,7 +26,7 @@ public CircuitMetrics(IMeterFactory meterFactory) _circuitTotalCounter = _meter.CreateCounter( "aspnetcore.components.circuits.count", unit: "{circuits}", - description: "Number of active circuits."); + description: "Total number of circuits."); _circuitActiveCounter = _meter.CreateUpDownCounter( "aspnetcore.components.circuits.active_circuits", @@ -47,57 +47,48 @@ public CircuitMetrics(IMeterFactory meterFactory) public void OnCircuitOpened() { - var tags = new TagList(); - if (_circuitActiveCounter.Enabled) { - _circuitActiveCounter.Add(1, tags); + _circuitActiveCounter.Add(1); } if (_circuitTotalCounter.Enabled) { - _circuitTotalCounter.Add(1, tags); + _circuitTotalCounter.Add(1); } } public void OnConnectionUp() { - var tags = new TagList(); - if (_circuitConnectedCounter.Enabled) { - _circuitConnectedCounter.Add(1, tags); + _circuitConnectedCounter.Add(1); } } public void OnConnectionDown() { - var tags = new TagList(); - if (_circuitConnectedCounter.Enabled) { - _circuitConnectedCounter.Add(-1, tags); + _circuitConnectedCounter.Add(-1); } } public void OnCircuitDown(long startTimestamp, long currentTimestamp) { - // Tags must match request start. - var tags = new TagList(); - if (_circuitActiveCounter.Enabled) { - _circuitActiveCounter.Add(-1, tags); + _circuitActiveCounter.Add(-1); } if (_circuitConnectedCounter.Enabled) { - _circuitConnectedCounter.Add(-1, tags); + _circuitConnectedCounter.Add(-1); } if (_circuitDuration.Enabled) { var duration = Stopwatch.GetElapsedTime(startTimestamp, currentTimestamp); - _circuitDuration.Record(duration.TotalSeconds, tags); + _circuitDuration.Record(duration.TotalSeconds); } } diff --git a/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs b/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs index 08195b0218c7..4c6eb34d27f6 100644 --- a/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs +++ b/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs @@ -2,7 +2,6 @@ // 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; @@ -62,11 +61,7 @@ public static IServerSideBlazorBuilder AddServerSideBlazor(this IServiceCollecti // 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(); - return meterFactory != null ? new CircuitMetrics(meterFactory) : null; - }); + services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); diff --git a/src/Components/Server/test/Circuits/CircuitMetricsTest.cs b/src/Components/Server/test/Circuits/CircuitMetricsTest.cs index 770125996634..9124d5d64294 100644 --- a/src/Components/Server/test/Circuits/CircuitMetricsTest.cs +++ b/src/Components/Server/test/Circuits/CircuitMetricsTest.cs @@ -95,7 +95,7 @@ public void OnConnectionDown_DecreasesConnectedCounter() } [Fact] - public void OnCircuitDown_UpdatesCountersAndRecordsDuration() + public async Task OnCircuitDown_UpdatesCountersAndRecordsDuration() { // Arrange var circuitMetrics = new CircuitMetrics(_meterFactory); @@ -108,7 +108,7 @@ public void OnCircuitDown_UpdatesCountersAndRecordsDuration() // Act var startTime = Stopwatch.GetTimestamp(); - Thread.Sleep(10); // Add a small delay to ensure a measurable duration + await Task.Delay(10); // Add a small delay to ensure a measurable duration var endTime = Stopwatch.GetTimestamp(); circuitMetrics.OnCircuitDown(startTime, endTime); diff --git a/src/Components/Web/src/HtmlRendering/StaticHtmlRenderer.cs b/src/Components/Web/src/HtmlRendering/StaticHtmlRenderer.cs index 104dcb930d66..a77b3e98c4ec 100644 --- a/src/Components/Web/src/HtmlRendering/StaticHtmlRenderer.cs +++ b/src/Components/Web/src/HtmlRendering/StaticHtmlRenderer.cs @@ -21,7 +21,6 @@ public partial class StaticHtmlRenderer : Renderer { private static readonly RendererInfo _componentPlatform = new RendererInfo("Static", isInteractive: false); - private static readonly Task CanceledRenderTask = Task.FromCanceled(new CancellationToken(canceled: true)); private readonly NavigationManager? _navigationManager; /// diff --git a/src/Shared/Metrics/MetricsConstants.cs b/src/Shared/Metrics/MetricsConstants.cs index ff64c6fefcad..cdb338f1d7a0 100644 --- a/src/Shared/Metrics/MetricsConstants.cs +++ b/src/Shared/Metrics/MetricsConstants.cs @@ -11,6 +11,6 @@ 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/signalR sessions, which can last a long time. - public static readonly IReadOnlyList VeryLongSecondsBucketBoundaries = [0.5, 1, 2, 5, 10, 30, 60, 120, 300, 600, 1500, 60*60, 2 * 60 * 60, 4 * 60 * 60]; + // For blazor circuit sessions, which can last a long time. + public static readonly IReadOnlyList VeryLongSecondsBucketBoundaries = [1, 10, 30, 1 * 60, 2 * 60, 3 * 60, 4 * 60, 5 * 60, 6 * 60, 7 * 60, 8 * 60, 9 * 60, 10 * 60, 1 * 60 * 60, 2 * 60 * 60, 3 * 60 * 60, 6 * 60 * 60, 12 * 60 * 60, 24 * 60 * 60]; } From 0bcd459a9a86b1bd19520cfa4b1f08d6390a4520 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 24 Apr 2025 09:07:38 +0200 Subject: [PATCH 2/9] more --- ...eringMetricsServiceCollectionExtensions.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs b/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs index da1a539278e9..875ba6998a65 100644 --- a/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs +++ b/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs @@ -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; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -22,10 +23,26 @@ public static IServiceCollection AddRenderingMetrics( { if (RenderingMetrics.IsMetricsSupported) { - services.AddMetrics(); + // do not register IConfigureOptions multiple times + if (!IsMeterFactoryRegistered(services)) + { + services.AddMetrics(); + } services.TryAddSingleton(); } return services; } + + private static bool IsMeterFactoryRegistered(IServiceCollection services) + { + foreach (var service in services) + { + if (service.ServiceType == typeof(IMeterFactory)) + { + return true; + } + } + return false; + } } From 2557f08a5f2dbfa216173d02e1ca75b03a1342ae Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 24 Apr 2025 12:56:56 +0200 Subject: [PATCH 3/9] - remove FeatureSwitchDefinition for now - add tracing --- .../Components/src/PublicAPI.Unshipped.txt | 1 + ...eringMetricsServiceCollectionExtensions.cs | 24 +++++--- .../Components/src/RenderTree/Renderer.cs | 54 ++++++++++++------ .../src/Rendering/ComponentState.cs | 14 ++--- .../src/Rendering/RenderingActivitySource.cs | 55 +++++++++++++++++++ .../src/Rendering/RenderingMetrics.cs | 31 ++++------- .../test/Rendering/RenderingMetricsTest.cs | 7 ++- ...orComponentsServiceCollectionExtensions.cs | 1 + 8 files changed, 134 insertions(+), 53 deletions(-) create mode 100644 src/Components/Components/src/Rendering/RenderingActivitySource.cs diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 477cb88e7d85..c372bbf750c2 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -15,5 +15,6 @@ Microsoft.AspNetCore.Components.SupplyParameterFromPersistentComponentStateAttri Microsoft.Extensions.DependencyInjection.SupplyParameterFromPersistentComponentStateProviderServiceCollectionExtensions static Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponentStateServiceCollectionExtensions.AddPersistentServiceRegistration(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.AspNetCore.Components.IComponentRenderMode! componentRenderMode) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.AspNetCore.Components.Infrastructure.RenderingMetricsServiceCollectionExtensions.AddRenderingMetrics(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.AspNetCore.Components.Infrastructure.RenderingMetricsServiceCollectionExtensions.AddRenderingTracing(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Extensions.DependencyInjection.SupplyParameterFromPersistentComponentStateProviderServiceCollectionExtensions.AddSupplyValueFromPersistentComponentStateProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.SignalRendererToFinishRendering() -> void \ No newline at end of file diff --git a/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs b/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs index 875ba6998a65..b40b3adf4de3 100644 --- a/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs +++ b/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs @@ -21,15 +21,25 @@ public static class RenderingMetricsServiceCollectionExtensions public static IServiceCollection AddRenderingMetrics( IServiceCollection services) { - if (RenderingMetrics.IsMetricsSupported) + // do not register IConfigureOptions multiple times + if (!IsMeterFactoryRegistered(services)) { - // do not register IConfigureOptions multiple times - if (!IsMeterFactoryRegistered(services)) - { - services.AddMetrics(); - } - services.TryAddSingleton(); + services.AddMetrics(); } + services.TryAddSingleton(); + + return services; + } + + /// + /// Registers component rendering traces + /// + /// The . + /// The . + public static IServiceCollection AddRenderingTracing( + IServiceCollection services) + { + services.TryAddSingleton(); return services; } diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index f616da46b94f..1e4036042417 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -36,6 +36,7 @@ public abstract partial class Renderer : IDisposable, IAsyncDisposable private readonly ILogger _logger; private readonly ComponentFactory _componentFactory; private readonly RenderingMetrics? _renderingMetrics; + private readonly RenderingActivitySource? _renderingActivitySource; private Dictionary? _rootComponentsLatestParameters; private Task? _ongoingQuiescenceTask; @@ -93,17 +94,16 @@ public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, // logger name in here as a string literal. _logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Components.RenderTree.Renderer"); _componentFactory = new ComponentFactory(componentActivator, this); - if (RenderingMetrics.IsMetricsSupported) - { - _renderingMetrics = serviceProvider.GetService(); - } + _renderingMetrics = serviceProvider.GetService(); + _renderingActivitySource = serviceProvider.GetService(); ServiceProviderCascadingValueSuppliers = serviceProvider.GetService() is null ? Array.Empty() : serviceProvider.GetServices().ToArray(); } - internal RenderingMetrics? RenderingMetrics => RenderingMetrics.IsMetricsSupported ? _renderingMetrics : null; + internal RenderingMetrics? RenderingMetrics => _renderingMetrics; + internal RenderingActivitySource? RenderingActivitySource => _renderingActivitySource; internal ICascadingValueSupplier[] ServiceProviderCascadingValueSuppliers { get; } @@ -440,8 +440,6 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie { Dispatcher.AssertAccess(); - var eventStartTimestamp = RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsEventDurationEnabled ? Stopwatch.GetTimestamp() : 0; - if (waitForQuiescence) { _pendingTasks ??= new(); @@ -449,6 +447,17 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie var (renderedByComponentId, callback, attributeName) = GetRequiredEventBindingEntry(eventHandlerId); + // collect trace + Activity? activity = null; + if (RenderingActivitySource != null) + { + var receiverName = (callback.Receiver?.GetType() ?? callback.Delegate.Target?.GetType())?.FullName; + var methodName = callback.Delegate.Method?.Name; + activity = RenderingActivitySource.StartEventActivity(receiverName, methodName, attributeName, null); + } + + var eventStartTimestamp = RenderingMetrics != null && RenderingMetrics.IsEventDurationEnabled ? Stopwatch.GetTimestamp() : 0; + // If this event attribute was rendered by a component that's since been disposed, don't dispatch the event at all. // This can occur because event handler disposal is deferred, so event handler IDs can outlive their components. // The reason the following check is based on "which component rendered this frame" and not on "which component @@ -491,23 +500,36 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie task = callback.InvokeAsync(eventArgs); // collect metrics - if (RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsEventDurationEnabled) + if (RenderingMetrics != null && RenderingMetrics.IsEventDurationEnabled) { var receiverName = (callback.Receiver?.GetType() ?? callback.Delegate.Target?.GetType())?.FullName; - RenderingMetrics.EventDurationSync(eventStartTimestamp, receiverName, attributeName); - _ = RenderingMetrics.CaptureEventDurationAsync(task, eventStartTimestamp, receiverName, attributeName); + var methodName = callback.Delegate.Method?.Name; + RenderingMetrics.EventDurationSync(eventStartTimestamp, receiverName, methodName, attributeName); + _ = RenderingMetrics.CaptureEventDurationAsync(task, eventStartTimestamp, receiverName, methodName, attributeName); } - if (RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsEventExceptionEnabled) + if (RenderingMetrics != null && RenderingMetrics.IsEventExceptionEnabled) { _ = RenderingMetrics.CaptureEventFailedAsync(task, callback, attributeName); } + + // stop activity/trace + if (RenderingActivitySource != null && activity != null) + { + _ = RenderingActivitySource.CaptureEventStopAsync(task, activity); + } } catch (Exception e) { - if (RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsEventExceptionEnabled) + if (RenderingMetrics != null && RenderingMetrics.IsEventExceptionEnabled) { RenderingMetrics.EventFailed(e.GetType().FullName, callback, attributeName); } + + if (RenderingActivitySource != null && activity != null) + { + RenderingActivitySource.FailEventActivity(activity, e); + } + HandleExceptionViaErrorBoundary(e, receiverComponentState); return Task.CompletedTask; } @@ -795,7 +817,7 @@ private void ProcessRenderQueue() _isBatchInProgress = true; var updateDisplayTask = Task.CompletedTask; - var batchStartTimestamp = RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsBatchDurationEnabled ? Stopwatch.GetTimestamp() : 0; + var batchStartTimestamp = RenderingMetrics != null && RenderingMetrics.IsBatchDurationEnabled ? Stopwatch.GetTimestamp() : 0; try { @@ -828,18 +850,18 @@ private void ProcessRenderQueue() // if there is async work to be done. _ = InvokeRenderCompletedCalls(batch.UpdatedComponents, updateDisplayTask); - if (RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsBatchDurationEnabled) + if (RenderingMetrics != null && RenderingMetrics.IsBatchDurationEnabled) { _renderingMetrics.BatchDuration(batchStartTimestamp, batch.UpdatedComponents.Count); } - if (RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsBatchExceptionEnabled) + if (RenderingMetrics != null && RenderingMetrics.IsBatchExceptionEnabled) { _ = _renderingMetrics.CaptureBatchFailedAsync(updateDisplayTask); } } catch (Exception e) { - if (RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsBatchExceptionEnabled) + if (RenderingMetrics != null && RenderingMetrics.IsBatchExceptionEnabled) { _renderingMetrics.BatchFailed(e.GetType().Name); } diff --git a/src/Components/Components/src/Rendering/ComponentState.cs b/src/Components/Components/src/Rendering/ComponentState.cs index 7b80404fb4d3..40ad68733248 100644 --- a/src/Components/Components/src/Rendering/ComponentState.cs +++ b/src/Components/Components/src/Rendering/ComponentState.cs @@ -53,7 +53,7 @@ public ComponentState(Renderer renderer, int componentId, IComponent component, _hasAnyCascadingParameterSubscriptions = AddCascadingParameterSubscriptions(); } - if (RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && (_renderer.RenderingMetrics.IsDiffDurationEnabled || _renderer.RenderingMetrics.IsStateDurationEnabled || _renderer.RenderingMetrics.IsStateExceptionEnabled)) + if (_renderer.RenderingMetrics != null && (_renderer.RenderingMetrics.IsDiffDurationEnabled || _renderer.RenderingMetrics.IsStateDurationEnabled || _renderer.RenderingMetrics.IsStateExceptionEnabled)) { _componentTypeName = component.GetType().FullName; } @@ -108,7 +108,7 @@ internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment re _nextRenderTree.Clear(); - var diffStartTimestamp = RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsDiffDurationEnabled ? Stopwatch.GetTimestamp() : 0; + var diffStartTimestamp = _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsDiffDurationEnabled ? Stopwatch.GetTimestamp() : 0; try { renderFragment(_nextRenderTree); @@ -139,7 +139,7 @@ internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment re batchBuilder.UpdatedComponentDiffs.Append(diff); batchBuilder.InvalidateParameterViews(); - if (RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsDiffDurationEnabled) + if (_renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsDiffDurationEnabled) { _renderer.RenderingMetrics.DiffDuration(diffStartTimestamp, _componentTypeName, batchBuilder.EditsBuffer.Count - startCount); } @@ -249,24 +249,24 @@ private void SupplyCombinedParameters(ParameterView directAndCascadingParameters Task setParametersAsyncTask; try { - var stateStartTimestamp = RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateDurationEnabled ? Stopwatch.GetTimestamp() : 0; + var stateStartTimestamp = _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateDurationEnabled ? Stopwatch.GetTimestamp() : 0; setParametersAsyncTask = Component.SetParametersAsync(directAndCascadingParameters); // collect metrics - if (RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateDurationEnabled) + if (_renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateDurationEnabled) { _renderer.RenderingMetrics.ParametersDurationSync(stateStartTimestamp, _componentTypeName); _ = _renderer.RenderingMetrics.CaptureParametersDurationAsync(setParametersAsyncTask, stateStartTimestamp, _componentTypeName); } - if (RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateExceptionEnabled) + if (_renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateExceptionEnabled) { _ = _renderer.RenderingMetrics.CapturePropertiesFailedAsync(setParametersAsyncTask, _componentTypeName); } } catch (Exception ex) { - if (RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateExceptionEnabled) + if (_renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateExceptionEnabled) { _renderer.RenderingMetrics.PropertiesFailed(ex.GetType().FullName, _componentTypeName); } diff --git a/src/Components/Components/src/Rendering/RenderingActivitySource.cs b/src/Components/Components/src/Rendering/RenderingActivitySource.cs new file mode 100644 index 000000000000..1471d3b7d722 --- /dev/null +++ b/src/Components/Components/src/Rendering/RenderingActivitySource.cs @@ -0,0 +1,55 @@ +// 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; + +namespace Microsoft.AspNetCore.Components.Rendering; +internal class RenderingActivitySource +{ + internal const string Name = "Microsoft.AspNetCore.Components.Rendering"; + internal const string OnEventName = $"{Name}.OnEvent"; + + public ActivitySource ActivitySource { get; } = new ActivitySource(Name); + + + public Activity? StartEventActivity(string? componentType, string? methodName, string? attributeName, Activity? linkedActivity) + { + IEnumerable> tags = + [ + new("component.type", componentType ?? "unknown"), + new("component.method", methodName ?? "unknown"), + new("attribute.name", attributeName ?? "unknown"), + ]; + IEnumerable? links = (linkedActivity is not null) ? [new ActivityLink(linkedActivity.Context)] : null; + + var activity = ActivitySource.CreateActivity(OnEventName, ActivityKind.Server, parentId: null, tags, links); + if (activity is not null) + { + activity.DisplayName = $"{componentType ?? "unknown"}/{methodName ?? "unknown"}/{attributeName ?? "unknown"}"; + activity.Start(); + } + return activity; + } + public static void FailEventActivity(Activity activity, Exception ex) + { + if (!activity.IsStopped) + { + activity.SetTag("error.type", ex.GetType().FullName); + activity.SetStatus(ActivityStatusCode.Error); + activity.Stop(); + } + } + + public static async Task CaptureEventStopAsync(Task task, Activity activity) + { + try + { + await task; + activity.Stop(); + } + catch (Exception ex) + { + FailEventActivity(activity, ex); + } + } +} diff --git a/src/Components/Components/src/Rendering/RenderingMetrics.cs b/src/Components/Components/src/Rendering/RenderingMetrics.cs index 6c0f9f918053..9e95487c502e 100644 --- a/src/Components/Components/src/Rendering/RenderingMetrics.cs +++ b/src/Components/Components/src/Rendering/RenderingMetrics.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; using Microsoft.AspNetCore.Http; @@ -26,29 +25,19 @@ internal sealed class RenderingMetrics : IDisposable private readonly Histogram _batchDuration; private readonly Counter _batchException; - [FeatureSwitchDefinition("System.Diagnostics.Metrics.Meter.IsSupported")] - public static bool IsMetricsSupported { get; } = InitializeIsMetricsSupported(); - private static bool InitializeIsMetricsSupported() => AppContext.TryGetSwitch("System.Diagnostics.Metrics.Meter.IsSupported", out bool isSupported) ? isSupported : true; + public bool IsEventDurationEnabled => _eventSyncDuration.Enabled || _eventAsyncDuration.Enabled; + public bool IsEventExceptionEnabled => _eventException.Enabled; - public bool IsEventDurationEnabled => IsMetricsSupported && (_eventSyncDuration.Enabled || _eventAsyncDuration.Enabled); - public bool IsEventExceptionEnabled => IsMetricsSupported && _eventException.Enabled; + public bool IsStateDurationEnabled => _parametersSyncDuration.Enabled || _parametersAsyncDuration.Enabled; + public bool IsStateExceptionEnabled => _parametersException.Enabled; - public bool IsStateDurationEnabled => IsMetricsSupported && (_parametersSyncDuration.Enabled || _parametersAsyncDuration.Enabled); - public bool IsStateExceptionEnabled => IsMetricsSupported && _parametersException.Enabled; + public bool IsDiffDurationEnabled => _diffDuration.Enabled; - public bool IsDiffDurationEnabled => IsMetricsSupported && _diffDuration.Enabled; - - public bool IsBatchDurationEnabled => IsMetricsSupported && _batchDuration.Enabled; - public bool IsBatchExceptionEnabled => IsMetricsSupported && _batchException.Enabled; + public bool IsBatchDurationEnabled => _batchDuration.Enabled; + public bool IsBatchExceptionEnabled => _batchException.Enabled; public RenderingMetrics(IMeterFactory meterFactory) { - if (!IsMetricsSupported) - { - // TryAddSingleton prevents trimming constructors, so we trim constructor this way - throw new NotSupportedException("Metrics are not supported in this environment."); - } - Debug.Assert(meterFactory != null); _meter = meterFactory.Create(MeterName); @@ -105,11 +94,12 @@ public RenderingMetrics(IMeterFactory meterFactory) description: "Total number of exceptions during batch rendering."); } - public void EventDurationSync(long startTimestamp, string? componentType, string? attributeName) + public void EventDurationSync(long startTimestamp, string? componentType, string? methodName, string? attributeName) { var tags = new TagList { { "component.type", componentType ?? "unknown" }, + { "component.method", methodName ?? "unknown" }, { "attribute.name", attributeName ?? "unknown"} }; @@ -117,7 +107,7 @@ public void EventDurationSync(long startTimestamp, string? componentType, string _eventSyncDuration.Record(duration.TotalSeconds, tags); } - public async Task CaptureEventDurationAsync(Task task, long startTimestamp, string? componentType, string? attributeName) + public async Task CaptureEventDurationAsync(Task task, long startTimestamp, string? componentType, string? methodName, string? attributeName) { try { @@ -126,6 +116,7 @@ public async Task CaptureEventDurationAsync(Task task, long startTimestamp, stri var tags = new TagList { { "component.type", componentType ?? "unknown" }, + { "component.method", methodName ?? "unknown" }, { "attribute.name", attributeName ?? "unknown" } }; diff --git a/src/Components/Components/test/Rendering/RenderingMetricsTest.cs b/src/Components/Components/test/Rendering/RenderingMetricsTest.cs index be4133ac9a9f..f1d7762d11ff 100644 --- a/src/Components/Components/test/Rendering/RenderingMetricsTest.cs +++ b/src/Components/Components/test/Rendering/RenderingMetricsTest.cs @@ -43,7 +43,7 @@ public void EventDurationSync_RecordsDuration() // Act var startTime = Stopwatch.GetTimestamp(); Thread.Sleep(10); // Add a small delay to ensure a measurable duration - renderingMetrics.EventDurationSync(startTime, "TestComponent", "OnClick"); + renderingMetrics.EventDurationSync(startTime, "TestComponent", "MyMethod", "OnClick"); // Assert var measurements = eventSyncDurationCollector.GetMeasurementSnapshot(); @@ -65,7 +65,7 @@ public async Task CaptureEventDurationAsync_RecordsDuration() // Act var startTime = Stopwatch.GetTimestamp(); var task = Task.Delay(10); // Create a delay task - await renderingMetrics.CaptureEventDurationAsync(task, startTime, "TestComponent", "OnClickAsync"); + await renderingMetrics.CaptureEventDurationAsync(task, startTime, "TestComponent", "MyMethod", "OnClickAsync"); // Assert var measurements = eventAsyncDurationCollector.GetMeasurementSnapshot(); @@ -74,6 +74,7 @@ public async Task CaptureEventDurationAsync_RecordsDuration() Assert.True(measurements[0].Value > 0); Assert.Equal("TestComponent", measurements[0].Tags["component.type"]); Assert.Equal("OnClickAsync", measurements[0].Tags["attribute.name"]); + Assert.Equal("MyMethod", measurements[0].Tags["component.method"]); } [Fact] @@ -386,7 +387,7 @@ public void Dispose_DisposesUnderlyingMeter() // Try to use the disposed meter - this should not throw since TestMeterFactory // doesn't actually dispose the meter in test contexts var startTime = Stopwatch.GetTimestamp(); - renderingMetrics.EventDurationSync(startTime, "TestComponent", "OnClick"); + renderingMetrics.EventDurationSync(startTime, "TestComponent", "MyMethod", "OnClick"); } // Helper class for mock components diff --git a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs index 249ee69fccd3..4618ab6dc7b5 100644 --- a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs +++ b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs @@ -77,6 +77,7 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection services.TryAddScoped(); RenderingMetricsServiceCollectionExtensions.AddRenderingMetrics(services); + RenderingMetricsServiceCollectionExtensions.AddRenderingTracing(services); // Form handling services.AddSupplyValueFromFormProvider(); From 4c7549527014d691693ef12275ad34e8e7dcdfb7 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 24 Apr 2025 13:09:19 +0200 Subject: [PATCH 4/9] whitespace --- .../Components/src/Rendering/RenderingActivitySource.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Components/Components/src/Rendering/RenderingActivitySource.cs b/src/Components/Components/src/Rendering/RenderingActivitySource.cs index 1471d3b7d722..af83cb7e4381 100644 --- a/src/Components/Components/src/Rendering/RenderingActivitySource.cs +++ b/src/Components/Components/src/Rendering/RenderingActivitySource.cs @@ -11,7 +11,6 @@ internal class RenderingActivitySource public ActivitySource ActivitySource { get; } = new ActivitySource(Name); - public Activity? StartEventActivity(string? componentType, string? methodName, string? attributeName, Activity? linkedActivity) { IEnumerable> tags = From 0f3d48a4d6c30cf3da7d7d00b90edf94ab4365a6 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 24 Apr 2025 18:35:42 +0200 Subject: [PATCH 5/9] navigation draft --- .../src/ComponentsActivitySource.cs | 102 ++++++++++++ ...nderingMetrics.cs => ComponentsMetrics.cs} | 41 +++-- .../Components/src/PublicAPI.Unshipped.txt | 6 +- ...eringMetricsServiceCollectionExtensions.cs | 11 +- src/Components/Components/src/RenderHandle.cs | 3 + .../Components/src/RenderTree/Renderer.cs | 55 +++---- .../src/Rendering/ComponentState.cs | 24 +-- .../src/Rendering/RenderingActivitySource.cs | 54 ------- .../Components/src/Routing/Router.cs | 17 ++ .../test/Rendering/RenderingMetricsTest.cs | 148 +++++++++--------- ...orComponentsServiceCollectionExtensions.cs | 4 +- .../src/RazorComponentEndpointInvoker.cs | 2 + 12 files changed, 279 insertions(+), 188 deletions(-) create mode 100644 src/Components/Components/src/ComponentsActivitySource.cs rename src/Components/Components/src/{Rendering/RenderingMetrics.cs => ComponentsMetrics.cs} (88%) delete mode 100644 src/Components/Components/src/Rendering/RenderingActivitySource.cs diff --git a/src/Components/Components/src/ComponentsActivitySource.cs b/src/Components/Components/src/ComponentsActivitySource.cs new file mode 100644 index 000000000000..2a314760cf86 --- /dev/null +++ b/src/Components/Components/src/ComponentsActivitySource.cs @@ -0,0 +1,102 @@ +// 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; + +namespace Microsoft.AspNetCore.Components; + +/// +/// This is instance scoped per renderer +/// +internal class ComponentsActivitySource +{ + internal const string Name = "Microsoft.AspNetCore.Components"; + internal const string OnEventName = $"{Name}.OnEvent"; + internal const string OnNavigationName = $"{Name}.OnNavigation"; + + public static ActivitySource ActivitySource { get; } = new ActivitySource(Name); + + private Activity? _routeActivity; + + public void StartRouteActivity(string componentType, string route) + { + StopRouteActivity(); + + IEnumerable> tags = + [ + new("component.type", componentType ?? "unknown"), + new("route", route ?? "unknown"), + ]; + var parentActivity = Activity.Current; + IEnumerable? links = parentActivity is not null ? [new ActivityLink(parentActivity.Context)] : null; + + var activity = ActivitySource.CreateActivity(OnEventName, ActivityKind.Server, parentId: null, tags, links); + if (activity is not null) + { + activity.DisplayName = $"NAVIGATE {route ?? "unknown"} -> {componentType ?? "unknown"}"; + activity.Start(); + _routeActivity = activity; + } + } + + public void StopRouteActivity() + { + if (_routeActivity != null) + { + _routeActivity.Stop(); + _routeActivity = null; + return; + } + } + + public Activity? StartEventActivity(string? componentType, string? methodName, string? attributeName) + { + IEnumerable> tags = + [ + new("component.type", componentType ?? "unknown"), + new("component.method", methodName ?? "unknown"), + new("attribute.name", attributeName ?? "unknown"), + ]; + List? links = new List(); + var parentActivity = Activity.Current; + if (parentActivity is not null) + { + links.Add(new ActivityLink(parentActivity.Context)); + } + if (_routeActivity is not null) + { + links.Add(new ActivityLink(_routeActivity.Context)); + } + + var activity = ActivitySource.CreateActivity(OnEventName, ActivityKind.Server, parentId: null, tags, links); + if (activity is not null) + { + activity.DisplayName = $"EVENT {attributeName ?? "unknown"} -> {componentType ?? "unknown"}.{methodName ?? "unknown"}"; + activity.Start(); + } + return activity; + } + + public static void FailEventActivity(Activity activity, Exception ex) + { + if (!activity.IsStopped) + { + activity.SetTag("error.type", ex.GetType().FullName); + activity.SetStatus(ActivityStatusCode.Error); + activity.Stop(); + } + } + + public static async Task CaptureEventStopAsync(Task task, Activity activity) + { + try + { + await task; + activity.Stop(); + } + catch (Exception ex) + { + FailEventActivity(activity, ex); + } + } +} diff --git a/src/Components/Components/src/Rendering/RenderingMetrics.cs b/src/Components/Components/src/ComponentsMetrics.cs similarity index 88% rename from src/Components/Components/src/Rendering/RenderingMetrics.cs rename to src/Components/Components/src/ComponentsMetrics.cs index 9e95487c502e..c9f5bafe1462 100644 --- a/src/Components/Components/src/Rendering/RenderingMetrics.cs +++ b/src/Components/Components/src/ComponentsMetrics.cs @@ -3,15 +3,18 @@ using System.Diagnostics; using System.Diagnostics.Metrics; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Components.Rendering; +namespace Microsoft.AspNetCore.Components; -internal sealed class RenderingMetrics : IDisposable +internal sealed class ComponentsMetrics : IDisposable { - public const string MeterName = "Microsoft.AspNetCore.Components.Rendering"; + public const string MeterName = "Microsoft.AspNetCore.Components"; private readonly Meter _meter; + private readonly Counter _navigationCount; + private readonly Histogram _eventSyncDuration; private readonly Histogram _eventAsyncDuration; private readonly Counter _eventException; @@ -25,6 +28,8 @@ internal sealed class RenderingMetrics : IDisposable private readonly Histogram _batchDuration; private readonly Counter _batchException; + public bool IsNavigationEnabled => _navigationCount.Enabled; + public bool IsEventDurationEnabled => _eventSyncDuration.Enabled || _eventAsyncDuration.Enabled; public bool IsEventExceptionEnabled => _eventException.Enabled; @@ -36,43 +41,48 @@ internal sealed class RenderingMetrics : IDisposable public bool IsBatchDurationEnabled => _batchDuration.Enabled; public bool IsBatchExceptionEnabled => _batchException.Enabled; - public RenderingMetrics(IMeterFactory meterFactory) + public ComponentsMetrics(IMeterFactory meterFactory) { Debug.Assert(meterFactory != null); _meter = meterFactory.Create(MeterName); + _navigationCount = _meter.CreateCounter( + "aspnetcore.components.navigation.count", + unit: "{exceptions}", + description: "Total number of route changes."); + _eventSyncDuration = _meter.CreateHistogram( - "aspnetcore.components.rendering.event.synchronous.duration", + "aspnetcore.components.event.synchronous.duration", unit: "s", description: "Duration of processing browser event synchronously.", advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); _eventAsyncDuration = _meter.CreateHistogram( - "aspnetcore.components.rendering.event.asynchronous.duration", + "aspnetcore.components.event.asynchronous.duration", unit: "s", description: "Duration of processing browser event asynchronously.", advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); _eventException = _meter.CreateCounter( - "aspnetcore.components.rendering.event.exception", + "aspnetcore.components.event.exception", unit: "{exceptions}", description: "Total number of exceptions during browser event processing."); _parametersSyncDuration = _meter.CreateHistogram( - "aspnetcore.components.rendering.parameters.synchronous.duration", + "aspnetcore.components.parameters.synchronous.duration", unit: "s", description: "Duration of processing component parameters synchronously.", advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); _parametersAsyncDuration = _meter.CreateHistogram( - "aspnetcore.components.rendering.parameters.asynchronous.duration", + "aspnetcore.components.parameters.asynchronous.duration", unit: "s", description: "Duration of processing component parameters asynchronously.", advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); _parametersException = _meter.CreateCounter( - "aspnetcore.components.rendering.parameters.exception", + "aspnetcore.components.parameters.exception", unit: "{exceptions}", description: "Total number of exceptions during processing component parameters."); @@ -94,6 +104,17 @@ public RenderingMetrics(IMeterFactory meterFactory) description: "Total number of exceptions during batch rendering."); } + public void Navigation(string componentType, string route) + { + var tags = new TagList + { + { "component.type", componentType ?? "unknown" }, + { "route", route ?? "unknown" }, + }; + + _navigationCount.Add(1, tags); + } + public void EventDurationSync(long startTimestamp, string? componentType, string? methodName, string? attributeName) { var tags = new TagList diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index c372bbf750c2..ce563e15a7cb 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1,7 +1,7 @@ #nullable enable Microsoft.AspNetCore.Components.Routing.Router.NotFoundPage.get -> System.Type! Microsoft.AspNetCore.Components.Routing.Router.NotFoundPage.set -> void -Microsoft.AspNetCore.Components.Infrastructure.RenderingMetricsServiceCollectionExtensions +Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions Microsoft.AspNetCore.Components.NavigationManager.OnNotFound -> System.EventHandler! Microsoft.AspNetCore.Components.NavigationManager.NotFound() -> void Microsoft.AspNetCore.Components.Routing.IHostEnvironmentNavigationManager.Initialize(string! baseUri, string! uri, System.Func! onNavigateTo) -> void @@ -14,7 +14,7 @@ Microsoft.AspNetCore.Components.SupplyParameterFromPersistentComponentStateAttri Microsoft.AspNetCore.Components.SupplyParameterFromPersistentComponentStateAttribute.SupplyParameterFromPersistentComponentStateAttribute() -> void Microsoft.Extensions.DependencyInjection.SupplyParameterFromPersistentComponentStateProviderServiceCollectionExtensions static Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponentStateServiceCollectionExtensions.AddPersistentServiceRegistration(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.AspNetCore.Components.IComponentRenderMode! componentRenderMode) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.AspNetCore.Components.Infrastructure.RenderingMetricsServiceCollectionExtensions.AddRenderingMetrics(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.AspNetCore.Components.Infrastructure.RenderingMetricsServiceCollectionExtensions.AddRenderingTracing(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsMetrics(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsTracing(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Extensions.DependencyInjection.SupplyParameterFromPersistentComponentStateProviderServiceCollectionExtensions.AddSupplyValueFromPersistentComponentStateProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.SignalRendererToFinishRendering() -> void \ No newline at end of file diff --git a/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs b/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs index b40b3adf4de3..e1e224e5f71b 100644 --- a/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs +++ b/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.Metrics; -using Microsoft.AspNetCore.Components.Rendering; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -11,14 +10,14 @@ namespace Microsoft.AspNetCore.Components.Infrastructure; /// /// Infrastructure APIs for registering diagnostic metrics. /// -public static class RenderingMetricsServiceCollectionExtensions +public static class ComponentsMetricsServiceCollectionExtensions { /// /// Registers component rendering metrics /// /// The . /// The . - public static IServiceCollection AddRenderingMetrics( + public static IServiceCollection AddComponentsMetrics( IServiceCollection services) { // do not register IConfigureOptions multiple times @@ -26,7 +25,7 @@ public static IServiceCollection AddRenderingMetrics( { services.AddMetrics(); } - services.TryAddSingleton(); + services.TryAddSingleton(); return services; } @@ -36,10 +35,10 @@ public static IServiceCollection AddRenderingMetrics( /// /// The . /// The . - public static IServiceCollection AddRenderingTracing( + public static IServiceCollection AddComponentsTracing( IServiceCollection services) { - services.TryAddSingleton(); + services.TryAddScoped(); return services; } diff --git a/src/Components/Components/src/RenderHandle.cs b/src/Components/Components/src/RenderHandle.cs index 6ac2b7b3cdec..edcb644bddfb 100644 --- a/src/Components/Components/src/RenderHandle.cs +++ b/src/Components/Components/src/RenderHandle.cs @@ -21,6 +21,9 @@ internal RenderHandle(Renderer renderer, int componentId) _componentId = componentId; } + internal ComponentsMetrics? ComponentMetrics => _renderer?.ComponentMetrics; + internal ComponentsActivitySource? ComponentActivitySource => _renderer?.ComponentActivitySource; + /// /// Gets the associated with the component. /// diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index 1e4036042417..baf363e620ae 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -35,8 +35,9 @@ public abstract partial class Renderer : IDisposable, IAsyncDisposable private readonly Dictionary _eventHandlerIdReplacements = new Dictionary(); private readonly ILogger _logger; private readonly ComponentFactory _componentFactory; - private readonly RenderingMetrics? _renderingMetrics; - private readonly RenderingActivitySource? _renderingActivitySource; + private readonly ComponentsMetrics? _componentsMetrics; + private readonly ComponentsActivitySource? _componentsActivitySource; + private Dictionary? _rootComponentsLatestParameters; private Task? _ongoingQuiescenceTask; @@ -94,16 +95,16 @@ public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, // logger name in here as a string literal. _logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Components.RenderTree.Renderer"); _componentFactory = new ComponentFactory(componentActivator, this); - _renderingMetrics = serviceProvider.GetService(); - _renderingActivitySource = serviceProvider.GetService(); + _componentsMetrics = serviceProvider.GetService(); + _componentsActivitySource = serviceProvider.GetService(); ServiceProviderCascadingValueSuppliers = serviceProvider.GetService() is null ? Array.Empty() : serviceProvider.GetServices().ToArray(); } - internal RenderingMetrics? RenderingMetrics => _renderingMetrics; - internal RenderingActivitySource? RenderingActivitySource => _renderingActivitySource; + internal ComponentsMetrics? ComponentMetrics => _componentsMetrics; + internal ComponentsActivitySource? ComponentActivitySource => _componentsActivitySource; internal ICascadingValueSupplier[] ServiceProviderCascadingValueSuppliers { get; } @@ -449,14 +450,14 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie // collect trace Activity? activity = null; - if (RenderingActivitySource != null) + if (ComponentActivitySource != null) { var receiverName = (callback.Receiver?.GetType() ?? callback.Delegate.Target?.GetType())?.FullName; var methodName = callback.Delegate.Method?.Name; - activity = RenderingActivitySource.StartEventActivity(receiverName, methodName, attributeName, null); + activity = ComponentActivitySource.StartEventActivity(receiverName, methodName, attributeName); } - var eventStartTimestamp = RenderingMetrics != null && RenderingMetrics.IsEventDurationEnabled ? Stopwatch.GetTimestamp() : 0; + var eventStartTimestamp = ComponentMetrics != null && ComponentMetrics.IsEventDurationEnabled ? Stopwatch.GetTimestamp() : 0; // If this event attribute was rendered by a component that's since been disposed, don't dispatch the event at all. // This can occur because event handler disposal is deferred, so event handler IDs can outlive their components. @@ -500,34 +501,34 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie task = callback.InvokeAsync(eventArgs); // collect metrics - if (RenderingMetrics != null && RenderingMetrics.IsEventDurationEnabled) + if (ComponentMetrics != null && ComponentMetrics.IsEventDurationEnabled) { var receiverName = (callback.Receiver?.GetType() ?? callback.Delegate.Target?.GetType())?.FullName; var methodName = callback.Delegate.Method?.Name; - RenderingMetrics.EventDurationSync(eventStartTimestamp, receiverName, methodName, attributeName); - _ = RenderingMetrics.CaptureEventDurationAsync(task, eventStartTimestamp, receiverName, methodName, attributeName); + ComponentMetrics.EventDurationSync(eventStartTimestamp, receiverName, methodName, attributeName); + _ = ComponentMetrics.CaptureEventDurationAsync(task, eventStartTimestamp, receiverName, methodName, attributeName); } - if (RenderingMetrics != null && RenderingMetrics.IsEventExceptionEnabled) + if (ComponentMetrics != null && ComponentMetrics.IsEventExceptionEnabled) { - _ = RenderingMetrics.CaptureEventFailedAsync(task, callback, attributeName); + _ = ComponentMetrics.CaptureEventFailedAsync(task, callback, attributeName); } // stop activity/trace - if (RenderingActivitySource != null && activity != null) + if (ComponentActivitySource != null && activity != null) { - _ = RenderingActivitySource.CaptureEventStopAsync(task, activity); + _ = ComponentsActivitySource.CaptureEventStopAsync(task, activity); } } catch (Exception e) { - if (RenderingMetrics != null && RenderingMetrics.IsEventExceptionEnabled) + if (ComponentMetrics != null && ComponentMetrics.IsEventExceptionEnabled) { - RenderingMetrics.EventFailed(e.GetType().FullName, callback, attributeName); + ComponentMetrics.EventFailed(e.GetType().FullName, callback, attributeName); } - if (RenderingActivitySource != null && activity != null) + if (ComponentActivitySource != null && activity != null) { - RenderingActivitySource.FailEventActivity(activity, e); + ComponentsActivitySource.FailEventActivity(activity, e); } HandleExceptionViaErrorBoundary(e, receiverComponentState); @@ -817,7 +818,7 @@ private void ProcessRenderQueue() _isBatchInProgress = true; var updateDisplayTask = Task.CompletedTask; - var batchStartTimestamp = RenderingMetrics != null && RenderingMetrics.IsBatchDurationEnabled ? Stopwatch.GetTimestamp() : 0; + var batchStartTimestamp = ComponentMetrics != null && ComponentMetrics.IsBatchDurationEnabled ? Stopwatch.GetTimestamp() : 0; try { @@ -850,20 +851,20 @@ private void ProcessRenderQueue() // if there is async work to be done. _ = InvokeRenderCompletedCalls(batch.UpdatedComponents, updateDisplayTask); - if (RenderingMetrics != null && RenderingMetrics.IsBatchDurationEnabled) + if (ComponentMetrics != null && ComponentMetrics.IsBatchDurationEnabled) { - _renderingMetrics.BatchDuration(batchStartTimestamp, batch.UpdatedComponents.Count); + ComponentMetrics.BatchDuration(batchStartTimestamp, batch.UpdatedComponents.Count); } - if (RenderingMetrics != null && RenderingMetrics.IsBatchExceptionEnabled) + if (ComponentMetrics != null && ComponentMetrics.IsBatchExceptionEnabled) { - _ = _renderingMetrics.CaptureBatchFailedAsync(updateDisplayTask); + _ = ComponentMetrics.CaptureBatchFailedAsync(updateDisplayTask); } } catch (Exception e) { - if (RenderingMetrics != null && RenderingMetrics.IsBatchExceptionEnabled) + if (ComponentMetrics != null && ComponentMetrics.IsBatchExceptionEnabled) { - _renderingMetrics.BatchFailed(e.GetType().Name); + ComponentMetrics.BatchFailed(e.GetType().Name); } // Ensure we catch errors while running the render functions of the components. diff --git a/src/Components/Components/src/Rendering/ComponentState.cs b/src/Components/Components/src/Rendering/ComponentState.cs index 40ad68733248..201c74e10019 100644 --- a/src/Components/Components/src/Rendering/ComponentState.cs +++ b/src/Components/Components/src/Rendering/ComponentState.cs @@ -53,7 +53,7 @@ public ComponentState(Renderer renderer, int componentId, IComponent component, _hasAnyCascadingParameterSubscriptions = AddCascadingParameterSubscriptions(); } - if (_renderer.RenderingMetrics != null && (_renderer.RenderingMetrics.IsDiffDurationEnabled || _renderer.RenderingMetrics.IsStateDurationEnabled || _renderer.RenderingMetrics.IsStateExceptionEnabled)) + if (_renderer.ComponentMetrics != null && (_renderer.ComponentMetrics.IsDiffDurationEnabled || _renderer.ComponentMetrics.IsStateDurationEnabled || _renderer.ComponentMetrics.IsStateExceptionEnabled)) { _componentTypeName = component.GetType().FullName; } @@ -108,7 +108,7 @@ internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment re _nextRenderTree.Clear(); - var diffStartTimestamp = _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsDiffDurationEnabled ? Stopwatch.GetTimestamp() : 0; + var diffStartTimestamp = _renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsDiffDurationEnabled ? Stopwatch.GetTimestamp() : 0; try { renderFragment(_nextRenderTree); @@ -139,9 +139,9 @@ internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment re batchBuilder.UpdatedComponentDiffs.Append(diff); batchBuilder.InvalidateParameterViews(); - if (_renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsDiffDurationEnabled) + if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsDiffDurationEnabled) { - _renderer.RenderingMetrics.DiffDuration(diffStartTimestamp, _componentTypeName, batchBuilder.EditsBuffer.Count - startCount); + _renderer.ComponentMetrics.DiffDuration(diffStartTimestamp, _componentTypeName, batchBuilder.EditsBuffer.Count - startCount); } } @@ -249,26 +249,26 @@ private void SupplyCombinedParameters(ParameterView directAndCascadingParameters Task setParametersAsyncTask; try { - var stateStartTimestamp = _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateDurationEnabled ? Stopwatch.GetTimestamp() : 0; + var stateStartTimestamp = _renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsStateDurationEnabled ? Stopwatch.GetTimestamp() : 0; setParametersAsyncTask = Component.SetParametersAsync(directAndCascadingParameters); // collect metrics - if (_renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateDurationEnabled) + if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsStateDurationEnabled) { - _renderer.RenderingMetrics.ParametersDurationSync(stateStartTimestamp, _componentTypeName); - _ = _renderer.RenderingMetrics.CaptureParametersDurationAsync(setParametersAsyncTask, stateStartTimestamp, _componentTypeName); + _renderer.ComponentMetrics.ParametersDurationSync(stateStartTimestamp, _componentTypeName); + _ = _renderer.ComponentMetrics.CaptureParametersDurationAsync(setParametersAsyncTask, stateStartTimestamp, _componentTypeName); } - if (_renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateExceptionEnabled) + if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsStateExceptionEnabled) { - _ = _renderer.RenderingMetrics.CapturePropertiesFailedAsync(setParametersAsyncTask, _componentTypeName); + _ = _renderer.ComponentMetrics.CapturePropertiesFailedAsync(setParametersAsyncTask, _componentTypeName); } } catch (Exception ex) { - if (_renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateExceptionEnabled) + if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsStateExceptionEnabled) { - _renderer.RenderingMetrics.PropertiesFailed(ex.GetType().FullName, _componentTypeName); + _renderer.ComponentMetrics.PropertiesFailed(ex.GetType().FullName, _componentTypeName); } setParametersAsyncTask = Task.FromException(ex); diff --git a/src/Components/Components/src/Rendering/RenderingActivitySource.cs b/src/Components/Components/src/Rendering/RenderingActivitySource.cs deleted file mode 100644 index af83cb7e4381..000000000000 --- a/src/Components/Components/src/Rendering/RenderingActivitySource.cs +++ /dev/null @@ -1,54 +0,0 @@ -// 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; - -namespace Microsoft.AspNetCore.Components.Rendering; -internal class RenderingActivitySource -{ - internal const string Name = "Microsoft.AspNetCore.Components.Rendering"; - internal const string OnEventName = $"{Name}.OnEvent"; - - public ActivitySource ActivitySource { get; } = new ActivitySource(Name); - - public Activity? StartEventActivity(string? componentType, string? methodName, string? attributeName, Activity? linkedActivity) - { - IEnumerable> tags = - [ - new("component.type", componentType ?? "unknown"), - new("component.method", methodName ?? "unknown"), - new("attribute.name", attributeName ?? "unknown"), - ]; - IEnumerable? links = (linkedActivity is not null) ? [new ActivityLink(linkedActivity.Context)] : null; - - var activity = ActivitySource.CreateActivity(OnEventName, ActivityKind.Server, parentId: null, tags, links); - if (activity is not null) - { - activity.DisplayName = $"{componentType ?? "unknown"}/{methodName ?? "unknown"}/{attributeName ?? "unknown"}"; - activity.Start(); - } - return activity; - } - public static void FailEventActivity(Activity activity, Exception ex) - { - if (!activity.IsStopped) - { - activity.SetTag("error.type", ex.GetType().FullName); - activity.SetStatus(ActivityStatusCode.Error); - activity.Stop(); - } - } - - public static async Task CaptureEventStopAsync(Task task, Activity activity) - { - try - { - await task; - activity.Stop(); - } - catch (Exception ex) - { - FailEventActivity(activity, ex); - } - } -} diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index d562b94bb639..8aef8f4fcba7 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection; +using System.Diagnostics; namespace Microsoft.AspNetCore.Components.Routing; @@ -226,6 +227,8 @@ internal virtual void Refresh(bool isNavigationIntercepted) // In order to avoid routing twice we check for RouteData if (RoutingStateProvider?.RouteData is { } endpointRouteData) { + RecordDiagnostics(endpointRouteData.PageType.FullName, endpointRouteData.Template); + // Other routers shouldn't provide RouteData, this is specific to our router component // and must abide by our syntax and behaviors. // Other routers must create their own abstractions to flow data from their SSR routing @@ -252,6 +255,8 @@ internal virtual void Refresh(bool isNavigationIntercepted) $"does not implement {typeof(IComponent).FullName}."); } + RecordDiagnostics(context.Handler.FullName, context.Entry.RoutePattern.RawText); + Log.NavigatingToComponent(_logger, context.Handler, locationPath, _baseUri); var routeData = new RouteData( @@ -286,6 +291,16 @@ internal virtual void Refresh(bool isNavigationIntercepted) } } + private void RecordDiagnostics(string componentType, string template) + { + _renderHandle.ComponentActivitySource?.StartRouteActivity(componentType, template); + + if (_renderHandle.ComponentMetrics != null && _renderHandle.ComponentMetrics.IsNavigationEnabled) + { + _renderHandle.ComponentMetrics.Navigation(componentType, template); + } + } + private static void DefaultNotFoundContent(RenderTreeBuilder builder) { // This output can't use any layout (none is specified), and it can't use any web-specific concepts @@ -340,6 +355,8 @@ internal async ValueTask RunOnNavigateAsync(string path, bool isNavigationInterc private void OnLocationChanged(object sender, LocationChangedEventArgs args) { + _renderHandle.ComponentActivitySource?.StopRouteActivity(); + _locationAbsolute = args.Location; if (_renderHandle.IsInitialized && Routes != null) { diff --git a/src/Components/Components/test/Rendering/RenderingMetricsTest.cs b/src/Components/Components/test/Rendering/RenderingMetricsTest.cs index f1d7762d11ff..c750ebec5ce4 100644 --- a/src/Components/Components/test/Rendering/RenderingMetricsTest.cs +++ b/src/Components/Components/test/Rendering/RenderingMetricsTest.cs @@ -12,11 +12,11 @@ namespace Microsoft.AspNetCore.Components.Rendering; -public class RenderingMetricsTest +public class ComponentsMetricsTest { private readonly TestMeterFactory _meterFactory; - public RenderingMetricsTest() + public ComponentsMetricsTest() { _meterFactory = new TestMeterFactory(); } @@ -25,25 +25,25 @@ public RenderingMetricsTest() public void Constructor_CreatesMetersCorrectly() { // Arrange & Act - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); // Assert Assert.Single(_meterFactory.Meters); - Assert.Equal(RenderingMetrics.MeterName, _meterFactory.Meters[0].Name); + Assert.Equal(ComponentsMetrics.MeterName, _meterFactory.Meters[0].Name); } [Fact] public void EventDurationSync_RecordsDuration() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); using var eventSyncDurationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.synchronous.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.synchronous.duration"); // Act var startTime = Stopwatch.GetTimestamp(); Thread.Sleep(10); // Add a small delay to ensure a measurable duration - renderingMetrics.EventDurationSync(startTime, "TestComponent", "MyMethod", "OnClick"); + componentsMetrics.EventDurationSync(startTime, "TestComponent", "MyMethod", "OnClick"); // Assert var measurements = eventSyncDurationCollector.GetMeasurementSnapshot(); @@ -58,14 +58,14 @@ public void EventDurationSync_RecordsDuration() public async Task CaptureEventDurationAsync_RecordsDuration() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); using var eventAsyncDurationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.asynchronous.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.asynchronous.duration"); // Act var startTime = Stopwatch.GetTimestamp(); var task = Task.Delay(10); // Create a delay task - await renderingMetrics.CaptureEventDurationAsync(task, startTime, "TestComponent", "MyMethod", "OnClickAsync"); + await componentsMetrics.CaptureEventDurationAsync(task, startTime, "TestComponent", "MyMethod", "OnClickAsync"); // Assert var measurements = eventAsyncDurationCollector.GetMeasurementSnapshot(); @@ -81,14 +81,14 @@ public async Task CaptureEventDurationAsync_RecordsDuration() public void ParametersDurationSync_RecordsDuration() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); using var parametersSyncDurationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.synchronous.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.synchronous.duration"); // Act var startTime = Stopwatch.GetTimestamp(); Thread.Sleep(10); // Add a small delay to ensure a measurable duration - renderingMetrics.ParametersDurationSync(startTime, "TestComponent"); + componentsMetrics.ParametersDurationSync(startTime, "TestComponent"); // Assert var measurements = parametersSyncDurationCollector.GetMeasurementSnapshot(); @@ -102,14 +102,14 @@ public void ParametersDurationSync_RecordsDuration() public async Task CaptureParametersDurationAsync_RecordsDuration() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); using var parametersAsyncDurationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.asynchronous.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.asynchronous.duration"); // Act var startTime = Stopwatch.GetTimestamp(); var task = Task.Delay(10); // Create a delay task - await renderingMetrics.CaptureParametersDurationAsync(task, startTime, "TestComponent"); + await componentsMetrics.CaptureParametersDurationAsync(task, startTime, "TestComponent"); // Assert var measurements = parametersAsyncDurationCollector.GetMeasurementSnapshot(); @@ -123,14 +123,14 @@ public async Task CaptureParametersDurationAsync_RecordsDuration() public void DiffDuration_RecordsDuration() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); using var diffDurationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.diff.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.diff.duration"); // Act var startTime = Stopwatch.GetTimestamp(); Thread.Sleep(10); // Add a small delay to ensure a measurable duration - renderingMetrics.DiffDuration(startTime, "TestComponent", 5); + componentsMetrics.DiffDuration(startTime, "TestComponent", 5); // Assert var measurements = diffDurationCollector.GetMeasurementSnapshot(); @@ -145,14 +145,14 @@ public void DiffDuration_RecordsDuration() public void BatchDuration_RecordsDuration() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); using var batchDurationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.batch.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.batch.duration"); // Act var startTime = Stopwatch.GetTimestamp(); Thread.Sleep(10); // Add a small delay to ensure a measurable duration - renderingMetrics.BatchDuration(startTime, 50); + componentsMetrics.BatchDuration(startTime, 50); // Assert var measurements = batchDurationCollector.GetMeasurementSnapshot(); @@ -166,15 +166,15 @@ public void BatchDuration_RecordsDuration() public void EventFailed_RecordsException() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); using var eventExceptionCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.exception"); // Create a mock EventCallback var callback = new EventCallback(new TestComponent(), (Action)(() => { })); // Act - renderingMetrics.EventFailed("ArgumentException", callback, "OnClick"); + componentsMetrics.EventFailed("ArgumentException", callback, "OnClick"); // Assert var measurements = eventExceptionCollector.GetMeasurementSnapshot(); @@ -183,16 +183,16 @@ public void EventFailed_RecordsException() Assert.Equal(1, measurements[0].Value); Assert.Equal("ArgumentException", measurements[0].Tags["error.type"]); Assert.Equal("OnClick", measurements[0].Tags["attribute.name"]); - Assert.Contains("Microsoft.AspNetCore.Components.Rendering.RenderingMetricsTest+TestComponent", (string)measurements[0].Tags["component.type"]); + Assert.Contains("Microsoft.AspNetCore.Components.Rendering.ComponentsMetricsTest+TestComponent", (string)measurements[0].Tags["component.type"]); } [Fact] public async Task CaptureEventFailedAsync_RecordsException() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); using var eventExceptionCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.exception"); // Create a mock EventCallback var callback = new EventCallback(new TestComponent(), (Action)(() => { })); @@ -201,7 +201,7 @@ public async Task CaptureEventFailedAsync_RecordsException() var task = Task.FromException(new InvalidOperationException()); // Act - await renderingMetrics.CaptureEventFailedAsync(task, callback, "OnClickAsync"); + await componentsMetrics.CaptureEventFailedAsync(task, callback, "OnClickAsync"); // Assert var measurements = eventExceptionCollector.GetMeasurementSnapshot(); @@ -210,19 +210,19 @@ public async Task CaptureEventFailedAsync_RecordsException() Assert.Equal(1, measurements[0].Value); Assert.Equal("InvalidOperationException", measurements[0].Tags["error.type"]); Assert.Equal("OnClickAsync", measurements[0].Tags["attribute.name"]); - Assert.Contains("Microsoft.AspNetCore.Components.Rendering.RenderingMetricsTest+TestComponent", (string)measurements[0].Tags["component.type"]); + Assert.Contains("Microsoft.AspNetCore.Components.Rendering.ComponentsMetricsTest+TestComponent", (string)measurements[0].Tags["component.type"]); } [Fact] public void PropertiesFailed_RecordsException() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); using var parametersExceptionCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.exception"); // Act - renderingMetrics.PropertiesFailed("ArgumentException", "TestComponent"); + componentsMetrics.PropertiesFailed("ArgumentException", "TestComponent"); // Assert var measurements = parametersExceptionCollector.GetMeasurementSnapshot(); @@ -237,15 +237,15 @@ public void PropertiesFailed_RecordsException() public async Task CapturePropertiesFailedAsync_RecordsException() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); using var parametersExceptionCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.exception"); // Create a task that throws an exception var task = Task.FromException(new InvalidOperationException()); // Act - await renderingMetrics.CapturePropertiesFailedAsync(task, "TestComponent"); + await componentsMetrics.CapturePropertiesFailedAsync(task, "TestComponent"); // Assert var measurements = parametersExceptionCollector.GetMeasurementSnapshot(); @@ -260,12 +260,12 @@ public async Task CapturePropertiesFailedAsync_RecordsException() public void BatchFailed_RecordsException() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); using var batchExceptionCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.batch.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.batch.exception"); // Act - renderingMetrics.BatchFailed("ArgumentException"); + componentsMetrics.BatchFailed("ArgumentException"); // Assert var measurements = batchExceptionCollector.GetMeasurementSnapshot(); @@ -279,15 +279,15 @@ public void BatchFailed_RecordsException() public async Task CaptureBatchFailedAsync_RecordsException() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); using var batchExceptionCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.batch.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.batch.exception"); // Create a task that throws an exception var task = Task.FromException(new InvalidOperationException()); // Act - await renderingMetrics.CaptureBatchFailedAsync(task); + await componentsMetrics.CaptureBatchFailedAsync(task); // Assert var measurements = batchExceptionCollector.GetMeasurementSnapshot(); @@ -301,60 +301,60 @@ public async Task CaptureBatchFailedAsync_RecordsException() public void EnabledProperties_ReflectMeterState() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); // Create collectors to ensure the meters are enabled using var eventSyncDurationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.synchronous.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.synchronous.duration"); using var eventAsyncDurationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.asynchronous.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.asynchronous.duration"); using var eventExceptionCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.exception"); using var parametersSyncDurationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.synchronous.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.synchronous.duration"); using var parametersAsyncDurationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.asynchronous.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.asynchronous.duration"); using var parametersExceptionCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.exception"); using var diffDurationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.diff.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.diff.duration"); using var batchDurationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.batch.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.batch.duration"); using var batchExceptionCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.batch.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.batch.exception"); // Assert - Assert.True(renderingMetrics.IsEventDurationEnabled); - Assert.True(renderingMetrics.IsEventExceptionEnabled); - Assert.True(renderingMetrics.IsStateDurationEnabled); - Assert.True(renderingMetrics.IsStateExceptionEnabled); - Assert.True(renderingMetrics.IsDiffDurationEnabled); - Assert.True(renderingMetrics.IsBatchDurationEnabled); - Assert.True(renderingMetrics.IsBatchExceptionEnabled); + Assert.True(componentsMetrics.IsEventDurationEnabled); + Assert.True(componentsMetrics.IsEventExceptionEnabled); + Assert.True(componentsMetrics.IsStateDurationEnabled); + Assert.True(componentsMetrics.IsStateExceptionEnabled); + Assert.True(componentsMetrics.IsDiffDurationEnabled); + Assert.True(componentsMetrics.IsBatchDurationEnabled); + Assert.True(componentsMetrics.IsBatchExceptionEnabled); } [Fact] public void BucketEditLength_ReturnsCorrectBucket() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); using var diffDurationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.diff.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.diff.duration"); // Act & Assert - Test different diff lengths var startTime = Stopwatch.GetTimestamp(); // Test each bucket boundary - renderingMetrics.DiffDuration(startTime, "Component", 1); - renderingMetrics.DiffDuration(startTime, "Component", 2); - renderingMetrics.DiffDuration(startTime, "Component", 5); - renderingMetrics.DiffDuration(startTime, "Component", 10); - renderingMetrics.DiffDuration(startTime, "Component", 50); - renderingMetrics.DiffDuration(startTime, "Component", 100); - renderingMetrics.DiffDuration(startTime, "Component", 500); - renderingMetrics.DiffDuration(startTime, "Component", 1000); - renderingMetrics.DiffDuration(startTime, "Component", 10000); - renderingMetrics.DiffDuration(startTime, "Component", 20000); // Should be 10001 + componentsMetrics.DiffDuration(startTime, "Component", 1); + componentsMetrics.DiffDuration(startTime, "Component", 2); + componentsMetrics.DiffDuration(startTime, "Component", 5); + componentsMetrics.DiffDuration(startTime, "Component", 10); + componentsMetrics.DiffDuration(startTime, "Component", 50); + componentsMetrics.DiffDuration(startTime, "Component", 100); + componentsMetrics.DiffDuration(startTime, "Component", 500); + componentsMetrics.DiffDuration(startTime, "Component", 1000); + componentsMetrics.DiffDuration(startTime, "Component", 10000); + componentsMetrics.DiffDuration(startTime, "Component", 20000); // Should be 10001 // Assert var measurements = diffDurationCollector.GetMeasurementSnapshot(); @@ -379,15 +379,15 @@ public void Dispose_DisposesUnderlyingMeter() // This is a bit tricky to test directly, so we'll use an indirect approach // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); // Act - renderingMetrics.Dispose(); + componentsMetrics.Dispose(); // Try to use the disposed meter - this should not throw since TestMeterFactory // doesn't actually dispose the meter in test contexts var startTime = Stopwatch.GetTimestamp(); - renderingMetrics.EventDurationSync(startTime, "TestComponent", "MyMethod", "OnClick"); + componentsMetrics.EventDurationSync(startTime, "TestComponent", "MyMethod", "OnClick"); } // Helper class for mock components diff --git a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs index 4618ab6dc7b5..de2e1f4707e0 100644 --- a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs +++ b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs @@ -76,8 +76,8 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection services.TryAddScoped(); - RenderingMetricsServiceCollectionExtensions.AddRenderingMetrics(services); - RenderingMetricsServiceCollectionExtensions.AddRenderingTracing(services); + ComponentsMetricsServiceCollectionExtensions.AddComponentsMetrics(services); + ComponentsMetricsServiceCollectionExtensions.AddComponentsTracing(services); // Form handling services.AddSupplyValueFromFormProvider(); diff --git a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs index 245f811d7f76..93c5579e6290 100644 --- a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs +++ b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs @@ -30,6 +30,8 @@ public RazorComponentEndpointInvoker(EndpointHtmlRenderer renderer, ILogger RenderComponentCore(context)); } From cf15c8d65a5b96c64564eb4106feee3f1df8d740 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 24 Apr 2025 18:47:44 +0200 Subject: [PATCH 6/9] cleanup --- src/Components/Components/src/Routing/Router.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index 8aef8f4fcba7..8c4d5069b60d 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -12,7 +12,6 @@ using Microsoft.AspNetCore.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection; -using System.Diagnostics; namespace Microsoft.AspNetCore.Components.Routing; From 550f633d81c860b6ceff21d0f18a34ab920f7bc4 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 24 Apr 2025 19:05:50 +0200 Subject: [PATCH 7/9] cleanup --- src/Components/Components/src/RenderTree/Renderer.cs | 4 ---- src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs | 2 -- 2 files changed, 6 deletions(-) diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index baf363e620ae..8ffc1369f684 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -541,10 +541,6 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie // Since the task has yielded - process any queued rendering work before we return control // to the caller. ProcessPendingRender(); - - //callback.Receiver - //callback.Delegate.Method. - } // Task completed synchronously or is still running. We already processed all of the rendering diff --git a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs index 93c5579e6290..245f811d7f76 100644 --- a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs +++ b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs @@ -30,8 +30,6 @@ public RazorComponentEndpointInvoker(EndpointHtmlRenderer renderer, ILogger RenderComponentCore(context)); } From 9ab8a8481f0c16925d8d1db762cfb2c1e709507f Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 25 Apr 2025 19:40:20 +0200 Subject: [PATCH 8/9] more --- .../src/ComponentsActivitySource.cs | 109 ++++++++++--- .../Components/src/ComponentsMetrics.cs | 86 ++-------- .../Microsoft.AspNetCore.Components.csproj | 1 + .../Components/src/RenderTree/Renderer.cs | 1 - .../src/Rendering/ComponentState.cs | 19 +-- .../Components/src/Routing/Router.cs | 28 +++- ...etricsTest.cs => ComponentsMetricsTest.cs} | 153 ++---------------- .../Server/src/Circuits/CircuitFactory.cs | 2 + .../Server/src/Circuits/CircuitHost.cs | 13 +- .../Server/src/Circuits/RemoteRenderer.cs | 10 +- src/Components/Server/src/ComponentHub.cs | 5 +- .../Server/test/Circuits/CircuitHostTest.cs | 6 +- .../Server/test/Circuits/TestCircuitHost.cs | 7 +- 13 files changed, 170 insertions(+), 270 deletions(-) rename src/Components/Components/test/Rendering/{RenderingMetricsTest.cs => ComponentsMetricsTest.cs} (62%) diff --git a/src/Components/Components/src/ComponentsActivitySource.cs b/src/Components/Components/src/ComponentsActivitySource.cs index 2a314760cf86..086de83edefc 100644 --- a/src/Components/Components/src/ComponentsActivitySource.cs +++ b/src/Components/Components/src/ComponentsActivitySource.cs @@ -12,40 +12,103 @@ internal class ComponentsActivitySource { internal const string Name = "Microsoft.AspNetCore.Components"; internal const string OnEventName = $"{Name}.OnEvent"; - internal const string OnNavigationName = $"{Name}.OnNavigation"; + internal const string OnRouteName = $"{Name}.OnRoute"; - public static ActivitySource ActivitySource { get; } = new ActivitySource(Name); + private ActivityContext _httpContext; + private ActivityContext _circuitContext; + private string? _circuitId; + private ActivityContext _routeContext; - private Activity? _routeActivity; + private ActivitySource ActivitySource { get; } = new ActivitySource(Name); - public void StartRouteActivity(string componentType, string route) + public static ActivityContext CaptureHttpContext() { - StopRouteActivity(); + var parentActivity = Activity.Current; + if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn") + { + return parentActivity.Context; + } + return default; + } + + public Activity? StartCircuitActivity(string circuitId, ActivityContext httpContext) + { + _circuitId = circuitId; + IEnumerable> tags = + [ + new("circuit.id", _circuitId ?? "unknown"), + ]; + + var links = new List(); + if (httpContext != default) + { + _httpContext = httpContext; + links.Add(new ActivityLink(httpContext)); + } + + var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Server, parentId:null, tags, links); + if (activity is not null) + { + activity.DisplayName = $"CIRCUIT {circuitId ?? "unknown"}"; + activity.Start(); + _circuitContext = activity.Context; + + Console.WriteLine($"StartCircuitActivity: {circuitId}"); + Console.WriteLine($"circuitContext: {_circuitContext.TraceId} {_circuitContext.SpanId} {_circuitContext.TraceState}"); + Console.WriteLine($"httpContext: {httpContext.TraceId} {httpContext.SpanId} {httpContext.TraceState}"); + } + return activity; + } + + public void FailCircuitActivity(Activity activity, Exception ex) + { + _circuitContext = default; + if (!activity.IsStopped) + { + activity.SetTag("error.type", ex.GetType().FullName); + activity.SetStatus(ActivityStatusCode.Error); + activity.Stop(); + } + } + public Activity? StartRouteActivity(string componentType, string route) + { IEnumerable> tags = [ + new("circuit.id", _circuitId ?? "unknown"), new("component.type", componentType ?? "unknown"), new("route", route ?? "unknown"), ]; - var parentActivity = Activity.Current; - IEnumerable? links = parentActivity is not null ? [new ActivityLink(parentActivity.Context)] : null; + var links = new List(); + if (_httpContext == default) + { + _httpContext = CaptureHttpContext(); + } + if (_httpContext != default) + { + links.Add(new ActivityLink(_httpContext)); + } + if (_circuitContext != default) + { + links.Add(new ActivityLink(_circuitContext)); + } - var activity = ActivitySource.CreateActivity(OnEventName, ActivityKind.Server, parentId: null, tags, links); + var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Server, parentId: null, tags, links); if (activity is not null) { - activity.DisplayName = $"NAVIGATE {route ?? "unknown"} -> {componentType ?? "unknown"}"; + _routeContext = activity.Context; + activity.DisplayName = $"ROUTE {route ?? "unknown"} -> {componentType ?? "unknown"}"; activity.Start(); - _routeActivity = activity; } + return activity; } - public void StopRouteActivity() + public void StopRouteActivity(Activity activity) { - if (_routeActivity != null) + _routeContext = default; + if (!activity.IsStopped) { - _routeActivity.Stop(); - _routeActivity = null; - return; + activity.Stop(); } } @@ -53,19 +116,23 @@ public void StopRouteActivity() { IEnumerable> tags = [ + new("circuit.id", _circuitId ?? "unknown"), new("component.type", componentType ?? "unknown"), new("component.method", methodName ?? "unknown"), new("attribute.name", attributeName ?? "unknown"), ]; - List? links = new List(); - var parentActivity = Activity.Current; - if (parentActivity is not null) + var links = new List(); + if (_httpContext != default) + { + links.Add(new ActivityLink(_httpContext)); + } + if (_circuitContext != default) { - links.Add(new ActivityLink(parentActivity.Context)); + links.Add(new ActivityLink(_circuitContext)); } - if (_routeActivity is not null) + if (_routeContext != default) { - links.Add(new ActivityLink(_routeActivity.Context)); + links.Add(new ActivityLink(_routeContext)); } var activity = ActivitySource.CreateActivity(OnEventName, ActivityKind.Server, parentId: null, tags, links); diff --git a/src/Components/Components/src/ComponentsMetrics.cs b/src/Components/Components/src/ComponentsMetrics.cs index c9f5bafe1462..609707e7f1ed 100644 --- a/src/Components/Components/src/ComponentsMetrics.cs +++ b/src/Components/Components/src/ComponentsMetrics.cs @@ -15,28 +15,22 @@ internal sealed class ComponentsMetrics : IDisposable private readonly Counter _navigationCount; - private readonly Histogram _eventSyncDuration; - private readonly Histogram _eventAsyncDuration; + private readonly Histogram _eventDuration; private readonly Counter _eventException; - private readonly Histogram _parametersSyncDuration; - private readonly Histogram _parametersAsyncDuration; + private readonly Histogram _parametersDuration; private readonly Counter _parametersException; - private readonly Histogram _diffDuration; - private readonly Histogram _batchDuration; private readonly Counter _batchException; public bool IsNavigationEnabled => _navigationCount.Enabled; - public bool IsEventDurationEnabled => _eventSyncDuration.Enabled || _eventAsyncDuration.Enabled; + public bool IsEventDurationEnabled => _eventDuration.Enabled; public bool IsEventExceptionEnabled => _eventException.Enabled; - public bool IsStateDurationEnabled => _parametersSyncDuration.Enabled || _parametersAsyncDuration.Enabled; - public bool IsStateExceptionEnabled => _parametersException.Enabled; - - public bool IsDiffDurationEnabled => _diffDuration.Enabled; + public bool IsParametersDurationEnabled => _parametersDuration.Enabled; + public bool IsParametersExceptionEnabled => _parametersException.Enabled; public bool IsBatchDurationEnabled => _batchDuration.Enabled; public bool IsBatchExceptionEnabled => _batchException.Enabled; @@ -52,16 +46,10 @@ public ComponentsMetrics(IMeterFactory meterFactory) unit: "{exceptions}", description: "Total number of route changes."); - _eventSyncDuration = _meter.CreateHistogram( - "aspnetcore.components.event.synchronous.duration", - unit: "s", - description: "Duration of processing browser event synchronously.", - advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); - - _eventAsyncDuration = _meter.CreateHistogram( - "aspnetcore.components.event.asynchronous.duration", + _eventDuration = _meter.CreateHistogram( + "aspnetcore.components.event.duration", unit: "s", - description: "Duration of processing browser event asynchronously.", + description: "Duration of processing browser event.", advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); _eventException = _meter.CreateCounter( @@ -69,16 +57,10 @@ public ComponentsMetrics(IMeterFactory meterFactory) unit: "{exceptions}", description: "Total number of exceptions during browser event processing."); - _parametersSyncDuration = _meter.CreateHistogram( - "aspnetcore.components.parameters.synchronous.duration", + _parametersDuration = _meter.CreateHistogram( + "aspnetcore.components.parameters.duration", unit: "s", - description: "Duration of processing component parameters synchronously.", - advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); - - _parametersAsyncDuration = _meter.CreateHistogram( - "aspnetcore.components.parameters.asynchronous.duration", - unit: "s", - description: "Duration of processing component parameters asynchronously.", + description: "Duration of processing component parameters.", advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); _parametersException = _meter.CreateCounter( @@ -86,12 +68,6 @@ public ComponentsMetrics(IMeterFactory meterFactory) unit: "{exceptions}", description: "Total number of exceptions during processing component parameters."); - _diffDuration = _meter.CreateHistogram( - "aspnetcore.components.rendering.diff.duration", - unit: "s", - description: "Duration of rendering component HTML diff.", - advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); - _batchDuration = _meter.CreateHistogram( "aspnetcore.components.rendering.batch.duration", unit: "s", @@ -115,19 +91,6 @@ public void Navigation(string componentType, string route) _navigationCount.Add(1, tags); } - public void EventDurationSync(long startTimestamp, string? componentType, string? methodName, string? attributeName) - { - var tags = new TagList - { - { "component.type", componentType ?? "unknown" }, - { "component.method", methodName ?? "unknown" }, - { "attribute.name", attributeName ?? "unknown"} - }; - - var duration = Stopwatch.GetElapsedTime(startTimestamp); - _eventSyncDuration.Record(duration.TotalSeconds, tags); - } - public async Task CaptureEventDurationAsync(Task task, long startTimestamp, string? componentType, string? methodName, string? attributeName) { try @@ -142,7 +105,7 @@ public async Task CaptureEventDurationAsync(Task task, long startTimestamp, stri }; var duration = Stopwatch.GetElapsedTime(startTimestamp); - _eventAsyncDuration.Record(duration.TotalSeconds, tags); + _eventDuration.Record(duration.TotalSeconds, tags); } catch { @@ -150,17 +113,6 @@ public async Task CaptureEventDurationAsync(Task task, long startTimestamp, stri } } - public void ParametersDurationSync(long startTimestamp, string? componentType) - { - var tags = new TagList - { - { "component.type", componentType ?? "unknown" }, - }; - - var duration = Stopwatch.GetElapsedTime(startTimestamp); - _parametersSyncDuration.Record(duration.TotalSeconds, tags); - } - public async Task CaptureParametersDurationAsync(Task task, long startTimestamp, string? componentType) { try @@ -173,7 +125,7 @@ public async Task CaptureParametersDurationAsync(Task task, long startTimestamp, }; var duration = Stopwatch.GetElapsedTime(startTimestamp); - _parametersAsyncDuration.Record(duration.TotalSeconds, tags); + _parametersDuration.Record(duration.TotalSeconds, tags); } catch { @@ -181,18 +133,6 @@ public async Task CaptureParametersDurationAsync(Task task, long startTimestamp, } } - public void DiffDuration(long startTimestamp, string? componentType, int diffLength) - { - var tags = new TagList - { - { "component.type", componentType ?? "unknown" }, - { "diff.length.bucket", BucketEditLength(diffLength) } - }; - - var duration = Stopwatch.GetElapsedTime(startTimestamp); - _diffDuration.Record(duration.TotalSeconds, tags); - } - public void BatchDuration(long startTimestamp, int diffLength) { var tags = new TagList diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj index ca3286f8b6c2..07fcc360fd7e 100644 --- a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj +++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj @@ -79,6 +79,7 @@ + diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index 8ffc1369f684..d68640ea8b7b 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -505,7 +505,6 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie { var receiverName = (callback.Receiver?.GetType() ?? callback.Delegate.Target?.GetType())?.FullName; var methodName = callback.Delegate.Method?.Name; - ComponentMetrics.EventDurationSync(eventStartTimestamp, receiverName, methodName, attributeName); _ = ComponentMetrics.CaptureEventDurationAsync(task, eventStartTimestamp, receiverName, methodName, attributeName); } if (ComponentMetrics != null && ComponentMetrics.IsEventExceptionEnabled) diff --git a/src/Components/Components/src/Rendering/ComponentState.cs b/src/Components/Components/src/Rendering/ComponentState.cs index 201c74e10019..a4a91ae9e0d2 100644 --- a/src/Components/Components/src/Rendering/ComponentState.cs +++ b/src/Components/Components/src/Rendering/ComponentState.cs @@ -53,7 +53,7 @@ public ComponentState(Renderer renderer, int componentId, IComponent component, _hasAnyCascadingParameterSubscriptions = AddCascadingParameterSubscriptions(); } - if (_renderer.ComponentMetrics != null && (_renderer.ComponentMetrics.IsDiffDurationEnabled || _renderer.ComponentMetrics.IsStateDurationEnabled || _renderer.ComponentMetrics.IsStateExceptionEnabled)) + if (_renderer.ComponentMetrics != null && (_renderer.ComponentMetrics.IsParametersDurationEnabled || _renderer.ComponentMetrics.IsParametersExceptionEnabled)) { _componentTypeName = component.GetType().FullName; } @@ -108,7 +108,6 @@ internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment re _nextRenderTree.Clear(); - var diffStartTimestamp = _renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsDiffDurationEnabled ? Stopwatch.GetTimestamp() : 0; try { renderFragment(_nextRenderTree); @@ -125,8 +124,6 @@ internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment re // We don't want to make errors from this be recoverable, because there's no legitimate reason for them to happen _nextRenderTree.AssertTreeIsValid(Component); - var startCount = batchBuilder.EditsBuffer.Count; - // Swap the old and new tree builders (CurrentRenderTree, _nextRenderTree) = (_nextRenderTree, CurrentRenderTree); @@ -138,11 +135,6 @@ internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment re CurrentRenderTree.GetFrames()); batchBuilder.UpdatedComponentDiffs.Append(diff); batchBuilder.InvalidateParameterViews(); - - if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsDiffDurationEnabled) - { - _renderer.ComponentMetrics.DiffDuration(diffStartTimestamp, _componentTypeName, batchBuilder.EditsBuffer.Count - startCount); - } } // Callers expect this method to always return a faulted task. @@ -249,24 +241,23 @@ private void SupplyCombinedParameters(ParameterView directAndCascadingParameters Task setParametersAsyncTask; try { - var stateStartTimestamp = _renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsStateDurationEnabled ? Stopwatch.GetTimestamp() : 0; + var stateStartTimestamp = _renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsParametersDurationEnabled ? Stopwatch.GetTimestamp() : 0; setParametersAsyncTask = Component.SetParametersAsync(directAndCascadingParameters); // collect metrics - if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsStateDurationEnabled) + if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsParametersDurationEnabled) { - _renderer.ComponentMetrics.ParametersDurationSync(stateStartTimestamp, _componentTypeName); _ = _renderer.ComponentMetrics.CaptureParametersDurationAsync(setParametersAsyncTask, stateStartTimestamp, _componentTypeName); } - if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsStateExceptionEnabled) + if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsParametersExceptionEnabled) { _ = _renderer.ComponentMetrics.CapturePropertiesFailedAsync(setParametersAsyncTask, _componentTypeName); } } catch (Exception ex) { - if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsStateExceptionEnabled) + if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsParametersExceptionEnabled) { _renderer.ComponentMetrics.PropertiesFailed(ex.GetType().FullName, _componentTypeName); } diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index 8c4d5069b60d..2d2afadc3d39 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -3,6 +3,7 @@ #nullable disable warnings +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Reflection.Metadata; @@ -10,8 +11,8 @@ using Microsoft.AspNetCore.Components.HotReload; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Internal; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Components.Routing; @@ -222,11 +223,12 @@ internal virtual void Refresh(bool isNavigationIntercepted) var relativePath = NavigationManager.ToBaseRelativePath(_locationAbsolute.AsSpan()); var locationPathSpan = TrimQueryOrHash(relativePath); var locationPath = $"/{locationPathSpan}"; + Activity? activity = null; // In order to avoid routing twice we check for RouteData if (RoutingStateProvider?.RouteData is { } endpointRouteData) { - RecordDiagnostics(endpointRouteData.PageType.FullName, endpointRouteData.Template); + activity = RecordDiagnostics(endpointRouteData.PageType.FullName, endpointRouteData.Template); // Other routers shouldn't provide RouteData, this is specific to our router component // and must abide by our syntax and behaviors. @@ -238,6 +240,8 @@ internal virtual void Refresh(bool isNavigationIntercepted) // - Convert constrained parameters with (int, double, etc) to the target type. endpointRouteData = RouteTable.ProcessParameters(endpointRouteData); _renderHandle.Render(Found(endpointRouteData)); + + activity?.Stop(); return; } @@ -254,7 +258,7 @@ internal virtual void Refresh(bool isNavigationIntercepted) $"does not implement {typeof(IComponent).FullName}."); } - RecordDiagnostics(context.Handler.FullName, context.Entry.RoutePattern.RawText); + activity = RecordDiagnostics(context.Handler.FullName, context.Entry.RoutePattern.RawText); Log.NavigatingToComponent(_logger, context.Handler, locationPath, _baseUri); @@ -275,6 +279,8 @@ internal virtual void Refresh(bool isNavigationIntercepted) { if (!isNavigationIntercepted) { + activity = RecordDiagnostics("NotFound", "NotFound"); + Log.DisplayingNotFound(_logger, locationPath, _baseUri); // We did not find a Component that matches the route. @@ -284,20 +290,30 @@ internal virtual void Refresh(bool isNavigationIntercepted) } else { + activity = RecordDiagnostics("External", "External"); + Log.NavigatingToExternalUri(_logger, _locationAbsolute, locationPath, _baseUri); NavigationManager.NavigateTo(_locationAbsolute, forceLoad: true); } } + activity?.Stop(); + } - private void RecordDiagnostics(string componentType, string template) + private Activity? RecordDiagnostics(string componentType, string template) { - _renderHandle.ComponentActivitySource?.StartRouteActivity(componentType, template); + Activity? activity = null; + if (_renderHandle.ComponentActivitySource != null) + { + activity = _renderHandle.ComponentActivitySource.StartRouteActivity(componentType, template); + } if (_renderHandle.ComponentMetrics != null && _renderHandle.ComponentMetrics.IsNavigationEnabled) { _renderHandle.ComponentMetrics.Navigation(componentType, template); } + + return activity; } private static void DefaultNotFoundContent(RenderTreeBuilder builder) @@ -354,8 +370,6 @@ internal async ValueTask RunOnNavigateAsync(string path, bool isNavigationInterc private void OnLocationChanged(object sender, LocationChangedEventArgs args) { - _renderHandle.ComponentActivitySource?.StopRouteActivity(); - _locationAbsolute = args.Location; if (_renderHandle.IsInitialized && Routes != null) { diff --git a/src/Components/Components/test/Rendering/RenderingMetricsTest.cs b/src/Components/Components/test/Rendering/ComponentsMetricsTest.cs similarity index 62% rename from src/Components/Components/test/Rendering/RenderingMetricsTest.cs rename to src/Components/Components/test/Rendering/ComponentsMetricsTest.cs index c750ebec5ce4..15a468cc819d 100644 --- a/src/Components/Components/test/Rendering/RenderingMetricsTest.cs +++ b/src/Components/Components/test/Rendering/ComponentsMetricsTest.cs @@ -32,35 +32,13 @@ public void Constructor_CreatesMetersCorrectly() Assert.Equal(ComponentsMetrics.MeterName, _meterFactory.Meters[0].Name); } - [Fact] - public void EventDurationSync_RecordsDuration() - { - // Arrange - var componentsMetrics = new ComponentsMetrics(_meterFactory); - using var eventSyncDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.synchronous.duration"); - - // Act - var startTime = Stopwatch.GetTimestamp(); - Thread.Sleep(10); // Add a small delay to ensure a measurable duration - componentsMetrics.EventDurationSync(startTime, "TestComponent", "MyMethod", "OnClick"); - - // Assert - var measurements = eventSyncDurationCollector.GetMeasurementSnapshot(); - - Assert.Single(measurements); - Assert.True(measurements[0].Value > 0); - Assert.Equal("TestComponent", measurements[0].Tags["component.type"]); - Assert.Equal("OnClick", measurements[0].Tags["attribute.name"]); - } - [Fact] public async Task CaptureEventDurationAsync_RecordsDuration() { // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var eventAsyncDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.asynchronous.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.event.duration"); // Act var startTime = Stopwatch.GetTimestamp(); @@ -77,34 +55,13 @@ public async Task CaptureEventDurationAsync_RecordsDuration() Assert.Equal("MyMethod", measurements[0].Tags["component.method"]); } - [Fact] - public void ParametersDurationSync_RecordsDuration() - { - // Arrange - var componentsMetrics = new ComponentsMetrics(_meterFactory); - using var parametersSyncDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.synchronous.duration"); - - // Act - var startTime = Stopwatch.GetTimestamp(); - Thread.Sleep(10); // Add a small delay to ensure a measurable duration - componentsMetrics.ParametersDurationSync(startTime, "TestComponent"); - - // Assert - var measurements = parametersSyncDurationCollector.GetMeasurementSnapshot(); - - Assert.Single(measurements); - Assert.True(measurements[0].Value > 0); - Assert.Equal("TestComponent", measurements[0].Tags["component.type"]); - } - [Fact] public async Task CaptureParametersDurationAsync_RecordsDuration() { // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var parametersAsyncDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.asynchronous.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.parameters.duration"); // Act var startTime = Stopwatch.GetTimestamp(); @@ -119,28 +76,6 @@ public async Task CaptureParametersDurationAsync_RecordsDuration() Assert.Equal("TestComponent", measurements[0].Tags["component.type"]); } - [Fact] - public void DiffDuration_RecordsDuration() - { - // Arrange - var componentsMetrics = new ComponentsMetrics(_meterFactory); - using var diffDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.diff.duration"); - - // Act - var startTime = Stopwatch.GetTimestamp(); - Thread.Sleep(10); // Add a small delay to ensure a measurable duration - componentsMetrics.DiffDuration(startTime, "TestComponent", 5); - - // Assert - var measurements = diffDurationCollector.GetMeasurementSnapshot(); - - Assert.Single(measurements); - Assert.True(measurements[0].Value > 0); - Assert.Equal("TestComponent", measurements[0].Tags["component.type"]); - Assert.Equal(5, measurements[0].Tags["diff.length.bucket"]); - } - [Fact] public void BatchDuration_RecordsDuration() { @@ -168,7 +103,7 @@ public void EventFailed_RecordsException() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var eventExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.event.exception"); // Create a mock EventCallback var callback = new EventCallback(new TestComponent(), (Action)(() => { })); @@ -192,7 +127,7 @@ public async Task CaptureEventFailedAsync_RecordsException() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var eventExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.event.exception"); // Create a mock EventCallback var callback = new EventCallback(new TestComponent(), (Action)(() => { })); @@ -219,7 +154,7 @@ public void PropertiesFailed_RecordsException() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var parametersExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.parameters.exception"); // Act componentsMetrics.PropertiesFailed("ArgumentException", "TestComponent"); @@ -239,7 +174,7 @@ public async Task CapturePropertiesFailedAsync_RecordsException() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var parametersExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.parameters.exception"); // Create a task that throws an exception var task = Task.FromException(new InvalidOperationException()); @@ -304,20 +239,14 @@ public void EnabledProperties_ReflectMeterState() var componentsMetrics = new ComponentsMetrics(_meterFactory); // Create collectors to ensure the meters are enabled - using var eventSyncDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.synchronous.duration"); using var eventAsyncDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.asynchronous.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.event.duration"); using var eventExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.exception"); - using var parametersSyncDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.synchronous.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.event.exception"); using var parametersAsyncDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.asynchronous.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.parameters.duration"); using var parametersExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.exception"); - using var diffDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.diff.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.parameters.exception"); using var batchDurationCollector = new MetricCollector(_meterFactory, ComponentsMetrics.MeterName, "aspnetcore.components.rendering.batch.duration"); using var batchExceptionCollector = new MetricCollector(_meterFactory, @@ -326,70 +255,12 @@ public void EnabledProperties_ReflectMeterState() // Assert Assert.True(componentsMetrics.IsEventDurationEnabled); Assert.True(componentsMetrics.IsEventExceptionEnabled); - Assert.True(componentsMetrics.IsStateDurationEnabled); - Assert.True(componentsMetrics.IsStateExceptionEnabled); - Assert.True(componentsMetrics.IsDiffDurationEnabled); + Assert.True(componentsMetrics.IsParametersDurationEnabled); + Assert.True(componentsMetrics.IsParametersExceptionEnabled); Assert.True(componentsMetrics.IsBatchDurationEnabled); Assert.True(componentsMetrics.IsBatchExceptionEnabled); } - [Fact] - public void BucketEditLength_ReturnsCorrectBucket() - { - // Arrange - var componentsMetrics = new ComponentsMetrics(_meterFactory); - using var diffDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.diff.duration"); - - // Act & Assert - Test different diff lengths - var startTime = Stopwatch.GetTimestamp(); - - // Test each bucket boundary - componentsMetrics.DiffDuration(startTime, "Component", 1); - componentsMetrics.DiffDuration(startTime, "Component", 2); - componentsMetrics.DiffDuration(startTime, "Component", 5); - componentsMetrics.DiffDuration(startTime, "Component", 10); - componentsMetrics.DiffDuration(startTime, "Component", 50); - componentsMetrics.DiffDuration(startTime, "Component", 100); - componentsMetrics.DiffDuration(startTime, "Component", 500); - componentsMetrics.DiffDuration(startTime, "Component", 1000); - componentsMetrics.DiffDuration(startTime, "Component", 10000); - componentsMetrics.DiffDuration(startTime, "Component", 20000); // Should be 10001 - - // Assert - var measurements = diffDurationCollector.GetMeasurementSnapshot(); - - Assert.Equal(10, measurements.Count); - Assert.Equal(1, measurements[0].Tags["diff.length.bucket"]); - Assert.Equal(2, measurements[1].Tags["diff.length.bucket"]); - Assert.Equal(5, measurements[2].Tags["diff.length.bucket"]); - Assert.Equal(10, measurements[3].Tags["diff.length.bucket"]); - Assert.Equal(50, measurements[4].Tags["diff.length.bucket"]); - Assert.Equal(100, measurements[5].Tags["diff.length.bucket"]); - Assert.Equal(500, measurements[6].Tags["diff.length.bucket"]); - Assert.Equal(1000, measurements[7].Tags["diff.length.bucket"]); - Assert.Equal(10000, measurements[8].Tags["diff.length.bucket"]); - Assert.Equal(10001, measurements[9].Tags["diff.length.bucket"]); - } - - [Fact] - public void Dispose_DisposesUnderlyingMeter() - { - // This test verifies that the meter is disposed when the metrics instance is disposed - // This is a bit tricky to test directly, so we'll use an indirect approach - - // Arrange - var componentsMetrics = new ComponentsMetrics(_meterFactory); - - // Act - componentsMetrics.Dispose(); - - // Try to use the disposed meter - this should not throw since TestMeterFactory - // doesn't actually dispose the meter in test contexts - var startTime = Stopwatch.GetTimestamp(); - componentsMetrics.EventDurationSync(startTime, "TestComponent", "MyMethod", "OnClick"); - } - // Helper class for mock components public class TestComponent : IComponent, IHandleEvent { diff --git a/src/Components/Server/src/Circuits/CircuitFactory.cs b/src/Components/Server/src/Circuits/CircuitFactory.cs index cb8573bd81b1..6683c2e20d75 100644 --- a/src/Components/Server/src/Circuits/CircuitFactory.cs +++ b/src/Components/Server/src/Circuits/CircuitFactory.cs @@ -66,6 +66,7 @@ public async ValueTask CreateCircuitHostAsync( { navigationManager.Initialize(baseUri, uri); } + var componentsActivitySource = scope.ServiceProvider.GetService(); if (components.Count > 0) { @@ -109,6 +110,7 @@ public async ValueTask CreateCircuitHostAsync( navigationManager, circuitHandlers, _circuitMetrics, + componentsActivitySource, _loggerFactory.CreateLogger()); Log.CreatedCircuit(_logger, circuitHost); diff --git a/src/Components/Server/src/Circuits/CircuitHost.cs b/src/Components/Server/src/Circuits/CircuitHost.cs index ab461d8b62a2..38b50461ce3e 100644 --- a/src/Components/Server/src/Circuits/CircuitHost.cs +++ b/src/Components/Server/src/Circuits/CircuitHost.cs @@ -25,6 +25,7 @@ internal partial class CircuitHost : IAsyncDisposable private readonly RemoteNavigationManager _navigationManager; private readonly ILogger _logger; private readonly CircuitMetrics? _circuitMetrics; + private readonly ComponentsActivitySource? _componentsActivitySource; private Func, Task> _dispatchInboundActivity; private CircuitHandler[] _circuitHandlers; private bool _initialized; @@ -51,6 +52,7 @@ public CircuitHost( RemoteNavigationManager navigationManager, CircuitHandler[] circuitHandlers, CircuitMetrics? circuitMetrics, + ComponentsActivitySource? componentsActivitySource, ILogger logger) { CircuitId = circuitId; @@ -69,6 +71,7 @@ public CircuitHost( _navigationManager = navigationManager ?? throw new ArgumentNullException(nameof(navigationManager)); _circuitHandlers = circuitHandlers ?? throw new ArgumentNullException(nameof(circuitHandlers)); _circuitMetrics = circuitMetrics; + _componentsActivitySource = componentsActivitySource; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); Services = scope.ServiceProvider; @@ -105,7 +108,7 @@ public CircuitHost( // InitializeAsync is used in a fire-and-forget context, so it's responsible for its own // error handling. - public Task InitializeAsync(ProtectedPrerenderComponentApplicationStore store, CancellationToken cancellationToken) + public Task InitializeAsync(ProtectedPrerenderComponentApplicationStore store, ActivityContext httpContext, CancellationToken cancellationToken) { Log.InitializationStarted(_logger); @@ -115,15 +118,17 @@ public Task InitializeAsync(ProtectedPrerenderComponentApplicationStore store, C { throw new InvalidOperationException("The circuit host is already initialized."); } + Activity? activity = null; try { _initialized = true; // We're ready to accept incoming JSInterop calls from here on + activity = _componentsActivitySource?.StartCircuitActivity(CircuitId.Id, httpContext); _startTime = (_circuitMetrics != null && _circuitMetrics.IsDurationEnabled()) ? Stopwatch.GetTimestamp() : 0; // We only run the handlers in case we are in a Blazor Server scenario, which renders - // the components inmediately during start. + // the components immediately during start. // On Blazor Web scenarios we delay running these handlers until the first UpdateRootComponents call // We do this so that the handlers can have access to the restored application state. if (Descriptors.Count > 0) @@ -164,9 +169,13 @@ public Task InitializeAsync(ProtectedPrerenderComponentApplicationStore store, C _isFirstUpdate = Descriptors.Count == 0; Log.InitializationSucceeded(_logger); + + activity?.Stop(); } catch (Exception ex) { + _componentsActivitySource?.FailCircuitActivity(activity, ex); + // Report errors asynchronously. InitializeAsync is designed not to throw. Log.InitializationFailed(_logger, ex); UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false)); diff --git a/src/Components/Server/src/Circuits/RemoteRenderer.cs b/src/Components/Server/src/Circuits/RemoteRenderer.cs index 31b29206212b..7f2345bff74a 100644 --- a/src/Components/Server/src/Circuits/RemoteRenderer.cs +++ b/src/Components/Server/src/Circuits/RemoteRenderer.cs @@ -60,11 +60,11 @@ public RemoteRenderer( public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault(); - protected override ResourceAssetCollection Assets => _resourceCollection ?? base.Assets; + protected internal override ResourceAssetCollection Assets => _resourceCollection ?? base.Assets; - protected override RendererInfo RendererInfo => _componentPlatform; + protected internal override RendererInfo RendererInfo => _componentPlatform; - protected override IComponentRenderMode? GetComponentRenderMode(IComponent component) => RenderMode.InteractiveServer; + protected internal override IComponentRenderMode? GetComponentRenderMode(IComponent component) => RenderMode.InteractiveServer; public Task AddComponentAsync(Type componentType, ParameterView parameters, string domElementSelector) { @@ -306,7 +306,7 @@ public Task OnRenderCompletedAsync(long incomingBatchId, string? errorMessageOrN } } - protected override IComponent ResolveComponentForRenderMode([DynamicallyAccessedMembers(Component)] Type componentType, int? parentComponentId, IComponentActivator componentActivator, IComponentRenderMode renderMode) + protected internal override IComponent ResolveComponentForRenderMode([DynamicallyAccessedMembers(Component)] Type componentType, int? parentComponentId, IComponentActivator componentActivator, IComponentRenderMode renderMode) => renderMode switch { InteractiveServerRenderMode or InteractiveAutoRenderMode => componentActivator.CreateInstance(componentType), @@ -369,7 +369,7 @@ private async Task CaptureAsyncExceptions(Task task) } } - private static partial class Log + private static new partial class Log { [LoggerMessage(100, LogLevel.Warning, "Unhandled exception rendering component: {Message}", EventName = "ExceptionRenderingComponent")] private static partial void UnhandledExceptionRenderingComponent(ILogger logger, string message, Exception exception); diff --git a/src/Components/Server/src/ComponentHub.cs b/src/Components/Server/src/ComponentHub.cs index b3308f17bfd7..84561349ee48 100644 --- a/src/Components/Server/src/ComponentHub.cs +++ b/src/Components/Server/src/ComponentHub.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Diagnostics; using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Components.Server.Circuits; using Microsoft.AspNetCore.DataProtection; @@ -43,6 +44,7 @@ internal sealed partial class ComponentHub : Hub private readonly CircuitRegistry _circuitRegistry; private readonly ICircuitHandleRegistry _circuitHandleRegistry; private readonly ILogger _logger; + private readonly ActivityContext _httpContext; public ComponentHub( IServerComponentDeserializer serializer, @@ -60,6 +62,7 @@ public ComponentHub( _circuitRegistry = circuitRegistry; _circuitHandleRegistry = circuitHandleRegistry; _logger = logger; + _httpContext = ComponentsActivitySource.CaptureHttpContext(); } /// @@ -137,7 +140,7 @@ public async ValueTask StartCircuit(string baseUri, string uri, string s // SignalR message loop (we'd get a deadlock if any of the initialization // logic relied on receiving a subsequent message from SignalR), and it will // take care of its own errors anyway. - _ = circuitHost.InitializeAsync(store, Context.ConnectionAborted); + _ = circuitHost.InitializeAsync(store, _httpContext, Context.ConnectionAborted); // It's safe to *publish* the circuit now because nothing will be able // to run inside it until after InitializeAsync completes. diff --git a/src/Components/Server/test/Circuits/CircuitHostTest.cs b/src/Components/Server/test/Circuits/CircuitHostTest.cs index 47d7e7cf5a76..b68bdf8286fa 100644 --- a/src/Components/Server/test/Circuits/CircuitHostTest.cs +++ b/src/Components/Server/test/Circuits/CircuitHostTest.cs @@ -193,7 +193,7 @@ public async Task InitializeAsync_InvokesHandlers() var circuitHost = TestCircuitHost.Create(handlers: new[] { handler1.Object, handler2.Object }); // Act - await circuitHost.InitializeAsync(new ProtectedPrerenderComponentApplicationStore(Mock.Of()), cancellationToken); + await circuitHost.InitializeAsync(new ProtectedPrerenderComponentApplicationStore(Mock.Of()), default, cancellationToken); // Assert handler1.VerifyAll(); @@ -236,7 +236,7 @@ public async Task InitializeAsync_RendersRootComponentsInParallel() // Act object initializeException = null; circuitHost.UnhandledException += (sender, eventArgs) => initializeException = eventArgs.ExceptionObject; - var initializeTask = circuitHost.InitializeAsync(new ProtectedPrerenderComponentApplicationStore(Mock.Of()), cancellationToken); + var initializeTask = circuitHost.InitializeAsync(new ProtectedPrerenderComponentApplicationStore(Mock.Of()), default, cancellationToken); await initializeTask.WaitAsync(initializeTimeout); // Assert: This was not reached only because an exception was thrown in InitializeAsync() @@ -266,7 +266,7 @@ public async Task InitializeAsync_ReportsOwnAsyncExceptions() }; // Act - var initializeAsyncTask = circuitHost.InitializeAsync(new ProtectedPrerenderComponentApplicationStore(Mock.Of()), new CancellationToken()); + var initializeAsyncTask = circuitHost.InitializeAsync(new ProtectedPrerenderComponentApplicationStore(Mock.Of()), default, new CancellationToken()); // Assert: No synchronous exceptions handler.VerifyAll(); diff --git a/src/Components/Server/test/Circuits/TestCircuitHost.cs b/src/Components/Server/test/Circuits/TestCircuitHost.cs index eeb86ad3a639..11e9f7ed4d65 100644 --- a/src/Components/Server/test/Circuits/TestCircuitHost.cs +++ b/src/Components/Server/test/Circuits/TestCircuitHost.cs @@ -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; using System.Diagnostics.Metrics; using Microsoft.AspNetCore.InternalTesting; using Microsoft.AspNetCore.SignalR; @@ -15,8 +16,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits; internal class TestCircuitHost : CircuitHost { - private TestCircuitHost(CircuitId circuitId, AsyncServiceScope scope, CircuitOptions options, CircuitClientProxy client, RemoteRenderer renderer, IReadOnlyList descriptors, RemoteJSRuntime jsRuntime, RemoteNavigationManager navigationManager, CircuitHandler[] circuitHandlers, CircuitMetrics circuitMetrics, ILogger logger) - : base(circuitId, scope, options, client, renderer, descriptors, jsRuntime, navigationManager, circuitHandlers, circuitMetrics, logger) + private TestCircuitHost(CircuitId circuitId, AsyncServiceScope scope, CircuitOptions options, CircuitClientProxy client, RemoteRenderer renderer, IReadOnlyList descriptors, RemoteJSRuntime jsRuntime, RemoteNavigationManager navigationManager, CircuitHandler[] circuitHandlers, CircuitMetrics circuitMetrics, ComponentsActivitySource componentsActivitySource, ILogger logger) + : base(circuitId, scope, options, client, renderer, descriptors, jsRuntime, navigationManager, circuitHandlers, circuitMetrics, componentsActivitySource, logger) { } @@ -38,6 +39,7 @@ public static CircuitHost Create( .Returns(jsRuntime); var serverComponentDeserializer = Mock.Of(); var circuitMetrics = new CircuitMetrics(new TestMeterFactory()); + var componentsActivitySource = new ComponentsActivitySource(); if (remoteRenderer == null) { @@ -64,6 +66,7 @@ public static CircuitHost Create( navigationManager, handlers, circuitMetrics, + componentsActivitySource, NullLogger.Instance); } } From 0a4d488faaea0f547ba0d92cf956f6227e3b48b2 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 25 Apr 2025 20:21:00 +0200 Subject: [PATCH 9/9] IsAllDataRequested --- .../src/ComponentsActivitySource.cs | 139 +++++++++--------- 1 file changed, 73 insertions(+), 66 deletions(-) diff --git a/src/Components/Components/src/ComponentsActivitySource.cs b/src/Components/Components/src/ComponentsActivitySource.cs index 086de83edefc..592d3bee9164 100644 --- a/src/Components/Components/src/ComponentsActivitySource.cs +++ b/src/Components/Components/src/ComponentsActivitySource.cs @@ -24,7 +24,7 @@ internal class ComponentsActivitySource public static ActivityContext CaptureHttpContext() { var parentActivity = Activity.Current; - if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn") + if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn" && parentActivity.Recorded) { return parentActivity.Context; } @@ -34,28 +34,24 @@ public static ActivityContext CaptureHttpContext() public Activity? StartCircuitActivity(string circuitId, ActivityContext httpContext) { _circuitId = circuitId; - IEnumerable> tags = - [ - new("circuit.id", _circuitId ?? "unknown"), - ]; - var links = new List(); - if (httpContext != default) - { - _httpContext = httpContext; - links.Add(new ActivityLink(httpContext)); - } - - var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Server, parentId:null, tags, links); + var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Server, parentId: null, null, null); if (activity is not null) { - activity.DisplayName = $"CIRCUIT {circuitId ?? "unknown"}"; + if (activity.IsAllDataRequested) + { + if (_circuitId != null) + { + activity.SetTag("circuit.id", _circuitId); + } + if (httpContext != default) + { + activity.AddLink(new ActivityLink(httpContext)); + } + } + activity.DisplayName = $"CIRCUIT {circuitId ?? ""}"; activity.Start(); _circuitContext = activity.Context; - - Console.WriteLine($"StartCircuitActivity: {circuitId}"); - Console.WriteLine($"circuitContext: {_circuitContext.TraceId} {_circuitContext.SpanId} {_circuitContext.TraceState}"); - Console.WriteLine($"httpContext: {httpContext.TraceId} {httpContext.SpanId} {httpContext.TraceState}"); } return activity; } @@ -73,71 +69,82 @@ public void FailCircuitActivity(Activity activity, Exception ex) public Activity? StartRouteActivity(string componentType, string route) { - IEnumerable> tags = - [ - new("circuit.id", _circuitId ?? "unknown"), - new("component.type", componentType ?? "unknown"), - new("route", route ?? "unknown"), - ]; - var links = new List(); if (_httpContext == default) { _httpContext = CaptureHttpContext(); } - if (_httpContext != default) - { - links.Add(new ActivityLink(_httpContext)); - } - if (_circuitContext != default) - { - links.Add(new ActivityLink(_circuitContext)); - } - var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Server, parentId: null, tags, links); + var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Server, parentId: null, null, null); if (activity is not null) { - _routeContext = activity.Context; + if (activity.IsAllDataRequested) + { + if (_circuitId != null) + { + activity.SetTag("circuit.id", _circuitId); + } + if (componentType != null) + { + activity.SetTag("component.type", componentType); + } + if (route != null) + { + activity.SetTag("route", route); + } + if (_httpContext != default) + { + activity.AddLink(new ActivityLink(_httpContext)); + } + if (_circuitContext != default) + { + activity.AddLink(new ActivityLink(_circuitContext)); + } + } + activity.DisplayName = $"ROUTE {route ?? "unknown"} -> {componentType ?? "unknown"}"; activity.Start(); + _routeContext = activity.Context; } return activity; } - public void StopRouteActivity(Activity activity) - { - _routeContext = default; - if (!activity.IsStopped) - { - activity.Stop(); - } - } - public Activity? StartEventActivity(string? componentType, string? methodName, string? attributeName) { - IEnumerable> tags = - [ - new("circuit.id", _circuitId ?? "unknown"), - new("component.type", componentType ?? "unknown"), - new("component.method", methodName ?? "unknown"), - new("attribute.name", attributeName ?? "unknown"), - ]; - var links = new List(); - if (_httpContext != default) - { - links.Add(new ActivityLink(_httpContext)); - } - if (_circuitContext != default) - { - links.Add(new ActivityLink(_circuitContext)); - } - if (_routeContext != default) - { - links.Add(new ActivityLink(_routeContext)); - } - - var activity = ActivitySource.CreateActivity(OnEventName, ActivityKind.Server, parentId: null, tags, links); + var activity = ActivitySource.CreateActivity(OnEventName, ActivityKind.Server, parentId: null, null, null); if (activity is not null) { + if (activity.IsAllDataRequested) + { + if (_circuitId != null) + { + activity.SetTag("circuit.id", _circuitId); + } + if (componentType != null) + { + activity.SetTag("component.type", componentType); + } + if (methodName != null) + { + activity.SetTag("component.method", methodName); + } + if (attributeName != null) + { + activity.SetTag("attribute.name", attributeName); + } + if (_httpContext != default) + { + activity.AddLink(new ActivityLink(_httpContext)); + } + if (_circuitContext != default) + { + activity.AddLink(new ActivityLink(_circuitContext)); + } + if (_routeContext != default) + { + activity.AddLink(new ActivityLink(_routeContext)); + } + } + activity.DisplayName = $"EVENT {attributeName ?? "unknown"} -> {componentType ?? "unknown"}.{methodName ?? "unknown"}"; activity.Start(); }