You've already forked opentelemetry-go
mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2025-06-23 00:07:52 +02:00
1103 lines
25 KiB
Go
1103 lines
25 KiB
Go
// Copyright The OpenTelemetry Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package aggregate
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"go.opentelemetry.io/otel/internal/global"
|
|
"go.opentelemetry.io/otel/sdk/metric/metricdata"
|
|
)
|
|
|
|
type noErrorHandler struct{ t *testing.T }
|
|
|
|
func (h *noErrorHandler) Handle(e error) {
|
|
require.NoError(h.t, e)
|
|
}
|
|
|
|
func withHandler(t *testing.T) func() {
|
|
t.Helper()
|
|
h := &noErrorHandler{t: t}
|
|
original := global.GetErrorHandler()
|
|
global.SetErrorHandler(h)
|
|
return func() { global.SetErrorHandler(original) }
|
|
}
|
|
|
|
func TestExpoHistogramDataPointRecord(t *testing.T) {
|
|
t.Run("float64", testExpoHistogramDataPointRecord[float64])
|
|
t.Run("float64 MinMaxSum", testExpoHistogramMinMaxSumFloat64)
|
|
t.Run("float64-2", testExpoHistogramDataPointRecordFloat64)
|
|
t.Run("int64", testExpoHistogramDataPointRecord[int64])
|
|
t.Run("int64 MinMaxSum", testExpoHistogramMinMaxSumInt64)
|
|
}
|
|
|
|
func testExpoHistogramDataPointRecord[N int64 | float64](t *testing.T) {
|
|
testCases := []struct {
|
|
maxSize int
|
|
values []N
|
|
expectedBuckets expoBuckets
|
|
expectedScale int32
|
|
}{
|
|
{
|
|
maxSize: 4,
|
|
values: []N{2, 4, 1},
|
|
expectedBuckets: expoBuckets{
|
|
startBin: -1,
|
|
|
|
counts: []uint64{1, 1, 1},
|
|
},
|
|
expectedScale: 0,
|
|
},
|
|
{
|
|
maxSize: 4,
|
|
values: []N{4, 4, 4, 2, 16, 1},
|
|
expectedBuckets: expoBuckets{
|
|
startBin: -1,
|
|
counts: []uint64{1, 4, 1},
|
|
},
|
|
expectedScale: -1,
|
|
},
|
|
{
|
|
maxSize: 2,
|
|
values: []N{1, 2, 4},
|
|
expectedBuckets: expoBuckets{
|
|
startBin: -1,
|
|
|
|
counts: []uint64{1, 2},
|
|
},
|
|
expectedScale: -1,
|
|
},
|
|
{
|
|
maxSize: 2,
|
|
values: []N{1, 4, 2},
|
|
expectedBuckets: expoBuckets{
|
|
startBin: -1,
|
|
|
|
counts: []uint64{1, 2},
|
|
},
|
|
expectedScale: -1,
|
|
},
|
|
{
|
|
maxSize: 2,
|
|
values: []N{2, 4, 1},
|
|
expectedBuckets: expoBuckets{
|
|
startBin: -1,
|
|
|
|
counts: []uint64{1, 2},
|
|
},
|
|
expectedScale: -1,
|
|
},
|
|
{
|
|
maxSize: 2,
|
|
values: []N{2, 1, 4},
|
|
expectedBuckets: expoBuckets{
|
|
startBin: -1,
|
|
|
|
counts: []uint64{1, 2},
|
|
},
|
|
expectedScale: -1,
|
|
},
|
|
{
|
|
maxSize: 2,
|
|
values: []N{4, 1, 2},
|
|
expectedBuckets: expoBuckets{
|
|
startBin: -1,
|
|
|
|
counts: []uint64{1, 2},
|
|
},
|
|
expectedScale: -1,
|
|
},
|
|
{
|
|
maxSize: 2,
|
|
values: []N{4, 2, 1},
|
|
expectedBuckets: expoBuckets{
|
|
startBin: -1,
|
|
|
|
counts: []uint64{1, 2},
|
|
},
|
|
expectedScale: -1,
|
|
},
|
|
}
|
|
for _, tt := range testCases {
|
|
t.Run(fmt.Sprint(tt.values), func(t *testing.T) {
|
|
restore := withHandler(t)
|
|
defer restore()
|
|
|
|
dp := newExpoHistogramDataPoint[N](alice, tt.maxSize, 20, false, false)
|
|
for _, v := range tt.values {
|
|
dp.record(v)
|
|
dp.record(-v)
|
|
}
|
|
|
|
assert.Equal(t, tt.expectedBuckets, dp.posBuckets, "positive buckets")
|
|
assert.Equal(t, tt.expectedBuckets, dp.negBuckets, "negative buckets")
|
|
assert.Equal(t, tt.expectedScale, dp.scale, "scale")
|
|
})
|
|
}
|
|
}
|
|
|
|
// TODO: This can be defined in the test after we drop support for go1.19.
|
|
type expectedMinMaxSum[N int64 | float64] struct {
|
|
min N
|
|
max N
|
|
sum N
|
|
count uint
|
|
}
|
|
type expoHistogramDataPointRecordMinMaxSumTestCase[N int64 | float64] struct {
|
|
values []N
|
|
expected expectedMinMaxSum[N]
|
|
}
|
|
|
|
func testExpoHistogramMinMaxSumInt64(t *testing.T) {
|
|
testCases := []expoHistogramDataPointRecordMinMaxSumTestCase[int64]{
|
|
{
|
|
values: []int64{2, 4, 1},
|
|
expected: expectedMinMaxSum[int64]{1, 4, 7, 3},
|
|
},
|
|
{
|
|
values: []int64{4, 4, 4, 2, 16, 1},
|
|
expected: expectedMinMaxSum[int64]{1, 16, 31, 6},
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(fmt.Sprint(tt.values), func(t *testing.T) {
|
|
restore := withHandler(t)
|
|
defer restore()
|
|
|
|
h := newExponentialHistogram[int64](4, 20, false, false, 0, dropExemplars[int64])
|
|
for _, v := range tt.values {
|
|
h.measure(context.Background(), v, alice, nil)
|
|
}
|
|
dp := h.values[alice.Equivalent()]
|
|
|
|
assert.Equal(t, tt.expected.max, dp.max)
|
|
assert.Equal(t, tt.expected.min, dp.min)
|
|
assert.InDelta(t, tt.expected.sum, dp.sum, 0.01)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testExpoHistogramMinMaxSumFloat64(t *testing.T) {
|
|
testCases := []expoHistogramDataPointRecordMinMaxSumTestCase[float64]{
|
|
{
|
|
values: []float64{2, 4, 1},
|
|
expected: expectedMinMaxSum[float64]{1, 4, 7, 3},
|
|
},
|
|
{
|
|
values: []float64{2, 4, 1, math.Inf(1)},
|
|
expected: expectedMinMaxSum[float64]{1, 4, 7, 4},
|
|
},
|
|
{
|
|
values: []float64{2, 4, 1, math.Inf(-1)},
|
|
expected: expectedMinMaxSum[float64]{1, 4, 7, 4},
|
|
},
|
|
{
|
|
values: []float64{2, 4, 1, math.NaN()},
|
|
expected: expectedMinMaxSum[float64]{1, 4, 7, 4},
|
|
},
|
|
{
|
|
values: []float64{4, 4, 4, 2, 16, 1},
|
|
expected: expectedMinMaxSum[float64]{1, 16, 31, 6},
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(fmt.Sprint(tt.values), func(t *testing.T) {
|
|
restore := withHandler(t)
|
|
defer restore()
|
|
|
|
h := newExponentialHistogram[float64](4, 20, false, false, 0, dropExemplars[float64])
|
|
for _, v := range tt.values {
|
|
h.measure(context.Background(), v, alice, nil)
|
|
}
|
|
dp := h.values[alice.Equivalent()]
|
|
|
|
assert.Equal(t, tt.expected.max, dp.max)
|
|
assert.Equal(t, tt.expected.min, dp.min)
|
|
assert.InDelta(t, tt.expected.sum, dp.sum, 0.01)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testExpoHistogramDataPointRecordFloat64(t *testing.T) {
|
|
type TestCase struct {
|
|
maxSize int
|
|
values []float64
|
|
expectedBuckets expoBuckets
|
|
expectedScale int32
|
|
}
|
|
|
|
testCases := []TestCase{
|
|
{
|
|
maxSize: 4,
|
|
values: []float64{2, 2, 2, 1, 8, 0.5},
|
|
expectedBuckets: expoBuckets{
|
|
startBin: -1,
|
|
counts: []uint64{2, 3, 1},
|
|
},
|
|
expectedScale: -1,
|
|
},
|
|
{
|
|
maxSize: 2,
|
|
values: []float64{1, 0.5, 2},
|
|
expectedBuckets: expoBuckets{
|
|
startBin: -1,
|
|
counts: []uint64{2, 1},
|
|
},
|
|
expectedScale: -1,
|
|
},
|
|
{
|
|
maxSize: 2,
|
|
values: []float64{1, 2, 0.5},
|
|
expectedBuckets: expoBuckets{
|
|
startBin: -1,
|
|
counts: []uint64{2, 1},
|
|
},
|
|
expectedScale: -1,
|
|
},
|
|
{
|
|
maxSize: 2,
|
|
values: []float64{2, 0.5, 1},
|
|
expectedBuckets: expoBuckets{
|
|
startBin: -1,
|
|
counts: []uint64{2, 1},
|
|
},
|
|
expectedScale: -1,
|
|
},
|
|
{
|
|
maxSize: 2,
|
|
values: []float64{2, 1, 0.5},
|
|
expectedBuckets: expoBuckets{
|
|
startBin: -1,
|
|
counts: []uint64{2, 1},
|
|
},
|
|
expectedScale: -1,
|
|
},
|
|
{
|
|
maxSize: 2,
|
|
values: []float64{0.5, 1, 2},
|
|
expectedBuckets: expoBuckets{
|
|
startBin: -1,
|
|
counts: []uint64{2, 1},
|
|
},
|
|
expectedScale: -1,
|
|
},
|
|
{
|
|
maxSize: 2,
|
|
values: []float64{0.5, 2, 1},
|
|
expectedBuckets: expoBuckets{
|
|
startBin: -1,
|
|
counts: []uint64{2, 1},
|
|
},
|
|
expectedScale: -1,
|
|
},
|
|
}
|
|
for _, tt := range testCases {
|
|
t.Run(fmt.Sprint(tt.values), func(t *testing.T) {
|
|
restore := withHandler(t)
|
|
defer restore()
|
|
|
|
dp := newExpoHistogramDataPoint[float64](alice, tt.maxSize, 20, false, false)
|
|
for _, v := range tt.values {
|
|
dp.record(v)
|
|
dp.record(-v)
|
|
}
|
|
|
|
assert.Equal(t, tt.expectedBuckets, dp.posBuckets)
|
|
assert.Equal(t, tt.expectedBuckets, dp.negBuckets)
|
|
assert.Equal(t, tt.expectedScale, dp.scale)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExponentialHistogramDataPointRecordLimits(t *testing.T) {
|
|
// These bins are calculated from the following formula:
|
|
// floor( log2( value) * 2^20 ) using an arbitrary precision calculator.
|
|
|
|
fdp := newExpoHistogramDataPoint[float64](alice, 4, 20, false, false)
|
|
fdp.record(math.MaxFloat64)
|
|
|
|
if fdp.posBuckets.startBin != 1073741823 {
|
|
t.Errorf("Expected startBin to be 1073741823, got %d", fdp.posBuckets.startBin)
|
|
}
|
|
|
|
fdp = newExpoHistogramDataPoint[float64](alice, 4, 20, false, false)
|
|
fdp.record(math.SmallestNonzeroFloat64)
|
|
|
|
if fdp.posBuckets.startBin != -1126170625 {
|
|
t.Errorf("Expected startBin to be -1126170625, got %d", fdp.posBuckets.startBin)
|
|
}
|
|
|
|
idp := newExpoHistogramDataPoint[int64](alice, 4, 20, false, false)
|
|
idp.record(math.MaxInt64)
|
|
|
|
if idp.posBuckets.startBin != 66060287 {
|
|
t.Errorf("Expected startBin to be 66060287, got %d", idp.posBuckets.startBin)
|
|
}
|
|
}
|
|
|
|
func TestExpoBucketDownscale(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
bucket *expoBuckets
|
|
scale int32
|
|
want *expoBuckets
|
|
}{
|
|
{
|
|
name: "Empty bucket",
|
|
bucket: &expoBuckets{},
|
|
scale: 3,
|
|
want: &expoBuckets{},
|
|
},
|
|
{
|
|
name: "1 size bucket",
|
|
bucket: &expoBuckets{
|
|
startBin: 50,
|
|
counts: []uint64{7},
|
|
},
|
|
scale: 4,
|
|
want: &expoBuckets{
|
|
startBin: 3,
|
|
counts: []uint64{7},
|
|
},
|
|
},
|
|
{
|
|
name: "zero scale",
|
|
bucket: &expoBuckets{
|
|
startBin: 50,
|
|
counts: []uint64{7, 5},
|
|
},
|
|
scale: 0,
|
|
want: &expoBuckets{
|
|
startBin: 50,
|
|
counts: []uint64{7, 5},
|
|
},
|
|
},
|
|
{
|
|
name: "aligned bucket scale 1",
|
|
bucket: &expoBuckets{
|
|
startBin: 0,
|
|
counts: []uint64{1, 2, 3, 4, 5, 6},
|
|
},
|
|
scale: 1,
|
|
want: &expoBuckets{
|
|
startBin: 0,
|
|
counts: []uint64{3, 7, 11},
|
|
},
|
|
},
|
|
{
|
|
name: "aligned bucket scale 2",
|
|
bucket: &expoBuckets{
|
|
startBin: 0,
|
|
counts: []uint64{1, 2, 3, 4, 5, 6},
|
|
},
|
|
scale: 2,
|
|
want: &expoBuckets{
|
|
startBin: 0,
|
|
counts: []uint64{10, 11},
|
|
},
|
|
},
|
|
{
|
|
name: "aligned bucket scale 3",
|
|
bucket: &expoBuckets{
|
|
startBin: 0,
|
|
counts: []uint64{1, 2, 3, 4, 5, 6},
|
|
},
|
|
scale: 3,
|
|
want: &expoBuckets{
|
|
startBin: 0,
|
|
counts: []uint64{21},
|
|
},
|
|
},
|
|
{
|
|
name: "unaligned bucket scale 1",
|
|
bucket: &expoBuckets{
|
|
startBin: 5,
|
|
counts: []uint64{1, 2, 3, 4, 5, 6},
|
|
}, // This is equivalent to [0,0,0,0,0,1,2,3,4,5,6]
|
|
scale: 1,
|
|
want: &expoBuckets{
|
|
startBin: 2,
|
|
counts: []uint64{1, 5, 9, 6},
|
|
}, // This is equivalent to [0,0,1,5,9,6]
|
|
},
|
|
{
|
|
name: "unaligned bucket scale 2",
|
|
bucket: &expoBuckets{
|
|
startBin: 7,
|
|
counts: []uint64{1, 2, 3, 4, 5, 6},
|
|
}, // This is equivalent to [0,0,0,0,0,0,0,1,2,3,4,5,6]
|
|
scale: 2,
|
|
want: &expoBuckets{
|
|
startBin: 1,
|
|
counts: []uint64{1, 14, 6},
|
|
}, // This is equivalent to [0,1,14,6]
|
|
},
|
|
{
|
|
name: "unaligned bucket scale 3",
|
|
bucket: &expoBuckets{
|
|
startBin: 3,
|
|
counts: []uint64{1, 2, 3, 4, 5, 6},
|
|
}, // This is equivalent to [0,0,0,1,2,3,4,5,6]
|
|
scale: 3,
|
|
want: &expoBuckets{
|
|
startBin: 0,
|
|
counts: []uint64{15, 6},
|
|
}, // This is equivalent to [0,15,6]
|
|
},
|
|
{
|
|
name: "unaligned bucket scale 1",
|
|
bucket: &expoBuckets{
|
|
startBin: 1,
|
|
counts: []uint64{1, 0, 1},
|
|
},
|
|
scale: 1,
|
|
want: &expoBuckets{
|
|
startBin: 0,
|
|
counts: []uint64{1, 1},
|
|
},
|
|
},
|
|
{
|
|
name: "negative startBin",
|
|
bucket: &expoBuckets{
|
|
startBin: -1,
|
|
counts: []uint64{1, 0, 3},
|
|
},
|
|
scale: 1,
|
|
want: &expoBuckets{
|
|
startBin: -1,
|
|
counts: []uint64{1, 3},
|
|
},
|
|
},
|
|
{
|
|
name: "negative startBin 2",
|
|
bucket: &expoBuckets{
|
|
startBin: -4,
|
|
counts: []uint64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
|
|
},
|
|
scale: 1,
|
|
want: &expoBuckets{
|
|
startBin: -2,
|
|
counts: []uint64{3, 7, 11, 15, 19},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tt.bucket.downscale(tt.scale)
|
|
|
|
assert.Equal(t, tt.want, tt.bucket)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExpoBucketRecord(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
bucket *expoBuckets
|
|
bin int32
|
|
want *expoBuckets
|
|
}{
|
|
{
|
|
name: "Empty Bucket creates first count",
|
|
bucket: &expoBuckets{},
|
|
bin: -5,
|
|
want: &expoBuckets{
|
|
startBin: -5,
|
|
counts: []uint64{1},
|
|
},
|
|
},
|
|
{
|
|
name: "Bin is in the bucket",
|
|
bucket: &expoBuckets{
|
|
startBin: 3,
|
|
counts: []uint64{1, 2, 3, 4, 5, 6},
|
|
},
|
|
bin: 5,
|
|
want: &expoBuckets{
|
|
startBin: 3,
|
|
counts: []uint64{1, 2, 4, 4, 5, 6},
|
|
},
|
|
},
|
|
{
|
|
name: "Bin is before the start of the bucket",
|
|
bucket: &expoBuckets{
|
|
startBin: 1,
|
|
counts: []uint64{1, 2, 3, 4, 5, 6},
|
|
},
|
|
bin: -2,
|
|
want: &expoBuckets{
|
|
startBin: -2,
|
|
counts: []uint64{1, 0, 0, 1, 2, 3, 4, 5, 6},
|
|
},
|
|
},
|
|
{
|
|
name: "Bin is after the end of the bucket",
|
|
bucket: &expoBuckets{
|
|
startBin: -2,
|
|
counts: []uint64{1, 2, 3, 4, 5, 6},
|
|
},
|
|
bin: 4,
|
|
want: &expoBuckets{
|
|
startBin: -2,
|
|
counts: []uint64{1, 2, 3, 4, 5, 6, 1},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tt.bucket.record(tt.bin)
|
|
|
|
assert.Equal(t, tt.want, tt.bucket)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScaleChange(t *testing.T) {
|
|
type args struct {
|
|
bin int32
|
|
startBin int32
|
|
length int
|
|
maxSize int
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want int32
|
|
}{
|
|
{
|
|
name: "if length is 0, no rescale is needed",
|
|
// [] -> [5] Length 1
|
|
args: args{
|
|
bin: 5,
|
|
startBin: 0,
|
|
length: 0,
|
|
maxSize: 4,
|
|
},
|
|
want: 0,
|
|
},
|
|
{
|
|
name: "if bin is between start, and the end, no rescale needed",
|
|
// [-1, ..., 8] Length 10 -> [-1, ..., 5, ..., 8] Length 10
|
|
args: args{
|
|
bin: 5,
|
|
startBin: -1,
|
|
length: 10,
|
|
maxSize: 20,
|
|
},
|
|
want: 0,
|
|
},
|
|
{
|
|
name: "if len([bin,... end]) > maxSize, rescale needed",
|
|
// [8,9,10] Length 3 -> [5, ..., 10] Length 6
|
|
args: args{
|
|
bin: 5,
|
|
startBin: 8,
|
|
length: 3,
|
|
maxSize: 5,
|
|
},
|
|
want: 1,
|
|
},
|
|
{
|
|
name: "if len([start, ..., bin]) > maxSize, rescale needed",
|
|
// [2,3,4] Length 3 -> [2, ..., 7] Length 6
|
|
args: args{
|
|
bin: 7,
|
|
startBin: 2,
|
|
length: 3,
|
|
maxSize: 5,
|
|
},
|
|
want: 1,
|
|
},
|
|
{
|
|
name: "if len([start, ..., bin]) > maxSize, rescale needed",
|
|
// [2,3,4] Length 3 -> [2, ..., 7] Length 12
|
|
args: args{
|
|
bin: 13,
|
|
startBin: 2,
|
|
length: 3,
|
|
maxSize: 5,
|
|
},
|
|
want: 2,
|
|
},
|
|
{
|
|
name: "It should not hang if it will never be able to rescale",
|
|
args: args{
|
|
bin: 1,
|
|
startBin: -1,
|
|
length: 1,
|
|
maxSize: 1,
|
|
},
|
|
want: 31,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
p := newExpoHistogramDataPoint[float64](alice, tt.args.maxSize, 20, false, false)
|
|
got := p.scaleChange(tt.args.bin, tt.args.startBin, tt.args.length)
|
|
if got != tt.want {
|
|
t.Errorf("scaleChange() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkPrepend(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
agg := newExpoHistogramDataPoint[float64](alice, 1024, 20, false, false)
|
|
n := math.MaxFloat64
|
|
for j := 0; j < 1024; j++ {
|
|
agg.record(n)
|
|
n = n / 2
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkAppend(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
agg := newExpoHistogramDataPoint[float64](alice, 1024, 20, false, false)
|
|
n := smallestNonZeroNormalFloat64
|
|
for j := 0; j < 1024; j++ {
|
|
agg.record(n)
|
|
n = n * 2
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkExponentialHistogram(b *testing.B) {
|
|
const (
|
|
maxSize = 160
|
|
maxScale = 20
|
|
noMinMax = false
|
|
noSum = false
|
|
)
|
|
|
|
b.Run("Int64/Cumulative", benchmarkAggregate(func() (Measure[int64], ComputeAggregation) {
|
|
return Builder[int64]{
|
|
Temporality: metricdata.CumulativeTemporality,
|
|
}.ExponentialBucketHistogram(maxSize, maxScale, noMinMax, noSum)
|
|
}))
|
|
b.Run("Int64/Delta", benchmarkAggregate(func() (Measure[int64], ComputeAggregation) {
|
|
return Builder[int64]{
|
|
Temporality: metricdata.DeltaTemporality,
|
|
}.ExponentialBucketHistogram(maxSize, maxScale, noMinMax, noSum)
|
|
}))
|
|
b.Run("Float64/Cumulative", benchmarkAggregate(func() (Measure[float64], ComputeAggregation) {
|
|
return Builder[float64]{
|
|
Temporality: metricdata.CumulativeTemporality,
|
|
}.ExponentialBucketHistogram(maxSize, maxScale, noMinMax, noSum)
|
|
}))
|
|
b.Run("Float64/Delta", benchmarkAggregate(func() (Measure[float64], ComputeAggregation) {
|
|
return Builder[float64]{
|
|
Temporality: metricdata.DeltaTemporality,
|
|
}.ExponentialBucketHistogram(maxSize, maxScale, noMinMax, noSum)
|
|
}))
|
|
}
|
|
|
|
func TestSubNormal(t *testing.T) {
|
|
want := &expoHistogramDataPoint[float64]{
|
|
attrs: alice,
|
|
maxSize: 4,
|
|
count: 3,
|
|
min: math.SmallestNonzeroFloat64,
|
|
max: math.SmallestNonzeroFloat64,
|
|
sum: 3 * math.SmallestNonzeroFloat64,
|
|
|
|
scale: 20,
|
|
posBuckets: expoBuckets{
|
|
startBin: -1126170625,
|
|
counts: []uint64{3},
|
|
},
|
|
}
|
|
|
|
ehdp := newExpoHistogramDataPoint[float64](alice, 4, 20, false, false)
|
|
ehdp.record(math.SmallestNonzeroFloat64)
|
|
ehdp.record(math.SmallestNonzeroFloat64)
|
|
ehdp.record(math.SmallestNonzeroFloat64)
|
|
|
|
assert.Equal(t, want, ehdp)
|
|
}
|
|
|
|
func TestExponentialHistogramAggregation(t *testing.T) {
|
|
c := new(clock)
|
|
t.Cleanup(c.Register())
|
|
|
|
t.Run("Int64/Delta", testDeltaExpoHist[int64]())
|
|
c.Reset()
|
|
|
|
t.Run("Float64/Delta", testDeltaExpoHist[float64]())
|
|
c.Reset()
|
|
|
|
t.Run("Int64/Cumulative", testCumulativeExpoHist[int64]())
|
|
c.Reset()
|
|
|
|
t.Run("Float64/Cumulative", testCumulativeExpoHist[float64]())
|
|
}
|
|
|
|
func testDeltaExpoHist[N int64 | float64]() func(t *testing.T) {
|
|
in, out := Builder[N]{
|
|
Temporality: metricdata.DeltaTemporality,
|
|
Filter: attrFltr,
|
|
AggregationLimit: 2,
|
|
}.ExponentialBucketHistogram(4, 20, false, false)
|
|
ctx := context.Background()
|
|
return test[N](in, out, []teststep[N]{
|
|
{
|
|
input: []arg[N]{},
|
|
expect: output{
|
|
n: 0,
|
|
agg: metricdata.ExponentialHistogram[N]{
|
|
Temporality: metricdata.DeltaTemporality,
|
|
DataPoints: []metricdata.ExponentialHistogramDataPoint[N]{},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
input: []arg[N]{
|
|
{ctx, 4, alice},
|
|
{ctx, 4, alice},
|
|
{ctx, 4, alice},
|
|
{ctx, 2, alice},
|
|
{ctx, 16, alice},
|
|
{ctx, 1, alice},
|
|
{ctx, -1, alice},
|
|
},
|
|
expect: output{
|
|
n: 1,
|
|
agg: metricdata.ExponentialHistogram[N]{
|
|
Temporality: metricdata.DeltaTemporality,
|
|
DataPoints: []metricdata.ExponentialHistogramDataPoint[N]{
|
|
{
|
|
Attributes: fltrAlice,
|
|
StartTime: y2kPlus(1),
|
|
Time: y2kPlus(2),
|
|
Count: 7,
|
|
Min: metricdata.NewExtrema[N](-1),
|
|
Max: metricdata.NewExtrema[N](16),
|
|
Sum: 30,
|
|
Scale: -1,
|
|
PositiveBucket: metricdata.ExponentialBucket{
|
|
Offset: -1,
|
|
Counts: []uint64{1, 4, 1},
|
|
},
|
|
NegativeBucket: metricdata.ExponentialBucket{
|
|
Offset: -1,
|
|
Counts: []uint64{1},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// Delta sums are expected to reset.
|
|
input: []arg[N]{},
|
|
expect: output{
|
|
n: 0,
|
|
agg: metricdata.ExponentialHistogram[N]{
|
|
Temporality: metricdata.DeltaTemporality,
|
|
DataPoints: []metricdata.ExponentialHistogramDataPoint[N]{},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
input: []arg[N]{
|
|
{ctx, 4, alice},
|
|
{ctx, 4, alice},
|
|
{ctx, 4, alice},
|
|
{ctx, 2, alice},
|
|
{ctx, 16, alice},
|
|
{ctx, 1, alice},
|
|
// These will exceed the cardinality limit.
|
|
{ctx, 4, bob},
|
|
{ctx, 4, bob},
|
|
{ctx, 4, bob},
|
|
{ctx, 2, carol},
|
|
{ctx, 16, carol},
|
|
{ctx, 1, dave},
|
|
{ctx, -1, alice},
|
|
},
|
|
expect: output{
|
|
n: 2,
|
|
agg: metricdata.ExponentialHistogram[N]{
|
|
Temporality: metricdata.DeltaTemporality,
|
|
DataPoints: []metricdata.ExponentialHistogramDataPoint[N]{
|
|
{
|
|
Attributes: fltrAlice,
|
|
StartTime: y2kPlus(3),
|
|
Time: y2kPlus(4),
|
|
Count: 7,
|
|
Min: metricdata.NewExtrema[N](-1),
|
|
Max: metricdata.NewExtrema[N](16),
|
|
Sum: 30,
|
|
Scale: -1,
|
|
PositiveBucket: metricdata.ExponentialBucket{
|
|
Offset: -1,
|
|
Counts: []uint64{1, 4, 1},
|
|
},
|
|
NegativeBucket: metricdata.ExponentialBucket{
|
|
Offset: -1,
|
|
Counts: []uint64{1},
|
|
},
|
|
},
|
|
{
|
|
Attributes: overflowSet,
|
|
StartTime: y2kPlus(3),
|
|
Time: y2kPlus(4),
|
|
Count: 6,
|
|
Min: metricdata.NewExtrema[N](1),
|
|
Max: metricdata.NewExtrema[N](16),
|
|
Sum: 31,
|
|
Scale: -1,
|
|
PositiveBucket: metricdata.ExponentialBucket{
|
|
Offset: -1,
|
|
Counts: []uint64{1, 4, 1},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
func testCumulativeExpoHist[N int64 | float64]() func(t *testing.T) {
|
|
in, out := Builder[N]{
|
|
Temporality: metricdata.CumulativeTemporality,
|
|
Filter: attrFltr,
|
|
AggregationLimit: 2,
|
|
}.ExponentialBucketHistogram(4, 20, false, false)
|
|
ctx := context.Background()
|
|
return test[N](in, out, []teststep[N]{
|
|
{
|
|
input: []arg[N]{},
|
|
expect: output{
|
|
n: 0,
|
|
agg: metricdata.ExponentialHistogram[N]{
|
|
Temporality: metricdata.CumulativeTemporality,
|
|
DataPoints: []metricdata.ExponentialHistogramDataPoint[N]{},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
input: []arg[N]{
|
|
{ctx, 4, alice},
|
|
{ctx, 4, alice},
|
|
{ctx, 4, alice},
|
|
{ctx, 2, alice},
|
|
{ctx, 16, alice},
|
|
{ctx, 1, alice},
|
|
{ctx, -1, alice},
|
|
},
|
|
expect: output{
|
|
n: 1,
|
|
agg: metricdata.ExponentialHistogram[N]{
|
|
Temporality: metricdata.CumulativeTemporality,
|
|
DataPoints: []metricdata.ExponentialHistogramDataPoint[N]{
|
|
{
|
|
Attributes: fltrAlice,
|
|
StartTime: y2kPlus(0),
|
|
Time: y2kPlus(2),
|
|
Count: 7,
|
|
Min: metricdata.NewExtrema[N](-1),
|
|
Max: metricdata.NewExtrema[N](16),
|
|
Sum: 30,
|
|
Scale: -1,
|
|
PositiveBucket: metricdata.ExponentialBucket{
|
|
Offset: -1,
|
|
Counts: []uint64{1, 4, 1},
|
|
},
|
|
NegativeBucket: metricdata.ExponentialBucket{
|
|
Offset: -1,
|
|
Counts: []uint64{1},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
input: []arg[N]{
|
|
{ctx, 2, alice},
|
|
{ctx, 3, alice},
|
|
{ctx, 8, alice},
|
|
},
|
|
expect: output{
|
|
n: 1,
|
|
agg: metricdata.ExponentialHistogram[N]{
|
|
Temporality: metricdata.CumulativeTemporality,
|
|
DataPoints: []metricdata.ExponentialHistogramDataPoint[N]{
|
|
{
|
|
Attributes: fltrAlice,
|
|
StartTime: y2kPlus(0),
|
|
Time: y2kPlus(3),
|
|
Count: 10,
|
|
Min: metricdata.NewExtrema[N](-1),
|
|
Max: metricdata.NewExtrema[N](16),
|
|
Sum: 43,
|
|
Scale: -1,
|
|
PositiveBucket: metricdata.ExponentialBucket{
|
|
Offset: -1,
|
|
Counts: []uint64{1, 6, 2},
|
|
},
|
|
NegativeBucket: metricdata.ExponentialBucket{
|
|
Offset: -1,
|
|
Counts: []uint64{1},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
input: []arg[N]{},
|
|
expect: output{
|
|
n: 1,
|
|
agg: metricdata.ExponentialHistogram[N]{
|
|
Temporality: metricdata.CumulativeTemporality,
|
|
DataPoints: []metricdata.ExponentialHistogramDataPoint[N]{
|
|
{
|
|
Attributes: fltrAlice,
|
|
StartTime: y2kPlus(0),
|
|
Time: y2kPlus(4),
|
|
Count: 10,
|
|
Min: metricdata.NewExtrema[N](-1),
|
|
Max: metricdata.NewExtrema[N](16),
|
|
Sum: 43,
|
|
Scale: -1,
|
|
PositiveBucket: metricdata.ExponentialBucket{
|
|
Offset: -1,
|
|
Counts: []uint64{1, 6, 2},
|
|
},
|
|
NegativeBucket: metricdata.ExponentialBucket{
|
|
Offset: -1,
|
|
Counts: []uint64{1},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
input: []arg[N]{
|
|
// These will exceed the cardinality limit.
|
|
{ctx, 4, bob},
|
|
{ctx, 4, bob},
|
|
{ctx, 4, bob},
|
|
{ctx, 2, carol},
|
|
{ctx, 16, carol},
|
|
{ctx, 1, dave},
|
|
},
|
|
expect: output{
|
|
n: 2,
|
|
agg: metricdata.ExponentialHistogram[N]{
|
|
Temporality: metricdata.CumulativeTemporality,
|
|
DataPoints: []metricdata.ExponentialHistogramDataPoint[N]{
|
|
{
|
|
Attributes: fltrAlice,
|
|
StartTime: y2kPlus(0),
|
|
Time: y2kPlus(5),
|
|
Count: 10,
|
|
Min: metricdata.NewExtrema[N](-1),
|
|
Max: metricdata.NewExtrema[N](16),
|
|
Sum: 43,
|
|
Scale: -1,
|
|
PositiveBucket: metricdata.ExponentialBucket{
|
|
Offset: -1,
|
|
Counts: []uint64{1, 6, 2},
|
|
},
|
|
NegativeBucket: metricdata.ExponentialBucket{
|
|
Offset: -1,
|
|
Counts: []uint64{1},
|
|
},
|
|
},
|
|
{
|
|
Attributes: overflowSet,
|
|
StartTime: y2kPlus(0),
|
|
Time: y2kPlus(5),
|
|
Count: 6,
|
|
Min: metricdata.NewExtrema[N](1),
|
|
Max: metricdata.NewExtrema[N](16),
|
|
Sum: 31,
|
|
Scale: -1,
|
|
PositiveBucket: metricdata.ExponentialBucket{
|
|
Offset: -1,
|
|
Counts: []uint64{1, 4, 1},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
func FuzzGetBin(f *testing.F) {
|
|
values := []float64{
|
|
2.0,
|
|
0x1p35,
|
|
0x1.0000000000001p35,
|
|
0x1.fffffffffffffp34,
|
|
0x1p300,
|
|
0x1.0000000000001p300,
|
|
0x1.fffffffffffffp299,
|
|
}
|
|
scales := []int32{0, 15, -5}
|
|
|
|
for _, s := range scales {
|
|
for _, v := range values {
|
|
f.Add(v, s)
|
|
}
|
|
}
|
|
|
|
f.Fuzz(func(t *testing.T, v float64, scale int32) {
|
|
// GetBin only works on positive values.
|
|
if math.Signbit(v) {
|
|
v = v * -1
|
|
}
|
|
// GetBin Doesn't work on zero.
|
|
if v == 0.0 {
|
|
t.Skip("skipping test for zero")
|
|
}
|
|
|
|
p := newExpoHistogramDataPoint[float64](alice, 4, 20, false, false)
|
|
// scale range is -10 to 20.
|
|
p.scale = (scale%31+31)%31 - 10
|
|
got := p.getBin(v)
|
|
if v <= lowerBound(got, p.scale) {
|
|
t.Errorf(
|
|
"v=%x scale =%d had bin %d, but was below lower bound %x",
|
|
v,
|
|
p.scale,
|
|
got,
|
|
lowerBound(got, p.scale),
|
|
)
|
|
}
|
|
if v > lowerBound(got+1, p.scale) {
|
|
t.Errorf(
|
|
"v=%x scale =%d had bin %d, but was above upper bound %x",
|
|
v,
|
|
p.scale,
|
|
got,
|
|
lowerBound(got+1, p.scale),
|
|
)
|
|
}
|
|
})
|
|
}
|
|
|
|
func lowerBound(index, scale int32) float64 {
|
|
// The lowerBound of the index of Math.SmallestNonzeroFloat64 at any scale
|
|
// is always rounded down to 0.0.
|
|
// For example lowerBound(getBin(Math.SmallestNonzeroFloat64, 7), 7) == 0.0
|
|
// 2 ^ (index * 2 ^ (-scale))
|
|
return math.Exp2(math.Ldexp(float64(index), -int(scale)))
|
|
}
|