github.com/badrootd/nibiru-cometbft@v0.37.5-0.20240307173500-2a75559eee9b/test/loadtime/report/report.go (about)

     1  package report
     2  
     3  import (
     4  	"math"
     5  	"sync"
     6  	"time"
     7  
     8  	"github.com/badrootd/nibiru-cometbft/test/loadtime/payload"
     9  	"github.com/badrootd/nibiru-cometbft/types"
    10  	"github.com/gofrs/uuid"
    11  	"gonum.org/v1/gonum/stat"
    12  )
    13  
    14  // BlockStore defines the set of methods needed by the report generator from
    15  // CometBFT's store.Blockstore type. Using an interface allows for tests to
    16  // more easily simulate the required behavior without having to use the more
    17  // complex real API.
    18  type BlockStore interface {
    19  	Height() int64
    20  	Base() int64
    21  	LoadBlock(int64) *types.Block
    22  }
    23  
    24  // DataPoint contains the set of data collected for each transaction.
    25  type DataPoint struct {
    26  	Duration  time.Duration
    27  	BlockTime time.Time
    28  	Hash      []byte
    29  }
    30  
    31  // Report contains the data calculated from reading the timestamped transactions
    32  // of each block found in the blockstore.
    33  type Report struct {
    34  	ID                      uuid.UUID
    35  	Rate, Connections, Size uint64
    36  	Max, Min, Avg, StdDev   time.Duration
    37  
    38  	// NegativeCount is the number of negative durations encountered while
    39  	// reading the transaction data. A negative duration means that
    40  	// a transaction timestamp was greater than the timestamp of the block it
    41  	// was included in and likely indicates an issue with the experimental
    42  	// setup.
    43  	NegativeCount int
    44  
    45  	// All contains all data points gathered from all valid transactions.
    46  	// The order of the contents of All is not guaranteed to be match the order of transactions
    47  	// in the chain.
    48  	All []DataPoint
    49  
    50  	// used for calculating average during report creation.
    51  	sum int64
    52  }
    53  
    54  type Reports struct {
    55  	s map[uuid.UUID]Report
    56  	l []Report
    57  
    58  	// errorCount is the number of parsing errors encountered while reading the
    59  	// transaction data. Parsing errors may occur if a transaction not generated
    60  	// by the payload package is submitted to the chain.
    61  	errorCount int
    62  }
    63  
    64  func (rs *Reports) List() []Report {
    65  	return rs.l
    66  }
    67  
    68  func (rs *Reports) ErrorCount() int {
    69  	return rs.errorCount
    70  }
    71  
    72  func (rs *Reports) addDataPoint(id uuid.UUID, l time.Duration, bt time.Time, hash []byte, conns, rate, size uint64) {
    73  	r, ok := rs.s[id]
    74  	if !ok {
    75  		r = Report{
    76  			Max:         0,
    77  			Min:         math.MaxInt64,
    78  			ID:          id,
    79  			Connections: conns,
    80  			Rate:        rate,
    81  			Size:        size,
    82  		}
    83  		rs.s[id] = r
    84  	}
    85  	r.All = append(r.All, DataPoint{Duration: l, BlockTime: bt, Hash: hash})
    86  	if l > r.Max {
    87  		r.Max = l
    88  	}
    89  	if l < r.Min {
    90  		r.Min = l
    91  	}
    92  	if int64(l) < 0 {
    93  		r.NegativeCount++
    94  	}
    95  	// Using an int64 here makes an assumption about the scale and quantity of the data we are processing.
    96  	// If all latencies were 2 seconds, we would need around 4 billion records to overflow this.
    97  	// We are therefore assuming that the data does not exceed these bounds.
    98  	r.sum += int64(l)
    99  	rs.s[id] = r
   100  }
   101  
   102  func (rs *Reports) calculateAll() {
   103  	rs.l = make([]Report, 0, len(rs.s))
   104  	for _, r := range rs.s {
   105  		if len(r.All) == 0 {
   106  			r.Min = 0
   107  			rs.l = append(rs.l, r)
   108  			continue
   109  		}
   110  		r.Avg = time.Duration(r.sum / int64(len(r.All)))
   111  		r.StdDev = time.Duration(int64(stat.StdDev(toFloat(r.All), nil)))
   112  		rs.l = append(rs.l, r)
   113  	}
   114  }
   115  
   116  func (rs *Reports) addError() {
   117  	rs.errorCount++
   118  }
   119  
   120  // GenerateFromBlockStore creates a Report using the data in the provided
   121  // BlockStore.
   122  func GenerateFromBlockStore(s BlockStore) (*Reports, error) {
   123  	type payloadData struct {
   124  		id                      uuid.UUID
   125  		l                       time.Duration
   126  		bt                      time.Time
   127  		hash                    []byte
   128  		connections, rate, size uint64
   129  		err                     error
   130  	}
   131  	type txData struct {
   132  		tx types.Tx
   133  		bt time.Time
   134  	}
   135  	reports := &Reports{
   136  		s: make(map[uuid.UUID]Report),
   137  	}
   138  
   139  	// Deserializing to proto can be slow but does not depend on other data
   140  	// and can therefore be done in parallel.
   141  	// Deserializing in parallel does mean that the resulting data is
   142  	// not guaranteed to be delivered in the same order it was given to the
   143  	// worker pool.
   144  	const poolSize = 16
   145  
   146  	txc := make(chan txData)
   147  	pdc := make(chan payloadData, poolSize)
   148  
   149  	wg := &sync.WaitGroup{}
   150  	wg.Add(poolSize)
   151  	for i := 0; i < poolSize; i++ {
   152  		go func() {
   153  			defer wg.Done()
   154  			for b := range txc {
   155  				p, err := payload.FromBytes(b.tx)
   156  				if err != nil {
   157  					pdc <- payloadData{err: err}
   158  					continue
   159  				}
   160  
   161  				l := b.bt.Sub(p.Time.AsTime())
   162  				idb := (*[16]byte)(p.Id)
   163  				pdc <- payloadData{
   164  					l:           l,
   165  					bt:          b.bt,
   166  					hash:        b.tx.Hash(),
   167  					id:          uuid.UUID(*idb),
   168  					connections: p.Connections,
   169  					rate:        p.Rate,
   170  					size:        p.Size,
   171  				}
   172  			}
   173  		}()
   174  	}
   175  	go func() {
   176  		wg.Wait()
   177  		close(pdc)
   178  	}()
   179  
   180  	go func() {
   181  		base, height := s.Base(), s.Height()
   182  		prev := s.LoadBlock(base)
   183  		for i := base + 1; i < height; i++ {
   184  			// Data from two adjacent block are used here simultaneously,
   185  			// blocks of height H and H+1. The transactions of the block of
   186  			// height H are used with the timestamp from the block of height
   187  			// H+1. This is done because the timestamp from H+1 is calculated
   188  			// by using the precommits submitted at height H. The timestamp in
   189  			// block H+1 represents the time at which block H was committed.
   190  			//
   191  			// In the (very unlikely) event that the very last block of the
   192  			// chain contains payload transactions, those transactions will not
   193  			// be used in the latency calculations because the last block whose
   194  			// transactions are used is the block one before the last.
   195  			cur := s.LoadBlock(i)
   196  			for _, tx := range prev.Data.Txs {
   197  				txc <- txData{tx: tx, bt: cur.Time}
   198  			}
   199  			prev = cur
   200  		}
   201  		close(txc)
   202  	}()
   203  	for pd := range pdc {
   204  		if pd.err != nil {
   205  			reports.addError()
   206  			continue
   207  		}
   208  		reports.addDataPoint(pd.id, pd.l, pd.bt, pd.hash, pd.connections, pd.rate, pd.size)
   209  	}
   210  	reports.calculateAll()
   211  	return reports, nil
   212  }
   213  
   214  func toFloat(in []DataPoint) []float64 {
   215  	r := make([]float64, len(in))
   216  	for i, v := range in {
   217  		r[i] = float64(int64(v.Duration))
   218  	}
   219  	return r
   220  }