You've already forked opentelemetry-go
mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2025-07-15 01:04:25 +02:00
Exponential Histogram mapping functions for public use (#2502)
* Exponential Histogram mapping functions for public use * pr num * Apply suggestions from code review 👍 Co-authored-by: Anthony Mirabella <a9@aneurysm9.com> * mapping interface comments * missed add * Update CHANGELOG.md Co-authored-by: Anthony Mirabella <a9@aneurysm9.com> Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>
This commit is contained in:
@ -15,6 +15,10 @@ Code instrumented with the `go.opentelemetry.io/otel/metric` will need to be mod
|
||||
|
||||
### Added
|
||||
|
||||
- Log the Exporters configuration in the TracerProviders message. (#2578)
|
||||
- Metrics Exponential Histogram support: Mapping functions have been made available
|
||||
in `sdk/metric/aggregator/exponential/mapping` for other OpenTelemetry projects to take
|
||||
dependencies on. (#2502)
|
||||
- Add go 1.18 to our compatibility tests. (#2679)
|
||||
- Allow configuring the Sampler with the `OTEL_TRACES_SAMPLER` and `OTEL_TRACES_SAMPLER_ARG` environment variables. (#2305, #2517)
|
||||
- Add the `metric/global` for obtaining and setting the global `MeterProvider` (#2660)
|
||||
|
27
sdk/metric/aggregator/exponential/README.md
Normal file
27
sdk/metric/aggregator/exponential/README.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Base-2 Exponential Histogram
|
||||
|
||||
## Design
|
||||
|
||||
This document is a placeholder for future Aggregator, once seen in [PR
|
||||
2393](https://github.com/open-telemetry/opentelemetry-go/pull/2393).
|
||||
|
||||
Only the mapping functions have been made available at this time. The
|
||||
equations tested here are specified in the [data model for Exponential
|
||||
Histogram data points](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/datamodel.md#exponentialhistogram).
|
||||
|
||||
### Mapping function
|
||||
|
||||
There are two mapping functions used, depending on the sign of the
|
||||
scale. Negative and zero scales use the `mapping/exponent` mapping
|
||||
function, which computes the bucket index directly from the bits of
|
||||
the `float64` exponent. This mapping function is used with scale `-10
|
||||
<= scale <= 0`. Scales smaller than -10 map the entire normal
|
||||
`float64` number range into a single bucket, thus are not considered
|
||||
useful.
|
||||
|
||||
The `mapping/logarithm` mapping function uses `math.Log(value)` times
|
||||
the scaling factor `math.Ldexp(math.Log2E, scale)`. This mapping
|
||||
function is used with `0 < scale <= 20`. The maximum scale is
|
||||
selected because at scale 21, simply, it becomes difficult to test
|
||||
correctness--at this point `math.MaxFloat64` maps to index
|
||||
`math.MaxInt32` and the `math/big` logic used in testing breaks down.
|
117
sdk/metric/aggregator/exponential/mapping/exponent/exponent.go
Normal file
117
sdk/metric/aggregator/exponential/mapping/exponent/exponent.go
Normal file
@ -0,0 +1,117 @@
|
||||
// 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.
|
||||
|
||||
package exponent // import "go.opentelemetry.io/otel/sdk/metric/aggregator/exponential/mapping/exponent"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"go.opentelemetry.io/otel/sdk/metric/aggregator/exponential/mapping"
|
||||
)
|
||||
|
||||
const (
|
||||
// MinScale defines the point at which the exponential mapping
|
||||
// function becomes useless for float64. With scale -10, ignoring
|
||||
// subnormal values, bucket indices range from -1 to 1.
|
||||
MinScale int32 = -10
|
||||
|
||||
// MaxScale is the largest scale supported in this code. Use
|
||||
// ../logarithm for larger scales.
|
||||
MaxScale int32 = 0
|
||||
)
|
||||
|
||||
type exponentMapping struct {
|
||||
shift uint8 // equals negative scale
|
||||
}
|
||||
|
||||
// exponentMapping is used for negative scales, effectively a
|
||||
// mapping of the base-2 logarithm of the exponent.
|
||||
var prebuiltMappings = [-MinScale + 1]exponentMapping{
|
||||
{10},
|
||||
{9},
|
||||
{8},
|
||||
{7},
|
||||
{6},
|
||||
{5},
|
||||
{4},
|
||||
{3},
|
||||
{2},
|
||||
{1},
|
||||
{0},
|
||||
}
|
||||
|
||||
// NewMapping constructs an exponential mapping function, used for scales <= 0.
|
||||
func NewMapping(scale int32) (mapping.Mapping, error) {
|
||||
if scale > MaxScale {
|
||||
return nil, fmt.Errorf("exponent mapping requires scale <= 0")
|
||||
}
|
||||
if scale < MinScale {
|
||||
return nil, fmt.Errorf("scale too low")
|
||||
}
|
||||
return &prebuiltMappings[scale-MinScale], nil
|
||||
}
|
||||
|
||||
// MapToIndex implements mapping.Mapping.
|
||||
func (e *exponentMapping) MapToIndex(value float64) int32 {
|
||||
// Note: we can assume not a 0, Inf, or NaN; positive sign bit.
|
||||
|
||||
// Note: bit-shifting does the right thing for negative
|
||||
// exponents, e.g., -1 >> 1 == -1.
|
||||
return getBase2(value) >> e.shift
|
||||
}
|
||||
|
||||
func (e *exponentMapping) minIndex() int32 {
|
||||
return int32(MinNormalExponent) >> e.shift
|
||||
}
|
||||
|
||||
func (e *exponentMapping) maxIndex() int32 {
|
||||
return int32(MaxNormalExponent) >> e.shift
|
||||
}
|
||||
|
||||
// LowerBoundary implements mapping.Mapping.
|
||||
func (e *exponentMapping) LowerBoundary(index int32) (float64, error) {
|
||||
if min := e.minIndex(); index < min {
|
||||
return 0, mapping.ErrUnderflow
|
||||
}
|
||||
|
||||
if max := e.maxIndex(); index > max {
|
||||
return 0, mapping.ErrOverflow
|
||||
}
|
||||
|
||||
unbiased := int64(index << e.shift)
|
||||
|
||||
// Note: although the mapping function rounds subnormal values
|
||||
// up to the smallest normal value, there are still buckets
|
||||
// that may be filled that start at subnormal values. The
|
||||
// following code handles this correctly. It's equivalent to and
|
||||
// faster than math.Ldexp(1, int(unbiased)).
|
||||
if unbiased < int64(MinNormalExponent) {
|
||||
subnormal := uint64(1 << SignificandWidth)
|
||||
for unbiased < int64(MinNormalExponent) {
|
||||
unbiased++
|
||||
subnormal >>= 1
|
||||
}
|
||||
return math.Float64frombits(subnormal), nil
|
||||
}
|
||||
exponent := unbiased + ExponentBias
|
||||
|
||||
bits := uint64(exponent << SignificandWidth)
|
||||
return math.Float64frombits(bits), nil
|
||||
}
|
||||
|
||||
// Scale implements mapping.Mapping.
|
||||
func (e *exponentMapping) Scale() int32 {
|
||||
return -int32(e.shift)
|
||||
}
|
@ -0,0 +1,310 @@
|
||||
// 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.
|
||||
|
||||
package exponent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.opentelemetry.io/otel/sdk/metric/aggregator/exponential/mapping"
|
||||
)
|
||||
|
||||
type expectMapping struct {
|
||||
value float64
|
||||
index int32
|
||||
}
|
||||
|
||||
// Tests that getBase2 returns the base-2 exponent as documented, unlike
|
||||
// math.Frexp.
|
||||
func TestGetBase2(t *testing.T) {
|
||||
require.Equal(t, int32(-1022), MinNormalExponent)
|
||||
require.Equal(t, int32(+1023), MaxNormalExponent)
|
||||
|
||||
require.Equal(t, MaxNormalExponent, getBase2(0x1p+1023))
|
||||
require.Equal(t, int32(1022), getBase2(0x1p+1022))
|
||||
|
||||
require.Equal(t, int32(0), getBase2(1))
|
||||
|
||||
require.Equal(t, int32(-1021), getBase2(0x1p-1021))
|
||||
require.Equal(t, int32(-1022), getBase2(0x1p-1022))
|
||||
|
||||
// Subnormals below this point
|
||||
require.Equal(t, int32(-1022), getBase2(0x1p-1023))
|
||||
require.Equal(t, int32(-1022), getBase2(0x1p-1024))
|
||||
require.Equal(t, int32(-1022), getBase2(0x1p-1025))
|
||||
require.Equal(t, int32(-1022), getBase2(0x1p-1074))
|
||||
}
|
||||
|
||||
// Tests a few cases with scale=0.
|
||||
func TestExponentMappingZero(t *testing.T) {
|
||||
m, err := NewMapping(0)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, int32(0), m.Scale())
|
||||
|
||||
for _, pair := range []expectMapping{
|
||||
{math.MaxFloat64, MaxNormalExponent},
|
||||
{0x1p+1023, MaxNormalExponent},
|
||||
{0x1p-1022, MinNormalExponent},
|
||||
{math.SmallestNonzeroFloat64, MinNormalExponent},
|
||||
{4, 2},
|
||||
{3, 1},
|
||||
{2, 1},
|
||||
{1.5, 0},
|
||||
{1, 0},
|
||||
{0.75, -1},
|
||||
{0.5, -1},
|
||||
{0.25, -2},
|
||||
} {
|
||||
idx := m.MapToIndex(pair.value)
|
||||
|
||||
require.Equal(t, pair.index, idx)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests a few cases with scale=MinScale.
|
||||
func TestExponentMappingMinScale(t *testing.T) {
|
||||
m, err := NewMapping(MinScale)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, MinScale, m.Scale())
|
||||
|
||||
for _, pair := range []expectMapping{
|
||||
{1, 0},
|
||||
{math.MaxFloat64 / 2, 0},
|
||||
{math.MaxFloat64, 0},
|
||||
{math.SmallestNonzeroFloat64, -1},
|
||||
{0.5, -1},
|
||||
} {
|
||||
t.Run(fmt.Sprint(pair.value), func(t *testing.T) {
|
||||
idx := m.MapToIndex(pair.value)
|
||||
|
||||
require.Equal(t, pair.index, idx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Tests invalid scales.
|
||||
func TestInvalidScale(t *testing.T) {
|
||||
m, err := NewMapping(1)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, m)
|
||||
|
||||
m, err = NewMapping(MinScale - 1)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, m)
|
||||
}
|
||||
|
||||
// Tests a few cases with scale=-1.
|
||||
func TestExponentMappingNegOne(t *testing.T) {
|
||||
m, _ := NewMapping(-1)
|
||||
|
||||
for _, pair := range []expectMapping{
|
||||
{16, 2},
|
||||
{15, 1},
|
||||
{9, 1},
|
||||
{8, 1},
|
||||
{5, 1},
|
||||
{4, 1},
|
||||
{3, 0},
|
||||
{2, 0},
|
||||
{1.5, 0},
|
||||
{1, 0},
|
||||
{0.75, -1},
|
||||
{0.5, -1},
|
||||
{0.25, -1},
|
||||
{0.20, -2},
|
||||
{0.13, -2},
|
||||
{0.125, -2},
|
||||
{0.10, -2},
|
||||
{0.0625, -2},
|
||||
{0.06, -3},
|
||||
} {
|
||||
idx := m.MapToIndex(pair.value)
|
||||
require.Equal(t, pair.index, idx, "value: %v", pair.value)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests a few cases with scale=-4.
|
||||
func TestExponentMappingNegFour(t *testing.T) {
|
||||
m, err := NewMapping(-4)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int32(-4), m.Scale())
|
||||
|
||||
for _, pair := range []expectMapping{
|
||||
{float64(0x1), 0},
|
||||
{float64(0x10), 0},
|
||||
{float64(0x100), 0},
|
||||
{float64(0x1000), 0},
|
||||
{float64(0x10000), 1}, // Base == 2**16
|
||||
{float64(0x100000), 1},
|
||||
{float64(0x1000000), 1},
|
||||
{float64(0x10000000), 1},
|
||||
{float64(0x100000000), 2}, // == 2**32
|
||||
{float64(0x1000000000), 2},
|
||||
{float64(0x10000000000), 2},
|
||||
{float64(0x100000000000), 2},
|
||||
{float64(0x1000000000000), 3}, // 2**48
|
||||
{float64(0x10000000000000), 3},
|
||||
{float64(0x100000000000000), 3},
|
||||
{float64(0x1000000000000000), 3},
|
||||
{float64(0x10000000000000000), 4}, // 2**64
|
||||
{float64(0x100000000000000000), 4},
|
||||
{float64(0x1000000000000000000), 4},
|
||||
{float64(0x10000000000000000000), 4},
|
||||
{float64(0x100000000000000000000), 5},
|
||||
|
||||
{1 / float64(0x1), 0},
|
||||
{1 / float64(0x10), -1},
|
||||
{1 / float64(0x100), -1},
|
||||
{1 / float64(0x1000), -1},
|
||||
{1 / float64(0x10000), -1}, // 2**-16
|
||||
{1 / float64(0x100000), -2},
|
||||
{1 / float64(0x1000000), -2},
|
||||
{1 / float64(0x10000000), -2},
|
||||
{1 / float64(0x100000000), -2}, // 2**-32
|
||||
{1 / float64(0x1000000000), -3},
|
||||
{1 / float64(0x10000000000), -3},
|
||||
{1 / float64(0x100000000000), -3},
|
||||
{1 / float64(0x1000000000000), -3}, // 2**-48
|
||||
{1 / float64(0x10000000000000), -4},
|
||||
{1 / float64(0x100000000000000), -4},
|
||||
{1 / float64(0x1000000000000000), -4},
|
||||
{1 / float64(0x10000000000000000), -4}, // 2**-64
|
||||
{1 / float64(0x100000000000000000), -5},
|
||||
|
||||
// Max values
|
||||
{0x1.FFFFFFFFFFFFFp1023, 63},
|
||||
{0x1p1023, 63},
|
||||
{0x1p1019, 63},
|
||||
{0x1p1008, 63},
|
||||
{0x1p1007, 62},
|
||||
{0x1p1000, 62},
|
||||
{0x1p0992, 62},
|
||||
{0x1p0991, 61},
|
||||
|
||||
// Min and subnormal values
|
||||
{0x1p-1074, -64},
|
||||
{0x1p-1073, -64},
|
||||
{0x1p-1072, -64},
|
||||
{0x1p-1057, -64},
|
||||
{0x1p-1056, -64},
|
||||
{0x1p-1041, -64},
|
||||
{0x1p-1040, -64},
|
||||
{0x1p-1025, -64},
|
||||
{0x1p-1024, -64},
|
||||
{0x1p-1023, -64},
|
||||
{0x1p-1022, -64},
|
||||
{0x1p-1009, -64},
|
||||
{0x1p-1008, -63},
|
||||
{0x1p-0993, -63},
|
||||
{0x1p-0992, -62},
|
||||
{0x1p-0977, -62},
|
||||
{0x1p-0976, -61},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("%x", pair.value), func(t *testing.T) {
|
||||
index := m.MapToIndex(pair.value)
|
||||
|
||||
require.Equal(t, pair.index, index, "value: %#x", pair.value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// roundedBoundary computes the correct boundary rounded to a float64
|
||||
// using math/big. Note that this function uses a Square() where the
|
||||
// one in ../logarithm uses a SquareRoot().
|
||||
func roundedBoundary(scale, index int32) float64 {
|
||||
one := big.NewFloat(1)
|
||||
f := (&big.Float{}).SetMantExp(one, int(index))
|
||||
for i := scale; i < 0; i++ {
|
||||
f = (&big.Float{}).Mul(f, f)
|
||||
}
|
||||
|
||||
result, _ := f.Float64()
|
||||
return result
|
||||
}
|
||||
|
||||
// TestExponentIndexMax ensures that for every valid scale, MaxFloat
|
||||
// maps into the correct maximum index. Also tests that the reverse
|
||||
// lookup does not produce infinity and the following index produces
|
||||
// an overflow error.
|
||||
func TestExponentIndexMax(t *testing.T) {
|
||||
for scale := MinScale; scale <= MaxScale; scale++ {
|
||||
m, err := NewMapping(scale)
|
||||
require.NoError(t, err)
|
||||
|
||||
index := m.MapToIndex(MaxValue)
|
||||
|
||||
// Correct max index is one less than the first index
|
||||
// that overflows math.MaxFloat64, i.e., one less than
|
||||
// the index of +Inf.
|
||||
maxIndex := (int32(MaxNormalExponent+1) >> -scale) - 1
|
||||
require.Equal(t, index, int32(maxIndex))
|
||||
|
||||
// The index maps to a finite boundary.
|
||||
bound, err := m.LowerBoundary(index)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, bound, roundedBoundary(scale, maxIndex))
|
||||
|
||||
// One larger index will overflow.
|
||||
_, err = m.LowerBoundary(index + 1)
|
||||
require.Equal(t, err, mapping.ErrOverflow)
|
||||
}
|
||||
}
|
||||
|
||||
// TestExponentIndexMin ensures that for every valid scale, the
|
||||
// smallest normal number and all smaller numbers map to the correct
|
||||
// index, which is that of the smallest normal number.
|
||||
//
|
||||
// Tests that the lower boundary of the smallest bucket is correct,
|
||||
// even when that number is subnormal.
|
||||
func TestExponentIndexMin(t *testing.T) {
|
||||
for scale := MinScale; scale <= MaxScale; scale++ {
|
||||
m, err := NewMapping(scale)
|
||||
require.NoError(t, err)
|
||||
|
||||
minIndex := m.MapToIndex(MinValue)
|
||||
|
||||
boundary, err := m.LowerBoundary(minIndex)
|
||||
require.NoError(t, err)
|
||||
|
||||
correctMinIndex := int64(MinNormalExponent) >> -scale
|
||||
require.Greater(t, correctMinIndex, int64(math.MinInt32))
|
||||
require.Equal(t, int32(correctMinIndex), minIndex)
|
||||
|
||||
correctBoundary := roundedBoundary(scale, int32(correctMinIndex))
|
||||
|
||||
require.Equal(t, correctBoundary, boundary)
|
||||
require.Greater(t, roundedBoundary(scale, int32(correctMinIndex+1)), boundary)
|
||||
|
||||
// Subnormal values map to the min index:
|
||||
require.Equal(t, m.MapToIndex(MinValue/2), int32(correctMinIndex))
|
||||
require.Equal(t, m.MapToIndex(MinValue/3), int32(correctMinIndex))
|
||||
require.Equal(t, m.MapToIndex(MinValue/100), int32(correctMinIndex))
|
||||
require.Equal(t, m.MapToIndex(0x1p-1050), int32(correctMinIndex))
|
||||
require.Equal(t, m.MapToIndex(0x1p-1073), int32(correctMinIndex))
|
||||
require.Equal(t, m.MapToIndex(0x1.1p-1073), int32(correctMinIndex))
|
||||
require.Equal(t, m.MapToIndex(0x1p-1074), int32(correctMinIndex))
|
||||
|
||||
// One smaller index will underflow.
|
||||
_, err = m.LowerBoundary(minIndex - 1)
|
||||
require.Equal(t, err, mapping.ErrUnderflow)
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
// 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.
|
||||
|
||||
package exponent // import "go.opentelemetry.io/otel/sdk/metric/aggregator/exponential/mapping/exponent"
|
||||
|
||||
import "math"
|
||||
|
||||
const (
|
||||
// SignificandWidth is the size of an IEEE 754 double-precision
|
||||
// floating-point significand.
|
||||
SignificandWidth = 52
|
||||
// ExponentWidth is the size of an IEEE 754 double-precision
|
||||
// floating-point exponent.
|
||||
ExponentWidth = 11
|
||||
|
||||
// SignificandMask is the mask for the significand of an IEEE 754
|
||||
// double-precision floating-point value: 0xFFFFFFFFFFFFF.
|
||||
SignificandMask = 1<<SignificandWidth - 1
|
||||
|
||||
// ExponentBias is the exponent bias specified for encoding
|
||||
// the IEEE 754 double-precision floating point exponent: 1023
|
||||
ExponentBias = 1<<(ExponentWidth-1) - 1
|
||||
|
||||
// ExponentMask are set to 1 for the bits of an IEEE 754
|
||||
// floating point exponent: 0x7FF0000000000000
|
||||
ExponentMask = ((1 << ExponentWidth) - 1) << SignificandWidth
|
||||
|
||||
// SignMask selects the sign bit of an IEEE 754 floating point
|
||||
// number.
|
||||
SignMask = (1 << (SignificandWidth + ExponentWidth))
|
||||
|
||||
// MinNormalExponent is the minimum exponent of a normalized
|
||||
// floating point: -1022
|
||||
MinNormalExponent int32 = -ExponentBias + 1
|
||||
|
||||
// MaxNormalExponent is the maximum exponent of a normalized
|
||||
// floating point: 1023
|
||||
MaxNormalExponent int32 = ExponentBias
|
||||
|
||||
// MinValue is the smallest normal number.
|
||||
MinValue = 0x1p-1022
|
||||
|
||||
// MaxValue is the largest normal number.
|
||||
MaxValue = math.MaxFloat64
|
||||
)
|
||||
|
||||
// getBase2 extracts the normalized base-2 fractional exponent. Like
|
||||
// math.Frexp(), rounds subnormal values up to the minimum normal
|
||||
// value. Unlike Frexp(), this returns k for the equation f x 2**k
|
||||
// where f is in the range [1, 2).
|
||||
func getBase2(value float64) int32 {
|
||||
if value <= MinValue {
|
||||
return MinNormalExponent
|
||||
}
|
||||
rawBits := math.Float64bits(value)
|
||||
rawExponent := (int64(rawBits) & ExponentMask) >> SignificandWidth
|
||||
return int32(rawExponent - ExponentBias)
|
||||
}
|
171
sdk/metric/aggregator/exponential/mapping/logarithm/logarithm.go
Normal file
171
sdk/metric/aggregator/exponential/mapping/logarithm/logarithm.go
Normal file
@ -0,0 +1,171 @@
|
||||
// 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.
|
||||
|
||||
package logarithm // import "go.opentelemetry.io/otel/sdk/metric/aggregator/exponential/mapping/logarithm"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
|
||||
"go.opentelemetry.io/otel/sdk/metric/aggregator/exponential/mapping"
|
||||
"go.opentelemetry.io/otel/sdk/metric/aggregator/exponential/mapping/exponent"
|
||||
)
|
||||
|
||||
const (
|
||||
// MinScale ensures that the ../exponent mapper is used for
|
||||
// zero and negative scale values. Do not use the logarithm
|
||||
// mapper for scales <= 0.
|
||||
MinScale int32 = 1
|
||||
|
||||
// MaxScale is selected as the largest scale that is possible
|
||||
// in current code, considering there are 10 bits of base-2
|
||||
// exponent combined with scale-bits of range. At this scale,
|
||||
// the growth factor is 0.0000661%.
|
||||
//
|
||||
// Scales larger than 20 complicate the logic in cmd/prebuild,
|
||||
// because math/big overflows when exponent is math.MaxInt32
|
||||
// (== the index of math.MaxFloat64 at scale=21),
|
||||
//
|
||||
// At scale=20, index values are in the interval [-0x3fe00000,
|
||||
// 0x3fffffff], having 31 bits of information. This is
|
||||
// sensible given that the OTLP exponential histogram data
|
||||
// point uses a signed 32 bit integer for indices.
|
||||
MaxScale int32 = 20
|
||||
|
||||
// MaxValue is the largest normal number.
|
||||
MaxValue = math.MaxFloat64
|
||||
|
||||
// MinValue is the smallest normal number.
|
||||
MinValue = 0x1p-1022
|
||||
)
|
||||
|
||||
// logarithmMapping contains the constants used to implement the
|
||||
// exponential mapping function for a particular scale > 0. Note that
|
||||
// these structs are compiled in using code generated by the
|
||||
// ./cmd/prebuild package, this way no allocations are required as the
|
||||
// aggregators switch between mapping functions and the two mapping
|
||||
// functions are kept separate.
|
||||
//
|
||||
// Note that some of these fields could be calculated easily at
|
||||
// runtime, but they are compiled in to avoid those operations at
|
||||
// runtime (e.g., calls to math.Ldexp(math.Log2E, scale) for every
|
||||
// measurement).
|
||||
type logarithmMapping struct {
|
||||
// scale is between MinScale and MaxScale
|
||||
scale int32
|
||||
|
||||
// minIndex is the index of MinValue
|
||||
minIndex int32
|
||||
// maxIndex is the index of MaxValue
|
||||
maxIndex int32
|
||||
|
||||
// scaleFactor is used and computed as follows:
|
||||
// index = log(value) / log(base)
|
||||
// = log(value) / log(2^(2^-scale))
|
||||
// = log(value) / (2^-scale * log(2))
|
||||
// = log(value) * (1/log(2) * 2^scale)
|
||||
// = log(value) * scaleFactor
|
||||
// where:
|
||||
// scaleFactor = (1/log(2) * 2^scale)
|
||||
// = math.Log2E * math.Exp2(scale)
|
||||
// = math.Ldexp(math.Log2E, scale)
|
||||
// Because multiplication is faster than division, we define scaleFactor as a multiplier.
|
||||
// This implementation was copied from a Java prototype. See:
|
||||
// https://github.com/newrelic-experimental/newrelic-sketch-java/blob/1ce245713603d61ba3a4510f6df930a5479cd3f6/src/main/java/com/newrelic/nrsketch/indexer/LogIndexer.java
|
||||
// for the equations used here.
|
||||
scaleFactor float64
|
||||
|
||||
// log(boundary) = index * log(base)
|
||||
// log(boundary) = index * log(2^(2^-scale))
|
||||
// log(boundary) = index * 2^-scale * log(2)
|
||||
// boundary = exp(index * inverseFactor)
|
||||
// where:
|
||||
// inverseFactor = 2^-scale * log(2)
|
||||
// = math.Ldexp(math.Ln2, -scale)
|
||||
inverseFactor float64
|
||||
}
|
||||
|
||||
var (
|
||||
_ mapping.Mapping = &logarithmMapping{}
|
||||
|
||||
prebuiltMappingsLock sync.Mutex
|
||||
prebuiltMappings = map[int32]*logarithmMapping{}
|
||||
)
|
||||
|
||||
// NewMapping constructs a logarithm mapping function, used for scales > 0.
|
||||
func NewMapping(scale int32) (mapping.Mapping, error) {
|
||||
// An assumption used in this code is that scale is > 0. If
|
||||
// scale is <= 0 it's better to use the exponent mapping.
|
||||
if scale < MinScale || scale > MaxScale {
|
||||
// scale 20 can represent the entire float64 range
|
||||
// with a 30 bit index, and we don't handle larger
|
||||
// scales to simplify range tests in this package.
|
||||
return nil, fmt.Errorf("scale out of bounds")
|
||||
}
|
||||
prebuiltMappingsLock.Lock()
|
||||
defer prebuiltMappingsLock.Unlock()
|
||||
|
||||
if p := prebuiltMappings[scale]; p != nil {
|
||||
return p, nil
|
||||
}
|
||||
l := &logarithmMapping{
|
||||
scale: scale,
|
||||
maxIndex: int32((int64(exponent.MaxNormalExponent+1) << scale) - 1),
|
||||
minIndex: int32(int64(exponent.MinNormalExponent) << scale),
|
||||
scaleFactor: math.Ldexp(math.Log2E, int(scale)),
|
||||
inverseFactor: math.Ldexp(math.Ln2, int(-scale)),
|
||||
}
|
||||
prebuiltMappings[scale] = l
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// MapToIndex implements mapping.Mapping.
|
||||
func (l *logarithmMapping) MapToIndex(value float64) int32 {
|
||||
// Note: we can assume not a 0, Inf, or NaN; positive sign bit.
|
||||
if value <= MinValue {
|
||||
return l.minIndex
|
||||
}
|
||||
// Use Floor() to round toward 0.
|
||||
index := int32(math.Floor(math.Log(value) * l.scaleFactor))
|
||||
|
||||
if index > l.maxIndex {
|
||||
return l.maxIndex
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
// LowerBoundary implements mapping.Mapping.
|
||||
func (l *logarithmMapping) LowerBoundary(index int32) (float64, error) {
|
||||
if index >= l.maxIndex {
|
||||
if index == l.maxIndex {
|
||||
// Note that the equation on the last line of this
|
||||
// function returns +Inf. Use the alternate equation.
|
||||
return 2 * math.Exp(float64(index-(int32(1)<<l.scale))*l.inverseFactor), nil
|
||||
}
|
||||
return 0, mapping.ErrOverflow
|
||||
}
|
||||
if index <= l.minIndex {
|
||||
if index == l.minIndex {
|
||||
return MinValue, nil
|
||||
}
|
||||
return 0, mapping.ErrUnderflow
|
||||
}
|
||||
return math.Exp(float64(index) * l.inverseFactor), nil
|
||||
}
|
||||
|
||||
// Scale implements mapping.Mapping.
|
||||
func (l *logarithmMapping) Scale() int32 {
|
||||
return l.scale
|
||||
}
|
@ -0,0 +1,250 @@
|
||||
// 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.
|
||||
|
||||
package logarithm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.opentelemetry.io/otel/sdk/metric/aggregator/exponential/mapping"
|
||||
"go.opentelemetry.io/otel/sdk/metric/aggregator/exponential/mapping/exponent"
|
||||
)
|
||||
|
||||
type expectMapping struct {
|
||||
value float64
|
||||
index int32
|
||||
}
|
||||
|
||||
// Tests an invalid scale.
|
||||
func TestInvalidScale(t *testing.T) {
|
||||
_, err := NewMapping(-1)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
// Tests a few values are mapped correctly at scale 1, where the
|
||||
// exponentiation factor is SquareRoot(2).
|
||||
func TestLogarithmMapping(t *testing.T) {
|
||||
// Scale 1 means 1 division between every power of two, having
|
||||
// a factor sqrt(2) times the lower boundary.
|
||||
m, err := NewMapping(+1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int32(+1), m.Scale())
|
||||
|
||||
// Note: Do not test exact boundaries, with the exception of
|
||||
// 1, because we expect errors in that case (e.g.,
|
||||
// MapToIndex(8) returns 5, an off-by-one. See the following
|
||||
// test.
|
||||
for _, pair := range []expectMapping{
|
||||
{15, 7},
|
||||
{9, 6},
|
||||
{7, 5},
|
||||
{5, 4},
|
||||
{3, 3},
|
||||
{2.5, 2},
|
||||
{1.5, 1},
|
||||
{1.2, 0},
|
||||
{1, 0},
|
||||
{0.75, -1},
|
||||
{0.55, -2},
|
||||
{0.45, -3},
|
||||
} {
|
||||
idx := m.MapToIndex(pair.value)
|
||||
require.Equal(t, pair.index, idx, "value: %v", pair.value)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the mapping function for correctness-within-epsilon for a few
|
||||
// scales and index values.
|
||||
func TestLogarithmBoundary(t *testing.T) {
|
||||
for _, scale := range []int32{1, 2, 3, 4, 10, 15} {
|
||||
t.Run(fmt.Sprint(scale), func(t *testing.T) {
|
||||
m, _ := NewMapping(scale)
|
||||
for _, index := range []int32{-100, -10, -1, 0, 1, 10, 100} {
|
||||
t.Run(fmt.Sprint(index), func(t *testing.T) {
|
||||
lowBoundary, err := m.LowerBoundary(index)
|
||||
require.NoError(t, err)
|
||||
mapped := m.MapToIndex(lowBoundary)
|
||||
|
||||
// At or near the boundary expected to be off-by-one sometimes.
|
||||
require.LessOrEqual(t, index-1, mapped)
|
||||
require.GreaterOrEqual(t, index, mapped)
|
||||
|
||||
// The values should be very close.
|
||||
require.InEpsilon(t, lowBoundary, roundedBoundary(scale, index), 1e-9)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// roundedBoundary computes the correct boundary rounded to a float64
|
||||
// using math/big. Note that this function uses a SquareRoot() where the
|
||||
// one in ../exponent uses a Square().
|
||||
func roundedBoundary(scale, index int32) float64 {
|
||||
one := big.NewFloat(1)
|
||||
f := (&big.Float{}).SetMantExp(one, int(index))
|
||||
for i := scale; i > 0; i-- {
|
||||
f = (&big.Float{}).Sqrt(f)
|
||||
}
|
||||
|
||||
result, _ := f.Float64()
|
||||
return result
|
||||
}
|
||||
|
||||
// TestLogarithmIndexMax ensures that for every valid scale, MaxFloat
|
||||
// maps into the correct maximum index. Also tests that the reverse
|
||||
// lookup does not produce infinity and the following index produces
|
||||
// an overflow error.
|
||||
func TestLogarithmIndexMax(t *testing.T) {
|
||||
for scale := MinScale; scale <= MaxScale; scale++ {
|
||||
m, err := NewMapping(scale)
|
||||
require.NoError(t, err)
|
||||
|
||||
index := m.MapToIndex(MaxValue)
|
||||
|
||||
// Correct max index is one less than the first index
|
||||
// that overflows math.MaxFloat64, i.e., one less than
|
||||
// the index of +Inf.
|
||||
maxIndex64 := (int64(exponent.MaxNormalExponent+1) << scale) - 1
|
||||
require.Less(t, maxIndex64, int64(math.MaxInt32))
|
||||
require.Equal(t, index, int32(maxIndex64))
|
||||
|
||||
// The index maps to a finite boundary near MaxFloat.
|
||||
bound, err := m.LowerBoundary(index)
|
||||
require.NoError(t, err)
|
||||
|
||||
base, _ := m.LowerBoundary(1)
|
||||
|
||||
require.Less(t, bound, MaxValue)
|
||||
|
||||
// The expected ratio equals the base factor.
|
||||
require.InEpsilon(t, (MaxValue-bound)/bound, base-1, 1e-6)
|
||||
|
||||
// One larger index will overflow.
|
||||
_, err = m.LowerBoundary(index + 1)
|
||||
require.Equal(t, err, mapping.ErrOverflow)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLogarithmIndexMin ensures that for every valid scale, Non-zero numbers
|
||||
func TestLogarithmIndexMin(t *testing.T) {
|
||||
for scale := MinScale; scale <= MaxScale; scale++ {
|
||||
m, err := NewMapping(scale)
|
||||
require.NoError(t, err)
|
||||
|
||||
minIndex := m.MapToIndex(MinValue)
|
||||
|
||||
mapped, err := m.LowerBoundary(minIndex)
|
||||
require.NoError(t, err)
|
||||
|
||||
correctMinIndex := int64(exponent.MinNormalExponent) << scale
|
||||
require.Greater(t, correctMinIndex, int64(math.MinInt32))
|
||||
|
||||
correctMapped := roundedBoundary(scale, int32(correctMinIndex))
|
||||
require.Equal(t, correctMapped, MinValue)
|
||||
require.InEpsilon(t, mapped, MinValue, 1e-6)
|
||||
|
||||
require.Equal(t, minIndex, int32(correctMinIndex))
|
||||
|
||||
// Subnormal values map to the min index:
|
||||
require.Equal(t, m.MapToIndex(MinValue/2), int32(correctMinIndex))
|
||||
require.Equal(t, m.MapToIndex(MinValue/3), int32(correctMinIndex))
|
||||
require.Equal(t, m.MapToIndex(MinValue/100), int32(correctMinIndex))
|
||||
require.Equal(t, m.MapToIndex(0x1p-1050), int32(correctMinIndex))
|
||||
require.Equal(t, m.MapToIndex(0x1p-1073), int32(correctMinIndex))
|
||||
require.Equal(t, m.MapToIndex(0x1.1p-1073), int32(correctMinIndex))
|
||||
require.Equal(t, m.MapToIndex(0x1p-1074), int32(correctMinIndex))
|
||||
|
||||
// One smaller index will underflow.
|
||||
_, err = m.LowerBoundary(minIndex - 1)
|
||||
require.Equal(t, err, mapping.ErrUnderflow)
|
||||
}
|
||||
}
|
||||
|
||||
// TestExponentIndexMax ensures that for every valid scale, MaxFloat
|
||||
// maps into the correct maximum index. Also tests that the reverse
|
||||
// lookup does not produce infinity and the following index produces
|
||||
// an overflow error.
|
||||
func TestExponentIndexMax(t *testing.T) {
|
||||
for scale := MinScale; scale <= MaxScale; scale++ {
|
||||
m, err := NewMapping(scale)
|
||||
require.NoError(t, err)
|
||||
|
||||
index := m.MapToIndex(MaxValue)
|
||||
|
||||
// Correct max index is one less than the first index
|
||||
// that overflows math.MaxFloat64, i.e., one less than
|
||||
// the index of +Inf.
|
||||
maxIndex64 := (int64(exponent.MaxNormalExponent+1) << scale) - 1
|
||||
require.Less(t, maxIndex64, int64(math.MaxInt32))
|
||||
require.Equal(t, index, int32(maxIndex64))
|
||||
|
||||
// The index maps to a finite boundary near MaxFloat.
|
||||
bound, err := m.LowerBoundary(index)
|
||||
require.NoError(t, err)
|
||||
|
||||
base, _ := m.LowerBoundary(1)
|
||||
|
||||
require.Less(t, bound, MaxValue)
|
||||
|
||||
// The expected ratio equals the base factor.
|
||||
require.InEpsilon(t, (MaxValue-bound)/bound, base-1, 1e-6)
|
||||
|
||||
// One larger index will overflow.
|
||||
_, err = m.LowerBoundary(index + 1)
|
||||
require.Equal(t, err, mapping.ErrOverflow)
|
||||
}
|
||||
}
|
||||
|
||||
// TestExponentIndexMin ensures that for every valid scale, the
|
||||
// smallest normal number and all smaller numbers map to the correct
|
||||
// index, which is that of the smallest normal number.
|
||||
func TestExponentIndexMin(t *testing.T) {
|
||||
for scale := MinScale; scale <= MaxScale; scale++ {
|
||||
m, err := NewMapping(scale)
|
||||
require.NoError(t, err)
|
||||
|
||||
minIndex := m.MapToIndex(MinValue)
|
||||
|
||||
mapped, err := m.LowerBoundary(minIndex)
|
||||
require.NoError(t, err)
|
||||
|
||||
correctMinIndex := int64(exponent.MinNormalExponent) << scale
|
||||
require.Greater(t, correctMinIndex, int64(math.MinInt32))
|
||||
|
||||
correctMapped := roundedBoundary(scale, int32(correctMinIndex))
|
||||
require.Equal(t, correctMapped, MinValue)
|
||||
require.InEpsilon(t, mapped, MinValue, 1e-6)
|
||||
|
||||
require.Equal(t, minIndex, int32(correctMinIndex))
|
||||
|
||||
// Subnormal values map to the min index:
|
||||
require.Equal(t, m.MapToIndex(MinValue/2), int32(correctMinIndex))
|
||||
require.Equal(t, m.MapToIndex(MinValue/3), int32(correctMinIndex))
|
||||
require.Equal(t, m.MapToIndex(MinValue/100), int32(correctMinIndex))
|
||||
require.Equal(t, m.MapToIndex(0x1p-1050), int32(correctMinIndex))
|
||||
require.Equal(t, m.MapToIndex(0x1p-1073), int32(correctMinIndex))
|
||||
require.Equal(t, m.MapToIndex(0x1.1p-1073), int32(correctMinIndex))
|
||||
require.Equal(t, m.MapToIndex(0x1p-1074), int32(correctMinIndex))
|
||||
|
||||
// One smaller index will underflow.
|
||||
_, err = m.LowerBoundary(minIndex - 1)
|
||||
require.Equal(t, err, mapping.ErrUnderflow)
|
||||
}
|
||||
}
|
48
sdk/metric/aggregator/exponential/mapping/mapping.go
Normal file
48
sdk/metric/aggregator/exponential/mapping/mapping.go
Normal file
@ -0,0 +1,48 @@
|
||||
// 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.
|
||||
|
||||
package mapping // import "go.opentelemetry.io/otel/sdk/metric/aggregator/exponential/mapping"
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Mapping is the interface of an exponential histogram mapper.
|
||||
type Mapping interface {
|
||||
// MapToIndex maps positive floating point values to indexes
|
||||
// corresponding to Scale(). Implementations are not expected
|
||||
// to handle zeros, +Inf, NaN, or negative values.
|
||||
MapToIndex(value float64) int32
|
||||
|
||||
// LowerBoundary returns the lower boundary of a given bucket
|
||||
// index. The index is expected to map onto a range that is
|
||||
// at least partially inside the range of normalized floating
|
||||
// point values. If the corresponding bucket's upper boundary
|
||||
// is less than or equal to 0x1p-1022, ErrUnderflow will be
|
||||
// returned. If the corresponding bucket's lower boundary is
|
||||
// greater than math.MaxFloat64, ErrOverflow will be returned.
|
||||
LowerBoundary(index int32) (float64, error)
|
||||
|
||||
// Scale returns the parameter that controls the resolution of
|
||||
// this mapping. For details see:
|
||||
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/datamodel.md#exponential-scale
|
||||
Scale() int32
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrUnderflow is returned when computing the lower boundary
|
||||
// of an index that maps into a denormalized floating point value.
|
||||
ErrUnderflow = fmt.Errorf("underflow")
|
||||
// ErrOverflow is returned when computing the lower boundary
|
||||
// of an index that maps into +Inf.
|
||||
ErrOverflow = fmt.Errorf("overflow")
|
||||
)
|
Reference in New Issue
Block a user