File: HistogramAllocationTests.cs
Web Access
Project: src\tests\Aspire.Dashboard.Tests\Aspire.Dashboard.Tests.csproj (Aspire.Dashboard.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Otlp.Model.MetricValues;
using Aspire.Tests.Shared.Telemetry;
using Xunit;
 
namespace Aspire.Dashboard.Tests;
 
public class HistogramAllocationTests
{
    private static readonly DateTime s_start = new(2025, 6, 15, 12, 0, 0, DateTimeKind.Utc);
 
    [Fact]
    public void Create_UnchangedCumulativeSnapshot_DoesNotAllocate()
    {
        var point = HistogramTestHelpers.CreatePoint(s_start, s_start.AddSeconds(1), [1, 2, 3, 4], [10, 50, 100]);
        var previous = HistogramValue.Create(point, OtlpAggregationTemporality.Cumulative, previous: null);
 
        // Warm the synchronous path before measuring so one-time runtime initialization is excluded.
        for (var i = 0; i < 100; i++)
        {
            HistogramValue.Create(point, OtlpAggregationTemporality.Cumulative, previous);
        }
 
        var current = previous;
        var before = GC.GetAllocatedBytesForCurrentThread();
        for (var i = 0; i < 1_000; i++)
        {
            point.TimeUnixNano += 1_000;
            current = HistogramValue.Create(point, OtlpAggregationTemporality.Cumulative, previous);
        }
        var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
 
        Assert.Equal(0, allocated);
        Assert.Same(previous, current);
        Assert.Equal(OtlpHelpers.UnixNanoSecondsToDateTime(point.TimeUnixNano), previous.End);
        Assert.Equal(point.TimeUnixNano, previous.EndTimeUnixNano);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void Add_MatchingBounds_DoesNotAllocate(bool cumulative)
    {
        ulong[] originalCounts = [0, 0, 0];
        double[] originalBounds = [10, 100];
        ulong[] incoming = [2, 4, 6];
        ulong[] previous = cumulative ? [1, 2, 3] : [];
        ulong[]? counts = originalCounts;
        double[]? bounds = originalBounds;
        for (var i = 0; i < 100; i++)
        {
            HistogramBuckets.Add(ref counts, ref bounds, incoming, originalBounds, previous);
        }
 
        var added = false;
        var before = GC.GetAllocatedBytesForCurrentThread();
        for (var i = 0; i < 1_000; i++)
        {
            added = HistogramBuckets.Add(ref counts, ref bounds, incoming, originalBounds, previous);
        }
        var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
 
        Assert.Equal(0, allocated);
        Assert.True(added);
        Assert.Same(originalCounts, counts);
        Assert.Same(originalBounds, bounds);
        Assert.Equal(cumulative ? [1_100ul, 2_200ul, 3_300ul] : [2_200ul, 4_400ul, 6_600ul], originalCounts);
    }
 
    [Fact]
    public void Add_FinerBoundsIntoCoarserAccumulator_DoesNotAllocate()
    {
        ulong[] originalCounts = [0, 0, 0];
        double[] originalBounds = [10, 100];
        ulong[] incoming = [1, 2, 3, 4];
        double[] incomingBounds = [10, 50, 100];
        ulong[]? counts = originalCounts;
        double[]? bounds = originalBounds;
        for (var i = 0; i < 100; i++)
        {
            HistogramBuckets.Add(ref counts, ref bounds, incoming, incomingBounds, []);
        }
 
        var before = GC.GetAllocatedBytesForCurrentThread();
        for (var i = 0; i < 1_000; i++)
        {
            HistogramBuckets.Add(ref counts, ref bounds, incoming, incomingBounds, []);
        }
        var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
 
        Assert.Equal(0, allocated);
        Assert.Same(originalCounts, counts);
        Assert.Same(originalBounds, bounds);
        Assert.Equal([1_100ul, 5_500ul, 4_400ul], originalCounts);
    }
 
    [Fact]
    public void Add_UnchangedBuckets_DoesNotAllocateAccumulator()
    {
        ulong[] incoming = [1, 2, 3];
        double[] incomingBounds = [10, 100];
        ulong[]? counts = null;
        double[]? bounds = null;
        for (var i = 0; i < 100; i++)
        {
            HistogramBuckets.Add(ref counts, ref bounds, incoming, incomingBounds, incoming);
        }
 
        var added = true;
        var before = GC.GetAllocatedBytesForCurrentThread();
        for (var i = 0; i < 1_000; i++)
        {
            added = HistogramBuckets.Add(ref counts, ref bounds, incoming, incomingBounds, incoming);
        }
        var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
 
        Assert.Equal(0, allocated);
        Assert.False(added);
        Assert.Null(counts);
        Assert.Null(bounds);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void AddBounds_CompatibleLayouts_DoesNotAllocate(bool cumulative)
    {
        double[] originalBounds = [10, 100];
        double[] incomingBounds = [10, 50, 100];
        ulong[] incoming = [2, 4, 6, 8];
        ulong[] previous = cumulative ? [1, 2, 3, 4] : [];
        double[]? bounds = originalBounds;
        for (var i = 0; i < 100; i++)
        {
            HistogramBuckets.AddBounds(ref bounds, incoming, incomingBounds, previous);
        }
 
        var added = false;
        var before = GC.GetAllocatedBytesForCurrentThread();
        for (var i = 0; i < 1_000; i++)
        {
            added = HistogramBuckets.AddBounds(ref bounds, incoming, incomingBounds, previous);
        }
        var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
 
        Assert.Equal(0, allocated);
        Assert.True(added);
        Assert.Same(originalBounds, bounds);
    }
}