Skip to content

Commit

Permalink
Exponential Bucket Histogram - part 2 (#3468)
Browse files Browse the repository at this point in the history
  • Loading branch information
reyang authored Jul 21, 2022
1 parent 54e83a9 commit cf3dfc5
Show file tree
Hide file tree
Showing 5 changed files with 399 additions and 157 deletions.
119 changes: 119 additions & 0 deletions src/OpenTelemetry/Metrics/CircularBufferBuckets.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// <copyright file="CircularBufferBuckets.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>

using System.Runtime.CompilerServices;

using OpenTelemetry.Internal;

namespace OpenTelemetry.Metrics;

/// <summary>
/// A histogram buckets implementation based on circular buffer.
/// </summary>
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;
}

/// <summary>
/// Gets the capacity of the <see cref="CircularBufferBuckets"/>.
/// </summary>
public int Capacity { get; }

/// <summary>
/// Gets the size of the <see cref="CircularBufferBuckets"/>.
/// </summary>
public int Size => this.end - this.begin + 1;

/// <summary>
/// Returns the value of <c>Bucket[index]</c>.
/// </summary>
/// <param name="index">The index of the bucket.</param>
/// <remarks>
/// 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.
/// </remarks>
public long this[int index]
{
get => this.trait[this.ModuloIndex(index)];
}

/// <summary>
/// Attempts to increment the value of <c>Bucket[index]</c>.
/// </summary>
/// <param name="index">The index of the bucket.</param>
/// <returns>
/// Returns <c>true</c> if the increment attempt succeeded;
/// <c>false</c> if the underlying buffer is running out of capacity.
/// </returns>
/// <remarks>
/// The "index" value can be positive, zero or negative.
/// </remarks>
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;
}
}
167 changes: 88 additions & 79 deletions src/OpenTelemetry/Metrics/ExponentialBucketHistogram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,114 +18,123 @@

using System;
using System.Diagnostics;

using OpenTelemetry.Internal;

namespace OpenTelemetry.Metrics
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[index] = ( base ^ index, base ^ (index + 1) ]</c>,
/// where <c>index</c> is an integer.
/// </summary>
internal class ExponentialBucketHistogram
{
/// <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)
{
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);
}
}
/// <inheritdoc/>
public override string ToString()
{
return nameof(ExponentialBucketHistogram)
+ "{"
+ nameof(this.Scale) + "=" + this.Scale
+ "}";
}

internal long ZeroCount { get; private set; }
/// <summary>
/// Maps a finite positive IEEE 754 double-precision floating-point
/// number to <c>Bucket[index] = ( base ^ index, base ^ (index + 1) ]</c>,
/// where <c>index</c> is an integer.
/// </summary>
/// <param name="value">
/// The value to be bucketized. Must be a finite positive number.
/// </param>
/// <returns>
/// Returns the index of the bucket.
/// </returns>
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.");

/// <inheritdoc/>
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));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,18 @@
// limitations under the License.
// </copyright>

namespace OpenTelemetry.Metrics
namespace OpenTelemetry.Metrics;

/// <summary>
/// Stores configuration for a histogram metric stream with exponential bucket boundaries.
/// </summary>
internal class ExponentialBucketHistogramConfiguration : MetricStreamConfiguration
{
/// <summary>
/// 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.
/// </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;
}
/// <remarks>
/// The default value is 160.
/// </remarks>
public int MaxSize { get; set; } = 160;
}
Loading

0 comments on commit cf3dfc5

Please sign in to comment.