github.com/aidoskuneen/adk-node@v0.0.0-20220315131952-2e32567cb7f4/les/vflux/client/timestats.go (about)

     1  // Copyright 2021 The adkgo Authors
     2  // This file is part of the adkgo library (adapted for adkgo from go--ethereum v1.10.8).
     3  //
     4  // the adkgo 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 adkgo 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 adkgo 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/aidoskuneen/adk-node/les/utils"
    25  	"github.com/aidoskuneen/adk-node/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  }