github.com/loov/hrtime@v1.0.3/histogram.go (about)

     1  package hrtime
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"math"
     7  	"sort"
     8  	"strings"
     9  	"time"
    10  )
    11  
    12  // HistogramOptions is configuration.
    13  type HistogramOptions struct {
    14  	BinCount int
    15  	// NiceRange will try to round the bucket sizes to have a nicer output.
    16  	NiceRange bool
    17  	// Clamp values to either percentile or to a specific ns value.
    18  	ClampMaximum    float64
    19  	ClampPercentile float64
    20  }
    21  
    22  var defaultOptions = HistogramOptions{
    23  	BinCount:        10,
    24  	NiceRange:       true,
    25  	ClampMaximum:    0,
    26  	ClampPercentile: 0.999,
    27  }
    28  
    29  // Histogram is a binned historgram with different statistics.
    30  type Histogram struct {
    31  	Minimum float64
    32  	Average float64
    33  	Maximum float64
    34  
    35  	P50, P90, P99, P999, P9999 float64
    36  
    37  	Bins []HistogramBin
    38  
    39  	// for pretty printing
    40  	Width int
    41  }
    42  
    43  // HistogramBin is a single bin in histogram
    44  type HistogramBin struct {
    45  	Start    float64
    46  	Count    int
    47  	Width    float64
    48  	andAbove bool
    49  }
    50  
    51  // NewDurationHistogram creates a histogram from time.Duration-s.
    52  func NewDurationHistogram(durations []time.Duration, opts *HistogramOptions) *Histogram {
    53  	nanos := make([]float64, len(durations))
    54  	for i, d := range durations {
    55  		nanos[i] = float64(d.Nanoseconds())
    56  	}
    57  	return NewHistogram(nanos, opts)
    58  }
    59  
    60  // NewHistogram creates a new histogram from the specified nanosecond values.
    61  func NewHistogram(nanoseconds []float64, opts *HistogramOptions) *Histogram {
    62  	if opts.BinCount <= 0 {
    63  		panic("binCount must be larger than 0")
    64  	}
    65  
    66  	hist := &Histogram{}
    67  	hist.Width = 40
    68  	hist.Bins = make([]HistogramBin, opts.BinCount)
    69  	if len(nanoseconds) == 0 {
    70  		return hist
    71  	}
    72  
    73  	nanoseconds = append(nanoseconds[:0:0], nanoseconds...)
    74  	sort.Float64s(nanoseconds)
    75  
    76  	hist.Minimum = nanoseconds[0]
    77  	hist.Maximum = nanoseconds[len(nanoseconds)-1]
    78  
    79  	hist.Average = float64(0)
    80  	for _, x := range nanoseconds {
    81  		hist.Average += x
    82  	}
    83  	hist.Average /= float64(len(nanoseconds))
    84  
    85  	p := func(p float64) float64 {
    86  		i := int(math.Round(p * float64(len(nanoseconds))))
    87  		if i < 0 {
    88  			i = 0
    89  		}
    90  		if i >= len(nanoseconds) {
    91  			i = len(nanoseconds) - 1
    92  		}
    93  		return nanoseconds[i]
    94  	}
    95  
    96  	hist.P50, hist.P90, hist.P99, hist.P999, hist.P9999 = p(0.50), p(0.90), p(0.99), p(0.999), p(0.9999)
    97  
    98  	clampMaximum := hist.Maximum
    99  	if opts.ClampPercentile > 0 {
   100  		clampMaximum = p(opts.ClampPercentile)
   101  	}
   102  	if opts.ClampMaximum > 0 {
   103  		clampMaximum = opts.ClampMaximum
   104  	}
   105  
   106  	var minimum, spacing float64
   107  
   108  	if opts.NiceRange {
   109  		minimum, spacing = calculateNiceSteps(hist.Minimum, clampMaximum, opts.BinCount)
   110  	} else {
   111  		minimum, spacing = calculateSteps(hist.Minimum, clampMaximum, opts.BinCount)
   112  	}
   113  
   114  	for i := range hist.Bins {
   115  		hist.Bins[i].Start = spacing*float64(i) + minimum
   116  	}
   117  	hist.Bins[0].Start = hist.Minimum
   118  
   119  	for _, x := range nanoseconds {
   120  		k := int(float64(x-minimum) / spacing)
   121  		if k < 0 {
   122  			k = 0
   123  		}
   124  		if k >= opts.BinCount {
   125  			k = opts.BinCount - 1
   126  			hist.Bins[k].andAbove = true
   127  		}
   128  		hist.Bins[k].Count++
   129  	}
   130  
   131  	maxBin := 0
   132  	for _, bin := range hist.Bins {
   133  		if bin.Count > maxBin {
   134  			maxBin = bin.Count
   135  		}
   136  	}
   137  
   138  	for k := range hist.Bins {
   139  		bin := &hist.Bins[k]
   140  		bin.Width = float64(bin.Count) / float64(maxBin)
   141  	}
   142  
   143  	return hist
   144  }
   145  
   146  // Divide divides histogram by number of repetitions for the tests.
   147  func (hist *Histogram) Divide(n int) {
   148  	hist.Minimum /= float64(n)
   149  	hist.Average /= float64(n)
   150  	hist.Maximum /= float64(n)
   151  
   152  	hist.P50 /= float64(n)
   153  	hist.P90 /= float64(n)
   154  	hist.P99 /= float64(n)
   155  	hist.P999 /= float64(n)
   156  	hist.P9999 /= float64(n)
   157  
   158  	for i := range hist.Bins {
   159  		hist.Bins[i].Start /= float64(n)
   160  	}
   161  }
   162  
   163  // WriteStatsTo writes formatted statistics to w.
   164  func (hist *Histogram) WriteStatsTo(w io.Writer) (int64, error) {
   165  	n, err := fmt.Fprintf(w, "  avg %v;  min %v;  p50 %v;  max %v;\n  p90 %v;  p99 %v;  p999 %v;  p9999 %v;\n",
   166  		time.Duration(truncate(hist.Average, 3)),
   167  		time.Duration(truncate(hist.Minimum, 3)),
   168  		time.Duration(truncate(hist.P50, 3)),
   169  		time.Duration(truncate(hist.Maximum, 3)),
   170  
   171  		time.Duration(truncate(hist.P90, 3)),
   172  		time.Duration(truncate(hist.P99, 3)),
   173  		time.Duration(truncate(hist.P999, 3)),
   174  		time.Duration(truncate(hist.P9999, 3)),
   175  	)
   176  	return int64(n), err
   177  }
   178  
   179  // WriteTo writes formatted statistics and histogram to w.
   180  func (hist *Histogram) WriteTo(w io.Writer) (int64, error) {
   181  	written, err := hist.WriteStatsTo(w)
   182  	if err != nil {
   183  		return written, err
   184  	}
   185  
   186  	// TODO: use consistently single unit instead of multiple
   187  	maxCountLength := 3
   188  	for i := range hist.Bins {
   189  		x := (int)(math.Ceil(math.Log10(float64(hist.Bins[i].Count + 1))))
   190  		if x > maxCountLength {
   191  			maxCountLength = x
   192  		}
   193  	}
   194  
   195  	var n int
   196  	for _, bin := range hist.Bins {
   197  		if bin.andAbove {
   198  			n, err = fmt.Fprintf(w, " %10v+[%[2]*[3]v] ", time.Duration(round(bin.Start, 3)), maxCountLength, bin.Count)
   199  		} else {
   200  			n, err = fmt.Fprintf(w, " %10v [%[2]*[3]v] ", time.Duration(round(bin.Start, 3)), maxCountLength, bin.Count)
   201  		}
   202  
   203  		written += int64(n)
   204  		if err != nil {
   205  			return written, err
   206  		}
   207  
   208  		width := float64(hist.Width) * bin.Width
   209  		frac := width - math.Trunc(width)
   210  
   211  		n, err = io.WriteString(w, strings.Repeat("█", int(width)))
   212  		written += int64(n)
   213  		if err != nil {
   214  			return written, err
   215  		}
   216  
   217  		if frac > 0.5 {
   218  			n, err = io.WriteString(w, `▌`)
   219  			written += int64(n)
   220  			if err != nil {
   221  				return written, err
   222  			}
   223  		}
   224  
   225  		n, err = fmt.Fprintf(w, "\n")
   226  		written += int64(n)
   227  		if err != nil {
   228  			return written, err
   229  		}
   230  	}
   231  	return written, nil
   232  }
   233  
   234  // StringStats returns a string representation of the histogram stats.
   235  func (hist *Histogram) StringStats() string {
   236  	var buffer strings.Builder
   237  	_, _ = hist.WriteStatsTo(&buffer)
   238  	return buffer.String()
   239  }
   240  
   241  // String returns a string representation of the histogram.
   242  func (hist *Histogram) String() string {
   243  	var buffer strings.Builder
   244  	_, _ = hist.WriteTo(&buffer)
   245  	return buffer.String()
   246  }