go-pulse/les/lespay/client/timestats.go
Felföldi Zsolt 0851646e48
les, les/lespay/client: add service value statistics and API (#20837)
This PR adds service value measurement statistics to the light client. It
also adds a private API that makes these statistics accessible. A follow-up
PR will add the new server pool which uses these statistics to select
servers with good performance.

This document describes the function of the new components:
https://gist.github.com/zsfelfoldi/3c7ace895234b7b345ab4f71dab102d4

Co-authored-by: rjl493456442 <garyrong0905@gmail.com>
Co-authored-by: rjl493456442 <garyrong0905@gmail.com>
2020-04-09 11:55:32 +02:00

238 lines
6.9 KiB
Go

// Copyright 2020 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package client
import (
"io"
"math"
"time"
"github.com/ethereum/go-ethereum/les/utils"
"github.com/ethereum/go-ethereum/rlp"
)
const (
minResponseTime = time.Millisecond * 50
maxResponseTime = time.Second * 10
timeStatLength = 32
weightScaleFactor = 1000000
)
// ResponseTimeStats is the response time distribution of a set of answered requests,
// weighted with request value, either served by a single server or aggregated for
// multiple servers.
// It it a fixed length (timeStatLength) distribution vector with linear interpolation.
// The X axis (the time values) are not linear, they should be transformed with
// TimeToStatScale and StatScaleToTime.
type (
ResponseTimeStats struct {
stats [timeStatLength]uint64
exp uint64
}
ResponseTimeWeights [timeStatLength]float64
)
var timeStatsLogFactor = (timeStatLength - 1) / (math.Log(float64(maxResponseTime)/float64(minResponseTime)) + 1)
// TimeToStatScale converts a response time to a distribution vector index. The index
// is represented by a float64 so that linear interpolation can be applied.
func TimeToStatScale(d time.Duration) float64 {
if d < 0 {
return 0
}
r := float64(d) / float64(minResponseTime)
if r > 1 {
r = math.Log(r) + 1
}
r *= timeStatsLogFactor
if r > timeStatLength-1 {
return timeStatLength - 1
}
return r
}
// StatScaleToTime converts a distribution vector index to a response time. The index
// is represented by a float64 so that linear interpolation can be applied.
func StatScaleToTime(r float64) time.Duration {
r /= timeStatsLogFactor
if r > 1 {
r = math.Exp(r - 1)
}
return time.Duration(r * float64(minResponseTime))
}
// TimeoutWeights calculates the weight function used for calculating service value
// based on the response time distribution of the received service.
// It is based on the request timeout value of the system. It consists of a half cosine
// function starting with 1, crossing zero at timeout and reaching -1 at 2*timeout.
// After 2*timeout the weight is constant -1.
func TimeoutWeights(timeout time.Duration) (res ResponseTimeWeights) {
for i := range res {
t := StatScaleToTime(float64(i))
if t < 2*timeout {
res[i] = math.Cos(math.Pi / 2 * float64(t) / float64(timeout))
} else {
res[i] = -1
}
}
return
}
// EncodeRLP implements rlp.Encoder
func (rt *ResponseTimeStats) EncodeRLP(w io.Writer) error {
enc := struct {
Stats [timeStatLength]uint64
Exp uint64
}{rt.stats, rt.exp}
return rlp.Encode(w, &enc)
}
// DecodeRLP implements rlp.Decoder
func (rt *ResponseTimeStats) DecodeRLP(s *rlp.Stream) error {
var enc struct {
Stats [timeStatLength]uint64
Exp uint64
}
if err := s.Decode(&enc); err != nil {
return err
}
rt.stats, rt.exp = enc.Stats, enc.Exp
return nil
}
// Add adds a new response time with the given weight to the distribution.
func (rt *ResponseTimeStats) Add(respTime time.Duration, weight float64, expFactor utils.ExpirationFactor) {
rt.setExp(expFactor.Exp)
weight *= expFactor.Factor * weightScaleFactor
r := TimeToStatScale(respTime)
i := int(r)
r -= float64(i)
rt.stats[i] += uint64(weight * (1 - r))
if i < timeStatLength-1 {
rt.stats[i+1] += uint64(weight * r)
}
}
// setExp sets the power of 2 exponent of the structure, scaling base values (the vector
// itself) up or down if necessary.
func (rt *ResponseTimeStats) setExp(exp uint64) {
if exp > rt.exp {
shift := exp - rt.exp
for i, v := range rt.stats {
rt.stats[i] = v >> shift
}
rt.exp = exp
}
if exp < rt.exp {
shift := rt.exp - exp
for i, v := range rt.stats {
rt.stats[i] = v << shift
}
rt.exp = exp
}
}
// Value calculates the total service value based on the given distribution, using the
// specified weight function.
func (rt ResponseTimeStats) Value(weights ResponseTimeWeights, expFactor utils.ExpirationFactor) float64 {
var v float64
for i, s := range rt.stats {
v += float64(s) * weights[i]
}
if v < 0 {
return 0
}
return expFactor.Value(v, rt.exp) / weightScaleFactor
}
// AddStats adds the given ResponseTimeStats to the current one.
func (rt *ResponseTimeStats) AddStats(s *ResponseTimeStats) {
rt.setExp(s.exp)
for i, v := range s.stats {
rt.stats[i] += v
}
}
// SubStats subtracts the given ResponseTimeStats from the current one.
func (rt *ResponseTimeStats) SubStats(s *ResponseTimeStats) {
rt.setExp(s.exp)
for i, v := range s.stats {
if v < rt.stats[i] {
rt.stats[i] -= v
} else {
rt.stats[i] = 0
}
}
}
// Timeout suggests a timeout value based on the previous distribution. The parameter
// is the desired rate of timeouts assuming a similar distribution in the future.
// Note that the actual timeout should have a sensible minimum bound so that operating
// under ideal working conditions for a long time (for example, using a local server
// with very low response times) will not make it very hard for the system to accommodate
// longer response times in the future.
func (rt ResponseTimeStats) Timeout(failRatio float64) time.Duration {
var sum uint64
for _, v := range rt.stats {
sum += v
}
s := uint64(float64(sum) * failRatio)
i := timeStatLength - 1
for i > 0 && s >= rt.stats[i] {
s -= rt.stats[i]
i--
}
r := float64(i) + 0.5
if rt.stats[i] > 0 {
r -= float64(s) / float64(rt.stats[i])
}
if r < 0 {
r = 0
}
th := StatScaleToTime(r)
if th > maxResponseTime {
th = maxResponseTime
}
return th
}
// RtDistribution represents a distribution as a series of (X, Y) chart coordinates,
// where the X axis is the response time in seconds while the Y axis is the amount of
// service value received with a response time close to the X coordinate.
type RtDistribution [timeStatLength][2]float64
// Distribution returns a RtDistribution, optionally normalized to a sum of 1.
func (rt ResponseTimeStats) Distribution(normalized bool, expFactor utils.ExpirationFactor) (res RtDistribution) {
var mul float64
if normalized {
var sum uint64
for _, v := range rt.stats {
sum += v
}
if sum > 0 {
mul = 1 / float64(sum)
}
} else {
mul = expFactor.Value(float64(1)/weightScaleFactor, rt.exp)
}
for i, v := range rt.stats {
res[i][0] = float64(StatScaleToTime(float64(i))) / float64(time.Second)
res[i][1] = float64(v) * mul
}
return
}