Exponential Bucket Histogram - part 5 (#3482)

This commit is contained in:
Reiley Yang 2022-07-25 10:16:21 -07:00 committed by GitHub
parent a55e3391cd
commit 6f2b1a0b34
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 477 additions and 17 deletions

View File

@ -15,6 +15,7 @@
// </copyright>
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace OpenTelemetry.Internal;
@ -96,6 +97,36 @@ internal static class MathHelper
}
}
[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)
{

View File

@ -14,6 +14,7 @@
// limitations under the License.
// </copyright>
using System.Diagnostics;
using System.Runtime.CompilerServices;
using OpenTelemetry.Internal;
@ -40,6 +41,11 @@ internal sealed class CircularBufferBuckets
/// </summary>
public int Capacity { get; }
/// <summary>
/// Gets the offset of the start index for the <see cref="CircularBufferBuckets"/>.
/// </summary>
public int Offset => this.begin;
/// <summary>
/// Gets the size of the <see cref="CircularBufferBuckets"/>.
/// </summary>
@ -60,20 +66,20 @@ internal sealed class CircularBufferBuckets
}
/// <summary>
/// Attempts to increment the value of <c>Bucket[index]</c>.
/// Attempts to increment the value of <c>Bucket[index]</c> by <c>value</c>.
/// </summary>
/// <param name="index">The index of the bucket.</param>
/// <param name="value">The increment.</param>
/// <returns>
/// Returns <c>0</c> if the increment attempt succeeded;
/// Returns a positive integer <c>Math.Ceiling(log_2(X))</c> if the
/// underlying buffer is running out of capacity, and the buffer has to
/// increase to <c>X * Capacity</c> at minimum.
/// Returns a positive integer indicating the minimum scale reduction level
/// if the increment attempt failed.
/// </returns>
/// <remarks>
/// The "index" value can be positive, zero or negative.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int TryIncrement(int index)
public int TryIncrement(int index, long value = 1)
{
var capacity = this.Capacity;
@ -107,7 +113,7 @@ internal sealed class CircularBufferBuckets
this.begin = index;
}
this.trait[this.ModuloIndex(index)] += 1;
this.trait[this.ModuloIndex(index)] += value;
return 0;
@ -130,16 +136,141 @@ internal sealed class CircularBufferBuckets
}
}
public void ScaleDown(int level = 1)
{
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);
}
}
[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)
{
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) + "}")
+ "}";
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int ModuloIndex(int value)
{
value %= this.Capacity;
if (value < 0)
{
value += this.Capacity;
}
return value;
return MathHelper.PositiveModulo32(value, this.Capacity);
}
}

View File

@ -124,11 +124,29 @@ internal class ExponentialBucketHistogram
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
{

View File

@ -22,7 +22,7 @@ namespace OpenTelemetry.Metrics.Tests;
public class CircularBufferBucketsTest
{
[Fact]
public void Constructor()
public void ConstructorThrowsOnInvalidCapacity()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new CircularBufferBuckets(0));
Assert.Throws<ArgumentOutOfRangeException>(() => new CircularBufferBuckets(-1));
@ -73,6 +73,9 @@ public class CircularBufferBucketsTest
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 class CircularBufferBucketsTest
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 class CircularBufferBucketsTest
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 class CircularBufferBucketsTest
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]);
}
}