| // 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 |
| } |