/* * * Copyright 2019 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package stats import ( "crypto/sha256" "encoding/csv" "encoding/hex" "fmt" "io/ioutil" "math" "math/rand" "os" "sort" "strconv" ) // payloadCurveRange represents a line within a payload curve CSV file. type payloadCurveRange struct { from, to int32 weight float64 } // newPayloadCurveRange receives a line from a payload curve CSV file and // returns a *payloadCurveRange if the values are acceptable. func newPayloadCurveRange(line []string) (*payloadCurveRange, error) { if len(line) != 3 { return nil, fmt.Errorf("invalid number of entries in line %v (expected 3)", line) } var from, to int64 var weight float64 var err error if from, err = strconv.ParseInt(line[0], 10, 32); err != nil { return nil, err } if from <= 0 { return nil, fmt.Errorf("line %v: field (%d) must be in (0, %d]", line, from, math.MaxInt32) } if to, err = strconv.ParseInt(line[1], 10, 32); err != nil { return nil, err } if to <= 0 { return nil, fmt.Errorf("line %v: field %d must be in (0, %d]", line, to, math.MaxInt32) } if from > to { return nil, fmt.Errorf("line %v: from (%d) > to (%d)", line, from, to) } if weight, err = strconv.ParseFloat(line[2], 64); err != nil { return nil, err } return &payloadCurveRange{from: int32(from), to: int32(to), weight: weight}, nil } // chooseRandom picks a payload size (in bytes) for a particular range. This is // done with a uniform distribution. func (pcr *payloadCurveRange) chooseRandom() int { if pcr.from == pcr.to { // fast path return int(pcr.from) } return int(rand.Int31n(pcr.to-pcr.from+1) + pcr.from) } // sha256file is a helper function that returns a hex string matching the // SHA-256 sum of the input file. func sha256file(file string) (string, error) { data, err := ioutil.ReadFile(file) if err != nil { return "", err } sum := sha256.Sum256(data) return hex.EncodeToString(sum[:]), nil } // PayloadCurve is an internal representation of a weighted random distribution // CSV file. Once a *PayloadCurve is created with NewPayloadCurve, the // ChooseRandom function should be called to generate random payload sizes. type PayloadCurve struct { pcrs []*payloadCurveRange // Sha256 must be a public field so that the gob encoder can write it to // disk. This will be needed at decode-time by the Hash function. Sha256 string } // NewPayloadCurve parses a .csv file and returns a *PayloadCurve if no errors // were encountered in parsing and initialization. func NewPayloadCurve(file string) (*PayloadCurve, error) { f, err := os.Open(file) if err != nil { return nil, err } defer f.Close() r := csv.NewReader(f) lines, err := r.ReadAll() if err != nil { return nil, err } ret := &PayloadCurve{} var total float64 for _, line := range lines { pcr, err := newPayloadCurveRange(line) if err != nil { return nil, err } ret.pcrs = append(ret.pcrs, pcr) total += pcr.weight } ret.Sha256, err = sha256file(file) if err != nil { return nil, err } for _, pcr := range ret.pcrs { pcr.weight /= total } sort.Slice(ret.pcrs, func(i, j int) bool { if ret.pcrs[i].from == ret.pcrs[j].from { return ret.pcrs[i].to < ret.pcrs[j].to } return ret.pcrs[i].from < ret.pcrs[j].from }) var lastTo int32 for _, pcr := range ret.pcrs { if lastTo >= pcr.from { return nil, fmt.Errorf("[%d, %d] overlaps with a different line", pcr.from, pcr.to) } lastTo = pcr.to } return ret, nil } // ChooseRandom picks a random payload size (in bytes) that follows the // underlying weighted random distribution. func (pc *PayloadCurve) ChooseRandom() int { target := rand.Float64() var seen float64 for _, pcr := range pc.pcrs { seen += pcr.weight if seen >= target { return pcr.chooseRandom() } } // This should never happen, but if it does, return a sane default. return 1 } // Hash returns a string uniquely identifying a payload curve file for feature // matching purposes. func (pc *PayloadCurve) Hash() string { return pc.Sha256 } // ShortHash returns a shortened version of Hash for display purposes. func (pc *PayloadCurve) ShortHash() string { return pc.Sha256[:8] }