google.golang.org/grpc@v1.62.1/benchmark/stats/curve.go (about) 1 /* 2 * 3 * Copyright 2019 gRPC authors. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 19 package stats 20 21 import ( 22 "crypto/sha256" 23 "encoding/csv" 24 "encoding/hex" 25 "fmt" 26 "math" 27 "math/rand" 28 "os" 29 "sort" 30 "strconv" 31 ) 32 33 // payloadCurveRange represents a line within a payload curve CSV file. 34 type payloadCurveRange struct { 35 from, to int32 36 weight float64 37 } 38 39 // newPayloadCurveRange receives a line from a payload curve CSV file and 40 // returns a *payloadCurveRange if the values are acceptable. 41 func newPayloadCurveRange(line []string) (*payloadCurveRange, error) { 42 if len(line) != 3 { 43 return nil, fmt.Errorf("invalid number of entries in line %v (expected 3)", line) 44 } 45 46 var from, to int64 47 var weight float64 48 var err error 49 if from, err = strconv.ParseInt(line[0], 10, 32); err != nil { 50 return nil, err 51 } 52 if from <= 0 { 53 return nil, fmt.Errorf("line %v: field (%d) must be in (0, %d]", line, from, math.MaxInt32) 54 } 55 if to, err = strconv.ParseInt(line[1], 10, 32); err != nil { 56 return nil, err 57 } 58 if to <= 0 { 59 return nil, fmt.Errorf("line %v: field %d must be in (0, %d]", line, to, math.MaxInt32) 60 } 61 if from > to { 62 return nil, fmt.Errorf("line %v: from (%d) > to (%d)", line, from, to) 63 } 64 if weight, err = strconv.ParseFloat(line[2], 64); err != nil { 65 return nil, err 66 } 67 return &payloadCurveRange{from: int32(from), to: int32(to), weight: weight}, nil 68 } 69 70 // chooseRandom picks a payload size (in bytes) for a particular range. This is 71 // done with a uniform distribution. 72 func (pcr *payloadCurveRange) chooseRandom() int { 73 if pcr.from == pcr.to { // fast path 74 return int(pcr.from) 75 } 76 77 return int(rand.Int31n(pcr.to-pcr.from+1) + pcr.from) 78 } 79 80 // sha256file is a helper function that returns a hex string matching the 81 // SHA-256 sum of the input file. 82 func sha256file(file string) (string, error) { 83 data, err := os.ReadFile(file) 84 if err != nil { 85 return "", err 86 } 87 sum := sha256.Sum256(data) 88 return hex.EncodeToString(sum[:]), nil 89 } 90 91 // PayloadCurve is an internal representation of a weighted random distribution 92 // CSV file. Once a *PayloadCurve is created with NewPayloadCurve, the 93 // ChooseRandom function should be called to generate random payload sizes. 94 type PayloadCurve struct { 95 pcrs []*payloadCurveRange 96 // Sha256 must be a public field so that the gob encoder can write it to 97 // disk. This will be needed at decode-time by the Hash function. 98 Sha256 string 99 } 100 101 // NewPayloadCurve parses a .csv file and returns a *PayloadCurve if no errors 102 // were encountered in parsing and initialization. 103 func NewPayloadCurve(file string) (*PayloadCurve, error) { 104 f, err := os.Open(file) 105 if err != nil { 106 return nil, err 107 } 108 defer f.Close() 109 110 r := csv.NewReader(f) 111 lines, err := r.ReadAll() 112 if err != nil { 113 return nil, err 114 } 115 116 ret := &PayloadCurve{} 117 var total float64 118 for _, line := range lines { 119 pcr, err := newPayloadCurveRange(line) 120 if err != nil { 121 return nil, err 122 } 123 124 ret.pcrs = append(ret.pcrs, pcr) 125 total += pcr.weight 126 } 127 128 ret.Sha256, err = sha256file(file) 129 if err != nil { 130 return nil, err 131 } 132 for _, pcr := range ret.pcrs { 133 pcr.weight /= total 134 } 135 136 sort.Slice(ret.pcrs, func(i, j int) bool { 137 if ret.pcrs[i].from == ret.pcrs[j].from { 138 return ret.pcrs[i].to < ret.pcrs[j].to 139 } 140 return ret.pcrs[i].from < ret.pcrs[j].from 141 }) 142 143 var lastTo int32 144 for _, pcr := range ret.pcrs { 145 if lastTo >= pcr.from { 146 return nil, fmt.Errorf("[%d, %d] overlaps with a different line", pcr.from, pcr.to) 147 } 148 lastTo = pcr.to 149 } 150 151 return ret, nil 152 } 153 154 // ChooseRandom picks a random payload size (in bytes) that follows the 155 // underlying weighted random distribution. 156 func (pc *PayloadCurve) ChooseRandom() int { 157 target := rand.Float64() 158 var seen float64 159 for _, pcr := range pc.pcrs { 160 seen += pcr.weight 161 if seen >= target { 162 return pcr.chooseRandom() 163 } 164 } 165 166 // This should never happen, but if it does, return a sane default. 167 return 1 168 } 169 170 // Hash returns a string uniquely identifying a payload curve file for feature 171 // matching purposes. 172 func (pc *PayloadCurve) Hash() string { 173 return pc.Sha256 174 } 175 176 // ShortHash returns a shortened version of Hash for display purposes. 177 func (pc *PayloadCurve) ShortHash() string { 178 return pc.Sha256[:8] 179 }