Skip to content

Commit 97c4271

Browse files
committed
PR feedback
* Move OTLP implementation to separate file * Simplify map key sorting
1 parent acef637 commit 97c4271

File tree

4 files changed

+281
-269
lines changed

4 files changed

+281
-269
lines changed

metric/distribution/exph/exph.go

Lines changed: 7 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
package exph
55

66
import (
7+
"cmp"
78
"fmt"
89
"log"
10+
"maps"
911
"math"
1012
"slices"
1113

@@ -77,12 +79,9 @@ func (d *ExpHistogramDistribution) ValuesAndCounts() ([]float64, []float64) {
7779
counts := []float64{}
7880

7981
// iterate through positive buckets in descending order
80-
posOffsetIndicies := make([]int, 0, len(d.positiveBuckets))
81-
for offsetIndex := range d.positiveBuckets {
82-
posOffsetIndicies = append(posOffsetIndicies, offsetIndex)
83-
}
84-
slices.Sort(posOffsetIndicies)
85-
slices.Reverse(posOffsetIndicies)
82+
posOffsetIndicies := slices.SortedFunc(maps.Keys(d.positiveBuckets), func(a, b int) int {
83+
return cmp.Compare(b, a)
84+
})
8685
for _, offsetIndex := range posOffsetIndicies {
8786
counter := d.positiveBuckets[offsetIndex]
8887
bucketBegin := LowerBoundary(offsetIndex, int(d.scale))
@@ -98,11 +97,7 @@ func (d *ExpHistogramDistribution) ValuesAndCounts() ([]float64, []float64) {
9897
}
9998

10099
// iterate through negative buckets in ascending order
101-
negOffsetIndicies := make([]int, 0, len(d.negativeBuckets))
102-
for offsetIndex := range d.negativeBuckets {
103-
negOffsetIndicies = append(negOffsetIndicies, offsetIndex)
104-
}
105-
slices.Sort(negOffsetIndicies)
100+
negOffsetIndicies := slices.Sorted(maps.Keys(d.negativeBuckets))
106101
for _, offsetIndex := range negOffsetIndicies {
107102
counter := d.negativeBuckets[offsetIndex]
108103
bucketBegin := LowerBoundary(offsetIndex, int(d.scale))
@@ -228,62 +223,6 @@ func (d *ExpHistogramDistribution) ConvertFromOtel(dp pmetric.ExponentialHistogr
228223
}
229224

230225
func (d *ExpHistogramDistribution) Resize(_ int) []*ExpHistogramDistribution {
231-
// for now, do not split data points into separate PMD requests
226+
// TODO: split data points into separate PMD requests if the number of buckets exceeds the API limit
232227
return []*ExpHistogramDistribution{d}
233228
}
234-
235-
// MapToIndexScale0 computes a bucket index at scale 0.
236-
func MapToIndexScale0(value float64) int {
237-
// Note: Frexp() rounds submnormal values to the smallest normal
238-
// value and returns an exponent corresponding to fractions in the
239-
// range [0.5, 1), whereas an exponent for the range [1, 2), so
240-
// subtract 1 from the exponent immediately.
241-
frac, exp := math.Frexp(value)
242-
exp--
243-
244-
if frac == 0.5 && value > 0 {
245-
// Special case for positive powers of two: they fall into the bucket
246-
// numbered one less.
247-
exp--
248-
}
249-
return exp
250-
}
251-
252-
// MapToIndexNegativeScale computes a bucket index for scales <= 0.
253-
func MapToIndexNegativeScale(value float64, scale int) int {
254-
return MapToIndexScale0(value) >> -scale
255-
}
256-
257-
// MapToIndex for any scale
258-
//
259-
// Values near a boundary could be mapped into the incorrect bucket due to float point calculation inaccuracy.
260-
func MapToIndex(value float64, scale int) int {
261-
// Special case for power-of-two values.
262-
if frac, exp := math.Frexp(value); frac == 0.5 {
263-
return ((exp - 1) << scale) - 1
264-
}
265-
scaleFactor := math.Ldexp(math.Log2E, scale)
266-
// The use of math.Log() to calculate the bucket index is not guaranteed to be exactly correct near powers of two.
267-
return int(math.Floor(math.Log(math.Abs(value)) * scaleFactor))
268-
}
269-
270-
// LowerBoundaryNegativeScale computes the lower boundary for index
271-
// with scales <= 0.
272-
func LowerBoundaryNegativeScale(index int, scale int) float64 {
273-
return math.Ldexp(1, index<<-scale)
274-
}
275-
276-
func LowerBoundary(index, scale int) float64 {
277-
if scale <= 0 {
278-
return LowerBoundaryNegativeScale(index, scale)
279-
}
280-
return LowerBoundaryPositiveScale(index, scale)
281-
}
282-
283-
// LowerBoundary computes the bucket boundary for positive scales.
284-
//
285-
// The returned value may be inaccurate due to accumulated floating point calculation errors
286-
func LowerBoundaryPositiveScale(index, scale int) float64 {
287-
inverseFactor := math.Ldexp(math.Ln2, -scale)
288-
return math.Exp(float64(index) * inverseFactor)
289-
}

metric/distribution/exph/exph_test.go

Lines changed: 0 additions & 201 deletions
Original file line numberDiff line numberDiff line change
@@ -638,207 +638,6 @@ func TestAddDistribution(t *testing.T) {
638638

639639
}
640640

641-
func TestMapToIndexPositiveScale(t *testing.T) {
642-
tests := []struct {
643-
name string
644-
scale int
645-
values []float64
646-
expected []int
647-
}{
648-
{
649-
name: "positive value inside bucket",
650-
scale: 1,
651-
values: []float64{1.3, 1.5, 2.2, 3.9, 4.2, 6.0},
652-
expected: []int{0, 1, 2, 3, 4, 5},
653-
},
654-
{
655-
// for positive values, histogram buckets use upper-inclusive boundaries
656-
// this is only reliable on boundaries that are powers of 2
657-
name: "positive value is on boundary",
658-
scale: 1,
659-
values: []float64{2.0, 4.0, 8.0},
660-
expected: []int{1, 3, 5},
661-
},
662-
{
663-
name: "negative value inside bucket",
664-
scale: 1,
665-
values: []float64{-1.3, -1.5, -2.2, -3.9, -4.2, -6.0},
666-
expected: []int{0, 1, 2, 3, 4, 5},
667-
},
668-
{
669-
// for negative values, histogram buckets use lower-inclusive boundaries
670-
// this is only reliable on boundaries that are powers of 2
671-
name: "negative value is on boundary",
672-
scale: 1,
673-
values: []float64{-1.0, -2.0, -4.0},
674-
expected: []int{0, 2, 4},
675-
},
676-
}
677-
for _, tt := range tests {
678-
t.Run(tt.name, func(t *testing.T) {
679-
for i, value := range tt.values {
680-
assert.Equal(t, tt.expected[i], MapToIndex(value, tt.scale), "expected value %f to map to index %d with scale %d", value, tt.expected[i], tt.scale)
681-
}
682-
})
683-
}
684-
685-
}
686-
687-
func TestMapToIndexScale0(t *testing.T) {
688-
tests := []struct {
689-
name string
690-
values []float64
691-
expected []int
692-
}{
693-
{
694-
name: "positive value inside bucket",
695-
values: []float64{1.5, 3.0, 6, 12, 18},
696-
expected: []int{0, 1, 2, 3, 4},
697-
},
698-
{
699-
// for negative values, histogram buckets use lower-inclusive boundaries
700-
name: "positive value is on boundary",
701-
values: []float64{2, 4, 8, 16.0, 32},
702-
expected: []int{0, 1, 2, 3, 4},
703-
},
704-
{
705-
name: "negative value inside bucket",
706-
values: []float64{-1.5, -3.0, -6, -12, -18},
707-
expected: []int{0, 1, 2, 3, 4},
708-
},
709-
{
710-
// for negative values, histogram buckets use lower-inclusive boundaries
711-
name: "negative value is on boundary",
712-
values: []float64{-2, -4, -8, -16, -32},
713-
expected: []int{1, 2, 3, 4, 5},
714-
},
715-
}
716-
for _, tt := range tests {
717-
t.Run(tt.name, func(t *testing.T) {
718-
for i, value := range tt.values {
719-
assert.Equal(t, tt.expected[i], MapToIndexNegativeScale(value, 0), "expected value %f to map to index %d with scale 0", value, tt.expected[i])
720-
}
721-
})
722-
}
723-
}
724-
725-
func TestMapToIndexNegativeScale(t *testing.T) {
726-
tests := []struct {
727-
name string
728-
scale int
729-
values []float64
730-
expected []int
731-
}{
732-
{
733-
name: "positive value inside bucket",
734-
scale: -1,
735-
values: []float64{1.5, 5.0, 32, 80, 500, 2000},
736-
expected: []int{0, 1, 2, 3, 4, 5},
737-
},
738-
{
739-
// for positive values, histogram buckets use upper-inclusive boundaries
740-
name: "positive value is on boundary",
741-
scale: -1,
742-
values: []float64{4, 16, 64, 256, 1024},
743-
expected: []int{0, 1, 2, 3, 4, 5},
744-
},
745-
{
746-
name: "negative value inside bucket",
747-
scale: -1,
748-
values: []float64{-1.5, -5.0, -32, -80, -500, -2000},
749-
expected: []int{0, 1, 2, 3, 4, 5},
750-
},
751-
{
752-
// for negative values, histogram buckets use lower-inclusive boundaries
753-
name: "negative value is on boundary",
754-
scale: -1,
755-
values: []float64{-1, -4, -16, -64, -256, -1024},
756-
expected: []int{0, 1, 2, 3, 4, 5, 6},
757-
},
758-
}
759-
for _, tt := range tests {
760-
t.Run(tt.name, func(t *testing.T) {
761-
for i, value := range tt.values {
762-
assert.Equal(t, tt.expected[i], MapToIndexNegativeScale(value, tt.scale), "expected value %f to map to index %d with scale %d", value, tt.expected[i], tt.scale)
763-
}
764-
})
765-
}
766-
}
767-
768-
func TestLowerBoundary(t *testing.T) {
769-
// scale = 1, base = 2^(1/2) or sqrt(2) = 1.41421
770-
assert.InDelta(t, 1.41421, LowerBoundary(1, 1), 0.01) // 2^(1/2)
771-
assert.InDelta(t, 2.0, LowerBoundary(2, 1), 0.01) // 2^(2/2)
772-
assert.InDelta(t, 2.82842, LowerBoundary(3, 1), 0.01) // 2^(3/2)
773-
assert.InDelta(t, 4.0, LowerBoundary(4, 1), 0.01) // 2^(4/2)
774-
assert.InDelta(t, 8.0, LowerBoundary(6, 1), 0.01) // 2^(6/2)
775-
assert.InDelta(t, 16.0, LowerBoundary(8, 1), 0.01) // 2^(8/2)
776-
777-
// scale = 2, base = 2^(1/4) = 1.18921
778-
assert.InDelta(t, 1.18921, LowerBoundary(1, 2), 0.01) // 2^(1/4)
779-
assert.InDelta(t, 1.41421, LowerBoundary(2, 2), 0.01) // 2^(2/4)
780-
assert.InDelta(t, 1.68180, LowerBoundary(3, 2), 0.01) // 2^(3/4)
781-
assert.InDelta(t, 2.0, LowerBoundary(4, 2), 0.01) // 2^(4/4)
782-
assert.InDelta(t, 2.82842, LowerBoundary(6, 2), 0.01) // 2^(6/8)
783-
assert.InDelta(t, 4.0, LowerBoundary(8, 2), 0.01) // 2^(8/8)
784-
785-
// scale = 0, base = 2
786-
assert.Equal(t, 1.0, LowerBoundary(0, 0)) // 2^0
787-
assert.Equal(t, 2.0, LowerBoundary(1, 0)) // 2^1
788-
assert.Equal(t, 4.0, LowerBoundary(2, 0)) // 2^2
789-
assert.Equal(t, 8.0, LowerBoundary(3, 0)) // 2^3
790-
791-
assert.Equal(t, 1.0, LowerBoundary(0, -1)) // 4^0
792-
assert.Equal(t, 4.0, LowerBoundary(1, -1)) // 4^1
793-
assert.Equal(t, 16.0, LowerBoundary(2, -1)) // 4^2
794-
assert.Equal(t, 64.0, LowerBoundary(3, -1)) // 4^3
795-
796-
// scale = -2, base = 2^(2^2) = 2^4 = 16
797-
assert.Equal(t, 1.0, LowerBoundary(0, -2)) // 16^0
798-
assert.Equal(t, 16.0, LowerBoundary(1, -2)) // 16^1
799-
assert.Equal(t, 256.0, LowerBoundary(2, -2)) // 16^2
800-
assert.Equal(t, 4096.0, LowerBoundary(3, -2)) // 16^3
801-
802-
assert.Equal(t, 1.0, LowerBoundary(0, -1)) // 4^0
803-
assert.Equal(t, 4.0, LowerBoundary(1, -1)) // 2^(2^1)^1 = 4^1 = 2^2 = 4^1
804-
assert.Equal(t, 16.0, LowerBoundary(2, -1)) // (2^2^1)^2 = 4^2 = 2^4 = 4^2
805-
assert.Equal(t, 64.0, LowerBoundary(3, -1)) // (2^2^1)^3 = 4^3 = 2^6 = 4^3
806-
807-
// scale = -2, base = 2^(2^2) = 2^4 = 16
808-
assert.Equal(t, 1.0, LowerBoundary(0, -2)) // (2^(2^2))^0 = 16^0 = 1
809-
assert.Equal(t, 16.0, LowerBoundary(1, -2)) // (2^(2^2))^1 = 2^4 = 16^1
810-
assert.Equal(t, 256.0, LowerBoundary(2, -2)) // (2^(2^2))^2 = 2^8 = 16^2
811-
assert.Equal(t, 4096.0, LowerBoundary(3, -2)) // (2^(2^2))^3 = 2^12 = 16^3
812-
}
813-
814-
func BenchmarkLowerBoundary(b *testing.B) {
815-
b.Run("positive scale", func(b *testing.B) {
816-
for i := 0; i < b.N; i++ {
817-
LowerBoundary(10, 1)
818-
}
819-
})
820-
821-
b.Run("scale 0", func(b *testing.B) {
822-
for i := 0; i < b.N; i++ {
823-
LowerBoundary(10, 0)
824-
}
825-
})
826-
827-
b.Run("negative scale", func(b *testing.B) {
828-
for i := 0; i < b.N; i++ {
829-
LowerBoundary(10, -1)
830-
}
831-
})
832-
}
833-
834-
func BenchmarkLowerBoundaryNegativeScale(b *testing.B) {
835-
b.Run("reference", func(b *testing.B) {
836-
for i := 0; i < b.N; i++ {
837-
LowerBoundaryNegativeScale(10, -9)
838-
}
839-
})
840-
}
841-
842641
func BenchmarkConvertFromOtel(b *testing.B) {
843642

844643
ts := time.Date(2025, time.March, 31, 22, 6, 30, 0, time.UTC)

metric/distribution/exph/mapping.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
// The following functions are originally sourced from OpenTelemetry's reference implementation. See
5+
// https://opentelemetry.io/docs/specs/otel/metrics/data-model/#producer-expectations
6+
package exph
7+
8+
import "math"
9+
10+
// MapToIndexScale0 computes a bucket index at scale 0.
11+
func MapToIndexScale0(value float64) int {
12+
// Note: Frexp() rounds submnormal values to the smallest normal
13+
// value and returns an exponent corresponding to fractions in the
14+
// range [0.5, 1), whereas an exponent for the range [1, 2), so
15+
// subtract 1 from the exponent immediately.
16+
frac, exp := math.Frexp(value)
17+
exp--
18+
19+
if frac == 0.5 && value > 0 {
20+
// Special case for positive powers of two: they fall into the bucket
21+
// numbered one less.
22+
exp--
23+
}
24+
return exp
25+
}
26+
27+
// MapToIndexNegativeScale computes a bucket index for scales <= 0.
28+
func MapToIndexNegativeScale(value float64, scale int) int {
29+
return MapToIndexScale0(value) >> -scale
30+
}
31+
32+
// MapToIndex for any scale
33+
//
34+
// Values near a boundary could be mapped into the incorrect bucket due to float point calculation inaccuracy.
35+
func MapToIndex(value float64, scale int) int {
36+
// Special case for power-of-two values.
37+
if frac, exp := math.Frexp(value); frac == 0.5 {
38+
return ((exp - 1) << scale) - 1
39+
}
40+
scaleFactor := math.Ldexp(math.Log2E, scale)
41+
// The use of math.Log() to calculate the bucket index is not guaranteed to be exactly correct near powers of two.
42+
return int(math.Floor(math.Log(math.Abs(value)) * scaleFactor))
43+
}
44+
45+
// LowerBoundaryNegativeScale computes the lower boundary for index
46+
// with scales <= 0.
47+
func LowerBoundaryNegativeScale(index int, scale int) float64 {
48+
return math.Ldexp(1, index<<-scale)
49+
}
50+
51+
func LowerBoundary(index, scale int) float64 {
52+
if scale <= 0 {
53+
return LowerBoundaryNegativeScale(index, scale)
54+
}
55+
return LowerBoundaryPositiveScale(index, scale)
56+
}
57+
58+
// LowerBoundary computes the bucket boundary for positive scales.
59+
//
60+
// The returned value may be inaccurate due to accumulated floating point calculation errors
61+
func LowerBoundaryPositiveScale(index, scale int) float64 {
62+
inverseFactor := math.Ldexp(math.Ln2, -scale)
63+
return math.Exp(float64(index) * inverseFactor)
64+
}

0 commit comments

Comments
 (0)