simulation/stats/histogram.go
2018-04-30 20:55:37 +02:00

197 lines
4.1 KiB
Go

package stats
import (
"bytes"
"fmt"
"math"
"strconv"
"sync"
)
// Histogram implements simple histogram for node counters.
type Histogram struct {
ranges []int
counts []int
totalCount int
TotalDataPoint int
MinDataPoint int
MaxDataPoint int
m sync.Mutex
}
// NewHistogram creates a new, ready to use Histogram. The numBins
// must be >= 2. The binFirst is the width of the first bin. The
// binGrowthFactor must be > 1.0 or 0.0.
//
// A special case of binGrowthFactor of 0.0 means the the allocated
// bins will have constant, non-growing size or "width".
func NewHistogram(
numBins int,
binFirst int,
binGrowthFactor float64) *Histogram {
gh := &Histogram{
ranges: make([]int, numBins),
counts: make([]int, numBins),
totalCount: 0,
MinDataPoint: math.MaxInt64,
MaxDataPoint: 0,
}
gh.ranges[0] = 0
gh.ranges[1] = binFirst
for i := 2; i < len(gh.ranges); i++ {
if binGrowthFactor == 0.0 {
gh.ranges[i] = gh.ranges[i-1] + binFirst
} else {
gh.ranges[i] =
int(math.Ceil(binGrowthFactor * float64(gh.ranges[i-1])))
}
}
return gh
}
// Add increases the count in the bin for the given dataPoint.
func (gh *Histogram) Add(dataPoint int, count int) {
gh.m.Lock()
idx := search(gh.ranges, dataPoint)
if idx >= 0 {
gh.counts[idx] += count
gh.totalCount += count
gh.TotalDataPoint += dataPoint
if gh.MinDataPoint > dataPoint {
gh.MinDataPoint = dataPoint
}
if gh.MaxDataPoint < dataPoint {
gh.MaxDataPoint = dataPoint
}
}
gh.m.Unlock()
}
// Finds the last arr index where the arr entry <= dataPoint.
func search(arr []int, dataPoint int) int {
i, j := 0, len(arr)
for i < j {
h := i + (j-i)/2 // Avoids h overflow, where i <= h < j.
if dataPoint >= arr[h] {
i = h + 1
} else {
j = h
}
}
return i - 1
}
// AddAll adds all the counts from the src histogram into this
// histogram. The src and this histogram must either have the same
// exact creation parameters.
func (gh *Histogram) AddAll(src *Histogram) {
src.m.Lock()
gh.m.Lock()
for i := 0; i < len(src.counts); i++ {
gh.counts[i] += src.counts[i]
}
gh.totalCount += src.totalCount
gh.TotalDataPoint += src.TotalDataPoint
if gh.MinDataPoint > src.MinDataPoint {
gh.MinDataPoint = src.MinDataPoint
}
if gh.MaxDataPoint < src.MaxDataPoint {
gh.MaxDataPoint = src.MaxDataPoint
}
gh.m.Unlock()
src.m.Unlock()
}
// Graph emits an ascii graph to the optional out buffer, allocating a
// out buffer if none was supplied. Returns the out buffer. Each
// line emitted may have an optional prefix.
//
// For example:
// 0+ 10=2 10.00% ********
// 10+ 10=1 10.00% ****
// 20+ 10=3 10.00% ************
func (gh *Histogram) EmitGraph(prefix []byte,
out *bytes.Buffer) *bytes.Buffer {
gh.m.Lock()
ranges := gh.ranges
rangesN := len(ranges)
counts := gh.counts
countsN := len(counts)
if out == nil {
out = bytes.NewBuffer(make([]byte, 0, 80*countsN))
}
var maxCount int
for _, c := range counts {
if maxCount < c {
maxCount = c
}
}
maxCountF := float64(maxCount)
totCountF := float64(gh.totalCount)
widthRange := len(strconv.Itoa(int(ranges[rangesN-1])))
widthWidth := len(strconv.Itoa(int(ranges[rangesN-1] - ranges[rangesN-2])))
widthCount := len(strconv.Itoa(int(maxCount)))
// Each line looks like: "[prefix]START+WIDTH=COUNT PCT% BAR\n"
f := fmt.Sprintf("%%%dd+%%%dd=%%%dd%% 7.2f%%%%",
widthRange, widthWidth, widthCount)
var runCount int // Running total while emitting lines.
barLen := float64(len(bar))
for i, c := range counts {
if prefix != nil {
out.Write(prefix)
}
var width int
if i < countsN-1 {
width = int(ranges[i+1] - ranges[i])
}
runCount += c
fmt.Fprintf(out, f, ranges[i], width, c,
100.0*(float64(runCount)/totCountF))
if c > 0 {
out.Write([]byte(" "))
barWant := int(math.Floor(barLen * (float64(c) / maxCountF)))
out.Write(bar[0:barWant])
}
out.Write([]byte("\n"))
}
gh.m.Unlock()
return out
}
var bar = []byte("******************************")
// CallSync invokes the callback func while the histogram is locked.
func (gh *Histogram) CallSync(f func()) {
gh.m.Lock()
f()
gh.m.Unlock()
}