github.com/jbendotnet/noms@v0.0.0-20190904222105-c43e4293ea92/go/metrics/histogram.go (about) 1 // Copyright 2017 Attic Labs, Inc. All rights reserved. 2 // Licensed under the Apache License, version 2.0: 3 // http://www.apache.org/licenses/LICENSE-2.0 4 5 package metrics 6 7 import ( 8 "fmt" 9 "strings" 10 "time" 11 12 "github.com/attic-labs/noms/go/d" 13 humanize "github.com/dustin/go-humanize" 14 ) 15 16 // Histogram is a shameless and low-rent knock of the chromium project's 17 // histogram: 18 // https://chromium.googlesource.com/chromium/src/base/+/master/metrics/histogram.h 19 // 20 // It logically stores a running histogram of uint64 values and shares some 21 // important features of its inspiration: 22 // * It acccepts a correctness deficit in return for not needing to lock. 23 // IOW, concurrent calls to Sample may clobber each other. 24 // * It trades compactness and ease of arithmatic across histograms for 25 // precision. Samples lose precision up to the range of the values which 26 // are stored in a bucket 27 // 28 // Only implemented: Log2-based histogram 29 type Histogram struct { 30 sum uint64 31 buckets [bucketCount]uint64 32 ToString ToStringFunc 33 } 34 35 type ToStringFunc func(v uint64) string 36 37 func identToString(v uint64) string { 38 return fmt.Sprintf("%d", v) 39 } 40 41 const bucketCount = 64 42 43 // Sample adds a uint64 data point to the histogram 44 func (h *Histogram) Sample(v uint64) { 45 d.PanicIfTrue(v == 0) 46 47 h.sum += v 48 49 pot := 0 50 for v > 0 { 51 v = v >> 1 52 pot++ 53 } 54 55 h.buckets[pot-1]++ 56 } 57 58 // SampleTimeSince is a convenience wrapper around Sample which takes the 59 // duration since |t|, if 0, rounds to 1 and passes to Sample() as an uint64 60 // number of nanoseconds. 61 func (h *Histogram) SampleTimeSince(t time.Time) { 62 d := time.Since(t) 63 if d == 0 { 64 d = 1 65 } 66 h.Sample(uint64(d)) 67 } 68 69 // SampleLen is a convenience wrapper around Sample which internally type 70 // asserts the int to a uint64 71 func (h *Histogram) SampleLen(l int) { 72 h.Sample(uint64(l)) 73 } 74 75 func (h Histogram) bucketVal(bucket int) uint64 { 76 return 1 << (uint64(bucket)) 77 } 78 79 // Sum return the sum of sampled values, note that Sum can be overflowed without 80 // overflowing the histogram buckets. 81 func (h Histogram) Sum() uint64 { 82 return h.sum 83 } 84 85 // Add returns a new Histogram which is the result of adding this and other 86 // bucket-wise. 87 func (h *Histogram) Add(other Histogram) { 88 h.sum += other.sum 89 90 for i := 0; i < bucketCount; i++ { 91 h.buckets[i] += other.buckets[i] 92 } 93 } 94 95 // Delta returns a new Histogram which is the result of subtracting other from 96 // this bucket-wise. The intent is to capture changes in the state of histogram 97 // which is collecting samples over some time period. It will panic if any 98 // bucket from other is larger than the corresponding bucket in this. 99 func (h Histogram) Delta(other Histogram) Histogram { 100 nh := Histogram{} 101 nh.sum = h.sum - other.sum 102 103 for i := 0; i < bucketCount; i++ { 104 c := h.buckets[i] 105 l := other.buckets[i] 106 d.PanicIfTrue(l > c) 107 nh.buckets[i] = c - l 108 } 109 return nh 110 } 111 112 // Mean returns 0 if there are no samples, and h.Sum()/h.Samples otherwise. 113 func (h Histogram) Mean() uint64 { 114 samples := h.Samples() 115 if samples == 0 { 116 return 0 117 } 118 119 return h.Sum() / samples 120 } 121 122 // Samples returns the number of samples contained in the histogram 123 func (h Histogram) Samples() uint64 { 124 s := uint64(0) 125 for i := 0; i < bucketCount; i++ { 126 s += h.buckets[i] 127 } 128 return s 129 } 130 131 func (h Histogram) String() string { 132 f := h.ToString 133 if f == nil { 134 f = identToString 135 } 136 return fmt.Sprintf("Mean: %s, Sum: %s, Samples: %d", f(h.Mean()), f(h.Sum()), h.Samples()) 137 } 138 139 func NewTimeHistogram() Histogram { 140 return Histogram{ToString: timeToString} 141 } 142 143 func timeToString(v uint64) string { 144 return time.Duration(v).String() 145 } 146 147 // NewByteHistogram stringifies values using humanize over byte values 148 func NewByteHistogram() Histogram { 149 return Histogram{ToString: humanize.Bytes} 150 } 151 152 const colWidth = 100 153 154 // Report returns an ASCII graph of the non-zero range of normalized buckets. 155 // IOW, it returns a basic graph of the histogram 156 func (h Histogram) Report() string { 157 ts := h.ToString 158 if ts == nil { 159 ts = identToString 160 } 161 162 maxSamples := uint64(0) 163 foundFirstNonEmpty := false 164 firstNonEmpty := 0 165 lastNonEmpty := 0 166 for i := 0; i < bucketCount; i++ { 167 samples := h.buckets[i] 168 169 if samples > 0 { 170 lastNonEmpty = i 171 if !foundFirstNonEmpty { 172 foundFirstNonEmpty = true 173 firstNonEmpty = i 174 } 175 } 176 177 if samples > maxSamples { 178 maxSamples = samples 179 } 180 } 181 182 if maxSamples == 0 { 183 return "" 184 } 185 186 val := uint64(1) 187 188 p := func(bucket int) string { 189 samples := h.buckets[bucket] 190 val := h.bucketVal(bucket) 191 adj := samples * colWidth / maxSamples 192 return fmt.Sprintf("%s> %s: (%d)", strings.Repeat("-", int(adj)), ts(val), samples) 193 } 194 195 lines := make([]string, 0) 196 for i := 0; i < bucketCount; i++ { 197 if i >= firstNonEmpty && i <= lastNonEmpty { 198 lines = append(lines, p(i)) 199 } 200 201 val = val << 1 202 } 203 204 return strings.Join(lines, "\n") 205 }