From ed02613122b4c9a4f7a6330af65cef9ede47996a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joan=20L=C3=B3pez=20de=20la=20Franca=20Beltran?= <5459617+joanlopez@users.noreply.github.com> Date: Mon, 10 Mar 2025 11:09:51 +0100 Subject: [PATCH] Extract HDR histogram implementation into a shared package (#4611) --- internal/ds/histogram/doc.go | 2 + internal/ds/histogram/hdr.go | 162 +++++++++++++++++++ internal/ds/histogram/hdr_test.go | 258 ++++++++++++++++++++++++++++++ output/cloud/expv2/hdr.go | 164 +------------------ output/cloud/expv2/hdr_test.go | 258 +----------------------------- output/cloud/expv2/mapping.go | 6 +- output/cloud/expv2/sink.go | 3 +- output/cloud/expv2/sink_test.go | 4 +- 8 files changed, 438 insertions(+), 419 deletions(-) create mode 100644 internal/ds/histogram/doc.go create mode 100644 internal/ds/histogram/hdr.go create mode 100644 internal/ds/histogram/hdr_test.go diff --git a/internal/ds/histogram/doc.go b/internal/ds/histogram/doc.go new file mode 100644 index 00000000000..880fec794ab --- /dev/null +++ b/internal/ds/histogram/doc.go @@ -0,0 +1,2 @@ +// Package histogram provides histogram implementations that are used to track the distribution of metrics. +package histogram diff --git a/internal/ds/histogram/hdr.go b/internal/ds/histogram/hdr.go new file mode 100644 index 00000000000..514475562d9 --- /dev/null +++ b/internal/ds/histogram/hdr.go @@ -0,0 +1,162 @@ +package histogram + +import ( + "math" + "math/bits" +) + +const ( + // defaultMinimumResolution is the default resolution used by Hdr. + // It allows to have a higher granularity compared to the basic 1.0 value, + // supporting floating points up to 3 digits. + defaultMinimumResolution = .001 + + // lowestTrackable represents the minimum value that the Hdr tracks. + // Essentially, it excludes negative numbers. + // Most of the metrics tracked by histograms are durations + // where we don't expect negative numbers. + lowestTrackable = 0 +) + +// Hdr represents a distribution of metrics samples' values as histogram. +// +// A Hdr is the representation of base-2 exponential histogram with two layers. +// The first layer has primary buckets in the form of a power of two, and a second layer of buckets +// for each primary bucket with an equally distributed amount of buckets inside. +// +// Hdr has a series of (N * 2^m) buckets, where: +// N = a power of 2 that defines the number of primary buckets +// m = a power of 2 that defines the number of the secondary buckets +// The current version is: f(N = 25, m = 7) = 3200. +type Hdr struct { + // Buckets stores the counters for each bin of the histogram. + // It does not include counters for the untrackable values, + // because they contain exception cases and require to be tracked in a dedicated way. + Buckets map[uint32]uint32 + + // ExtraLowBucket counts occurrences of observed values smaller + // than the minimum trackable value. + ExtraLowBucket uint32 + + // ExtraHighBucket counts occurrences of observed values bigger + // than the maximum trackable value. + ExtraHighBucket uint32 + + // Max is the absolute observed maximum value. + Max float64 + + // Min is the absolute observed minimum value. + Min float64 + + // Sum is the sum of all observed values. + Sum float64 + + // Count is counts the amount of observed values. + Count uint32 + + // MinimumResolution represents resolution used by Hdr. + // In principle, it is a multiplier factor for the tracked values. + MinimumResolution float64 +} + +// NewHdr creates a new Hdr histogram with default settings. +func NewHdr() *Hdr { + return &Hdr{ + MinimumResolution: defaultMinimumResolution, + Buckets: make(map[uint32]uint32), + Max: -math.MaxFloat64, + Min: math.MaxFloat64, + } +} + +// Add adds a value to the Hdr histogram. +func (h *Hdr) Add(v float64) { + h.addToBucket(v) +} + +// addToBucket increments the counter of the bucket of the provided value. +// If the value is lower or higher than the trackable limits +// then it is counted into specific buckets. All the stats are also updated accordingly. +func (h *Hdr) addToBucket(v float64) { + if v > h.Max { + h.Max = v + } + if v < h.Min { + h.Min = v + } + + h.Count++ + h.Sum += v + + v /= h.MinimumResolution + + if v < lowestTrackable { + h.ExtraLowBucket++ + return + } + if v > math.MaxInt64 { + h.ExtraHighBucket++ + return + } + + h.Buckets[resolveBucketIndex(v)]++ +} + +// resolveBucketIndex returns the index +// of the bucket in the histogram for the provided value. +func resolveBucketIndex(val float64) uint32 { + if val < lowestTrackable { + return 0 + } + + // We upscale to the next integer to ensure that each sample falls + // within a specific bucket, even when the value is fractional. + // This avoids under-representing the distribution in the Hdr histogram. + upscaled := uint64(math.Ceil(val)) + + // In Hdr histograms, bucket boundaries are usually defined as multiples of powers of 2, + // allowing for efficient computation of bucket indexes. + // + // We define k=7 in our case, because it allows for sufficient granularity in the + // distribution (2^7=128 primary buckets of which each can be further + // subdivided if needed). + // + // k is the constant balancing factor between granularity and + // computational efficiency. + // + // In our case: + // i.e 2^7 = 128 ~ 100 = 10^2 + // 2^10 = 1024 ~ 1000 = 10^3 + // f(x) = 3*x + 1 - empiric formula that works for us + // since f(2)=7 and f(3)=10 + const k = uint64(7) + + // 256 = 1 << (k+1) + if upscaled < 256 { + return uint32(upscaled) + } + + // `nkdiff` helps us find the right bucket for `upscaled`. It does so by determining the + // index for the "major" bucket (a set of values within a power of two range) and then + // the "sub" bucket within that major bucket. This system provides us with a fine level + // of granularity within a computationally efficient bucketing system. The result is a + // histogram that provides a detailed representation of the distribution of values. + // + // Here we use some math to get simple formula + // derivation: + // let u = upscaled + // let n = msb(u) - most significant digit position + // i.e. n = floor(log(u, 2)) + // major_bucket_index = n - k + 1 + // sub_bucket_index = u>>(n - k) - (1<>(n-k) - (1<>(n-k) + // + nkdiff := uint64(bits.Len64(upscaled>>k)) - 1 //nolint:gosec // msb index + + // We cast safely downscaling because we don't expect we may hit the uint32 limit + // with the bucket index. The bucket represented from the index as MaxUint32 + // would be a very huge number bigger than the trackable limits. + return uint32((nkdiff << k) + (upscaled >> nkdiff)) //nolint:gosec +} diff --git a/internal/ds/histogram/hdr_test.go b/internal/ds/histogram/hdr_test.go new file mode 100644 index 00000000000..5c372b1c7f1 --- /dev/null +++ b/internal/ds/histogram/hdr_test.go @@ -0,0 +1,258 @@ +package histogram + +import ( + "math" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestResolveBucketIndex(t *testing.T) { + t.Parallel() + + tests := []struct { + in float64 + exp uint32 + }{ + {in: -1029, exp: 0}, + {in: -12, exp: 0}, + {in: -0.82673, exp: 0}, + {in: 0, exp: 0}, + {in: 0.12, exp: 1}, + {in: 1.91, exp: 2}, + {in: 10, exp: 10}, + {in: 12, exp: 12}, + {in: 12.5, exp: 13}, + {in: 20, exp: 20}, + {in: 255, exp: 255}, + {in: 256, exp: 256}, + {in: 282.29, exp: 269}, + {in: 1029, exp: 512}, + {in: 39751, exp: 1179}, + {in: 100000, exp: 1347}, + {in: 182272, exp: 1458}, + {in: 183000, exp: 1458}, + {in: 184000, exp: 1459}, + {in: 200000, exp: 1475}, + + {in: 1 << 20, exp: 1792}, + {in: (1 << 30) - 1, exp: 3071}, + {in: 1 << 30, exp: 3072}, + {in: 1 << 40, exp: 4352}, + {in: 1 << 62, exp: 7168}, + + {in: math.MaxInt32, exp: 3199}, // 2B + {in: math.MaxUint32, exp: 3327}, // 4B + {in: math.MaxInt64, exp: 7296}, // Huge number // 9.22...e+18 + {in: math.MaxInt64 + 2000, exp: 7296}, // Assert that it does not overflow + } + for _, tc := range tests { + assert.Equal(t, int(tc.exp), int(resolveBucketIndex(tc.in)), tc.in) + } +} + +func TestHistogramAddWithSimpleValues(t *testing.T) { + t.Parallel() + + cases := []struct { + vals []float64 + exp *Hdr + }{ + { + vals: []float64{0}, + exp: &Hdr{ + Buckets: map[uint32]uint32{0: 1}, + ExtraLowBucket: 0, + ExtraHighBucket: 0, + Max: 0, + Min: 0, + Sum: 0, + Count: 1, + }, + }, + { + vals: []float64{8, 5}, + exp: &Hdr{ + Buckets: map[uint32]uint32{5: 1, 8: 1}, + ExtraLowBucket: 0, + ExtraHighBucket: 0, + Max: 8, + Min: 5, + Sum: 13, + Count: 2, + }, + }, + { + vals: []float64{8, 9, 10, 5}, + exp: &Hdr{ + Buckets: map[uint32]uint32{8: 1, 9: 1, 10: 1, 5: 1}, + ExtraLowBucket: 0, + ExtraHighBucket: 0, + Max: 10, + Min: 5, + Sum: 32, + Count: 4, + }, + }, + { + vals: []float64{100, 101}, + exp: &Hdr{ + Buckets: map[uint32]uint32{100: 1, 101: 1}, + ExtraLowBucket: 0, + ExtraHighBucket: 0, + Max: 101, + Min: 100, + Sum: 201, + Count: 2, + }, + }, + { + vals: []float64{101, 100}, + exp: &Hdr{ + Buckets: map[uint32]uint32{100: 1, 101: 1}, + ExtraLowBucket: 0, + ExtraHighBucket: 0, + Max: 101, + Min: 100, + Sum: 201, + Count: 2, + }, + }, + } + + for i, tc := range cases { + tc := tc + t.Run(strconv.Itoa(i), func(t *testing.T) { + t.Parallel() + h := NewHdr() + // We use a lower resolution instead of the default + // so we can keep smaller numbers in this test + h.MinimumResolution = 1.0 + for _, v := range tc.vals { + h.Add(v) + } + tc.exp.MinimumResolution = 1.0 + assert.Equal(t, tc.exp, h) + }) + } +} + +func TestHistogramAddWithUntrackables(t *testing.T) { + t.Parallel() + + h := NewHdr() + h.MinimumResolution = 1.0 + for _, v := range []float64{5, -3.14, math.MaxInt64 + 3239, 1} { + h.Add(v) + } + + exp := &Hdr{ + Buckets: map[uint32]uint32{1: 1, 5: 1}, + ExtraLowBucket: 1, + ExtraHighBucket: 1, + Max: 9223372036854779046, + Min: -3.14, + Sum: math.MaxInt64 + 3239 + 5 + 1 - 3.14, + Count: 4, + MinimumResolution: 1.0, + } + assert.Equal(t, exp, h) +} + +func TestHistogramAddWithMultipleOccurances(t *testing.T) { + t.Parallel() + + h := NewHdr() + h.MinimumResolution = 1.0 + for _, v := range []float64{51.8, 103.6, 103.6, 103.6, 103.6} { + h.Add(v) + } + + exp := &Hdr{ + Buckets: map[uint32]uint32{52: 1, 104: 4}, + Max: 103.6, + Min: 51.8, + ExtraLowBucket: 0, + ExtraHighBucket: 0, + Sum: 466.20000000000005, + Count: 5, + } + exp.MinimumResolution = 1.0 + assert.Equal(t, exp, h) +} + +func TestHistogramAddWithNegativeNum(t *testing.T) { + t.Parallel() + + h := NewHdr() + h.Add(-2.42314) + + exp := &Hdr{ + Max: -2.42314, + Min: -2.42314, + Buckets: map[uint32]uint32{}, + ExtraLowBucket: 1, + ExtraHighBucket: 0, + Sum: -2.42314, + Count: 1, + MinimumResolution: .001, + } + assert.Equal(t, exp, h) +} + +func TestHistogramAddWithMultipleNegativeNums(t *testing.T) { + t.Parallel() + h := NewHdr() + for _, v := range []float64{-0.001, -0.001, -0.001} { + h.Add(v) + } + + exp := &Hdr{ + Buckets: map[uint32]uint32{}, + ExtraLowBucket: 3, + ExtraHighBucket: 0, + Max: -0.001, + Min: -0.001, + Sum: -0.003, + Count: 3, + MinimumResolution: .001, + } + assert.Equal(t, exp, h) +} + +func TestHistogramAddWithZeroToOneValues(t *testing.T) { + t.Parallel() + h := NewHdr() + for _, v := range []float64{0.000052, 0.002115, 0.012013, 0.05017, 0.250, 0.54, 0.541, 0.803} { + h.Add(v) + } + + exp := &Hdr{ + Buckets: map[uint32]uint32{1: 1, 3: 1, 13: 1, 51: 1, 250: 1, 391: 2, 456: 1}, + ExtraLowBucket: 0, + ExtraHighBucket: 0, + Max: .803, + Min: .000052, + Sum: 2.19835, + Count: 8, + MinimumResolution: .001, + } + assert.Equal(t, exp, h) +} + +func TestNewHistogram(t *testing.T) { + t.Parallel() + + h := NewHdr() + exp := &Hdr{ + Buckets: map[uint32]uint32{}, + ExtraLowBucket: 0, + ExtraHighBucket: 0, + Max: -math.MaxFloat64, + Min: math.MaxFloat64, + Sum: 0, + MinimumResolution: .001, + } + assert.Equal(t, exp, h) +} diff --git a/output/cloud/expv2/hdr.go b/output/cloud/expv2/hdr.go index acb750d56d9..0212f33da11 100644 --- a/output/cloud/expv2/hdr.go +++ b/output/cloud/expv2/hdr.go @@ -1,110 +1,14 @@ package expv2 import ( - "math" - "math/bits" "sort" + "go.k6.io/k6/internal/ds/histogram" "go.k6.io/k6/internal/output/cloud/expv2/pbcloud" ) -const ( - // defaultMinimumResolution is the default resolution used by histogram. - // It allows to have a higher granularity compared to the basic 1.0 value, - // supporting floating points up to 3 digits. - defaultMinimumResolution = .001 - - // lowestTrackable represents the minimum value that the histogram tracks. - // Essentially, it excludes negative numbers. - // Most of metrics tracked by histograms are durations - // where we don't expect negative numbers. - // - // In the future, we may expand and include them, - // probably after https://github.com/grafana/k6/issues/763. - lowestTrackable = 0 -) - -// histogram represents a distribution -// of metrics samples' values as histogram. -// -// The histogram is the representation of base-2 exponential Histogram with two layers. -// The first layer has primary buckets in the form of a power of two, and a second layer of buckets -// for each primary bucket with an equally distributed amount of buckets inside. -// -// The histogram has a series of (N * 2^m) buckets, where: -// N = a power of 2 that defines the number of primary buckets -// m = a power of 2 that defines the number of the secondary buckets -// The current version is: f(N = 25, m = 7) = 3200. -type histogram struct { - // Buckets stores the counters for each bin of the histogram. - // It does not include counters for the untrackable values, - // because they contain exception cases and require to be tracked in a dedicated way. - Buckets map[uint32]uint32 - - // ExtraLowBucket counts occurrences of observed values smaller - // than the minimum trackable value. - ExtraLowBucket uint32 - - // ExtraLowBucket counts occurrences of observed values bigger - // than the maximum trackable value. - ExtraHighBucket uint32 - - // Max is the absolute maximum observed value. - Max float64 - - // Min is the absolute minimum observed value. - Min float64 - - // Sum is the sum of all observed values. - Sum float64 - - // Count is counts the amount of observed values. - Count uint32 - - // MinimumResolution represents resolution used by Histogram. - // In principle, it is a multiplier factor for the tracked values. - MinimumResolution float64 -} - -func newHistogram() *histogram { - return &histogram{ - MinimumResolution: defaultMinimumResolution, - Buckets: make(map[uint32]uint32), - Max: -math.MaxFloat64, - Min: math.MaxFloat64, - } -} - -// addToBucket increments the counter of the bucket of the provided value. -// If the value is lower or higher than the trackable limits -// then it is counted into specific buckets. All the stats are also updated accordingly. -func (h *histogram) addToBucket(v float64) { - if v > h.Max { - h.Max = v - } - if v < h.Min { - h.Min = v - } - - h.Count++ - h.Sum += v - - v /= h.MinimumResolution - - if v < lowestTrackable { - h.ExtraLowBucket++ - return - } - if v > math.MaxInt64 { - h.ExtraHighBucket++ - return - } - - h.Buckets[resolveBucketIndex(v)]++ -} - // histogramAsProto converts the histogram into the equivalent Protobuf version. -func histogramAsProto(h *histogram, time int64) *pbcloud.TrendHdrValue { +func histogramAsProto(h *histogram.Hdr, time int64) *pbcloud.TrendHdrValue { var ( indexes []uint32 counters []uint32 @@ -160,67 +64,3 @@ func histogramAsProto(h *histogram, time int64) *pbcloud.TrendHdrValue { } return hval } - -// resolveBucketIndex returns the index -// of the bucket in the histogram for the provided value. -func resolveBucketIndex(val float64) uint32 { - if val < lowestTrackable { - return 0 - } - - // We upscale to the next integer to ensure that each sample falls - // within a specific bucket, even when the value is fractional. - // This avoids under-representing the distribution in the histogram. - upscaled := uint64(math.Ceil(val)) - - // In histograms, bucket boundaries are usually defined as multiples of powers of 2, - // allowing for efficient computation of bucket indexes. - // - // We define k=7 in our case, because it allows for sufficient granularity in the - // distribution (2^7=128 primary buckets of which each can be further - // subdivided if needed). - // - // k is the constant balancing factor between granularity and - // computational efficiency. - // - // In our case: - // i.e 2^7 = 128 ~ 100 = 10^2 - // 2^10 = 1024 ~ 1000 = 10^3 - // f(x) = 3*x + 1 - empiric formula that works for us - // since f(2)=7 and f(3)=10 - const k = uint64(7) - - // 256 = 1 << (k+1) - if upscaled < 256 { - return uint32(upscaled) - } - - // `nkdiff` helps us find the right bucket for `upscaled`. It does so by determining the - // index for the "major" bucket (a set of values within a power of two range) and then - // the "sub" bucket within that major bucket. This system provides us with a fine level - // of granularity within a computationally efficient bucketing system. The result is a - // histogram that provides a detailed representation of the distribution of values. - // - // Here we use some math to get simple formula - // derivation: - // let u = upscaled - // let n = msb(u) - most significant digit position - // i.e. n = floor(log(u, 2)) - // major_bucket_index = n - k + 1 - // sub_bucket_index = u>>(n - k) - (1<>(n-k) - (1<>(n-k) - // - nkdiff := uint64(bits.Len64(upscaled>>k)) - 1 //nolint:gosec // msb index - - // We cast safely downscaling because we don't expect we may hit the uint32 limit - // with the bucket index. The bucket represented from the index as MaxUint32 - // would be a very huge number bigger than the trackable limits. - return uint32((nkdiff << k) + (upscaled >> nkdiff)) //nolint:gosec -} - -// Add implements the metricValue interface. -func (h *histogram) Add(v float64) { - h.addToBucket(v) -} diff --git a/output/cloud/expv2/hdr_test.go b/output/cloud/expv2/hdr_test.go index c17811ff920..d2187acc5a5 100644 --- a/output/cloud/expv2/hdr_test.go +++ b/output/cloud/expv2/hdr_test.go @@ -2,264 +2,16 @@ package expv2 import ( "math" - "strconv" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.k6.io/k6/internal/output/cloud/expv2/pbcloud" "google.golang.org/protobuf/types/known/timestamppb" -) - -func TestResolveBucketIndex(t *testing.T) { - t.Parallel() - - tests := []struct { - in float64 - exp uint32 - }{ - {in: -1029, exp: 0}, - {in: -12, exp: 0}, - {in: -0.82673, exp: 0}, - {in: 0, exp: 0}, - {in: 0.12, exp: 1}, - {in: 1.91, exp: 2}, - {in: 10, exp: 10}, - {in: 12, exp: 12}, - {in: 12.5, exp: 13}, - {in: 20, exp: 20}, - {in: 255, exp: 255}, - {in: 256, exp: 256}, - {in: 282.29, exp: 269}, - {in: 1029, exp: 512}, - {in: 39751, exp: 1179}, - {in: 100000, exp: 1347}, - {in: 182272, exp: 1458}, - {in: 183000, exp: 1458}, - {in: 184000, exp: 1459}, - {in: 200000, exp: 1475}, - - {in: 1 << 20, exp: 1792}, - {in: (1 << 30) - 1, exp: 3071}, - {in: 1 << 30, exp: 3072}, - {in: 1 << 40, exp: 4352}, - {in: 1 << 62, exp: 7168}, - - {in: math.MaxInt32, exp: 3199}, // 2B - {in: math.MaxUint32, exp: 3327}, // 4B - {in: math.MaxInt64, exp: 7296}, // Huge number // 9.22...e+18 - {in: math.MaxInt64 + 2000, exp: 7296}, // Assert that it does not overflow - } - for _, tc := range tests { - assert.Equal(t, int(tc.exp), int(resolveBucketIndex(tc.in)), tc.in) - } -} - -func TestHistogramAddWithSimpleValues(t *testing.T) { - t.Parallel() - - cases := []struct { - vals []float64 - exp histogram - }{ - { - vals: []float64{0}, - exp: histogram{ - Buckets: map[uint32]uint32{0: 1}, - ExtraLowBucket: 0, - ExtraHighBucket: 0, - Max: 0, - Min: 0, - Sum: 0, - Count: 1, - }, - }, - { - vals: []float64{8, 5}, - exp: histogram{ - Buckets: map[uint32]uint32{5: 1, 8: 1}, - ExtraLowBucket: 0, - ExtraHighBucket: 0, - Max: 8, - Min: 5, - Sum: 13, - Count: 2, - }, - }, - { - vals: []float64{8, 9, 10, 5}, - exp: histogram{ - Buckets: map[uint32]uint32{8: 1, 9: 1, 10: 1, 5: 1}, - ExtraLowBucket: 0, - ExtraHighBucket: 0, - Max: 10, - Min: 5, - Sum: 32, - Count: 4, - }, - }, - { - vals: []float64{100, 101}, - exp: histogram{ - Buckets: map[uint32]uint32{100: 1, 101: 1}, - ExtraLowBucket: 0, - ExtraHighBucket: 0, - Max: 101, - Min: 100, - Sum: 201, - Count: 2, - }, - }, - { - vals: []float64{101, 100}, - exp: histogram{ - Buckets: map[uint32]uint32{100: 1, 101: 1}, - ExtraLowBucket: 0, - ExtraHighBucket: 0, - Max: 101, - Min: 100, - Sum: 201, - Count: 2, - }, - }, - } - for i, tc := range cases { - tc := tc - t.Run(strconv.Itoa(i), func(t *testing.T) { - t.Parallel() - h := newHistogram() - // We use a lower resolution instead of the default - // so we can keep smaller numbers in this test - h.MinimumResolution = 1.0 - for _, v := range tc.vals { - h.Add(v) - } - tc.exp.MinimumResolution = 1.0 - assert.Equal(t, &tc.exp, h) - }) - } -} - -func TestHistogramAddWithUntrackables(t *testing.T) { - t.Parallel() - - h := newHistogram() - h.MinimumResolution = 1.0 - for _, v := range []float64{5, -3.14, math.MaxInt64 + 3239, 1} { - h.Add(v) - } - - exp := &histogram{ - Buckets: map[uint32]uint32{1: 1, 5: 1}, - ExtraLowBucket: 1, - ExtraHighBucket: 1, - Max: 9223372036854779046, - Min: -3.14, - Sum: math.MaxInt64 + 3239 + 5 + 1 - 3.14, - Count: 4, - MinimumResolution: 1.0, - } - assert.Equal(t, exp, h) -} - -func TestHistogramAddWithMultipleOccurances(t *testing.T) { - t.Parallel() - - h := newHistogram() - h.MinimumResolution = 1.0 - for _, v := range []float64{51.8, 103.6, 103.6, 103.6, 103.6} { - h.Add(v) - } - - exp := &histogram{ - Buckets: map[uint32]uint32{52: 1, 104: 4}, - Max: 103.6, - Min: 51.8, - ExtraLowBucket: 0, - ExtraHighBucket: 0, - Sum: 466.20000000000005, - Count: 5, - } - exp.MinimumResolution = 1.0 - assert.Equal(t, exp, h) -} - -func TestHistogramAddWithNegativeNum(t *testing.T) { - t.Parallel() - - h := newHistogram() - h.Add(-2.42314) - - exp := &histogram{ - Max: -2.42314, - Min: -2.42314, - Buckets: map[uint32]uint32{}, - ExtraLowBucket: 1, - ExtraHighBucket: 0, - Sum: -2.42314, - Count: 1, - MinimumResolution: .001, - } - assert.Equal(t, exp, h) -} - -func TestHistogramAddWithMultipleNegativeNums(t *testing.T) { - t.Parallel() - h := newHistogram() - for _, v := range []float64{-0.001, -0.001, -0.001} { - h.Add(v) - } - - exp := &histogram{ - Buckets: map[uint32]uint32{}, - ExtraLowBucket: 3, - ExtraHighBucket: 0, - Max: -0.001, - Min: -0.001, - Sum: -0.003, - Count: 3, - MinimumResolution: .001, - } - assert.Equal(t, exp, h) -} - -func TestHistogramAddWithZeroToOneValues(t *testing.T) { - t.Parallel() - h := newHistogram() - for _, v := range []float64{0.000052, 0.002115, 0.012013, 0.05017, 0.250, 0.54, 0.541, 0.803} { - h.Add(v) - } - - exp := &histogram{ - Buckets: map[uint32]uint32{1: 1, 3: 1, 13: 1, 51: 1, 250: 1, 391: 2, 456: 1}, - ExtraLowBucket: 0, - ExtraHighBucket: 0, - Max: .803, - Min: .000052, - Sum: 2.19835, - Count: 8, - MinimumResolution: .001, - } - assert.Equal(t, exp, h) -} - -func TestNewHistoram(t *testing.T) { - t.Parallel() - - h := newHistogram() - exp := &histogram{ - Buckets: map[uint32]uint32{}, - ExtraLowBucket: 0, - ExtraHighBucket: 0, - Max: -math.MaxFloat64, - Min: math.MaxFloat64, - Sum: 0, - MinimumResolution: .001, - } - assert.Equal(t, exp, h) -} + "go.k6.io/k6/internal/ds/histogram" + "go.k6.io/k6/internal/output/cloud/expv2/pbcloud" +) func TestHistogramAsProto(t *testing.T) { t.Parallel() @@ -462,7 +214,7 @@ func TestHistogramAsProto(t *testing.T) { Count: 3, ExtraLowValuesCounter: nil, ExtraHighValuesCounter: nil, - MinResolution: defaultMinimumResolution, + MinResolution: .001, Counters: []uint32{1, 2}, Spans: []*pbcloud.BucketSpan{ { @@ -486,7 +238,7 @@ func TestHistogramAsProto(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - h := newHistogram() + h := histogram.NewHdr() h.MinimumResolution = tc.minResolution for _, v := range tc.vals { diff --git a/output/cloud/expv2/mapping.go b/output/cloud/expv2/mapping.go index 5e5585880ce..1c5cc9d441b 100644 --- a/output/cloud/expv2/mapping.go +++ b/output/cloud/expv2/mapping.go @@ -5,9 +5,11 @@ import ( "strings" "github.com/mstoykov/atlas" + "google.golang.org/protobuf/types/known/timestamppb" + + "go.k6.io/k6/internal/ds/histogram" "go.k6.io/k6/internal/output/cloud/expv2/pbcloud" "go.k6.io/k6/metrics" - "google.golang.org/protobuf/types/known/timestamppb" ) // TODO: unit test @@ -99,7 +101,7 @@ func addBucketToTimeSeriesProto( NonzeroCount: typedMetricValue.NonZeroCount, TotalCount: typedMetricValue.Total, }) - case *histogram: + case *histogram.Hdr: samples := timeSeries.GetTrendHdrSamples() samples.Values = append(samples.Values, histogramAsProto(typedMetricValue, time)) default: diff --git a/output/cloud/expv2/sink.go b/output/cloud/expv2/sink.go index 185453c0bbc..97423a3ef8c 100644 --- a/output/cloud/expv2/sink.go +++ b/output/cloud/expv2/sink.go @@ -3,6 +3,7 @@ package expv2 import ( "fmt" + "go.k6.io/k6/internal/ds/histogram" "go.k6.io/k6/metrics" ) @@ -22,7 +23,7 @@ func newMetricValue(mt metrics.MetricType) metricValue { case metrics.Rate: am = &rate{} case metrics.Trend: - am = newHistogram() + am = histogram.NewHdr() default: // Should not be possible to create // an invalid metric type except for specific diff --git a/output/cloud/expv2/sink_test.go b/output/cloud/expv2/sink_test.go index 5d0fa243ee6..a78baa1054b 100644 --- a/output/cloud/expv2/sink_test.go +++ b/output/cloud/expv2/sink_test.go @@ -4,6 +4,8 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "go.k6.io/k6/internal/ds/histogram" "go.k6.io/k6/metrics" ) @@ -16,7 +18,7 @@ func TestNewSink(t *testing.T) { {metrics.Counter, &counter{}}, {metrics.Gauge, &gauge{}}, {metrics.Rate, &rate{}}, - {metrics.Trend, newHistogram()}, + {metrics.Trend, histogram.NewHdr()}, } for _, tc := range tests { assert.Equal(t, tc.exp, newMetricValue(tc.mt))