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 }