/
benchmark.go
234 lines (202 loc) · 5.57 KB
/
benchmark.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
package main
import (
"context"
"encoding/json"
"fmt"
"math"
"path/filepath"
"time"
"github.com/hellarcore/tenderhellar/libs/log"
e2e "github.com/hellarcore/tenderhellar/test/e2e/pkg"
"github.com/hellarcore/tenderhellar/types"
)
// Benchmark is a simple function for fetching, calculating and printing
// the following metrics:
// 1. Average block production time
// 2. Block interval standard deviation
// 3. Max block interval (slowest block)
// 4. Min block interval (fastest block)
//
// Metrics are based of the `benchmarkLength`, the amount of consecutive blocks
// sampled from in the testnet
func Benchmark(ctx context.Context, logger log.Logger, testnet *e2e.Testnet, benchmarkLength int64) error {
block, err := getLatestBlock(ctx, testnet)
if err != nil {
return err
}
logger.Info("Beginning benchmark period...", "height", block.Height)
startAt := time.Now()
// wait for the length of the benchmark period in blocks to pass. We allow 5 seconds for each block
// which should be sufficient.
waitingTime := time.Duration(benchmarkLength*5) * time.Second
ctx, cancel := context.WithTimeout(ctx, waitingTime)
defer cancel()
block, _, err = waitForHeight(ctx, testnet, block.Height+benchmarkLength)
if err != nil {
return err
}
dur := time.Since(startAt)
logger.Info("Ending benchmark period", "height", block.Height)
// fetch a sample of blocks
blocks, err := fetchBlockChainSample(ctx, testnet, benchmarkLength)
if err != nil {
return err
}
// slice into time intervals and collate data
timeIntervals := splitIntoBlockIntervals(blocks)
testnetStats := extractTestnetStats(timeIntervals)
// populate data
testnetStats.populateTxns(blocks)
testnetStats.totalTime = dur
testnetStats.benchmarkLength = benchmarkLength
testnetStats.startHeight = blocks[0].Header.Height
testnetStats.endHeight = blocks[len(blocks)-1].Header.Height
// print and return
logger.Info(testnetStats.String())
logger.Info(testnetStats.getReportJSON(testnet))
return nil
}
func (t *testnetStats) populateTxns(blocks []*types.BlockMeta) {
t.numtxns = 0
for _, b := range blocks {
t.numtxns += int64(b.NumTxs)
}
}
type testnetStats struct {
startHeight int64
endHeight int64
benchmarkLength int64
numtxns int64
totalTime time.Duration
// average time to produce a block
mean time.Duration
// standard deviation of block production
std float64
// longest time to produce a block
max time.Duration
// shortest time to produce a block
min time.Duration
}
func (t *testnetStats) getReportJSON(net *e2e.Testnet) string {
jsn, err := json.Marshal(map[string]interface{}{
"case": filepath.Base(net.File),
"blocks": t.endHeight - t.startHeight,
"stddev": t.std,
"mean": t.mean.Seconds(),
"max": t.max.Seconds(),
"min": t.min.Seconds(),
"size": len(net.Nodes),
"txns": t.numtxns,
"dur": t.totalTime.Seconds(),
"length": t.benchmarkLength,
})
if err != nil {
return ""
}
return string(jsn)
}
func (t *testnetStats) String() string {
return fmt.Sprintf(`Benchmarked from height %v to %v
Mean Block Interval: %v
Standard Deviation: %f
Max Block Interval: %v
Min Block Interval: %v
`,
t.startHeight,
t.endHeight,
t.mean,
t.std,
t.max,
t.min,
)
}
// fetchBlockChainSample waits for `benchmarkLength` amount of blocks to pass, fetching
// all of the headers for these blocks from an archive node and returning it.
func fetchBlockChainSample(ctx context.Context, testnet *e2e.Testnet, benchmarkLength int64) ([]*types.BlockMeta, error) {
var blocks []*types.BlockMeta
// Find the first archive node
archiveNode := testnet.ArchiveNodes()[0]
c, err := archiveNode.Client()
if err != nil {
return nil, err
}
// find the latest height
s, err := c.Status(ctx)
if err != nil {
return nil, err
}
to := s.SyncInfo.LatestBlockHeight
from := to - benchmarkLength + 1
if from <= testnet.InitialHeight {
return nil, fmt.Errorf("tesnet was unable to reach required height for benchmarking (latest height %d)", to)
}
// Fetch blocks
for from < to {
// fetch the blockchain metas. Currently we can only fetch 20 at a time
resp, err := c.BlockchainInfo(ctx, from, min(from+19, to))
if err != nil {
return nil, err
}
blockMetas := resp.BlockMetas
// we receive blocks in descending order so we have to add them in reverse
for i := len(blockMetas) - 1; i >= 0; i-- {
if blockMetas[i].Header.Height != from {
return nil, fmt.Errorf("node gave us another header. Wanted %d, got %d",
from,
blockMetas[i].Header.Height,
)
}
from++
blocks = append(blocks, blockMetas[i])
}
}
return blocks, nil
}
func splitIntoBlockIntervals(blocks []*types.BlockMeta) []time.Duration {
intervals := make([]time.Duration, len(blocks)-1)
lastTime := blocks[0].Header.Time
for i, block := range blocks {
// skip the first block
if i == 0 {
continue
}
intervals[i-1] = block.Header.Time.Sub(lastTime)
lastTime = block.Header.Time
}
return intervals
}
func extractTestnetStats(intervals []time.Duration) testnetStats {
var (
sum, mean time.Duration
std float64
max = intervals[0]
min = intervals[0]
)
for _, interval := range intervals {
sum += interval
if interval > max {
max = interval
}
if interval < min {
min = interval
}
}
mean = sum / time.Duration(len(intervals))
for _, interval := range intervals {
diff := (interval - mean).Seconds()
std += math.Pow(diff, 2)
}
std = math.Sqrt(std / float64(len(intervals)))
return testnetStats{
mean: mean,
std: std,
max: max,
min: min,
}
}
func min(a, b int64) int64 {
if a > b {
return b
}
return a
}