| File: Otlp\Model\MetricValues\HistogramBuckets.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. namespace Aspire.Dashboard.Otlp.Model.MetricValues; /// <summary> /// Combines histogram distributions by merging buckets at shared explicit boundaries. /// </summary> internal static class HistogramBuckets { public static bool Add(ref ulong[]? counts, ref double[]? bounds, ReadOnlySpan<ulong> incomingCounts, double[] incomingBounds, ReadOnlySpan<ulong> previousCounts) { if (!HasObservations(incomingCounts, previousCounts)) { return false; } if (counts is null) { counts = new ulong[incomingCounts.Length]; bounds = incomingBounds; } var currentBounds = bounds ?? throw new InvalidOperationException("Histogram bucket counts have no explicit boundaries."); if (!currentBounds.AsSpan().SequenceEqual(incomingBounds)) { // Only shared boundaries allow exact merging. Splitting a source bucket would require // inventing a distribution within it. OTLP buckets are (lower, upper], plus +Inf: // https://opentelemetry.io/docs/specs/otel/metrics/data-model/#histogram var sharedBounds = GetSharedBounds(currentBounds, incomingBounds); if (!ReferenceEquals(sharedBounds, currentBounds) || counts.Length != sharedBounds.Length + 1) { var merged = new ulong[sharedBounds.Length + 1]; AddBuckets(merged, counts, currentBounds, sharedBounds, []); counts = merged; } AddBuckets(counts, incomingCounts, incomingBounds, sharedBounds, previousCounts); bounds = sharedBounds; return true; } if (counts.Length != incomingCounts.Length) { throw new InvalidOperationException("Histogram bucket counts do not match their explicit boundaries."); } for (var i = 0; i < counts.Length; i++) { var value = incomingCounts[i] - (previousCounts.IsEmpty ? 0 : previousCounts[i]); counts[i] = checked(counts[i] + value); } return true; } public static bool AddBounds(ref double[]? bounds, ReadOnlySpan<ulong> incomingCounts, double[] incomingBounds, ReadOnlySpan<ulong> previousCounts) { if (!HasObservations(incomingCounts, previousCounts)) { return false; } bounds = bounds is null ? incomingBounds : GetSharedBounds(bounds, incomingBounds); return true; } private static bool HasObservations(ReadOnlySpan<ulong> incomingCounts, ReadOnlySpan<ulong> previousCounts) { if (!previousCounts.IsEmpty && previousCounts.Length != incomingCounts.Length) { throw new InvalidOperationException("Cumulative histogram bucket counts changed size."); } for (var i = 0; i < incomingCounts.Length; i++) { if (incomingCounts[i] != (previousCounts.IsEmpty ? 0 : previousCounts[i])) { return true; } } return false; } private static void AddBuckets(ulong[] destination, ReadOnlySpan<ulong> counts, double[] bounds, double[] sharedBounds, ReadOnlySpan<ulong> previousCounts) { var target = 0; for (var i = 0; i < counts.Length; i++) { while (target < sharedBounds.Length && (i >= bounds.Length || bounds[i] > sharedBounds[target])) { target++; } var value = counts[i] - (previousCounts.IsEmpty ? 0 : previousCounts[i]); destination[target] = checked(destination[target] + value); } } private static double[] GetSharedBounds(double[] left, double[] right) { // OTLP explicit bounds are strictly increasing, so intersect them with a linear merge. // Reuse either existing layout when it is already the common coarser layout. var count = 0; var leftIndex = 0; var rightIndex = 0; while (leftIndex < left.Length && rightIndex < right.Length) { if (left[leftIndex].Equals(right[rightIndex])) { count++; leftIndex++; rightIndex++; } else if (left[leftIndex] < right[rightIndex]) { leftIndex++; } else { rightIndex++; } } if (count == left.Length) { return left; } if (count == right.Length) { return right; } if (count == 0) { return []; } var shared = new double[count]; leftIndex = 0; rightIndex = 0; var sharedIndex = 0; while (sharedIndex < count) { if (left[leftIndex].Equals(right[rightIndex])) { shared[sharedIndex++] = left[leftIndex]; leftIndex++; rightIndex++; } else if (left[leftIndex] < right[rightIndex]) { leftIndex++; } else { rightIndex++; } } return shared; } }