aboutsummaryrefslogblamecommitdiffstats
path: root/vendor/golang.org/x/net/trace/histogram.go
blob: 9bf4286c794b8febfd7091fe998011ce5f42a1f0 (plain) (tree)












































































































































































































































































































































































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