From 6f2b1a0b3412f2860d065c81b058441977927584 Mon Sep 17 00:00:00 2001 From: Reiley Yang Date: Mon, 25 Jul 2022 10:16:21 -0700 Subject: [PATCH] Exponential Bucket Histogram - part 5 (#3482) --- src/OpenTelemetry/Internal/MathHelper.cs | 31 ++ .../Metrics/CircularBufferBuckets.cs | 155 +++++++++- .../Metrics/ExponentialBucketHistogram.cs | 22 +- .../Metrics/CircularBufferBucketsTest.cs | 282 +++++++++++++++++- 4 files changed, 475 insertions(+), 15 deletions(-) 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]); + } }