github.com/haraldrudell/parl@v0.4.176/slow-detector-core.go (about)

     1  /*
     2  © 2023–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  package parl
     7  
     8  import (
     9  	"sync/atomic"
    10  	"time"
    11  
    12  	"github.com/haraldrudell/parl/perrors"
    13  	"github.com/haraldrudell/parl/ptime"
    14  )
    15  
    16  const (
    17  	defaultMinReportDuration = 100 * time.Millisecond
    18  	defaultNonReturnPeriod   = time.Minute
    19  )
    20  
    21  type CbSlowDetector func(sdi *SlowDetectorInvocation, hasReturned bool, duration time.Duration)
    22  type slowID uint64
    23  
    24  var slowIDGenerator UniqueIDTypedUint64[slowID]
    25  
    26  // SlowDetectorCore measures latency via Start-Stop invocations
    27  //   - Thread-Safe and multi-threaded, parallel invocations
    28  //   - Separate thread measures time of non-returning, hung invocations
    29  type SlowDetectorCore struct {
    30  	ID       slowID
    31  	callback CbSlowDetector
    32  	thread   *SlowDetectorThread
    33  
    34  	max       AtomicMax[time.Duration]
    35  	alwaysMax AtomicMax[time.Duration]
    36  	last      time.Duration // atomic
    37  	average   ptime.Averager[time.Duration]
    38  }
    39  
    40  // NewSlowDetectorCore returns an object tracking nonm-returning or slow function invocations
    41  //   - callback receives offending slow-detector invocations, cannot be nil
    42  //   - slowTyp configures whether the support-thread is shared
    43  //   - goGen is used for a possible deferred thread-launch
    44  //   - optional values are:
    45  //   - — nonReturnPeriod: how often non-returning invocations are reported, default once per minute
    46  //   - — minimum slowness duration that is being reported, default 100 ms
    47  func NewSlowDetectorCore(callback CbSlowDetector, slowTyp slowType, goGen GoGen, nonReturnPeriod ...time.Duration) (slowDetector *SlowDetectorCore) {
    48  	if callback == nil {
    49  		panic(perrors.NewPF("callback cannot be nil"))
    50  	}
    51  
    52  	// nonReturnPeriod[0]: time between non-return reports, default 1 minute
    53  	var nonReturnPeriod0 time.Duration
    54  	if len(nonReturnPeriod) > 0 {
    55  		nonReturnPeriod0 = nonReturnPeriod[0]
    56  	} else {
    57  		nonReturnPeriod0 = defaultNonReturnPeriod
    58  	}
    59  
    60  	// nonReturnPeriod[1]: minimum duration for slowness to be reported, default 100 ms
    61  	var minReportedDuration time.Duration
    62  	if len(nonReturnPeriod) > 1 {
    63  		minReportedDuration = nonReturnPeriod[1]
    64  	} else {
    65  		minReportedDuration = defaultMinReportDuration
    66  	}
    67  
    68  	return &SlowDetectorCore{
    69  		ID:       slowIDGenerator.ID(),
    70  		callback: callback,
    71  		thread:   NewSlowDetectorThread(slowTyp, nonReturnPeriod0, goGen),
    72  		max:      *NewAtomicMax[time.Duration](minReportedDuration),
    73  		average:  *ptime.NewAverager[time.Duration](),
    74  	}
    75  }
    76  
    77  // Start returns the effective start time for a new timing cycle
    78  //   - value is optional start time, default time.Now()
    79  func (sd *SlowDetectorCore) Start(invoLabel string, value ...time.Time) (invocation *SlowDetectorInvocation) {
    80  
    81  	// get time value for this operation
    82  	var t0 time.Time
    83  	if len(value) > 0 {
    84  		t0 = value[0]
    85  	} else {
    86  		t0 = time.Now()
    87  	}
    88  
    89  	// save in map, launch thread if not already running
    90  	s := SlowDetectorInvocation{
    91  		sID:       slowIDGenerator.ID(),
    92  		invoLabel: invoLabel,
    93  		threadID:  goID(),
    94  		t0:        t0,
    95  		stop:      sd.stop,
    96  		sd:        sd,
    97  	}
    98  	sd.thread.Start(&s)
    99  	return &s
   100  }
   101  
   102  func (sd *SlowDetectorCore) Values() (
   103  	last, average, max time.Duration,
   104  	hasValue bool,
   105  ) {
   106  	last = time.Duration(atomic.LoadInt64((*int64)(&sd.last)))
   107  	averageFloat, _ := sd.average.Average()
   108  	average = time.Duration(averageFloat)
   109  	max, hasValue = sd.alwaysMax.Max()
   110  	return
   111  }
   112  
   113  // Stop is invoked via SlowDetectorInvocation
   114  func (sd *SlowDetectorCore) stop(sdi *SlowDetectorInvocation, value ...time.Time) {
   115  
   116  	// remove from map and possibly shutdown thread
   117  	sd.thread.Stop(sdi)
   118  
   119  	// get time value for this operation
   120  	var t1 time.Time
   121  	if len(value) > 0 {
   122  		t1 = value[0]
   123  	} else {
   124  		t1 = time.Now()
   125  	}
   126  
   127  	// store last and average
   128  	duration := t1.Sub(sdi.t0)
   129  	atomic.StoreInt64((*int64)(&sd.last), int64(duration))
   130  	sd.average.Add(duration, t1)
   131  	sd.alwaysMax.Value(duration)
   132  
   133  	// check against max
   134  	if sd.max.Value(duration) {
   135  		sd.callback(sdi, true, duration)
   136  	}
   137  }