Skip to content

Commit 6942f9e

Browse files
committed
sparse buckets: Fix handling of +Inf/-Inf/NaN observations
NaN observations now go to no bucket, but increment count (and effectively set sum to NaN, too). ±Inf observations now go to the bucket following the bucket that would have received math.MaxFloat64. The former is now the last bucket that can be created. The getLe is modified to return math.MaxFloat64 for the penultimate possible bucket. Also add a test for getLe. Signed-off-by: beorn7 <[email protected]>
1 parent 95cf173 commit 6942f9e

File tree

2 files changed

+162
-18
lines changed

2 files changed

+162
-18
lines changed

prometheus/histogram.go

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -577,28 +577,37 @@ func (hc *histogramCounts) observe(v float64, bucket int, doSparse bool) {
577577
atomic.AddUint64(&hc.buckets[bucket], 1)
578578
}
579579
atomicAddFloat(&hc.sumBits, v)
580-
if doSparse {
580+
if doSparse && !math.IsNaN(v) {
581581
var (
582-
sparseKey int
583-
sparseSchema = atomic.LoadInt32(&hc.sparseSchema)
584-
sparseZeroThreshold = math.Float64frombits(atomic.LoadUint64(&hc.sparseZeroThresholdBits))
585-
frac, exp = math.Frexp(math.Abs(v))
586-
bucketCreated bool
582+
sparseKey int
583+
sparseSchema = atomic.LoadInt32(&hc.sparseSchema)
584+
sparseZeroThreshold = math.Float64frombits(atomic.LoadUint64(&hc.sparseZeroThresholdBits))
585+
bucketCreated, isInf bool
587586
)
588-
switch {
589-
case math.IsInf(v, 0):
590-
sparseKey = math.MaxInt32 // Largest possible sparseKey.
591-
case sparseSchema > 0:
587+
if math.IsInf(v, 0) {
588+
// Pretend v is MaxFloat64 but later increment sparseKey by one.
589+
if math.IsInf(v, +1) {
590+
v = math.MaxFloat64
591+
} else {
592+
v = -math.MaxFloat64
593+
}
594+
isInf = true
595+
}
596+
frac, exp := math.Frexp(math.Abs(v))
597+
if sparseSchema > 0 {
592598
bounds := sparseBounds[sparseSchema]
593599
sparseKey = sort.SearchFloat64s(bounds, frac) + (exp-1)*len(bounds)
594-
default:
600+
} else {
595601
sparseKey = exp
596602
if frac == 0.5 {
597603
sparseKey--
598604
}
599605
div := 1 << -sparseSchema
600606
sparseKey = (sparseKey + div - 1) / div
601607
}
608+
if isInf {
609+
sparseKey++
610+
}
602611
switch {
603612
case v > sparseZeroThreshold:
604613
bucketCreated = addToSparseBucket(&hc.sparseBucketsPositive, sparseKey, 1)
@@ -1062,7 +1071,8 @@ func (v *HistogramVec) GetMetricWith(labels Labels) (Observer, error) {
10621071
// WithLabelValues works as GetMetricWithLabelValues, but panics where
10631072
// GetMetricWithLabelValues would have returned an error. Not returning an
10641073
// error allows shortcuts like
1065-
// myVec.WithLabelValues("404", "GET").Observe(42.21)
1074+
//
1075+
// myVec.WithLabelValues("404", "GET").Observe(42.21)
10661076
func (v *HistogramVec) WithLabelValues(lvs ...string) Observer {
10671077
h, err := v.GetMetricWithLabelValues(lvs...)
10681078
if err != nil {
@@ -1073,7 +1083,8 @@ func (v *HistogramVec) WithLabelValues(lvs ...string) Observer {
10731083

10741084
// With works as GetMetricWith but panics where GetMetricWithLabels would have
10751085
// returned an error. Not returning an error allows shortcuts like
1076-
// myVec.With(prometheus.Labels{"code": "404", "method": "GET"}).Observe(42.21)
1086+
//
1087+
// myVec.With(prometheus.Labels{"code": "404", "method": "GET"}).Observe(42.21)
10771088
func (v *HistogramVec) With(labels Labels) Observer {
10781089
h, err := v.GetMetricWith(labels)
10791090
if err != nil {
@@ -1219,8 +1230,8 @@ func (s buckSort) Less(i, j int) bool {
12191230
// 2^(2^-n) is less or equal the provided bucketFactor.
12201231
//
12211232
// Special cases:
1222-
// - bucketFactor <= 1: panics.
1223-
// - bucketFactor < 2^(2^-8) (but > 1): still returns 8.
1233+
// - bucketFactor <= 1: panics.
1234+
// - bucketFactor < 2^(2^-8) (but > 1): still returns 8.
12241235
func pickSparseSchema(bucketFactor float64) int32 {
12251236
if bucketFactor <= 1 {
12261237
panic(fmt.Errorf("bucketFactor %f is <=1", bucketFactor))
@@ -1346,13 +1357,55 @@ func findSmallestKey(m *sync.Map) int {
13461357
}
13471358

13481359
func getLe(key int, schema int32) float64 {
1360+
// Here a bit of context about the behavior for the last bucket counting
1361+
// regular numbers (called simply "last bucket" below) and the bucket
1362+
// counting observations of ±Inf (called "inf bucket" below, with a key
1363+
// one higher than that of the "last bucket"):
1364+
//
1365+
// If we apply the usual formula to the last bucket, its upper bound
1366+
// would be calculated as +Inf. The reason is that the max possible
1367+
// regular float64 number (math.MaxFloat64) doesn't coincide with one of
1368+
// the calculated bucket boundaries. So the calculated boundary has to
1369+
// be larger than math.MaxFloat64, and the only float64 larger than
1370+
// math.MaxFloat64 is +Inf. However, we want to count actual
1371+
// observations of ±Inf in the inf bucket. Therefore, we have to treat
1372+
// the upper bound of the last bucket specially and set it to
1373+
// math.MaxFloat64. (The upper bound of the inf bucket, with its key
1374+
// being one higher than that of the last bucket, naturally comes out as
1375+
// +Inf by the usual formula. So that's fine.)
1376+
//
1377+
// math.MaxFloat64 has a frac of 0.9999999999999999 and an exp of
1378+
// 1024. If there were a float64 number following math.MaxFloat64, it
1379+
// would have a frac of 1.0 and an exp of 1024, or equivalently a frac
1380+
// of 0.5 and an exp of 1025. However, since frac must be smaller than
1381+
// 1, and exp must be smaller than 1025, either representation overflows
1382+
// a float64. (Which, in turn, is the reason that math.MaxFloat64 is the
1383+
// largest possible float64. Q.E.D.) However, the formula for
1384+
// calculating the upper bound from the idx and schema of the last
1385+
// bucket results in precisely that. It is either frac=1.0 & exp=1024
1386+
// (for schema < 0) or frac=0.5 & exp=1025 (for schema >=0). (This is,
1387+
// by the way, a power of two where the exponent itself is a power of
1388+
// two, 2¹⁰ in fact, which coinicides with a bucket boundary in all
1389+
// schemas.) So these are the special cases we have to catch below.
13491390
if schema < 0 {
1350-
return math.Ldexp(1, key<<(-schema))
1391+
exp := key << -schema
1392+
if exp == 1024 {
1393+
// This is the last bucket before the overflow bucket
1394+
// (for ±Inf observations). Return math.MaxFloat64 as
1395+
// explained above.
1396+
return math.MaxFloat64
1397+
}
1398+
return math.Ldexp(1, exp)
13511399
}
13521400

13531401
fracIdx := key & ((1 << schema) - 1)
13541402
frac := sparseBounds[schema][fracIdx]
13551403
exp := (key >> schema) + 1
1404+
if frac == 0.5 && exp == 1025 {
1405+
// This is the last bucket before the overflow bucket (for ±Inf
1406+
// observations). Return math.MaxFloat64 as explained above.
1407+
return math.MaxFloat64
1408+
}
13561409
return math.Ldexp(frac, exp)
13571410
}
13581411

prometheus/histogram_test.go

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -548,13 +548,13 @@ func TestSparseHistogram(t *testing.T) {
548548
name: "+Inf observation",
549549
observations: []float64{0, 1, 1.2, 1.4, 1.8, 2, math.Inf(+1)},
550550
factor: 1.2,
551-
want: `sample_count:7 sample_sum:inf schema:2 zero_threshold:2.938735877055719e-39 zero_count:1 positive_span:<offset:0 length:5 > positive_span:<offset:2147483642 length:1 > positive_delta:1 positive_delta:-1 positive_delta:2 positive_delta:-2 positive_delta:2 positive_delta:-1 `,
551+
want: `sample_count:7 sample_sum:inf schema:2 zero_threshold:2.938735877055719e-39 zero_count:1 positive_span:<offset:0 length:5 > positive_span:<offset:4092 length:1 > positive_delta:1 positive_delta:-1 positive_delta:2 positive_delta:-2 positive_delta:2 positive_delta:-1 `,
552552
},
553553
{
554554
name: "-Inf observation",
555555
observations: []float64{0, 1, 1.2, 1.4, 1.8, 2, math.Inf(-1)},
556556
factor: 1.2,
557-
want: `sample_count:7 sample_sum:-inf schema:2 zero_threshold:2.938735877055719e-39 zero_count:1 negative_span:<offset:2147483647 length:1 > negative_delta:1 positive_span:<offset:0 length:5 > positive_delta:1 positive_delta:-1 positive_delta:2 positive_delta:-2 positive_delta:2 `,
557+
want: `sample_count:7 sample_sum:-inf schema:2 zero_threshold:2.938735877055719e-39 zero_count:1 negative_span:<offset:4097 length:1 > negative_delta:1 positive_span:<offset:0 length:5 > positive_delta:1 positive_delta:-1 positive_delta:2 positive_delta:-2 positive_delta:2 `,
558558
},
559559
{
560560
name: "limited buckets but nothing triggered",
@@ -782,3 +782,94 @@ func TestSparseHistogramConcurrency(t *testing.T) {
782782
t.Error(err)
783783
}
784784
}
785+
786+
func TestGetLe(t *testing.T) {
787+
scenarios := []struct {
788+
key int
789+
schema int32
790+
want float64
791+
}{
792+
{
793+
key: -1,
794+
schema: -1,
795+
want: 0.25,
796+
},
797+
{
798+
key: 0,
799+
schema: -1,
800+
want: 1,
801+
},
802+
{
803+
key: 1,
804+
schema: -1,
805+
want: 4,
806+
},
807+
{
808+
key: 512,
809+
schema: -1,
810+
want: math.MaxFloat64,
811+
},
812+
{
813+
key: 513,
814+
schema: -1,
815+
want: math.Inf(+1),
816+
},
817+
{
818+
key: -1,
819+
schema: 0,
820+
want: 0.5,
821+
},
822+
{
823+
key: 0,
824+
schema: 0,
825+
want: 1,
826+
},
827+
{
828+
key: 1,
829+
schema: 0,
830+
want: 2,
831+
},
832+
{
833+
key: 1024,
834+
schema: 0,
835+
want: math.MaxFloat64,
836+
},
837+
{
838+
key: 1025,
839+
schema: 0,
840+
want: math.Inf(+1),
841+
},
842+
{
843+
key: -1,
844+
schema: 2,
845+
want: 0.8408964152537144,
846+
},
847+
{
848+
key: 0,
849+
schema: 2,
850+
want: 1,
851+
},
852+
{
853+
key: 1,
854+
schema: 2,
855+
want: 1.189207115002721,
856+
},
857+
{
858+
key: 4096,
859+
schema: 2,
860+
want: math.MaxFloat64,
861+
},
862+
{
863+
key: 4097,
864+
schema: 2,
865+
want: math.Inf(+1),
866+
},
867+
}
868+
869+
for i, s := range scenarios {
870+
got := getLe(s.key, s.schema)
871+
if s.want != got {
872+
t.Errorf("%d. key %d, schema %d, want upper bound of %g, got %g", i, s.key, s.schema, s.want, got)
873+
}
874+
}
875+
}

0 commit comments

Comments
 (0)