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:
parent
8c3a85a5be
commit
09bf345912
@ -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
|
||||
|
||||
|
67
sdk/metric/aggregator/exponential/benchmark_test.go
Normal file
67
sdk/metric/aggregator/exponential/benchmark_test.go
Normal 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)
|
||||
}
|
@ -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.
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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))
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user