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 }