github.com/loov/hrtime@v1.0.3/histogram.go (about) 1 package hrtime 2 3 import ( 4 "fmt" 5 "io" 6 "math" 7 "sort" 8 "strings" 9 "time" 10 ) 11 12 // HistogramOptions is configuration. 13 type HistogramOptions struct { 14 BinCount int 15 // NiceRange will try to round the bucket sizes to have a nicer output. 16 NiceRange bool 17 // Clamp values to either percentile or to a specific ns value. 18 ClampMaximum float64 19 ClampPercentile float64 20 } 21 22 var defaultOptions = HistogramOptions{ 23 BinCount: 10, 24 NiceRange: true, 25 ClampMaximum: 0, 26 ClampPercentile: 0.999, 27 } 28 29 // Histogram is a binned historgram with different statistics. 30 type Histogram struct { 31 Minimum float64 32 Average float64 33 Maximum float64 34 35 P50, P90, P99, P999, P9999 float64 36 37 Bins []HistogramBin 38 39 // for pretty printing 40 Width int 41 } 42 43 // HistogramBin is a single bin in histogram 44 type HistogramBin struct { 45 Start float64 46 Count int 47 Width float64 48 andAbove bool 49 } 50 51 // NewDurationHistogram creates a histogram from time.Duration-s. 52 func NewDurationHistogram(durations []time.Duration, opts *HistogramOptions) *Histogram { 53 nanos := make([]float64, len(durations)) 54 for i, d := range durations { 55 nanos[i] = float64(d.Nanoseconds()) 56 } 57 return NewHistogram(nanos, opts) 58 } 59 60 // NewHistogram creates a new histogram from the specified nanosecond values. 61 func NewHistogram(nanoseconds []float64, opts *HistogramOptions) *Histogram { 62 if opts.BinCount <= 0 { 63 panic("binCount must be larger than 0") 64 } 65 66 hist := &Histogram{} 67 hist.Width = 40 68 hist.Bins = make([]HistogramBin, opts.BinCount) 69 if len(nanoseconds) == 0 { 70 return hist 71 } 72 73 nanoseconds = append(nanoseconds[:0:0], nanoseconds...) 74 sort.Float64s(nanoseconds) 75 76 hist.Minimum = nanoseconds[0] 77 hist.Maximum = nanoseconds[len(nanoseconds)-1] 78 79 hist.Average = float64(0) 80 for _, x := range nanoseconds { 81 hist.Average += x 82 } 83 hist.Average /= float64(len(nanoseconds)) 84 85 p := func(p float64) float64 { 86 i := int(math.Round(p * float64(len(nanoseconds)))) 87 if i < 0 { 88 i = 0 89 } 90 if i >= len(nanoseconds) { 91 i = len(nanoseconds) - 1 92 } 93 return nanoseconds[i] 94 } 95 96 hist.P50, hist.P90, hist.P99, hist.P999, hist.P9999 = p(0.50), p(0.90), p(0.99), p(0.999), p(0.9999) 97 98 clampMaximum := hist.Maximum 99 if opts.ClampPercentile > 0 { 100 clampMaximum = p(opts.ClampPercentile) 101 } 102 if opts.ClampMaximum > 0 { 103 clampMaximum = opts.ClampMaximum 104 } 105 106 var minimum, spacing float64 107 108 if opts.NiceRange { 109 minimum, spacing = calculateNiceSteps(hist.Minimum, clampMaximum, opts.BinCount) 110 } else { 111 minimum, spacing = calculateSteps(hist.Minimum, clampMaximum, opts.BinCount) 112 } 113 114 for i := range hist.Bins { 115 hist.Bins[i].Start = spacing*float64(i) + minimum 116 } 117 hist.Bins[0].Start = hist.Minimum 118 119 for _, x := range nanoseconds { 120 k := int(float64(x-minimum) / spacing) 121 if k < 0 { 122 k = 0 123 } 124 if k >= opts.BinCount { 125 k = opts.BinCount - 1 126 hist.Bins[k].andAbove = true 127 } 128 hist.Bins[k].Count++ 129 } 130 131 maxBin := 0 132 for _, bin := range hist.Bins { 133 if bin.Count > maxBin { 134 maxBin = bin.Count 135 } 136 } 137 138 for k := range hist.Bins { 139 bin := &hist.Bins[k] 140 bin.Width = float64(bin.Count) / float64(maxBin) 141 } 142 143 return hist 144 } 145 146 // Divide divides histogram by number of repetitions for the tests. 147 func (hist *Histogram) Divide(n int) { 148 hist.Minimum /= float64(n) 149 hist.Average /= float64(n) 150 hist.Maximum /= float64(n) 151 152 hist.P50 /= float64(n) 153 hist.P90 /= float64(n) 154 hist.P99 /= float64(n) 155 hist.P999 /= float64(n) 156 hist.P9999 /= float64(n) 157 158 for i := range hist.Bins { 159 hist.Bins[i].Start /= float64(n) 160 } 161 } 162 163 // WriteStatsTo writes formatted statistics to w. 164 func (hist *Histogram) WriteStatsTo(w io.Writer) (int64, error) { 165 n, err := fmt.Fprintf(w, " avg %v; min %v; p50 %v; max %v;\n p90 %v; p99 %v; p999 %v; p9999 %v;\n", 166 time.Duration(truncate(hist.Average, 3)), 167 time.Duration(truncate(hist.Minimum, 3)), 168 time.Duration(truncate(hist.P50, 3)), 169 time.Duration(truncate(hist.Maximum, 3)), 170 171 time.Duration(truncate(hist.P90, 3)), 172 time.Duration(truncate(hist.P99, 3)), 173 time.Duration(truncate(hist.P999, 3)), 174 time.Duration(truncate(hist.P9999, 3)), 175 ) 176 return int64(n), err 177 } 178 179 // WriteTo writes formatted statistics and histogram to w. 180 func (hist *Histogram) WriteTo(w io.Writer) (int64, error) { 181 written, err := hist.WriteStatsTo(w) 182 if err != nil { 183 return written, err 184 } 185 186 // TODO: use consistently single unit instead of multiple 187 maxCountLength := 3 188 for i := range hist.Bins { 189 x := (int)(math.Ceil(math.Log10(float64(hist.Bins[i].Count + 1)))) 190 if x > maxCountLength { 191 maxCountLength = x 192 } 193 } 194 195 var n int 196 for _, bin := range hist.Bins { 197 if bin.andAbove { 198 n, err = fmt.Fprintf(w, " %10v+[%[2]*[3]v] ", time.Duration(round(bin.Start, 3)), maxCountLength, bin.Count) 199 } else { 200 n, err = fmt.Fprintf(w, " %10v [%[2]*[3]v] ", time.Duration(round(bin.Start, 3)), maxCountLength, bin.Count) 201 } 202 203 written += int64(n) 204 if err != nil { 205 return written, err 206 } 207 208 width := float64(hist.Width) * bin.Width 209 frac := width - math.Trunc(width) 210 211 n, err = io.WriteString(w, strings.Repeat("█", int(width))) 212 written += int64(n) 213 if err != nil { 214 return written, err 215 } 216 217 if frac > 0.5 { 218 n, err = io.WriteString(w, `▌`) 219 written += int64(n) 220 if err != nil { 221 return written, err 222 } 223 } 224 225 n, err = fmt.Fprintf(w, "\n") 226 written += int64(n) 227 if err != nil { 228 return written, err 229 } 230 } 231 return written, nil 232 } 233 234 // StringStats returns a string representation of the histogram stats. 235 func (hist *Histogram) StringStats() string { 236 var buffer strings.Builder 237 _, _ = hist.WriteStatsTo(&buffer) 238 return buffer.String() 239 } 240 241 // String returns a string representation of the histogram. 242 func (hist *Histogram) String() string { 243 var buffer strings.Builder 244 _, _ = hist.WriteTo(&buffer) 245 return buffer.String() 246 }