Browse Source

e2e: add benchmarking functionality (#6210)

pull/6215/head
Callum Waters 4 years ago
committed by GitHub
parent
commit
0566bea8b2
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 260 additions and 1 deletions
  1. +10
    -0
      test/e2e/README.md
  2. +192
    -0
      test/e2e/runner/benchmark.go
  3. +57
    -0
      test/e2e/runner/main.go
  4. +1
    -1
      test/e2e/runner/wait.go

+ 10
- 0
test/e2e/README.md View File

@ -70,6 +70,8 @@ The test runner has the following stages, which can also be executed explicitly
* `cleanup`: removes configuration files and Docker containers/networks.
Auxiliary commands:
* `logs`: outputs all node logs.
* `tail`: tails (follows) node logs until canceled.
@ -139,3 +141,11 @@ Docker does not enable IPv6 by default. To do so, enter the following in
"fixed-cidr-v6": "2001:db8:1::/64"
}
```
## Benchmarking testnets
It is also possible to run a simple benchmark on a testnet. This is done through the `benchmark` command. This manages the entire process: setting up the environment, starting the test net, waiting for a considerable amount of blocks to be used (currently 100), and then returning the following metrics from the sample of the blockchain:
- Average time to produce a block
- Standard deviation of producing a block
- Minimum and maximum time to produce a block

+ 192
- 0
test/e2e/runner/benchmark.go View File

@ -0,0 +1,192 @@
package main
import (
"context"
"fmt"
"math"
"time"
e2e "github.com/tendermint/tendermint/test/e2e/pkg"
"github.com/tendermint/tendermint/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(testnet *e2e.Testnet, benchmarkLength int64) error {
block, _, err := waitForHeight(testnet, 0)
if err != nil {
return err
}
logger.Info("Beginning benchmark period...", "height", block.Height)
// 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
endHeight, err := waitForAllNodes(testnet, block.Height+benchmarkLength, waitingTime)
if err != nil {
return err
}
logger.Info("Ending benchmark period", "height", endHeight)
// fetch a sample of blocks
blocks, err := fetchBlockChainSample(testnet, benchmarkLength)
if err != nil {
return err
}
// slice into time intervals and collate data
timeIntervals := splitIntoBlockIntervals(blocks)
testnetStats := extractTestnetStats(timeIntervals)
testnetStats.startHeight = blocks[0].Header.Height
testnetStats.endHeight = blocks[len(blocks)-1].Header.Height
// print and return
logger.Info(testnetStats.String())
return nil
}
type testnetStats struct {
startHeight int64
endHeight int64
// 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) 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(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
ctx := context.Background()
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
}

+ 57
- 0
test/e2e/runner/main.go View File

@ -218,6 +218,63 @@ func NewCLI() *CLI {
},
})
cli.root.AddCommand(&cobra.Command{
Use: "benchmark",
Short: "Benchmarks testnet",
Long: `Benchmarks the following metrics:
Mean Block Interval
Standard Deviation
Min Block Interval
Max Block Interval
over a 100 block sampling period.
Does not run any perbutations.
`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := Cleanup(cli.testnet); err != nil {
return err
}
if err := Setup(cli.testnet); err != nil {
return err
}
chLoadResult := make(chan error)
ctx, loadCancel := context.WithCancel(context.Background())
defer loadCancel()
go func() {
err := Load(ctx, cli.testnet)
if err != nil {
logger.Error(fmt.Sprintf("Transaction load failed: %v", err.Error()))
}
chLoadResult <- err
}()
if err := Start(cli.testnet); err != nil {
return err
}
if err := Wait(cli.testnet, 5); err != nil { // allow some txs to go through
return err
}
// we benchmark performance over the next 100 blocks
if err := Benchmark(cli.testnet, 100); err != nil {
return err
}
loadCancel()
if err := <-chLoadResult; err != nil {
return err
}
if err := Cleanup(cli.testnet); err != nil {
return err
}
return nil
},
})
return cli
}


+ 1
- 1
test/e2e/runner/wait.go View File

@ -30,5 +30,5 @@ func WaitUntil(testnet *e2e.Testnet, height int64) error {
// waitingTime estimates how long it should take for a node to reach the height.
// More nodes in a network implies we may expect a slower network and may have to wait longer.
func waitingTime(nodes int) time.Duration {
return time.Duration(20+(nodes*2)) * time.Second
return time.Duration(20+(nodes*4)) * time.Second
}

Loading…
Cancel
Save