mirror of https://github.com/XTLS/Xray-core
144 lines
3.2 KiB
Go
144 lines
3.2 KiB
Go
package burst
|
|
|
|
import (
|
|
"math"
|
|
"time"
|
|
)
|
|
|
|
// HealthPingStats is the statistics of HealthPingRTTS
|
|
type HealthPingStats struct {
|
|
All int
|
|
Fail int
|
|
Deviation time.Duration
|
|
Average time.Duration
|
|
Max time.Duration
|
|
Min time.Duration
|
|
}
|
|
|
|
// HealthPingRTTS holds ping rtts for health Checker
|
|
type HealthPingRTTS struct {
|
|
idx int
|
|
cap int
|
|
validity time.Duration
|
|
rtts []*pingRTT
|
|
|
|
lastUpdateAt time.Time
|
|
stats *HealthPingStats
|
|
}
|
|
|
|
type pingRTT struct {
|
|
time time.Time
|
|
value time.Duration
|
|
}
|
|
|
|
// NewHealthPingResult returns a *HealthPingResult with specified capacity
|
|
func NewHealthPingResult(cap int, validity time.Duration) *HealthPingRTTS {
|
|
return &HealthPingRTTS{cap: cap, validity: validity}
|
|
}
|
|
|
|
// Get gets statistics of the HealthPingRTTS
|
|
func (h *HealthPingRTTS) Get() *HealthPingStats {
|
|
return h.getStatistics()
|
|
}
|
|
|
|
// GetWithCache get statistics and write cache for next call
|
|
// Make sure use Mutex.Lock() before calling it, RWMutex.RLock()
|
|
// is not an option since it writes cache
|
|
func (h *HealthPingRTTS) GetWithCache() *HealthPingStats {
|
|
lastPutAt := h.rtts[h.idx].time
|
|
now := time.Now()
|
|
if h.stats == nil || h.lastUpdateAt.Before(lastPutAt) || h.findOutdated(now) >= 0 {
|
|
h.stats = h.getStatistics()
|
|
h.lastUpdateAt = now
|
|
}
|
|
return h.stats
|
|
}
|
|
|
|
// Put puts a new rtt to the HealthPingResult
|
|
func (h *HealthPingRTTS) Put(d time.Duration) {
|
|
if h.rtts == nil {
|
|
h.rtts = make([]*pingRTT, h.cap)
|
|
for i := 0; i < h.cap; i++ {
|
|
h.rtts[i] = &pingRTT{}
|
|
}
|
|
h.idx = -1
|
|
}
|
|
h.idx = h.calcIndex(1)
|
|
now := time.Now()
|
|
h.rtts[h.idx].time = now
|
|
h.rtts[h.idx].value = d
|
|
}
|
|
|
|
func (h *HealthPingRTTS) calcIndex(step int) int {
|
|
idx := h.idx
|
|
idx += step
|
|
if idx >= h.cap {
|
|
idx %= h.cap
|
|
}
|
|
return idx
|
|
}
|
|
|
|
func (h *HealthPingRTTS) getStatistics() *HealthPingStats {
|
|
stats := &HealthPingStats{}
|
|
stats.Fail = 0
|
|
stats.Max = 0
|
|
stats.Min = rttFailed
|
|
sum := time.Duration(0)
|
|
cnt := 0
|
|
validRTTs := make([]time.Duration, 0)
|
|
for _, rtt := range h.rtts {
|
|
switch {
|
|
case rtt.value == 0 || time.Since(rtt.time) > h.validity:
|
|
continue
|
|
case rtt.value == rttFailed:
|
|
stats.Fail++
|
|
continue
|
|
}
|
|
cnt++
|
|
sum += rtt.value
|
|
validRTTs = append(validRTTs, rtt.value)
|
|
if stats.Max < rtt.value {
|
|
stats.Max = rtt.value
|
|
}
|
|
if stats.Min > rtt.value {
|
|
stats.Min = rtt.value
|
|
}
|
|
}
|
|
stats.All = cnt + stats.Fail
|
|
if cnt == 0 {
|
|
stats.Min = 0
|
|
return stats
|
|
}
|
|
stats.Average = time.Duration(int(sum) / cnt)
|
|
var std float64
|
|
if cnt < 2 {
|
|
// no enough data for standard deviation, we assume it's half of the average rtt
|
|
// if we don't do this, standard deviation of 1 round tested nodes is 0, will always
|
|
// selected before 2 or more rounds tested nodes
|
|
std = float64(stats.Average / 2)
|
|
} else {
|
|
variance := float64(0)
|
|
for _, rtt := range validRTTs {
|
|
variance += math.Pow(float64(rtt-stats.Average), 2)
|
|
}
|
|
std = math.Sqrt(variance / float64(cnt))
|
|
}
|
|
stats.Deviation = time.Duration(std)
|
|
return stats
|
|
}
|
|
|
|
func (h *HealthPingRTTS) findOutdated(now time.Time) int {
|
|
for i := h.cap - 1; i < 2*h.cap; i++ {
|
|
// from oldest to latest
|
|
idx := h.calcIndex(i)
|
|
validity := h.rtts[idx].time.Add(h.validity)
|
|
if h.lastUpdateAt.After(validity) {
|
|
return idx
|
|
}
|
|
if validity.Before(now) {
|
|
return idx
|
|
}
|
|
}
|
|
return -1
|
|
}
|