File: Components\Controls\Chart\MetricInstrumentDataCache.cs
Web Access
Project: src\src\Aspire.Dashboard\Aspire.Dashboard.csproj (Aspire.Dashboard)
// 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.Dashboard.Otlp.Storage;
 
namespace Aspire.Dashboard.Components;
 
internal static class MetricInstrumentDataCache
{
    public static List<MetricDimensionCursor> CreateCursors(OtlpInstrumentData data, TimeSpan refreshLookback, TimeSpan dataPointInterval)
    {
        ArgumentOutOfRangeException.ThrowIfLessThan(refreshLookback, TimeSpan.Zero);
        ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(dataPointInterval, TimeSpan.Zero);
 
        var cursors = new List<MetricDimensionCursor>(data.Dimensions.Count);
        foreach (var dimension in data.Dimensions)
        {
            if (dimension.Values.Count == 0)
            {
                continue;
            }
 
            var latestValue = dimension.Values[^1];
            // Refresh recent complete buckets because data can arrive behind the latest displayed value. Starting from
            // the rolled value's start when it is earlier also retains source points that determine its representative.
            var lookbackStartTicks = latestValue.End.Ticks - Math.Min(latestValue.End.Ticks, refreshLookback.Ticks);
            var alignedLookbackStartTicks = lookbackStartTicks - (lookbackStartTicks % dataPointInterval.Ticks);
            cursors.Add(new MetricDimensionCursor
            {
                Attributes = dimension.Attributes,
                StartTime = new DateTime(Math.Min(latestValue.Start.Ticks, alignedLookbackStartTicks), DateTimeKind.Utc)
            });
        }
        return cursors;
    }
 
    public static OtlpInstrumentData Merge(
        OtlpInstrumentData cached,
        OtlpInstrumentData refreshed,
        IReadOnlyList<MetricDimensionCursor> cursors,
        DateTime windowStart)
    {
        var dimensions = new List<DimensionScope>(refreshed.Dimensions.Count);
        foreach (var refreshedDimension in refreshed.Dimensions)
        {
            var cachedDimension = cached.Dimensions.FirstOrDefault(dimension => AttributesEqual(dimension.Attributes, refreshedDimension.Attributes));
            var cursor = cursors.FirstOrDefault(cursor => AttributesEqual(cursor.Attributes, refreshedDimension.Attributes));
            if (cachedDimension is null || cursor is null)
            {
                dimensions.Add(CloneDimension(refreshedDimension, windowStart));
                continue;
            }
 
            var mergedDimension = new DimensionScope(refreshedDimension.Capacity, refreshedDimension.Attributes);
            foreach (var value in cachedDimension.Values)
            {
                if (value.End >= windowStart && value.End < cursor.StartTime)
                {
                    mergedDimension.Values.Add(MetricValueBase.Clone(value));
                }
            }
            foreach (var value in refreshedDimension.Values)
            {
                if (value.End >= windowStart)
                {
                    mergedDimension.Values.Add(MetricValueBase.Clone(value));
                }
            }
            dimensions.Add(mergedDimension);
        }
 
        return new OtlpInstrumentData
        {
            Summary = refreshed.Summary,
            Dimensions = dimensions,
            KnownAttributeValues = refreshed.KnownAttributeValues,
            HasOverflow = refreshed.HasOverflow
        };
    }
 
    private static DimensionScope CloneDimension(DimensionScope dimension, DateTime windowStart)
    {
        var clone = new DimensionScope(dimension.Capacity, dimension.Attributes);
        foreach (var value in dimension.Values)
        {
            if (value.End >= windowStart)
            {
                clone.Values.Add(MetricValueBase.Clone(value));
            }
        }
        return clone;
    }
 
    private static bool AttributesEqual(KeyValuePair<string, string>[] left, KeyValuePair<string, string>[] right)
        => left.AsSpan().SequenceEqual(right);
}