github.com/imannamdari/v2ray-core/v5@v5.0.5/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 }