mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-11-03 23:02:24 -06:00 
			
		
		
		
	
		
			
				
	
	
		
			365 lines
		
	
	
	
		
			9.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			365 lines
		
	
	
	
		
			9.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// 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
 | 
						|
}
 |