|  | // Copyright 2015 The Go Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style | 
|  | // license that can be found in the LICENSE file. | 
|  |  | 
|  | package trace | 
|  |  | 
|  | // This file implements histogramming for RPC statistics collection. | 
|  |  | 
|  | import ( | 
|  | "bytes" | 
|  | "fmt" | 
|  | "html/template" | 
|  | "log" | 
|  | "math" | 
|  | "sync" | 
|  |  | 
|  | "golang.org/x/net/internal/timeseries" | 
|  | ) | 
|  |  | 
|  | const ( | 
|  | bucketCount = 38 | 
|  | ) | 
|  |  | 
|  | // histogram keeps counts of values in buckets that are spaced | 
|  | // out in powers of 2: 0-1, 2-3, 4-7... | 
|  | // histogram implements timeseries.Observable | 
|  | type histogram struct { | 
|  | sum          int64   // running total of measurements | 
|  | sumOfSquares float64 // square of running total | 
|  | buckets      []int64 // bucketed values for histogram | 
|  | value        int     // holds a single value as an optimization | 
|  | valueCount   int64   // number of values recorded for single value | 
|  | } | 
|  |  | 
|  | // AddMeasurement records a value measurement observation to the histogram. | 
|  | func (h *histogram) addMeasurement(value int64) { | 
|  | // TODO: assert invariant | 
|  | h.sum += value | 
|  | h.sumOfSquares += float64(value) * float64(value) | 
|  |  | 
|  | bucketIndex := getBucket(value) | 
|  |  | 
|  | if h.valueCount == 0 || (h.valueCount > 0 && h.value == bucketIndex) { | 
|  | h.value = bucketIndex | 
|  | h.valueCount++ | 
|  | } else { | 
|  | h.allocateBuckets() | 
|  | h.buckets[bucketIndex]++ | 
|  | } | 
|  | } | 
|  |  | 
|  | func (h *histogram) allocateBuckets() { | 
|  | if h.buckets == nil { | 
|  | h.buckets = make([]int64, bucketCount) | 
|  | h.buckets[h.value] = h.valueCount | 
|  | h.value = 0 | 
|  | h.valueCount = -1 | 
|  | } | 
|  | } | 
|  |  | 
|  | func log2(i int64) int { | 
|  | n := 0 | 
|  | for ; i >= 0x100; i >>= 8 { | 
|  | n += 8 | 
|  | } | 
|  | for ; i > 0; i >>= 1 { | 
|  | n += 1 | 
|  | } | 
|  | return n | 
|  | } | 
|  |  | 
|  | func getBucket(i int64) (index int) { | 
|  | index = log2(i) - 1 | 
|  | if index < 0 { | 
|  | index = 0 | 
|  | } | 
|  | if index >= bucketCount { | 
|  | index = bucketCount - 1 | 
|  | } | 
|  | return | 
|  | } | 
|  |  | 
|  | // Total returns the number of recorded observations. | 
|  | func (h *histogram) total() (total int64) { | 
|  | if h.valueCount >= 0 { | 
|  | total = h.valueCount | 
|  | } | 
|  | for _, val := range h.buckets { | 
|  | total += int64(val) | 
|  | } | 
|  | return | 
|  | } | 
|  |  | 
|  | // Average returns the average value of recorded observations. | 
|  | func (h *histogram) average() float64 { | 
|  | t := h.total() | 
|  | if t == 0 { | 
|  | return 0 | 
|  | } | 
|  | return float64(h.sum) / float64(t) | 
|  | } | 
|  |  | 
|  | // Variance returns the variance of recorded observations. | 
|  | func (h *histogram) variance() float64 { | 
|  | t := float64(h.total()) | 
|  | if t == 0 { | 
|  | return 0 | 
|  | } | 
|  | s := float64(h.sum) / t | 
|  | return h.sumOfSquares/t - s*s | 
|  | } | 
|  |  | 
|  | // StandardDeviation returns the standard deviation of recorded observations. | 
|  | func (h *histogram) standardDeviation() float64 { | 
|  | return math.Sqrt(h.variance()) | 
|  | } | 
|  |  | 
|  | // PercentileBoundary estimates the value that the given fraction of recorded | 
|  | // observations are less than. | 
|  | func (h *histogram) percentileBoundary(percentile float64) int64 { | 
|  | total := h.total() | 
|  |  | 
|  | // Corner cases (make sure result is strictly less than Total()) | 
|  | if total == 0 { | 
|  | return 0 | 
|  | } else if total == 1 { | 
|  | return int64(h.average()) | 
|  | } | 
|  |  | 
|  | percentOfTotal := round(float64(total) * percentile) | 
|  | var runningTotal int64 | 
|  |  | 
|  | for i := range h.buckets { | 
|  | value := h.buckets[i] | 
|  | runningTotal += value | 
|  | if runningTotal == percentOfTotal { | 
|  | // We hit an exact bucket boundary. If the next bucket has data, it is a | 
|  | // good estimate of the value. If the bucket is empty, we interpolate the | 
|  | // midpoint between the next bucket's boundary and the next non-zero | 
|  | // bucket. If the remaining buckets are all empty, then we use the | 
|  | // boundary for the next bucket as the estimate. | 
|  | j := uint8(i + 1) | 
|  | min := bucketBoundary(j) | 
|  | if runningTotal < total { | 
|  | for h.buckets[j] == 0 { | 
|  | j++ | 
|  | } | 
|  | } | 
|  | max := bucketBoundary(j) | 
|  | return min + round(float64(max-min)/2) | 
|  | } else if runningTotal > percentOfTotal { | 
|  | // The value is in this bucket. Interpolate the value. | 
|  | delta := runningTotal - percentOfTotal | 
|  | percentBucket := float64(value-delta) / float64(value) | 
|  | bucketMin := bucketBoundary(uint8(i)) | 
|  | nextBucketMin := bucketBoundary(uint8(i + 1)) | 
|  | bucketSize := nextBucketMin - bucketMin | 
|  | return bucketMin + round(percentBucket*float64(bucketSize)) | 
|  | } | 
|  | } | 
|  | return bucketBoundary(bucketCount - 1) | 
|  | } | 
|  |  | 
|  | // Median returns the estimated median of the observed values. | 
|  | func (h *histogram) median() int64 { | 
|  | return h.percentileBoundary(0.5) | 
|  | } | 
|  |  | 
|  | // Add adds other to h. | 
|  | func (h *histogram) Add(other timeseries.Observable) { | 
|  | o := other.(*histogram) | 
|  | if o.valueCount == 0 { | 
|  | // Other histogram is empty | 
|  | } else if h.valueCount >= 0 && o.valueCount > 0 && h.value == o.value { | 
|  | // Both have a single bucketed value, aggregate them | 
|  | h.valueCount += o.valueCount | 
|  | } else { | 
|  | // Two different values necessitate buckets in this histogram | 
|  | h.allocateBuckets() | 
|  | if o.valueCount >= 0 { | 
|  | h.buckets[o.value] += o.valueCount | 
|  | } else { | 
|  | for i := range h.buckets { | 
|  | h.buckets[i] += o.buckets[i] | 
|  | } | 
|  | } | 
|  | } | 
|  | h.sumOfSquares += o.sumOfSquares | 
|  | h.sum += o.sum | 
|  | } | 
|  |  | 
|  | // Clear resets the histogram to an empty state, removing all observed values. | 
|  | func (h *histogram) Clear() { | 
|  | h.buckets = nil | 
|  | h.value = 0 | 
|  | h.valueCount = 0 | 
|  | h.sum = 0 | 
|  | h.sumOfSquares = 0 | 
|  | } | 
|  |  | 
|  | // CopyFrom copies from other, which must be a *histogram, into h. | 
|  | func (h *histogram) CopyFrom(other timeseries.Observable) { | 
|  | o := other.(*histogram) | 
|  | if o.valueCount == -1 { | 
|  | h.allocateBuckets() | 
|  | copy(h.buckets, o.buckets) | 
|  | } | 
|  | h.sum = o.sum | 
|  | h.sumOfSquares = o.sumOfSquares | 
|  | h.value = o.value | 
|  | h.valueCount = o.valueCount | 
|  | } | 
|  |  | 
|  | // Multiply scales the histogram by the specified ratio. | 
|  | func (h *histogram) Multiply(ratio float64) { | 
|  | if h.valueCount == -1 { | 
|  | for i := range h.buckets { | 
|  | h.buckets[i] = int64(float64(h.buckets[i]) * ratio) | 
|  | } | 
|  | } else { | 
|  | h.valueCount = int64(float64(h.valueCount) * ratio) | 
|  | } | 
|  | h.sum = int64(float64(h.sum) * ratio) | 
|  | h.sumOfSquares = h.sumOfSquares * ratio | 
|  | } | 
|  |  | 
|  | // New creates a new histogram. | 
|  | func (h *histogram) New() timeseries.Observable { | 
|  | r := new(histogram) | 
|  | r.Clear() | 
|  | return r | 
|  | } | 
|  |  | 
|  | func (h *histogram) String() string { | 
|  | return fmt.Sprintf("%d, %f, %d, %d, %v", | 
|  | h.sum, h.sumOfSquares, h.value, h.valueCount, h.buckets) | 
|  | } | 
|  |  | 
|  | // round returns the closest int64 to the argument | 
|  | func round(in float64) int64 { | 
|  | return int64(math.Floor(in + 0.5)) | 
|  | } | 
|  |  | 
|  | // bucketBoundary returns the first value in the bucket. | 
|  | func bucketBoundary(bucket uint8) int64 { | 
|  | if bucket == 0 { | 
|  | return 0 | 
|  | } | 
|  | return 1 << bucket | 
|  | } | 
|  |  | 
|  | // bucketData holds data about a specific bucket for use in distTmpl. | 
|  | type bucketData struct { | 
|  | Lower, Upper       int64 | 
|  | N                  int64 | 
|  | Pct, CumulativePct float64 | 
|  | GraphWidth         int | 
|  | } | 
|  |  | 
|  | // data holds data about a Distribution for use in distTmpl. | 
|  | type data struct { | 
|  | Buckets                 []*bucketData | 
|  | Count, Median           int64 | 
|  | Mean, StandardDeviation float64 | 
|  | } | 
|  |  | 
|  | // maxHTMLBarWidth is the maximum width of the HTML bar for visualizing buckets. | 
|  | const maxHTMLBarWidth = 350.0 | 
|  |  | 
|  | // newData returns data representing h for use in distTmpl. | 
|  | func (h *histogram) newData() *data { | 
|  | // Force the allocation of buckets to simplify the rendering implementation | 
|  | h.allocateBuckets() | 
|  | // We scale the bars on the right so that the largest bar is | 
|  | // maxHTMLBarWidth pixels in width. | 
|  | maxBucket := int64(0) | 
|  | for _, n := range h.buckets { | 
|  | if n > maxBucket { | 
|  | maxBucket = n | 
|  | } | 
|  | } | 
|  | total := h.total() | 
|  | barsizeMult := maxHTMLBarWidth / float64(maxBucket) | 
|  | var pctMult float64 | 
|  | if total == 0 { | 
|  | pctMult = 1.0 | 
|  | } else { | 
|  | pctMult = 100.0 / float64(total) | 
|  | } | 
|  |  | 
|  | buckets := make([]*bucketData, len(h.buckets)) | 
|  | runningTotal := int64(0) | 
|  | for i, n := range h.buckets { | 
|  | if n == 0 { | 
|  | continue | 
|  | } | 
|  | runningTotal += n | 
|  | var upperBound int64 | 
|  | if i < bucketCount-1 { | 
|  | upperBound = bucketBoundary(uint8(i + 1)) | 
|  | } else { | 
|  | upperBound = math.MaxInt64 | 
|  | } | 
|  | buckets[i] = &bucketData{ | 
|  | Lower:         bucketBoundary(uint8(i)), | 
|  | Upper:         upperBound, | 
|  | N:             n, | 
|  | Pct:           float64(n) * pctMult, | 
|  | CumulativePct: float64(runningTotal) * pctMult, | 
|  | GraphWidth:    int(float64(n) * barsizeMult), | 
|  | } | 
|  | } | 
|  | return &data{ | 
|  | Buckets:           buckets, | 
|  | Count:             total, | 
|  | Median:            h.median(), | 
|  | Mean:              h.average(), | 
|  | StandardDeviation: h.standardDeviation(), | 
|  | } | 
|  | } | 
|  |  | 
|  | func (h *histogram) html() template.HTML { | 
|  | buf := new(bytes.Buffer) | 
|  | if err := distTmpl().Execute(buf, h.newData()); err != nil { | 
|  | buf.Reset() | 
|  | log.Printf("net/trace: couldn't execute template: %v", err) | 
|  | } | 
|  | return template.HTML(buf.String()) | 
|  | } | 
|  |  | 
|  | var distTmplCache *template.Template | 
|  | var distTmplOnce sync.Once | 
|  |  | 
|  | func distTmpl() *template.Template { | 
|  | distTmplOnce.Do(func() { | 
|  | // Input: data | 
|  | distTmplCache = template.Must(template.New("distTmpl").Parse(` | 
|  | <table> | 
|  | <tr> | 
|  | <td style="padding:0.25em">Count: {{.Count}}</td> | 
|  | <td style="padding:0.25em">Mean: {{printf "%.0f" .Mean}}</td> | 
|  | <td style="padding:0.25em">StdDev: {{printf "%.0f" .StandardDeviation}}</td> | 
|  | <td style="padding:0.25em">Median: {{.Median}}</td> | 
|  | </tr> | 
|  | </table> | 
|  | <hr> | 
|  | <table> | 
|  | {{range $b := .Buckets}} | 
|  | {{if $b}} | 
|  | <tr> | 
|  | <td style="padding:0 0 0 0.25em">[</td> | 
|  | <td style="text-align:right;padding:0 0.25em">{{.Lower}},</td> | 
|  | <td style="text-align:right;padding:0 0.25em">{{.Upper}})</td> | 
|  | <td style="text-align:right;padding:0 0.25em">{{.N}}</td> | 
|  | <td style="text-align:right;padding:0 0.25em">{{printf "%#.3f" .Pct}}%</td> | 
|  | <td style="text-align:right;padding:0 0.25em">{{printf "%#.3f" .CumulativePct}}%</td> | 
|  | <td><div style="background-color: blue; height: 1em; width: {{.GraphWidth}};"></div></td> | 
|  | </tr> | 
|  | {{end}} | 
|  | {{end}} | 
|  | </table> | 
|  | `)) | 
|  | }) | 
|  | return distTmplCache | 
|  | } |