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