117 lines
2.7 KiB
Go
117 lines
2.7 KiB
Go
|
package stats
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"math"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
// Stats is a simple helper for gathering additional statistics like histogram
|
||
|
// during benchmarks. This is not thread safe.
|
||
|
type Stats struct {
|
||
|
numBuckets int
|
||
|
unit time.Duration
|
||
|
min, max int64
|
||
|
histogram *Histogram
|
||
|
|
||
|
durations durationSlice
|
||
|
dirty bool
|
||
|
}
|
||
|
|
||
|
type durationSlice []time.Duration
|
||
|
|
||
|
// NewStats creates a new Stats instance. If numBuckets is not positive,
|
||
|
// the default value (16) will be used.
|
||
|
func NewStats(numBuckets int) *Stats {
|
||
|
if numBuckets <= 0 {
|
||
|
numBuckets = 16
|
||
|
}
|
||
|
return &Stats{
|
||
|
// Use one more bucket for the last unbounded bucket.
|
||
|
numBuckets: numBuckets + 1,
|
||
|
durations: make(durationSlice, 0, 100000),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Add adds an elapsed time per operation to the stats.
|
||
|
func (stats *Stats) Add(d time.Duration) {
|
||
|
stats.durations = append(stats.durations, d)
|
||
|
stats.dirty = true
|
||
|
}
|
||
|
|
||
|
// Clear resets the stats, removing all values.
|
||
|
func (stats *Stats) Clear() {
|
||
|
stats.durations = stats.durations[:0]
|
||
|
stats.histogram = nil
|
||
|
stats.dirty = false
|
||
|
}
|
||
|
|
||
|
// maybeUpdate updates internal stat data if there was any newly added
|
||
|
// stats since this was updated.
|
||
|
func (stats *Stats) maybeUpdate() {
|
||
|
if !stats.dirty {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
stats.min = math.MaxInt64
|
||
|
stats.max = 0
|
||
|
for _, d := range stats.durations {
|
||
|
if stats.min > int64(d) {
|
||
|
stats.min = int64(d)
|
||
|
}
|
||
|
if stats.max < int64(d) {
|
||
|
stats.max = int64(d)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Use the largest unit that can represent the minimum time duration.
|
||
|
stats.unit = time.Nanosecond
|
||
|
for _, u := range []time.Duration{time.Microsecond, time.Millisecond, time.Second} {
|
||
|
if stats.min <= int64(u) {
|
||
|
break
|
||
|
}
|
||
|
stats.unit = u
|
||
|
}
|
||
|
|
||
|
// Adjust the min/max according to the new unit.
|
||
|
stats.min /= int64(stats.unit)
|
||
|
stats.max /= int64(stats.unit)
|
||
|
numBuckets := stats.numBuckets
|
||
|
if n := int(stats.max - stats.min + 1); n < numBuckets {
|
||
|
numBuckets = n
|
||
|
}
|
||
|
stats.histogram = NewHistogram(HistogramOptions{
|
||
|
NumBuckets: numBuckets,
|
||
|
// max-min(lower bound of last bucket) = (1 + growthFactor)^(numBuckets-2) * baseBucketSize.
|
||
|
GrowthFactor: math.Pow(float64(stats.max-stats.min), 1/float64(numBuckets-2)) - 1,
|
||
|
BaseBucketSize: 1.0,
|
||
|
MinValue: stats.min})
|
||
|
|
||
|
for _, d := range stats.durations {
|
||
|
stats.histogram.Add(int64(d / stats.unit))
|
||
|
}
|
||
|
|
||
|
stats.dirty = false
|
||
|
}
|
||
|
|
||
|
// Print writes textual output of the Stats.
|
||
|
func (stats *Stats) Print(w io.Writer) {
|
||
|
stats.maybeUpdate()
|
||
|
|
||
|
if stats.histogram == nil {
|
||
|
fmt.Fprint(w, "Histogram (empty)\n")
|
||
|
} else {
|
||
|
fmt.Fprintf(w, "Histogram (unit: %s)\n", fmt.Sprintf("%v", stats.unit)[1:])
|
||
|
stats.histogram.Print(w)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// String returns the textual output of the Stats as string.
|
||
|
func (stats *Stats) String() string {
|
||
|
var b bytes.Buffer
|
||
|
stats.Print(&b)
|
||
|
return b.String()
|
||
|
}
|