go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/tsmon/distribution/bucketer.go (about) 1 // Copyright 2015 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package distribution 16 17 import ( 18 "fmt" 19 "math" 20 "sort" 21 ) 22 23 // A Bucketer maps samples into discrete buckets. 24 type Bucketer struct { 25 width, growthFactor float64 26 numFiniteBuckets int 27 lowerBounds []float64 28 } 29 30 // NewBucketer creates a bucketer from custom parameters. 31 func NewBucketer(width, growthFactor float64, numFiniteBuckets int) *Bucketer { 32 b := &Bucketer{ 33 width: width, 34 growthFactor: growthFactor, 35 numFiniteBuckets: numFiniteBuckets, 36 } 37 b.init() 38 return b 39 } 40 41 // FixedWidthBucketer returns a Bucketer that uses numFiniteBuckets+2 buckets: 42 // 43 // bucket[0] covers (-Inf...0) 44 // bucket[i] covers [width*(i-1)...width*i) for i > 0 and i <= numFiniteBuckets 45 // bucket[numFiniteBuckets+1] covers [width*numFiniteBuckets...+Inf) 46 // 47 // The width must be greater than 0, and the number of finite buckets must be 48 // greater than 0. 49 func FixedWidthBucketer(width float64, numFiniteBuckets int) *Bucketer { 50 return NewBucketer(width, 0, numFiniteBuckets) 51 } 52 53 // GeometricBucketer returns a Bucketer that uses numFiniteBuckets+2 buckets: 54 // 55 // bucket[0] covers (−Inf, 1) 56 // bucket[i] covers [growthFactor^(i−1), growthFactor^i) for i > 0 and i <= numFiniteBuckets 57 // bucket[numFiniteBuckets+1] covers [growthFactor^(numFiniteBuckets−1), +Inf) 58 // 59 // growthFactor must be positive, and the number of finite buckets must be 60 // greater than 0. 61 func GeometricBucketer(growthFactor float64, numFiniteBuckets int) *Bucketer { 62 return NewBucketer(0, growthFactor, numFiniteBuckets) 63 } 64 65 // DefaultBucketer is a bucketer with sensible bucket sizes. 66 var DefaultBucketer = GeometricBucketer(math.Pow(10, 0.2), 100) 67 68 // NumFiniteBuckets returns the number of finite buckets. 69 func (b *Bucketer) NumFiniteBuckets() int { return b.numFiniteBuckets } 70 71 // NumBuckets returns the number of buckets including the underflow and overflow 72 // buckets. 73 func (b *Bucketer) NumBuckets() int { return b.numFiniteBuckets + 2 } 74 75 // Width returns the bucket width used to configure this Bucketer. 76 func (b *Bucketer) Width() float64 { return b.width } 77 78 // GrowthFactor returns the growth factor used to configure this Bucketer. 79 func (b *Bucketer) GrowthFactor() float64 { return b.growthFactor } 80 81 // UnderflowBucket returns the index of the underflow bucket. 82 func (b *Bucketer) UnderflowBucket() int { return 0 } 83 84 // OverflowBucket returns the index of the overflow bucket. 85 func (b *Bucketer) OverflowBucket() int { return b.numFiniteBuckets + 1 } 86 87 // Bucket returns the index of the bucket for sample. 88 // TODO(dsansome): consider reimplementing sort.Search inline to avoid overhead 89 // of calling a function to compare two values. 90 func (b *Bucketer) Bucket(sample float64) int { 91 return sort.Search(b.NumBuckets(), func(i int) bool { return sample < b.lowerBounds[i] }) - 1 92 } 93 94 func (b *Bucketer) init() { 95 if b.numFiniteBuckets < 0 { 96 panic(fmt.Sprintf("numFiniteBuckets must be positive (was %d)", b.numFiniteBuckets)) 97 } 98 99 b.lowerBounds = make([]float64, b.NumBuckets()) 100 b.lowerBounds[0] = math.Inf(-1) 101 102 if b.width != 0 { 103 b.fillLinearBounds() 104 } else { 105 b.fillExponentialBounds() 106 } 107 108 // Sanity check that the bucket boundaries are monotonically increasing. 109 var previous float64 110 for i, bound := range b.lowerBounds { 111 if i != 0 && bound <= previous { 112 panic("bucket boundaries must be monotonically increasing") 113 } 114 previous = bound 115 } 116 } 117 118 func (b *Bucketer) fillLinearBounds() { 119 for i := 1; i < b.NumBuckets(); i++ { 120 b.lowerBounds[i] = b.width * float64(i-1) 121 } 122 } 123 124 func (b *Bucketer) fillExponentialBounds() { 125 for i := 1; i < b.NumBuckets(); i++ { 126 b.lowerBounds[i] = math.Pow(b.growthFactor, float64(i-1)) 127 } 128 }