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)); } }