github.com/zeebo/mon@v0.0.0-20211012163247-13d39bdb54fa/inthist/histogram.go (about)

     1  package inthist
     2  
     3  import (
     4  	"math/bits"
     5  	"sync/atomic"
     6  	"unsafe"
     7  
     8  	"github.com/zeebo/mon/internal/bitmap"
     9  	"golang.org/x/sys/cpu"
    10  )
    11  
    12  //go:noescape
    13  func sumHistogramAVX2(data *[64]uint32) uint64
    14  
    15  // sumHistogram is either backed by AVX2 or a partially unrolled loop.
    16  var sumHistogram = map[bool]func(*[64]uint32) uint64{
    17  	true:  sumHistogramAVX2,
    18  	false: sumHistogramSlow,
    19  }[cpu.X86.HasAVX2]
    20  
    21  // sumHistogramSlow sums the histogram buffers using an unrolled loop.
    22  func sumHistogramSlow(buf *[64]uint32) (total uint64) {
    23  	for i := 0; i <= 56; i += 8 {
    24  		total += 0 +
    25  			uint64(buf[i+0]) +
    26  			uint64(buf[i+1]) +
    27  			uint64(buf[i+2]) +
    28  			uint64(buf[i+3]) +
    29  			uint64(buf[i+4]) +
    30  			uint64(buf[i+5]) +
    31  			uint64(buf[i+6]) +
    32  			uint64(buf[i+7])
    33  	}
    34  	return total
    35  }
    36  
    37  const ( // histEntriesBits of 6 keeps ~1.5% error.
    38  	histEntriesBits = 6
    39  	histBuckets     = 63 - histEntriesBits
    40  	histEntries     = 1 << histEntriesBits // 64
    41  )
    42  
    43  // histBucket is the type of a histogram bucket.
    44  type histBucket struct {
    45  	entries [histEntries]uint32
    46  }
    47  
    48  // loadBucket atomically loads the bucket pointer from the address.
    49  func loadBucket(addr **histBucket) *histBucket {
    50  	return (*histBucket)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(addr))))
    51  }
    52  
    53  // casBucket atomically compares and swaps the bucket pointer into the address.
    54  func casBucket(addr **histBucket, old, new *histBucket) bool {
    55  	return atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(addr)),
    56  		unsafe.Pointer(old), unsafe.Pointer(new))
    57  }
    58  
    59  // lowerValue returns the smallest value that can be stored at the entry.
    60  func lowerValue(bucket, entry uint64) int64 {
    61  	return (1<<bucket-1)<<histEntriesBits + int64(entry<<bucket)
    62  }
    63  
    64  // upperValue returns the largest value that can be stored at the entry (inclusive).
    65  func upperValue(bucket, entry uint64) int64 {
    66  	return (1<<bucket-1)<<histEntriesBits + int64(entry<<bucket) + (1<<bucket - 1)
    67  }
    68  
    69  // middleBase returns the base offset for finding the middleValue for a bucket.
    70  func middleBase(bucket uint64) float64 {
    71  	return float64((int64(1)<<bucket-1)<<histEntriesBits) +
    72  		float64((int64(1)<<bucket)-1)/2
    73  }
    74  
    75  // middleOffset returns the amount to add to the appropriate middleBase to get the
    76  // middleValue for some bucket and entry.
    77  func middleOffset(bucket, entry uint64) float64 {
    78  	return float64(int64(entry << bucket))
    79  }
    80  
    81  // bucketEntry returns the bucket and entry that should contain the value v.
    82  func bucketEntry(v int64) (bucket, entry uint64) {
    83  	uv := uint64(v + histEntries)
    84  	bucket = uint64(bits.Len64(uv)) - histEntriesBits - 1
    85  	return bucket % 64, (uv>>bucket - histEntries) % histEntries
    86  }
    87  
    88  // Histogram keeps track of an exponentially increasing range of buckets
    89  // so that there is a consistent relative error per bucket.
    90  type Histogram struct {
    91  	bitmap  bitmap.B64      // encodes which buckets are set
    92  	buckets [64]*histBucket // 64 so that bounds checks can be removed easier
    93  }
    94  
    95  // Observe records the value in the histogram.
    96  func (h *Histogram) Observe(v int64) {
    97  	// upperValue is inlined and constant folded
    98  	if v < 0 || v > upperValue(histBuckets-1, histEntries-1) {
    99  		return
   100  	}
   101  
   102  	bucket, entry := bucketEntry(v)
   103  
   104  	b := loadBucket(&h.buckets[bucket])
   105  	if b == nil {
   106  		b = new(histBucket)
   107  		if !casBucket(&h.buckets[bucket], nil, b) {
   108  			b = loadBucket(&h.buckets[bucket])
   109  		} else {
   110  			h.bitmap.Set(uint(bucket))
   111  		}
   112  	}
   113  
   114  	atomic.AddUint32(&b.entries[entry], 1)
   115  }
   116  
   117  // Total returns the number of completed calls.
   118  func (h *Histogram) Total() (total int64) {
   119  	bm := h.bitmap.Clone()
   120  	for {
   121  		bucket, ok := bm.Next()
   122  		if !ok {
   123  			return total
   124  		}
   125  		total += int64(sumHistogram(&loadBucket(&h.buckets[bucket]).entries))
   126  	}
   127  }
   128  
   129  // For quantile, we compute a target value at the start. After that, when
   130  // walking the counts, we are sure we'll still hit the target since the
   131  // counts and totals monotonically increase. This means that the returned
   132  // result might be slightly smaller than the real result, but since
   133  // the call is so fast, it's unlikely to drift very much.
   134  
   135  // Quantile returns an estimation of the qth quantile in [0, 1].
   136  func (h *Histogram) Quantile(q float64) int64 {
   137  	target, acc := uint64(q*float64(h.Total())+0.5), uint64(0)
   138  
   139  	bm := h.bitmap.Clone()
   140  	for {
   141  		bucket, ok := bm.Next()
   142  		if !ok {
   143  			return upperValue(histBuckets-1, histEntries-1)
   144  		}
   145  
   146  		b := loadBucket(&h.buckets[bucket])
   147  		bacc := acc + sumHistogram(&b.entries)
   148  		if bacc < target {
   149  			acc = bacc
   150  			continue
   151  		}
   152  
   153  		for entry := range b.entries[:] {
   154  			acc += uint64(atomic.LoadUint32(&b.entries[entry]))
   155  			if acc >= target {
   156  				base := middleBase(uint64(bucket))
   157  				return int64(base + 0.5 + middleOffset(uint64(bucket), uint64(entry)))
   158  			}
   159  		}
   160  	}
   161  }
   162  
   163  // CDF returns an estimate for what quantile the value v is.
   164  func (h *Histogram) CDF(v int64) float64 {
   165  	var sum, total int64
   166  	vbucket, ventry := bucketEntry(v)
   167  	bm := h.bitmap.Clone()
   168  	for {
   169  		bucket, ok := bm.Next()
   170  		if !ok {
   171  			return float64(sum) / float64(total)
   172  		}
   173  
   174  		entries := &loadBucket(&h.buckets[bucket]).entries
   175  		bucketSum := int64(sumHistogram(entries))
   176  		total += bucketSum
   177  
   178  		if uint64(bucket) < vbucket {
   179  			sum += bucketSum
   180  		} else if uint64(bucket) == vbucket {
   181  			for i := uint64(0); i <= ventry; i++ {
   182  				sum += int64(entries[i])
   183  			}
   184  		}
   185  	}
   186  }
   187  
   188  // When computing the average or variance, we don't do any locking.
   189  // When we have finished adding up into the accumulator, we know the
   190  // actual statistic has to be somewhere between acc / stotal and
   191  // acc / etotal, because the counts and totals monotonically increase.
   192  // We return the average of those bounds. Since we're dominated by
   193  // cache misses, this doesn't cost much extra.
   194  
   195  // Sum returns an estimation of the sum.
   196  func (h *Histogram) Sum() float64 {
   197  	var values float64
   198  
   199  	bm := h.bitmap.Clone()
   200  	for {
   201  		bucket, ok := bm.Next()
   202  		if !ok {
   203  			return values
   204  		}
   205  
   206  		b := loadBucket(&h.buckets[bucket])
   207  		base := middleBase(uint64(bucket))
   208  
   209  		for entry := range b.entries[:] {
   210  			if count := float64(atomic.LoadUint32(&b.entries[entry])); count > 0 {
   211  				value := base + middleOffset(uint64(bucket), uint64(entry))
   212  				values += count * value
   213  			}
   214  		}
   215  	}
   216  }
   217  
   218  // Average returns an estimation of the sum and average.
   219  func (h *Histogram) Average() (float64, float64) {
   220  	var values, total float64
   221  
   222  	bm := h.bitmap.Clone()
   223  	for {
   224  		bucket, ok := bm.Next()
   225  		if !ok {
   226  			if total == 0 {
   227  				return 0, 0
   228  			}
   229  			return values, values / total
   230  		}
   231  
   232  		b := loadBucket(&h.buckets[bucket])
   233  		base := middleBase(uint64(bucket))
   234  
   235  		for entry := range b.entries[:] {
   236  			if count := float64(atomic.LoadUint32(&b.entries[entry])); count > 0 {
   237  				value := base + middleOffset(uint64(bucket), uint64(entry))
   238  				values += count * value
   239  				total += count
   240  			}
   241  		}
   242  	}
   243  }
   244  
   245  // Variance returns an estimation of the sum, average and variance.
   246  func (h *Histogram) Variance() (float64, float64, float64) {
   247  	var values, total, total2, mean, vari float64
   248  
   249  	bm := h.bitmap.Clone()
   250  	for {
   251  		bucket, ok := bm.Next()
   252  		if !ok {
   253  			if total == 0 {
   254  				return 0, 0, 0
   255  			}
   256  			return values, values / total, vari / total
   257  		}
   258  
   259  		b := loadBucket(&h.buckets[bucket])
   260  		base := middleBase(uint64(bucket))
   261  
   262  		for entry := range b.entries[:] {
   263  			if count := float64(atomic.LoadUint32(&b.entries[entry])); count > 0 {
   264  				value := base + middleOffset(uint64(bucket), uint64(entry))
   265  				values += count * value
   266  				total += count
   267  				total2 += count * count
   268  				mean_ := mean
   269  				mean += (count / total) * (value - mean_)
   270  				vari += count * (value - mean_) * (value - mean)
   271  			}
   272  		}
   273  	}
   274  }
   275  
   276  // Percentiles returns calls the callback with information about the CDF.
   277  // The total may increase during the call, but it should never be less
   278  // than the count.
   279  func (h *Histogram) Percentiles(cb func(value, count, total int64)) {
   280  	acc, total := int64(0), h.Total()
   281  
   282  	bm := h.bitmap.Clone()
   283  	for {
   284  		bucket, ok := bm.Next()
   285  		if !ok {
   286  			return
   287  		}
   288  
   289  		b := loadBucket(&h.buckets[bucket])
   290  		for entry := range b.entries[:] {
   291  			if count := int64(atomic.LoadUint32(&b.entries[entry])); count > 0 {
   292  				if acc == 0 {
   293  					cb(lowerValue(uint64(bucket), uint64(entry)), 0, total)
   294  				}
   295  				acc += count
   296  				if acc > total {
   297  					total = h.Total()
   298  				}
   299  				cb(upperValue(uint64(bucket), uint64(entry)), acc, total)
   300  			}
   301  		}
   302  	}
   303  }