github.com/uber-go/tally/v4@v4.1.17/stats.go (about)

     1  // Copyright (c) 2021 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package tally
    22  
    23  import (
    24  	"fmt"
    25  	"math"
    26  	"sort"
    27  	"sync"
    28  	"sync/atomic"
    29  	"time"
    30  
    31  	"github.com/uber-go/tally/v4/internal/identity"
    32  )
    33  
    34  var (
    35  	capabilitiesNone = &capabilities{
    36  		reporting: false,
    37  		tagging:   false,
    38  	}
    39  	capabilitiesReportingNoTagging = &capabilities{
    40  		reporting: true,
    41  		tagging:   false,
    42  	}
    43  	capabilitiesReportingTagging = &capabilities{
    44  		reporting: true,
    45  		tagging:   true,
    46  	}
    47  )
    48  
    49  type capabilities struct {
    50  	reporting bool
    51  	tagging   bool
    52  }
    53  
    54  func (c *capabilities) Reporting() bool {
    55  	return c.reporting
    56  }
    57  
    58  func (c *capabilities) Tagging() bool {
    59  	return c.tagging
    60  }
    61  
    62  type counter struct {
    63  	prev        int64
    64  	curr        int64
    65  	cachedCount CachedCount
    66  }
    67  
    68  func newCounter(cachedCount CachedCount) *counter {
    69  	return &counter{cachedCount: cachedCount}
    70  }
    71  
    72  func (c *counter) Inc(v int64) {
    73  	atomic.AddInt64(&c.curr, v)
    74  }
    75  
    76  func (c *counter) value() int64 {
    77  	curr := atomic.LoadInt64(&c.curr)
    78  
    79  	prev := atomic.LoadInt64(&c.prev)
    80  	if prev == curr {
    81  		return 0
    82  	}
    83  	atomic.StoreInt64(&c.prev, curr)
    84  	return curr - prev
    85  }
    86  
    87  func (c *counter) report(name string, tags map[string]string, r StatsReporter) {
    88  	delta := c.value()
    89  	if delta == 0 {
    90  		return
    91  	}
    92  
    93  	r.ReportCounter(name, tags, delta)
    94  }
    95  
    96  func (c *counter) cachedReport() {
    97  	delta := c.value()
    98  	if delta == 0 {
    99  		return
   100  	}
   101  
   102  	c.cachedCount.ReportCount(delta)
   103  }
   104  
   105  func (c *counter) snapshot() int64 {
   106  	return atomic.LoadInt64(&c.curr) - atomic.LoadInt64(&c.prev)
   107  }
   108  
   109  type gauge struct {
   110  	updated     uint64
   111  	curr        uint64
   112  	cachedGauge CachedGauge
   113  }
   114  
   115  func newGauge(cachedGauge CachedGauge) *gauge {
   116  	return &gauge{cachedGauge: cachedGauge}
   117  }
   118  
   119  func (g *gauge) Update(v float64) {
   120  	atomic.StoreUint64(&g.curr, math.Float64bits(v))
   121  	atomic.StoreUint64(&g.updated, 1)
   122  }
   123  
   124  func (g *gauge) value() float64 {
   125  	return math.Float64frombits(atomic.LoadUint64(&g.curr))
   126  }
   127  
   128  func (g *gauge) report(name string, tags map[string]string, r StatsReporter) {
   129  	if atomic.SwapUint64(&g.updated, 0) == 1 {
   130  		r.ReportGauge(name, tags, g.value())
   131  	}
   132  }
   133  
   134  func (g *gauge) cachedReport() {
   135  	if atomic.SwapUint64(&g.updated, 0) == 1 {
   136  		g.cachedGauge.ReportGauge(g.value())
   137  	}
   138  }
   139  
   140  func (g *gauge) snapshot() float64 {
   141  	return math.Float64frombits(atomic.LoadUint64(&g.curr))
   142  }
   143  
   144  // NB(jra3): timers are a little special because they do no aggregate any data
   145  // at the timer level. The reporter buffers may timer entries and periodically
   146  // flushes.
   147  type timer struct {
   148  	name        string
   149  	tags        map[string]string
   150  	reporter    StatsReporter
   151  	cachedTimer CachedTimer
   152  	unreported  timerValues
   153  }
   154  
   155  type timerValues struct {
   156  	sync.RWMutex
   157  	values []time.Duration
   158  }
   159  
   160  func newTimer(
   161  	name string,
   162  	tags map[string]string,
   163  	r StatsReporter,
   164  	cachedTimer CachedTimer,
   165  ) *timer {
   166  	t := &timer{
   167  		name:        name,
   168  		tags:        tags,
   169  		reporter:    r,
   170  		cachedTimer: cachedTimer,
   171  	}
   172  	if r == nil {
   173  		t.reporter = &timerNoReporterSink{timer: t}
   174  	}
   175  	return t
   176  }
   177  
   178  func (t *timer) Record(interval time.Duration) {
   179  	if t.cachedTimer != nil {
   180  		t.cachedTimer.ReportTimer(interval)
   181  	} else {
   182  		t.reporter.ReportTimer(t.name, t.tags, interval)
   183  	}
   184  }
   185  
   186  func (t *timer) Start() Stopwatch {
   187  	return NewStopwatch(globalNow(), t)
   188  }
   189  
   190  func (t *timer) RecordStopwatch(stopwatchStart time.Time) {
   191  	d := globalNow().Sub(stopwatchStart)
   192  	t.Record(d)
   193  }
   194  
   195  func (t *timer) snapshot() []time.Duration {
   196  	t.unreported.RLock()
   197  	snap := make([]time.Duration, len(t.unreported.values))
   198  	copy(snap, t.unreported.values)
   199  	t.unreported.RUnlock()
   200  	return snap
   201  }
   202  
   203  type timerNoReporterSink struct {
   204  	sync.RWMutex
   205  	timer *timer
   206  }
   207  
   208  func (r *timerNoReporterSink) ReportCounter(
   209  	name string,
   210  	tags map[string]string,
   211  	value int64,
   212  ) {
   213  }
   214  
   215  func (r *timerNoReporterSink) ReportGauge(
   216  	name string,
   217  	tags map[string]string,
   218  	value float64,
   219  ) {
   220  }
   221  
   222  func (r *timerNoReporterSink) ReportTimer(
   223  	name string,
   224  	tags map[string]string,
   225  	interval time.Duration,
   226  ) {
   227  	r.timer.unreported.Lock()
   228  	r.timer.unreported.values = append(r.timer.unreported.values, interval)
   229  	r.timer.unreported.Unlock()
   230  }
   231  
   232  func (r *timerNoReporterSink) ReportHistogramValueSamples(
   233  	name string,
   234  	tags map[string]string,
   235  	buckets Buckets,
   236  	bucketLowerBound float64,
   237  	bucketUpperBound float64,
   238  	samples int64,
   239  ) {
   240  }
   241  
   242  func (r *timerNoReporterSink) ReportHistogramDurationSamples(
   243  	name string,
   244  	tags map[string]string,
   245  	buckets Buckets,
   246  	bucketLowerBound time.Duration,
   247  	bucketUpperBound time.Duration,
   248  	samples int64,
   249  ) {
   250  }
   251  
   252  func (r *timerNoReporterSink) Capabilities() Capabilities {
   253  	return capabilitiesReportingTagging
   254  }
   255  
   256  func (r *timerNoReporterSink) Flush() {
   257  }
   258  
   259  type sampleCounter struct {
   260  	counter      *counter
   261  	cachedBucket CachedHistogramBucket
   262  }
   263  
   264  type histogram struct {
   265  	htype         histogramType
   266  	name          string
   267  	tags          map[string]string
   268  	reporter      StatsReporter
   269  	specification Buckets
   270  	buckets       []histogramBucket
   271  	samples       []sampleCounter
   272  }
   273  
   274  type histogramType int
   275  
   276  const (
   277  	valueHistogramType histogramType = iota
   278  	durationHistogramType
   279  )
   280  
   281  func newHistogram(
   282  	htype histogramType,
   283  	name string,
   284  	tags map[string]string,
   285  	reporter StatsReporter,
   286  	storage bucketStorage,
   287  	cachedHistogram CachedHistogram,
   288  ) *histogram {
   289  	h := &histogram{
   290  		htype:         htype,
   291  		name:          name,
   292  		tags:          tags,
   293  		reporter:      reporter,
   294  		specification: storage.buckets,
   295  		buckets:       storage.hbuckets,
   296  		samples:       make([]sampleCounter, len(storage.hbuckets)),
   297  	}
   298  
   299  	for i := range h.samples {
   300  		h.samples[i].counter = newCounter(nil)
   301  
   302  		if cachedHistogram != nil {
   303  			switch htype {
   304  			case durationHistogramType:
   305  				h.samples[i].cachedBucket = cachedHistogram.DurationBucket(
   306  					durationLowerBound(storage.hbuckets, i),
   307  					storage.hbuckets[i].durationUpperBound,
   308  				)
   309  			case valueHistogramType:
   310  				h.samples[i].cachedBucket = cachedHistogram.ValueBucket(
   311  					valueLowerBound(storage.hbuckets, i),
   312  					storage.hbuckets[i].valueUpperBound,
   313  				)
   314  			}
   315  		}
   316  	}
   317  
   318  	return h
   319  }
   320  
   321  func (h *histogram) report(name string, tags map[string]string, r StatsReporter) {
   322  	for i := range h.buckets {
   323  		samples := h.samples[i].counter.value()
   324  		if samples == 0 {
   325  			continue
   326  		}
   327  
   328  		switch h.htype {
   329  		case valueHistogramType:
   330  			r.ReportHistogramValueSamples(
   331  				name,
   332  				tags,
   333  				h.specification,
   334  				valueLowerBound(h.buckets, i),
   335  				h.buckets[i].valueUpperBound,
   336  				samples,
   337  			)
   338  		case durationHistogramType:
   339  			r.ReportHistogramDurationSamples(
   340  				name,
   341  				tags,
   342  				h.specification,
   343  				durationLowerBound(h.buckets, i),
   344  				h.buckets[i].durationUpperBound,
   345  				samples,
   346  			)
   347  		}
   348  	}
   349  }
   350  
   351  func (h *histogram) cachedReport() {
   352  	for i := range h.buckets {
   353  		samples := h.samples[i].counter.value()
   354  		if samples == 0 {
   355  			continue
   356  		}
   357  
   358  		switch h.htype {
   359  		case valueHistogramType:
   360  			h.samples[i].cachedBucket.ReportSamples(samples)
   361  		case durationHistogramType:
   362  			h.samples[i].cachedBucket.ReportSamples(samples)
   363  		}
   364  	}
   365  }
   366  
   367  func (h *histogram) RecordValue(value float64) {
   368  	if h.htype != valueHistogramType {
   369  		return
   370  	}
   371  
   372  	// Find the highest inclusive of the bucket upper bound
   373  	// and emit directly to it. Since we use BucketPairs to derive
   374  	// buckets there will always be an inclusive bucket as
   375  	// we always have a math.MaxFloat64 bucket.
   376  	idx := sort.Search(len(h.buckets), func(i int) bool {
   377  		return h.buckets[i].valueUpperBound >= value
   378  	})
   379  	h.samples[idx].counter.Inc(1)
   380  }
   381  
   382  func (h *histogram) RecordDuration(value time.Duration) {
   383  	if h.htype != durationHistogramType {
   384  		return
   385  	}
   386  
   387  	// Find the highest inclusive of the bucket upper bound
   388  	// and emit directly to it. Since we use BucketPairs to derive
   389  	// buckets there will always be an inclusive bucket as
   390  	// we always have a math.MaxInt64 bucket.
   391  	idx := sort.Search(len(h.buckets), func(i int) bool {
   392  		return h.buckets[i].durationUpperBound >= value
   393  	})
   394  	h.samples[idx].counter.Inc(1)
   395  }
   396  
   397  func (h *histogram) Start() Stopwatch {
   398  	return NewStopwatch(globalNow(), h)
   399  }
   400  
   401  func (h *histogram) RecordStopwatch(stopwatchStart time.Time) {
   402  	d := globalNow().Sub(stopwatchStart)
   403  	h.RecordDuration(d)
   404  }
   405  
   406  func (h *histogram) snapshotValues() map[float64]int64 {
   407  	if h.htype != valueHistogramType {
   408  		return nil
   409  	}
   410  
   411  	vals := make(map[float64]int64, len(h.buckets))
   412  	for i := range h.buckets {
   413  		vals[h.buckets[i].valueUpperBound] = h.samples[i].counter.snapshot()
   414  	}
   415  
   416  	return vals
   417  }
   418  
   419  func (h *histogram) snapshotDurations() map[time.Duration]int64 {
   420  	if h.htype != durationHistogramType {
   421  		return nil
   422  	}
   423  
   424  	durations := make(map[time.Duration]int64, len(h.buckets))
   425  	for i := range h.buckets {
   426  		durations[h.buckets[i].durationUpperBound] = h.samples[i].counter.snapshot()
   427  	}
   428  
   429  	return durations
   430  }
   431  
   432  type histogramBucket struct {
   433  	valueUpperBound    float64
   434  	durationUpperBound time.Duration
   435  }
   436  
   437  func durationLowerBound(buckets []histogramBucket, i int) time.Duration {
   438  	if i <= 0 {
   439  		return time.Duration(math.MinInt64)
   440  	}
   441  	return buckets[i-1].durationUpperBound
   442  }
   443  
   444  func valueLowerBound(buckets []histogramBucket, i int) float64 {
   445  	if i <= 0 {
   446  		return -math.MaxFloat64
   447  	}
   448  	return buckets[i-1].valueUpperBound
   449  }
   450  
   451  type bucketStorage struct {
   452  	buckets  Buckets
   453  	hbuckets []histogramBucket
   454  }
   455  
   456  func newBucketStorage(
   457  	htype histogramType,
   458  	buckets Buckets,
   459  ) bucketStorage {
   460  	var (
   461  		pairs   = BucketPairs(buckets)
   462  		storage = bucketStorage{
   463  			buckets:  buckets,
   464  			hbuckets: make([]histogramBucket, 0, len(pairs)),
   465  		}
   466  	)
   467  
   468  	for _, pair := range pairs {
   469  		storage.hbuckets = append(storage.hbuckets, histogramBucket{
   470  			valueUpperBound:    pair.UpperBoundValue(),
   471  			durationUpperBound: pair.UpperBoundDuration(),
   472  		})
   473  	}
   474  
   475  	return storage
   476  }
   477  
   478  type bucketCache struct {
   479  	mtx   sync.RWMutex
   480  	cache map[uint64]bucketStorage
   481  }
   482  
   483  func newBucketCache() *bucketCache {
   484  	return &bucketCache{
   485  		cache: make(map[uint64]bucketStorage),
   486  	}
   487  }
   488  
   489  func (c *bucketCache) Get(
   490  	htype histogramType,
   491  	buckets Buckets,
   492  ) bucketStorage {
   493  	id := getBucketsIdentity(buckets)
   494  
   495  	c.mtx.RLock()
   496  	storage, ok := c.cache[id]
   497  	if !ok {
   498  		c.mtx.RUnlock()
   499  		c.mtx.Lock()
   500  		storage = newBucketStorage(htype, buckets)
   501  		c.cache[id] = storage
   502  		c.mtx.Unlock()
   503  	} else {
   504  		c.mtx.RUnlock()
   505  		if !bucketsEqual(buckets, storage.buckets) {
   506  			storage = newBucketStorage(htype, buckets)
   507  		}
   508  	}
   509  
   510  	return storage
   511  }
   512  
   513  // NullStatsReporter is an implementation of StatsReporter than simply does nothing.
   514  var NullStatsReporter StatsReporter = nullStatsReporter{}
   515  
   516  func (r nullStatsReporter) ReportCounter(name string, tags map[string]string, value int64) {
   517  }
   518  
   519  func (r nullStatsReporter) ReportGauge(name string, tags map[string]string, value float64) {
   520  }
   521  
   522  func (r nullStatsReporter) ReportTimer(name string, tags map[string]string, interval time.Duration) {
   523  }
   524  
   525  func (r nullStatsReporter) ReportHistogramValueSamples(
   526  	name string,
   527  	tags map[string]string,
   528  	buckets Buckets,
   529  	bucketLowerBound,
   530  	bucketUpperBound float64,
   531  	samples int64,
   532  ) {
   533  }
   534  
   535  func (r nullStatsReporter) ReportHistogramDurationSamples(
   536  	name string,
   537  	tags map[string]string,
   538  	buckets Buckets,
   539  	bucketLowerBound,
   540  	bucketUpperBound time.Duration,
   541  	samples int64,
   542  ) {
   543  }
   544  
   545  func (r nullStatsReporter) Capabilities() Capabilities {
   546  	return capabilitiesNone
   547  }
   548  
   549  func (r nullStatsReporter) Flush() {
   550  }
   551  
   552  type nullStatsReporter struct{}
   553  
   554  func getBucketsIdentity(buckets Buckets) uint64 {
   555  	switch b := buckets.(type) {
   556  	case DurationBuckets:
   557  		return identity.Durations(b.AsDurations())
   558  	case ValueBuckets:
   559  		return identity.Float64s(b.AsValues())
   560  	default:
   561  		panic(fmt.Sprintf("unexpected bucket type: %T", b))
   562  	}
   563  }