1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2025-01-03 22:52:30 +02:00

Change the inclusivity of exponential histogram bounds (#2982)

* Use lower-inclusive boundaries

* make exponent and logarithm more symmetric

Co-authored-by: Anthony Mirabella <a9@aneurysm9.com>
Co-authored-by: Aaron Clawson <3766680+MadVikingGod@users.noreply.github.com>
This commit is contained in:
Joshua MacDonald 2022-08-24 11:09:37 -07:00 committed by GitHub
parent 8c3a85a5be
commit 09bf345912
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 341 additions and 180 deletions

View File

@ -13,9 +13,12 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Support Go 1.19.
Include compatibility testing and document support. (#3077)
### Fixed
### Changed
- Fix misidentification of OpenTelemetry `SpanKind` in OpenTracing bridge (`go.opentelemetry.io/otel/bridge/opentracing`). (#3096)
- The exponential histogram mapping functions have been updated with
exact upper-inclusive boundary support following the [corresponding
specification change](https://github.com/open-telemetry/opentelemetry-specification/pull/2633). (#2982)
## [1.9.0/0.0.3] - 2022-08-01

View File

@ -0,0 +1,67 @@
// 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 exponential
import (
"fmt"
"math/rand"
"testing"
"go.opentelemetry.io/otel/sdk/metric/aggregator/exponential/mapping"
"go.opentelemetry.io/otel/sdk/metric/aggregator/exponential/mapping/exponent"
"go.opentelemetry.io/otel/sdk/metric/aggregator/exponential/mapping/logarithm"
)
func benchmarkMapping(b *testing.B, name string, mapper mapping.Mapping) {
b.Run(fmt.Sprintf("mapping_%s", name), func(b *testing.B) {
src := rand.New(rand.NewSource(54979))
for i := 0; i < b.N; i++ {
_ = mapper.MapToIndex(1 + src.Float64())
}
})
}
func benchmarkBoundary(b *testing.B, name string, mapper mapping.Mapping) {
b.Run(fmt.Sprintf("boundary_%s", name), func(b *testing.B) {
src := rand.New(rand.NewSource(54979))
for i := 0; i < b.N; i++ {
_, _ = mapper.LowerBoundary(int32(src.Int63()))
}
})
}
// An earlier draft of this benchmark included a lookup-table based
// implementation:
// https://github.com/open-telemetry/opentelemetry-go-contrib/pull/1353
// That mapping function uses O(2^scale) extra space and falls
// somewhere between the exponent and logarithm methods compared here.
// In the test, lookuptable was 40% faster than logarithm, which did
// not justify the significant extra complexity.
// Benchmarks the MapToIndex function.
func BenchmarkMapping(b *testing.B) {
em, _ := exponent.NewMapping(-1)
lm, _ := logarithm.NewMapping(1)
benchmarkMapping(b, "exponent", em)
benchmarkMapping(b, "logarithm", lm)
}
// Benchmarks the LowerBoundary function.
func BenchmarkReverseMapping(b *testing.B) {
em, _ := exponent.NewMapping(-1)
lm, _ := logarithm.NewMapping(1)
benchmarkBoundary(b, "exponent", em)
benchmarkBoundary(b, "logarithm", lm)
}

View File

@ -19,6 +19,7 @@ import (
"math"
"go.opentelemetry.io/otel/sdk/metric/aggregator/exponential/mapping"
"go.opentelemetry.io/otel/sdk/metric/aggregator/exponential/mapping/internal"
)
const (
@ -63,52 +64,61 @@ func NewMapping(scale int32) (mapping.Mapping, error) {
return &prebuiltMappings[scale-MinScale], nil
}
// minNormalLowerBoundaryIndex is the largest index such that
// base**index is <= MinValue. A histogram bucket with this index
// covers the range (base**index, base**(index+1)], including
// MinValue.
func (e *exponentMapping) minNormalLowerBoundaryIndex() int32 {
idx := int32(internal.MinNormalExponent) >> e.shift
if e.shift < 2 {
// For scales -1 and 0 the minimum value 2**-1022
// is a power-of-two multiple, meaning it belongs
// to the index one less.
idx--
}
return idx
}
// maxNormalLowerBoundaryIndex is the index such that base**index
// equals the largest representable boundary. A histogram bucket with this
// index covers the range (0x1p+1024/base, 0x1p+1024], which includes
// MaxValue; note that this bucket is incomplete, since the upper
// boundary cannot be represented. One greater than this index
// corresponds with the bucket containing values > 0x1p1024.
func (e *exponentMapping) maxNormalLowerBoundaryIndex() int32 {
return int32(internal.MaxNormalExponent) >> e.shift
}
// MapToIndex implements mapping.Mapping.
func (e *exponentMapping) MapToIndex(value float64) int32 {
// Note: we can assume not a 0, Inf, or NaN; positive sign bit.
if value < internal.MinValue {
return e.minNormalLowerBoundaryIndex()
}
// Extract the raw exponent.
rawExp := internal.GetNormalBase2(value)
// In case the value is an exact power of two, compute a
// correction of -1:
correction := int32((internal.GetSignificand(value) - 1) >> internal.SignificandWidth)
// 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
return (rawExp + correction) >> e.shift
}
// LowerBoundary implements mapping.Mapping.
func (e *exponentMapping) LowerBoundary(index int32) (float64, error) {
if min := e.minIndex(); index < min {
if min := e.minNormalLowerBoundaryIndex(); index < min {
return 0, mapping.ErrUnderflow
}
if max := e.maxIndex(); index > max {
if max := e.maxNormalLowerBoundaryIndex(); 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
return math.Ldexp(1, int(index<<e.shift)), nil
}
// Scale implements mapping.Mapping.

View File

@ -23,6 +23,14 @@ import (
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/sdk/metric/aggregator/exponential/mapping"
"go.opentelemetry.io/otel/sdk/metric/aggregator/exponential/mapping/internal"
)
const (
MaxNormalExponent = internal.MaxNormalExponent
MinNormalExponent = internal.MinNormalExponent
MaxValue = internal.MaxValue
MinValue = internal.MinValue
)
type expectMapping struct {
@ -30,27 +38,6 @@ type expectMapping struct {
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)
@ -59,22 +46,41 @@ func TestExponentMappingZero(t *testing.T) {
require.Equal(t, int32(0), m.Scale())
for _, pair := range []expectMapping{
// Near +Inf
{math.MaxFloat64, MaxNormalExponent},
{0x1p+1023, MaxNormalExponent},
{0x1p-1022, MinNormalExponent},
{math.SmallestNonzeroFloat64, MinNormalExponent},
{4, 2},
{math.MaxFloat64, 1023},
{0x1p+1023, 1022},
{0x1.1p+1023, 1023},
{0x1p+1022, 1021},
{0x1.1p+1022, 1022},
// Near 0
{0x1p-1022, -1023},
{0x1.1p-1022, -1022},
{0x1p-1021, -1022},
{0x1.1p-1021, -1021},
{0x1p-1022, MinNormalExponent - 1},
{0x1p-1021, MinNormalExponent},
{math.SmallestNonzeroFloat64, MinNormalExponent - 1},
// Near 1
{4, 1},
{3, 1},
{2, 1},
{2, 0},
{1.5, 0},
{1, 0},
{1, -1},
{0.75, -1},
{0.5, -1},
{0.25, -2},
{0.51, -1},
{0.5, -2},
{0.26, -2},
{0.25, -3},
{0.126, -3},
{0.125, -4},
} {
idx := m.MapToIndex(pair.value)
require.Equal(t, pair.index, idx)
require.Equal(t, pair.index, idx, "value:%x", pair.value)
}
}
@ -86,7 +92,8 @@ func TestExponentMappingMinScale(t *testing.T) {
require.Equal(t, MinScale, m.Scale())
for _, pair := range []expectMapping{
{1, 0},
{1.000001, 0},
{1, -1},
{math.MaxFloat64 / 2, 0},
{math.MaxFloat64, 0},
{math.SmallestNonzeroFloat64, -1},
@ -116,24 +123,25 @@ func TestExponentMappingNegOne(t *testing.T) {
m, _ := NewMapping(-1)
for _, pair := range []expectMapping{
{16, 2},
{17, 2},
{16, 1},
{15, 1},
{9, 1},
{8, 1},
{5, 1},
{4, 1},
{4, 0},
{3, 0},
{2, 0},
{1.5, 0},
{1, 0},
{1, -1},
{0.75, -1},
{0.5, -1},
{0.25, -1},
{0.25, -2},
{0.20, -2},
{0.13, -2},
{0.125, -2},
{0.10, -2},
{0.0625, -2},
{0.0625, -3},
{0.06, -3},
} {
idx := m.MapToIndex(pair.value)
@ -148,55 +156,58 @@ func TestExponentMappingNegFour(t *testing.T) {
require.Equal(t, int32(-4), m.Scale())
for _, pair := range []expectMapping{
{float64(0x1), 0},
{float64(0x1), -1},
{float64(0x10), 0},
{float64(0x100), 0},
{float64(0x1000), 0},
{float64(0x10000), 1}, // Base == 2**16
{float64(0x10000), 0}, // Base == 2**16
{float64(0x100000), 1},
{float64(0x1000000), 1},
{float64(0x10000000), 1},
{float64(0x100000000), 2}, // == 2**32
{float64(0x100000000), 1}, // == 2**32
{float64(0x1000000000), 2},
{float64(0x10000000000), 2},
{float64(0x100000000000), 2},
{float64(0x1000000000000), 3}, // 2**48
{float64(0x1000000000000), 2}, // 2**48
{float64(0x10000000000000), 3},
{float64(0x100000000000000), 3},
{float64(0x1000000000000000), 3},
{float64(0x10000000000000000), 4}, // 2**64
{float64(0x10000000000000000), 3}, // 2**64
{float64(0x100000000000000000), 4},
{float64(0x1000000000000000000), 4},
{float64(0x10000000000000000000), 4},
{float64(0x100000000000000000000), 5},
{float64(0x100000000000000000000), 4}, // 2**80
{float64(0x1000000000000000000000), 5},
{1 / float64(0x1), 0},
{1 / float64(0x1), -1},
{1 / float64(0x10), -1},
{1 / float64(0x100), -1},
{1 / float64(0x1000), -1},
{1 / float64(0x10000), -1}, // 2**-16
{1 / float64(0x10000), -2}, // 2**-16
{1 / float64(0x100000), -2},
{1 / float64(0x1000000), -2},
{1 / float64(0x10000000), -2},
{1 / float64(0x100000000), -2}, // 2**-32
{1 / float64(0x100000000), -3}, // 2**-32
{1 / float64(0x1000000000), -3},
{1 / float64(0x10000000000), -3},
{1 / float64(0x100000000000), -3},
{1 / float64(0x1000000000000), -3}, // 2**-48
{1 / float64(0x1000000000000), -4}, // 2**-48
{1 / float64(0x10000000000000), -4},
{1 / float64(0x100000000000000), -4},
{1 / float64(0x1000000000000000), -4},
{1 / float64(0x10000000000000000), -4}, // 2**-64
{1 / float64(0x10000000000000000), -5}, // 2**-64
{1 / float64(0x100000000000000000), -5},
// Max values
{0x1.FFFFFFFFFFFFFp1023, 63},
{0x1p1023, 63},
{0x1p1019, 63},
{0x1p1008, 63},
{0x1p1009, 63},
{0x1p1008, 62},
{0x1p1007, 62},
{0x1p1000, 62},
{0x1p0992, 62},
{0x1p0993, 62},
{0x1p0992, 61},
{0x1p0991, 61},
// Min and subnormal values
@ -212,11 +223,14 @@ func TestExponentMappingNegFour(t *testing.T) {
{0x1p-1023, -64},
{0x1p-1022, -64},
{0x1p-1009, -64},
{0x1p-1008, -63},
{0x1p-1008, -64},
{0x1p-1007, -63},
{0x1p-0993, -63},
{0x1p-0992, -62},
{0x1p-0992, -63},
{0x1p-0991, -62},
{0x1p-0977, -62},
{0x1p-0976, -61},
{0x1p-0976, -62},
{0x1p-0975, -61},
} {
t.Run(fmt.Sprintf("%x", pair.value), func(t *testing.T) {
index := m.MapToIndex(pair.value)
@ -280,12 +294,20 @@ func TestExponentIndexMin(t *testing.T) {
m, err := NewMapping(scale)
require.NoError(t, err)
// Test the smallest normal value.
minIndex := m.MapToIndex(MinValue)
boundary, err := m.LowerBoundary(minIndex)
require.NoError(t, err)
// The correct index for MinValue depends on whether
// 2**(-scale) evenly divides -1022. This is true for
// scales -1 and 0.
correctMinIndex := int64(MinNormalExponent) >> -scale
if MinNormalExponent%(int32(1)<<-scale) == 0 {
correctMinIndex--
}
require.Greater(t, correctMinIndex, int64(math.MinInt32))
require.Equal(t, int32(correctMinIndex), minIndex)
@ -295,16 +317,25 @@ func TestExponentIndexMin(t *testing.T) {
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))
require.Equal(t, int32(correctMinIndex), m.MapToIndex(MinValue/2))
require.Equal(t, int32(correctMinIndex), m.MapToIndex(MinValue/3))
require.Equal(t, int32(correctMinIndex), m.MapToIndex(MinValue/100))
require.Equal(t, int32(correctMinIndex), m.MapToIndex(0x1p-1050))
require.Equal(t, int32(correctMinIndex), m.MapToIndex(0x1p-1073))
require.Equal(t, int32(correctMinIndex), m.MapToIndex(0x1.1p-1073))
require.Equal(t, int32(correctMinIndex), m.MapToIndex(0x1p-1074))
// One smaller index will underflow.
_, err = m.LowerBoundary(minIndex - 1)
require.Equal(t, err, mapping.ErrUnderflow)
// Next value above MinValue (not a power of two).
minPlus1Index := m.MapToIndex(math.Nextafter(MinValue, math.Inf(+1)))
// The following boundary equation always works for
// non-powers of two (same as correctMinIndex before its
// power-of-two correction, above).
correctMinPlus1Index := int64(MinNormalExponent) >> -scale
require.Equal(t, int32(correctMinPlus1Index), minPlus1Index)
}
}

View File

@ -12,7 +12,7 @@
// 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"
package internal // import "go.opentelemetry.io/otel/sdk/metric/aggregator/exponential/mapping/internal"
import "math"
@ -55,15 +55,18 @@ const (
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
}
// GetNormalBase2 extracts the normalized base-2 fractional exponent.
// Unlike Frexp(), this returns k for the equation f x 2**k where f is
// in the range [1, 2). Note that this function is not called for
// subnormal numbers.
func GetNormalBase2(value float64) int32 {
rawBits := math.Float64bits(value)
rawExponent := (int64(rawBits) & ExponentMask) >> SignificandWidth
return int32(rawExponent - ExponentBias)
}
// GetSignificand returns the 52 bit (unsigned) significand as a
// signed value.
func GetSignificand(value float64) int64 {
return int64(math.Float64bits(value)) & SignificandMask
}

View File

@ -0,0 +1,47 @@
// 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 internal
import (
"testing"
"github.com/stretchr/testify/require"
)
// Tests that GetNormalBase2 returns the base-2 exponent as documented, unlike
// math.Frexp.
func TestGetNormalBase2(t *testing.T) {
require.Equal(t, int32(-1022), MinNormalExponent)
require.Equal(t, int32(+1023), MaxNormalExponent)
require.Equal(t, MaxNormalExponent, GetNormalBase2(0x1p+1023))
require.Equal(t, int32(1022), GetNormalBase2(0x1p+1022))
require.Equal(t, int32(0), GetNormalBase2(1))
require.Equal(t, int32(-1021), GetNormalBase2(0x1p-1021))
require.Equal(t, int32(-1022), GetNormalBase2(0x1p-1022))
// Subnormals below this point
require.Equal(t, int32(-1023), GetNormalBase2(0x1p-1023))
require.Equal(t, int32(-1023), GetNormalBase2(0x1p-1024))
require.Equal(t, int32(-1023), GetNormalBase2(0x1p-1025))
require.Equal(t, int32(-1023), GetNormalBase2(0x1p-1074))
}
func TestGetSignificand(t *testing.T) {
// The number 1.5 has a single most-significant bit set, i.e., 1<<51.
require.Equal(t, int64(1)<<(SignificandWidth-1), GetSignificand(1.5))
}

View File

@ -20,7 +20,7 @@ import (
"sync"
"go.opentelemetry.io/otel/sdk/metric/aggregator/exponential/mapping"
"go.opentelemetry.io/otel/sdk/metric/aggregator/exponential/mapping/exponent"
"go.opentelemetry.io/otel/sdk/metric/aggregator/exponential/mapping/internal"
)
const (
@ -44,33 +44,20 @@ const (
// 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
MinValue = internal.MinValue
// MaxValue is the largest normal number.
MaxValue = internal.MaxValue
)
// 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).
// exponential mapping function for a particular scale > 0.
type logarithmMapping struct {
// scale is between MinScale and MaxScale
// scale is between MinScale and MaxScale. The exponential
// base is defined as 2**(2**(-scale)).
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))
@ -122,8 +109,6 @@ func NewMapping(scale int32) (mapping.Mapping, error) {
}
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)),
}
@ -131,34 +116,68 @@ func NewMapping(scale int32) (mapping.Mapping, error) {
return l, nil
}
// minNormalLowerBoundaryIndex is the index such that base**index equals
// MinValue. A histogram bucket with this index covers the range
// (MinValue, MinValue*base]. One less than this index corresponds
// with the bucket containing values <= MinValue.
func (l *logarithmMapping) minNormalLowerBoundaryIndex() int32 {
return int32(internal.MinNormalExponent << l.scale)
}
// maxNormalLowerBoundaryIndex is the index such that base**index equals the
// greatest representable lower boundary. A histogram bucket with this
// index covers the range (0x1p+1024/base, 0x1p+1024], which includes
// MaxValue; note that this bucket is incomplete, since the upper
// boundary cannot be represented. One greater than this index
// corresponds with the bucket containing values > 0x1p1024.
func (l *logarithmMapping) maxNormalLowerBoundaryIndex() int32 {
return (int32(internal.MaxNormalExponent+1) << l.scale) - 1
}
// 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
return l.minNormalLowerBoundaryIndex() - 1
}
// Use Floor() to round toward 0.
// Exact power-of-two correctness: an optional special case.
if internal.GetSignificand(value) == 0 {
exp := internal.GetNormalBase2(value)
return (exp << l.scale) - 1
}
// Non-power of two cases. Use Floor(x) to round the scaled
// logarithm. We could use Ceil(x)-1 to achieve the same
// result, though Ceil() is typically defined as -Floor(-x)
// and typically not performed in hardware, so this is likely
// less code.
index := int32(math.Floor(math.Log(value) * l.scaleFactor))
if index > l.maxIndex {
return l.maxIndex
if max := l.maxNormalLowerBoundaryIndex(); index >= max {
return max
}
return index
}
// LowerBoundary implements mapping.Mapping.
func (l *logarithmMapping) LowerBoundary(index int32) (float64, error) {
if index >= l.maxIndex {
if index == l.maxIndex {
if max := l.maxNormalLowerBoundaryIndex(); index >= max {
if index == max {
// 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 {
if min := l.minNormalLowerBoundaryIndex(); index <= min {
if index == min {
return MinValue, nil
} else if index == min-1 {
// Similar to the logic above, the math.Exp()
// formulation is not accurate for subnormal
// values.
return math.Exp(float64(index+(int32(1)<<l.scale))*l.inverseFactor) / 2, nil
}
return 0, mapping.ErrUnderflow
}

View File

@ -23,7 +23,12 @@ import (
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/sdk/metric/aggregator/exponential/mapping"
"go.opentelemetry.io/otel/sdk/metric/aggregator/exponential/mapping/exponent"
"go.opentelemetry.io/otel/sdk/metric/aggregator/exponential/mapping/internal"
)
const (
MaxNormalExponent = internal.MaxNormalExponent
MinNormalExponent = internal.MinNormalExponent
)
type expectMapping struct {
@ -39,7 +44,7 @@ func TestInvalidScale(t *testing.T) {
// Tests a few values are mapped correctly at scale 1, where the
// exponentiation factor is SquareRoot(2).
func TestLogarithmMapping(t *testing.T) {
func TestLogarithmMappingScaleOne(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)
@ -59,7 +64,7 @@ func TestLogarithmMapping(t *testing.T) {
{2.5, 2},
{1.5, 1},
{1.2, 0},
{1, 0},
{1, -1}, // Exact test!
{0.75, -1},
{0.55, -2},
{0.45, -3},
@ -121,7 +126,7 @@ func TestLogarithmIndexMax(t *testing.T) {
// 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
maxIndex64 := (int64(MaxNormalExponent+1) << scale) - 1
require.Less(t, maxIndex64, int64(math.MaxInt32))
require.Equal(t, index, int32(maxIndex64))
@ -139,10 +144,16 @@ func TestLogarithmIndexMax(t *testing.T) {
// One larger index will overflow.
_, err = m.LowerBoundary(index + 1)
require.Equal(t, err, mapping.ErrOverflow)
// Two larger will overflow.
_, err = m.LowerBoundary(index + 2)
require.Equal(t, err, mapping.ErrOverflow)
}
}
// TestLogarithmIndexMin ensures that for every valid scale, Non-zero numbers.
// TestLogarithmIndexMin ensures that for every valid scale, the
// smallest normal number and all smaller numbers map to the correct
// index.
func TestLogarithmIndexMin(t *testing.T) {
for scale := MinScale; scale <= MaxScale; scale++ {
m, err := NewMapping(scale)
@ -150,17 +161,19 @@ func TestLogarithmIndexMin(t *testing.T) {
minIndex := m.MapToIndex(MinValue)
mapped, err := m.LowerBoundary(minIndex)
require.NoError(t, err)
correctMinIndex := int64(exponent.MinNormalExponent) << scale
correctMinIndex := (int64(MinNormalExponent) << scale) - 1
require.Greater(t, correctMinIndex, int64(math.MinInt32))
require.Equal(t, minIndex, int32(correctMinIndex))
correctMapped := roundedBoundary(scale, int32(correctMinIndex))
require.Equal(t, correctMapped, MinValue)
require.InEpsilon(t, mapped, MinValue, 1e-6)
require.Less(t, correctMapped, MinValue)
require.Equal(t, minIndex, int32(correctMinIndex))
correctMappedUpper := roundedBoundary(scale, int32(correctMinIndex+1))
require.Equal(t, correctMappedUpper, MinValue)
mapped, err := m.LowerBoundary(minIndex + 1)
require.NoError(t, err)
require.InEpsilon(t, mapped, MinValue, 1e-6)
// Subnormal values map to the min index:
require.Equal(t, m.MapToIndex(MinValue/2), int32(correctMinIndex))
@ -171,6 +184,11 @@ func TestLogarithmIndexMin(t *testing.T) {
require.Equal(t, m.MapToIndex(0x1.1p-1073), int32(correctMinIndex))
require.Equal(t, m.MapToIndex(0x1p-1074), int32(correctMinIndex))
// All subnormal values map and MinValue to the min index:
mappedLower, err := m.LowerBoundary(minIndex)
require.NoError(t, err)
require.InEpsilon(t, correctMapped, mappedLower, 1e-6)
// One smaller index will underflow.
_, err = m.LowerBoundary(minIndex - 1)
require.Equal(t, err, mapping.ErrUnderflow)
@ -191,7 +209,7 @@ func TestExponentIndexMax(t *testing.T) {
// 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
maxIndex64 := (int64(MaxNormalExponent+1) << scale) - 1
require.Less(t, maxIndex64, int64(math.MaxInt32))
require.Equal(t, index, int32(maxIndex64))
@ -211,40 +229,3 @@ func TestExponentIndexMax(t *testing.T) {
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)
}
}