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  }