github.com/cryptogateway/go-paymex@v0.0.0-20210204174735-96277fb1e602/les/lespay/client/timestats.go (about) 1 // Copyright 2020 The go-ethereum Authors 2 // This file is part of the go-ethereum library. 3 // 4 // The go-ethereum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The go-ethereum library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 16 17 package client 18 19 import ( 20 "io" 21 "math" 22 "time" 23 24 "github.com/cryptogateway/go-paymex/les/utils" 25 "github.com/cryptogateway/go-paymex/rlp" 26 ) 27 28 const ( 29 minResponseTime = time.Millisecond * 50 30 maxResponseTime = time.Second * 10 31 timeStatLength = 32 32 weightScaleFactor = 1000000 33 ) 34 35 // ResponseTimeStats is the response time distribution of a set of answered requests, 36 // weighted with request value, either served by a single server or aggregated for 37 // multiple servers. 38 // It it a fixed length (timeStatLength) distribution vector with linear interpolation. 39 // The X axis (the time values) are not linear, they should be transformed with 40 // TimeToStatScale and StatScaleToTime. 41 type ( 42 ResponseTimeStats struct { 43 stats [timeStatLength]uint64 44 exp uint64 45 } 46 ResponseTimeWeights [timeStatLength]float64 47 ) 48 49 var timeStatsLogFactor = (timeStatLength - 1) / (math.Log(float64(maxResponseTime)/float64(minResponseTime)) + 1) 50 51 // TimeToStatScale converts a response time to a distribution vector index. The index 52 // is represented by a float64 so that linear interpolation can be applied. 53 func TimeToStatScale(d time.Duration) float64 { 54 if d < 0 { 55 return 0 56 } 57 r := float64(d) / float64(minResponseTime) 58 if r > 1 { 59 r = math.Log(r) + 1 60 } 61 r *= timeStatsLogFactor 62 if r > timeStatLength-1 { 63 return timeStatLength - 1 64 } 65 return r 66 } 67 68 // StatScaleToTime converts a distribution vector index to a response time. The index 69 // is represented by a float64 so that linear interpolation can be applied. 70 func StatScaleToTime(r float64) time.Duration { 71 r /= timeStatsLogFactor 72 if r > 1 { 73 r = math.Exp(r - 1) 74 } 75 return time.Duration(r * float64(minResponseTime)) 76 } 77 78 // TimeoutWeights calculates the weight function used for calculating service value 79 // based on the response time distribution of the received service. 80 // It is based on the request timeout value of the system. It consists of a half cosine 81 // function starting with 1, crossing zero at timeout and reaching -1 at 2*timeout. 82 // After 2*timeout the weight is constant -1. 83 func TimeoutWeights(timeout time.Duration) (res ResponseTimeWeights) { 84 for i := range res { 85 t := StatScaleToTime(float64(i)) 86 if t < 2*timeout { 87 res[i] = math.Cos(math.Pi / 2 * float64(t) / float64(timeout)) 88 } else { 89 res[i] = -1 90 } 91 } 92 return 93 } 94 95 // EncodeRLP implements rlp.Encoder 96 func (rt *ResponseTimeStats) EncodeRLP(w io.Writer) error { 97 enc := struct { 98 Stats [timeStatLength]uint64 99 Exp uint64 100 }{rt.stats, rt.exp} 101 return rlp.Encode(w, &enc) 102 } 103 104 // DecodeRLP implements rlp.Decoder 105 func (rt *ResponseTimeStats) DecodeRLP(s *rlp.Stream) error { 106 var enc struct { 107 Stats [timeStatLength]uint64 108 Exp uint64 109 } 110 if err := s.Decode(&enc); err != nil { 111 return err 112 } 113 rt.stats, rt.exp = enc.Stats, enc.Exp 114 return nil 115 } 116 117 // Add adds a new response time with the given weight to the distribution. 118 func (rt *ResponseTimeStats) Add(respTime time.Duration, weight float64, expFactor utils.ExpirationFactor) { 119 rt.setExp(expFactor.Exp) 120 weight *= expFactor.Factor * weightScaleFactor 121 r := TimeToStatScale(respTime) 122 i := int(r) 123 r -= float64(i) 124 rt.stats[i] += uint64(weight * (1 - r)) 125 if i < timeStatLength-1 { 126 rt.stats[i+1] += uint64(weight * r) 127 } 128 } 129 130 // setExp sets the power of 2 exponent of the structure, scaling base values (the vector 131 // itself) up or down if necessary. 132 func (rt *ResponseTimeStats) setExp(exp uint64) { 133 if exp > rt.exp { 134 shift := exp - rt.exp 135 for i, v := range rt.stats { 136 rt.stats[i] = v >> shift 137 } 138 rt.exp = exp 139 } 140 if exp < rt.exp { 141 shift := rt.exp - exp 142 for i, v := range rt.stats { 143 rt.stats[i] = v << shift 144 } 145 rt.exp = exp 146 } 147 } 148 149 // Value calculates the total service value based on the given distribution, using the 150 // specified weight function. 151 func (rt ResponseTimeStats) Value(weights ResponseTimeWeights, expFactor utils.ExpirationFactor) float64 { 152 var v float64 153 for i, s := range rt.stats { 154 v += float64(s) * weights[i] 155 } 156 if v < 0 { 157 return 0 158 } 159 return expFactor.Value(v, rt.exp) / weightScaleFactor 160 } 161 162 // AddStats adds the given ResponseTimeStats to the current one. 163 func (rt *ResponseTimeStats) AddStats(s *ResponseTimeStats) { 164 rt.setExp(s.exp) 165 for i, v := range s.stats { 166 rt.stats[i] += v 167 } 168 } 169 170 // SubStats subtracts the given ResponseTimeStats from the current one. 171 func (rt *ResponseTimeStats) SubStats(s *ResponseTimeStats) { 172 rt.setExp(s.exp) 173 for i, v := range s.stats { 174 if v < rt.stats[i] { 175 rt.stats[i] -= v 176 } else { 177 rt.stats[i] = 0 178 } 179 } 180 } 181 182 // Timeout suggests a timeout value based on the previous distribution. The parameter 183 // is the desired rate of timeouts assuming a similar distribution in the future. 184 // Note that the actual timeout should have a sensible minimum bound so that operating 185 // under ideal working conditions for a long time (for example, using a local server 186 // with very low response times) will not make it very hard for the system to accommodate 187 // longer response times in the future. 188 func (rt ResponseTimeStats) Timeout(failRatio float64) time.Duration { 189 var sum uint64 190 for _, v := range rt.stats { 191 sum += v 192 } 193 s := uint64(float64(sum) * failRatio) 194 i := timeStatLength - 1 195 for i > 0 && s >= rt.stats[i] { 196 s -= rt.stats[i] 197 i-- 198 } 199 r := float64(i) + 0.5 200 if rt.stats[i] > 0 { 201 r -= float64(s) / float64(rt.stats[i]) 202 } 203 if r < 0 { 204 r = 0 205 } 206 th := StatScaleToTime(r) 207 if th > maxResponseTime { 208 th = maxResponseTime 209 } 210 return th 211 } 212 213 // RtDistribution represents a distribution as a series of (X, Y) chart coordinates, 214 // where the X axis is the response time in seconds while the Y axis is the amount of 215 // service value received with a response time close to the X coordinate. 216 type RtDistribution [timeStatLength][2]float64 217 218 // Distribution returns a RtDistribution, optionally normalized to a sum of 1. 219 func (rt ResponseTimeStats) Distribution(normalized bool, expFactor utils.ExpirationFactor) (res RtDistribution) { 220 var mul float64 221 if normalized { 222 var sum uint64 223 for _, v := range rt.stats { 224 sum += v 225 } 226 if sum > 0 { 227 mul = 1 / float64(sum) 228 } 229 } else { 230 mul = expFactor.Value(float64(1)/weightScaleFactor, rt.exp) 231 } 232 for i, v := range rt.stats { 233 res[i][0] = float64(StatScaleToTime(float64(i))) / float64(time.Second) 234 res[i][1] = float64(v) * mul 235 } 236 return 237 }