2012-05-24 22:02:44 +04:00
|
|
|
/*
|
|
|
|
Copyright (c) 2012, Matt T. Proud
|
|
|
|
All rights reserved.
|
2012-05-20 01:59:25 +04:00
|
|
|
|
2012-05-24 22:02:44 +04:00
|
|
|
Use of this source code is governed by a BSD-style
|
|
|
|
license that can be found in the LICENSE file.
|
|
|
|
*/
|
2012-05-20 01:59:25 +04:00
|
|
|
|
|
|
|
package metrics
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"fmt"
|
|
|
|
"math"
|
|
|
|
"strconv"
|
|
|
|
)
|
|
|
|
|
2012-05-24 22:02:44 +04:00
|
|
|
/*
|
|
|
|
This generates count-buckets of equal size distributed along the open
|
|
|
|
interval of lower to upper. For instance, {lower=0, upper=10, count=5}
|
|
|
|
yields the following: [0, 2, 4, 6, 8].
|
|
|
|
*/
|
2012-05-20 01:59:25 +04:00
|
|
|
func EquallySizedBucketsFor(lower, upper float64, count int) []float64 {
|
|
|
|
buckets := make([]float64, count)
|
|
|
|
|
|
|
|
partitionSize := (upper - lower) / float64(count)
|
|
|
|
|
|
|
|
for i := 0; i < count; i++ {
|
|
|
|
m := float64(i)
|
|
|
|
buckets[i] = lower + (m * partitionSize)
|
|
|
|
}
|
|
|
|
|
|
|
|
return buckets
|
|
|
|
}
|
|
|
|
|
2012-05-24 22:02:44 +04:00
|
|
|
/*
|
|
|
|
This generates log2-sized buckets spanning from lower to upper inclusively
|
|
|
|
as well as values beyond it.
|
|
|
|
*/
|
2012-05-20 01:59:25 +04:00
|
|
|
func LogarithmicSizedBucketsFor(lower, upper float64) []float64 {
|
|
|
|
bucketCount := int(math.Ceil(math.Log2(upper)))
|
|
|
|
|
|
|
|
buckets := make([]float64, bucketCount)
|
|
|
|
|
|
|
|
for i, j := 0, 0.0; i < bucketCount; i, j = i+1, math.Pow(2, float64(i+1.0)) {
|
|
|
|
buckets[i] = j
|
|
|
|
}
|
|
|
|
|
|
|
|
return buckets
|
|
|
|
}
|
|
|
|
|
2012-05-24 22:02:44 +04:00
|
|
|
/*
|
|
|
|
A HistogramSpecification defines how a Histogram is to be built.
|
|
|
|
*/
|
2012-05-20 01:59:25 +04:00
|
|
|
type HistogramSpecification struct {
|
|
|
|
Starts []float64
|
|
|
|
BucketMaker BucketBuilder
|
|
|
|
ReportablePercentiles []float64
|
|
|
|
}
|
|
|
|
|
2012-05-24 22:02:44 +04:00
|
|
|
/*
|
|
|
|
The histogram is an accumulator for samples. It merely routes into which
|
|
|
|
to bucket to capture an event and provides a percentile calculation
|
|
|
|
mechanism.
|
|
|
|
|
|
|
|
Histogram makes do without locking by employing the law of large numbers
|
|
|
|
to presume a convergence toward a given bucket distribution. Locking
|
|
|
|
may be implemented in the buckets themselves, though.
|
|
|
|
*/
|
2012-05-20 01:59:25 +04:00
|
|
|
type Histogram struct {
|
2012-05-24 22:02:44 +04:00
|
|
|
/*
|
|
|
|
This represents the open interval's start at which values shall be added to
|
|
|
|
the bucket. The interval continues until the beginning of the next bucket
|
|
|
|
exclusive or positive infinity.
|
|
|
|
|
|
|
|
N.B.
|
|
|
|
- bucketStarts should be sorted in ascending order;
|
|
|
|
- len(bucketStarts) must be equivalent to len(buckets);
|
|
|
|
- The index of a given bucketStarts' element is presumed to match
|
|
|
|
correspond to the appropriate element in buckets.
|
|
|
|
*/
|
2012-05-20 01:59:25 +04:00
|
|
|
bucketStarts []float64
|
2012-05-24 22:02:44 +04:00
|
|
|
/*
|
|
|
|
These are the buckets that capture samples as they are emitted to the
|
|
|
|
histogram. Please consult the reference interface and its implements for
|
|
|
|
further details about behavior expectations.
|
|
|
|
*/
|
2012-05-20 01:59:25 +04:00
|
|
|
buckets []Bucket
|
2012-05-24 22:02:44 +04:00
|
|
|
/*
|
|
|
|
These are the percentile values that will be reported on marshalling.
|
|
|
|
*/
|
2012-05-20 01:59:25 +04:00
|
|
|
reportablePercentiles []float64
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Histogram) Add(value float64) {
|
|
|
|
lastIndex := 0
|
|
|
|
|
|
|
|
for i, bucketStart := range h.bucketStarts {
|
|
|
|
if value < bucketStart {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
lastIndex = i
|
|
|
|
}
|
|
|
|
|
|
|
|
h.buckets[lastIndex].Add(value)
|
|
|
|
}
|
|
|
|
|
2012-12-19 14:10:09 +04:00
|
|
|
func (h *Histogram) String() string {
|
2012-05-20 01:59:25 +04:00
|
|
|
stringBuffer := bytes.NewBufferString("")
|
|
|
|
stringBuffer.WriteString("[Histogram { ")
|
|
|
|
|
|
|
|
for i, bucketStart := range h.bucketStarts {
|
|
|
|
bucket := h.buckets[i]
|
2012-12-19 14:10:09 +04:00
|
|
|
stringBuffer.WriteString(fmt.Sprintf("[%f, inf) = %s, ", bucketStart, bucket.String()))
|
2012-05-20 01:59:25 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
stringBuffer.WriteString("}]")
|
|
|
|
|
|
|
|
return string(stringBuffer.Bytes())
|
|
|
|
}
|
|
|
|
|
2012-05-24 22:02:44 +04:00
|
|
|
/*
|
|
|
|
Determine the number of previous observations up to a given index.
|
|
|
|
*/
|
2012-05-20 01:59:25 +04:00
|
|
|
func previousCumulativeObservations(cumulativeObservations []int, bucketIndex int) int {
|
|
|
|
if bucketIndex == 0 {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
return cumulativeObservations[bucketIndex-1]
|
|
|
|
}
|
|
|
|
|
2012-05-24 22:02:44 +04:00
|
|
|
/*
|
|
|
|
Determine the index for an element given a percentage of length.
|
|
|
|
*/
|
2012-05-20 01:59:25 +04:00
|
|
|
func prospectiveIndexForPercentile(percentile float64, totalObservations int) int {
|
2012-05-22 11:20:09 +04:00
|
|
|
return int(percentile * float64(totalObservations-1))
|
|
|
|
}
|
|
|
|
|
2012-05-24 22:02:44 +04:00
|
|
|
/*
|
|
|
|
Determine the next bucket element when interim bucket intervals may be empty.
|
|
|
|
*/
|
2012-05-22 11:20:09 +04:00
|
|
|
func (h *Histogram) nextNonEmptyBucketElement(currentIndex, bucketCount int, observationsByBucket []int) (*Bucket, int) {
|
|
|
|
for i := currentIndex; i < bucketCount; i++ {
|
|
|
|
if observationsByBucket[i] == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
return &h.buckets[i], 0
|
|
|
|
}
|
|
|
|
|
|
|
|
panic("Illegal Condition: There were no remaining buckets to provide a value.")
|
2012-05-20 01:59:25 +04:00
|
|
|
}
|
|
|
|
|
2012-05-24 22:02:44 +04:00
|
|
|
/*
|
|
|
|
Find what bucket and element index contains a given percentile value.
|
|
|
|
If a percentile is requested that results in a corresponding index that is no
|
|
|
|
longer contained by the bucket, the index of the last item is returned. This
|
|
|
|
may occur if the underlying bucket catalogs values and employs an eviction
|
|
|
|
strategy.
|
|
|
|
*/
|
2012-05-22 11:20:09 +04:00
|
|
|
func (h *Histogram) bucketForPercentile(percentile float64) (*Bucket, int) {
|
2012-05-20 01:59:25 +04:00
|
|
|
bucketCount := len(h.buckets)
|
|
|
|
|
2012-05-24 22:02:44 +04:00
|
|
|
/*
|
|
|
|
This captures the quantity of samples in a given bucket's range.
|
|
|
|
*/
|
2012-05-20 01:59:25 +04:00
|
|
|
observationsByBucket := make([]int, bucketCount)
|
2012-05-24 22:02:44 +04:00
|
|
|
/*
|
|
|
|
This captures the cumulative quantity of observations from all preceding
|
|
|
|
buckets up and to the end of this bucket.
|
|
|
|
*/
|
2012-05-20 01:59:25 +04:00
|
|
|
cumulativeObservationsByBucket := make([]int, bucketCount)
|
|
|
|
|
|
|
|
var totalObservations int = 0
|
|
|
|
|
|
|
|
for i, bucket := range h.buckets {
|
|
|
|
observations := bucket.Observations()
|
|
|
|
observationsByBucket[i] = observations
|
|
|
|
totalObservations += bucket.Observations()
|
|
|
|
cumulativeObservationsByBucket[i] = totalObservations
|
|
|
|
}
|
|
|
|
|
2012-05-24 22:02:44 +04:00
|
|
|
/*
|
|
|
|
This captures the index offset where the given percentile value would be
|
|
|
|
were all submitted samples stored and never down-/re-sampled nor deleted
|
|
|
|
and housed in a singular array.
|
|
|
|
*/
|
2012-05-20 01:59:25 +04:00
|
|
|
prospectiveIndex := prospectiveIndexForPercentile(percentile, totalObservations)
|
|
|
|
|
|
|
|
for i, cumulativeObservation := range cumulativeObservationsByBucket {
|
|
|
|
if cumulativeObservation == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2012-05-24 22:02:44 +04:00
|
|
|
/*
|
|
|
|
Find the bucket that contains the given index.
|
|
|
|
*/
|
2012-05-20 01:59:25 +04:00
|
|
|
if cumulativeObservation >= prospectiveIndex {
|
|
|
|
var subIndex int
|
2012-05-24 22:02:44 +04:00
|
|
|
/*
|
|
|
|
This calculates the index within the current bucket where the given
|
|
|
|
percentile may be found.
|
|
|
|
*/
|
2012-05-20 01:59:25 +04:00
|
|
|
subIndex = prospectiveIndex - previousCumulativeObservations(cumulativeObservationsByBucket, i)
|
2012-05-22 11:20:09 +04:00
|
|
|
|
2012-05-24 22:02:44 +04:00
|
|
|
/*
|
|
|
|
Sometimes the index may be the last item, in which case we need to
|
|
|
|
take this into account.
|
|
|
|
*/
|
2012-05-20 01:59:25 +04:00
|
|
|
if observationsByBucket[i] == subIndex {
|
2012-05-22 11:20:09 +04:00
|
|
|
return h.nextNonEmptyBucketElement(i+1, bucketCount, observationsByBucket)
|
2012-05-20 01:59:25 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
return &h.buckets[i], subIndex
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return &h.buckets[0], 0
|
|
|
|
}
|
|
|
|
|
2012-05-24 22:02:44 +04:00
|
|
|
/*
|
|
|
|
Return the histogram's estimate of the value for a given percentile of
|
|
|
|
collected samples. The requested percentile is expected to be a real
|
|
|
|
value within (0, 1.0].
|
|
|
|
*/
|
2012-05-20 01:59:25 +04:00
|
|
|
func (h *Histogram) Percentile(percentile float64) float64 {
|
2012-05-20 02:10:52 +04:00
|
|
|
bucket, index := h.bucketForPercentile(percentile)
|
2012-05-20 01:59:25 +04:00
|
|
|
|
|
|
|
return (*bucket).ValueForIndex(index)
|
|
|
|
}
|
|
|
|
|
2012-05-24 22:02:44 +04:00
|
|
|
func formatFloat(value float64) string {
|
|
|
|
return strconv.FormatFloat(value, floatFormat, floatPrecision, floatBitCount)
|
|
|
|
}
|
|
|
|
|
2012-05-20 01:59:25 +04:00
|
|
|
func (h *Histogram) Marshallable() map[string]interface{} {
|
|
|
|
numberOfPercentiles := len(h.reportablePercentiles)
|
|
|
|
result := make(map[string]interface{}, 2)
|
|
|
|
|
2012-05-24 22:02:44 +04:00
|
|
|
result[typeKey] = histogramTypeValue
|
2012-05-20 01:59:25 +04:00
|
|
|
|
|
|
|
value := make(map[string]interface{}, numberOfPercentiles)
|
|
|
|
|
|
|
|
for _, percentile := range h.reportablePercentiles {
|
2012-05-24 22:02:44 +04:00
|
|
|
percentileString := formatFloat(percentile)
|
|
|
|
value[percentileString] = formatFloat(h.Percentile(percentile))
|
2012-05-20 01:59:25 +04:00
|
|
|
}
|
|
|
|
|
2012-05-24 22:02:44 +04:00
|
|
|
result[valueKey] = value
|
2012-05-20 01:59:25 +04:00
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2012-05-24 22:02:44 +04:00
|
|
|
/*
|
|
|
|
Produce a histogram from a given specification.
|
|
|
|
*/
|
2012-05-20 01:59:25 +04:00
|
|
|
func CreateHistogram(specification *HistogramSpecification) *Histogram {
|
|
|
|
bucketCount := len(specification.Starts)
|
|
|
|
|
|
|
|
metric := &Histogram{
|
|
|
|
bucketStarts: specification.Starts,
|
|
|
|
buckets: make([]Bucket, bucketCount),
|
|
|
|
reportablePercentiles: specification.ReportablePercentiles,
|
|
|
|
}
|
|
|
|
|
|
|
|
for i := 0; i < bucketCount; i++ {
|
|
|
|
metric.buckets[i] = specification.BucketMaker()
|
|
|
|
}
|
|
|
|
|
|
|
|
return metric
|
|
|
|
}
|