github.com/haraldrudell/parl@v0.4.176/counter/rate-counter.go (about)

     1  /*
     2  © 2022–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  package counter
     7  
     8  import (
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/haraldrudell/parl"
    13  	"golang.org/x/exp/maps"
    14  )
    15  
    16  const (
    17  	averagerSize = 10
    18  	minInterval  = time.Microsecond
    19  )
    20  
    21  // RateCounter is a value/running/max counter with averaging.
    22  //   - rate of increase, maximum and average rate of increase in value
    23  //   - rate of increase, maximum increase and decrease rates and average of value
    24  type RateCounter struct {
    25  	Counter // value-running-max atomic-access container
    26  
    27  	lock             sync.Mutex
    28  	lastDoInvocation time.Time // used to calculate true duration of each period
    29  	hasValues        bool      // indicates that value and running was initialized at start of period
    30  	value            uint64    // value at beginning of period
    31  	running          uint64    // running at beginning of period
    32  	m                map[parl.RateType]float64
    33  	valueAvg         Averager
    34  	runningAvg       Averager
    35  }
    36  
    37  var _ parl.RateCounterValues = &RateCounter{} // RateCounter is parl.RateCounterValues
    38  
    39  // newRateCounter returns a rate-counter, an extension to a regular 3-value counter
    40  func newRateCounter() (counter *RateCounter) {
    41  	return &RateCounter{
    42  		lastDoInvocation: time.Now(),
    43  		m:                make(map[parl.RateType]float64),
    44  		valueAvg:         *NewAverager(averagerSize),
    45  		runningAvg:       *NewAverager(averagerSize),
    46  	}
    47  }
    48  
    49  // Rates returns a map of current rate-results
    50  func (r *RateCounter) Rates() (rates map[parl.RateType]float64) {
    51  	r.lock.Lock()
    52  	defer r.lock.Unlock()
    53  
    54  	rates = maps.Clone(r.m)
    55  	return
    56  }
    57  
    58  // Do completes rate-calculations for a period
    59  //   - Do is invoked by the task container
    60  //   - at is an accurate timestamp, ie. not from a time.Interval
    61  func (r *RateCounter) Do(at time.Time) {
    62  	r.lock.Lock()
    63  	defer r.lock.Unlock()
    64  
    65  	// calculate interval duration: positive, at least minInterval
    66  	var duration = at.Sub(r.lastDoInvocation)
    67  	if duration < minInterval {
    68  		return // ignore if too close to last occasion, or negative
    69  	}
    70  	r.lastDoInvocation = at
    71  	var seconds = duration.Seconds()
    72  
    73  	// get current values from the underlying rate/running/max counter
    74  	var value, running, _ = r.Counter.Get()
    75  
    76  	// running goes up and down
    77  	//	- running average is total change over the period divided by duration of all periods
    78  	r.m[parl.RunningAverage] = r.runningAvg.Add(running, duration)
    79  
    80  	// rates requires values from beginning of the period
    81  	//	- collect values at beginning of the very first period
    82  	if !r.hasValues {
    83  
    84  		// first invocation: initialize values
    85  		r.value = value
    86  		r.running = running
    87  		r.hasValues = true
    88  		return // populated start of period return
    89  	}
    90  
    91  	// update rates since previous period
    92  	r.do(r.value, value, seconds, parl.ValueRate, parl.ValueMaxRate, parl.NotAValue)
    93  	r.do(r.running, running, seconds, parl.RunningRate, parl.RunningMaxRate, parl.RunningMaxDecRate)
    94  
    95  	// update last period’s values
    96  	r.value = value
    97  	r.running = running
    98  
    99  	// for value, average its rate of increase
   100  	//	- since value is monotonically increasing, this is meaningful
   101  	r.m[parl.ValueRateAverage] = r.valueAvg.Add(uint64(r.m[parl.ValueRate]), duration)
   102  }
   103  
   104  // do performs rate counter calculation over a period starting at from value and
   105  // ending at to value.
   106  //   - from is value at beginning of period
   107  //   - to is current value
   108  func (r *RateCounter) do(from, to uint64, seconds float64, rateIndex, maxRateIndex, maxDecRateIndex parl.RateType) {
   109  	var m = r.m
   110  	if to == from {
   111  		return // value is zero, rate is zero return: keep last rate
   112  	}
   113  
   114  	// calculate positive rate and max rate
   115  	if to > from { // not negative
   116  		rate := float64(to-from) / seconds
   117  		m[rateIndex] = rate
   118  		if rate > m[maxRateIndex] {
   119  			m[maxRateIndex] = rate
   120  		}
   121  		return // positive rate return
   122  	}
   123  
   124  	// calculate decreasing rate
   125  	if maxDecRateIndex == parl.NotAValue {
   126  		return // max decrease rate should not be calculated
   127  	}
   128  	rate := (float64(to) - float64(from)) / seconds
   129  	if rate < m[maxDecRateIndex] {
   130  		m[maxDecRateIndex] = rate
   131  	}
   132  	// negative rate return
   133  }