328 lines
9.6 KiB
Go
328 lines
9.6 KiB
Go
|
// Copyright 2022 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 histogram
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
// BucketCount is a type constraint for the count in a bucket, which can be
|
||
|
// float64 (for type FloatHistogram) or uint64 (for type Histogram).
|
||
|
type BucketCount interface {
|
||
|
float64 | uint64
|
||
|
}
|
||
|
|
||
|
// internalBucketCount is used internally by Histogram and FloatHistogram. The
|
||
|
// difference to the BucketCount above is that Histogram internally uses deltas
|
||
|
// between buckets rather than absolute counts (while FloatHistogram uses
|
||
|
// absolute counts directly). Go type parameters don't allow type
|
||
|
// specialization. Therefore, where special treatment of deltas between buckets
|
||
|
// vs. absolute counts is important, this information has to be provided as a
|
||
|
// separate boolean parameter "deltaBuckets"
|
||
|
type internalBucketCount interface {
|
||
|
float64 | int64
|
||
|
}
|
||
|
|
||
|
// Bucket represents a bucket with lower and upper limit and the absolute count
|
||
|
// of samples in the bucket. It also specifies if each limit is inclusive or
|
||
|
// not. (Mathematically, inclusive limits create a closed interval, and
|
||
|
// non-inclusive limits an open interval.)
|
||
|
//
|
||
|
// To represent cumulative buckets, Lower is set to -Inf, and the Count is then
|
||
|
// cumulative (including the counts of all buckets for smaller values).
|
||
|
type Bucket[BC BucketCount] struct {
|
||
|
Lower, Upper float64
|
||
|
LowerInclusive, UpperInclusive bool
|
||
|
Count BC
|
||
|
|
||
|
// Index within schema. To easily compare buckets that share the same
|
||
|
// schema and sign (positive or negative). Irrelevant for the zero bucket.
|
||
|
Index int32
|
||
|
}
|
||
|
|
||
|
// String returns a string representation of a Bucket, using the usual
|
||
|
// mathematical notation of '['/']' for inclusive bounds and '('/')' for
|
||
|
// non-inclusive bounds.
|
||
|
func (b Bucket[BC]) String() string {
|
||
|
var sb strings.Builder
|
||
|
if b.LowerInclusive {
|
||
|
sb.WriteRune('[')
|
||
|
} else {
|
||
|
sb.WriteRune('(')
|
||
|
}
|
||
|
fmt.Fprintf(&sb, "%g,%g", b.Lower, b.Upper)
|
||
|
if b.UpperInclusive {
|
||
|
sb.WriteRune(']')
|
||
|
} else {
|
||
|
sb.WriteRune(')')
|
||
|
}
|
||
|
fmt.Fprintf(&sb, ":%v", b.Count)
|
||
|
return sb.String()
|
||
|
}
|
||
|
|
||
|
// BucketIterator iterates over the buckets of a Histogram, returning decoded
|
||
|
// buckets.
|
||
|
type BucketIterator[BC BucketCount] interface {
|
||
|
// Next advances the iterator by one.
|
||
|
Next() bool
|
||
|
// At returns the current bucket.
|
||
|
At() Bucket[BC]
|
||
|
}
|
||
|
|
||
|
// baseBucketIterator provides a struct that is shared by most BucketIterator
|
||
|
// implementations, together with an implementation of the At method. This
|
||
|
// iterator can be embedded in full implementations of BucketIterator to save on
|
||
|
// code replication.
|
||
|
type baseBucketIterator[BC BucketCount, IBC internalBucketCount] struct {
|
||
|
schema int32
|
||
|
spans []Span
|
||
|
buckets []IBC
|
||
|
|
||
|
positive bool // Whether this is for positive buckets.
|
||
|
|
||
|
spansIdx int // Current span within spans slice.
|
||
|
idxInSpan uint32 // Index in the current span. 0 <= idxInSpan < span.Length.
|
||
|
bucketsIdx int // Current bucket within buckets slice.
|
||
|
|
||
|
currCount IBC // Count in the current bucket.
|
||
|
currIdx int32 // The actual bucket index.
|
||
|
}
|
||
|
|
||
|
func (b baseBucketIterator[BC, IBC]) At() Bucket[BC] {
|
||
|
bucket := Bucket[BC]{
|
||
|
Count: BC(b.currCount),
|
||
|
Index: b.currIdx,
|
||
|
}
|
||
|
if b.positive {
|
||
|
bucket.Upper = getBound(b.currIdx, b.schema)
|
||
|
bucket.Lower = getBound(b.currIdx-1, b.schema)
|
||
|
} else {
|
||
|
bucket.Lower = -getBound(b.currIdx, b.schema)
|
||
|
bucket.Upper = -getBound(b.currIdx-1, b.schema)
|
||
|
}
|
||
|
bucket.LowerInclusive = bucket.Lower < 0
|
||
|
bucket.UpperInclusive = bucket.Upper > 0
|
||
|
return bucket
|
||
|
}
|
||
|
|
||
|
// compactBuckets is a generic function used by both Histogram.Compact and
|
||
|
// FloatHistogram.Compact. Set deltaBuckets to true if the provided buckets are
|
||
|
// deltas. Set it to false if the buckets contain absolute counts.
|
||
|
func compactBuckets[IBC internalBucketCount](buckets []IBC, spans []Span, maxEmptyBuckets int, deltaBuckets bool) ([]IBC, []Span) {
|
||
|
// Fast path: If there are no empty buckets AND no offset in any span is
|
||
|
// <= maxEmptyBuckets AND no span has length 0, there is nothing to do and we can return
|
||
|
// immediately. We check that first because it's cheap and presumably
|
||
|
// common.
|
||
|
nothingToDo := true
|
||
|
var currentBucketAbsolute IBC
|
||
|
for _, bucket := range buckets {
|
||
|
if deltaBuckets {
|
||
|
currentBucketAbsolute += bucket
|
||
|
} else {
|
||
|
currentBucketAbsolute = bucket
|
||
|
}
|
||
|
if currentBucketAbsolute == 0 {
|
||
|
nothingToDo = false
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
if nothingToDo {
|
||
|
for _, span := range spans {
|
||
|
if int(span.Offset) <= maxEmptyBuckets || span.Length == 0 {
|
||
|
nothingToDo = false
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
if nothingToDo {
|
||
|
return buckets, spans
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var iBucket, iSpan int
|
||
|
var posInSpan uint32
|
||
|
currentBucketAbsolute = 0
|
||
|
|
||
|
// Helper function.
|
||
|
emptyBucketsHere := func() int {
|
||
|
i := 0
|
||
|
abs := currentBucketAbsolute
|
||
|
for uint32(i)+posInSpan < spans[iSpan].Length && abs == 0 {
|
||
|
i++
|
||
|
if i+iBucket >= len(buckets) {
|
||
|
break
|
||
|
}
|
||
|
abs = buckets[i+iBucket]
|
||
|
}
|
||
|
return i
|
||
|
}
|
||
|
|
||
|
// Merge spans with zero-offset to avoid special cases later.
|
||
|
if len(spans) > 1 {
|
||
|
for i, span := range spans[1:] {
|
||
|
if span.Offset == 0 {
|
||
|
spans[iSpan].Length += span.Length
|
||
|
continue
|
||
|
}
|
||
|
iSpan++
|
||
|
if i+1 != iSpan {
|
||
|
spans[iSpan] = span
|
||
|
}
|
||
|
}
|
||
|
spans = spans[:iSpan+1]
|
||
|
iSpan = 0
|
||
|
}
|
||
|
|
||
|
// Merge spans with zero-length to avoid special cases later.
|
||
|
for i, span := range spans {
|
||
|
if span.Length == 0 {
|
||
|
if i+1 < len(spans) {
|
||
|
spans[i+1].Offset += span.Offset
|
||
|
}
|
||
|
continue
|
||
|
}
|
||
|
if i != iSpan {
|
||
|
spans[iSpan] = span
|
||
|
}
|
||
|
iSpan++
|
||
|
}
|
||
|
spans = spans[:iSpan]
|
||
|
iSpan = 0
|
||
|
|
||
|
// Cut out empty buckets from start and end of spans, no matter
|
||
|
// what. Also cut out empty buckets from the middle of a span but only
|
||
|
// if there are more than maxEmptyBuckets consecutive empty buckets.
|
||
|
for iBucket < len(buckets) {
|
||
|
if deltaBuckets {
|
||
|
currentBucketAbsolute += buckets[iBucket]
|
||
|
} else {
|
||
|
currentBucketAbsolute = buckets[iBucket]
|
||
|
}
|
||
|
if nEmpty := emptyBucketsHere(); nEmpty > 0 {
|
||
|
if posInSpan > 0 &&
|
||
|
nEmpty < int(spans[iSpan].Length-posInSpan) &&
|
||
|
nEmpty <= maxEmptyBuckets {
|
||
|
// The empty buckets are in the middle of a
|
||
|
// span, and there are few enough to not bother.
|
||
|
// Just fast-forward.
|
||
|
iBucket += nEmpty
|
||
|
if deltaBuckets {
|
||
|
currentBucketAbsolute = 0
|
||
|
}
|
||
|
posInSpan += uint32(nEmpty)
|
||
|
continue
|
||
|
}
|
||
|
// In all other cases, we cut out the empty buckets.
|
||
|
if deltaBuckets && iBucket+nEmpty < len(buckets) {
|
||
|
currentBucketAbsolute = -buckets[iBucket]
|
||
|
buckets[iBucket+nEmpty] += buckets[iBucket]
|
||
|
}
|
||
|
buckets = append(buckets[:iBucket], buckets[iBucket+nEmpty:]...)
|
||
|
if posInSpan == 0 {
|
||
|
// Start of span.
|
||
|
if nEmpty == int(spans[iSpan].Length) {
|
||
|
// The whole span is empty.
|
||
|
offset := spans[iSpan].Offset
|
||
|
spans = append(spans[:iSpan], spans[iSpan+1:]...)
|
||
|
if len(spans) > iSpan {
|
||
|
spans[iSpan].Offset += offset + int32(nEmpty)
|
||
|
}
|
||
|
continue
|
||
|
}
|
||
|
spans[iSpan].Length -= uint32(nEmpty)
|
||
|
spans[iSpan].Offset += int32(nEmpty)
|
||
|
continue
|
||
|
}
|
||
|
// It's in the middle or in the end of the span.
|
||
|
// Split the current span.
|
||
|
newSpan := Span{
|
||
|
Offset: int32(nEmpty),
|
||
|
Length: spans[iSpan].Length - posInSpan - uint32(nEmpty),
|
||
|
}
|
||
|
spans[iSpan].Length = posInSpan
|
||
|
// In any case, we have to split to the next span.
|
||
|
iSpan++
|
||
|
posInSpan = 0
|
||
|
if newSpan.Length == 0 {
|
||
|
// The span is empty, so we were already at the end of a span.
|
||
|
// We don't have to insert the new span, just adjust the next
|
||
|
// span's offset, if there is one.
|
||
|
if iSpan < len(spans) {
|
||
|
spans[iSpan].Offset += int32(nEmpty)
|
||
|
}
|
||
|
continue
|
||
|
}
|
||
|
// Insert the new span.
|
||
|
spans = append(spans, Span{})
|
||
|
if iSpan+1 < len(spans) {
|
||
|
copy(spans[iSpan+1:], spans[iSpan:])
|
||
|
}
|
||
|
spans[iSpan] = newSpan
|
||
|
continue
|
||
|
}
|
||
|
iBucket++
|
||
|
posInSpan++
|
||
|
if posInSpan >= spans[iSpan].Length {
|
||
|
posInSpan = 0
|
||
|
iSpan++
|
||
|
}
|
||
|
}
|
||
|
if maxEmptyBuckets == 0 || len(buckets) == 0 {
|
||
|
return buckets, spans
|
||
|
}
|
||
|
|
||
|
// Finally, check if any offsets between spans are small enough to merge
|
||
|
// the spans.
|
||
|
iBucket = int(spans[0].Length)
|
||
|
if deltaBuckets {
|
||
|
currentBucketAbsolute = 0
|
||
|
for _, bucket := range buckets[:iBucket] {
|
||
|
currentBucketAbsolute += bucket
|
||
|
}
|
||
|
}
|
||
|
iSpan = 1
|
||
|
for iSpan < len(spans) {
|
||
|
if int(spans[iSpan].Offset) > maxEmptyBuckets {
|
||
|
l := int(spans[iSpan].Length)
|
||
|
if deltaBuckets {
|
||
|
for _, bucket := range buckets[iBucket : iBucket+l] {
|
||
|
currentBucketAbsolute += bucket
|
||
|
}
|
||
|
}
|
||
|
iBucket += l
|
||
|
iSpan++
|
||
|
continue
|
||
|
}
|
||
|
// Merge span with previous one and insert empty buckets.
|
||
|
offset := int(spans[iSpan].Offset)
|
||
|
spans[iSpan-1].Length += uint32(offset) + spans[iSpan].Length
|
||
|
spans = append(spans[:iSpan], spans[iSpan+1:]...)
|
||
|
newBuckets := make([]IBC, len(buckets)+offset)
|
||
|
copy(newBuckets, buckets[:iBucket])
|
||
|
copy(newBuckets[iBucket+offset:], buckets[iBucket:])
|
||
|
if deltaBuckets {
|
||
|
newBuckets[iBucket] = -currentBucketAbsolute
|
||
|
newBuckets[iBucket+offset] += currentBucketAbsolute
|
||
|
}
|
||
|
iBucket += offset
|
||
|
buckets = newBuckets
|
||
|
currentBucketAbsolute = buckets[iBucket]
|
||
|
// Note that with many merges, it would be more efficient to
|
||
|
// first record all the chunks of empty buckets to insert and
|
||
|
// then do it in one go through all the buckets.
|
||
|
}
|
||
|
|
||
|
return buckets, spans
|
||
|
}
|