Skip to content

Commit

Permalink
Exponential Bucket Histogram - part 3 (#3473)
Browse files Browse the repository at this point in the history
  • Loading branch information
reyang authored Jul 22, 2022
1 parent 7de3f00 commit 795aedc
Show file tree
Hide file tree
Showing 6 changed files with 287 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
using OpenTelemetry.Internal;

namespace OpenTelemetry.Exporter.Prometheus
{
Expand All @@ -39,11 +40,7 @@ internal static partial class PrometheusSerializer
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int WriteDouble(byte[] buffer, int cursor, double value)
{
#if NETCOREAPP3_1_OR_GREATER
if (double.IsFinite(value))
#else
if (!double.IsInfinity(value) && !double.IsNaN(value))
#endif
if (MathHelper.IsFinite(value))
{
#if NETCOREAPP3_1_OR_GREATER
Span<char> span = stackalloc char[128];
Expand Down
119 changes: 119 additions & 0 deletions src/OpenTelemetry/Internal/MathHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// <copyright file="MathHelper.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;
using System.Runtime.CompilerServices;

namespace OpenTelemetry.Internal;

internal static class MathHelper
{
// https://en.wikipedia.org/wiki/Leading_zero
private static readonly byte[] LeadingZeroLookupTable = new byte[]
{
8, 7, 6, 6, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int LeadingZero8(byte value)
{
return LeadingZeroLookupTable[value];
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int LeadingZero16(short value)
{
unchecked
{
var high8 = (byte)(value >> 8);

if (high8 != 0)
{
return LeadingZero8(high8);
}

return LeadingZero8((byte)value) + 8;
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int LeadingZero32(int value)
{
unchecked
{
var high16 = (short)(value >> 16);

if (high16 != 0)
{
return LeadingZero16(high16);
}

return LeadingZero16((short)value) + 16;
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int LeadingZero64(long value)
{
unchecked
{
var high32 = (int)(value >> 32);

if (high32 != 0)
{
return LeadingZero32(high32);
}

return LeadingZero32((int)value) + 32;
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsFinite(double value)
{
#if NETCOREAPP3_1_OR_GREATER
return double.IsFinite(value);
#else
return !double.IsInfinity(value) && !double.IsNaN(value);
#endif
}

public static string DoubleToString(double value)
{
var repr = Convert.ToString(BitConverter.DoubleToInt64Bits(value), 2);
return new string('0', 64 - repr.Length) + repr;
}

public static double DoubleFromString(string value)
{
return BitConverter.Int64BitsToDouble(Convert.ToInt64(value, 2));
}
}
39 changes: 29 additions & 10 deletions src/OpenTelemetry/Metrics/CircularBufferBuckets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
// </copyright>

using System.Runtime.CompilerServices;

using OpenTelemetry.Internal;

namespace OpenTelemetry.Metrics;
Expand Down Expand Up @@ -65,43 +64,63 @@ public long this[int index]
/// </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 <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>
/// <remarks>
/// The "index" value can be positive, zero or negative.
/// </remarks>
public bool TryIncrement(int index)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int TryIncrement(int index)
{
var capacity = this.Capacity;

if (this.trait == null)
{
this.trait = new long[this.Capacity];
this.trait = new long[capacity];

this.begin = index;
this.end = index;
}
else if (index > this.end)
{
if (index - this.begin >= this.Capacity)
var diff = index - this.begin;
if (diff >= capacity)
{
return false;
return CalculateScaleReduction(diff + 1, capacity);
}

this.end = index;
}
else if (index < this.begin)
{
if (this.end - index >= this.Capacity)
var diff = this.end - index;
if (diff >= this.Capacity)
{
return false;
return CalculateScaleReduction(diff + 1, capacity);
}

this.begin = index;
}

this.trait[this.ModuloIndex(index)] += 1;

return true;
return 0;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
int CalculateScaleReduction(int size, int capacity)
{
var shift = MathHelper.LeadingZero32(capacity) - MathHelper.LeadingZero32(size);

if (size > (capacity << shift))
{
shift++;
}

return shift;
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down
56 changes: 27 additions & 29 deletions src/OpenTelemetry/Metrics/ExponentialBucketHistogram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ internal class ExponentialBucketHistogram
public ExponentialBucketHistogram(int scale, int maxBuckets = 160)
{
Guard.ThrowIfOutOfRange(scale, min: -20, max: 20); // TODO: calculate the actual range
Guard.ThrowIfOutOfRange(maxBuckets, min: 1);

this.Scale = scale;
this.PositiveBuckets = new CircularBufferBuckets(maxBuckets);
this.NegativeBuckets = new CircularBufferBuckets(maxBuckets);
}

internal int Scale
Expand All @@ -53,8 +56,12 @@ private set
}
}

internal CircularBufferBuckets PositiveBuckets { get; }

internal long ZeroCount { get; private set; }

internal CircularBufferBuckets NegativeBuckets { get; }

/// <inheritdoc/>
public override string ToString()
{
Expand All @@ -77,7 +84,7 @@ public override string ToString()
/// </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(MathHelper.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.");

Expand All @@ -90,51 +97,42 @@ public int MapToIndex(double value)
else
{
var bits = BitConverter.DoubleToInt64Bits(value);
var exp = (int)((bits & IEEE754Double.EXPONENT_MASK) >> IEEE754Double.FRACTION_BITS);
var fraction = bits & IEEE754Double.FRACTION_MASK;
var exp = (int)((bits & 0x7FF0000000000000L /* exponent mask */) >> 52 /* fraction width */);
var fraction = bits & 0xFFFFFFFFFFFFFL /* 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--;
}
exp -= MathHelper.LeadingZero64(fraction - 1) - 12 /* 64 - fraction width */;
}
else if (fraction == 0)
{
exp--;
}

return (exp - IEEE754Double.EXPONENT_BIAS) >> -this.Scale;
return (exp - 1023 /* exponent bias */) >> -this.Scale;
}
}

public sealed class IEEE754Double
public void Record(double value)
{
#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)
if (!MathHelper.IsFinite(value))
{
var repr = Convert.ToString(BitConverter.DoubleToInt64Bits(value), 2);
return new string('0', 64 - repr.Length) + repr + ":" + "(" + value + ")";
return;
}

public static double FromString(string value)
var c = value.CompareTo(0);

if (c > 0)
{
this.PositiveBuckets.TryIncrement(this.MapToIndex(value));
}
else if (c < 0)
{
this.NegativeBuckets.TryIncrement(this.MapToIndex(-value));
}
else
{
return BitConverter.Int64BitsToDouble(Convert.ToInt64(value, 2));
this.ZeroCount++;
}
}
}
Expand Down
Loading

0 comments on commit 795aedc

Please sign in to comment.