diff --git a/src/OpenTelemetry/Metrics/CircularBufferBuckets.cs b/src/OpenTelemetry/Metrics/CircularBufferBuckets.cs
new file mode 100644
index 00000000000..15584c5e57d
--- /dev/null
+++ b/src/OpenTelemetry/Metrics/CircularBufferBuckets.cs
@@ -0,0 +1,119 @@
+//
+// Copyright The OpenTelemetry Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System.Runtime.CompilerServices;
+
+using OpenTelemetry.Internal;
+
+namespace OpenTelemetry.Metrics;
+
+///
+/// A histogram buckets implementation based on circular buffer.
+///
+internal class CircularBufferBuckets
+{
+ private long[] trait;
+ private int begin = 0;
+ private int end = -1;
+
+ public CircularBufferBuckets(int capacity)
+ {
+ Guard.ThrowIfOutOfRange(capacity, min: 1);
+
+ this.Capacity = capacity;
+ }
+
+ ///
+ /// Gets the capacity of the .
+ ///
+ public int Capacity { get; }
+
+ ///
+ /// Gets the size of the .
+ ///
+ public int Size => this.end - this.begin + 1;
+
+ ///
+ /// Returns the value of Bucket[index].
+ ///
+ /// The index of the bucket.
+ ///
+ /// The "index" value can be positive, zero or negative.
+ /// This method does not validate if "index" falls into [begin, end],
+ /// the caller is responsible for the validation.
+ ///
+ public long this[int index]
+ {
+ get => this.trait[this.ModuloIndex(index)];
+ }
+
+ ///
+ /// Attempts to increment the value of Bucket[index].
+ ///
+ /// The index of the bucket.
+ ///
+ /// Returns true if the increment attempt succeeded;
+ /// false if the underlying buffer is running out of capacity.
+ ///
+ ///
+ /// The "index" value can be positive, zero or negative.
+ ///
+ public bool TryIncrement(int index)
+ {
+ if (this.trait == null)
+ {
+ this.trait = new long[this.Capacity];
+
+ this.begin = index;
+ this.end = index;
+ }
+ else if (index > this.end)
+ {
+ if (index - this.begin >= this.Capacity)
+ {
+ return false;
+ }
+
+ this.end = index;
+ }
+ else if (index < this.begin)
+ {
+ if (this.end - index >= this.Capacity)
+ {
+ return false;
+ }
+
+ this.begin = index;
+ }
+
+ this.trait[this.ModuloIndex(index)] += 1;
+
+ return true;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private int ModuloIndex(int value)
+ {
+ value %= this.Capacity;
+
+ if (value < 0)
+ {
+ value += this.Capacity;
+ }
+
+ return value;
+ }
+}
diff --git a/src/OpenTelemetry/Metrics/ExponentialBucketHistogram.cs b/src/OpenTelemetry/Metrics/ExponentialBucketHistogram.cs
index ca70f59f441..a8c4fb5d22e 100644
--- a/src/OpenTelemetry/Metrics/ExponentialBucketHistogram.cs
+++ b/src/OpenTelemetry/Metrics/ExponentialBucketHistogram.cs
@@ -18,114 +18,123 @@
using System;
using System.Diagnostics;
-
using OpenTelemetry.Internal;
-namespace OpenTelemetry.Metrics
+namespace OpenTelemetry.Metrics;
+
+///
+/// Represents an exponential bucket histogram with base = 2 ^ (2 ^ (-scale)).
+/// An exponential bucket histogram has infinite number of buckets, which are
+/// identified by Bucket[index] = ( base ^ index, base ^ (index + 1) ],
+/// where index is an integer.
+///
+internal class ExponentialBucketHistogram
{
- ///
- /// Represents an exponential bucket histogram with base = 2 ^ (2 ^ (-scale)).
- /// An exponential bucket histogram has infinite number of buckets, which are
- /// identified by Bucket[i] = ( base ^ i, base ^ (i + 1) ], where i
- /// is an integer.
- ///
- internal class ExponentialBucketHistogram
+ private static readonly double Log2E = Math.Log2(Math.E); // 1 / Math.Log(2)
+
+ private int scale;
+ private double scalingFactor; // 2 ^ scale / log(2)
+
+ public ExponentialBucketHistogram(int scale, int maxBuckets = 160)
{
- private static readonly double Log2E = Math.Log2(Math.E); // 1 / Math.Log(2)
+ Guard.ThrowIfOutOfRange(scale, min: -20, max: 20); // TODO: calculate the actual range
- private int scale;
- private double scalingFactor; // 2 ^ scale / log(2)
+ this.Scale = scale;
+ }
- public ExponentialBucketHistogram(int scale, int maxBuckets = 160)
- {
- Guard.ThrowIfOutOfRange(scale, min: -20, max: 20); // TODO: calculate the actual range
+ internal int Scale
+ {
+ get => this.scale;
- this.Scale = scale;
+ private set
+ {
+ this.scale = value;
+ this.scalingFactor = Math.ScaleB(Log2E, value);
}
+ }
- internal int Scale
- {
- get
- {
- return this.scale;
- }
+ internal long ZeroCount { get; private set; }
- private set
- {
- this.scale = value;
- this.scalingFactor = Math.ScaleB(Log2E, value);
- }
- }
+ ///
+ public override string ToString()
+ {
+ return nameof(ExponentialBucketHistogram)
+ + "{"
+ + nameof(this.Scale) + "=" + this.Scale
+ + "}";
+ }
- internal long ZeroCount { get; private set; }
+ ///
+ /// Maps a finite positive IEEE 754 double-precision floating-point
+ /// number to Bucket[index] = ( base ^ index, base ^ (index + 1) ],
+ /// where index is an integer.
+ ///
+ ///
+ /// The value to be bucketized. Must be a finite positive number.
+ ///
+ ///
+ /// Returns the index of the bucket.
+ ///
+ public int MapToIndex(double value)
+ {
+ Debug.Assert(double.IsFinite(value), "IEEE-754 +Inf, -Inf and NaN should be filtered out before calling this method.");
+ Debug.Assert(value != 0, "IEEE-754 zero values should be handled by ZeroCount.");
+ Debug.Assert(!double.IsNegative(value), "IEEE-754 negative values should be normalized before calling this method.");
- ///
- public override string ToString()
+ if (this.Scale > 0)
{
- return nameof(ExponentialBucketHistogram)
- + "{"
- + nameof(this.Scale) + "=" + this.Scale
- + "}";
+ // TODO: due to precision issue, the values that are close to the bucket
+ // boundaries should be closely examined to avoid off-by-one.
+ return (int)Math.Ceiling(Math.Log(value) * this.scalingFactor) - 1;
}
-
- public int MapToIndex(double value)
+ else
{
- Debug.Assert(value != 0, "IEEE-754 zero values should be handled by ZeroCount.");
+ var bits = BitConverter.DoubleToInt64Bits(value);
+ var exp = (int)((bits & IEEE754Double.EXPONENT_MASK) >> IEEE754Double.FRACTION_BITS);
+ var fraction = bits & IEEE754Double.FRACTION_MASK;
- // TODO: handle +Inf, -Inf, NaN
-
- value = Math.Abs(value);
-
- if (this.Scale > 0)
- {
- // TODO: due to precision issue, the values that are close to the bucket
- // boundaries should be closely examined to avoid off-by-one.
- return (int)Math.Ceiling(Math.Log(value) * this.scalingFactor) - 1;
- }
- else
+ if (exp == 0)
{
- var bits = BitConverter.DoubleToInt64Bits(value);
- var exp = (int)((bits & IEEE754Double.EXPONENT_MASK) >> IEEE754Double.FRACTION_BITS);
- var fraction = bits & IEEE754Double.FRACTION_MASK;
+ // TODO: benchmark and see if this should be changed to a lookup table.
+ fraction--;
- if (exp == 0)
+ for (int i = IEEE754Double.FRACTION_BITS - 1; i >= 0; i--)
{
- // TODO: benchmark and see if this should be changed to a lookup table.
- fraction--;
-
- for (int i = IEEE754Double.FRACTION_BITS - 1; i >= 0; i--)
+ if ((fraction >> i) != 0)
{
- if ((fraction >> i) != 0)
- {
- break;
- }
-
- exp--;
+ break;
}
- }
- else if (fraction == 0)
- {
+
exp--;
}
-
- return (exp - IEEE754Double.EXPONENT_BIAS) >> -this.Scale;
}
+ else if (fraction == 0)
+ {
+ exp--;
+ }
+
+ return (exp - IEEE754Double.EXPONENT_BIAS) >> -this.Scale;
}
+ }
- public sealed class IEEE754Double
- {
+ public sealed class IEEE754Double
+ {
#pragma warning disable SA1310 // Field name should not contain an underscore
- internal const int EXPONENT_BIAS = 1023;
- internal const long EXPONENT_MASK = 0x7FF0000000000000L;
- internal const int FRACTION_BITS = 52;
- internal const long FRACTION_MASK = 0xFFFFFFFFFFFFFL;
+ internal const int EXPONENT_BIAS = 1023;
+ internal const long EXPONENT_MASK = 0x7FF0000000000000L;
+ internal const int FRACTION_BITS = 52;
+ internal const long FRACTION_MASK = 0xFFFFFFFFFFFFFL;
#pragma warning restore SA1310 // Field name should not contain an underscore
- public static string ToString(double value)
- {
- var repr = Convert.ToString(BitConverter.DoubleToInt64Bits(value), 2);
- return new string('0', 64 - repr.Length) + repr + ":" + "(" + value + ")";
- }
+ public static string ToString(double value)
+ {
+ var repr = Convert.ToString(BitConverter.DoubleToInt64Bits(value), 2);
+ return new string('0', 64 - repr.Length) + repr + ":" + "(" + value + ")";
+ }
+
+ public static double FromString(string value)
+ {
+ return BitConverter.Int64BitsToDouble(Convert.ToInt64(value, 2));
}
}
}
diff --git a/src/OpenTelemetry/Metrics/ExponentialBucketHistogramConfiguration.cs b/src/OpenTelemetry/Metrics/ExponentialBucketHistogramConfiguration.cs
index 2fcd109f4a6..389b2e0e4b0 100644
--- a/src/OpenTelemetry/Metrics/ExponentialBucketHistogramConfiguration.cs
+++ b/src/OpenTelemetry/Metrics/ExponentialBucketHistogramConfiguration.cs
@@ -14,19 +14,18 @@
// limitations under the License.
//
-namespace OpenTelemetry.Metrics
+namespace OpenTelemetry.Metrics;
+
+///
+/// Stores configuration for a histogram metric stream with exponential bucket boundaries.
+///
+internal class ExponentialBucketHistogramConfiguration : MetricStreamConfiguration
{
///
- /// Stores configuration for a histogram metric stream with exponential bucket boundaries.
+ /// Gets or sets the maximum number of buckets in each of the positive and negative ranges, not counting the special zero bucket.
///
- internal class ExponentialBucketHistogramConfiguration : MetricStreamConfiguration
- {
- ///
- /// Gets or sets the maximum number of buckets in each of the positive and negative ranges, not counting the special zero bucket.
- ///
- ///
- /// The default value is 160.
- ///
- public int MaxSize { get; set; } = 160;
- }
+ ///
+ /// The default value is 160.
+ ///
+ public int MaxSize { get; set; } = 160;
}
diff --git a/test/OpenTelemetry.Tests/Metrics/CircularBufferBucketsTest.cs b/test/OpenTelemetry.Tests/Metrics/CircularBufferBucketsTest.cs
new file mode 100644
index 00000000000..26f1477c121
--- /dev/null
+++ b/test/OpenTelemetry.Tests/Metrics/CircularBufferBucketsTest.cs
@@ -0,0 +1,116 @@
+//
+// Copyright The OpenTelemetry Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using Xunit;
+
+namespace OpenTelemetry.Metrics.Tests;
+
+public class CircularBufferBucketsTest
+{
+ [Fact]
+ public void Constructor()
+ {
+ Assert.Throws(() => new CircularBufferBuckets(0));
+ Assert.Throws(() => new CircularBufferBuckets(-1));
+ }
+
+ [Fact]
+ public void BasicInsertions()
+ {
+ var buckets = new CircularBufferBuckets(5);
+
+ Assert.Equal(5, buckets.Capacity);
+ Assert.Equal(0, buckets.Size);
+
+ Assert.True(buckets.TryIncrement(0));
+ Assert.Equal(1, buckets.Size);
+
+ Assert.True(buckets.TryIncrement(1));
+ Assert.Equal(2, buckets.Size);
+
+ Assert.True(buckets.TryIncrement(3));
+ Assert.Equal(4, buckets.Size);
+
+ Assert.True(buckets.TryIncrement(4));
+ Assert.Equal(5, buckets.Size);
+
+ Assert.True(buckets.TryIncrement(2));
+ Assert.Equal(5, buckets.Size);
+
+ Assert.False(buckets.TryIncrement(5));
+ Assert.False(buckets.TryIncrement(-1));
+ Assert.Equal(5, buckets.Size);
+ }
+
+ [Fact]
+ public void PositiveInsertions()
+ {
+ var buckets = new CircularBufferBuckets(5);
+
+ Assert.True(buckets.TryIncrement(102));
+ Assert.True(buckets.TryIncrement(103));
+ Assert.True(buckets.TryIncrement(101));
+ Assert.True(buckets.TryIncrement(100));
+ Assert.True(buckets.TryIncrement(104));
+
+ Assert.False(buckets.TryIncrement(99));
+ Assert.False(buckets.TryIncrement(105));
+ }
+
+ [Fact]
+ public void NegativeInsertions()
+ {
+ var buckets = new CircularBufferBuckets(5);
+
+ Assert.True(buckets.TryIncrement(2));
+ Assert.True(buckets.TryIncrement(0));
+ Assert.True(buckets.TryIncrement(-2));
+ Assert.True(buckets.TryIncrement(1));
+ Assert.True(buckets.TryIncrement(-1));
+
+ Assert.False(buckets.TryIncrement(3));
+ Assert.False(buckets.TryIncrement(-3));
+ }
+
+ [Fact]
+ public void IndexOperations()
+ {
+ var buckets = new CircularBufferBuckets(5);
+
+ buckets.TryIncrement(2);
+ buckets.TryIncrement(2);
+ buckets.TryIncrement(2);
+ buckets.TryIncrement(2);
+ buckets.TryIncrement(2);
+ buckets.TryIncrement(0);
+ buckets.TryIncrement(0);
+ buckets.TryIncrement(0);
+ buckets.TryIncrement(-2);
+ buckets.TryIncrement(1);
+ buckets.TryIncrement(1);
+ buckets.TryIncrement(1);
+ buckets.TryIncrement(1);
+ buckets.TryIncrement(-1);
+ buckets.TryIncrement(-1);
+
+ 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]);
+ }
+}
diff --git a/test/OpenTelemetry.Tests/Metrics/ExponentialBucketHistogramTest.cs b/test/OpenTelemetry.Tests/Metrics/ExponentialBucketHistogramTest.cs
index ce13bfdde44..7a1ea4708db 100644
--- a/test/OpenTelemetry.Tests/Metrics/ExponentialBucketHistogramTest.cs
+++ b/test/OpenTelemetry.Tests/Metrics/ExponentialBucketHistogramTest.cs
@@ -19,95 +19,94 @@
using System;
using Xunit;
-namespace OpenTelemetry.Metrics.Tests
+namespace OpenTelemetry.Metrics.Tests;
+
+public class ExponentialBucketHistogramTest
{
- public class ExponentialBucketHistogramTest
+ [Fact]
+ public void IndexLookup()
{
- [Fact]
- public void IndexLookup()
- {
- // An exponential bucket histogram with scale = 0.
- // The base is 2 ^ (2 ^ -0) = 2.
- // The buckets are:
- //
- // ...
- // bucket[-3]: (1/8, 1/4]
- // bucket[-2]: (1/4, 1/2]
- // bucket[-1]: (1/2, 1]
- // bucket[0]: (1, 2]
- // bucket[1]: (2, 4]
- // bucket[2]: (4, 8]
- // bucket[3]: (8, 16]
- // ...
+ // An exponential bucket histogram with scale = 0.
+ // The base is 2 ^ (2 ^ -0) = 2.
+ // The buckets are:
+ //
+ // ...
+ // bucket[-3]: (1/8, 1/4]
+ // bucket[-2]: (1/4, 1/2]
+ // bucket[-1]: (1/2, 1]
+ // bucket[0]: (1, 2]
+ // bucket[1]: (2, 4]
+ // bucket[2]: (4, 8]
+ // bucket[3]: (8, 16]
+ // ...
- var histogram_scale0 = new ExponentialBucketHistogram(0);
+ var histogram_scale0 = new ExponentialBucketHistogram(0);
- Assert.Equal(-1075, histogram_scale0.MapToIndex(double.Epsilon));
+ Assert.Equal(-1075, histogram_scale0.MapToIndex(double.Epsilon));
- Assert.Equal(-1074, histogram_scale0.MapToIndex(double.Epsilon * 2));
+ Assert.Equal(-1074, histogram_scale0.MapToIndex(double.Epsilon * 2));
- Assert.Equal(-1073, histogram_scale0.MapToIndex(double.Epsilon * 3));
- Assert.Equal(-1073, histogram_scale0.MapToIndex(double.Epsilon * 4));
+ Assert.Equal(-1073, histogram_scale0.MapToIndex(double.Epsilon * 3));
+ Assert.Equal(-1073, histogram_scale0.MapToIndex(double.Epsilon * 4));
- Assert.Equal(-1072, histogram_scale0.MapToIndex(double.Epsilon * 5));
- Assert.Equal(-1072, histogram_scale0.MapToIndex(double.Epsilon * 6));
- Assert.Equal(-1072, histogram_scale0.MapToIndex(double.Epsilon * 7));
- Assert.Equal(-1072, histogram_scale0.MapToIndex(double.Epsilon * 8));
+ Assert.Equal(-1072, histogram_scale0.MapToIndex(double.Epsilon * 5));
+ Assert.Equal(-1072, histogram_scale0.MapToIndex(double.Epsilon * 6));
+ Assert.Equal(-1072, histogram_scale0.MapToIndex(double.Epsilon * 7));
+ Assert.Equal(-1072, histogram_scale0.MapToIndex(double.Epsilon * 8));
- Assert.Equal(-1023, histogram_scale0.MapToIndex(2.2250738585072009E-308));
- Assert.Equal(-1023, histogram_scale0.MapToIndex(2.2250738585072014E-308));
+ Assert.Equal(-1023, histogram_scale0.MapToIndex(2.2250738585072009E-308));
+ Assert.Equal(-1023, histogram_scale0.MapToIndex(2.2250738585072014E-308));
- Assert.Equal(-3, histogram_scale0.MapToIndex(0.25));
+ Assert.Equal(-3, histogram_scale0.MapToIndex(0.25));
- Assert.Equal(-2, histogram_scale0.MapToIndex(0.375));
- Assert.Equal(-2, histogram_scale0.MapToIndex(0.5));
+ Assert.Equal(-2, histogram_scale0.MapToIndex(0.375));
+ Assert.Equal(-2, histogram_scale0.MapToIndex(0.5));
- Assert.Equal(-1, histogram_scale0.MapToIndex(0.75));
- Assert.Equal(-1, histogram_scale0.MapToIndex(1));
+ Assert.Equal(-1, histogram_scale0.MapToIndex(0.75));
+ Assert.Equal(-1, histogram_scale0.MapToIndex(1));
- Assert.Equal(0, histogram_scale0.MapToIndex(1.5));
- Assert.Equal(0, histogram_scale0.MapToIndex(2));
+ Assert.Equal(0, histogram_scale0.MapToIndex(1.5));
+ Assert.Equal(0, histogram_scale0.MapToIndex(2));
- Assert.Equal(1, histogram_scale0.MapToIndex(3));
- Assert.Equal(1, histogram_scale0.MapToIndex(4));
+ Assert.Equal(1, histogram_scale0.MapToIndex(3));
+ Assert.Equal(1, histogram_scale0.MapToIndex(4));
- Assert.Equal(2, histogram_scale0.MapToIndex(5));
- Assert.Equal(2, histogram_scale0.MapToIndex(6));
- Assert.Equal(2, histogram_scale0.MapToIndex(7));
- Assert.Equal(2, histogram_scale0.MapToIndex(8));
+ Assert.Equal(2, histogram_scale0.MapToIndex(5));
+ Assert.Equal(2, histogram_scale0.MapToIndex(6));
+ Assert.Equal(2, histogram_scale0.MapToIndex(7));
+ Assert.Equal(2, histogram_scale0.MapToIndex(8));
- Assert.Equal(3, histogram_scale0.MapToIndex(9));
- Assert.Equal(3, histogram_scale0.MapToIndex(16));
+ Assert.Equal(3, histogram_scale0.MapToIndex(9));
+ Assert.Equal(3, histogram_scale0.MapToIndex(16));
- Assert.Equal(4, histogram_scale0.MapToIndex(17));
- Assert.Equal(4, histogram_scale0.MapToIndex(32));
+ Assert.Equal(4, histogram_scale0.MapToIndex(17));
+ Assert.Equal(4, histogram_scale0.MapToIndex(32));
- // An exponential bucket histogram with scale = 1.
- // The base is 2 ^ (2 ^ -1) = sqrt(2) = 1.41421356237.
- // The buckets are:
- //
- // ...
- // bucket[-3]: (0.35355339059, 1/2]
- // bucket[-2]: (1/2, 0.70710678118]
- // bucket[-1]: (0.70710678118, 1]
- // bucket[0]: (1, 1.41421356237]
- // bucket[1]: (1.41421356237, 2]
- // bucket[2]: (2, 2.82842712474]
- // bucket[3]: (2.82842712474, 4]
- // ...
+ // An exponential bucket histogram with scale = 1.
+ // The base is 2 ^ (2 ^ -1) = sqrt(2) = 1.41421356237.
+ // The buckets are:
+ //
+ // ...
+ // bucket[-3]: (0.35355339059, 1/2]
+ // bucket[-2]: (1/2, 0.70710678118]
+ // bucket[-1]: (0.70710678118, 1]
+ // bucket[0]: (1, 1.41421356237]
+ // bucket[1]: (1.41421356237, 2]
+ // bucket[2]: (2, 2.82842712474]
+ // bucket[3]: (2.82842712474, 4]
+ // ...
- var histogram_scale1 = new ExponentialBucketHistogram(1);
+ var histogram_scale1 = new ExponentialBucketHistogram(1);
- Assert.Equal(-3, histogram_scale1.MapToIndex(0.5));
+ Assert.Equal(-3, histogram_scale1.MapToIndex(0.5));
- Assert.Equal(-2, histogram_scale1.MapToIndex(0.6));
+ Assert.Equal(-2, histogram_scale1.MapToIndex(0.6));
- Assert.Equal(-1, histogram_scale1.MapToIndex(1));
+ Assert.Equal(-1, histogram_scale1.MapToIndex(1));
- Assert.Equal(1, histogram_scale1.MapToIndex(2));
+ Assert.Equal(1, histogram_scale1.MapToIndex(2));
- Assert.Equal(3, histogram_scale1.MapToIndex(4));
- }
+ Assert.Equal(3, histogram_scale1.MapToIndex(4));
}
}