blob: 68821bcc2690f0d74767475e690cb9d6dbff63d5 [file] [log] [blame]
/*
*
* 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]
}