github.com/jbendotnet/noms@v0.0.0-20190904222105-c43e4293ea92/go/metrics/histogram.go (about)

     1  // Copyright 2017 Attic Labs, Inc. All rights reserved.
     2  // Licensed under the Apache License, version 2.0:
     3  // http://www.apache.org/licenses/LICENSE-2.0
     4  
     5  package metrics
     6  
     7  import (
     8  	"fmt"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/attic-labs/noms/go/d"
    13  	humanize "github.com/dustin/go-humanize"
    14  )
    15  
    16  // Histogram is a shameless and low-rent knock of the chromium project's
    17  // histogram:
    18  //   https://chromium.googlesource.com/chromium/src/base/+/master/metrics/histogram.h
    19  //
    20  // It logically stores a running histogram of uint64 values and shares some
    21  // important features of its inspiration:
    22  //   * It acccepts a correctness deficit in return for not needing to lock.
    23  //     IOW, concurrent calls to Sample may clobber each other.
    24  //   * It trades compactness and ease of arithmatic across histograms for
    25  //     precision. Samples lose precision up to the range of the values which
    26  //     are stored in a bucket
    27  //
    28  // Only implemented: Log2-based histogram
    29  type Histogram struct {
    30  	sum      uint64
    31  	buckets  [bucketCount]uint64
    32  	ToString ToStringFunc
    33  }
    34  
    35  type ToStringFunc func(v uint64) string
    36  
    37  func identToString(v uint64) string {
    38  	return fmt.Sprintf("%d", v)
    39  }
    40  
    41  const bucketCount = 64
    42  
    43  // Sample adds a uint64 data point to the histogram
    44  func (h *Histogram) Sample(v uint64) {
    45  	d.PanicIfTrue(v == 0)
    46  
    47  	h.sum += v
    48  
    49  	pot := 0
    50  	for v > 0 {
    51  		v = v >> 1
    52  		pot++
    53  	}
    54  
    55  	h.buckets[pot-1]++
    56  }
    57  
    58  // SampleTimeSince is a convenience wrapper around Sample which takes the
    59  // duration since |t|, if 0, rounds to 1 and passes to Sample() as an uint64
    60  // number of nanoseconds.
    61  func (h *Histogram) SampleTimeSince(t time.Time) {
    62  	d := time.Since(t)
    63  	if d == 0 {
    64  		d = 1
    65  	}
    66  	h.Sample(uint64(d))
    67  }
    68  
    69  // SampleLen is a convenience wrapper around Sample which internally type
    70  // asserts the int to a uint64
    71  func (h *Histogram) SampleLen(l int) {
    72  	h.Sample(uint64(l))
    73  }
    74  
    75  func (h Histogram) bucketVal(bucket int) uint64 {
    76  	return 1 << (uint64(bucket))
    77  }
    78  
    79  // Sum return the sum of sampled values, note that Sum can be overflowed without
    80  // overflowing the histogram buckets.
    81  func (h Histogram) Sum() uint64 {
    82  	return h.sum
    83  }
    84  
    85  // Add returns a new Histogram which is the result of adding this and other
    86  // bucket-wise.
    87  func (h *Histogram) Add(other Histogram) {
    88  	h.sum += other.sum
    89  
    90  	for i := 0; i < bucketCount; i++ {
    91  		h.buckets[i] += other.buckets[i]
    92  	}
    93  }
    94  
    95  // Delta returns a new Histogram which is the result of subtracting other from
    96  // this bucket-wise. The intent is to capture changes in the state of histogram
    97  // which is collecting samples over some time period. It will panic if any
    98  // bucket from other is larger than the corresponding bucket in this.
    99  func (h Histogram) Delta(other Histogram) Histogram {
   100  	nh := Histogram{}
   101  	nh.sum = h.sum - other.sum
   102  
   103  	for i := 0; i < bucketCount; i++ {
   104  		c := h.buckets[i]
   105  		l := other.buckets[i]
   106  		d.PanicIfTrue(l > c)
   107  		nh.buckets[i] = c - l
   108  	}
   109  	return nh
   110  }
   111  
   112  // Mean returns 0 if there are no samples, and h.Sum()/h.Samples otherwise.
   113  func (h Histogram) Mean() uint64 {
   114  	samples := h.Samples()
   115  	if samples == 0 {
   116  		return 0
   117  	}
   118  
   119  	return h.Sum() / samples
   120  }
   121  
   122  // Samples returns the number of samples contained in the histogram
   123  func (h Histogram) Samples() uint64 {
   124  	s := uint64(0)
   125  	for i := 0; i < bucketCount; i++ {
   126  		s += h.buckets[i]
   127  	}
   128  	return s
   129  }
   130  
   131  func (h Histogram) String() string {
   132  	f := h.ToString
   133  	if f == nil {
   134  		f = identToString
   135  	}
   136  	return fmt.Sprintf("Mean: %s, Sum: %s, Samples: %d", f(h.Mean()), f(h.Sum()), h.Samples())
   137  }
   138  
   139  func NewTimeHistogram() Histogram {
   140  	return Histogram{ToString: timeToString}
   141  }
   142  
   143  func timeToString(v uint64) string {
   144  	return time.Duration(v).String()
   145  }
   146  
   147  // NewByteHistogram stringifies values using humanize over byte values
   148  func NewByteHistogram() Histogram {
   149  	return Histogram{ToString: humanize.Bytes}
   150  }
   151  
   152  const colWidth = 100
   153  
   154  // Report returns an ASCII graph of the non-zero range of normalized buckets.
   155  // IOW, it returns a basic graph of the histogram
   156  func (h Histogram) Report() string {
   157  	ts := h.ToString
   158  	if ts == nil {
   159  		ts = identToString
   160  	}
   161  
   162  	maxSamples := uint64(0)
   163  	foundFirstNonEmpty := false
   164  	firstNonEmpty := 0
   165  	lastNonEmpty := 0
   166  	for i := 0; i < bucketCount; i++ {
   167  		samples := h.buckets[i]
   168  
   169  		if samples > 0 {
   170  			lastNonEmpty = i
   171  			if !foundFirstNonEmpty {
   172  				foundFirstNonEmpty = true
   173  				firstNonEmpty = i
   174  			}
   175  		}
   176  
   177  		if samples > maxSamples {
   178  			maxSamples = samples
   179  		}
   180  	}
   181  
   182  	if maxSamples == 0 {
   183  		return ""
   184  	}
   185  
   186  	val := uint64(1)
   187  
   188  	p := func(bucket int) string {
   189  		samples := h.buckets[bucket]
   190  		val := h.bucketVal(bucket)
   191  		adj := samples * colWidth / maxSamples
   192  		return fmt.Sprintf("%s> %s: (%d)", strings.Repeat("-", int(adj)), ts(val), samples)
   193  	}
   194  
   195  	lines := make([]string, 0)
   196  	for i := 0; i < bucketCount; i++ {
   197  		if i >= firstNonEmpty && i <= lastNonEmpty {
   198  			lines = append(lines, p(i))
   199  		}
   200  
   201  		val = val << 1
   202  	}
   203  
   204  	return strings.Join(lines, "\n")
   205  }