github.com/psiphon-labs/goarista@v0.0.0-20160825065156-d002785f4c67/monitor/stats/histogram.go (about)

     1  package stats
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"strconv"
     8  	"strings"
     9  	"time"
    10  )
    11  
    12  // HistogramValue is the value of Histogram objects.
    13  type HistogramValue struct {
    14  	// Count is the total number of values added to the histogram.
    15  	Count int64
    16  	// Sum is the sum of all the values added to the histogram.
    17  	Sum int64
    18  	// Min is the minimum of all the values added to the histogram.
    19  	Min int64
    20  	// Max is the maximum of all the values added to the histogram.
    21  	Max int64
    22  	// Buckets contains all the buckets of the histogram.
    23  	Buckets []HistogramBucket
    24  }
    25  
    26  // HistogramBucket is one histogram bucket.
    27  type HistogramBucket struct {
    28  	// LowBound is the lower bound of the bucket.
    29  	LowBound int64
    30  	// Count is the number of values in the bucket.
    31  	Count int64
    32  }
    33  
    34  // Print writes textual output of the histogram values.
    35  func (v HistogramValue) Print(w io.Writer) {
    36  	avg := float64(v.Sum) / float64(v.Count)
    37  	fmt.Fprintf(w, "Count: %d  Min: %d  Max: %d  Avg: %.2f\n", v.Count, v.Min, v.Max, avg)
    38  	fmt.Fprintf(w, "%s\n", strings.Repeat("-", 60))
    39  	if v.Count <= 0 {
    40  		return
    41  	}
    42  
    43  	maxBucketDigitLen := len(strconv.FormatInt(v.Buckets[len(v.Buckets)-1].LowBound, 10))
    44  	if maxBucketDigitLen < 3 {
    45  		// For "inf".
    46  		maxBucketDigitLen = 3
    47  	}
    48  	maxCountDigitLen := len(strconv.FormatInt(v.Count, 10))
    49  	percentMulti := 100 / float64(v.Count)
    50  
    51  	accCount := int64(0)
    52  	for i, b := range v.Buckets {
    53  		fmt.Fprintf(w, "[%*d, ", maxBucketDigitLen, b.LowBound)
    54  		if i+1 < len(v.Buckets) {
    55  			fmt.Fprintf(w, "%*d)", maxBucketDigitLen, v.Buckets[i+1].LowBound)
    56  		} else {
    57  			fmt.Fprintf(w, "%*s)", maxBucketDigitLen, "inf")
    58  		}
    59  
    60  		accCount += b.Count
    61  		fmt.Fprintf(w, "  %*d  %5.1f%%  %5.1f%%", maxCountDigitLen, b.Count,
    62  			float64(b.Count)*percentMulti, float64(accCount)*percentMulti)
    63  
    64  		const barScale = 0.1
    65  		barLength := int(float64(b.Count)*percentMulti*barScale + 0.5)
    66  		fmt.Fprintf(w, "  %s\n", strings.Repeat("#", barLength))
    67  	}
    68  }
    69  
    70  // String returns the textual output of the histogram values as string.
    71  func (v HistogramValue) String() string {
    72  	var b bytes.Buffer
    73  	v.Print(&b)
    74  	return b.String()
    75  }
    76  
    77  // A Histogram accumulates values in the form of a histogram. The type of the
    78  // values is int64, which is suitable for keeping track of things like RPC
    79  // latency in milliseconds. New histogram objects should be obtained via the
    80  // New() function.
    81  type Histogram struct {
    82  	opts    HistogramOptions
    83  	buckets []bucketInternal
    84  	count   *Counter
    85  	sum     *Counter
    86  	tracker *Tracker
    87  }
    88  
    89  // HistogramOptions contains the parameters that define the histogram's buckets.
    90  type HistogramOptions struct {
    91  	// NumBuckets is the number of buckets.
    92  	NumBuckets int
    93  	// GrowthFactor is the growth factor of the buckets. A value of 0.1
    94  	// indicates that bucket N+1 will be 10% larger than bucket N.
    95  	GrowthFactor float64
    96  	// SmallestBucketSize is the size of the first bucket. Bucket sizes are
    97  	// rounded down to the nearest integer.
    98  	SmallestBucketSize float64
    99  	// MinValue is the lower bound of the first bucket.
   100  	MinValue int64
   101  }
   102  
   103  // bucketInternal is the internal representation of a bucket, which includes a
   104  // rate counter.
   105  type bucketInternal struct {
   106  	lowBound int64
   107  	count    *Counter
   108  }
   109  
   110  // NewHistogram returns a pointer to a new Histogram object that was created
   111  // with the provided options.
   112  func NewHistogram(opts HistogramOptions) *Histogram {
   113  	if opts.NumBuckets == 0 {
   114  		opts.NumBuckets = 32
   115  	}
   116  	if opts.SmallestBucketSize == 0.0 {
   117  		opts.SmallestBucketSize = 1.0
   118  	}
   119  	h := Histogram{
   120  		opts:    opts,
   121  		buckets: make([]bucketInternal, opts.NumBuckets),
   122  		count:   newCounter(),
   123  		sum:     newCounter(),
   124  		tracker: newTracker(),
   125  	}
   126  	low := opts.MinValue
   127  	delta := opts.SmallestBucketSize
   128  	for i := 0; i < opts.NumBuckets; i++ {
   129  		h.buckets[i].lowBound = low
   130  		h.buckets[i].count = newCounter()
   131  		low = low + int64(delta)
   132  		delta = delta * (1.0 + opts.GrowthFactor)
   133  	}
   134  	return &h
   135  }
   136  
   137  // Opts returns a copy of the options used to create the Histogram.
   138  func (h *Histogram) Opts() HistogramOptions {
   139  	return h.opts
   140  }
   141  
   142  // Add adds a value to the histogram.
   143  func (h *Histogram) Add(value int64) error {
   144  	bucket, err := h.findBucket(value)
   145  	if err != nil {
   146  		return err
   147  	}
   148  	h.buckets[bucket].count.Incr(1)
   149  	h.count.Incr(1)
   150  	h.sum.Incr(value)
   151  	h.tracker.Push(value)
   152  	return nil
   153  }
   154  
   155  // LastUpdate returns the time at which the object was last updated.
   156  func (h *Histogram) LastUpdate() time.Time {
   157  	return h.count.LastUpdate()
   158  }
   159  
   160  // Value returns the accumulated state of the histogram since it was created.
   161  func (h *Histogram) Value() HistogramValue {
   162  	b := make([]HistogramBucket, len(h.buckets))
   163  	for i, v := range h.buckets {
   164  		b[i] = HistogramBucket{
   165  			LowBound: v.lowBound,
   166  			Count:    v.count.Value(),
   167  		}
   168  	}
   169  
   170  	v := HistogramValue{
   171  		Count:   h.count.Value(),
   172  		Sum:     h.sum.Value(),
   173  		Min:     h.tracker.Min(),
   174  		Max:     h.tracker.Max(),
   175  		Buckets: b,
   176  	}
   177  	return v
   178  }
   179  
   180  // Delta1h returns the change in the last hour.
   181  func (h *Histogram) Delta1h() HistogramValue {
   182  	b := make([]HistogramBucket, len(h.buckets))
   183  	for i, v := range h.buckets {
   184  		b[i] = HistogramBucket{
   185  			LowBound: v.lowBound,
   186  			Count:    v.count.Delta1h(),
   187  		}
   188  	}
   189  
   190  	v := HistogramValue{
   191  		Count:   h.count.Delta1h(),
   192  		Sum:     h.sum.Delta1h(),
   193  		Min:     h.tracker.Min1h(),
   194  		Max:     h.tracker.Max1h(),
   195  		Buckets: b,
   196  	}
   197  	return v
   198  }
   199  
   200  // Delta10m returns the change in the last 10 minutes.
   201  func (h *Histogram) Delta10m() HistogramValue {
   202  	b := make([]HistogramBucket, len(h.buckets))
   203  	for i, v := range h.buckets {
   204  		b[i] = HistogramBucket{
   205  			LowBound: v.lowBound,
   206  			Count:    v.count.Delta10m(),
   207  		}
   208  	}
   209  
   210  	v := HistogramValue{
   211  		Count:   h.count.Delta10m(),
   212  		Sum:     h.sum.Delta10m(),
   213  		Min:     h.tracker.Min10m(),
   214  		Max:     h.tracker.Max10m(),
   215  		Buckets: b,
   216  	}
   217  	return v
   218  }
   219  
   220  // Delta1m returns the change in the last 10 minutes.
   221  func (h *Histogram) Delta1m() HistogramValue {
   222  	b := make([]HistogramBucket, len(h.buckets))
   223  	for i, v := range h.buckets {
   224  		b[i] = HistogramBucket{
   225  			LowBound: v.lowBound,
   226  			Count:    v.count.Delta1m(),
   227  		}
   228  	}
   229  
   230  	v := HistogramValue{
   231  		Count:   h.count.Delta1m(),
   232  		Sum:     h.sum.Delta1m(),
   233  		Min:     h.tracker.Min1m(),
   234  		Max:     h.tracker.Max1m(),
   235  		Buckets: b,
   236  	}
   237  	return v
   238  }
   239  
   240  // findBucket does a binary search to find in which bucket the value goes.
   241  func (h *Histogram) findBucket(value int64) (int, error) {
   242  	lastBucket := len(h.buckets) - 1
   243  	min, max := 0, lastBucket
   244  	for max >= min {
   245  		b := (min + max) / 2
   246  		if value >= h.buckets[b].lowBound && (b == lastBucket || value < h.buckets[b+1].lowBound) {
   247  			return b, nil
   248  		}
   249  		if value < h.buckets[b].lowBound {
   250  			max = b - 1
   251  			continue
   252  		}
   253  		min = b + 1
   254  	}
   255  	return 0, fmt.Errorf("no bucket for value: %d", value)
   256  }