github.com/xmplusdev/xray-core@v1.8.10/app/observatory/burst/healthping_result.go (about)

     1  package burst
     2  
     3  import (
     4  	"math"
     5  	"time"
     6  )
     7  
     8  // HealthPingStats is the statistics of HealthPingRTTS
     9  type HealthPingStats struct {
    10  	All       int
    11  	Fail      int
    12  	Deviation time.Duration
    13  	Average   time.Duration
    14  	Max       time.Duration
    15  	Min       time.Duration
    16  }
    17  
    18  // HealthPingRTTS holds ping rtts for health Checker
    19  type HealthPingRTTS struct {
    20  	idx      int
    21  	cap      int
    22  	validity time.Duration
    23  	rtts     []*pingRTT
    24  
    25  	lastUpdateAt time.Time
    26  	stats        *HealthPingStats
    27  }
    28  
    29  type pingRTT struct {
    30  	time  time.Time
    31  	value time.Duration
    32  }
    33  
    34  // NewHealthPingResult returns a *HealthPingResult with specified capacity
    35  func NewHealthPingResult(cap int, validity time.Duration) *HealthPingRTTS {
    36  	return &HealthPingRTTS{cap: cap, validity: validity}
    37  }
    38  
    39  // Get gets statistics of the HealthPingRTTS
    40  func (h *HealthPingRTTS) Get() *HealthPingStats {
    41  	return h.getStatistics()
    42  }
    43  
    44  // GetWithCache get statistics and write cache for next call
    45  // Make sure use Mutex.Lock() before calling it, RWMutex.RLock()
    46  // is not an option since it writes cache
    47  func (h *HealthPingRTTS) GetWithCache() *HealthPingStats {
    48  	lastPutAt := h.rtts[h.idx].time
    49  	now := time.Now()
    50  	if h.stats == nil || h.lastUpdateAt.Before(lastPutAt) || h.findOutdated(now) >= 0 {
    51  		h.stats = h.getStatistics()
    52  		h.lastUpdateAt = now
    53  	}
    54  	return h.stats
    55  }
    56  
    57  // Put puts a new rtt to the HealthPingResult
    58  func (h *HealthPingRTTS) Put(d time.Duration) {
    59  	if h.rtts == nil {
    60  		h.rtts = make([]*pingRTT, h.cap)
    61  		for i := 0; i < h.cap; i++ {
    62  			h.rtts[i] = &pingRTT{}
    63  		}
    64  		h.idx = -1
    65  	}
    66  	h.idx = h.calcIndex(1)
    67  	now := time.Now()
    68  	h.rtts[h.idx].time = now
    69  	h.rtts[h.idx].value = d
    70  }
    71  
    72  func (h *HealthPingRTTS) calcIndex(step int) int {
    73  	idx := h.idx
    74  	idx += step
    75  	if idx >= h.cap {
    76  		idx %= h.cap
    77  	}
    78  	return idx
    79  }
    80  
    81  func (h *HealthPingRTTS) getStatistics() *HealthPingStats {
    82  	stats := &HealthPingStats{}
    83  	stats.Fail = 0
    84  	stats.Max = 0
    85  	stats.Min = rttFailed
    86  	sum := time.Duration(0)
    87  	cnt := 0
    88  	validRTTs := make([]time.Duration, 0)
    89  	for _, rtt := range h.rtts {
    90  		switch {
    91  		case rtt.value == 0 || time.Since(rtt.time) > h.validity:
    92  			continue
    93  		case rtt.value == rttFailed:
    94  			stats.Fail++
    95  			continue
    96  		}
    97  		cnt++
    98  		sum += rtt.value
    99  		validRTTs = append(validRTTs, rtt.value)
   100  		if stats.Max < rtt.value {
   101  			stats.Max = rtt.value
   102  		}
   103  		if stats.Min > rtt.value {
   104  			stats.Min = rtt.value
   105  		}
   106  	}
   107  	stats.All = cnt + stats.Fail
   108  	if cnt == 0 {
   109  		stats.Min = 0
   110  		return stats
   111  	}
   112  	stats.Average = time.Duration(int(sum) / cnt)
   113  	var std float64
   114  	if cnt < 2 {
   115  		// no enough data for standard deviation, we assume it's half of the average rtt
   116  		// if we don't do this, standard deviation of 1 round tested nodes is 0, will always
   117  		// selected before 2 or more rounds tested nodes
   118  		std = float64(stats.Average / 2)
   119  	} else {
   120  		variance := float64(0)
   121  		for _, rtt := range validRTTs {
   122  			variance += math.Pow(float64(rtt-stats.Average), 2)
   123  		}
   124  		std = math.Sqrt(variance / float64(cnt))
   125  	}
   126  	stats.Deviation = time.Duration(std)
   127  	return stats
   128  }
   129  
   130  func (h *HealthPingRTTS) findOutdated(now time.Time) int {
   131  	for i := h.cap - 1; i < 2*h.cap; i++ {
   132  		// from oldest to latest
   133  		idx := h.calcIndex(i)
   134  		validity := h.rtts[idx].time.Add(h.validity)
   135  		if h.lastUpdateAt.After(validity) {
   136  			return idx
   137  		}
   138  		if validity.Before(now) {
   139  			return idx
   140  		}
   141  	}
   142  	return -1
   143  }