vitess.io/vitess@v0.16.2/go/stats/rates.go (about)

     1  /*
     2  Copyright 2019 The Vitess Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package stats
    18  
    19  import (
    20  	"encoding/json"
    21  	"math"
    22  	"sync"
    23  	"time"
    24  )
    25  
    26  var timeNow = time.Now
    27  
    28  // CountTracker defines the interface that needs to
    29  // be supported by a variable for being tracked by
    30  // Rates.
    31  type CountTracker interface {
    32  	// Counts returns a map which maps each category to a count.
    33  	// Subsequent calls must return a monotonously increasing count for the same
    34  	// category.
    35  	// Optionally, an implementation may include the "All" category which has
    36  	// the total count across all categories (e.g. timing.go does this).
    37  	Counts() map[string]int64
    38  }
    39  
    40  // wrappedCountTracker implements the CountTracker interface.
    41  // It is used in multidimensional.go to publish specific, one-dimensional
    42  // counters.
    43  type wrappedCountTracker struct {
    44  	f func() map[string]int64
    45  }
    46  
    47  func (t wrappedCountTracker) Counts() map[string]int64 { return t.f() }
    48  
    49  // Rates is capable of reporting the rate (typically QPS)
    50  // for any variable that satisfies the CountTracker interface.
    51  type Rates struct {
    52  	// mu guards all fields.
    53  	mu           sync.Mutex
    54  	timeStamps   *RingInt64
    55  	counts       map[string]*RingInt64
    56  	countTracker CountTracker
    57  	samples      int
    58  	interval     time.Duration
    59  	// previousTotalCount is the total number of counts (across all categories)
    60  	// seen in the last sampling interval.
    61  	// It's used to calculate the latest total rate.
    62  	previousTotalCount int64
    63  	// timestampLastSampling is the time the periodic sampling was run last.
    64  	timestampLastSampling time.Time
    65  	// totalRate is the rate of total counts per second seen in the latest
    66  	// sampling interval e.g. 100 queries / 5 seconds sampling interval = 20 QPS.
    67  	totalRate float64
    68  }
    69  
    70  // NewRates reports rolling rate information for countTracker. samples specifies
    71  // the number of samples to report, and interval specifies the time interval
    72  // between samples. The minimum interval is 1 second.
    73  // If passing the special value of -1s as interval, we don't snapshot.
    74  // (use this for tests).
    75  func NewRates(name string, countTracker CountTracker, samples int, interval time.Duration) *Rates {
    76  	if interval < 1*time.Second && interval != -1*time.Second {
    77  		panic("interval too small")
    78  	}
    79  	rt := &Rates{
    80  		timeStamps:            NewRingInt64(samples + 1),
    81  		counts:                make(map[string]*RingInt64),
    82  		countTracker:          countTracker,
    83  		samples:               samples + 1,
    84  		interval:              interval,
    85  		timestampLastSampling: timeNow(),
    86  	}
    87  	if name != "" {
    88  		publish(name, rt)
    89  	}
    90  	if interval > 0 {
    91  		go rt.track()
    92  	}
    93  	return rt
    94  }
    95  
    96  func (rt *Rates) track() {
    97  	for {
    98  		rt.snapshot()
    99  		<-time.After(rt.interval)
   100  	}
   101  }
   102  
   103  func (rt *Rates) snapshot() {
   104  	rt.mu.Lock()
   105  	defer rt.mu.Unlock()
   106  
   107  	now := timeNow()
   108  	rt.timeStamps.Add(now.UnixNano())
   109  
   110  	// Record current count for each category.
   111  	var totalCount int64
   112  	for k, v := range rt.countTracker.Counts() {
   113  		if k != "All" {
   114  			// Include call categories except "All" (which is returned by the
   115  			// "Timer.Counts()" implementation) to avoid double counting.
   116  			totalCount += v
   117  		}
   118  		if values, ok := rt.counts[k]; ok {
   119  			values.Add(v)
   120  		} else {
   121  			rt.counts[k] = NewRingInt64(rt.samples)
   122  			rt.counts[k].Add(0)
   123  			rt.counts[k].Add(v)
   124  		}
   125  	}
   126  
   127  	// Calculate current total rate.
   128  	// NOTE: We assume that every category with a non-zero value, which was
   129  	// tracked in "rt.previousTotalCount" in a previous sampling interval, is
   130  	// tracked in the current sampling interval in "totalCount" as well.
   131  	// (I.e. categories and their count must not "disappear" in
   132  	//  "rt.countTracker.Counts()".)
   133  	durationSeconds := now.Sub(rt.timestampLastSampling).Seconds()
   134  	rate := float64(totalCount-rt.previousTotalCount) / durationSeconds
   135  	// Round rate with a precision of 0.1.
   136  	rt.totalRate = math.Floor(rate*10+0.5) / 10
   137  	rt.previousTotalCount = totalCount
   138  	rt.timestampLastSampling = now
   139  }
   140  
   141  // Get returns for each category (string) its latest rates (up to X values
   142  // where X is the configured number of samples of the Rates struct).
   143  // Rates are ordered from least recent (index 0) to most recent (end of slice).
   144  func (rt *Rates) Get() (rateMap map[string][]float64) {
   145  	rt.mu.Lock()
   146  	defer rt.mu.Unlock()
   147  
   148  	rateMap = make(map[string][]float64)
   149  	timeStamps := rt.timeStamps.Values()
   150  	if len(timeStamps) <= 1 {
   151  		return
   152  	}
   153  	for k, v := range rt.counts {
   154  		rateMap[k] = make([]float64, len(timeStamps)-1)
   155  		values := v.Values()
   156  		valueIndex := len(values) - 1
   157  		for i := len(timeStamps) - 1; i > 0; i-- {
   158  			if valueIndex <= 0 {
   159  				rateMap[k][i-1] = 0
   160  				continue
   161  			}
   162  			elapsed := float64((timeStamps[i] - timeStamps[i-1]) / 1e9)
   163  			rateMap[k][i-1] = float64(values[valueIndex]-values[valueIndex-1]) / elapsed
   164  			valueIndex--
   165  		}
   166  	}
   167  	return
   168  }
   169  
   170  // TotalRate returns the current total rate (counted across categories).
   171  func (rt *Rates) TotalRate() float64 {
   172  	rt.mu.Lock()
   173  	defer rt.mu.Unlock()
   174  
   175  	return rt.totalRate
   176  }
   177  
   178  func (rt *Rates) String() string {
   179  	data, err := json.Marshal(rt.Get())
   180  	if err != nil {
   181  		data, _ = json.Marshal(err.Error())
   182  	}
   183  	return string(data)
   184  }
   185  
   186  type RatesFunc struct {
   187  	F    func() map[string][]float64
   188  	help string
   189  }
   190  
   191  func NewRateFunc(name string, help string, f func() map[string][]float64) *RatesFunc {
   192  	c := &RatesFunc{
   193  		F:    f,
   194  		help: help,
   195  	}
   196  
   197  	if name != "" {
   198  		publish(name, c)
   199  	}
   200  	return c
   201  }
   202  
   203  func (rf *RatesFunc) Help() string {
   204  	return rf.help
   205  }
   206  
   207  func (rf *RatesFunc) String() string {
   208  	data, err := json.Marshal(rf.F())
   209  	if err != nil {
   210  		data, _ = json.Marshal(err.Error())
   211  	}
   212  	return string(data)
   213  }