Files
tendermint/test/loadtime/report/report.go
mergify[bot] 014d0d6ca0 add separated runs by UUID (backport #9367) (#9380)
* 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>
2022-09-06 11:07:59 -04:00

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
}