From f02d22cf8fce15714ffde4293d9f15669017473c Mon Sep 17 00:00:00 2001 From: William Banfield Date: Thu, 1 Sep 2022 16:19:56 -0400 Subject: [PATCH] initial working version of all --- test/loadtime/Makefile | 5 +- test/loadtime/basic.sh | 2 +- test/loadtime/cmd/{generate => load}/main.go | 13 +-- test/loadtime/cmd/report/main.go | 4 +- test/loadtime/payload/payload.go | 87 +++++++++++++++++++ .../payload/{size_test.go => payload_test.go} | 21 ++++- test/loadtime/payload/size.go | 76 ---------------- test/loadtime/report/report.go | 45 ++++++++-- test/loadtime/report/report_test.go | 34 +++++++- 9 files changed, 186 insertions(+), 101 deletions(-) rename test/loadtime/cmd/{generate => load}/main.go (89%) create mode 100644 test/loadtime/payload/payload.go rename test/loadtime/payload/{size_test.go => payload_test.go} (53%) delete mode 100644 test/loadtime/payload/size.go diff --git a/test/loadtime/Makefile b/test/loadtime/Makefile index 7805e04b9..bab69e28e 100644 --- a/test/loadtime/Makefile +++ b/test/loadtime/Makefile @@ -1,8 +1,9 @@ GOMOD="github.com/tendermint/tendermint/test/loadtime" -OUTPUT?=build/loadtime +OUTPUT?=build/ build: - go build $(BUILD_FLAGS) -tags '$(BUILD_TAGS)' -o $(OUTPUT) . + go build $(BUILD_FLAGS) -tags '$(BUILD_TAGS)' -o $(OUTPUT)load ./cmd/load/ + go build $(BUILD_FLAGS) -tags '$(BUILD_TAGS)' -o $(OUTPUT)report ./cmd/report/ .PHONY: build check-proto-gen-deps: diff --git a/test/loadtime/basic.sh b/test/loadtime/basic.sh index e83486dfc..b135232b8 100755 --- a/test/loadtime/basic.sh +++ b/test/loadtime/basic.sh @@ -4,7 +4,7 @@ set -euo pipefail # A basic invocation of the loadtime tool. -./build/loadtime \ +./build/load \ -c 1 -T 10 -r 1000 -s 1024 \ --broadcast-tx-method sync \ --endpoints ws://localhost:26657/websocket diff --git a/test/loadtime/cmd/generate/main.go b/test/loadtime/cmd/load/main.go similarity index 89% rename from test/loadtime/cmd/generate/main.go rename to test/loadtime/cmd/load/main.go index 59460baf8..ce1659b40 100644 --- a/test/loadtime/cmd/generate/main.go +++ b/test/loadtime/cmd/load/main.go @@ -38,7 +38,10 @@ func main() { } func (f *ClientFactory) ValidateConfig(cfg loadtest.Config) error { - psb := payload.UnpaddedSizeBytes() + psb, err := payload.MaxUnpaddedSize() + if err != nil { + return err + } if psb > cfg.Size { return fmt.Errorf("payload size exceeds configured size") } @@ -54,9 +57,9 @@ func (f *ClientFactory) NewClient(cfg loadtest.Config) (loadtest.Client, error) } func (c *TxGenerator) GenerateTx() ([]byte, error) { - return payload.NewBytes(payload.Options{ - Conns: c.conns, - Rate: c.rate, - Size: c.size, + return payload.NewBytes(&payload.Payload{ + Connections: c.conns, + Rate: c.rate, + Size: c.size, }) } diff --git a/test/loadtime/cmd/report/main.go b/test/loadtime/cmd/report/main.go index 424473920..e3bebf4f4 100644 --- a/test/loadtime/cmd/report/main.go +++ b/test/loadtime/cmd/report/main.go @@ -2,8 +2,6 @@ package main import ( "fmt" - "math" - "time" "github.com/tendermint/tendermint/store" "github.com/tendermint/tendermint/test/loadtime/report" @@ -26,5 +24,5 @@ func main() { fmt.Println(r.Min) fmt.Println(r.Max) fmt.Println(r.Avg) - fmt.Println(int64(time.Duration(math.MaxInt64) / (2 * time.Second))) + fmt.Println(r.StdDev) } diff --git a/test/loadtime/payload/payload.go b/test/loadtime/payload/payload.go new file mode 100644 index 000000000..521c5e476 --- /dev/null +++ b/test/loadtime/payload/payload.go @@ -0,0 +1,87 @@ +package payload + +import ( + "bytes" + "crypto/rand" + "errors" + "fmt" + "math" + + "google.golang.org/protobuf/proto" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" +) + +const keyPrefix = "a=" + +// NewBytes generates a new payload and returns the encoded representation of +// the payload as a slice of bytes. NewBytes uses the fields on the Options +// to create the payload. +func NewBytes(p *Payload) ([]byte, error) { + p.Padding = make([]byte, 1) + if p.Time == nil { + p.Time = timestamppb.Now() + } + us, err := CalculateUnpaddedSize(p) + if err != nil { + return nil, err + } + if p.Size < uint64(us) { + return nil, fmt.Errorf("configured size %d not large enough to fit unpadded transaction of size %d", p.Size, us) + } + p.Padding = make([]byte, p.Size-uint64(us)) + _, err = rand.Read(p.Padding) + if err != nil { + return nil, err + } + b, err := proto.Marshal(p) + if err != nil { + return nil, err + } + + // prepend a single key so that the kv store only ever stores a single + // transaction instead of storing all tx and ballooning in size. + return append([]byte(keyPrefix), b...), nil +} + +// FromBytes extracts a paylod from the byte representation of the payload. +// FromBytes leaves the padding untouched, returning it to the caller to handle +// or discard per their preference. +func FromBytes(b []byte) (*Payload, error) { + p := &Payload{} + tr := bytes.TrimPrefix(b, []byte(keyPrefix)) + if bytes.Equal(b, tr) { + return nil, errors.New("payload bytes missing key prefix") + } + err := proto.Unmarshal(tr, p) + if err != nil { + return nil, err + } + return p, nil +} + +// MaxUnpaddedSize returns the maximum size that a payload may be if no padding +// is included. +func MaxUnpaddedSize() (int, error) { + p := &Payload{ + Time: timestamppb.Now(), + Connections: math.MaxUint64, + Rate: math.MaxUint64, + Size: math.MaxUint64, + Padding: make([]byte, 1), + } + return CalculateUnpaddedSize(p) +} + +// CalculateUnpaddedSize calculates the size of the passed in payload for the +// purpose of determining how much padding to add to add to reach the target size. +// CalculateUnpaddedSize returns an error if the payload Padding field is longer than 1. +func CalculateUnpaddedSize(p *Payload) (int, error) { + if len(p.Padding) != 1 { + return 0, fmt.Errorf("expected length of padding to be 1, received %d", len(p.Padding)) + } + b, err := proto.Marshal(p) + if err != nil { + return 0, err + } + return len(b) + len(keyPrefix), nil +} diff --git a/test/loadtime/payload/size_test.go b/test/loadtime/payload/payload_test.go similarity index 53% rename from test/loadtime/payload/size_test.go rename to test/loadtime/payload/payload_test.go index 46cd93750..aceb47717 100644 --- a/test/loadtime/payload/size_test.go +++ b/test/loadtime/payload/payload_test.go @@ -16,17 +16,32 @@ func TestSize(t *testing.T) { } func TestRoundTrip(t *testing.T) { + const ( + testConns = 512 + testRate = 4 + ) b, err := payload.NewBytes(payload.Options{ - Size: 1024, + Size: payloadSizeTarget, + Conns: testConns, + Rate: testRate, }) if err != nil { t.Fatalf("generating payload %s", err) } + if len(b) < payloadSizeTarget { + t.Fatalf("payload size %d less than expected %d", len(b), payloadSizeTarget) + } p, err := payload.FromBytes(b) if err != nil { t.Fatalf("reading payload %s", err) } - if p.Size != 1024 { - t.Fatalf("payload size value %d does not match expected %d", p.Size, 1024) + if p.Size != payloadSizeTarget { + t.Fatalf("payload size value %d does not match expected %d", p.Size, payloadSizeTarget) + } + if p.Connections != testConns { + t.Fatalf("payload size value %d does not match expected %d", p.Size, payloadSizeTarget) + } + if p.Size != payloadSizeTarget { + t.Fatalf("payload size value %d does not match expected %d", p.Size, payloadSizeTarget) } } diff --git a/test/loadtime/payload/size.go b/test/loadtime/payload/size.go deleted file mode 100644 index c2792cad6..000000000 --- a/test/loadtime/payload/size.go +++ /dev/null @@ -1,76 +0,0 @@ -package payload - -import ( - "bytes" - "crypto/rand" - "fmt" - "math" - - "google.golang.org/protobuf/proto" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" -) - -func init() { - p := &Payload{ - Time: timestamppb.Now(), - Connections: math.MaxUint64, - Rate: math.MaxUint64, - Size: math.MaxUint64, - Padding: make([]byte, 1), - } - b, err := proto.Marshal(p) - if err != nil { - panic(err) - } - payloadSizeBytes = len(b) -} - -const keyPrefix = "a=" - -var payloadSizeBytes int - -type Options struct { - Conns uint64 - Rate uint64 - Size uint64 -} - -func UnpaddedSizeBytes() int { - return payloadSizeBytes -} - -func NewBytes(o Options) ([]byte, error) { - if o.Size < uint64(UnpaddedSizeBytes()) { - return nil, fmt.Errorf("configured size %d not large enough to fit unpadded transaction size %d", o.Size, UnpaddedSizeBytes()) - } - p := &Payload{ - Time: timestamppb.Now(), - Connections: o.Conns, - Rate: o.Rate, - Size: o.Size, - Padding: make([]byte, o.Size-uint64(UnpaddedSizeBytes())), - } - _, err := rand.Read(p.Padding) - if err != nil { - return nil, err - } - b, err := proto.Marshal(p) - if err != nil { - return nil, err - } - - // prepend a single key so that the kv store only ever stores a single - // transaction instead of storing all tx and ballooning in size. - return append([]byte(keyPrefix), b...), nil - // return b, nil -} - -func FromBytes(b []byte) (*Payload, error) { - p := &Payload{} - tr := bytes.TrimPrefix(b, []byte(keyPrefix)) - err := proto.Unmarshal(tr, p) - if err != nil { - return nil, err - } - return p, nil -} diff --git a/test/loadtime/report/report.go b/test/loadtime/report/report.go index 3bc2949a4..85844c518 100644 --- a/test/loadtime/report/report.go +++ b/test/loadtime/report/report.go @@ -1,6 +1,7 @@ package report import ( + "fmt" "math" "sync" "time" @@ -9,20 +10,33 @@ import ( "github.com/tendermint/tendermint/types" ) -type blockStore interface { +// BlockStore defines the set of methods needed by the report generator from +// Tendermint's store.Blockstore type. Using an interface allows for tests to +// more easily simulate the required behavior without having to use the more +// complex real API. +type BlockStore interface { Height() int64 Base() int64 LoadBlock(int64) *types.Block } +// Report contains the data calculated from reading the timestamped transactions +// of each block found in the blockstore. type Report struct { - Max, Min, Avg time.Duration - StdDev int64 - All []time.Duration - ErrorCount int + Max, Min, Avg, StdDev time.Duration + + // ErrorCount is the number of parsing errors encountered while reading the + // transaction data. Parsing errors may occur if a transaction not generated + // by the payload package is submitted to the chain. + ErrorCount int + + // All contains all data points gathered from all valid transactions. + All []time.Duration } -func GenerateFromBlockStore(s blockStore) (Report, error) { +// GenerateFromBlockStore creates a Report using the data in the provided +// BlockStore. +func GenerateFromBlockStore(s BlockStore) (Report, error) { type payloadData struct { l time.Duration err error @@ -48,7 +62,7 @@ func GenerateFromBlockStore(s blockStore) (Report, error) { pdc <- payloadData{err: err} continue } - l := p.Time.AsTime().Sub(b.bt) + l := b.bt.Sub(p.Time.AsTime()) pdc <- payloadData{l: l} } }() @@ -89,5 +103,22 @@ func GenerateFromBlockStore(s blockStore) (Report, error) { sum += int64(pd.l) } r.Avg = time.Duration(sum / int64(len(r.All))) + r.StdDev = time.Duration(stddev(r.All)) return r, nil } + +func stddev(l []time.Duration) int64 { + var s1 int64 + for _, x := range l { + s1 += int64(x) + } + u := s1 / int64(len(l)) + var s2 int64 + for _, x := range l { + s2 += (int64(x) - u) * (int64(x) - u) + } + r := s2 / (int64(len(l))) + sqr := math.Sqrt(float64(r)) + fmt.Println(sqr) + return int64(sqr) +} diff --git a/test/loadtime/report/report_test.go b/test/loadtime/report/report_test.go index 0c6732d4e..8af9b0b34 100644 --- a/test/loadtime/report/report_test.go +++ b/test/loadtime/report/report_test.go @@ -7,6 +7,7 @@ import ( "github.com/tendermint/tendermint/test/loadtime/payload" "github.com/tendermint/tendermint/test/loadtime/report" "github.com/tendermint/tendermint/types" + "google.golang.org/protobuf/types/known/timestamppb" ) type mockBlockStore struct { @@ -27,13 +28,16 @@ func (m *mockBlockStore) LoadBlock(i int64) *types.Block { } func TestGenerateReport(t *testing.T) { - b1, err := payload.NewBytes(payload.Options{ + tn := time.Now() + b1, err := payload.NewBytes(&payload.Payload{ + Time: timestamppb.New(tn.Add(-6 * time.Second)), Size: 1024, }) if err != nil { t.Fatalf("generating payload %s", err) } - b2, err := payload.NewBytes(payload.Options{ + b2, err := payload.NewBytes(&payload.Payload{ + Time: timestamppb.New(tn.Add(-4 * time.Second)), Size: 1024, }) if err != nil { @@ -43,12 +47,17 @@ func TestGenerateReport(t *testing.T) { blocks: []*types.Block{ { Header: types.Header{ - Time: time.Now(), + Time: tn, }, Data: types.Data{ Txs: []types.Tx{b1, b2}, }, }, + { + Data: types.Data{ + Txs: []types.Tx{[]byte("error")}, + }, + }, }, } r, err := report.GenerateFromBlockStore(s) @@ -56,6 +65,23 @@ func TestGenerateReport(t *testing.T) { t.Fatalf("generating report %s", err) } if len(r.All) != 2 { - t.Fatalf("report contained different number of data points from expected. Expected %d but contained %d", 1, len(r.All)) + t.Fatalf("report contained different number of data points from expected. Expected %d but contained %d", 2, len(r.All)) + } + if r.ErrorCount != 1 { + t.Fatalf("ErrorCount did not match expected. Expected %d but contained %d", 1, r.ErrorCount) + } + if r.Avg != 5*time.Second { + t.Fatalf("Avg did not match expected. Expected %s but contained %s", 5*time.Second, r.Avg) + } + if r.Min != 4*time.Second { + t.Fatalf("Avg did not match expected. Expected %s but contained %s", 4*time.Second, r.Min) + } + if r.Max != 6*time.Second { + t.Fatalf("Avg did not match expected. Expected %s but contained %s", 6*time.Second, r.Max) + } + // Verified using online standard deviation calculator: + // https://www.calculator.net/standard-deviation-calculator.html?numberinputs=6%2C+4&ctype=p&x=84&y=27 + if r.StdDev != time.Second { + t.Fatalf("StdDev did not match expected. Expected %s but contained %s", time.Second, r.StdDev) } }