Skip to content

Commit

Permalink
Exponential Bucket Histogram - part 1 (#3462)
Browse files Browse the repository at this point in the history
  • Loading branch information
reyang authored Jul 20, 2022
1 parent f58d909 commit 95419ed
Show file tree
Hide file tree
Showing 3 changed files with 279 additions and 0 deletions.
133 changes: 133 additions & 0 deletions src/OpenTelemetry/Metrics/ExponentialBucketHistogram.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// <copyright file="ExponentialBucketHistogram.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>

#if NET6_0_OR_GREATER

using System;
using System.Diagnostics;

using OpenTelemetry.Internal;

namespace OpenTelemetry.Metrics
{
/// <summary>
/// Represents an exponential bucket histogram with base = 2 ^ (2 ^ (-scale)).
/// An exponential bucket histogram has infinite number of buckets, which are
/// identified by <c>Bucket[i] = ( base ^ i, base ^ (i + 1) ]</c>, where <c>i</c>
/// is an integer.
/// </summary>
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)
{
Guard.ThrowIfOutOfRange(scale, min: -20, max: 20); // TODO: calculate the actual range

this.Scale = scale;
}

internal int Scale
{
get
{
return this.scale;
}

private set
{
this.scale = value;
this.scalingFactor = Math.ScaleB(Log2E, value);
}
}

internal long ZeroCount { get; private set; }

/// <inheritdoc/>
public override string ToString()
{
return nameof(ExponentialBucketHistogram)
+ "{"
+ nameof(this.Scale) + "=" + this.Scale
+ "}";
}

public int MapToIndex(double value)
{
Debug.Assert(value != 0, "IEEE-754 zero values should be handled by ZeroCount.");

// 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
{
var bits = BitConverter.DoubleToInt64Bits(value);
var exp = (int)((bits & IEEE754Double.EXPONENT_MASK) >> IEEE754Double.FRACTION_BITS);
var fraction = bits & IEEE754Double.FRACTION_MASK;

if (exp == 0)
{
// 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)
{
break;
}

exp--;
}
}
else if (fraction == 0)
{
exp--;
}

return (exp - IEEE754Double.EXPONENT_BIAS) >> -this.Scale;
}
}

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;
#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 + ")";
}
}
}
}

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// <copyright file="ExponentialBucketHistogramConfiguration.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>

namespace OpenTelemetry.Metrics
{
/// <summary>
/// Stores configuration for a histogram metric stream with exponential bucket boundaries.
/// </summary>
internal class ExponentialBucketHistogramConfiguration : MetricStreamConfiguration
{
/// <summary>
/// Gets or sets the maximum number of buckets in each of the positive and negative ranges, not counting the special zero bucket.
/// </summary>
/// <remarks>
/// The default value is 160.
/// </remarks>
public int MaxSize { get; set; } = 160;
}
}
114 changes: 114 additions & 0 deletions test/OpenTelemetry.Tests/Metrics/ExponentialBucketHistogramTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// <copyright file="ExponentialBucketHistogramTest.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>

#if NET6_0_OR_GREATER

using System;
using Xunit;

namespace OpenTelemetry.Metrics.Tests
{
public class ExponentialBucketHistogramTest
{
[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]
// ...

var histogram_scale0 = new ExponentialBucketHistogram(0);

Assert.Equal(-1075, histogram_scale0.MapToIndex(double.Epsilon));

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(-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(-3, histogram_scale0.MapToIndex(0.25));

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(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(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(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]
// ...

var histogram_scale1 = new ExponentialBucketHistogram(1);

Assert.Equal(-3, histogram_scale1.MapToIndex(0.5));

Assert.Equal(-2, histogram_scale1.MapToIndex(0.6));

Assert.Equal(-1, histogram_scale1.MapToIndex(1));

Assert.Equal(1, histogram_scale1.MapToIndex(2));

Assert.Equal(3, histogram_scale1.MapToIndex(4));
}
}
}

#endif

0 comments on commit 95419ed

Please sign in to comment.