2015-03-30 17:13:36 +00:00
|
|
|
|
// Copyright 2015 The Prometheus Authors
|
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
|
//
|
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
//
|
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
|
// limitations under the License.
|
|
|
|
|
|
|
|
|
|
package promql
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"math"
|
2024-01-15 16:24:46 +00:00
|
|
|
|
"slices"
|
2015-03-30 17:13:36 +00:00
|
|
|
|
"sort"
|
|
|
|
|
|
2021-12-06 13:47:22 +00:00
|
|
|
|
"github.com/prometheus/prometheus/model/histogram"
|
2021-11-08 14:23:17 +00:00
|
|
|
|
"github.com/prometheus/prometheus/model/labels"
|
2024-04-29 11:37:32 +00:00
|
|
|
|
"github.com/prometheus/prometheus/util/almost"
|
2015-03-30 17:13:36 +00:00
|
|
|
|
)
|
|
|
|
|
|
2023-11-24 23:05:38 +00:00
|
|
|
|
// smallDeltaTolerance is the threshold for relative deltas between classic
|
|
|
|
|
// histogram buckets that will be ignored by the histogram_quantile function
|
|
|
|
|
// because they are most likely artifacts of floating point precision issues.
|
|
|
|
|
// Testing on 2 sets of real data with bugs arising from small deltas,
|
|
|
|
|
// the safe ranges were from:
|
|
|
|
|
// - 1e-05 to 1e-15
|
|
|
|
|
// - 1e-06 to 1e-15
|
|
|
|
|
// Anything to the left of that would cause non-query-sharded data to have
|
|
|
|
|
// small deltas ignored (unnecessary and we should avoid this), and anything
|
|
|
|
|
// to the right of that would cause query-sharded data to not have its small
|
|
|
|
|
// deltas ignored (so the problem won't be fixed).
|
|
|
|
|
// For context, query sharding triggers these float precision errors in Mimir.
|
|
|
|
|
// To illustrate, with a relative deviation of 1e-12, we need to have 1e12
|
|
|
|
|
// observations in the bucket so that the change of one observation is small
|
|
|
|
|
// enough to get ignored. With the usual observation rate even of very busy
|
|
|
|
|
// services, this will hardly be reached in timeframes that matters for
|
|
|
|
|
// monitoring.
|
|
|
|
|
const smallDeltaTolerance = 1e-12
|
|
|
|
|
|
2015-03-30 17:13:36 +00:00
|
|
|
|
// Helpers to calculate quantiles.
|
|
|
|
|
|
|
|
|
|
// excludedLabels are the labels to exclude from signature calculation for
|
|
|
|
|
// quantiles.
|
2016-12-23 12:51:59 +00:00
|
|
|
|
var excludedLabels = []string{
|
2016-12-24 13:35:24 +00:00
|
|
|
|
labels.MetricName,
|
|
|
|
|
labels.BucketLabel,
|
2015-03-30 17:13:36 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type bucket struct {
|
|
|
|
|
upperBound float64
|
2016-12-23 12:51:59 +00:00
|
|
|
|
count float64
|
2015-03-30 17:13:36 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// buckets implements sort.Interface.
|
|
|
|
|
type buckets []bucket
|
|
|
|
|
|
|
|
|
|
type metricWithBuckets struct {
|
2016-12-23 12:51:59 +00:00
|
|
|
|
metric labels.Labels
|
2015-03-30 17:13:36 +00:00
|
|
|
|
buckets buckets
|
|
|
|
|
}
|
|
|
|
|
|
2016-07-08 12:33:20 +00:00
|
|
|
|
// bucketQuantile calculates the quantile 'q' based on the given buckets. The
|
|
|
|
|
// buckets will be sorted by upperBound by this function (i.e. no sorting
|
|
|
|
|
// needed before calling this function). The quantile value is interpolated
|
|
|
|
|
// assuming a linear distribution within a bucket. However, if the quantile
|
|
|
|
|
// falls into the highest bucket, the upper bound of the 2nd highest bucket is
|
|
|
|
|
// returned. A natural lower bound of 0 is assumed if the upper bound of the
|
|
|
|
|
// lowest bucket is greater 0. In that case, interpolation in the lowest bucket
|
|
|
|
|
// happens linearly between 0 and the upper bound of the lowest bucket.
|
|
|
|
|
// However, if the lowest bucket has an upper bound less or equal 0, this upper
|
|
|
|
|
// bound is returned if the quantile falls into the lowest bucket.
|
2015-03-30 17:13:36 +00:00
|
|
|
|
//
|
|
|
|
|
// There are a number of special cases (once we have a way to report errors
|
|
|
|
|
// happening during evaluations of AST functions, we should report those
|
|
|
|
|
// explicitly):
|
|
|
|
|
//
|
2020-06-01 08:40:39 +00:00
|
|
|
|
// If 'buckets' has 0 observations, NaN is returned.
|
|
|
|
|
//
|
2015-03-30 17:13:36 +00:00
|
|
|
|
// If 'buckets' has fewer than 2 elements, NaN is returned.
|
|
|
|
|
//
|
|
|
|
|
// If the highest bucket is not +Inf, NaN is returned.
|
|
|
|
|
//
|
2022-02-13 13:59:03 +00:00
|
|
|
|
// If q==NaN, NaN is returned.
|
|
|
|
|
//
|
2015-03-30 17:13:36 +00:00
|
|
|
|
// If q<0, -Inf is returned.
|
|
|
|
|
//
|
|
|
|
|
// If q>1, +Inf is returned.
|
2023-10-04 10:53:55 +00:00
|
|
|
|
//
|
2023-11-24 23:05:38 +00:00
|
|
|
|
// We also return a bool to indicate if monotonicity needed to be forced,
|
|
|
|
|
// and another bool to indicate if small differences between buckets (that
|
|
|
|
|
// are likely artifacts of floating point precision issues) have been
|
|
|
|
|
// ignored.
|
|
|
|
|
func bucketQuantile(q float64, buckets buckets) (float64, bool, bool) {
|
2022-02-13 13:59:03 +00:00
|
|
|
|
if math.IsNaN(q) {
|
2023-11-24 23:05:38 +00:00
|
|
|
|
return math.NaN(), false, false
|
2022-02-13 13:41:28 +00:00
|
|
|
|
}
|
2015-03-30 17:13:36 +00:00
|
|
|
|
if q < 0 {
|
2023-11-24 23:05:38 +00:00
|
|
|
|
return math.Inf(-1), false, false
|
2015-03-30 17:13:36 +00:00
|
|
|
|
}
|
|
|
|
|
if q > 1 {
|
2023-11-24 23:05:38 +00:00
|
|
|
|
return math.Inf(+1), false, false
|
2015-03-30 17:13:36 +00:00
|
|
|
|
}
|
2023-09-21 20:53:51 +00:00
|
|
|
|
slices.SortFunc(buckets, func(a, b bucket) int {
|
|
|
|
|
// We don't expect the bucket boundary to be a NaN.
|
|
|
|
|
if a.upperBound < b.upperBound {
|
|
|
|
|
return -1
|
|
|
|
|
}
|
|
|
|
|
if a.upperBound > b.upperBound {
|
|
|
|
|
return +1
|
|
|
|
|
}
|
|
|
|
|
return 0
|
2023-07-08 12:45:56 +00:00
|
|
|
|
})
|
2015-03-30 17:13:36 +00:00
|
|
|
|
if !math.IsInf(buckets[len(buckets)-1].upperBound, +1) {
|
2023-11-24 23:05:38 +00:00
|
|
|
|
return math.NaN(), false, false
|
2015-03-30 17:13:36 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-02-01 10:22:44 +00:00
|
|
|
|
buckets = coalesceBuckets(buckets)
|
2023-11-24 23:05:38 +00:00
|
|
|
|
forcedMonotonic, fixedPrecision := ensureMonotonicAndIgnoreSmallDeltas(buckets, smallDeltaTolerance)
|
Force buckets in a histogram to be monotonic for quantile estimation (#2610)
* Force buckets in a histogram to be monotonic for quantile estimation
The assumption that bucket counts increase monotonically with increasing
upperBound may be violated during:
* Recording rule evaluation of histogram_quantile, especially when rate()
has been applied to the underlying bucket timeseries.
* Evaluation of histogram_quantile computed over federated bucket
timeseries, especially when rate() has been applied
This is because scraped data is not made available to RR evalution or
federation atomically, so some buckets are computed with data from the N
most recent scrapes, but the other buckets are missing the most recent
observations.
Monotonicity is usually guaranteed because if a bucket with upper bound
u1 has count c1, then any bucket with a higher upper bound u > u1 must
have counted all c1 observations and perhaps more, so that c >= c1.
Randomly interspersed partial sampling breaks that guarantee, and rate()
exacerbates it. Specifically, suppose bucket le=1000 has a count of 10 from
4 samples but the bucket with le=2000 has a count of 7, from 3 samples. The
monotonicity is broken. It is exacerbated by rate() because under normal
operation, cumulative counting of buckets will cause the bucket counts to
diverge such that small differences from missing samples are not a problem.
rate() removes this divergence.)
bucketQuantile depends on that monotonicity to do a binary search for the
bucket with the qth percentile count, so breaking the monotonicity
guarantee causes bucketQuantile() to return undefined (nonsense) results.
As a somewhat hacky solution until the Prometheus project is ready to
accept the changes required to make scrapes atomic, we calculate the
"envelope" of the histogram buckets, essentially removing any decreases
in the count between successive buckets.
* Fix up comment docs for ensureMonotonic
* ensureMonotonic: Use switch statement
Use switch statement rather than if/else for better readability.
Process the most frequent cases first.
2017-04-14 14:21:49 +00:00
|
|
|
|
|
2019-02-01 10:22:44 +00:00
|
|
|
|
if len(buckets) < 2 {
|
2023-11-24 23:05:38 +00:00
|
|
|
|
return math.NaN(), false, false
|
2019-02-01 10:22:44 +00:00
|
|
|
|
}
|
2020-06-01 08:40:39 +00:00
|
|
|
|
observations := buckets[len(buckets)-1].count
|
|
|
|
|
if observations == 0 {
|
2023-11-24 23:05:38 +00:00
|
|
|
|
return math.NaN(), false, false
|
2020-06-01 08:40:39 +00:00
|
|
|
|
}
|
|
|
|
|
rank := q * observations
|
2015-03-30 17:13:36 +00:00
|
|
|
|
b := sort.Search(len(buckets)-1, func(i int) bool { return buckets[i].count >= rank })
|
|
|
|
|
|
|
|
|
|
if b == len(buckets)-1 {
|
2023-11-24 23:05:38 +00:00
|
|
|
|
return buckets[len(buckets)-2].upperBound, forcedMonotonic, fixedPrecision
|
2015-03-30 17:13:36 +00:00
|
|
|
|
}
|
|
|
|
|
if b == 0 && buckets[0].upperBound <= 0 {
|
2023-11-24 23:05:38 +00:00
|
|
|
|
return buckets[0].upperBound, forcedMonotonic, fixedPrecision
|
2015-03-30 17:13:36 +00:00
|
|
|
|
}
|
|
|
|
|
var (
|
|
|
|
|
bucketStart float64
|
|
|
|
|
bucketEnd = buckets[b].upperBound
|
|
|
|
|
count = buckets[b].count
|
|
|
|
|
)
|
|
|
|
|
if b > 0 {
|
|
|
|
|
bucketStart = buckets[b-1].upperBound
|
|
|
|
|
count -= buckets[b-1].count
|
|
|
|
|
rank -= buckets[b-1].count
|
|
|
|
|
}
|
2023-11-24 23:05:38 +00:00
|
|
|
|
return bucketStart + (bucketEnd-bucketStart)*(rank/count), forcedMonotonic, fixedPrecision
|
2015-03-30 17:13:36 +00:00
|
|
|
|
}
|
2016-07-08 12:33:20 +00:00
|
|
|
|
|
2021-12-06 13:47:22 +00:00
|
|
|
|
// histogramQuantile calculates the quantile 'q' based on the given histogram.
|
2021-12-15 15:50:37 +00:00
|
|
|
|
//
|
promql(native histograms): Introduce exponential interpolation
The linear interpolation (assuming that observations are uniformly
distributed within a bucket) is a solid and simple assumption in lack
of any other information. However, the exponential bucketing used by
standard schemas of native histograms has been chosen to cover the
whole range of observations in a way that bucket populations are
spread out over buckets in a reasonably way for typical distributions
encountered in real-world scenarios.
This is the origin of the idea implemented here: If we divide a given
bucket into two (or more) smaller exponential buckets, we "most
naturally" expect that the samples in the original buckets will split
among those smaller buckets in a more or less uniform fashion. With
this assumption, we end up with an "exponential interpolation", which
therefore appears to be a better match for histograms with exponential
bucketing.
This commit leaves the linear interpolation in place for NHCB, but
changes the interpolation for exponential native histograms to
exponential. This affects `histogram_quantile` and
`histogram_fraction` (because the latter is more or less the inverse
of the former).
The zero bucket has to be treated specially because the assumption
above would lead to an "interpolation to zero" (the bucket density
approaches infinity around zero, and with the postulated uniform usage
of buckets, we would end up with an estimate of zero for all quantiles
ending up in the zero bucket). We simply fall back to linear
interpolation within the zero bucket.
At the same time, this commit makes the call to stick with the
assumption that the zero bucket only contains positive observations
for native histograms without negative buckets (and vice versa). (This
is an assumption relevant for interpolation. It is a mostly academic
point, as the zero bucket is supposed to be very small anyway.
However, in cases where it _is_ relevantly broad, the assumption helps
a lot in practice.)
This commit also updates and completes the documentation to match both
details about interpolation.
As a more high level note: The approach here attempts to strike a
balance between a more simplistic approach without any assumption, and
a more involved approach with more sophisticated assumptions. I will
shortly describe both for reference:
The "zero assumption" approach would be to not interpolate at all, but
_always_ return the harmonic mean of the bucket boundaries of the
bucket the quantile ends up in. This has the advantage of minimizing
the maximum possible relative error of the quantile estimation.
(Depending on the exact definition of the relative error of an
estimation, there is also an argument to return the arithmetic mean of
the bucket boundaries.) While limiting the maximum possible relative
error is a good property, this approach would throw away the
information if a quantile is closer to the upper or lower end of the
population within a bucket. This can be valuable trending information
in a dashboard. With any kind of interpolation, the maximum possible
error of a quantile estimation increases to the full width of a bucket
(i.e. it more than doubles for the harmonic mean approach, and
precisely doubles for the arithmetic mean approach). However, in
return the _expectation value_ of the error decreases. The increase of
the theoretical maximum only has practical relevance for pathologic
distributions. For example, if there are thousand observations within
a bucket, they could _all_ be at the upper bound of the bucket. If the
quantile calculation picks the 1st observation in the bucket as the
relevant one, an interpolation will yield a value close to the lower
bucket boundary, while the true quantile value is close to the upper
boundary.
The "fancy interpolation" approach would be one that analyses the
_actual_ distribution of samples in the histogram. A lot of statistics
could be applied based on the information we have available in the
histogram. This would include the population of neighboring (or even
all) buckets in the histogram. In general, the resolution of a native
histogram should be quite high, and therefore, those "fancy"
approaches would increase the computational cost quite a bit with very
little practical benefits (i.e. just tiny corrections of the estimated
quantile value). The results are also much harder to reason with.
Signed-off-by: beorn7 <beorn@grafana.com>
2024-08-15 12:19:16 +00:00
|
|
|
|
// For custom buckets, the result is interpolated linearly, i.e. it is assumed
|
|
|
|
|
// the observations are uniformly distributed within each bucket. (This is a
|
|
|
|
|
// quite blunt assumption, but it is consistent with the interpolation method
|
|
|
|
|
// used for classic histograms so far.)
|
|
|
|
|
//
|
|
|
|
|
// For exponential buckets, the interpolation is done under the assumption that
|
|
|
|
|
// the samples within each bucket are distributed in a way that they would
|
|
|
|
|
// uniformly populate the buckets in a hypothetical histogram with higher
|
|
|
|
|
// resolution. For example, if the rank calculation suggests that the requested
|
|
|
|
|
// quantile is right in the middle of the population of the (1,2] bucket, we
|
|
|
|
|
// assume the quantile would be right at the bucket boundary between the two
|
|
|
|
|
// buckets the (1,2] bucket would be divided into if the histogram had double
|
|
|
|
|
// the resolution, which is 2**2**-1 = 1.4142... We call this exponential
|
|
|
|
|
// interpolation.
|
|
|
|
|
//
|
|
|
|
|
// However, for a quantile that ends up in the zero bucket, this method isn't
|
|
|
|
|
// very helpful (because there is an infinite number of buckets close to zero,
|
|
|
|
|
// so we would have to assume zero as the result). Therefore, we return to
|
|
|
|
|
// linear interpolation in the zero bucket.
|
2021-12-15 15:50:37 +00:00
|
|
|
|
//
|
2022-06-16 16:54:28 +00:00
|
|
|
|
// A natural lower bound of 0 is assumed if the histogram has only positive
|
|
|
|
|
// buckets. Likewise, a natural upper bound of 0 is assumed if the histogram has
|
|
|
|
|
// only negative buckets.
|
2021-12-06 13:47:22 +00:00
|
|
|
|
//
|
promql(native histograms): Introduce exponential interpolation
The linear interpolation (assuming that observations are uniformly
distributed within a bucket) is a solid and simple assumption in lack
of any other information. However, the exponential bucketing used by
standard schemas of native histograms has been chosen to cover the
whole range of observations in a way that bucket populations are
spread out over buckets in a reasonably way for typical distributions
encountered in real-world scenarios.
This is the origin of the idea implemented here: If we divide a given
bucket into two (or more) smaller exponential buckets, we "most
naturally" expect that the samples in the original buckets will split
among those smaller buckets in a more or less uniform fashion. With
this assumption, we end up with an "exponential interpolation", which
therefore appears to be a better match for histograms with exponential
bucketing.
This commit leaves the linear interpolation in place for NHCB, but
changes the interpolation for exponential native histograms to
exponential. This affects `histogram_quantile` and
`histogram_fraction` (because the latter is more or less the inverse
of the former).
The zero bucket has to be treated specially because the assumption
above would lead to an "interpolation to zero" (the bucket density
approaches infinity around zero, and with the postulated uniform usage
of buckets, we would end up with an estimate of zero for all quantiles
ending up in the zero bucket). We simply fall back to linear
interpolation within the zero bucket.
At the same time, this commit makes the call to stick with the
assumption that the zero bucket only contains positive observations
for native histograms without negative buckets (and vice versa). (This
is an assumption relevant for interpolation. It is a mostly academic
point, as the zero bucket is supposed to be very small anyway.
However, in cases where it _is_ relevantly broad, the assumption helps
a lot in practice.)
This commit also updates and completes the documentation to match both
details about interpolation.
As a more high level note: The approach here attempts to strike a
balance between a more simplistic approach without any assumption, and
a more involved approach with more sophisticated assumptions. I will
shortly describe both for reference:
The "zero assumption" approach would be to not interpolate at all, but
_always_ return the harmonic mean of the bucket boundaries of the
bucket the quantile ends up in. This has the advantage of minimizing
the maximum possible relative error of the quantile estimation.
(Depending on the exact definition of the relative error of an
estimation, there is also an argument to return the arithmetic mean of
the bucket boundaries.) While limiting the maximum possible relative
error is a good property, this approach would throw away the
information if a quantile is closer to the upper or lower end of the
population within a bucket. This can be valuable trending information
in a dashboard. With any kind of interpolation, the maximum possible
error of a quantile estimation increases to the full width of a bucket
(i.e. it more than doubles for the harmonic mean approach, and
precisely doubles for the arithmetic mean approach). However, in
return the _expectation value_ of the error decreases. The increase of
the theoretical maximum only has practical relevance for pathologic
distributions. For example, if there are thousand observations within
a bucket, they could _all_ be at the upper bound of the bucket. If the
quantile calculation picks the 1st observation in the bucket as the
relevant one, an interpolation will yield a value close to the lower
bucket boundary, while the true quantile value is close to the upper
boundary.
The "fancy interpolation" approach would be one that analyses the
_actual_ distribution of samples in the histogram. A lot of statistics
could be applied based on the information we have available in the
histogram. This would include the population of neighboring (or even
all) buckets in the histogram. In general, the resolution of a native
histogram should be quite high, and therefore, those "fancy"
approaches would increase the computational cost quite a bit with very
little practical benefits (i.e. just tiny corrections of the estimated
quantile value). The results are also much harder to reason with.
Signed-off-by: beorn7 <beorn@grafana.com>
2024-08-15 12:19:16 +00:00
|
|
|
|
// There are a number of special cases:
|
2021-12-06 13:47:22 +00:00
|
|
|
|
//
|
2021-12-15 15:50:37 +00:00
|
|
|
|
// If the histogram has 0 observations, NaN is returned.
|
2021-12-06 13:47:22 +00:00
|
|
|
|
//
|
|
|
|
|
// If q<0, -Inf is returned.
|
|
|
|
|
//
|
2021-12-15 15:50:37 +00:00
|
|
|
|
// If q>1, +Inf is returned.
|
2022-06-16 18:44:12 +00:00
|
|
|
|
//
|
|
|
|
|
// If q is NaN, NaN is returned.
|
2021-12-06 13:47:22 +00:00
|
|
|
|
func histogramQuantile(q float64, h *histogram.FloatHistogram) float64 {
|
|
|
|
|
if q < 0 {
|
|
|
|
|
return math.Inf(-1)
|
|
|
|
|
}
|
|
|
|
|
if q > 1 {
|
|
|
|
|
return math.Inf(+1)
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-16 18:44:12 +00:00
|
|
|
|
if h.Count == 0 || math.IsNaN(q) {
|
2021-12-06 13:47:22 +00:00
|
|
|
|
return math.NaN()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (
|
2022-10-03 11:15:27 +00:00
|
|
|
|
bucket histogram.Bucket[float64]
|
2021-12-06 13:47:22 +00:00
|
|
|
|
count float64
|
2023-07-05 11:05:53 +00:00
|
|
|
|
it histogram.BucketIterator[float64]
|
|
|
|
|
rank float64
|
2021-12-06 13:47:22 +00:00
|
|
|
|
)
|
2023-07-05 11:05:53 +00:00
|
|
|
|
|
promql(native histograms): Introduce exponential interpolation
The linear interpolation (assuming that observations are uniformly
distributed within a bucket) is a solid and simple assumption in lack
of any other information. However, the exponential bucketing used by
standard schemas of native histograms has been chosen to cover the
whole range of observations in a way that bucket populations are
spread out over buckets in a reasonably way for typical distributions
encountered in real-world scenarios.
This is the origin of the idea implemented here: If we divide a given
bucket into two (or more) smaller exponential buckets, we "most
naturally" expect that the samples in the original buckets will split
among those smaller buckets in a more or less uniform fashion. With
this assumption, we end up with an "exponential interpolation", which
therefore appears to be a better match for histograms with exponential
bucketing.
This commit leaves the linear interpolation in place for NHCB, but
changes the interpolation for exponential native histograms to
exponential. This affects `histogram_quantile` and
`histogram_fraction` (because the latter is more or less the inverse
of the former).
The zero bucket has to be treated specially because the assumption
above would lead to an "interpolation to zero" (the bucket density
approaches infinity around zero, and with the postulated uniform usage
of buckets, we would end up with an estimate of zero for all quantiles
ending up in the zero bucket). We simply fall back to linear
interpolation within the zero bucket.
At the same time, this commit makes the call to stick with the
assumption that the zero bucket only contains positive observations
for native histograms without negative buckets (and vice versa). (This
is an assumption relevant for interpolation. It is a mostly academic
point, as the zero bucket is supposed to be very small anyway.
However, in cases where it _is_ relevantly broad, the assumption helps
a lot in practice.)
This commit also updates and completes the documentation to match both
details about interpolation.
As a more high level note: The approach here attempts to strike a
balance between a more simplistic approach without any assumption, and
a more involved approach with more sophisticated assumptions. I will
shortly describe both for reference:
The "zero assumption" approach would be to not interpolate at all, but
_always_ return the harmonic mean of the bucket boundaries of the
bucket the quantile ends up in. This has the advantage of minimizing
the maximum possible relative error of the quantile estimation.
(Depending on the exact definition of the relative error of an
estimation, there is also an argument to return the arithmetic mean of
the bucket boundaries.) While limiting the maximum possible relative
error is a good property, this approach would throw away the
information if a quantile is closer to the upper or lower end of the
population within a bucket. This can be valuable trending information
in a dashboard. With any kind of interpolation, the maximum possible
error of a quantile estimation increases to the full width of a bucket
(i.e. it more than doubles for the harmonic mean approach, and
precisely doubles for the arithmetic mean approach). However, in
return the _expectation value_ of the error decreases. The increase of
the theoretical maximum only has practical relevance for pathologic
distributions. For example, if there are thousand observations within
a bucket, they could _all_ be at the upper bound of the bucket. If the
quantile calculation picks the 1st observation in the bucket as the
relevant one, an interpolation will yield a value close to the lower
bucket boundary, while the true quantile value is close to the upper
boundary.
The "fancy interpolation" approach would be one that analyses the
_actual_ distribution of samples in the histogram. A lot of statistics
could be applied based on the information we have available in the
histogram. This would include the population of neighboring (or even
all) buckets in the histogram. In general, the resolution of a native
histogram should be quite high, and therefore, those "fancy"
approaches would increase the computational cost quite a bit with very
little practical benefits (i.e. just tiny corrections of the estimated
quantile value). The results are also much harder to reason with.
Signed-off-by: beorn7 <beorn@grafana.com>
2024-08-15 12:19:16 +00:00
|
|
|
|
// If there are NaN observations in the histogram (h.Sum is NaN), use the forward iterator.
|
|
|
|
|
// If q < 0.5, use the forward iterator.
|
|
|
|
|
// If q >= 0.5, use the reverse iterator.
|
2023-07-05 11:05:53 +00:00
|
|
|
|
if math.IsNaN(h.Sum) || q < 0.5 {
|
|
|
|
|
it = h.AllBucketIterator()
|
|
|
|
|
rank = q * h.Count
|
|
|
|
|
} else {
|
|
|
|
|
it = h.AllReverseBucketIterator()
|
|
|
|
|
rank = (1 - q) * h.Count
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-06 13:47:22 +00:00
|
|
|
|
for it.Next() {
|
2021-12-15 16:39:32 +00:00
|
|
|
|
bucket = it.At()
|
2024-04-24 07:36:05 +00:00
|
|
|
|
if bucket.Count == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2021-12-15 16:39:32 +00:00
|
|
|
|
count += bucket.Count
|
2021-12-06 13:47:22 +00:00
|
|
|
|
if count >= rank {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-04-24 07:36:05 +00:00
|
|
|
|
if !h.UsesCustomBuckets() && bucket.Lower < 0 && bucket.Upper > 0 {
|
style: Replace `else if` cascades with `switch`
Wiser coders than myself have come to the conclusion that a `switch`
statement is almost always superior to a statement that includes any
`else if`.
The exceptions that I have found in our codebase are just these two:
* The `if else` is followed by an additional statement before the next
condition (separated by a `;`).
* The whole thing is within a `for` loop and `break` statements are
used. In this case, using `switch` would require tagging the `for`
loop, which probably tips the balance.
Why are `switch` statements more readable?
For one, fewer curly braces. But more importantly, the conditions all
have the same alignment, so the whole thing follows the natural flow
of going down a list of conditions. With `else if`, in contrast, all
conditions but the first are "hidden" behind `} else if `, harder to
spot and (for no good reason) presented differently from the first
condition.
I'm sure the aforemention wise coders can list even more reasons.
In any case, I like it so much that I have found myself recommending
it in code reviews. I would like to make it a habit in our code base,
without making it a hard requirement that we would test on the CI. But
for that, there has to be a role model, so this commit eliminates all
`if else` occurrences, unless it is autogenerated code or fits one of
the exceptions above.
Signed-off-by: beorn7 <beorn@grafana.com>
2023-04-12 14:14:31 +00:00
|
|
|
|
switch {
|
|
|
|
|
case len(h.NegativeBuckets) == 0 && len(h.PositiveBuckets) > 0:
|
2022-06-16 16:54:28 +00:00
|
|
|
|
// The result is in the zero bucket and the histogram has only
|
|
|
|
|
// positive buckets. So we consider 0 to be the lower bound.
|
|
|
|
|
bucket.Lower = 0
|
style: Replace `else if` cascades with `switch`
Wiser coders than myself have come to the conclusion that a `switch`
statement is almost always superior to a statement that includes any
`else if`.
The exceptions that I have found in our codebase are just these two:
* The `if else` is followed by an additional statement before the next
condition (separated by a `;`).
* The whole thing is within a `for` loop and `break` statements are
used. In this case, using `switch` would require tagging the `for`
loop, which probably tips the balance.
Why are `switch` statements more readable?
For one, fewer curly braces. But more importantly, the conditions all
have the same alignment, so the whole thing follows the natural flow
of going down a list of conditions. With `else if`, in contrast, all
conditions but the first are "hidden" behind `} else if `, harder to
spot and (for no good reason) presented differently from the first
condition.
I'm sure the aforemention wise coders can list even more reasons.
In any case, I like it so much that I have found myself recommending
it in code reviews. I would like to make it a habit in our code base,
without making it a hard requirement that we would test on the CI. But
for that, there has to be a role model, so this commit eliminates all
`if else` occurrences, unless it is autogenerated code or fits one of
the exceptions above.
Signed-off-by: beorn7 <beorn@grafana.com>
2023-04-12 14:14:31 +00:00
|
|
|
|
case len(h.PositiveBuckets) == 0 && len(h.NegativeBuckets) > 0:
|
2022-06-16 16:54:28 +00:00
|
|
|
|
// The result is in the zero bucket and the histogram has only
|
|
|
|
|
// negative buckets. So we consider 0 to be the upper bound.
|
|
|
|
|
bucket.Upper = 0
|
|
|
|
|
}
|
2024-04-24 07:36:05 +00:00
|
|
|
|
} else if h.UsesCustomBuckets() {
|
|
|
|
|
if bucket.Lower == math.Inf(-1) {
|
|
|
|
|
// first bucket, with lower bound -Inf
|
|
|
|
|
if bucket.Upper <= 0 {
|
|
|
|
|
return bucket.Upper
|
|
|
|
|
}
|
|
|
|
|
bucket.Lower = 0
|
|
|
|
|
} else if bucket.Upper == math.Inf(1) {
|
|
|
|
|
// last bucket, with upper bound +Inf
|
|
|
|
|
return bucket.Lower
|
|
|
|
|
}
|
2021-12-06 13:47:22 +00:00
|
|
|
|
}
|
2021-12-15 16:39:32 +00:00
|
|
|
|
// Due to numerical inaccuracies, we could end up with a higher count
|
|
|
|
|
// than h.Count. Thus, make sure count is never higher than h.Count.
|
|
|
|
|
if count > h.Count {
|
|
|
|
|
count = h.Count
|
|
|
|
|
}
|
|
|
|
|
// We could have hit the highest bucket without even reaching the rank
|
2022-10-05 13:34:47 +00:00
|
|
|
|
// (this should only happen if the histogram contains observations of
|
|
|
|
|
// the value NaN), in which case we simply return the upper limit of the
|
|
|
|
|
// highest explicit bucket.
|
2021-12-15 16:39:32 +00:00
|
|
|
|
if count < rank {
|
|
|
|
|
return bucket.Upper
|
|
|
|
|
}
|
2021-12-06 13:47:22 +00:00
|
|
|
|
|
2023-07-12 12:52:49 +00:00
|
|
|
|
// NaN observations increase h.Count but not the total number of
|
|
|
|
|
// observations in the buckets. Therefore, we have to use the forward
|
|
|
|
|
// iterator to find percentiles. We recognize histograms containing NaN
|
|
|
|
|
// observations by checking if their h.Sum is NaN.
|
2023-07-05 11:05:53 +00:00
|
|
|
|
if math.IsNaN(h.Sum) || q < 0.5 {
|
|
|
|
|
rank -= count - bucket.Count
|
|
|
|
|
} else {
|
|
|
|
|
rank = count - rank
|
|
|
|
|
}
|
|
|
|
|
|
promql(native histograms): Introduce exponential interpolation
The linear interpolation (assuming that observations are uniformly
distributed within a bucket) is a solid and simple assumption in lack
of any other information. However, the exponential bucketing used by
standard schemas of native histograms has been chosen to cover the
whole range of observations in a way that bucket populations are
spread out over buckets in a reasonably way for typical distributions
encountered in real-world scenarios.
This is the origin of the idea implemented here: If we divide a given
bucket into two (or more) smaller exponential buckets, we "most
naturally" expect that the samples in the original buckets will split
among those smaller buckets in a more or less uniform fashion. With
this assumption, we end up with an "exponential interpolation", which
therefore appears to be a better match for histograms with exponential
bucketing.
This commit leaves the linear interpolation in place for NHCB, but
changes the interpolation for exponential native histograms to
exponential. This affects `histogram_quantile` and
`histogram_fraction` (because the latter is more or less the inverse
of the former).
The zero bucket has to be treated specially because the assumption
above would lead to an "interpolation to zero" (the bucket density
approaches infinity around zero, and with the postulated uniform usage
of buckets, we would end up with an estimate of zero for all quantiles
ending up in the zero bucket). We simply fall back to linear
interpolation within the zero bucket.
At the same time, this commit makes the call to stick with the
assumption that the zero bucket only contains positive observations
for native histograms without negative buckets (and vice versa). (This
is an assumption relevant for interpolation. It is a mostly academic
point, as the zero bucket is supposed to be very small anyway.
However, in cases where it _is_ relevantly broad, the assumption helps
a lot in practice.)
This commit also updates and completes the documentation to match both
details about interpolation.
As a more high level note: The approach here attempts to strike a
balance between a more simplistic approach without any assumption, and
a more involved approach with more sophisticated assumptions. I will
shortly describe both for reference:
The "zero assumption" approach would be to not interpolate at all, but
_always_ return the harmonic mean of the bucket boundaries of the
bucket the quantile ends up in. This has the advantage of minimizing
the maximum possible relative error of the quantile estimation.
(Depending on the exact definition of the relative error of an
estimation, there is also an argument to return the arithmetic mean of
the bucket boundaries.) While limiting the maximum possible relative
error is a good property, this approach would throw away the
information if a quantile is closer to the upper or lower end of the
population within a bucket. This can be valuable trending information
in a dashboard. With any kind of interpolation, the maximum possible
error of a quantile estimation increases to the full width of a bucket
(i.e. it more than doubles for the harmonic mean approach, and
precisely doubles for the arithmetic mean approach). However, in
return the _expectation value_ of the error decreases. The increase of
the theoretical maximum only has practical relevance for pathologic
distributions. For example, if there are thousand observations within
a bucket, they could _all_ be at the upper bound of the bucket. If the
quantile calculation picks the 1st observation in the bucket as the
relevant one, an interpolation will yield a value close to the lower
bucket boundary, while the true quantile value is close to the upper
boundary.
The "fancy interpolation" approach would be one that analyses the
_actual_ distribution of samples in the histogram. A lot of statistics
could be applied based on the information we have available in the
histogram. This would include the population of neighboring (or even
all) buckets in the histogram. In general, the resolution of a native
histogram should be quite high, and therefore, those "fancy"
approaches would increase the computational cost quite a bit with very
little practical benefits (i.e. just tiny corrections of the estimated
quantile value). The results are also much harder to reason with.
Signed-off-by: beorn7 <beorn@grafana.com>
2024-08-15 12:19:16 +00:00
|
|
|
|
// The fraction of how far we are into the current bucket.
|
|
|
|
|
fraction := rank / bucket.Count
|
|
|
|
|
|
|
|
|
|
// Return linear interpolation for custom buckets and for quantiles that
|
|
|
|
|
// end up in the zero bucket.
|
|
|
|
|
if h.UsesCustomBuckets() || (bucket.Lower <= 0 && bucket.Upper >= 0) {
|
|
|
|
|
return bucket.Lower + (bucket.Upper-bucket.Lower)*fraction
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For exponential buckets, we interpolate on a logarithmic scale. On a
|
|
|
|
|
// logarithmic scale, the exponential bucket boundaries (for any schema)
|
|
|
|
|
// become linear (every bucket has the same width). Therefore, after
|
|
|
|
|
// taking the logarithm of both bucket boundaries, we can use the
|
|
|
|
|
// calculated fraction in the same way as for linear interpolation (see
|
|
|
|
|
// above). Finally, we return to the normal scale by applying the
|
|
|
|
|
// exponential function to the result.
|
|
|
|
|
logLower := math.Log2(math.Abs(bucket.Lower))
|
|
|
|
|
logUpper := math.Log2(math.Abs(bucket.Upper))
|
|
|
|
|
if bucket.Lower > 0 { // Positive bucket.
|
|
|
|
|
return math.Exp2(logLower + (logUpper-logLower)*fraction)
|
|
|
|
|
}
|
|
|
|
|
// Otherwise, we are in a negative bucket and have to mirror things.
|
|
|
|
|
return -math.Exp2(logUpper + (logLower-logUpper)*(1-fraction))
|
2021-12-06 13:47:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-06-16 18:44:12 +00:00
|
|
|
|
// histogramFraction calculates the fraction of observations between the
|
|
|
|
|
// provided lower and upper bounds, based on the provided histogram.
|
|
|
|
|
//
|
|
|
|
|
// histogramFraction is in a certain way the inverse of histogramQuantile. If
|
|
|
|
|
// histogramQuantile(0.9, h) returns 123.4, then histogramFraction(-Inf, 123.4, h)
|
|
|
|
|
// returns 0.9.
|
|
|
|
|
//
|
promql(native histograms): Introduce exponential interpolation
The linear interpolation (assuming that observations are uniformly
distributed within a bucket) is a solid and simple assumption in lack
of any other information. However, the exponential bucketing used by
standard schemas of native histograms has been chosen to cover the
whole range of observations in a way that bucket populations are
spread out over buckets in a reasonably way for typical distributions
encountered in real-world scenarios.
This is the origin of the idea implemented here: If we divide a given
bucket into two (or more) smaller exponential buckets, we "most
naturally" expect that the samples in the original buckets will split
among those smaller buckets in a more or less uniform fashion. With
this assumption, we end up with an "exponential interpolation", which
therefore appears to be a better match for histograms with exponential
bucketing.
This commit leaves the linear interpolation in place for NHCB, but
changes the interpolation for exponential native histograms to
exponential. This affects `histogram_quantile` and
`histogram_fraction` (because the latter is more or less the inverse
of the former).
The zero bucket has to be treated specially because the assumption
above would lead to an "interpolation to zero" (the bucket density
approaches infinity around zero, and with the postulated uniform usage
of buckets, we would end up with an estimate of zero for all quantiles
ending up in the zero bucket). We simply fall back to linear
interpolation within the zero bucket.
At the same time, this commit makes the call to stick with the
assumption that the zero bucket only contains positive observations
for native histograms without negative buckets (and vice versa). (This
is an assumption relevant for interpolation. It is a mostly academic
point, as the zero bucket is supposed to be very small anyway.
However, in cases where it _is_ relevantly broad, the assumption helps
a lot in practice.)
This commit also updates and completes the documentation to match both
details about interpolation.
As a more high level note: The approach here attempts to strike a
balance between a more simplistic approach without any assumption, and
a more involved approach with more sophisticated assumptions. I will
shortly describe both for reference:
The "zero assumption" approach would be to not interpolate at all, but
_always_ return the harmonic mean of the bucket boundaries of the
bucket the quantile ends up in. This has the advantage of minimizing
the maximum possible relative error of the quantile estimation.
(Depending on the exact definition of the relative error of an
estimation, there is also an argument to return the arithmetic mean of
the bucket boundaries.) While limiting the maximum possible relative
error is a good property, this approach would throw away the
information if a quantile is closer to the upper or lower end of the
population within a bucket. This can be valuable trending information
in a dashboard. With any kind of interpolation, the maximum possible
error of a quantile estimation increases to the full width of a bucket
(i.e. it more than doubles for the harmonic mean approach, and
precisely doubles for the arithmetic mean approach). However, in
return the _expectation value_ of the error decreases. The increase of
the theoretical maximum only has practical relevance for pathologic
distributions. For example, if there are thousand observations within
a bucket, they could _all_ be at the upper bound of the bucket. If the
quantile calculation picks the 1st observation in the bucket as the
relevant one, an interpolation will yield a value close to the lower
bucket boundary, while the true quantile value is close to the upper
boundary.
The "fancy interpolation" approach would be one that analyses the
_actual_ distribution of samples in the histogram. A lot of statistics
could be applied based on the information we have available in the
histogram. This would include the population of neighboring (or even
all) buckets in the histogram. In general, the resolution of a native
histogram should be quite high, and therefore, those "fancy"
approaches would increase the computational cost quite a bit with very
little practical benefits (i.e. just tiny corrections of the estimated
quantile value). The results are also much harder to reason with.
Signed-off-by: beorn7 <beorn@grafana.com>
2024-08-15 12:19:16 +00:00
|
|
|
|
// The same notes with regard to interpolation and assumptions about the zero
|
|
|
|
|
// bucket boundaries apply as for histogramQuantile.
|
2022-06-16 18:44:12 +00:00
|
|
|
|
//
|
|
|
|
|
// Whether either boundary is inclusive or exclusive doesn’t actually matter as
|
|
|
|
|
// long as interpolation has to be performed anyway. In the case of a boundary
|
|
|
|
|
// coinciding with a bucket boundary, the inclusive or exclusive nature of the
|
|
|
|
|
// boundary determines the exact behavior of the threshold. With the current
|
|
|
|
|
// implementation, that means that lower is exclusive for positive values and
|
|
|
|
|
// inclusive for negative values, while upper is inclusive for positive values
|
|
|
|
|
// and exclusive for negative values.
|
|
|
|
|
//
|
|
|
|
|
// Special cases:
|
|
|
|
|
//
|
|
|
|
|
// If the histogram has 0 observations, NaN is returned.
|
|
|
|
|
//
|
|
|
|
|
// Use a lower bound of -Inf to get the fraction of all observations below the
|
|
|
|
|
// upper bound.
|
|
|
|
|
//
|
|
|
|
|
// Use an upper bound of +Inf to get the fraction of all observations above the
|
|
|
|
|
// lower bound.
|
|
|
|
|
//
|
|
|
|
|
// If lower or upper is NaN, NaN is returned.
|
|
|
|
|
//
|
|
|
|
|
// If lower >= upper and the histogram has at least 1 observation, zero is returned.
|
|
|
|
|
func histogramFraction(lower, upper float64, h *histogram.FloatHistogram) float64 {
|
|
|
|
|
if h.Count == 0 || math.IsNaN(lower) || math.IsNaN(upper) {
|
|
|
|
|
return math.NaN()
|
|
|
|
|
}
|
|
|
|
|
if lower >= upper {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
rank, lowerRank, upperRank float64
|
|
|
|
|
lowerSet, upperSet bool
|
|
|
|
|
it = h.AllBucketIterator()
|
|
|
|
|
)
|
|
|
|
|
for it.Next() {
|
|
|
|
|
b := it.At()
|
promql(native histograms): Introduce exponential interpolation
The linear interpolation (assuming that observations are uniformly
distributed within a bucket) is a solid and simple assumption in lack
of any other information. However, the exponential bucketing used by
standard schemas of native histograms has been chosen to cover the
whole range of observations in a way that bucket populations are
spread out over buckets in a reasonably way for typical distributions
encountered in real-world scenarios.
This is the origin of the idea implemented here: If we divide a given
bucket into two (or more) smaller exponential buckets, we "most
naturally" expect that the samples in the original buckets will split
among those smaller buckets in a more or less uniform fashion. With
this assumption, we end up with an "exponential interpolation", which
therefore appears to be a better match for histograms with exponential
bucketing.
This commit leaves the linear interpolation in place for NHCB, but
changes the interpolation for exponential native histograms to
exponential. This affects `histogram_quantile` and
`histogram_fraction` (because the latter is more or less the inverse
of the former).
The zero bucket has to be treated specially because the assumption
above would lead to an "interpolation to zero" (the bucket density
approaches infinity around zero, and with the postulated uniform usage
of buckets, we would end up with an estimate of zero for all quantiles
ending up in the zero bucket). We simply fall back to linear
interpolation within the zero bucket.
At the same time, this commit makes the call to stick with the
assumption that the zero bucket only contains positive observations
for native histograms without negative buckets (and vice versa). (This
is an assumption relevant for interpolation. It is a mostly academic
point, as the zero bucket is supposed to be very small anyway.
However, in cases where it _is_ relevantly broad, the assumption helps
a lot in practice.)
This commit also updates and completes the documentation to match both
details about interpolation.
As a more high level note: The approach here attempts to strike a
balance between a more simplistic approach without any assumption, and
a more involved approach with more sophisticated assumptions. I will
shortly describe both for reference:
The "zero assumption" approach would be to not interpolate at all, but
_always_ return the harmonic mean of the bucket boundaries of the
bucket the quantile ends up in. This has the advantage of minimizing
the maximum possible relative error of the quantile estimation.
(Depending on the exact definition of the relative error of an
estimation, there is also an argument to return the arithmetic mean of
the bucket boundaries.) While limiting the maximum possible relative
error is a good property, this approach would throw away the
information if a quantile is closer to the upper or lower end of the
population within a bucket. This can be valuable trending information
in a dashboard. With any kind of interpolation, the maximum possible
error of a quantile estimation increases to the full width of a bucket
(i.e. it more than doubles for the harmonic mean approach, and
precisely doubles for the arithmetic mean approach). However, in
return the _expectation value_ of the error decreases. The increase of
the theoretical maximum only has practical relevance for pathologic
distributions. For example, if there are thousand observations within
a bucket, they could _all_ be at the upper bound of the bucket. If the
quantile calculation picks the 1st observation in the bucket as the
relevant one, an interpolation will yield a value close to the lower
bucket boundary, while the true quantile value is close to the upper
boundary.
The "fancy interpolation" approach would be one that analyses the
_actual_ distribution of samples in the histogram. A lot of statistics
could be applied based on the information we have available in the
histogram. This would include the population of neighboring (or even
all) buckets in the histogram. In general, the resolution of a native
histogram should be quite high, and therefore, those "fancy"
approaches would increase the computational cost quite a bit with very
little practical benefits (i.e. just tiny corrections of the estimated
quantile value). The results are also much harder to reason with.
Signed-off-by: beorn7 <beorn@grafana.com>
2024-08-15 12:19:16 +00:00
|
|
|
|
zeroBucket := false
|
|
|
|
|
|
|
|
|
|
// interpolateLinearly is used for custom buckets to be
|
|
|
|
|
// consistent with the linear interpolation known from classic
|
|
|
|
|
// histograms. It is also used for the zero bucket.
|
|
|
|
|
interpolateLinearly := func(v float64) float64 {
|
|
|
|
|
return rank + b.Count*(v-b.Lower)/(b.Upper-b.Lower)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// interpolateExponentially is using the same exponential
|
|
|
|
|
// interpolation method as above for histogramQuantile. This
|
|
|
|
|
// method is a better fit for exponential bucketing.
|
|
|
|
|
interpolateExponentially := func(v float64) float64 {
|
|
|
|
|
var (
|
|
|
|
|
logLower = math.Log2(math.Abs(b.Lower))
|
|
|
|
|
logUpper = math.Log2(math.Abs(b.Upper))
|
|
|
|
|
logV = math.Log2(math.Abs(v))
|
|
|
|
|
fraction float64
|
|
|
|
|
)
|
|
|
|
|
if v > 0 {
|
|
|
|
|
fraction = (logV - logLower) / (logUpper - logLower)
|
|
|
|
|
} else {
|
|
|
|
|
fraction = 1 - ((logV - logUpper) / (logLower - logUpper))
|
|
|
|
|
}
|
|
|
|
|
return rank + b.Count*fraction
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if b.Lower <= 0 && b.Upper >= 0 {
|
|
|
|
|
zeroBucket = true
|
style: Replace `else if` cascades with `switch`
Wiser coders than myself have come to the conclusion that a `switch`
statement is almost always superior to a statement that includes any
`else if`.
The exceptions that I have found in our codebase are just these two:
* The `if else` is followed by an additional statement before the next
condition (separated by a `;`).
* The whole thing is within a `for` loop and `break` statements are
used. In this case, using `switch` would require tagging the `for`
loop, which probably tips the balance.
Why are `switch` statements more readable?
For one, fewer curly braces. But more importantly, the conditions all
have the same alignment, so the whole thing follows the natural flow
of going down a list of conditions. With `else if`, in contrast, all
conditions but the first are "hidden" behind `} else if `, harder to
spot and (for no good reason) presented differently from the first
condition.
I'm sure the aforemention wise coders can list even more reasons.
In any case, I like it so much that I have found myself recommending
it in code reviews. I would like to make it a habit in our code base,
without making it a hard requirement that we would test on the CI. But
for that, there has to be a role model, so this commit eliminates all
`if else` occurrences, unless it is autogenerated code or fits one of
the exceptions above.
Signed-off-by: beorn7 <beorn@grafana.com>
2023-04-12 14:14:31 +00:00
|
|
|
|
switch {
|
|
|
|
|
case len(h.NegativeBuckets) == 0 && len(h.PositiveBuckets) > 0:
|
2022-06-16 18:44:12 +00:00
|
|
|
|
// This is the zero bucket and the histogram has only
|
|
|
|
|
// positive buckets. So we consider 0 to be the lower
|
|
|
|
|
// bound.
|
|
|
|
|
b.Lower = 0
|
style: Replace `else if` cascades with `switch`
Wiser coders than myself have come to the conclusion that a `switch`
statement is almost always superior to a statement that includes any
`else if`.
The exceptions that I have found in our codebase are just these two:
* The `if else` is followed by an additional statement before the next
condition (separated by a `;`).
* The whole thing is within a `for` loop and `break` statements are
used. In this case, using `switch` would require tagging the `for`
loop, which probably tips the balance.
Why are `switch` statements more readable?
For one, fewer curly braces. But more importantly, the conditions all
have the same alignment, so the whole thing follows the natural flow
of going down a list of conditions. With `else if`, in contrast, all
conditions but the first are "hidden" behind `} else if `, harder to
spot and (for no good reason) presented differently from the first
condition.
I'm sure the aforemention wise coders can list even more reasons.
In any case, I like it so much that I have found myself recommending
it in code reviews. I would like to make it a habit in our code base,
without making it a hard requirement that we would test on the CI. But
for that, there has to be a role model, so this commit eliminates all
`if else` occurrences, unless it is autogenerated code or fits one of
the exceptions above.
Signed-off-by: beorn7 <beorn@grafana.com>
2023-04-12 14:14:31 +00:00
|
|
|
|
case len(h.PositiveBuckets) == 0 && len(h.NegativeBuckets) > 0:
|
2022-06-16 18:44:12 +00:00
|
|
|
|
// This is in the zero bucket and the histogram has only
|
|
|
|
|
// negative buckets. So we consider 0 to be the upper
|
|
|
|
|
// bound.
|
|
|
|
|
b.Upper = 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !lowerSet && b.Lower >= lower {
|
promql(native histograms): Introduce exponential interpolation
The linear interpolation (assuming that observations are uniformly
distributed within a bucket) is a solid and simple assumption in lack
of any other information. However, the exponential bucketing used by
standard schemas of native histograms has been chosen to cover the
whole range of observations in a way that bucket populations are
spread out over buckets in a reasonably way for typical distributions
encountered in real-world scenarios.
This is the origin of the idea implemented here: If we divide a given
bucket into two (or more) smaller exponential buckets, we "most
naturally" expect that the samples in the original buckets will split
among those smaller buckets in a more or less uniform fashion. With
this assumption, we end up with an "exponential interpolation", which
therefore appears to be a better match for histograms with exponential
bucketing.
This commit leaves the linear interpolation in place for NHCB, but
changes the interpolation for exponential native histograms to
exponential. This affects `histogram_quantile` and
`histogram_fraction` (because the latter is more or less the inverse
of the former).
The zero bucket has to be treated specially because the assumption
above would lead to an "interpolation to zero" (the bucket density
approaches infinity around zero, and with the postulated uniform usage
of buckets, we would end up with an estimate of zero for all quantiles
ending up in the zero bucket). We simply fall back to linear
interpolation within the zero bucket.
At the same time, this commit makes the call to stick with the
assumption that the zero bucket only contains positive observations
for native histograms without negative buckets (and vice versa). (This
is an assumption relevant for interpolation. It is a mostly academic
point, as the zero bucket is supposed to be very small anyway.
However, in cases where it _is_ relevantly broad, the assumption helps
a lot in practice.)
This commit also updates and completes the documentation to match both
details about interpolation.
As a more high level note: The approach here attempts to strike a
balance between a more simplistic approach without any assumption, and
a more involved approach with more sophisticated assumptions. I will
shortly describe both for reference:
The "zero assumption" approach would be to not interpolate at all, but
_always_ return the harmonic mean of the bucket boundaries of the
bucket the quantile ends up in. This has the advantage of minimizing
the maximum possible relative error of the quantile estimation.
(Depending on the exact definition of the relative error of an
estimation, there is also an argument to return the arithmetic mean of
the bucket boundaries.) While limiting the maximum possible relative
error is a good property, this approach would throw away the
information if a quantile is closer to the upper or lower end of the
population within a bucket. This can be valuable trending information
in a dashboard. With any kind of interpolation, the maximum possible
error of a quantile estimation increases to the full width of a bucket
(i.e. it more than doubles for the harmonic mean approach, and
precisely doubles for the arithmetic mean approach). However, in
return the _expectation value_ of the error decreases. The increase of
the theoretical maximum only has practical relevance for pathologic
distributions. For example, if there are thousand observations within
a bucket, they could _all_ be at the upper bound of the bucket. If the
quantile calculation picks the 1st observation in the bucket as the
relevant one, an interpolation will yield a value close to the lower
bucket boundary, while the true quantile value is close to the upper
boundary.
The "fancy interpolation" approach would be one that analyses the
_actual_ distribution of samples in the histogram. A lot of statistics
could be applied based on the information we have available in the
histogram. This would include the population of neighboring (or even
all) buckets in the histogram. In general, the resolution of a native
histogram should be quite high, and therefore, those "fancy"
approaches would increase the computational cost quite a bit with very
little practical benefits (i.e. just tiny corrections of the estimated
quantile value). The results are also much harder to reason with.
Signed-off-by: beorn7 <beorn@grafana.com>
2024-08-15 12:19:16 +00:00
|
|
|
|
// We have hit the lower value at the lower bucket boundary.
|
2022-06-16 18:44:12 +00:00
|
|
|
|
lowerRank = rank
|
|
|
|
|
lowerSet = true
|
|
|
|
|
}
|
|
|
|
|
if !upperSet && b.Lower >= upper {
|
promql(native histograms): Introduce exponential interpolation
The linear interpolation (assuming that observations are uniformly
distributed within a bucket) is a solid and simple assumption in lack
of any other information. However, the exponential bucketing used by
standard schemas of native histograms has been chosen to cover the
whole range of observations in a way that bucket populations are
spread out over buckets in a reasonably way for typical distributions
encountered in real-world scenarios.
This is the origin of the idea implemented here: If we divide a given
bucket into two (or more) smaller exponential buckets, we "most
naturally" expect that the samples in the original buckets will split
among those smaller buckets in a more or less uniform fashion. With
this assumption, we end up with an "exponential interpolation", which
therefore appears to be a better match for histograms with exponential
bucketing.
This commit leaves the linear interpolation in place for NHCB, but
changes the interpolation for exponential native histograms to
exponential. This affects `histogram_quantile` and
`histogram_fraction` (because the latter is more or less the inverse
of the former).
The zero bucket has to be treated specially because the assumption
above would lead to an "interpolation to zero" (the bucket density
approaches infinity around zero, and with the postulated uniform usage
of buckets, we would end up with an estimate of zero for all quantiles
ending up in the zero bucket). We simply fall back to linear
interpolation within the zero bucket.
At the same time, this commit makes the call to stick with the
assumption that the zero bucket only contains positive observations
for native histograms without negative buckets (and vice versa). (This
is an assumption relevant for interpolation. It is a mostly academic
point, as the zero bucket is supposed to be very small anyway.
However, in cases where it _is_ relevantly broad, the assumption helps
a lot in practice.)
This commit also updates and completes the documentation to match both
details about interpolation.
As a more high level note: The approach here attempts to strike a
balance between a more simplistic approach without any assumption, and
a more involved approach with more sophisticated assumptions. I will
shortly describe both for reference:
The "zero assumption" approach would be to not interpolate at all, but
_always_ return the harmonic mean of the bucket boundaries of the
bucket the quantile ends up in. This has the advantage of minimizing
the maximum possible relative error of the quantile estimation.
(Depending on the exact definition of the relative error of an
estimation, there is also an argument to return the arithmetic mean of
the bucket boundaries.) While limiting the maximum possible relative
error is a good property, this approach would throw away the
information if a quantile is closer to the upper or lower end of the
population within a bucket. This can be valuable trending information
in a dashboard. With any kind of interpolation, the maximum possible
error of a quantile estimation increases to the full width of a bucket
(i.e. it more than doubles for the harmonic mean approach, and
precisely doubles for the arithmetic mean approach). However, in
return the _expectation value_ of the error decreases. The increase of
the theoretical maximum only has practical relevance for pathologic
distributions. For example, if there are thousand observations within
a bucket, they could _all_ be at the upper bound of the bucket. If the
quantile calculation picks the 1st observation in the bucket as the
relevant one, an interpolation will yield a value close to the lower
bucket boundary, while the true quantile value is close to the upper
boundary.
The "fancy interpolation" approach would be one that analyses the
_actual_ distribution of samples in the histogram. A lot of statistics
could be applied based on the information we have available in the
histogram. This would include the population of neighboring (or even
all) buckets in the histogram. In general, the resolution of a native
histogram should be quite high, and therefore, those "fancy"
approaches would increase the computational cost quite a bit with very
little practical benefits (i.e. just tiny corrections of the estimated
quantile value). The results are also much harder to reason with.
Signed-off-by: beorn7 <beorn@grafana.com>
2024-08-15 12:19:16 +00:00
|
|
|
|
// We have hit the upper value at the lower bucket boundary.
|
2022-06-16 18:44:12 +00:00
|
|
|
|
upperRank = rank
|
|
|
|
|
upperSet = true
|
|
|
|
|
}
|
|
|
|
|
if lowerSet && upperSet {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
if !lowerSet && b.Lower < lower && b.Upper > lower {
|
promql(native histograms): Introduce exponential interpolation
The linear interpolation (assuming that observations are uniformly
distributed within a bucket) is a solid and simple assumption in lack
of any other information. However, the exponential bucketing used by
standard schemas of native histograms has been chosen to cover the
whole range of observations in a way that bucket populations are
spread out over buckets in a reasonably way for typical distributions
encountered in real-world scenarios.
This is the origin of the idea implemented here: If we divide a given
bucket into two (or more) smaller exponential buckets, we "most
naturally" expect that the samples in the original buckets will split
among those smaller buckets in a more or less uniform fashion. With
this assumption, we end up with an "exponential interpolation", which
therefore appears to be a better match for histograms with exponential
bucketing.
This commit leaves the linear interpolation in place for NHCB, but
changes the interpolation for exponential native histograms to
exponential. This affects `histogram_quantile` and
`histogram_fraction` (because the latter is more or less the inverse
of the former).
The zero bucket has to be treated specially because the assumption
above would lead to an "interpolation to zero" (the bucket density
approaches infinity around zero, and with the postulated uniform usage
of buckets, we would end up with an estimate of zero for all quantiles
ending up in the zero bucket). We simply fall back to linear
interpolation within the zero bucket.
At the same time, this commit makes the call to stick with the
assumption that the zero bucket only contains positive observations
for native histograms without negative buckets (and vice versa). (This
is an assumption relevant for interpolation. It is a mostly academic
point, as the zero bucket is supposed to be very small anyway.
However, in cases where it _is_ relevantly broad, the assumption helps
a lot in practice.)
This commit also updates and completes the documentation to match both
details about interpolation.
As a more high level note: The approach here attempts to strike a
balance between a more simplistic approach without any assumption, and
a more involved approach with more sophisticated assumptions. I will
shortly describe both for reference:
The "zero assumption" approach would be to not interpolate at all, but
_always_ return the harmonic mean of the bucket boundaries of the
bucket the quantile ends up in. This has the advantage of minimizing
the maximum possible relative error of the quantile estimation.
(Depending on the exact definition of the relative error of an
estimation, there is also an argument to return the arithmetic mean of
the bucket boundaries.) While limiting the maximum possible relative
error is a good property, this approach would throw away the
information if a quantile is closer to the upper or lower end of the
population within a bucket. This can be valuable trending information
in a dashboard. With any kind of interpolation, the maximum possible
error of a quantile estimation increases to the full width of a bucket
(i.e. it more than doubles for the harmonic mean approach, and
precisely doubles for the arithmetic mean approach). However, in
return the _expectation value_ of the error decreases. The increase of
the theoretical maximum only has practical relevance for pathologic
distributions. For example, if there are thousand observations within
a bucket, they could _all_ be at the upper bound of the bucket. If the
quantile calculation picks the 1st observation in the bucket as the
relevant one, an interpolation will yield a value close to the lower
bucket boundary, while the true quantile value is close to the upper
boundary.
The "fancy interpolation" approach would be one that analyses the
_actual_ distribution of samples in the histogram. A lot of statistics
could be applied based on the information we have available in the
histogram. This would include the population of neighboring (or even
all) buckets in the histogram. In general, the resolution of a native
histogram should be quite high, and therefore, those "fancy"
approaches would increase the computational cost quite a bit with very
little practical benefits (i.e. just tiny corrections of the estimated
quantile value). The results are also much harder to reason with.
Signed-off-by: beorn7 <beorn@grafana.com>
2024-08-15 12:19:16 +00:00
|
|
|
|
// The lower value is in this bucket.
|
|
|
|
|
if h.UsesCustomBuckets() || zeroBucket {
|
|
|
|
|
lowerRank = interpolateLinearly(lower)
|
|
|
|
|
} else {
|
|
|
|
|
lowerRank = interpolateExponentially(lower)
|
|
|
|
|
}
|
2022-06-16 18:44:12 +00:00
|
|
|
|
lowerSet = true
|
|
|
|
|
}
|
|
|
|
|
if !upperSet && b.Lower < upper && b.Upper > upper {
|
promql(native histograms): Introduce exponential interpolation
The linear interpolation (assuming that observations are uniformly
distributed within a bucket) is a solid and simple assumption in lack
of any other information. However, the exponential bucketing used by
standard schemas of native histograms has been chosen to cover the
whole range of observations in a way that bucket populations are
spread out over buckets in a reasonably way for typical distributions
encountered in real-world scenarios.
This is the origin of the idea implemented here: If we divide a given
bucket into two (or more) smaller exponential buckets, we "most
naturally" expect that the samples in the original buckets will split
among those smaller buckets in a more or less uniform fashion. With
this assumption, we end up with an "exponential interpolation", which
therefore appears to be a better match for histograms with exponential
bucketing.
This commit leaves the linear interpolation in place for NHCB, but
changes the interpolation for exponential native histograms to
exponential. This affects `histogram_quantile` and
`histogram_fraction` (because the latter is more or less the inverse
of the former).
The zero bucket has to be treated specially because the assumption
above would lead to an "interpolation to zero" (the bucket density
approaches infinity around zero, and with the postulated uniform usage
of buckets, we would end up with an estimate of zero for all quantiles
ending up in the zero bucket). We simply fall back to linear
interpolation within the zero bucket.
At the same time, this commit makes the call to stick with the
assumption that the zero bucket only contains positive observations
for native histograms without negative buckets (and vice versa). (This
is an assumption relevant for interpolation. It is a mostly academic
point, as the zero bucket is supposed to be very small anyway.
However, in cases where it _is_ relevantly broad, the assumption helps
a lot in practice.)
This commit also updates and completes the documentation to match both
details about interpolation.
As a more high level note: The approach here attempts to strike a
balance between a more simplistic approach without any assumption, and
a more involved approach with more sophisticated assumptions. I will
shortly describe both for reference:
The "zero assumption" approach would be to not interpolate at all, but
_always_ return the harmonic mean of the bucket boundaries of the
bucket the quantile ends up in. This has the advantage of minimizing
the maximum possible relative error of the quantile estimation.
(Depending on the exact definition of the relative error of an
estimation, there is also an argument to return the arithmetic mean of
the bucket boundaries.) While limiting the maximum possible relative
error is a good property, this approach would throw away the
information if a quantile is closer to the upper or lower end of the
population within a bucket. This can be valuable trending information
in a dashboard. With any kind of interpolation, the maximum possible
error of a quantile estimation increases to the full width of a bucket
(i.e. it more than doubles for the harmonic mean approach, and
precisely doubles for the arithmetic mean approach). However, in
return the _expectation value_ of the error decreases. The increase of
the theoretical maximum only has practical relevance for pathologic
distributions. For example, if there are thousand observations within
a bucket, they could _all_ be at the upper bound of the bucket. If the
quantile calculation picks the 1st observation in the bucket as the
relevant one, an interpolation will yield a value close to the lower
bucket boundary, while the true quantile value is close to the upper
boundary.
The "fancy interpolation" approach would be one that analyses the
_actual_ distribution of samples in the histogram. A lot of statistics
could be applied based on the information we have available in the
histogram. This would include the population of neighboring (or even
all) buckets in the histogram. In general, the resolution of a native
histogram should be quite high, and therefore, those "fancy"
approaches would increase the computational cost quite a bit with very
little practical benefits (i.e. just tiny corrections of the estimated
quantile value). The results are also much harder to reason with.
Signed-off-by: beorn7 <beorn@grafana.com>
2024-08-15 12:19:16 +00:00
|
|
|
|
// The upper value is in this bucket.
|
|
|
|
|
if h.UsesCustomBuckets() || zeroBucket {
|
|
|
|
|
upperRank = interpolateLinearly(upper)
|
|
|
|
|
} else {
|
|
|
|
|
upperRank = interpolateExponentially(upper)
|
|
|
|
|
}
|
2022-06-16 18:44:12 +00:00
|
|
|
|
upperSet = true
|
|
|
|
|
}
|
|
|
|
|
if lowerSet && upperSet {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
rank += b.Count
|
|
|
|
|
}
|
|
|
|
|
if !lowerSet || lowerRank > h.Count {
|
|
|
|
|
lowerRank = h.Count
|
|
|
|
|
}
|
|
|
|
|
if !upperSet || upperRank > h.Count {
|
|
|
|
|
upperRank = h.Count
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (upperRank - lowerRank) / h.Count
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-01 10:22:44 +00:00
|
|
|
|
// coalesceBuckets merges buckets with the same upper bound.
|
|
|
|
|
//
|
|
|
|
|
// The input buckets must be sorted.
|
|
|
|
|
func coalesceBuckets(buckets buckets) buckets {
|
|
|
|
|
last := buckets[0]
|
|
|
|
|
i := 0
|
|
|
|
|
for _, b := range buckets[1:] {
|
|
|
|
|
if b.upperBound == last.upperBound {
|
|
|
|
|
last.count += b.count
|
|
|
|
|
} else {
|
|
|
|
|
buckets[i] = last
|
|
|
|
|
last = b
|
|
|
|
|
i++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
buckets[i] = last
|
|
|
|
|
return buckets[:i+1]
|
|
|
|
|
}
|
|
|
|
|
|
Force buckets in a histogram to be monotonic for quantile estimation (#2610)
* Force buckets in a histogram to be monotonic for quantile estimation
The assumption that bucket counts increase monotonically with increasing
upperBound may be violated during:
* Recording rule evaluation of histogram_quantile, especially when rate()
has been applied to the underlying bucket timeseries.
* Evaluation of histogram_quantile computed over federated bucket
timeseries, especially when rate() has been applied
This is because scraped data is not made available to RR evalution or
federation atomically, so some buckets are computed with data from the N
most recent scrapes, but the other buckets are missing the most recent
observations.
Monotonicity is usually guaranteed because if a bucket with upper bound
u1 has count c1, then any bucket with a higher upper bound u > u1 must
have counted all c1 observations and perhaps more, so that c >= c1.
Randomly interspersed partial sampling breaks that guarantee, and rate()
exacerbates it. Specifically, suppose bucket le=1000 has a count of 10 from
4 samples but the bucket with le=2000 has a count of 7, from 3 samples. The
monotonicity is broken. It is exacerbated by rate() because under normal
operation, cumulative counting of buckets will cause the bucket counts to
diverge such that small differences from missing samples are not a problem.
rate() removes this divergence.)
bucketQuantile depends on that monotonicity to do a binary search for the
bucket with the qth percentile count, so breaking the monotonicity
guarantee causes bucketQuantile() to return undefined (nonsense) results.
As a somewhat hacky solution until the Prometheus project is ready to
accept the changes required to make scrapes atomic, we calculate the
"envelope" of the histogram buckets, essentially removing any decreases
in the count between successive buckets.
* Fix up comment docs for ensureMonotonic
* ensureMonotonic: Use switch statement
Use switch statement rather than if/else for better readability.
Process the most frequent cases first.
2017-04-14 14:21:49 +00:00
|
|
|
|
// The assumption that bucket counts increase monotonically with increasing
|
|
|
|
|
// upperBound may be violated during:
|
|
|
|
|
//
|
2023-10-06 11:09:32 +00:00
|
|
|
|
// - Circumstances where data is already inconsistent at the target's side.
|
|
|
|
|
// - Ingestion via the remote write receiver that Prometheus implements.
|
|
|
|
|
// - Optimisation of query execution where precision is sacrificed for other
|
|
|
|
|
// benefits, not by Prometheus but by systems built on top of it.
|
2023-11-24 23:05:38 +00:00
|
|
|
|
// - Circumstances where floating point precision errors accumulate.
|
Force buckets in a histogram to be monotonic for quantile estimation (#2610)
* Force buckets in a histogram to be monotonic for quantile estimation
The assumption that bucket counts increase monotonically with increasing
upperBound may be violated during:
* Recording rule evaluation of histogram_quantile, especially when rate()
has been applied to the underlying bucket timeseries.
* Evaluation of histogram_quantile computed over federated bucket
timeseries, especially when rate() has been applied
This is because scraped data is not made available to RR evalution or
federation atomically, so some buckets are computed with data from the N
most recent scrapes, but the other buckets are missing the most recent
observations.
Monotonicity is usually guaranteed because if a bucket with upper bound
u1 has count c1, then any bucket with a higher upper bound u > u1 must
have counted all c1 observations and perhaps more, so that c >= c1.
Randomly interspersed partial sampling breaks that guarantee, and rate()
exacerbates it. Specifically, suppose bucket le=1000 has a count of 10 from
4 samples but the bucket with le=2000 has a count of 7, from 3 samples. The
monotonicity is broken. It is exacerbated by rate() because under normal
operation, cumulative counting of buckets will cause the bucket counts to
diverge such that small differences from missing samples are not a problem.
rate() removes this divergence.)
bucketQuantile depends on that monotonicity to do a binary search for the
bucket with the qth percentile count, so breaking the monotonicity
guarantee causes bucketQuantile() to return undefined (nonsense) results.
As a somewhat hacky solution until the Prometheus project is ready to
accept the changes required to make scrapes atomic, we calculate the
"envelope" of the histogram buckets, essentially removing any decreases
in the count between successive buckets.
* Fix up comment docs for ensureMonotonic
* ensureMonotonic: Use switch statement
Use switch statement rather than if/else for better readability.
Process the most frequent cases first.
2017-04-14 14:21:49 +00:00
|
|
|
|
//
|
|
|
|
|
// Monotonicity is usually guaranteed because if a bucket with upper bound
|
|
|
|
|
// u1 has count c1, then any bucket with a higher upper bound u > u1 must
|
2023-10-06 11:09:32 +00:00
|
|
|
|
// have counted all c1 observations and perhaps more, so that c >= c1.
|
Force buckets in a histogram to be monotonic for quantile estimation (#2610)
* Force buckets in a histogram to be monotonic for quantile estimation
The assumption that bucket counts increase monotonically with increasing
upperBound may be violated during:
* Recording rule evaluation of histogram_quantile, especially when rate()
has been applied to the underlying bucket timeseries.
* Evaluation of histogram_quantile computed over federated bucket
timeseries, especially when rate() has been applied
This is because scraped data is not made available to RR evalution or
federation atomically, so some buckets are computed with data from the N
most recent scrapes, but the other buckets are missing the most recent
observations.
Monotonicity is usually guaranteed because if a bucket with upper bound
u1 has count c1, then any bucket with a higher upper bound u > u1 must
have counted all c1 observations and perhaps more, so that c >= c1.
Randomly interspersed partial sampling breaks that guarantee, and rate()
exacerbates it. Specifically, suppose bucket le=1000 has a count of 10 from
4 samples but the bucket with le=2000 has a count of 7, from 3 samples. The
monotonicity is broken. It is exacerbated by rate() because under normal
operation, cumulative counting of buckets will cause the bucket counts to
diverge such that small differences from missing samples are not a problem.
rate() removes this divergence.)
bucketQuantile depends on that monotonicity to do a binary search for the
bucket with the qth percentile count, so breaking the monotonicity
guarantee causes bucketQuantile() to return undefined (nonsense) results.
As a somewhat hacky solution until the Prometheus project is ready to
accept the changes required to make scrapes atomic, we calculate the
"envelope" of the histogram buckets, essentially removing any decreases
in the count between successive buckets.
* Fix up comment docs for ensureMonotonic
* ensureMonotonic: Use switch statement
Use switch statement rather than if/else for better readability.
Process the most frequent cases first.
2017-04-14 14:21:49 +00:00
|
|
|
|
//
|
|
|
|
|
// bucketQuantile depends on that monotonicity to do a binary search for the
|
|
|
|
|
// bucket with the φ-quantile count, so breaking the monotonicity
|
|
|
|
|
// guarantee causes bucketQuantile() to return undefined (nonsense) results.
|
|
|
|
|
//
|
2023-11-24 23:05:38 +00:00
|
|
|
|
// As a somewhat hacky solution, we first silently ignore any numerically
|
|
|
|
|
// insignificant (relative delta below the requested tolerance and likely to
|
|
|
|
|
// be from floating point precision errors) differences between successive
|
|
|
|
|
// buckets regardless of the direction. Then we calculate the "envelope" of
|
|
|
|
|
// the histogram buckets, essentially removing any decreases in the count
|
|
|
|
|
// between successive buckets.
|
|
|
|
|
//
|
|
|
|
|
// We return a bool to indicate if this monotonicity was forced or not, and
|
|
|
|
|
// another bool to indicate if small deltas were ignored or not.
|
|
|
|
|
func ensureMonotonicAndIgnoreSmallDeltas(buckets buckets, tolerance float64) (bool, bool) {
|
|
|
|
|
var forcedMonotonic, fixedPrecision bool
|
|
|
|
|
prev := buckets[0].count
|
2020-06-15 10:32:10 +00:00
|
|
|
|
for i := 1; i < len(buckets); i++ {
|
2023-11-24 23:05:38 +00:00
|
|
|
|
curr := buckets[i].count // Assumed always positive.
|
|
|
|
|
if curr == prev {
|
|
|
|
|
// No correction needed if the counts are identical between buckets.
|
|
|
|
|
continue
|
|
|
|
|
}
|
2024-04-29 11:37:32 +00:00
|
|
|
|
if almost.Equal(prev, curr, tolerance) {
|
2023-11-24 23:05:38 +00:00
|
|
|
|
// Silently correct numerically insignificant differences from floating
|
|
|
|
|
// point precision errors, regardless of direction.
|
|
|
|
|
// Do not update the 'prev' value as we are ignoring the difference.
|
|
|
|
|
buckets[i].count = prev
|
|
|
|
|
fixedPrecision = true
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if curr < prev {
|
|
|
|
|
// Force monotonicity by removing any decreases regardless of magnitude.
|
|
|
|
|
// Do not update the 'prev' value as we are ignoring the decrease.
|
|
|
|
|
buckets[i].count = prev
|
|
|
|
|
forcedMonotonic = true
|
|
|
|
|
continue
|
Force buckets in a histogram to be monotonic for quantile estimation (#2610)
* Force buckets in a histogram to be monotonic for quantile estimation
The assumption that bucket counts increase monotonically with increasing
upperBound may be violated during:
* Recording rule evaluation of histogram_quantile, especially when rate()
has been applied to the underlying bucket timeseries.
* Evaluation of histogram_quantile computed over federated bucket
timeseries, especially when rate() has been applied
This is because scraped data is not made available to RR evalution or
federation atomically, so some buckets are computed with data from the N
most recent scrapes, but the other buckets are missing the most recent
observations.
Monotonicity is usually guaranteed because if a bucket with upper bound
u1 has count c1, then any bucket with a higher upper bound u > u1 must
have counted all c1 observations and perhaps more, so that c >= c1.
Randomly interspersed partial sampling breaks that guarantee, and rate()
exacerbates it. Specifically, suppose bucket le=1000 has a count of 10 from
4 samples but the bucket with le=2000 has a count of 7, from 3 samples. The
monotonicity is broken. It is exacerbated by rate() because under normal
operation, cumulative counting of buckets will cause the bucket counts to
diverge such that small differences from missing samples are not a problem.
rate() removes this divergence.)
bucketQuantile depends on that monotonicity to do a binary search for the
bucket with the qth percentile count, so breaking the monotonicity
guarantee causes bucketQuantile() to return undefined (nonsense) results.
As a somewhat hacky solution until the Prometheus project is ready to
accept the changes required to make scrapes atomic, we calculate the
"envelope" of the histogram buckets, essentially removing any decreases
in the count between successive buckets.
* Fix up comment docs for ensureMonotonic
* ensureMonotonic: Use switch statement
Use switch statement rather than if/else for better readability.
Process the most frequent cases first.
2017-04-14 14:21:49 +00:00
|
|
|
|
}
|
2023-11-24 23:05:38 +00:00
|
|
|
|
prev = curr
|
Force buckets in a histogram to be monotonic for quantile estimation (#2610)
* Force buckets in a histogram to be monotonic for quantile estimation
The assumption that bucket counts increase monotonically with increasing
upperBound may be violated during:
* Recording rule evaluation of histogram_quantile, especially when rate()
has been applied to the underlying bucket timeseries.
* Evaluation of histogram_quantile computed over federated bucket
timeseries, especially when rate() has been applied
This is because scraped data is not made available to RR evalution or
federation atomically, so some buckets are computed with data from the N
most recent scrapes, but the other buckets are missing the most recent
observations.
Monotonicity is usually guaranteed because if a bucket with upper bound
u1 has count c1, then any bucket with a higher upper bound u > u1 must
have counted all c1 observations and perhaps more, so that c >= c1.
Randomly interspersed partial sampling breaks that guarantee, and rate()
exacerbates it. Specifically, suppose bucket le=1000 has a count of 10 from
4 samples but the bucket with le=2000 has a count of 7, from 3 samples. The
monotonicity is broken. It is exacerbated by rate() because under normal
operation, cumulative counting of buckets will cause the bucket counts to
diverge such that small differences from missing samples are not a problem.
rate() removes this divergence.)
bucketQuantile depends on that monotonicity to do a binary search for the
bucket with the qth percentile count, so breaking the monotonicity
guarantee causes bucketQuantile() to return undefined (nonsense) results.
As a somewhat hacky solution until the Prometheus project is ready to
accept the changes required to make scrapes atomic, we calculate the
"envelope" of the histogram buckets, essentially removing any decreases
in the count between successive buckets.
* Fix up comment docs for ensureMonotonic
* ensureMonotonic: Use switch statement
Use switch statement rather than if/else for better readability.
Process the most frequent cases first.
2017-04-14 14:21:49 +00:00
|
|
|
|
}
|
2023-11-24 23:05:38 +00:00
|
|
|
|
return forcedMonotonic, fixedPrecision
|
Force buckets in a histogram to be monotonic for quantile estimation (#2610)
* Force buckets in a histogram to be monotonic for quantile estimation
The assumption that bucket counts increase monotonically with increasing
upperBound may be violated during:
* Recording rule evaluation of histogram_quantile, especially when rate()
has been applied to the underlying bucket timeseries.
* Evaluation of histogram_quantile computed over federated bucket
timeseries, especially when rate() has been applied
This is because scraped data is not made available to RR evalution or
federation atomically, so some buckets are computed with data from the N
most recent scrapes, but the other buckets are missing the most recent
observations.
Monotonicity is usually guaranteed because if a bucket with upper bound
u1 has count c1, then any bucket with a higher upper bound u > u1 must
have counted all c1 observations and perhaps more, so that c >= c1.
Randomly interspersed partial sampling breaks that guarantee, and rate()
exacerbates it. Specifically, suppose bucket le=1000 has a count of 10 from
4 samples but the bucket with le=2000 has a count of 7, from 3 samples. The
monotonicity is broken. It is exacerbated by rate() because under normal
operation, cumulative counting of buckets will cause the bucket counts to
diverge such that small differences from missing samples are not a problem.
rate() removes this divergence.)
bucketQuantile depends on that monotonicity to do a binary search for the
bucket with the qth percentile count, so breaking the monotonicity
guarantee causes bucketQuantile() to return undefined (nonsense) results.
As a somewhat hacky solution until the Prometheus project is ready to
accept the changes required to make scrapes atomic, we calculate the
"envelope" of the histogram buckets, essentially removing any decreases
in the count between successive buckets.
* Fix up comment docs for ensureMonotonic
* ensureMonotonic: Use switch statement
Use switch statement rather than if/else for better readability.
Process the most frequent cases first.
2017-04-14 14:21:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-06 05:11:16 +00:00
|
|
|
|
// quantile calculates the given quantile of a vector of samples.
|
2016-07-08 12:48:48 +00:00
|
|
|
|
//
|
2016-12-24 09:40:09 +00:00
|
|
|
|
// The Vector will be sorted.
|
2016-07-08 12:48:48 +00:00
|
|
|
|
// If 'values' has zero elements, NaN is returned.
|
2022-02-13 13:59:03 +00:00
|
|
|
|
// If q==NaN, NaN is returned.
|
2016-07-08 12:48:48 +00:00
|
|
|
|
// If q<0, -Inf is returned.
|
|
|
|
|
// If q>1, +Inf is returned.
|
2016-12-24 10:37:16 +00:00
|
|
|
|
func quantile(q float64, values vectorByValueHeap) float64 {
|
2022-02-13 13:59:03 +00:00
|
|
|
|
if len(values) == 0 || math.IsNaN(q) {
|
2016-07-08 12:48:48 +00:00
|
|
|
|
return math.NaN()
|
|
|
|
|
}
|
2016-07-08 12:33:20 +00:00
|
|
|
|
if q < 0 {
|
|
|
|
|
return math.Inf(-1)
|
|
|
|
|
}
|
|
|
|
|
if q > 1 {
|
|
|
|
|
return math.Inf(+1)
|
|
|
|
|
}
|
2016-07-08 12:48:48 +00:00
|
|
|
|
sort.Sort(values)
|
2016-07-08 12:33:20 +00:00
|
|
|
|
|
|
|
|
|
n := float64(len(values))
|
|
|
|
|
// When the quantile lies between two samples,
|
|
|
|
|
// we use a weighted average of the two samples.
|
|
|
|
|
rank := q * (n - 1)
|
|
|
|
|
|
|
|
|
|
lowerIndex := math.Max(0, math.Floor(rank))
|
|
|
|
|
upperIndex := math.Min(n-1, lowerIndex+1)
|
|
|
|
|
|
|
|
|
|
weight := rank - math.Floor(rank)
|
promql: Separate `Point` into `FPoint` and `HPoint`
In other words: Instead of having a “polymorphous” `Point` that can
either contain a float value or a histogram value, use an `FPoint` for
floats and an `HPoint` for histograms.
This seemingly small change has a _lot_ of repercussions throughout
the codebase.
The idea here is to avoid the increase in size of `Point` arrays that
happened after native histograms had been added.
The higher-level data structures (`Sample`, `Series`, etc.) are still
“polymorphous”. The same idea could be applied to them, but at each
step the trade-offs needed to be evaluated.
The idea with this change is to do the minimum necessary to get back
to pre-histogram performance for functions that do not touch
histograms. Here are comparisons for the `changes` function. The test
data doesn't include histograms yet. Ideally, there would be no change
in the benchmark result at all.
First runtime v2.39 compared to directly prior to this commit:
```
name old time/op new time/op delta
RangeQuery/expr=changes(a_one[1d]),steps=1-16 391µs ± 2% 542µs ± 1% +38.58% (p=0.000 n=9+8)
RangeQuery/expr=changes(a_one[1d]),steps=10-16 452µs ± 2% 617µs ± 2% +36.48% (p=0.000 n=10+10)
RangeQuery/expr=changes(a_one[1d]),steps=100-16 1.12ms ± 1% 1.36ms ± 2% +21.58% (p=0.000 n=8+10)
RangeQuery/expr=changes(a_one[1d]),steps=1000-16 7.83ms ± 1% 8.94ms ± 1% +14.21% (p=0.000 n=10+10)
RangeQuery/expr=changes(a_ten[1d]),steps=1-16 2.98ms ± 0% 3.30ms ± 1% +10.67% (p=0.000 n=9+10)
RangeQuery/expr=changes(a_ten[1d]),steps=10-16 3.66ms ± 1% 4.10ms ± 1% +11.82% (p=0.000 n=10+10)
RangeQuery/expr=changes(a_ten[1d]),steps=100-16 10.5ms ± 0% 11.8ms ± 1% +12.50% (p=0.000 n=8+10)
RangeQuery/expr=changes(a_ten[1d]),steps=1000-16 77.6ms ± 1% 87.4ms ± 1% +12.63% (p=0.000 n=9+9)
RangeQuery/expr=changes(a_hundred[1d]),steps=1-16 30.4ms ± 2% 32.8ms ± 1% +8.01% (p=0.000 n=10+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=10-16 37.1ms ± 2% 40.6ms ± 2% +9.64% (p=0.000 n=10+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=100-16 105ms ± 1% 117ms ± 1% +11.69% (p=0.000 n=10+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=1000-16 783ms ± 3% 876ms ± 1% +11.83% (p=0.000 n=9+10)
```
And then runtime v2.39 compared to after this commit:
```
name old time/op new time/op delta
RangeQuery/expr=changes(a_one[1d]),steps=1-16 391µs ± 2% 547µs ± 1% +39.84% (p=0.000 n=9+8)
RangeQuery/expr=changes(a_one[1d]),steps=10-16 452µs ± 2% 616µs ± 2% +36.15% (p=0.000 n=10+10)
RangeQuery/expr=changes(a_one[1d]),steps=100-16 1.12ms ± 1% 1.26ms ± 1% +12.20% (p=0.000 n=8+10)
RangeQuery/expr=changes(a_one[1d]),steps=1000-16 7.83ms ± 1% 7.95ms ± 1% +1.59% (p=0.000 n=10+8)
RangeQuery/expr=changes(a_ten[1d]),steps=1-16 2.98ms ± 0% 3.38ms ± 2% +13.49% (p=0.000 n=9+10)
RangeQuery/expr=changes(a_ten[1d]),steps=10-16 3.66ms ± 1% 4.02ms ± 1% +9.80% (p=0.000 n=10+9)
RangeQuery/expr=changes(a_ten[1d]),steps=100-16 10.5ms ± 0% 10.8ms ± 1% +3.08% (p=0.000 n=8+10)
RangeQuery/expr=changes(a_ten[1d]),steps=1000-16 77.6ms ± 1% 78.1ms ± 1% +0.58% (p=0.035 n=9+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=1-16 30.4ms ± 2% 33.5ms ± 4% +10.18% (p=0.000 n=10+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=10-16 37.1ms ± 2% 40.0ms ± 1% +7.98% (p=0.000 n=10+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=100-16 105ms ± 1% 107ms ± 1% +1.92% (p=0.000 n=10+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=1000-16 783ms ± 3% 775ms ± 1% -1.02% (p=0.019 n=9+9)
```
In summary, the runtime doesn't really improve with this change for
queries with just a few steps. For queries with many steps, this
commit essentially reinstates the old performance. This is good
because the many-step queries are the one that matter most (longest
absolute runtime).
In terms of allocations, though, this commit doesn't make a dent at
all (numbers not shown). The reason is that most of the allocations
happen in the sampleRingIterator (in the storage package), which has
to be addressed in a separate commit.
Signed-off-by: beorn7 <beorn@grafana.com>
2022-10-28 14:58:40 +00:00
|
|
|
|
return values[int(lowerIndex)].F*(1-weight) + values[int(upperIndex)].F*weight
|
2016-07-08 12:33:20 +00:00
|
|
|
|
}
|