File: Rendering\RenderingMetricsTest.cs
Web Access
Project: src\src\Components\Components\test\Microsoft.AspNetCore.Components.Tests.csproj (Microsoft.AspNetCore.Components.Tests)
// 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.Extensions.Diagnostics.Metrics.Testing;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.InternalTesting;
using Moq;
 
namespace Microsoft.AspNetCore.Components.Rendering;
 
public class RenderingMetricsTest
{
    private readonly TestMeterFactory _meterFactory;
 
    public RenderingMetricsTest()
    {
        _meterFactory = new TestMeterFactory();
    }
 
    [Fact]
    public void Constructor_CreatesMetersCorrectly()
    {
        // Arrange & Act
        var renderingMetrics = new RenderingMetrics(_meterFactory);
 
        // Assert
        Assert.Single(_meterFactory.Meters);
        Assert.Equal(RenderingMetrics.MeterName, _meterFactory.Meters[0].Name);
    }
 
    [Fact]
    public void RenderStart_IncreasesCounters()
    {
        // Arrange
        var renderingMetrics = new RenderingMetrics(_meterFactory);
        using var totalCounter = new MetricCollector<long>(_meterFactory,
            RenderingMetrics.MeterName, "aspnetcore.components.rendering.count");
        using var activeCounter = new MetricCollector<long>(_meterFactory,
            RenderingMetrics.MeterName, "aspnetcore.components.rendering.active_renders");
 
        var componentType = "TestComponent";
 
        // Act
        renderingMetrics.RenderStart(componentType);
 
        // Assert
        var totalMeasurements = totalCounter.GetMeasurementSnapshot();
        var activeMeasurements = activeCounter.GetMeasurementSnapshot();
 
        Assert.Single(totalMeasurements);
        Assert.Equal(1, totalMeasurements[0].Value);
        Assert.Equal(componentType, totalMeasurements[0].Tags["component.type"]);
 
        Assert.Single(activeMeasurements);
        Assert.Equal(1, activeMeasurements[0].Value);
        Assert.Equal(componentType, activeMeasurements[0].Tags["component.type"]);
    }
 
    [Fact]
    public void RenderEnd_DecreasesActiveCounterAndRecordsDuration()
    {
        // Arrange
        var renderingMetrics = new RenderingMetrics(_meterFactory);
        using var activeCounter = new MetricCollector<long>(_meterFactory,
            RenderingMetrics.MeterName, "aspnetcore.components.rendering.active_renders");
        using var durationCollector = new MetricCollector<double>(_meterFactory,
            RenderingMetrics.MeterName, "aspnetcore.components.rendering.duration");
 
        var componentType = "TestComponent";
 
        // 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);
 
        // Assert
        var activeMeasurements = activeCounter.GetMeasurementSnapshot();
        var durationMeasurements = durationCollector.GetMeasurementSnapshot();
 
        Assert.Single(activeMeasurements);
        Assert.Equal(-1, activeMeasurements[0].Value);
        Assert.Equal(componentType, activeMeasurements[0].Tags["component.type"]);
 
        Assert.Single(durationMeasurements);
        Assert.True(durationMeasurements[0].Value > 0);
        Assert.Equal(componentType, durationMeasurements[0].Tags["component.type"]);
    }
 
    [Fact]
    public void RenderEnd_AddsErrorTypeTag_WhenExceptionIsProvided()
    {
        // Arrange
        var renderingMetrics = new RenderingMetrics(_meterFactory);
        using var durationCollector = new MetricCollector<double>(_meterFactory,
            RenderingMetrics.MeterName, "aspnetcore.components.rendering.duration");
 
        var componentType = "TestComponent";
        var exception = new InvalidOperationException("Test exception");
 
        // Act
        var startTime = Stopwatch.GetTimestamp();
        Thread.Sleep(10);
        var endTime = Stopwatch.GetTimestamp();
        renderingMetrics.RenderEnd(componentType, exception, startTime, endTime);
 
        // Assert
        var durationMeasurements = durationCollector.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"]);
    }
 
    [Fact]
    public void IsDurationEnabled_ReturnsMeterEnabledState()
    {
        // Arrange
        var renderingMetrics = new RenderingMetrics(_meterFactory);
 
        // Create a collector to ensure the meter is enabled
        using var durationCollector = new MetricCollector<double>(_meterFactory,
            RenderingMetrics.MeterName, "aspnetcore.components.rendering.duration");
 
        // Act & Assert
        Assert.True(renderingMetrics.IsDurationEnabled());
    }
 
    [Fact]
    public void FullRenderingLifecycle_RecordsAllMetricsCorrectly()
    {
        // Arrange
        var renderingMetrics = new RenderingMetrics(_meterFactory);
        using var totalCounter = new MetricCollector<long>(_meterFactory,
            RenderingMetrics.MeterName, "aspnetcore.components.rendering.count");
        using var activeCounter = new MetricCollector<long>(_meterFactory,
            RenderingMetrics.MeterName, "aspnetcore.components.rendering.active_renders");
        using var durationCollector = new MetricCollector<double>(_meterFactory,
            RenderingMetrics.MeterName, "aspnetcore.components.rendering.duration");
 
        var componentType = "TestComponent";
 
        // Act - Simulating a full rendering lifecycle
        var startTime = Stopwatch.GetTimestamp();
 
        // 1. Component render starts
        renderingMetrics.RenderStart(componentType);
 
        // 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);
 
        // Assert
        var totalMeasurements = totalCounter.GetMeasurementSnapshot();
        var activeMeasurements = activeCounter.GetMeasurementSnapshot();
        var durationMeasurements = durationCollector.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"]);
 
        // 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"]);
 
        // 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"]);
    }
 
    [Fact]
    public void MultipleRenders_TracksMetricsIndependently()
    {
        // Arrange
        var renderingMetrics = new RenderingMetrics(_meterFactory);
        using var totalCounter = new MetricCollector<long>(_meterFactory,
            RenderingMetrics.MeterName, "aspnetcore.components.rendering.count");
        using var activeCounter = new MetricCollector<long>(_meterFactory,
            RenderingMetrics.MeterName, "aspnetcore.components.rendering.active_renders");
        using var durationCollector = new MetricCollector<double>(_meterFactory,
            RenderingMetrics.MeterName, "aspnetcore.components.rendering.duration");
 
        var componentType1 = "TestComponent1";
        var componentType2 = "TestComponent2";
 
        // Act
        // First component render
        var startTime1 = Stopwatch.GetTimestamp();
        renderingMetrics.RenderStart(componentType1);
 
        // Second component render starts while first is still rendering
        var startTime2 = Stopwatch.GetTimestamp();
        renderingMetrics.RenderStart(componentType2);
 
        // First component render ends
        Thread.Sleep(5);
        var endTime1 = Stopwatch.GetTimestamp();
        renderingMetrics.RenderEnd(componentType1, null, startTime1, endTime1);
 
        // Second component render ends
        Thread.Sleep(5);
        var endTime2 = Stopwatch.GetTimestamp();
        renderingMetrics.RenderEnd(componentType2, null, startTime2, endTime2);
 
        // 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);
    }
}