blob: 285fabc334eb0903ce75ef9adb20c43ee7cd8ac4 [file] [log] [blame]
// Copyright 2015 The Vanadium 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 histogram implements a basic histogram to keep track of data
// distribution.
package histogram
import (
"time"
"v.io/v23/verror"
"v.io/x/ref/lib/stats/counter"
"v.io/x/ref/services/stats"
)
const pkgPath = "v.io/x/ref/lib/stats/histogram"
var (
errNoBucketForValue = verror.Register(pkgPath+".errNoBucketForValue", verror.NoRetry, "{1:}{2:} no bucket for value{:_}")
)
// A Histogram accumulates values in the form of a histogram. 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 Options
buckets []bucketInternal
count *counter.Counter
sum *counter.Counter
tracker *counter.Tracker
}
// Options contains the parameters that define the histogram's buckets.
type Options 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
// SmallestBucketSize is the size of the first bucket. Bucket sizes are
// rounded down to the nearest integer.
SmallestBucketSize 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 int64
count *counter.Counter
}
// New returns a pointer to a new Histogram object that was created with the
// provided options.
func New(opts Options) *Histogram {
if opts.NumBuckets == 0 {
opts.NumBuckets = 32
}
if opts.SmallestBucketSize == 0.0 {
opts.SmallestBucketSize = 1.0
}
h := Histogram{
opts: opts,
buckets: make([]bucketInternal, opts.NumBuckets),
count: counter.New(),
sum: counter.New(),
tracker: counter.NewTracker(),
}
low := opts.MinValue
delta := opts.SmallestBucketSize
for i := 0; i < opts.NumBuckets; i++ {
h.buckets[i].lowBound = low
h.buckets[i].count = counter.New()
low = low + int64(delta)
delta = delta * (1.0 + opts.GrowthFactor)
}
return &h
}
// Opts returns a copy of the options used to create the Histogram.
func (h *Histogram) Opts() Options {
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.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() stats.HistogramValue {
b := make([]stats.HistogramBucket, len(h.buckets))
for i, v := range h.buckets {
b[i] = stats.HistogramBucket{
LowBound: v.lowBound,
Count: v.count.Value(),
}
}
v := stats.HistogramValue{
Count: h.count.Value(),
Sum: h.sum.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() stats.HistogramValue {
b := make([]stats.HistogramBucket, len(h.buckets))
for i, v := range h.buckets {
b[i] = stats.HistogramBucket{
LowBound: v.lowBound,
Count: v.count.Delta1h(),
}
}
v := stats.HistogramValue{
Count: h.count.Delta1h(),
Sum: h.sum.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() stats.HistogramValue {
b := make([]stats.HistogramBucket, len(h.buckets))
for i, v := range h.buckets {
b[i] = stats.HistogramBucket{
LowBound: v.lowBound,
Count: v.count.Delta10m(),
}
}
v := stats.HistogramValue{
Count: h.count.Delta10m(),
Sum: h.sum.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() stats.HistogramValue {
b := make([]stats.HistogramBucket, len(h.buckets))
for i, v := range h.buckets {
b[i] = stats.HistogramBucket{
LowBound: v.lowBound,
Count: v.count.Delta1m(),
}
}
v := stats.HistogramValue{
Count: h.count.Delta1m(),
Sum: h.sum.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) {
lastBucket := len(h.buckets) - 1
min, max := 0, lastBucket
for max >= min {
b := (min + max) / 2
if value >= h.buckets[b].lowBound && (b == lastBucket || value < h.buckets[b+1].lowBound) {
return b, nil
}
if value < h.buckets[b].lowBound {
max = b - 1
continue
}
min = b + 1
}
return 0, verror.New(errNoBucketForValue, nil, value)
}