github.com/psiphon-labs/goarista@v0.0.0-20160825065156-d002785f4c67/monitor/stats/histogram.go (about) 1 package stats 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "strconv" 8 "strings" 9 "time" 10 ) 11 12 // HistogramValue is the value of Histogram objects. 13 type HistogramValue struct { 14 // Count is the total number of values added to the histogram. 15 Count int64 16 // Sum is the sum of all the values added to the histogram. 17 Sum int64 18 // Min is the minimum of all the values added to the histogram. 19 Min int64 20 // Max is the maximum of all the values added to the histogram. 21 Max int64 22 // Buckets contains all the buckets of the histogram. 23 Buckets []HistogramBucket 24 } 25 26 // HistogramBucket is one histogram bucket. 27 type HistogramBucket struct { 28 // LowBound is the lower bound of the bucket. 29 LowBound int64 30 // Count is the number of values in the bucket. 31 Count int64 32 } 33 34 // Print writes textual output of the histogram values. 35 func (v HistogramValue) Print(w io.Writer) { 36 avg := float64(v.Sum) / float64(v.Count) 37 fmt.Fprintf(w, "Count: %d Min: %d Max: %d Avg: %.2f\n", v.Count, v.Min, v.Max, avg) 38 fmt.Fprintf(w, "%s\n", strings.Repeat("-", 60)) 39 if v.Count <= 0 { 40 return 41 } 42 43 maxBucketDigitLen := len(strconv.FormatInt(v.Buckets[len(v.Buckets)-1].LowBound, 10)) 44 if maxBucketDigitLen < 3 { 45 // For "inf". 46 maxBucketDigitLen = 3 47 } 48 maxCountDigitLen := len(strconv.FormatInt(v.Count, 10)) 49 percentMulti := 100 / float64(v.Count) 50 51 accCount := int64(0) 52 for i, b := range v.Buckets { 53 fmt.Fprintf(w, "[%*d, ", maxBucketDigitLen, b.LowBound) 54 if i+1 < len(v.Buckets) { 55 fmt.Fprintf(w, "%*d)", maxBucketDigitLen, v.Buckets[i+1].LowBound) 56 } else { 57 fmt.Fprintf(w, "%*s)", maxBucketDigitLen, "inf") 58 } 59 60 accCount += b.Count 61 fmt.Fprintf(w, " %*d %5.1f%% %5.1f%%", maxCountDigitLen, b.Count, 62 float64(b.Count)*percentMulti, float64(accCount)*percentMulti) 63 64 const barScale = 0.1 65 barLength := int(float64(b.Count)*percentMulti*barScale + 0.5) 66 fmt.Fprintf(w, " %s\n", strings.Repeat("#", barLength)) 67 } 68 } 69 70 // String returns the textual output of the histogram values as string. 71 func (v HistogramValue) String() string { 72 var b bytes.Buffer 73 v.Print(&b) 74 return b.String() 75 } 76 77 // A Histogram accumulates values in the form of a histogram. The type of the 78 // values is int64, which is suitable for keeping track of things like RPC 79 // latency in milliseconds. New histogram objects should be obtained via the 80 // New() function. 81 type Histogram struct { 82 opts HistogramOptions 83 buckets []bucketInternal 84 count *Counter 85 sum *Counter 86 tracker *Tracker 87 } 88 89 // HistogramOptions contains the parameters that define the histogram's buckets. 90 type HistogramOptions struct { 91 // NumBuckets is the number of buckets. 92 NumBuckets int 93 // GrowthFactor is the growth factor of the buckets. A value of 0.1 94 // indicates that bucket N+1 will be 10% larger than bucket N. 95 GrowthFactor float64 96 // SmallestBucketSize is the size of the first bucket. Bucket sizes are 97 // rounded down to the nearest integer. 98 SmallestBucketSize float64 99 // MinValue is the lower bound of the first bucket. 100 MinValue int64 101 } 102 103 // bucketInternal is the internal representation of a bucket, which includes a 104 // rate counter. 105 type bucketInternal struct { 106 lowBound int64 107 count *Counter 108 } 109 110 // NewHistogram returns a pointer to a new Histogram object that was created 111 // with the provided options. 112 func NewHistogram(opts HistogramOptions) *Histogram { 113 if opts.NumBuckets == 0 { 114 opts.NumBuckets = 32 115 } 116 if opts.SmallestBucketSize == 0.0 { 117 opts.SmallestBucketSize = 1.0 118 } 119 h := Histogram{ 120 opts: opts, 121 buckets: make([]bucketInternal, opts.NumBuckets), 122 count: newCounter(), 123 sum: newCounter(), 124 tracker: newTracker(), 125 } 126 low := opts.MinValue 127 delta := opts.SmallestBucketSize 128 for i := 0; i < opts.NumBuckets; i++ { 129 h.buckets[i].lowBound = low 130 h.buckets[i].count = newCounter() 131 low = low + int64(delta) 132 delta = delta * (1.0 + opts.GrowthFactor) 133 } 134 return &h 135 } 136 137 // Opts returns a copy of the options used to create the Histogram. 138 func (h *Histogram) Opts() HistogramOptions { 139 return h.opts 140 } 141 142 // Add adds a value to the histogram. 143 func (h *Histogram) Add(value int64) error { 144 bucket, err := h.findBucket(value) 145 if err != nil { 146 return err 147 } 148 h.buckets[bucket].count.Incr(1) 149 h.count.Incr(1) 150 h.sum.Incr(value) 151 h.tracker.Push(value) 152 return nil 153 } 154 155 // LastUpdate returns the time at which the object was last updated. 156 func (h *Histogram) LastUpdate() time.Time { 157 return h.count.LastUpdate() 158 } 159 160 // Value returns the accumulated state of the histogram since it was created. 161 func (h *Histogram) Value() HistogramValue { 162 b := make([]HistogramBucket, len(h.buckets)) 163 for i, v := range h.buckets { 164 b[i] = HistogramBucket{ 165 LowBound: v.lowBound, 166 Count: v.count.Value(), 167 } 168 } 169 170 v := HistogramValue{ 171 Count: h.count.Value(), 172 Sum: h.sum.Value(), 173 Min: h.tracker.Min(), 174 Max: h.tracker.Max(), 175 Buckets: b, 176 } 177 return v 178 } 179 180 // Delta1h returns the change in the last hour. 181 func (h *Histogram) Delta1h() HistogramValue { 182 b := make([]HistogramBucket, len(h.buckets)) 183 for i, v := range h.buckets { 184 b[i] = HistogramBucket{ 185 LowBound: v.lowBound, 186 Count: v.count.Delta1h(), 187 } 188 } 189 190 v := HistogramValue{ 191 Count: h.count.Delta1h(), 192 Sum: h.sum.Delta1h(), 193 Min: h.tracker.Min1h(), 194 Max: h.tracker.Max1h(), 195 Buckets: b, 196 } 197 return v 198 } 199 200 // Delta10m returns the change in the last 10 minutes. 201 func (h *Histogram) Delta10m() HistogramValue { 202 b := make([]HistogramBucket, len(h.buckets)) 203 for i, v := range h.buckets { 204 b[i] = HistogramBucket{ 205 LowBound: v.lowBound, 206 Count: v.count.Delta10m(), 207 } 208 } 209 210 v := HistogramValue{ 211 Count: h.count.Delta10m(), 212 Sum: h.sum.Delta10m(), 213 Min: h.tracker.Min10m(), 214 Max: h.tracker.Max10m(), 215 Buckets: b, 216 } 217 return v 218 } 219 220 // Delta1m returns the change in the last 10 minutes. 221 func (h *Histogram) Delta1m() HistogramValue { 222 b := make([]HistogramBucket, len(h.buckets)) 223 for i, v := range h.buckets { 224 b[i] = HistogramBucket{ 225 LowBound: v.lowBound, 226 Count: v.count.Delta1m(), 227 } 228 } 229 230 v := HistogramValue{ 231 Count: h.count.Delta1m(), 232 Sum: h.sum.Delta1m(), 233 Min: h.tracker.Min1m(), 234 Max: h.tracker.Max1m(), 235 Buckets: b, 236 } 237 return v 238 } 239 240 // findBucket does a binary search to find in which bucket the value goes. 241 func (h *Histogram) findBucket(value int64) (int, error) { 242 lastBucket := len(h.buckets) - 1 243 min, max := 0, lastBucket 244 for max >= min { 245 b := (min + max) / 2 246 if value >= h.buckets[b].lowBound && (b == lastBucket || value < h.buckets[b+1].lowBound) { 247 return b, nil 248 } 249 if value < h.buckets[b].lowBound { 250 max = b - 1 251 continue 252 } 253 min = b + 1 254 } 255 return 0, fmt.Errorf("no bucket for value: %d", value) 256 }