mirror of
https://github.com/tendermint/tendermint.git
synced 2026-01-05 04:55:18 +00:00
* add separated runs by UUID (#9367)
This _should_ be the last piece needed for this tool.
This allows the tool to generate reports on multiple experimental runs that may have been performed against the same chain.
The `load` tool has been updated to generate a `UUID` on startup to uniquely identify each experimental run. The `report` tool separates all of the results it reads by `UUID` and performs separate calculations for each discovered experiment.
Sample output is as follows
```
Experiment ID: 6bd7d1e8-d82c-4dbe-a1b3-40ab99e4fa30
Connections: 1
Rate: 1000
Size: 1024
Total Valid Tx: 9000
Total Negative Latencies: 0
Minimum Latency: 86.632837ms
Maximum Latency: 1.151089602s
Average Latency: 813.759361ms
Standard Deviation: 225.189977ms
Experiment ID: 453960af-6295-4282-aed6-367fc17c0de0
Connections: 1
Rate: 1000
Size: 1024
Total Valid Tx: 9000
Total Negative Latencies: 0
Minimum Latency: 79.312992ms
Maximum Latency: 1.162446243s
Average Latency: 422.755139ms
Standard Deviation: 241.832475ms
Total Invalid Tx: 0
```
closes: #9352
#### PR checklist
- [ ] Tests written/updated, or no tests needed
- [ ] `CHANGELOG_PENDING.md` updated, or no changelog entry needed
- [ ] Updated relevant documentation (`docs/`) and code comments, or no
documentation updates needed
(cherry picked from commit 1067ba1571)
# Conflicts:
# go.mod
* fix merge conflict
* fix lint
Co-authored-by: William Banfield <4561443+williambanfield@users.noreply.github.com>
Co-authored-by: William Banfield <wbanfield@gmail.com>
210 lines
5.4 KiB
Go
210 lines
5.4 KiB
Go
package report
|
|
|
|
import (
|
|
"math"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gofrs/uuid"
|
|
"github.com/tendermint/tendermint/test/loadtime/payload"
|
|
"github.com/tendermint/tendermint/types"
|
|
"gonum.org/v1/gonum/stat"
|
|
)
|
|
|
|
// 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 {
|
|
ID uuid.UUID
|
|
Rate, Connections, Size uint64
|
|
Max, Min, Avg, StdDev time.Duration
|
|
|
|
// NegativeCount is the number of negative durations encountered while
|
|
// reading the transaction data. A negative duration means that
|
|
// a transaction timestamp was greater than the timestamp of the block it
|
|
// was included in and likely indicates an issue with the experimental
|
|
// setup.
|
|
NegativeCount int
|
|
|
|
// All contains all data points gathered from all valid transactions.
|
|
// The order of the contents of All is not guaranteed to be match the order of transactions
|
|
// in the chain.
|
|
All []time.Duration
|
|
|
|
// used for calculating average during report creation.
|
|
sum int64
|
|
}
|
|
|
|
type Reports struct {
|
|
s map[uuid.UUID]Report
|
|
l []Report
|
|
|
|
// 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
|
|
}
|
|
|
|
func (rs *Reports) List() []Report {
|
|
return rs.l
|
|
}
|
|
|
|
func (rs *Reports) ErrorCount() int {
|
|
return rs.errorCount
|
|
}
|
|
|
|
func (rs *Reports) addDataPoint(id uuid.UUID, l time.Duration, conns, rate, size uint64) {
|
|
r, ok := rs.s[id]
|
|
if !ok {
|
|
r = Report{
|
|
Max: 0,
|
|
Min: math.MaxInt64,
|
|
ID: id,
|
|
Connections: conns,
|
|
Rate: rate,
|
|
Size: size,
|
|
}
|
|
rs.s[id] = r
|
|
}
|
|
r.All = append(r.All, l)
|
|
if l > r.Max {
|
|
r.Max = l
|
|
}
|
|
if l < r.Min {
|
|
r.Min = l
|
|
}
|
|
if int64(l) < 0 {
|
|
r.NegativeCount++
|
|
}
|
|
// Using an int64 here makes an assumption about the scale and quantity of the data we are processing.
|
|
// If all latencies were 2 seconds, we would need around 4 billion records to overflow this.
|
|
// We are therefore assuming that the data does not exceed these bounds.
|
|
r.sum += int64(l)
|
|
rs.s[id] = r
|
|
}
|
|
|
|
func (rs *Reports) calculateAll() {
|
|
rs.l = make([]Report, 0, len(rs.s))
|
|
for _, r := range rs.s {
|
|
if len(r.All) == 0 {
|
|
r.Min = 0
|
|
rs.l = append(rs.l, r)
|
|
continue
|
|
}
|
|
r.Avg = time.Duration(r.sum / int64(len(r.All)))
|
|
r.StdDev = time.Duration(int64(stat.StdDev(toFloat(r.All), nil)))
|
|
rs.l = append(rs.l, r)
|
|
}
|
|
}
|
|
|
|
func (rs *Reports) addError() {
|
|
rs.errorCount++
|
|
}
|
|
|
|
// GenerateFromBlockStore creates a Report using the data in the provided
|
|
// BlockStore.
|
|
func GenerateFromBlockStore(s BlockStore) (*Reports, error) {
|
|
type payloadData struct {
|
|
id uuid.UUID
|
|
l time.Duration
|
|
connections, rate, size uint64
|
|
err error
|
|
}
|
|
type txData struct {
|
|
tx []byte
|
|
bt time.Time
|
|
}
|
|
reports := &Reports{
|
|
s: make(map[uuid.UUID]Report),
|
|
}
|
|
|
|
// Deserializing to proto can be slow but does not depend on other data
|
|
// and can therefore be done in parallel.
|
|
// Deserializing in parallel does mean that the resulting data is
|
|
// not guaranteed to be delivered in the same order it was given to the
|
|
// worker pool.
|
|
const poolSize = 16
|
|
|
|
txc := make(chan txData)
|
|
pdc := make(chan payloadData, poolSize)
|
|
|
|
wg := &sync.WaitGroup{}
|
|
wg.Add(poolSize)
|
|
for i := 0; i < poolSize; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
for b := range txc {
|
|
p, err := payload.FromBytes(b.tx)
|
|
if err != nil {
|
|
pdc <- payloadData{err: err}
|
|
continue
|
|
}
|
|
|
|
l := b.bt.Sub(p.Time.AsTime())
|
|
b := (*[16]byte)(p.Id)
|
|
pdc <- payloadData{
|
|
l: l,
|
|
id: uuid.UUID(*b),
|
|
connections: p.Connections,
|
|
rate: p.Rate,
|
|
size: p.Size,
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
go func() {
|
|
wg.Wait()
|
|
close(pdc)
|
|
}()
|
|
|
|
go func() {
|
|
base, height := s.Base(), s.Height()
|
|
prev := s.LoadBlock(base)
|
|
for i := base + 1; i < height; i++ {
|
|
// Data from two adjacent block are used here simultaneously,
|
|
// blocks of height H and H+1. The transactions of the block of
|
|
// height H are used with the timestamp from the block of height
|
|
// H+1. This is done because the timestamp from H+1 is calculated
|
|
// by using the precommits submitted at height H. The timestamp in
|
|
// block H+1 represents the time at which block H was committed.
|
|
//
|
|
// In the (very unlikely) event that the very last block of the
|
|
// chain contains payload transactions, those transactions will not
|
|
// be used in the latency calculations because the last block whose
|
|
// transactions are used is the block one before the last.
|
|
cur := s.LoadBlock(i)
|
|
for _, tx := range prev.Data.Txs {
|
|
txc <- txData{tx: tx, bt: cur.Time}
|
|
}
|
|
prev = cur
|
|
}
|
|
close(txc)
|
|
}()
|
|
for pd := range pdc {
|
|
if pd.err != nil {
|
|
reports.addError()
|
|
continue
|
|
}
|
|
reports.addDataPoint(pd.id, pd.l, pd.connections, pd.rate, pd.size)
|
|
}
|
|
reports.calculateAll()
|
|
return reports, nil
|
|
}
|
|
|
|
func toFloat(in []time.Duration) []float64 {
|
|
r := make([]float64, len(in))
|
|
for i, v := range in {
|
|
r[i] = float64(int64(v))
|
|
}
|
|
return r
|
|
}
|