github.com/onflow/flow-go@v0.33.17/fvm/evm/emulator/state/state_growth_test.go (about)

     1  package state_test
     2  
     3  import (
     4  	"encoding/binary"
     5  	"fmt"
     6  	"math/big"
     7  	"os"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/onflow/flow-go/utils/io"
    12  
    13  	"github.com/ethereum/go-ethereum/common"
    14  	"github.com/stretchr/testify/require"
    15  
    16  	"github.com/onflow/flow-go/fvm/evm/emulator/state"
    17  	"github.com/onflow/flow-go/fvm/evm/testutils"
    18  	"github.com/onflow/flow-go/fvm/evm/types"
    19  	"github.com/onflow/flow-go/model/flow"
    20  )
    21  
    22  const (
    23  	storageBytesMetric = "storage_size_bytes"
    24  	storageItemsMetric = "storage_items"
    25  	bytesReadMetric    = "bytes_read"
    26  	bytesWrittenMetric = "bytes_written"
    27  )
    28  
    29  // storage test is designed to evaluate the impact of state modifications on storage size.
    30  // It measures the bytes used in the underlying storage, aiming to understand how storage size scales with changes in state.
    31  // While the specific operation details are not crucial for this benchmark, the primary goal is to analyze how the storage
    32  // size evolves in response to state modifications.
    33  
    34  type storageTest struct {
    35  	store        *testutils.TestValueStore
    36  	addressIndex uint64
    37  	metrics      *metrics
    38  }
    39  
    40  func newStorageTest() (*storageTest, error) {
    41  	simpleStore := testutils.GetSimpleValueStore()
    42  
    43  	return &storageTest{
    44  		store:        simpleStore,
    45  		addressIndex: 100,
    46  		metrics:      newMetrics(),
    47  	}, nil
    48  }
    49  
    50  func (s *storageTest) newAddress() common.Address {
    51  	s.addressIndex++
    52  	var addr common.Address
    53  	binary.BigEndian.PutUint64(addr[12:], s.addressIndex)
    54  	return addr
    55  }
    56  
    57  // run the provided runner with a newly created state which gets comitted after the runner
    58  // is finished. Storage metrics are being recorded with each run.
    59  func (s *storageTest) run(runner func(state types.StateDB)) error {
    60  	state, err := state.NewStateDB(s.store, flow.Address{0x01})
    61  	if err != nil {
    62  		return err
    63  	}
    64  
    65  	runner(state)
    66  
    67  	err = state.Commit()
    68  	if err != nil {
    69  		return err
    70  	}
    71  
    72  	s.metrics.add(bytesWrittenMetric, s.store.TotalBytesWritten())
    73  	s.metrics.add(bytesReadMetric, s.store.TotalBytesRead())
    74  	s.metrics.add(storageItemsMetric, s.store.TotalStorageItems())
    75  	s.metrics.add(storageBytesMetric, s.store.TotalStorageSize())
    76  
    77  	return nil
    78  }
    79  
    80  // metrics offers adding custom metrics as well as plotting the metrics on the provided x-axis
    81  // as well as generating csv export for visualisation.
    82  type metrics struct {
    83  	data   map[string]int
    84  	charts map[string][][2]int
    85  }
    86  
    87  func newMetrics() *metrics {
    88  	return &metrics{
    89  		data:   make(map[string]int),
    90  		charts: make(map[string][][2]int),
    91  	}
    92  }
    93  
    94  func (m *metrics) add(name string, value int) {
    95  	m.data[name] = value
    96  }
    97  
    98  func (m *metrics) get(name string) int {
    99  	return m.data[name]
   100  }
   101  
   102  func (m *metrics) plot(chartName string, x int, y int) {
   103  	if _, ok := m.charts[chartName]; !ok {
   104  		m.charts[chartName] = make([][2]int, 0)
   105  	}
   106  	m.charts[chartName] = append(m.charts[chartName], [2]int{x, y})
   107  }
   108  
   109  func (m *metrics) chartCSV(name string) string {
   110  	c, ok := m.charts[name]
   111  	if !ok {
   112  		return ""
   113  	}
   114  
   115  	s := strings.Builder{}
   116  	s.WriteString(name + "\n") // header
   117  	for _, line := range c {
   118  		s.WriteString(fmt.Sprintf("%d,%d\n", line[0], line[1]))
   119  	}
   120  
   121  	return s.String()
   122  }
   123  
   124  func Test_AccountCreations(t *testing.T) {
   125  	if os.Getenv("benchmark") == "" {
   126  		t.Skip("Skipping benchmarking")
   127  	}
   128  
   129  	tester, err := newStorageTest()
   130  	require.NoError(t, err)
   131  
   132  	accountChart := "accounts,storage_size"
   133  	maxAccounts := 50_000
   134  	for i := 0; i < maxAccounts; i++ {
   135  		err = tester.run(func(state types.StateDB) {
   136  			state.AddBalance(tester.newAddress(), big.NewInt(100))
   137  		})
   138  		require.NoError(t, err)
   139  
   140  		if i%50 == 0 { // plot with resolution
   141  			tester.metrics.plot(accountChart, i, tester.metrics.get(storageBytesMetric))
   142  		}
   143  	}
   144  
   145  	csv := tester.metrics.chartCSV(accountChart)
   146  	err = io.WriteFile("./account_storage_size.csv", []byte(csv))
   147  	require.NoError(t, err)
   148  }
   149  
   150  func Test_AccountContractInteraction(t *testing.T) {
   151  	if os.Getenv("benchmark") == "" {
   152  		t.Skip("Skipping benchmarking")
   153  	}
   154  
   155  	tester, err := newStorageTest()
   156  	require.NoError(t, err)
   157  	interactionChart := "interactions,storage_size_bytes"
   158  
   159  	// build test contract storage state
   160  	contractState := make(map[common.Hash]common.Hash)
   161  	for i := 0; i < 10; i++ {
   162  		h := common.HexToHash(fmt.Sprintf("%d", i))
   163  		v := common.HexToHash(fmt.Sprintf("%d %s", i, make([]byte, 32)))
   164  		contractState[h] = v
   165  	}
   166  
   167  	// build test contract code, aprox kitty contract size
   168  	code := make([]byte, 50000)
   169  
   170  	interactions := 50000
   171  	for i := 0; i < interactions; i++ {
   172  		err = tester.run(func(state types.StateDB) {
   173  			// create a new account
   174  			accAddr := tester.newAddress()
   175  			state.AddBalance(accAddr, big.NewInt(100))
   176  
   177  			// create a contract
   178  			contractAddr := tester.newAddress()
   179  			state.AddBalance(contractAddr, big.NewInt(int64(i)))
   180  			state.SetCode(contractAddr, code)
   181  
   182  			for k, v := range contractState {
   183  				state.SetState(contractAddr, k, v)
   184  			}
   185  
   186  			// simulate interaction with contract state and account balance for fees
   187  			state.SetState(contractAddr, common.HexToHash("0x03"), common.HexToHash("0x40"))
   188  			state.AddBalance(accAddr, big.NewInt(1))
   189  		})
   190  		require.NoError(t, err)
   191  
   192  		if i%50 == 0 { // plot with resolution
   193  			tester.metrics.plot(interactionChart, i, tester.metrics.get(storageBytesMetric))
   194  		}
   195  	}
   196  
   197  	csv := tester.metrics.chartCSV(interactionChart)
   198  	err = io.WriteFile("./interactions_storage_size.csv", []byte(csv))
   199  	require.NoError(t, err)
   200  }