mirror of https://github.com/grpc/grpc-go.git
180 lines
4.6 KiB
Go
180 lines
4.6 KiB
Go
/*
|
|
*
|
|
* 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"
|
|
"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 := os.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]
|
|
}
|