2023-08-04 13:57:44 -05:00
// Copyright The OpenTelemetry Authors
2024-02-29 07:05:28 +01:00
// SPDX-License-Identifier: Apache-2.0
2023-08-04 13:57:44 -05:00
package aggregate // import "go.opentelemetry.io/otel/sdk/metric/internal/aggregate"
import (
"context"
"errors"
"math"
"sync"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
)
const (
expoMaxScale = 20
expoMinScale = - 10
smallestNonZeroNormalFloat64 = 0x1 p - 1022
// These redefine the Math constants with a type, so the compiler won't coerce
// them into an int on 32 bit platforms.
maxInt64 int64 = math . MaxInt64
minInt64 int64 = math . MinInt64
)
// expoHistogramDataPoint is a single data point in an exponential histogram.
type expoHistogramDataPoint [ N int64 | float64 ] struct {
2024-03-06 02:11:16 -08:00
attrs attribute . Set
2024-10-23 10:48:07 -07:00
res FilteredExemplarReservoir [ N ]
2024-01-31 13:15:35 -08:00
2023-08-04 13:57:44 -05:00
count uint64
min N
max N
sum N
maxSize int
noMinMax bool
noSum bool
2024-08-23 08:07:25 -07:00
scale int32
2023-08-04 13:57:44 -05:00
posBuckets expoBuckets
negBuckets expoBuckets
zeroCount uint64
}
2025-02-12 12:23:12 +01:00
func newExpoHistogramDataPoint [ N int64 | float64 ] ( attrs attribute . Set , maxSize int , maxScale int32 , noMinMax , noSum bool ) * expoHistogramDataPoint [ N ] { // nolint:revive // we need this control flag
2023-08-04 13:57:44 -05:00
f := math . MaxFloat64
2024-11-13 08:58:59 +01:00
ma := N ( f ) // if N is int64, max will overflow to -9223372036854775808
mi := N ( - f )
2023-08-04 13:57:44 -05:00
if N ( maxInt64 ) > N ( f ) {
2024-11-13 08:58:59 +01:00
ma = N ( maxInt64 )
mi = N ( minInt64 )
2023-08-04 13:57:44 -05:00
}
return & expoHistogramDataPoint [ N ] {
2024-03-06 02:11:16 -08:00
attrs : attrs ,
2024-11-13 08:58:59 +01:00
min : ma ,
max : mi ,
2023-08-04 13:57:44 -05:00
maxSize : maxSize ,
noMinMax : noMinMax ,
noSum : noSum ,
scale : maxScale ,
}
}
// record adds a new measurement to the histogram. It will rescale the buckets if needed.
func ( p * expoHistogramDataPoint [ N ] ) record ( v N ) {
p . count ++
if ! p . noMinMax {
if v < p . min {
p . min = v
}
if v > p . max {
p . max = v
}
}
if ! p . noSum {
p . sum += v
}
absV := math . Abs ( float64 ( v ) )
if float64 ( absV ) == 0.0 {
p . zeroCount ++
return
}
2023-08-17 15:06:46 -07:00
bin := p . getBin ( absV )
2023-08-04 13:57:44 -05:00
bucket := & p . posBuckets
if v < 0 {
bucket = & p . negBuckets
}
// If the new bin would make the counts larger than maxScale, we need to
// downscale current measurements.
2023-08-17 15:06:46 -07:00
if scaleDelta := p . scaleChange ( bin , bucket . startBin , len ( bucket . counts ) ) ; scaleDelta > 0 {
2023-08-04 13:57:44 -05:00
if p . scale - scaleDelta < expoMinScale {
// With a scale of -10 there is only two buckets for the whole range of float64 values.
// This can only happen if there is a max size of 1.
otel . Handle ( errors . New ( "exponential histogram scale underflow" ) )
return
}
2023-10-16 10:02:21 -07:00
// Downscale
2023-08-04 13:57:44 -05:00
p . scale -= scaleDelta
p . posBuckets . downscale ( scaleDelta )
p . negBuckets . downscale ( scaleDelta )
2023-08-17 15:06:46 -07:00
bin = p . getBin ( absV )
2023-08-04 13:57:44 -05:00
}
bucket . record ( bin )
}
2023-08-17 15:06:46 -07:00
// getBin returns the bin v should be recorded into.
2024-08-23 08:07:25 -07:00
func ( p * expoHistogramDataPoint [ N ] ) getBin ( v float64 ) int32 {
frac , expInt := math . Frexp ( v )
// 11-bit exponential.
exp := int32 ( expInt ) // nolint: gosec
2023-08-17 15:06:46 -07:00
if p . scale <= 0 {
2023-08-04 13:57:44 -05:00
// Because of the choice of fraction is always 1 power of two higher than we want.
2024-08-23 08:07:25 -07:00
var correction int32 = 1
2023-08-04 13:57:44 -05:00
if frac == .5 {
// If v is an exact power of two the frac will be .5 and the exp
// will be one higher than we want.
correction = 2
}
2023-08-17 15:06:46 -07:00
return ( exp - correction ) >> ( - p . scale )
2023-08-04 13:57:44 -05:00
}
2024-08-23 08:07:25 -07:00
return exp << p . scale + int32 ( math . Log ( frac ) * scaleFactors [ p . scale ] ) - 1
2023-08-04 13:57:44 -05:00
}
// scaleFactors are constants used in calculating the logarithm index. They are
// equivalent to 2^index/log(2).
var scaleFactors = [ 21 ] float64 {
math . Ldexp ( math . Log2E , 0 ) ,
math . Ldexp ( math . Log2E , 1 ) ,
math . Ldexp ( math . Log2E , 2 ) ,
math . Ldexp ( math . Log2E , 3 ) ,
math . Ldexp ( math . Log2E , 4 ) ,
math . Ldexp ( math . Log2E , 5 ) ,
math . Ldexp ( math . Log2E , 6 ) ,
math . Ldexp ( math . Log2E , 7 ) ,
math . Ldexp ( math . Log2E , 8 ) ,
math . Ldexp ( math . Log2E , 9 ) ,
math . Ldexp ( math . Log2E , 10 ) ,
math . Ldexp ( math . Log2E , 11 ) ,
math . Ldexp ( math . Log2E , 12 ) ,
math . Ldexp ( math . Log2E , 13 ) ,
math . Ldexp ( math . Log2E , 14 ) ,
math . Ldexp ( math . Log2E , 15 ) ,
math . Ldexp ( math . Log2E , 16 ) ,
math . Ldexp ( math . Log2E , 17 ) ,
math . Ldexp ( math . Log2E , 18 ) ,
math . Ldexp ( math . Log2E , 19 ) ,
math . Ldexp ( math . Log2E , 20 ) ,
}
2023-08-17 15:06:46 -07:00
// scaleChange returns the magnitude of the scale change needed to fit bin in
// the bucket. If no scale change is needed 0 is returned.
2024-08-23 08:07:25 -07:00
func ( p * expoHistogramDataPoint [ N ] ) scaleChange ( bin , startBin int32 , length int ) int32 {
2023-08-04 13:57:44 -05:00
if length == 0 {
// No need to rescale if there are no buckets.
return 0
}
2024-08-23 08:07:25 -07:00
low := int ( startBin )
high := int ( bin )
2023-08-04 13:57:44 -05:00
if startBin >= bin {
2024-08-23 08:07:25 -07:00
low = int ( bin )
high = int ( startBin ) + length - 1
2023-08-04 13:57:44 -05:00
}
2024-08-23 08:07:25 -07:00
var count int32
2023-08-17 15:06:46 -07:00
for high - low >= p . maxSize {
2023-08-04 13:57:44 -05:00
low = low >> 1
high = high >> 1
count ++
if count > expoMaxScale - expoMinScale {
return count
}
}
return count
}
// expoBuckets is a set of buckets in an exponential histogram.
type expoBuckets struct {
2024-08-23 08:07:25 -07:00
startBin int32
2023-08-04 13:57:44 -05:00
counts [ ] uint64
}
// record increments the count for the given bin, and expands the buckets if needed.
// Size changes must be done before calling this function.
2024-08-23 08:07:25 -07:00
func ( b * expoBuckets ) record ( bin int32 ) {
2023-08-04 13:57:44 -05:00
if len ( b . counts ) == 0 {
b . counts = [ ] uint64 { 1 }
b . startBin = bin
return
}
2024-08-23 08:07:25 -07:00
endBin := int ( b . startBin ) + len ( b . counts ) - 1
2023-08-04 13:57:44 -05:00
// if the new bin is inside the current range
2024-08-23 08:07:25 -07:00
if bin >= b . startBin && int ( bin ) <= endBin {
2023-08-04 13:57:44 -05:00
b . counts [ bin - b . startBin ] ++
return
}
// if the new bin is before the current start add spaces to the counts
if bin < b . startBin {
origLen := len ( b . counts )
2024-08-23 08:07:25 -07:00
newLength := endBin - int ( bin ) + 1
2023-08-04 13:57:44 -05:00
shift := b . startBin - bin
if newLength > cap ( b . counts ) {
b . counts = append ( b . counts , make ( [ ] uint64 , newLength - len ( b . counts ) ) ... )
}
2024-08-23 08:07:25 -07:00
copy ( b . counts [ shift : origLen + int ( shift ) ] , b . counts [ : ] )
2023-08-04 13:57:44 -05:00
b . counts = b . counts [ : newLength ]
2024-08-23 08:07:25 -07:00
for i := 1 ; i < int ( shift ) ; i ++ {
2023-08-04 13:57:44 -05:00
b . counts [ i ] = 0
}
b . startBin = bin
b . counts [ 0 ] = 1
return
}
// if the new is after the end add spaces to the end
2024-08-23 08:07:25 -07:00
if int ( bin ) > endBin {
if int ( bin - b . startBin ) < cap ( b . counts ) {
2023-08-04 13:57:44 -05:00
b . counts = b . counts [ : bin - b . startBin + 1 ]
2024-08-23 08:07:25 -07:00
for i := endBin + 1 - int ( b . startBin ) ; i < len ( b . counts ) ; i ++ {
2023-08-04 13:57:44 -05:00
b . counts [ i ] = 0
}
b . counts [ bin - b . startBin ] = 1
return
}
2024-08-23 08:07:25 -07:00
end := make ( [ ] uint64 , int ( bin - b . startBin ) - len ( b . counts ) + 1 )
2023-08-04 13:57:44 -05:00
b . counts = append ( b . counts , end ... )
b . counts [ bin - b . startBin ] = 1
}
}
// downscale shrinks a bucket by a factor of 2*s. It will sum counts into the
// correct lower resolution bucket.
2024-08-23 08:07:25 -07:00
func ( b * expoBuckets ) downscale ( delta int32 ) {
2023-08-04 13:57:44 -05:00
// Example
// delta = 2
// Original offset: -6
// Counts: [ 3, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// bins: -6 -5, -4, -3, -2, -1, 0, 1, 2, 3, 4
// new bins:-2, -2, -1, -1, -1, -1, 0, 0, 0, 0, 1
// new Offset: -2
// new Counts: [4, 14, 30, 10]
if len ( b . counts ) <= 1 || delta < 1 {
b . startBin = b . startBin >> delta
return
}
2024-08-23 08:07:25 -07:00
steps := int32 ( 1 ) << delta
2023-08-04 13:57:44 -05:00
offset := b . startBin % steps
offset = ( offset + steps ) % steps // to make offset positive
for i := 1 ; i < len ( b . counts ) ; i ++ {
2024-08-23 08:07:25 -07:00
idx := i + int ( offset )
if idx % int ( steps ) == 0 {
b . counts [ idx / int ( steps ) ] = b . counts [ i ]
2023-08-04 13:57:44 -05:00
continue
}
2024-08-23 08:07:25 -07:00
b . counts [ idx / int ( steps ) ] += b . counts [ i ]
2023-08-04 13:57:44 -05:00
}
2024-08-23 08:07:25 -07:00
lastIdx := ( len ( b . counts ) - 1 + int ( offset ) ) / int ( steps )
2023-08-04 13:57:44 -05:00
b . counts = b . counts [ : lastIdx + 1 ]
b . startBin = b . startBin >> delta
}
// newExponentialHistogram returns an Aggregator that summarizes a set of
// measurements as an exponential histogram. Each histogram is scoped by attributes
// and the aggregation cycle the measurements were made in.
2024-10-23 10:48:07 -07:00
func newExponentialHistogram [ N int64 | float64 ] ( maxSize , maxScale int32 , noMinMax , noSum bool , limit int , r func ( attribute . Set ) FilteredExemplarReservoir [ N ] ) * expoHistogram [ N ] {
2023-08-04 13:57:44 -05:00
return & expoHistogram [ N ] {
2023-08-17 10:31:02 -07:00
noSum : noSum ,
noMinMax : noMinMax ,
maxSize : int ( maxSize ) ,
2024-08-23 08:07:25 -07:00
maxScale : maxScale ,
2023-08-17 10:31:02 -07:00
2024-01-31 13:15:35 -08:00
newRes : r ,
2023-12-19 07:53:01 -08:00
limit : newLimiter [ * expoHistogramDataPoint [ N ] ] ( limit ) ,
2024-03-06 02:11:16 -08:00
values : make ( map [ attribute . Distinct ] * expoHistogramDataPoint [ N ] ) ,
2023-08-17 10:31:02 -07:00
2023-08-04 13:57:44 -05:00
start : now ( ) ,
}
}
// expoHistogram summarizes a set of measurements as an histogram with exponentially
// defined buckets.
type expoHistogram [ N int64 | float64 ] struct {
2023-08-17 10:31:02 -07:00
noSum bool
noMinMax bool
maxSize int
2024-08-23 08:07:25 -07:00
maxScale int32
2023-08-17 10:31:02 -07:00
2024-10-23 10:48:07 -07:00
newRes func ( attribute . Set ) FilteredExemplarReservoir [ N ]
2023-12-19 07:53:01 -08:00
limit limiter [ * expoHistogramDataPoint [ N ] ]
2024-03-06 02:11:16 -08:00
values map [ attribute . Distinct ] * expoHistogramDataPoint [ N ]
2023-08-17 10:31:02 -07:00
valuesMu sync . Mutex
2023-08-04 13:57:44 -05:00
start time . Time
}
2024-01-31 13:15:35 -08:00
func ( e * expoHistogram [ N ] ) measure ( ctx context . Context , value N , fltrAttr attribute . Set , droppedAttr [ ] attribute . KeyValue ) {
2023-08-18 06:43:09 -07:00
// Ignore NaN and infinity.
if math . IsInf ( float64 ( value ) , 0 ) || math . IsNaN ( float64 ( value ) ) {
return
}
2023-08-17 10:31:02 -07:00
e . valuesMu . Lock ( )
defer e . valuesMu . Unlock ( )
2024-01-31 13:15:35 -08:00
attr := e . limit . Attributes ( fltrAttr , e . values )
2024-03-06 02:11:16 -08:00
v , ok := e . values [ attr . Equivalent ( ) ]
2023-08-17 10:31:02 -07:00
if ! ok {
2024-03-06 02:11:16 -08:00
v = newExpoHistogramDataPoint [ N ] ( attr , e . maxSize , e . maxScale , e . noMinMax , e . noSum )
2024-10-18 09:05:10 -04:00
v . res = e . newRes ( attr )
2024-01-31 13:15:35 -08:00
2024-03-06 02:11:16 -08:00
e . values [ attr . Equivalent ( ) ] = v
2023-08-17 10:31:02 -07:00
}
v . record ( value )
2024-07-01 12:36:11 -04:00
v . res . Offer ( ctx , value , droppedAttr )
2023-08-17 10:31:02 -07:00
}
2023-08-04 13:57:44 -05:00
func ( e * expoHistogram [ N ] ) delta ( dest * metricdata . Aggregation ) int {
t := now ( )
// If *dest is not a metricdata.ExponentialHistogram, memory reuse is missed.
// In that case, use the zero-value h and hope for better alignment next cycle.
h , _ := ( * dest ) . ( metricdata . ExponentialHistogram [ N ] )
h . Temporality = metricdata . DeltaTemporality
e . valuesMu . Lock ( )
defer e . valuesMu . Unlock ( )
n := len ( e . values )
hDPts := reset ( h . DataPoints , n , n )
var i int
2024-03-06 02:11:16 -08:00
for _ , val := range e . values {
hDPts [ i ] . Attributes = val . attrs
2023-08-04 13:57:44 -05:00
hDPts [ i ] . StartTime = e . start
hDPts [ i ] . Time = t
2024-03-06 02:11:16 -08:00
hDPts [ i ] . Count = val . count
2024-08-23 08:07:25 -07:00
hDPts [ i ] . Scale = val . scale
2024-03-06 02:11:16 -08:00
hDPts [ i ] . ZeroCount = val . zeroCount
2023-08-04 13:57:44 -05:00
hDPts [ i ] . ZeroThreshold = 0.0
2024-08-23 08:07:25 -07:00
hDPts [ i ] . PositiveBucket . Offset = val . posBuckets . startBin
2024-03-06 02:11:16 -08:00
hDPts [ i ] . PositiveBucket . Counts = reset ( hDPts [ i ] . PositiveBucket . Counts , len ( val . posBuckets . counts ) , len ( val . posBuckets . counts ) )
copy ( hDPts [ i ] . PositiveBucket . Counts , val . posBuckets . counts )
2023-08-04 13:57:44 -05:00
2024-08-23 08:07:25 -07:00
hDPts [ i ] . NegativeBucket . Offset = val . negBuckets . startBin
2024-03-06 02:11:16 -08:00
hDPts [ i ] . NegativeBucket . Counts = reset ( hDPts [ i ] . NegativeBucket . Counts , len ( val . negBuckets . counts ) , len ( val . negBuckets . counts ) )
copy ( hDPts [ i ] . NegativeBucket . Counts , val . negBuckets . counts )
2023-08-04 13:57:44 -05:00
if ! e . noSum {
2024-03-06 02:11:16 -08:00
hDPts [ i ] . Sum = val . sum
2023-08-04 13:57:44 -05:00
}
if ! e . noMinMax {
2024-03-06 02:11:16 -08:00
hDPts [ i ] . Min = metricdata . NewExtrema ( val . min )
hDPts [ i ] . Max = metricdata . NewExtrema ( val . max )
2023-08-04 13:57:44 -05:00
}
2024-05-07 08:12:59 -07:00
collectExemplars ( & hDPts [ i ] . Exemplars , val . res . Collect )
2024-01-31 13:15:35 -08:00
2023-08-04 13:57:44 -05:00
i ++
}
2024-02-26 23:22:58 -08:00
// Unused attribute sets do not report.
clear ( e . values )
2023-08-04 13:57:44 -05:00
e . start = t
h . DataPoints = hDPts
* dest = h
return n
}
2023-08-10 00:23:25 -07:00
2023-08-04 13:57:44 -05:00
func ( e * expoHistogram [ N ] ) cumulative ( dest * metricdata . Aggregation ) int {
t := now ( )
// If *dest is not a metricdata.ExponentialHistogram, memory reuse is missed.
// In that case, use the zero-value h and hope for better alignment next cycle.
h , _ := ( * dest ) . ( metricdata . ExponentialHistogram [ N ] )
h . Temporality = metricdata . CumulativeTemporality
e . valuesMu . Lock ( )
defer e . valuesMu . Unlock ( )
n := len ( e . values )
hDPts := reset ( h . DataPoints , n , n )
var i int
2024-03-06 02:11:16 -08:00
for _ , val := range e . values {
hDPts [ i ] . Attributes = val . attrs
2023-08-04 13:57:44 -05:00
hDPts [ i ] . StartTime = e . start
hDPts [ i ] . Time = t
2024-03-06 02:11:16 -08:00
hDPts [ i ] . Count = val . count
2024-08-23 08:07:25 -07:00
hDPts [ i ] . Scale = val . scale
2024-03-06 02:11:16 -08:00
hDPts [ i ] . ZeroCount = val . zeroCount
2023-08-04 13:57:44 -05:00
hDPts [ i ] . ZeroThreshold = 0.0
2024-08-23 08:07:25 -07:00
hDPts [ i ] . PositiveBucket . Offset = val . posBuckets . startBin
2024-03-06 02:11:16 -08:00
hDPts [ i ] . PositiveBucket . Counts = reset ( hDPts [ i ] . PositiveBucket . Counts , len ( val . posBuckets . counts ) , len ( val . posBuckets . counts ) )
copy ( hDPts [ i ] . PositiveBucket . Counts , val . posBuckets . counts )
2023-08-04 13:57:44 -05:00
2024-08-23 08:07:25 -07:00
hDPts [ i ] . NegativeBucket . Offset = val . negBuckets . startBin
2024-03-06 02:11:16 -08:00
hDPts [ i ] . NegativeBucket . Counts = reset ( hDPts [ i ] . NegativeBucket . Counts , len ( val . negBuckets . counts ) , len ( val . negBuckets . counts ) )
copy ( hDPts [ i ] . NegativeBucket . Counts , val . negBuckets . counts )
2023-08-04 13:57:44 -05:00
if ! e . noSum {
2024-03-06 02:11:16 -08:00
hDPts [ i ] . Sum = val . sum
2023-08-04 13:57:44 -05:00
}
if ! e . noMinMax {
2024-03-06 02:11:16 -08:00
hDPts [ i ] . Min = metricdata . NewExtrema ( val . min )
hDPts [ i ] . Max = metricdata . NewExtrema ( val . max )
2023-08-04 13:57:44 -05:00
}
2024-05-07 08:12:59 -07:00
collectExemplars ( & hDPts [ i ] . Exemplars , val . res . Collect )
2024-01-31 13:15:35 -08:00
2023-08-04 13:57:44 -05:00
i ++
// TODO (#3006): This will use an unbounded amount of memory if there
// are unbounded number of attribute sets being aggregated. Attribute
// sets that become "stale" need to be forgotten so this will not
// overload the system.
}
h . DataPoints = hDPts
* dest = h
return n
}