Skip to content

Commit f630c55

Browse files
iliar-turdushevmartincostelloKielek
authored
[OpenTelemetry] Move BucketLookup tree to AggregatorStore (#6715)
Co-authored-by: Martin Costello <[email protected]> Co-authored-by: Piotr Kiełkowicz <[email protected]>
1 parent a685fee commit f630c55

File tree

6 files changed

+171
-128
lines changed

6 files changed

+171
-128
lines changed

src/OpenTelemetry/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ Notes](../../RELEASENOTES.md).
1919
* Added support for `Meter.TelemetrySchemaUrl` property.
2020
([#6714](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6714))
2121

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

2427
Released 2025-Nov-12

src/OpenTelemetry/Metrics/AggregatorStore.cs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ internal sealed class AggregatorStore
4242
private readonly MetricPoint[] metricPoints;
4343
private readonly int[] currentMetricPointBatch;
4444
private readonly AggregationType aggType;
45-
private readonly double[] histogramBounds;
45+
private readonly HistogramExplicitBounds histogramExplicitBounds;
4646
private readonly int exponentialHistogramMaxSize;
4747
private readonly int exponentialHistogramMaxScale;
4848
private readonly UpdateLongDelegate updateLongCallback;
@@ -74,7 +74,7 @@ internal AggregatorStore(
7474
this.currentMetricPointBatch = new int[this.NumberOfMetricPoints];
7575
this.aggType = aggType;
7676
this.OutputDelta = temporality == AggregationTemporality.Delta;
77-
this.histogramBounds = metricStreamIdentity.HistogramBucketBounds ?? FindDefaultHistogramBounds(in metricStreamIdentity);
77+
this.histogramExplicitBounds = new(metricStreamIdentity.HistogramBucketBounds ?? FindDefaultHistogramBounds(in metricStreamIdentity));
7878
this.exponentialHistogramMaxSize = metricStreamIdentity.ExponentialHistogramMaxSize;
7979
this.exponentialHistogramMaxScale = metricStreamIdentity.ExponentialHistogramMaxScale;
8080
this.StartTimeExclusive = DateTimeOffset.UtcNow;
@@ -143,7 +143,7 @@ internal AggregatorStore(
143143

144144
internal DateTimeOffset EndTimeInclusive { get; private set; }
145145

146-
internal double[] HistogramBounds => this.histogramBounds;
146+
internal double[] HistogramBounds => this.histogramExplicitBounds.Bounds;
147147

148148
internal bool IsExemplarEnabled()
149149
{
@@ -393,11 +393,11 @@ private void InitializeZeroTagPointIfNotInitialized()
393393
if (this.OutputDelta)
394394
{
395395
var lookupData = new LookupData(0, Tags.EmptyTags, Tags.EmptyTags);
396-
this.metricPoints[0] = new MetricPoint(this, this.aggType, null, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale, lookupData);
396+
this.metricPoints[0] = new MetricPoint(this, this.aggType, null, this.histogramExplicitBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale, lookupData);
397397
}
398398
else
399399
{
400-
this.metricPoints[0] = new MetricPoint(this, this.aggType, null, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale);
400+
this.metricPoints[0] = new MetricPoint(this, this.aggType, null, this.histogramExplicitBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale);
401401
}
402402

403403
this.zeroTagMetricPointInitialized = true;
@@ -421,11 +421,11 @@ private void InitializeOverflowTagPointIfNotInitialized()
421421
if (this.OutputDelta)
422422
{
423423
var lookupData = new LookupData(1, tags, tags);
424-
this.metricPoints[1] = new MetricPoint(this, this.aggType, keyValuePairs, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale, lookupData);
424+
this.metricPoints[1] = new MetricPoint(this, this.aggType, keyValuePairs, this.histogramExplicitBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale, lookupData);
425425
}
426426
else
427427
{
428-
this.metricPoints[1] = new MetricPoint(this, this.aggType, keyValuePairs, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale);
428+
this.metricPoints[1] = new MetricPoint(this, this.aggType, keyValuePairs, this.histogramExplicitBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale);
429429
}
430430

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

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

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

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

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

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

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

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

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

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

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

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

813813
// Add to dictionary *after* initializing MetricPoint
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using System.Diagnostics;
5+
using System.Runtime.CompilerServices;
6+
7+
namespace OpenTelemetry.Metrics;
8+
9+
internal sealed class HistogramExplicitBounds
10+
{
11+
internal const int DefaultBoundaryCountForBinarySearch = 50;
12+
13+
private readonly BucketLookupNode? bucketLookupTreeRoot;
14+
private readonly Func<double, int> findHistogramBucketIndex;
15+
16+
public HistogramExplicitBounds(double[] bounds)
17+
{
18+
this.Bounds = CleanUpInfinitiesFromExplicitBounds(bounds);
19+
this.findHistogramBucketIndex = this.FindBucketIndexLinear;
20+
21+
if (this.Bounds.Length >= DefaultBoundaryCountForBinarySearch)
22+
{
23+
this.bucketLookupTreeRoot = ConstructBalancedBST(this.Bounds, 0, this.Bounds.Length);
24+
this.findHistogramBucketIndex = this.FindBucketIndexBinary;
25+
26+
static BucketLookupNode? ConstructBalancedBST(double[] values, int min, int max)
27+
{
28+
if (min == max)
29+
{
30+
return null;
31+
}
32+
33+
int median = min + ((max - min) / 2);
34+
return new BucketLookupNode
35+
{
36+
Index = median,
37+
UpperBoundInclusive = values[median],
38+
LowerBoundExclusive = median > 0 ? values[median - 1] : double.NegativeInfinity,
39+
Left = ConstructBalancedBST(values, min, median),
40+
Right = ConstructBalancedBST(values, median + 1, max),
41+
};
42+
}
43+
}
44+
}
45+
46+
public double[] Bounds { get; }
47+
48+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
49+
public int FindBucketIndex(double value)
50+
{
51+
return this.findHistogramBucketIndex(value);
52+
}
53+
54+
private static double[] CleanUpInfinitiesFromExplicitBounds(double[] bounds)
55+
{
56+
for (var i = 0; i < bounds.Length; i++)
57+
{
58+
if (double.IsNegativeInfinity(bounds[i]) || double.IsPositiveInfinity(bounds[i]))
59+
{
60+
return bounds
61+
.Where(b => !double.IsNegativeInfinity(b) && !double.IsPositiveInfinity(b))
62+
.ToArray();
63+
}
64+
}
65+
66+
return bounds;
67+
}
68+
69+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
70+
private int FindBucketIndexBinary(double value)
71+
{
72+
BucketLookupNode? current = this.bucketLookupTreeRoot;
73+
74+
Debug.Assert(current != null, "Bucket root was null.");
75+
76+
do
77+
{
78+
if (value <= current!.LowerBoundExclusive)
79+
{
80+
current = current.Left;
81+
}
82+
else if (value > current.UpperBoundInclusive)
83+
{
84+
current = current.Right;
85+
}
86+
else
87+
{
88+
return current.Index;
89+
}
90+
}
91+
while (current != null);
92+
93+
Debug.Assert(this.Bounds != null, "ExplicitBounds was null.");
94+
95+
return this.Bounds!.Length;
96+
}
97+
98+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
99+
private int FindBucketIndexLinear(double value)
100+
{
101+
Debug.Assert(this.Bounds != null, "ExplicitBounds was null.");
102+
103+
int i;
104+
for (i = 0; i < this.Bounds!.Length; i++)
105+
{
106+
// Upper bound is inclusive
107+
if (value <= this.Bounds[i])
108+
{
109+
break;
110+
}
111+
}
112+
113+
return i;
114+
}
115+
116+
private sealed class BucketLookupNode
117+
{
118+
public double UpperBoundInclusive { get; set; }
119+
120+
public double LowerBoundExclusive { get; set; }
121+
122+
public int Index { get; set; }
123+
124+
public BucketLookupNode? Left { get; set; }
125+
126+
public BucketLookupNode? Right { get; set; }
127+
}
128+
}

src/OpenTelemetry/Metrics/MetricPoint/HistogramBuckets.cs

Lines changed: 8 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ namespace OpenTelemetry.Metrics;
1212
// Note: Does not implement IEnumerable<> to prevent accidental boxing.
1313
public class HistogramBuckets
1414
{
15-
internal const int DefaultBoundaryCountForBinarySearch = 50;
16-
1715
internal readonly double[]? ExplicitBounds;
1816

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

30-
private readonly BucketLookupNode? bucketLookupTreeRoot;
31-
32-
private readonly Func<double, int> findHistogramBucketIndex;
28+
private readonly HistogramExplicitBounds? histogramExplicitBounds;
3329

34-
internal HistogramBuckets(double[]? explicitBounds)
30+
internal HistogramBuckets(HistogramExplicitBounds? histogramExplicitBounds)
3531
{
36-
explicitBounds = CleanUpInfinitiesFromExplicitBounds(explicitBounds);
37-
this.ExplicitBounds = explicitBounds;
38-
this.findHistogramBucketIndex = this.FindBucketIndexLinear;
39-
if (explicitBounds != null && explicitBounds.Length >= DefaultBoundaryCountForBinarySearch)
40-
{
41-
this.bucketLookupTreeRoot = ConstructBalancedBST(explicitBounds, 0, explicitBounds.Length)!;
42-
this.findHistogramBucketIndex = this.FindBucketIndexBinary;
43-
44-
static BucketLookupNode? ConstructBalancedBST(double[] values, int min, int max)
45-
{
46-
if (min == max)
47-
{
48-
return null;
49-
}
50-
51-
int median = min + ((max - min) / 2);
52-
return new BucketLookupNode
53-
{
54-
Index = median,
55-
UpperBoundInclusive = values[median],
56-
LowerBoundExclusive = median > 0 ? values[median - 1] : double.NegativeInfinity,
57-
Left = ConstructBalancedBST(values, min, median),
58-
Right = ConstructBalancedBST(values, median + 1, max),
59-
};
60-
}
61-
}
62-
63-
this.BucketCounts = explicitBounds != null ? new HistogramBucketValues[explicitBounds.Length + 1] : Array.Empty<HistogramBucketValues>();
32+
this.histogramExplicitBounds = histogramExplicitBounds;
33+
this.ExplicitBounds = histogramExplicitBounds?.Bounds;
34+
this.BucketCounts = this.ExplicitBounds != null ? new HistogramBucketValues[this.ExplicitBounds.Length + 1] : [];
6435
}
6536

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

7243
internal HistogramBuckets Copy()
7344
{
74-
HistogramBuckets copy = new HistogramBuckets(this.ExplicitBounds);
45+
HistogramBuckets copy = new HistogramBuckets(this.histogramExplicitBounds);
7546

7647
Array.Copy(this.BucketCounts, copy.BucketCounts, this.BucketCounts.Length);
7748
copy.SnapshotSum = this.SnapshotSum;
@@ -84,54 +55,8 @@ internal HistogramBuckets Copy()
8455
[MethodImpl(MethodImplOptions.AggressiveInlining)]
8556
internal int FindBucketIndex(double value)
8657
{
87-
return this.findHistogramBucketIndex(value);
88-
}
89-
90-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
91-
internal int FindBucketIndexBinary(double value)
92-
{
93-
BucketLookupNode? current = this.bucketLookupTreeRoot;
94-
95-
Debug.Assert(current != null, "Bucket root was null.");
96-
97-
do
98-
{
99-
if (value <= current!.LowerBoundExclusive)
100-
{
101-
current = current.Left;
102-
}
103-
else if (value > current.UpperBoundInclusive)
104-
{
105-
current = current.Right;
106-
}
107-
else
108-
{
109-
return current.Index;
110-
}
111-
}
112-
while (current != null);
113-
114-
Debug.Assert(this.ExplicitBounds != null, "ExplicitBounds was null.");
115-
116-
return this.ExplicitBounds!.Length;
117-
}
118-
119-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
120-
internal int FindBucketIndexLinear(double value)
121-
{
122-
Debug.Assert(this.ExplicitBounds != null, "ExplicitBounds was null.");
123-
124-
int i;
125-
for (i = 0; i < this.ExplicitBounds!.Length; i++)
126-
{
127-
// Upper bound is inclusive
128-
if (value <= this.ExplicitBounds[i])
129-
{
130-
break;
131-
}
132-
}
133-
134-
return i;
58+
Debug.Assert(this.histogramExplicitBounds != null, "histogramExplicitBounds was null.");
59+
return this.histogramExplicitBounds!.FindBucketIndex(value);
13560
}
13661

13762
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -159,9 +84,6 @@ internal void Snapshot(bool outputDelta)
15984
}
16085
}
16186

162-
private static double[]? CleanUpInfinitiesFromExplicitBounds(double[]? explicitBounds) => explicitBounds
163-
?.Where(b => !double.IsNegativeInfinity(b) && !double.IsPositiveInfinity(b)).ToArray();
164-
16587
/// <summary>
16688
/// Enumerates the elements of a <see cref="HistogramBuckets"/>.
16789
/// </summary>
@@ -217,17 +139,4 @@ internal struct HistogramBucketValues
217139
public long RunningValue;
218140
public long SnapshotValue;
219141
}
220-
221-
private sealed class BucketLookupNode
222-
{
223-
public double UpperBoundInclusive { get; set; }
224-
225-
public double LowerBoundExclusive { get; set; }
226-
227-
public int Index { get; set; }
228-
229-
public BucketLookupNode? Left { get; set; }
230-
231-
public BucketLookupNode? Right { get; set; }
232-
}
233142
}

0 commit comments

Comments
 (0)