github.com/haraldrudell/parl@v0.4.176/counter/datapoint.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  	"github.com/haraldrudell/parl/perrors"
    14  	"github.com/haraldrudell/parl/ptime"
    15  )
    16  
    17  // Datapoint tracks a fluctuating value with average. Thread-safe.
    18  type Datapoint struct {
    19  	period time.Duration
    20  
    21  	lock  sync.Mutex
    22  	value uint64
    23  	max   uint64
    24  	min   uint64
    25  	// period in in progress
    26  	periodStart  time.Time
    27  	isFullPeriod bool
    28  	nPeriod      uint64
    29  	aggregate    float64
    30  	// valid values
    31  	average float64
    32  	n       uint64
    33  }
    34  
    35  var _ parl.DatapointValue = &Datapoint{}
    36  
    37  func newDatapoint(period time.Duration) (datapoint parl.Datapoint) {
    38  	if period <= 0 {
    39  		panic(perrors.ErrorfPF("period must be positive: %s", ptime.Duration(period)))
    40  	}
    41  	return &Datapoint{period: period}
    42  }
    43  
    44  // SetValue records a new datapoint value
    45  func (dt *Datapoint) SetValue(value uint64) (datapoint parl.Datapoint) {
    46  	tNow := time.Now() // best possible tNow
    47  	datapoint = dt
    48  	dt.lock.Lock()
    49  	defer dt.lock.Unlock()
    50  
    51  	// update value max min
    52  	dt.value = value
    53  	isFirst := dt.periodStart.IsZero()
    54  	if isFirst || value > dt.max {
    55  		dt.max = value
    56  	}
    57  	if isFirst || value < dt.min {
    58  		dt.min = value
    59  	}
    60  
    61  	// determine period
    62  	thisPeriodStart, isEnd := dt.isEnd(tNow)
    63  
    64  	// first SetValue invocation: initialize periodStart
    65  	if isFirst {
    66  		dt.periodStart = thisPeriodStart
    67  		return // first period started return
    68  	}
    69  
    70  	// check for end of a period
    71  	if !isEnd {
    72  		if !dt.isFullPeriod {
    73  			// not at end of a period and current period is the first, partial, period
    74  			return // in the first, non-full, period return: no averaging data updates
    75  		}
    76  		// not at end of a full period
    77  	} else {
    78  		// beyond the end of the recording period: handle period change
    79  		dt.updatePeriod(thisPeriodStart)
    80  	}
    81  
    82  	// update aggregate
    83  	dt.nPeriod++
    84  	dt.aggregate += float64(value)
    85  
    86  	return
    87  }
    88  
    89  func (dt *Datapoint) CloneDatapoint() (datapoint parl.Datapoint) {
    90  	return dt.cloneDatapoint(false)
    91  }
    92  func (dt *Datapoint) CloneDatapointReset() (datapoint parl.Datapoint) {
    93  	return dt.cloneDatapoint(true)
    94  }
    95  func (dt *Datapoint) cloneDatapoint(reset bool) (datapoint parl.Datapoint) {
    96  	tNow := time.Now()
    97  	dt.lock.Lock()
    98  	defer dt.lock.Unlock()
    99  
   100  	// handle period end
   101  	if thisPeriodStart, isEnd := dt.isEnd(tNow); isEnd {
   102  		dt.updatePeriod(thisPeriodStart)
   103  	}
   104  
   105  	datapoint = &Datapoint{
   106  		period:       dt.period,
   107  		value:        dt.value,
   108  		max:          dt.max,
   109  		min:          dt.min,
   110  		periodStart:  dt.periodStart,
   111  		isFullPeriod: dt.isFullPeriod,
   112  		nPeriod:      dt.nPeriod,
   113  		aggregate:    dt.aggregate,
   114  		average:      dt.average,
   115  		n:            dt.n,
   116  	}
   117  
   118  	if !reset {
   119  		return
   120  	}
   121  
   122  	dt.value = 0
   123  	dt.max = 0
   124  	dt.min = 0
   125  	dt.periodStart = time.Time{}
   126  	dt.isFullPeriod = false
   127  	dt.nPeriod = 0
   128  	dt.aggregate = 0
   129  	dt.average = 0
   130  	dt.n = 0
   131  
   132  	return
   133  }
   134  
   135  func (dt *Datapoint) GetDatapoint() (value, max, min uint64, isValid bool, average float64, n uint64) {
   136  	tNow := time.Now() // best possible tNow
   137  	dt.lock.Lock()
   138  	defer dt.lock.Unlock()
   139  
   140  	// handle period end
   141  	if thisPeriodStart, isEnd := dt.isEnd(tNow); isEnd {
   142  		dt.updatePeriod(thisPeriodStart)
   143  	}
   144  
   145  	value = dt.value
   146  	max = dt.max
   147  	min = dt.min
   148  	isValid = !dt.periodStart.IsZero()
   149  	average = dt.average
   150  	n = dt.n
   151  
   152  	return
   153  }
   154  
   155  func (dt *Datapoint) DatapointValue() (value uint64) {
   156  	value, _, _, _, _, _ = dt.GetDatapoint()
   157  	return
   158  }
   159  func (dt *Datapoint) DatapointMax() (max uint64) {
   160  	_, max, _, _, _, _ = dt.GetDatapoint()
   161  	return
   162  }
   163  func (dt *Datapoint) DatapointMin() (min uint64) {
   164  	_, _, min, _, _, _ = dt.GetDatapoint()
   165  	return
   166  }
   167  
   168  // isEnd determines if the current period has ended.
   169  // isEnd returns the start of the current period.
   170  func (dt *Datapoint) isEnd(tNow time.Time) (thisPeriodStart time.Time, isEnd bool) {
   171  	thisPeriodStart = tNow.Truncate(dt.period)
   172  
   173  	// if the first SetValue has not happened, it cannot be isEnd
   174  	if dt.periodStart.IsZero() {
   175  		return
   176  	}
   177  
   178  	isEnd = !dt.periodStart.Equal(thisPeriodStart)
   179  	return
   180  }
   181  
   182  // updatePeriod carries otu a period change.
   183  // updatePeriod must only be executed when isEnd returns true.
   184  func (dt *Datapoint) updatePeriod(thisPeriodStart time.Time) {
   185  	if !dt.isFullPeriod {
   186  		// the ending period was not a full period
   187  		// no avergaing was done for that non-full period
   188  		dt.isFullPeriod = true // first full period return
   189  		return
   190  	}
   191  
   192  	// save valid average
   193  	if dt.nPeriod > 0 {
   194  		dt.average = dt.aggregate / float64(dt.nPeriod)
   195  		dt.n = dt.nPeriod
   196  	}
   197  
   198  	// restart period averaging
   199  	dt.periodStart = thisPeriodStart
   200  	dt.aggregate = 0
   201  	dt.nPeriod = 0
   202  }