Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/OpenTelemetry/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ Notes](../../RELEASENOTES.md).
* Added support for `Meter.TelemetrySchemaUrl` property.
([#6714](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6714))

* Improve performance and reduce memory consumption for metrics histograms.
([#6715](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6715))

## 1.14.0

Released 2025-Nov-12
Expand Down
26 changes: 13 additions & 13 deletions src/OpenTelemetry/Metrics/AggregatorStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ internal sealed class AggregatorStore
private readonly MetricPoint[] metricPoints;
private readonly int[] currentMetricPointBatch;
private readonly AggregationType aggType;
private readonly double[] histogramBounds;
private readonly HistogramExplicitBounds histogramExplicitBounds;
private readonly int exponentialHistogramMaxSize;
private readonly int exponentialHistogramMaxScale;
private readonly UpdateLongDelegate updateLongCallback;
Expand Down Expand Up @@ -74,7 +74,7 @@ internal AggregatorStore(
this.currentMetricPointBatch = new int[this.NumberOfMetricPoints];
this.aggType = aggType;
this.OutputDelta = temporality == AggregationTemporality.Delta;
this.histogramBounds = metricStreamIdentity.HistogramBucketBounds ?? FindDefaultHistogramBounds(in metricStreamIdentity);
this.histogramExplicitBounds = new(metricStreamIdentity.HistogramBucketBounds ?? FindDefaultHistogramBounds(in metricStreamIdentity));
this.exponentialHistogramMaxSize = metricStreamIdentity.ExponentialHistogramMaxSize;
this.exponentialHistogramMaxScale = metricStreamIdentity.ExponentialHistogramMaxScale;
this.StartTimeExclusive = DateTimeOffset.UtcNow;
Expand Down Expand Up @@ -143,7 +143,7 @@ internal AggregatorStore(

internal DateTimeOffset EndTimeInclusive { get; private set; }

internal double[] HistogramBounds => this.histogramBounds;
internal double[] HistogramBounds => this.histogramExplicitBounds.Bounds;

internal bool IsExemplarEnabled()
{
Expand Down Expand Up @@ -393,11 +393,11 @@ private void InitializeZeroTagPointIfNotInitialized()
if (this.OutputDelta)
{
var lookupData = new LookupData(0, Tags.EmptyTags, Tags.EmptyTags);
this.metricPoints[0] = new MetricPoint(this, this.aggType, null, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale, lookupData);
this.metricPoints[0] = new MetricPoint(this, this.aggType, null, this.histogramExplicitBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale, lookupData);
}
else
{
this.metricPoints[0] = new MetricPoint(this, this.aggType, null, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale);
this.metricPoints[0] = new MetricPoint(this, this.aggType, null, this.histogramExplicitBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale);
}

this.zeroTagMetricPointInitialized = true;
Expand All @@ -421,11 +421,11 @@ private void InitializeOverflowTagPointIfNotInitialized()
if (this.OutputDelta)
{
var lookupData = new LookupData(1, tags, tags);
this.metricPoints[1] = new MetricPoint(this, this.aggType, keyValuePairs, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale, lookupData);
this.metricPoints[1] = new MetricPoint(this, this.aggType, keyValuePairs, this.histogramExplicitBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale, lookupData);
}
else
{
this.metricPoints[1] = new MetricPoint(this, this.aggType, keyValuePairs, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale);
this.metricPoints[1] = new MetricPoint(this, this.aggType, keyValuePairs, this.histogramExplicitBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale);
}

this.overflowTagMetricPointInitialized = true;
Expand Down Expand Up @@ -494,7 +494,7 @@ private int LookupAggregatorStore(KeyValuePair<string, object?>[] tagKeysAndValu
}

ref var metricPoint = ref this.metricPoints[aggregatorIndex];
metricPoint = new MetricPoint(this, this.aggType, sortedTags.KeyValuePairs, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale);
metricPoint = new MetricPoint(this, this.aggType, sortedTags.KeyValuePairs, this.histogramExplicitBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale);

// Add to dictionary *after* initializing MetricPoint
// as other threads can start writing to the
Expand Down Expand Up @@ -543,7 +543,7 @@ private int LookupAggregatorStore(KeyValuePair<string, object?>[] tagKeysAndValu
}

ref var metricPoint = ref this.metricPoints[aggregatorIndex];
metricPoint = new MetricPoint(this, this.aggType, givenTags.KeyValuePairs, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale);
metricPoint = new MetricPoint(this, this.aggType, givenTags.KeyValuePairs, this.histogramExplicitBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale);

// Add to dictionary *after* initializing MetricPoint
// as other threads can start writing to the
Expand Down Expand Up @@ -619,7 +619,7 @@ private int LookupAggregatorStoreForDeltaWithReclaim(KeyValuePair<string, object
lookupData = new LookupData(index, sortedTags, givenTags);

ref var metricPoint = ref this.metricPoints[index];
metricPoint = new MetricPoint(this, this.aggType, sortedTags.KeyValuePairs, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale, lookupData);
metricPoint = new MetricPoint(this, this.aggType, sortedTags.KeyValuePairs, this.histogramExplicitBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale, lookupData);
newMetricPointCreated = true;

// Add to dictionary *after* initializing MetricPoint
Expand Down Expand Up @@ -665,7 +665,7 @@ private int LookupAggregatorStoreForDeltaWithReclaim(KeyValuePair<string, object
lookupData = new LookupData(index, Tags.EmptyTags, givenTags);

ref var metricPoint = ref this.metricPoints[index];
metricPoint = new MetricPoint(this, this.aggType, givenTags.KeyValuePairs, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale, lookupData);
metricPoint = new MetricPoint(this, this.aggType, givenTags.KeyValuePairs, this.histogramExplicitBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale, lookupData);
newMetricPointCreated = true;

// Add to dictionary *after* initializing MetricPoint
Expand Down Expand Up @@ -776,7 +776,7 @@ private bool TryGetAvailableMetricPointRare(
lookupData = new LookupData(index, sortedTags, givenTags);

ref var metricPoint = ref this.metricPoints[index];
metricPoint = new MetricPoint(this, this.aggType, sortedTags.KeyValuePairs, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale, lookupData);
metricPoint = new MetricPoint(this, this.aggType, sortedTags.KeyValuePairs, this.histogramExplicitBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale, lookupData);
newMetricPointCreated = true;

// Add to dictionary *after* initializing MetricPoint
Expand Down Expand Up @@ -807,7 +807,7 @@ private bool TryGetAvailableMetricPointRare(
lookupData = new LookupData(index, Tags.EmptyTags, givenTags);

ref var metricPoint = ref this.metricPoints[index];
metricPoint = new MetricPoint(this, this.aggType, givenTags.KeyValuePairs, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale, lookupData);
metricPoint = new MetricPoint(this, this.aggType, givenTags.KeyValuePairs, this.histogramExplicitBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale, lookupData);
newMetricPointCreated = true;

// Add to dictionary *after* initializing MetricPoint
Expand Down
128 changes: 128 additions & 0 deletions src/OpenTelemetry/Metrics/HistogramExplicitBounds.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace OpenTelemetry.Metrics;

internal sealed class HistogramExplicitBounds
{
internal const int DefaultBoundaryCountForBinarySearch = 50;

private readonly BucketLookupNode? bucketLookupTreeRoot;
private readonly Func<double, int> findHistogramBucketIndex;

public HistogramExplicitBounds(double[] bounds)
{
this.Bounds = CleanUpInfinitiesFromExplicitBounds(bounds);
this.findHistogramBucketIndex = this.FindBucketIndexLinear;

if (this.Bounds.Length >= DefaultBoundaryCountForBinarySearch)
{
this.bucketLookupTreeRoot = ConstructBalancedBST(this.Bounds, 0, this.Bounds.Length);
this.findHistogramBucketIndex = this.FindBucketIndexBinary;

static BucketLookupNode? ConstructBalancedBST(double[] values, int min, int max)
{
if (min == max)
{
return null;
}

int median = min + ((max - min) / 2);
return new BucketLookupNode
{
Index = median,
UpperBoundInclusive = values[median],
LowerBoundExclusive = median > 0 ? values[median - 1] : double.NegativeInfinity,
Left = ConstructBalancedBST(values, min, median),
Right = ConstructBalancedBST(values, median + 1, max),
};
}
}
}

public double[] Bounds { get; private set; }

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int FindBucketIndex(double value)
{
return this.findHistogramBucketIndex(value);
}

private static double[] CleanUpInfinitiesFromExplicitBounds(double[] bounds)
{
for (var i = 0; i < bounds.Length; i++)
{
if (double.IsNegativeInfinity(bounds[i]) || double.IsPositiveInfinity(bounds[i]))
{
return bounds
.Where(b => !double.IsNegativeInfinity(b) && !double.IsPositiveInfinity(b))
.ToArray();
}
}

return bounds;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int FindBucketIndexBinary(double value)
{
BucketLookupNode? current = this.bucketLookupTreeRoot;

Debug.Assert(current != null, "Bucket root was null.");

do
{
if (value <= current!.LowerBoundExclusive)
{
current = current.Left;
}
else if (value > current.UpperBoundInclusive)
{
current = current.Right;
}
else
{
return current.Index;
}
}
while (current != null);

Debug.Assert(this.Bounds != null, "ExplicitBounds was null.");

return this.Bounds!.Length;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int FindBucketIndexLinear(double value)
{
Debug.Assert(this.Bounds != null, "ExplicitBounds was null.");

int i;
for (i = 0; i < this.Bounds!.Length; i++)
{
// Upper bound is inclusive
if (value <= this.Bounds[i])
{
break;
}
}

return i;
}

private sealed class BucketLookupNode
{
public double UpperBoundInclusive { get; set; }

public double LowerBoundExclusive { get; set; }

public int Index { get; set; }

public BucketLookupNode? Left { get; set; }

public BucketLookupNode? Right { get; set; }
}
}
107 changes: 8 additions & 99 deletions src/OpenTelemetry/Metrics/MetricPoint/HistogramBuckets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ namespace OpenTelemetry.Metrics;
// Note: Does not implement IEnumerable<> to prevent accidental boxing.
public class HistogramBuckets
{
internal const int DefaultBoundaryCountForBinarySearch = 50;

internal readonly double[]? ExplicitBounds;

internal readonly HistogramBucketValues[] BucketCounts;
Expand All @@ -27,40 +25,13 @@ public class HistogramBuckets
internal double RunningMax = double.NegativeInfinity;
internal double SnapshotMax;

private readonly BucketLookupNode? bucketLookupTreeRoot;

private readonly Func<double, int> findHistogramBucketIndex;
private readonly HistogramExplicitBounds? histogramExplicitBounds;

internal HistogramBuckets(double[]? explicitBounds)
internal HistogramBuckets(HistogramExplicitBounds? histogramExplicitBounds)
{
explicitBounds = CleanUpInfinitiesFromExplicitBounds(explicitBounds);
this.ExplicitBounds = explicitBounds;
this.findHistogramBucketIndex = this.FindBucketIndexLinear;
if (explicitBounds != null && explicitBounds.Length >= DefaultBoundaryCountForBinarySearch)
{
this.bucketLookupTreeRoot = ConstructBalancedBST(explicitBounds, 0, explicitBounds.Length)!;
this.findHistogramBucketIndex = this.FindBucketIndexBinary;

static BucketLookupNode? ConstructBalancedBST(double[] values, int min, int max)
{
if (min == max)
{
return null;
}

int median = min + ((max - min) / 2);
return new BucketLookupNode
{
Index = median,
UpperBoundInclusive = values[median],
LowerBoundExclusive = median > 0 ? values[median - 1] : double.NegativeInfinity,
Left = ConstructBalancedBST(values, min, median),
Right = ConstructBalancedBST(values, median + 1, max),
};
}
}

this.BucketCounts = explicitBounds != null ? new HistogramBucketValues[explicitBounds.Length + 1] : Array.Empty<HistogramBucketValues>();
this.histogramExplicitBounds = histogramExplicitBounds;
this.ExplicitBounds = histogramExplicitBounds?.Bounds;
this.BucketCounts = this.ExplicitBounds != null ? new HistogramBucketValues[this.ExplicitBounds.Length + 1] : [];
}

/// <summary>
Expand All @@ -71,7 +42,7 @@ internal HistogramBuckets(double[]? explicitBounds)

internal HistogramBuckets Copy()
{
HistogramBuckets copy = new HistogramBuckets(this.ExplicitBounds);
HistogramBuckets copy = new HistogramBuckets(this.histogramExplicitBounds);

Array.Copy(this.BucketCounts, copy.BucketCounts, this.BucketCounts.Length);
copy.SnapshotSum = this.SnapshotSum;
Expand All @@ -84,54 +55,8 @@ internal HistogramBuckets Copy()
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal int FindBucketIndex(double value)
{
return this.findHistogramBucketIndex(value);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal int FindBucketIndexBinary(double value)
{
BucketLookupNode? current = this.bucketLookupTreeRoot;

Debug.Assert(current != null, "Bucket root was null.");

do
{
if (value <= current!.LowerBoundExclusive)
{
current = current.Left;
}
else if (value > current.UpperBoundInclusive)
{
current = current.Right;
}
else
{
return current.Index;
}
}
while (current != null);

Debug.Assert(this.ExplicitBounds != null, "ExplicitBounds was null.");

return this.ExplicitBounds!.Length;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal int FindBucketIndexLinear(double value)
{
Debug.Assert(this.ExplicitBounds != null, "ExplicitBounds was null.");

int i;
for (i = 0; i < this.ExplicitBounds!.Length; i++)
{
// Upper bound is inclusive
if (value <= this.ExplicitBounds[i])
{
break;
}
}

return i;
Debug.Assert(this.histogramExplicitBounds != null, "histogramExplicitBounds was null.");
return this.histogramExplicitBounds!.FindBucketIndex(value);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down Expand Up @@ -159,9 +84,6 @@ internal void Snapshot(bool outputDelta)
}
}

private static double[]? CleanUpInfinitiesFromExplicitBounds(double[]? explicitBounds) => explicitBounds
?.Where(b => !double.IsNegativeInfinity(b) && !double.IsPositiveInfinity(b)).ToArray();

/// <summary>
/// Enumerates the elements of a <see cref="HistogramBuckets"/>.
/// </summary>
Expand Down Expand Up @@ -217,17 +139,4 @@ internal struct HistogramBucketValues
public long RunningValue;
public long SnapshotValue;
}

private sealed class BucketLookupNode
{
public double UpperBoundInclusive { get; set; }

public double LowerBoundExclusive { get; set; }

public int Index { get; set; }

public BucketLookupNode? Left { get; set; }

public BucketLookupNode? Right { get; set; }
}
}
Loading
Loading