Skip to content

Commit

Permalink
Fix loadbot gas metric collection (#497)
Browse files Browse the repository at this point in the history
* Rework how gas metrics are gathered

* Resolve receiving address issue

* Fix typo

* Change up the ordering in var init

* Simplify contract metric init logic

* Simplify the metric fetching code blocks
  • Loading branch information
zivkovicmilos authored Apr 20, 2022
1 parent 2555afd commit aa8feb0
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 76 deletions.
18 changes: 12 additions & 6 deletions command/loadbot/deploy_contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,18 @@ func (l *Loadbot) deployContract(
}

end := time.Now()
// initialize gas metrics map with block nuber as index
l.metrics.ContractMetrics.ContractGasMetrics.Blocks[receipt.BlockNumber] = GasMetrics{}

// initialize gas metrics map with block number as index
l.metrics.ContractMetrics.ContractGasMetrics, err = getBlockGasMetrics(
jsonClient,
map[uint64]struct{}{
receipt.BlockNumber: {},
},
)
if err != nil {
return fmt.Errorf("unable to fetch gas metrics, %w", err)
}

// fetch contract address
l.metrics.ContractMetrics.ContractAddress = receipt.ContractAddress
// set contract address in order to get new example txn and gas estimate
Expand All @@ -88,10 +98,6 @@ func (l *Loadbot) deployContract(
blockNumber: receipt.BlockNumber,
},
)
// calculate contract deployment metrics
if err := l.calculateGasMetrics(jsonClient, l.metrics.ContractMetrics.ContractGasMetrics); err != nil {
return fmt.Errorf("unable to calculate contract block gas metrics: %w", err)
}

l.metrics.ContractMetrics.ContractDeploymentDuration.calcTurnAroundMetrics()
l.metrics.ContractMetrics.ContractDeploymentDuration.TotalExecTime = end.Sub(start)
Expand Down
78 changes: 60 additions & 18 deletions command/loadbot/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,17 @@ type GasMetrics struct {
}

type BlockGasMetrics struct {
Blocks map[uint64]GasMetrics
BlockGasMutex *sync.Mutex
sync.Mutex

Blocks map[uint64]GasMetrics
}

// AddBlockMetric adds a block gas metric for the specified block number [Thread safe]
func (b *BlockGasMetrics) AddBlockMetric(blockNum uint64, gasMetric GasMetrics) {
b.Lock()
defer b.Unlock()

b.Blocks[blockNum] = gasMetric
}

type ContractMetricsData struct {
Expand All @@ -91,7 +100,7 @@ type Metrics struct {
TotalTransactionsSentCount uint64
FailedTransactionsCount uint64
TransactionDuration ExecDuration
ContractMetrics ContractMetricsData
ContractMetrics *ContractMetricsData
GasMetrics *BlockGasMetrics
}

Expand All @@ -102,29 +111,47 @@ type Loadbot struct {
}

func NewLoadbot(cfg *Configuration) *Loadbot {
return &Loadbot{
loadbot := &Loadbot{
cfg: cfg,
metrics: &Metrics{
TotalTransactionsSentCount: 0,
FailedTransactionsCount: 0,
TransactionDuration: ExecDuration{
blockTransactions: make(map[uint64]uint64),
},
ContractMetrics: ContractMetricsData{
ContractDeploymentDuration: ExecDuration{
blockTransactions: make(map[uint64]uint64),
},
ContractGasMetrics: &BlockGasMetrics{
Blocks: make(map[uint64]GasMetrics),
BlockGasMutex: &sync.Mutex{},
},
},
GasMetrics: &BlockGasMetrics{
Blocks: make(map[uint64]GasMetrics),
BlockGasMutex: &sync.Mutex{},
Blocks: make(map[uint64]GasMetrics),
},
},
}

// Attempt to initialize contract metrics if needed
loadbot.initContractMetricsIfNeeded()

return loadbot
}

// initContractMetrics initializes contract metrics for
// the loadbot instance
func (l *Loadbot) initContractMetricsIfNeeded() {
if !l.needsContractMetrics() {
return
}

l.metrics.ContractMetrics = &ContractMetricsData{
ContractDeploymentDuration: ExecDuration{
blockTransactions: make(map[uint64]uint64),
},
ContractGasMetrics: &BlockGasMetrics{
Blocks: make(map[uint64]GasMetrics),
},
}
}

func (l *Loadbot) needsContractMetrics() bool {
return l.cfg.GeneratorMode == deploy ||
l.cfg.GeneratorMode == erc20 ||
l.cfg.GeneratorMode == erc721
}

func (l *Loadbot) GetMetrics() *Metrics {
Expand Down Expand Up @@ -235,7 +262,18 @@ func (l *Loadbot) Run() error {
}
}

var wg sync.WaitGroup
var (
seenBlockNums = make(map[uint64]struct{})
seenBlockNumsLock sync.Mutex
wg sync.WaitGroup
)

markSeenBlock := func(blockNum uint64) {
seenBlockNumsLock.Lock()
defer seenBlockNumsLock.Unlock()

seenBlockNums[blockNum] = struct{}{}
}

for i := uint64(0); i < l.cfg.Count; i++ {
<-ticker.C
Expand Down Expand Up @@ -284,7 +322,9 @@ func (l *Loadbot) Run() error {
return
}

l.initGasMetricsBlocksMap(receipt.BlockNumber)
// Mark the block as seen so data on it
// is gathered later
markSeenBlock(receipt.BlockNumber)

// Stop the performance timer
end := time.Now()
Expand All @@ -303,7 +343,9 @@ func (l *Loadbot) Run() error {

endTime := time.Now()

if err := l.calculateGasMetrics(jsonClient, l.metrics.GasMetrics); err != nil {
// Fetch the block gas metrics for seen blocks
l.metrics.GasMetrics, err = getBlockGasMetrics(jsonClient, seenBlockNums)
if err != nil {
return fmt.Errorf("unable to calculate block gas metrics: %w", err)
}

Expand Down
140 changes: 90 additions & 50 deletions command/loadbot/helper.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package loadbot

import (
"context"
"fmt"
"math/big"
"sync"
"time"

"golang.org/x/sync/errgroup"

"github.com/0xPolygon/polygon-edge/types"
"github.com/umbracle/go-web3"
"github.com/umbracle/go-web3/jsonrpc"
Expand Down Expand Up @@ -57,12 +55,12 @@ func estimateGas(client *jsonrpc.Client, txn *types.Transaction) (uint64, error)
return gasEstimate, nil
}

// calculate block utilization in percents
func calculateBlockUtilization(blockInfo GasMetrics) float64 {
return float64(blockInfo.GasUsed) / float64(blockInfo.GasLimit) * 100
// calculateBlockUtilization calculates block utilization in percents
func calculateBlockUtilization(gasUsed, gasLimit uint64) float64 {
return float64(gasUsed) / float64(gasLimit) * 100
}

// calculate average block utilization across all blocks
// calculateAvgBlockUtil calculates average block utilization across all blocks
func calculateAvgBlockUtil(gasData map[uint64]GasMetrics) float64 {
sum := float64(0)
for _, i := range gasData {
Expand All @@ -72,58 +70,107 @@ func calculateAvgBlockUtil(gasData map[uint64]GasMetrics) float64 {
return sum / float64(len(gasData))
}

// fetch block gas usage and gas limit and calculate block utilization
func (l *Loadbot) calculateGasMetrics(jsonClient *jsonrpc.Client, gasMetrics *BlockGasMetrics) error {
errGr, _ := errgroup.WithContext(context.Background())
// getBlockGasMetrics fetches block gas metrics from the JSON-RPC client
// for the specified blocks
func getBlockGasMetrics(
jsonClient *jsonrpc.Client,
blockNums map[uint64]struct{},
) (*BlockGasMetrics, error) {
var (
errors = make([]error, 0)
errorsLock sync.Mutex
blockGasMetrics = &BlockGasMetrics{
Blocks: make(map[uint64]GasMetrics),
}
wg sync.WaitGroup
)

// addError is a helper for accumulating errors
// in go routines
addError := func(fetchErr error) {
errorsLock.Lock()
defer errorsLock.Unlock()

errors = append(errors, fetchErr)
}

queryBlockInfo := func(
blockNum uint64,
) {
// Query node for block
blockInfo, err := jsonClient.Eth().GetBlockByNumber(
web3.BlockNumber(blockNum),
false,
)
if err != nil {
addError(
fmt.Errorf("could not fetch block %d by number, %w", blockNum, err),
)

return
}

for num, data := range gasMetrics.Blocks {
blockNum := num
blockData := data
// Update the block gas metrics
blockGasMetrics.AddBlockMetric(
blockNum,
GasMetrics{
GasUsed: blockInfo.GasUsed,
GasLimit: blockInfo.GasLimit,
Utilization: calculateBlockUtilization(blockInfo.GasUsed, blockInfo.GasLimit),
},
)
}

errGr.Go(func() error {
blockInfom, err := jsonClient.Eth().GetBlockByNumber(web3.BlockNumber(blockNum), false)
if err != nil {
return fmt.Errorf("could not fetch block %d by number, %w", blockNum, err)
}
// For each block number, fetch the corresponding
// block info data
for blockNum := range blockNums {
wg.Add(1)

blockData.GasLimit = blockInfom.GasLimit
blockData.GasUsed = blockInfom.GasUsed
blockData.Utilization = calculateBlockUtilization(blockData)
gasMetrics.Blocks[blockNum] = blockData
go func(blockNum uint64) {
defer wg.Done()

return nil
})
queryBlockInfo(blockNum)
}(blockNum)
}

if err := errGr.Wait(); err != nil {
return err
wg.Wait()

if len(errors) > 1 {
return nil, fmt.Errorf(
"unable to successfully fetch gas metrics, %v",
errors,
)
}

return nil
return blockGasMetrics, nil
}

// updateGasEstimate updates the loadbot generator gas estimate
func (l *Loadbot) updateGasEstimate(jsonClient *jsonrpc.Client) error {
//nolint: ifshort
gasLimit := l.cfg.GasLimit

if gasLimit == nil {
// Get the gas estimate
exampleTxn, err := l.generator.GetExampleTransaction()
if err != nil {
return fmt.Errorf("unable to get example transaction, %w", err)
}

// No gas limit specified, query the network for an estimation
gasEstimate, estimateErr := estimateGas(jsonClient, exampleTxn)
if estimateErr != nil {
return fmt.Errorf("unable to get gas estimate, %w", err)
}
if gasLimit != nil {
// User specified a gas limit to use,
// no need to calculate one
return nil
}

gasLimit = new(big.Int).SetUint64(gasEstimate)
// User didn't specify a gas limit to use, calculate it
exampleTxn, err := l.generator.GetExampleTransaction()
if err != nil {
return fmt.Errorf("unable to get example transaction, %w", err)
}

l.generator.SetGasEstimate(gasLimit.Uint64())
// No gas limit specified, query the network for an estimation
gasEstimate, estimateErr := estimateGas(jsonClient, exampleTxn)
if estimateErr != nil {
return fmt.Errorf("unable to get gas estimate, %w", err)
}

gasLimit = new(big.Int).SetUint64(gasEstimate)

l.generator.SetGasEstimate(gasLimit.Uint64())

return nil
}

Expand All @@ -143,7 +190,7 @@ func calcMaxTimeout(count, tps uint64) time.Duration {
return waitTime + waitFactor
}

// returns true if this is erc20 or erc721 mode
// isTokenTransferMode checks if the mode is erc20 or erc721
func (l *Loadbot) isTokenTransferMode() bool {
switch l.cfg.GeneratorMode {
case erc20, erc721:
Expand All @@ -152,10 +199,3 @@ func (l *Loadbot) isTokenTransferMode() bool {
return false
}
}

//initialze gas metrics blocks map with block number as key
func (l *Loadbot) initGasMetricsBlocksMap(blockNum uint64) {
l.metrics.GasMetrics.BlockGasMutex.Lock()
l.metrics.GasMetrics.Blocks[blockNum] = GasMetrics{}
l.metrics.GasMetrics.BlockGasMutex.Unlock()
}
14 changes: 13 additions & 1 deletion command/loadbot/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,25 @@ func (p *loadbotParams) initAddressValues() error {
return fmt.Errorf("failed to decode sender address: %w", err)
}

if err := p.receiver.UnmarshalText([]byte(p.receiverRaw)); err != nil {
if err := p.initReceiverAddress(); err != nil {
return fmt.Errorf("failed to decode receiver address: %w", err)
}

return nil
}

func (p *loadbotParams) initReceiverAddress() error {
if p.receiverRaw == "" {
// No receiving address specified,
// use the sender address as the receiving address
p.receiver = p.sender

return nil
}

return p.receiver.UnmarshalText([]byte(p.receiverRaw))
}

func (p *loadbotParams) initTxnValue() error {
value, err := types.ParseUint256orHex(&p.valueRaw)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion command/loadbot/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ func (lr *LoadbotResult) writeLoadbotResults(buffer *bytes.Buffer) {
func (lr *LoadbotResult) writeAverageBlockUtilization(buffer *bytes.Buffer) {
buffer.WriteString("\n\n[AVERAGE BLOCK UTILIZATION]\n")
buffer.WriteString(helper.FormatKV([]string{
fmt.Sprintf("Average utilization acorss all blocks|%.2f%%", calculateAvgBlockUtil(lr.BlockData.GasData)),
fmt.Sprintf("Average utilization across all blocks|%.2f%%", calculateAvgBlockUtil(lr.BlockData.GasData)),
}))
}

Expand Down

0 comments on commit aa8feb0

Please sign in to comment.