Files
grpc-go/benchmark/stats/histogram.go

261 lines
6.8 KiB
Go

package stats
import (
"bytes"
"fmt"
"io"
"math"
"strconv"
"strings"
"time"
)
// HistogramValue is the value of Histogram objects.
type HistogramValue struct {
// Count is the total number of values added to the histogram.
Count int64
// Sum is the sum of all the values added to the histogram.
Sum int64
// SumOfSquares is the sum of squares of all values.
SumOfSquares int64
// Min is the minimum of all the values added to the histogram.
Min int64
// Max is the maximum of all the values added to the histogram.
Max int64
// Buckets contains all the buckets of the histogram.
Buckets []HistogramBucket
}
// HistogramBucket is one histogram bucket.
type HistogramBucket struct {
// LowBound is the lower bound of the bucket.
LowBound float64
// Count is the number of values in the bucket.
Count int64
}
// Print writes textual output of the histogram values.
func (v HistogramValue) Print(w io.Writer) {
avg := float64(v.Sum) / float64(v.Count)
fmt.Fprintf(w, "Count: %d Min: %d Max: %d Avg: %.2f\n", v.Count, v.Min, v.Max, avg)
fmt.Fprintf(w, "%s\n", strings.Repeat("-", 60))
if v.Count <= 0 {
return
}
maxBucketDigitLen := len(strconv.FormatFloat(v.Buckets[len(v.Buckets)-1].LowBound, 'f', 6, 64))
if maxBucketDigitLen < 3 {
// For "inf".
maxBucketDigitLen = 3
}
maxCountDigitLen := len(strconv.FormatInt(v.Count, 10))
percentMulti := 100 / float64(v.Count)
accCount := int64(0)
for i, b := range v.Buckets {
fmt.Fprintf(w, "[%*f, ", maxBucketDigitLen, b.LowBound)
if i+1 < len(v.Buckets) {
fmt.Fprintf(w, "%*f)", maxBucketDigitLen, v.Buckets[i+1].LowBound)
} else {
fmt.Fprintf(w, "%*s)", maxBucketDigitLen, "inf")
}
accCount += b.Count
fmt.Fprintf(w, " %*d %5.1f%% %5.1f%%", maxCountDigitLen, b.Count, float64(b.Count)*percentMulti, float64(accCount)*percentMulti)
const barScale = 0.1
barLength := int(float64(b.Count)*percentMulti*barScale + 0.5)
fmt.Fprintf(w, " %s\n", strings.Repeat("#", barLength))
}
}
// String returns the textual output of the histogram values as string.
func (v HistogramValue) String() string {
var b bytes.Buffer
v.Print(&b)
return b.String()
}
// A histograms is stored with exponentially increasing bucket sizes.
// The first bucket is [0, m) where m = 1 + GrowthFactor
// Bucket n (n>=1) contains [m**n, m**(n+1))
// The type of the values is int64, which is suitable for keeping track
// of things like RPC latency in milliseconds.
//New histogram objects should be obtained via the New() function.
type Histogram struct {
opts HistogramOptions
buckets []bucketInternal
count *Counter
sum *Counter
sumOfSquares *Counter
tracker *Tracker
}
// HistogramOptions contains the parameters that define the histogram's buckets.
type HistogramOptions struct {
// NumBuckets is the number of buckets.
NumBuckets int
// GrowthFactor is the growth factor of the buckets. A value of 0.1
// indicates that bucket N+1 will be 10% larger than bucket N.
GrowthFactor float64
// MinValue is the lower bound of the first bucket.
MinValue int64
}
// bucketInternal is the internal representation of a bucket, which includes a
// rate counter.
type bucketInternal struct {
lowBound float64
count *Counter
}
// NewHistogram returns a pointer to a new Histogram object that was created
// with the provided options.
func NewHistogram(opts HistogramOptions) *Histogram {
if opts.NumBuckets == 0 {
opts.NumBuckets = 32
}
h := Histogram{
opts: opts,
buckets: make([]bucketInternal, opts.NumBuckets),
count: newCounter(),
sum: newCounter(),
sumOfSquares: newCounter(),
tracker: newTracker(),
}
delta := 1.0
for i := 0; i < opts.NumBuckets; i++ {
if i == 0 {
h.buckets[i].lowBound = float64(opts.MinValue)
} else {
h.buckets[i].lowBound = float64(opts.MinValue) + delta
}
h.buckets[i].count = newCounter()
delta = delta * (1.0 + opts.GrowthFactor)
}
return &h
}
// Opts returns a copy of the options used to create the Histogram.
func (h *Histogram) Opts() HistogramOptions {
return h.opts
}
// Add adds a value to the histogram.
func (h *Histogram) Add(value int64) error {
bucket, err := h.findBucket(value)
if err != nil {
return err
}
h.buckets[bucket].count.Incr(1)
h.count.Incr(1)
h.sum.Incr(value)
h.sumOfSquares.Incr(value * value)
h.tracker.Push(value)
return nil
}
// LastUpdate returns the time at which the object was last updated.
func (h *Histogram) LastUpdate() time.Time {
return h.count.LastUpdate()
}
// Value returns the accumulated state of the histogram since it was created.
func (h *Histogram) Value() HistogramValue {
b := make([]HistogramBucket, len(h.buckets))
for i, v := range h.buckets {
b[i] = HistogramBucket{
LowBound: v.lowBound,
Count: v.count.Value(),
}
}
v := HistogramValue{
Count: h.count.Value(),
Sum: h.sum.Value(),
SumOfSquares: h.sumOfSquares.Value(),
Min: h.tracker.Min(),
Max: h.tracker.Max(),
Buckets: b,
}
return v
}
// Delta1h returns the change in the last hour.
func (h *Histogram) Delta1h() HistogramValue {
b := make([]HistogramBucket, len(h.buckets))
for i, v := range h.buckets {
b[i] = HistogramBucket{
LowBound: v.lowBound,
Count: v.count.Delta1h(),
}
}
v := HistogramValue{
Count: h.count.Delta1h(),
Sum: h.sum.Delta1h(),
SumOfSquares: h.sumOfSquares.Delta1h(),
Min: h.tracker.Min1h(),
Max: h.tracker.Max1h(),
Buckets: b,
}
return v
}
// Delta10m returns the change in the last 10 minutes.
func (h *Histogram) Delta10m() HistogramValue {
b := make([]HistogramBucket, len(h.buckets))
for i, v := range h.buckets {
b[i] = HistogramBucket{
LowBound: v.lowBound,
Count: v.count.Delta10m(),
}
}
v := HistogramValue{
Count: h.count.Delta10m(),
Sum: h.sum.Delta10m(),
SumOfSquares: h.sumOfSquares.Delta10m(),
Min: h.tracker.Min10m(),
Max: h.tracker.Max10m(),
Buckets: b,
}
return v
}
// Delta1m returns the change in the last 10 minutes.
func (h *Histogram) Delta1m() HistogramValue {
b := make([]HistogramBucket, len(h.buckets))
for i, v := range h.buckets {
b[i] = HistogramBucket{
LowBound: v.lowBound,
Count: v.count.Delta1m(),
}
}
v := HistogramValue{
Count: h.count.Delta1m(),
Sum: h.sum.Delta1m(),
SumOfSquares: h.sumOfSquares.Delta1m(),
Min: h.tracker.Min1m(),
Max: h.tracker.Max1m(),
Buckets: b,
}
return v
}
// findBucket does a binary search to find in which bucket the value goes.
func (h *Histogram) findBucket(value int64) (int, error) {
deltaValue := float64(value - h.opts.MinValue)
var b int
if deltaValue < 1 {
b = 0
} else {
b = int(math.Log(deltaValue) / math.Log(1+h.opts.GrowthFactor))
}
if b >= len(h.buckets) {
return 0, fmt.Errorf("no bucket for value: %d", value)
}
return b, nil
}