histogram.go raw

   1  /*
   2   * SPDX-FileCopyrightText: © Hypermode Inc. <hello@hypermode.com>
   3   * SPDX-License-Identifier: Apache-2.0
   4   */
   5  
   6  package z
   7  
   8  import (
   9  	"fmt"
  10  	"math"
  11  	"strings"
  12  
  13  	"github.com/dustin/go-humanize"
  14  )
  15  
  16  // Creates bounds for an histogram. The bounds are powers of two of the form
  17  // [2^min_exponent, ..., 2^max_exponent].
  18  func HistogramBounds(minExponent, maxExponent uint32) []float64 {
  19  	var bounds []float64
  20  	for i := minExponent; i <= maxExponent; i++ {
  21  		bounds = append(bounds, float64(int(1)<<i))
  22  	}
  23  	return bounds
  24  }
  25  
  26  func Fibonacci(num int) []float64 {
  27  	assert(num > 4)
  28  	bounds := make([]float64, num)
  29  	bounds[0] = 1
  30  	bounds[1] = 2
  31  	for i := 2; i < num; i++ {
  32  		bounds[i] = bounds[i-1] + bounds[i-2]
  33  	}
  34  	return bounds
  35  }
  36  
  37  // HistogramData stores the information needed to represent the sizes of the keys and values
  38  // as a histogram.
  39  type HistogramData struct {
  40  	Bounds         []float64
  41  	Count          int64
  42  	CountPerBucket []int64
  43  	Min            int64
  44  	Max            int64
  45  	Sum            int64
  46  }
  47  
  48  // NewHistogramData returns a new instance of HistogramData with properly initialized fields.
  49  func NewHistogramData(bounds []float64) *HistogramData {
  50  	return &HistogramData{
  51  		Bounds:         bounds,
  52  		CountPerBucket: make([]int64, len(bounds)+1),
  53  		Max:            0,
  54  		Min:            math.MaxInt64,
  55  	}
  56  }
  57  
  58  func (histogram *HistogramData) Copy() *HistogramData {
  59  	if histogram == nil {
  60  		return nil
  61  	}
  62  	return &HistogramData{
  63  		Bounds:         append([]float64{}, histogram.Bounds...),
  64  		CountPerBucket: append([]int64{}, histogram.CountPerBucket...),
  65  		Count:          histogram.Count,
  66  		Min:            histogram.Min,
  67  		Max:            histogram.Max,
  68  		Sum:            histogram.Sum,
  69  	}
  70  }
  71  
  72  // Update changes the Min and Max fields if value is less than or greater than the current values.
  73  func (histogram *HistogramData) Update(value int64) {
  74  	if histogram == nil {
  75  		return
  76  	}
  77  	if value > histogram.Max {
  78  		histogram.Max = value
  79  	}
  80  	if value < histogram.Min {
  81  		histogram.Min = value
  82  	}
  83  
  84  	histogram.Sum += value
  85  	histogram.Count++
  86  
  87  	for index := 0; index <= len(histogram.Bounds); index++ {
  88  		// Allocate value in the last buckets if we reached the end of the Bounds array.
  89  		if index == len(histogram.Bounds) {
  90  			histogram.CountPerBucket[index]++
  91  			break
  92  		}
  93  
  94  		if value < int64(histogram.Bounds[index]) {
  95  			histogram.CountPerBucket[index]++
  96  			break
  97  		}
  98  	}
  99  }
 100  
 101  // Mean returns the mean value for the histogram.
 102  func (histogram *HistogramData) Mean() float64 {
 103  	if histogram.Count == 0 {
 104  		return 0
 105  	}
 106  	return float64(histogram.Sum) / float64(histogram.Count)
 107  }
 108  
 109  // String converts the histogram data into human-readable string.
 110  func (histogram *HistogramData) String() string {
 111  	if histogram == nil {
 112  		return ""
 113  	}
 114  	var b strings.Builder
 115  
 116  	b.WriteString("\n -- Histogram: \n")
 117  	b.WriteString(fmt.Sprintf("Min value: %d \n", histogram.Min))
 118  	b.WriteString(fmt.Sprintf("Max value: %d \n", histogram.Max))
 119  	b.WriteString(fmt.Sprintf("Count: %d \n", histogram.Count))
 120  	b.WriteString(fmt.Sprintf("50p: %.2f \n", histogram.Percentile(0.5)))
 121  	b.WriteString(fmt.Sprintf("75p: %.2f \n", histogram.Percentile(0.75)))
 122  	b.WriteString(fmt.Sprintf("90p: %.2f \n", histogram.Percentile(0.90)))
 123  
 124  	numBounds := len(histogram.Bounds)
 125  	var cum float64
 126  	for index, count := range histogram.CountPerBucket {
 127  		if count == 0 {
 128  			continue
 129  		}
 130  
 131  		// The last bucket represents the bucket that contains the range from
 132  		// the last bound up to infinity so it's processed differently than the
 133  		// other buckets.
 134  		if index == len(histogram.CountPerBucket)-1 {
 135  			lowerBound := uint64(histogram.Bounds[numBounds-1])
 136  			page := float64(count*100) / float64(histogram.Count)
 137  			cum += page
 138  			b.WriteString(fmt.Sprintf("[%s, %s) %d %.2f%% %.2f%%\n",
 139  				humanize.IBytes(lowerBound), "infinity", count, page, cum))
 140  			continue
 141  		}
 142  
 143  		upperBound := uint64(histogram.Bounds[index])
 144  		lowerBound := uint64(0)
 145  		if index > 0 {
 146  			lowerBound = uint64(histogram.Bounds[index-1])
 147  		}
 148  
 149  		page := float64(count*100) / float64(histogram.Count)
 150  		cum += page
 151  		b.WriteString(fmt.Sprintf("[%d, %d) %d %.2f%% %.2f%%\n",
 152  			lowerBound, upperBound, count, page, cum))
 153  	}
 154  	b.WriteString(" --\n")
 155  	return b.String()
 156  }
 157  
 158  // Percentile returns the percentile value for the histogram.
 159  // value of p should be between [0.0-1.0]
 160  func (histogram *HistogramData) Percentile(p float64) float64 {
 161  	if histogram == nil {
 162  		return 0
 163  	}
 164  
 165  	if histogram.Count == 0 {
 166  		// if no data return the minimum range
 167  		return histogram.Bounds[0]
 168  	}
 169  	pval := int64(float64(histogram.Count) * p)
 170  	for i, v := range histogram.CountPerBucket {
 171  		pval = pval - v
 172  		if pval <= 0 {
 173  			if i == len(histogram.Bounds) {
 174  				break
 175  			}
 176  			return histogram.Bounds[i]
 177  		}
 178  	}
 179  	// default return should be the max range
 180  	return histogram.Bounds[len(histogram.Bounds)-1]
 181  }
 182  
 183  // Clear reset the histogram. Helpful in situations where we need to reset the metrics
 184  func (histogram *HistogramData) Clear() {
 185  	if histogram == nil {
 186  		return
 187  	}
 188  
 189  	histogram.Count = 0
 190  	histogram.CountPerBucket = make([]int64, len(histogram.Bounds)+1)
 191  	histogram.Sum = 0
 192  	histogram.Max = 0
 193  	histogram.Min = math.MaxInt64
 194  }
 195