diff --git a/src/OpenTelemetry/Internal/MathHelper.cs b/src/OpenTelemetry/Internal/MathHelper.cs
index c8f786bb175..043fcac6654 100644
--- a/src/OpenTelemetry/Internal/MathHelper.cs
+++ b/src/OpenTelemetry/Internal/MathHelper.cs
@@ -15,6 +15,7 @@
//
using System;
+using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace OpenTelemetry.Internal;
@@ -96,6 +97,36 @@ public static int LeadingZero64(long value)
}
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int PositiveModulo32(int value, int divisor)
+ {
+ Debug.Assert(divisor > 0, $"{nameof(divisor)} must be a positive integer.");
+
+ value %= divisor;
+
+ if (value < 0)
+ {
+ value += divisor;
+ }
+
+ return value;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static long PositiveModulo64(long value, long divisor)
+ {
+ Debug.Assert(divisor > 0, $"{nameof(divisor)} must be a positive integer.");
+
+ value %= divisor;
+
+ if (value < 0)
+ {
+ value += divisor;
+ }
+
+ return value;
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsFinite(double value)
{
diff --git a/src/OpenTelemetry/Metrics/CircularBufferBuckets.cs b/src/OpenTelemetry/Metrics/CircularBufferBuckets.cs
index 5c636d8a78f..06d318a25a7 100644
--- a/src/OpenTelemetry/Metrics/CircularBufferBuckets.cs
+++ b/src/OpenTelemetry/Metrics/CircularBufferBuckets.cs
@@ -14,6 +14,7 @@
// limitations under the License.
//
+using System.Diagnostics;
using System.Runtime.CompilerServices;
using OpenTelemetry.Internal;
@@ -40,6 +41,11 @@ public CircularBufferBuckets(int capacity)
///
public int Capacity { get; }
+ ///
+ /// Gets the offset of the start index for the .
+ ///
+ public int Offset => this.begin;
+
///
/// Gets the size of the .
///
@@ -60,20 +66,20 @@ public long this[int index]
}
///
- /// Attempts to increment the value of Bucket[index].
+ /// Attempts to increment the value of Bucket[index] by value.
///
/// The index of the bucket.
+ /// The increment.
///
/// Returns 0 if the increment attempt succeeded;
- /// Returns a positive integer Math.Ceiling(log_2(X)) if the
- /// underlying buffer is running out of capacity, and the buffer has to
- /// increase to X * Capacity at minimum.
+ /// Returns a positive integer indicating the minimum scale reduction level
+ /// if the increment attempt failed.
///
///
/// The "index" value can be positive, zero or negative.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public int TryIncrement(int index)
+ public int TryIncrement(int index, long value = 1)
{
var capacity = this.Capacity;
@@ -107,7 +113,7 @@ public int TryIncrement(int index)
this.begin = index;
}
- this.trait[this.ModuloIndex(index)] += 1;
+ this.trait[this.ModuloIndex(index)] += value;
return 0;
@@ -130,16 +136,141 @@ static int CalculateScaleReduction(int size, int capacity)
}
}
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private int ModuloIndex(int value)
+ public void ScaleDown(int level = 1)
{
- value %= this.Capacity;
+ Debug.Assert(level > 0, "The scale down level must be a positive integer.");
+
+ if (this.trait == null)
+ {
+ return;
+ }
+
+ // 0 <= offset < capacity <= 2147483647
+ uint capacity = (uint)this.Capacity;
+ var offset = (uint)this.ModuloIndex(this.begin);
+
+ var currentBegin = this.begin;
+ var currentEnd = this.end;
+
+ for (int i = 0; i < level; i++)
+ {
+ var newBegin = currentBegin >> 1;
+ var newEnd = currentEnd >> 1;
+
+ if (currentBegin != currentEnd)
+ {
+ if (currentBegin % 2 == 0)
+ {
+ ScaleDownInternal(this.trait, offset, currentBegin, currentEnd, capacity);
+ }
+ else
+ {
+ currentBegin++;
+
+ if (currentBegin != currentEnd)
+ {
+ ScaleDownInternal(this.trait, offset + 1, currentBegin, currentEnd, capacity);
+ }
+ }
+ }
+
+ currentBegin = newBegin;
+ currentEnd = newEnd;
+ }
+
+ this.begin = currentBegin;
+ this.end = currentEnd;
+
+ if (capacity > 1)
+ {
+ AdjustPosition(this.trait, offset, (uint)this.ModuloIndex(currentBegin), (uint)(currentEnd - currentBegin + 1), capacity);
+ }
+
+ return;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static void ScaleDownInternal(long[] array, uint offset, int begin, int end, uint capacity)
+ {
+ for (var index = begin + 1; index < end; index++)
+ {
+ Consolidate(array, (offset + (uint)(index - begin)) % capacity, (offset + (uint)((index >> 1) - (begin >> 1))) % capacity);
+ }
+
+ Consolidate(array, (offset + (uint)(end - begin)) % capacity, (offset + (uint)((end >> 1) - (begin >> 1))) % capacity);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static void AdjustPosition(long[] array, uint src, uint dst, uint size, uint capacity)
+ {
+ var advancement = (dst + capacity - src) % capacity;
+
+ if (advancement == 0)
+ {
+ return;
+ }
+
+ if (size - 1 == advancement && advancement << 1 == capacity)
+ {
+ Exchange(array, src++, dst++);
+ size -= 2;
+ }
+ else if (advancement < size)
+ {
+ src = src + size - 1;
+ dst = dst + size - 1;
+
+ while (size-- != 0)
+ {
+ Move(array, src-- % capacity, dst-- % capacity);
+ }
+
+ return;
+ }
+
+ while (size-- != 0)
+ {
+ Move(array, src++ % capacity, dst++ % capacity);
+ }
+ }
- if (value < 0)
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static void Consolidate(long[] array, uint src, uint dst)
+ {
+ array[dst] += array[src];
+ array[src] = 0;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static void Exchange(long[] array, uint src, uint dst)
+ {
+ var value = array[dst];
+ array[dst] = array[src];
+ array[src] = value;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static void Move(long[] array, uint src, uint dst)
{
- value += this.Capacity;
+ array[dst] = array[src];
+ array[src] = 0;
}
+ }
+
+ public override string ToString()
+ {
+ return nameof(CircularBufferBuckets)
+ + "{"
+ + nameof(this.Capacity) + "=" + this.Capacity + ", "
+ + nameof(this.Size) + "=" + this.Size + ", "
+ + nameof(this.begin) + "=" + this.begin + ", "
+ + nameof(this.end) + "=" + this.end + ", "
+ + (this.trait == null ? "null" : "{" + string.Join(", ", this.trait) + "}")
+ + "}";
+ }
- return value;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private int ModuloIndex(int value)
+ {
+ return MathHelper.PositiveModulo32(value, this.Capacity);
}
}
diff --git a/src/OpenTelemetry/Metrics/ExponentialBucketHistogram.cs b/src/OpenTelemetry/Metrics/ExponentialBucketHistogram.cs
index 7bcba194e62..0c2f2843244 100644
--- a/src/OpenTelemetry/Metrics/ExponentialBucketHistogram.cs
+++ b/src/OpenTelemetry/Metrics/ExponentialBucketHistogram.cs
@@ -124,11 +124,29 @@ public void Record(double value)
if (c > 0)
{
- this.PositiveBuckets.TryIncrement(this.MapToIndex(value));
+ var index = this.MapToIndex(value);
+ var n = this.PositiveBuckets.TryIncrement(index);
+
+ if (n != 0)
+ {
+ this.PositiveBuckets.ScaleDown(n);
+ this.NegativeBuckets.ScaleDown(n);
+ n = this.PositiveBuckets.TryIncrement(index);
+ Debug.Assert(n == 0, "Increment should always succeed after scale down.");
+ }
}
else if (c < 0)
{
- this.NegativeBuckets.TryIncrement(this.MapToIndex(-value));
+ var index = this.MapToIndex(-value);
+ var n = this.NegativeBuckets.TryIncrement(index);
+
+ if (n != 0)
+ {
+ this.PositiveBuckets.ScaleDown(n);
+ this.NegativeBuckets.ScaleDown(n);
+ n = this.NegativeBuckets.TryIncrement(index);
+ Debug.Assert(n == 0, "Increment should always succeed after scale down.");
+ }
}
else
{
diff --git a/test/OpenTelemetry.Tests/Metrics/CircularBufferBucketsTest.cs b/test/OpenTelemetry.Tests/Metrics/CircularBufferBucketsTest.cs
index 419cfc9400f..68122562250 100644
--- a/test/OpenTelemetry.Tests/Metrics/CircularBufferBucketsTest.cs
+++ b/test/OpenTelemetry.Tests/Metrics/CircularBufferBucketsTest.cs
@@ -22,7 +22,7 @@ namespace OpenTelemetry.Metrics.Tests;
public class CircularBufferBucketsTest
{
[Fact]
- public void Constructor()
+ public void ConstructorThrowsOnInvalidCapacity()
{
Assert.Throws(() => new CircularBufferBuckets(0));
Assert.Throws(() => new CircularBufferBuckets(-1));
@@ -73,6 +73,9 @@ public void PositiveInsertions()
Assert.Equal(0, buckets.TryIncrement(100));
Assert.Equal(0, buckets.TryIncrement(104));
+ Assert.Equal(100, buckets.Offset);
+ Assert.Equal(5, buckets.Size);
+
Assert.Equal(1, buckets.TryIncrement(99));
Assert.Equal(1, buckets.TryIncrement(105));
}
@@ -88,6 +91,9 @@ public void NegativeInsertions()
Assert.Equal(0, buckets.TryIncrement(1));
Assert.Equal(0, buckets.TryIncrement(-1));
+ Assert.Equal(-2, buckets.Offset);
+ Assert.Equal(5, buckets.Size);
+
Assert.Equal(1, buckets.TryIncrement(3));
Assert.Equal(1, buckets.TryIncrement(-3));
}
@@ -98,6 +104,10 @@ public void IntegerOverflow()
var buckets = new CircularBufferBuckets(1);
Assert.Equal(0, buckets.TryIncrement(int.MaxValue));
+
+ Assert.Equal(int.MaxValue, buckets.Offset);
+ Assert.Equal(1, buckets.Size);
+
Assert.Equal(31, buckets.TryIncrement(1));
Assert.Equal(31, buckets.TryIncrement(0));
Assert.Equal(32, buckets.TryIncrement(-1));
@@ -126,10 +136,280 @@ public void IndexOperations()
buckets.TryIncrement(-1);
buckets.TryIncrement(-1);
+ Assert.Equal(-2, buckets.Offset);
+
Assert.Equal(1, buckets[-2]);
Assert.Equal(2, buckets[-1]);
Assert.Equal(3, buckets[0]);
Assert.Equal(4, buckets[1]);
Assert.Equal(5, buckets[2]);
}
+
+ [Fact]
+ public void ScaleDownCapacity1()
+ {
+ var buckets = new CircularBufferBuckets(1);
+
+ buckets.ScaleDown(1);
+ buckets.ScaleDown(2);
+ buckets.ScaleDown(3);
+ buckets.ScaleDown(4);
+
+ buckets.TryIncrement(0);
+
+ Assert.Equal(0, buckets.Offset);
+ Assert.Equal(1, buckets.Size);
+ Assert.Equal(1, buckets[0]);
+ }
+
+ [Fact]
+ public void ScaleDownIntMaxValue()
+ {
+ var buckets = new CircularBufferBuckets(1);
+
+ buckets.TryIncrement(int.MaxValue);
+
+ Assert.Equal(int.MaxValue, buckets.Offset);
+
+ buckets.ScaleDown(1);
+
+ Assert.Equal(0x3FFFFFFF, buckets.Offset);
+ Assert.Equal(1, buckets[0x3FFFFFFF]);
+ }
+
+ [Fact]
+ public void ScaleDownIntMinValue()
+ {
+ var buckets = new CircularBufferBuckets(1);
+
+ buckets.TryIncrement(int.MinValue);
+
+ Assert.Equal(int.MinValue, buckets.Offset);
+
+ buckets.ScaleDown(1);
+
+ Assert.Equal(-0x40000000, buckets.Offset);
+ Assert.Equal(1, buckets[-0x40000000]);
+ }
+
+ [Fact]
+ public void ScaleDownCapacity2()
+ {
+ var buckets = new CircularBufferBuckets(2);
+
+ buckets.TryIncrement(int.MinValue, 2);
+ buckets.TryIncrement(int.MinValue + 1);
+ buckets.ScaleDown(1);
+
+ Assert.Equal(1, buckets.Size);
+ Assert.Equal(3, buckets[buckets.Offset]);
+
+ buckets = new CircularBufferBuckets(2);
+
+ buckets.TryIncrement(int.MaxValue - 1, 2);
+ buckets.TryIncrement(int.MaxValue);
+ buckets.ScaleDown(1);
+
+ Assert.Equal(1, buckets.Size);
+ Assert.Equal(3, buckets[buckets.Offset]);
+ Assert.Equal(0, buckets[buckets.Offset + 1]);
+
+ buckets = new CircularBufferBuckets(2);
+
+ buckets.TryIncrement(int.MaxValue - 2, 2);
+ buckets.TryIncrement(int.MaxValue - 1);
+ buckets.ScaleDown(1);
+
+ Assert.Equal(2, buckets.Size);
+ Assert.Equal(2, buckets[buckets.Offset]);
+ Assert.Equal(1, buckets[buckets.Offset + 1]);
+ }
+
+ [Fact]
+ public void ScaleDownCapacity3()
+ {
+ var buckets = new CircularBufferBuckets(3);
+
+ buckets.TryIncrement(0, 2);
+ buckets.TryIncrement(1, 4);
+ buckets.TryIncrement(2, 8);
+ buckets.ScaleDown(1);
+
+ Assert.Equal(0, buckets.Offset);
+ Assert.Equal(2, buckets.Size);
+ Assert.Equal(6, buckets[buckets.Offset]);
+ Assert.Equal(8, buckets[buckets.Offset + 1]);
+
+ buckets = new CircularBufferBuckets(3);
+
+ buckets.TryIncrement(1, 2);
+ buckets.TryIncrement(2, 4);
+ buckets.TryIncrement(3, 8);
+ buckets.ScaleDown(1);
+
+ Assert.Equal(0, buckets.Offset);
+ Assert.Equal(2, buckets.Size);
+ Assert.Equal(2, buckets[buckets.Offset]);
+ Assert.Equal(12, buckets[buckets.Offset + 1]);
+
+ buckets = new CircularBufferBuckets(3);
+
+ buckets.TryIncrement(2, 2);
+ buckets.TryIncrement(3, 4);
+ buckets.TryIncrement(4, 8);
+ buckets.ScaleDown(1);
+
+ Assert.Equal(1, buckets.Offset);
+ Assert.Equal(2, buckets.Size);
+ Assert.Equal(6, buckets[buckets.Offset]);
+ Assert.Equal(8, buckets[buckets.Offset + 1]);
+
+ buckets = new CircularBufferBuckets(3);
+
+ buckets.TryIncrement(3, 2);
+ buckets.TryIncrement(4, 4);
+ buckets.TryIncrement(5, 8);
+ buckets.ScaleDown(1);
+
+ Assert.Equal(1, buckets.Offset);
+ Assert.Equal(2, buckets.Size);
+ Assert.Equal(2, buckets[buckets.Offset]);
+ Assert.Equal(12, buckets[buckets.Offset + 1]);
+
+ buckets = new CircularBufferBuckets(3);
+
+ buckets.TryIncrement(4, 2);
+ buckets.TryIncrement(5, 4);
+ buckets.TryIncrement(6, 8);
+ buckets.ScaleDown(1);
+
+ Assert.Equal(2, buckets.Offset);
+ Assert.Equal(2, buckets.Size);
+ Assert.Equal(6, buckets[buckets.Offset]);
+ Assert.Equal(8, buckets[buckets.Offset + 1]);
+
+ buckets = new CircularBufferBuckets(3);
+
+ buckets.TryIncrement(5, 2);
+ buckets.TryIncrement(6, 4);
+ buckets.TryIncrement(7, 8);
+ buckets.ScaleDown(1);
+
+ Assert.Equal(2, buckets.Offset);
+ Assert.Equal(2, buckets.Size);
+ Assert.Equal(2, buckets[buckets.Offset]);
+ Assert.Equal(12, buckets[buckets.Offset + 1]);
+ }
+
+ [Fact]
+ public void ScaleDownCapacity4()
+ {
+ var buckets = new CircularBufferBuckets(4);
+
+ buckets.TryIncrement(0, 2);
+ buckets.TryIncrement(1, 4);
+ buckets.TryIncrement(2, 8);
+ buckets.TryIncrement(2, 16);
+ buckets.ScaleDown(1);
+
+ Assert.Equal(0, buckets.Offset);
+ Assert.Equal(2, buckets.Size);
+ Assert.Equal(6, buckets[buckets.Offset]);
+ Assert.Equal(24, buckets[buckets.Offset + 1]);
+
+ buckets = new CircularBufferBuckets(4);
+
+ buckets.TryIncrement(1, 2);
+ buckets.TryIncrement(2, 4);
+ buckets.TryIncrement(3, 8);
+ buckets.TryIncrement(4, 16);
+ buckets.ScaleDown(1);
+
+ Assert.Equal(0, buckets.Offset);
+ Assert.Equal(3, buckets.Size);
+ Assert.Equal(2, buckets[buckets.Offset]);
+ Assert.Equal(12, buckets[buckets.Offset + 1]);
+ Assert.Equal(16, buckets[buckets.Offset + 2]);
+
+ buckets = new CircularBufferBuckets(4);
+
+ buckets.TryIncrement(2, 2);
+ buckets.TryIncrement(3, 4);
+ buckets.TryIncrement(4, 8);
+ buckets.TryIncrement(5, 16);
+ buckets.ScaleDown(1);
+
+ Assert.Equal(1, buckets.Offset);
+ Assert.Equal(2, buckets.Size);
+ Assert.Equal(6, buckets[buckets.Offset]);
+ Assert.Equal(24, buckets[buckets.Offset + 1]);
+
+ buckets = new CircularBufferBuckets(4);
+
+ buckets.TryIncrement(3, 2);
+ buckets.TryIncrement(4, 4);
+ buckets.TryIncrement(5, 8);
+ buckets.TryIncrement(6, 16);
+ buckets.ScaleDown(1);
+
+ Assert.Equal(1, buckets.Offset);
+ Assert.Equal(3, buckets.Size);
+ Assert.Equal(2, buckets[buckets.Offset]);
+ Assert.Equal(12, buckets[buckets.Offset + 1]);
+ Assert.Equal(16, buckets[buckets.Offset + 2]);
+
+ buckets = new CircularBufferBuckets(4);
+
+ buckets.TryIncrement(4, 2);
+ buckets.TryIncrement(5, 4);
+ buckets.TryIncrement(6, 8);
+ buckets.TryIncrement(7, 16);
+ buckets.ScaleDown(1);
+
+ Assert.Equal(2, buckets.Offset);
+ Assert.Equal(2, buckets.Size);
+ Assert.Equal(6, buckets[buckets.Offset]);
+ Assert.Equal(24, buckets[buckets.Offset + 1]);
+
+ buckets = new CircularBufferBuckets(4);
+
+ buckets.TryIncrement(5, 2);
+ buckets.TryIncrement(6, 4);
+ buckets.TryIncrement(7, 8);
+ buckets.TryIncrement(8, 16);
+ buckets.ScaleDown(1);
+
+ Assert.Equal(2, buckets.Offset);
+ Assert.Equal(3, buckets.Size);
+ Assert.Equal(2, buckets[buckets.Offset]);
+ Assert.Equal(12, buckets[buckets.Offset + 1]);
+ Assert.Equal(16, buckets[buckets.Offset + 2]);
+
+ buckets = new CircularBufferBuckets(4);
+
+ buckets.TryIncrement(6, 2);
+ buckets.TryIncrement(7, 4);
+ buckets.TryIncrement(8, 8);
+ buckets.TryIncrement(9, 16);
+ buckets.ScaleDown(1);
+
+ Assert.Equal(3, buckets.Offset);
+ Assert.Equal(2, buckets.Size);
+ Assert.Equal(6, buckets[buckets.Offset]);
+ Assert.Equal(24, buckets[buckets.Offset + 1]);
+
+ buckets = new CircularBufferBuckets(4);
+
+ buckets.TryIncrement(7, 2);
+ buckets.TryIncrement(8, 4);
+ buckets.TryIncrement(9, 8);
+ buckets.TryIncrement(10, 16);
+ buckets.ScaleDown(1);
+
+ Assert.Equal(3, buckets.Offset);
+ Assert.Equal(3, buckets.Size);
+ Assert.Equal(2, buckets[buckets.Offset]);
+ Assert.Equal(12, buckets[buckets.Offset + 1]);
+ Assert.Equal(16, buckets[buckets.Offset + 2]);
+ }
}