Skip to content

Commit 25bc188

Browse files
authored
Merge pull request #1144 from prometheus/beorn7/histogram2
sparse buckets: Fix handling of +Inf/-Inf/NaN observations
2 parents 95cf173 + 6942f9e commit 25bc188

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)