-
Notifications
You must be signed in to change notification settings - Fork 886
Cut per-read allocations on the flatkv EVM state view hot path #4163
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -80,36 +80,37 @@ func (v *flatKVStateView) Get(module string, key []byte) ([]byte, bool) { | |
|
|
||
| // AccountExists reports whether addr has an account in this block. | ||
| func (v *flatKVStateView) AccountExists(addr gigatypes.Address) bool { | ||
| return v.accountData(addr[:]) != nil | ||
| _, ok := v.accountRow(addr) | ||
| return ok | ||
| } | ||
|
|
||
| // GetNonce returns addr's account nonce, or 0 when the account does not exist. | ||
| func (v *flatKVStateView) GetNonce(addr gigatypes.Address) uint64 { | ||
| account := v.accountData(addr[:]) | ||
| if account == nil { | ||
| account, ok := v.accountRow(addr) | ||
| if !ok { | ||
| return 0 | ||
| } | ||
| return account.GetNonce() | ||
| return account.Nonce() | ||
| } | ||
|
|
||
| // GetBalance returns addr's balance as a 256-bit big-endian value, or the zero value when addr holds | ||
| // no balance. | ||
| func (v *flatKVStateView) GetBalance(addr gigatypes.Address) gigatypes.Hash { | ||
| account := v.accountData(addr[:]) | ||
| if account == nil { | ||
| account, ok := v.accountRow(addr) | ||
| if !ok { | ||
| return gigatypes.Hash{} | ||
| } | ||
| return gigatypes.Hash(*account.GetBalance()) | ||
| return gigatypes.Hash(account.Balance()) | ||
| } | ||
|
|
||
| // GetCodeHash returns the hash of addr's contract code, gigatypes.EmptyCodeHash when the account exists | ||
| // and holds no code, or the zero hash when it does not exist. | ||
| func (v *flatKVStateView) GetCodeHash(addr gigatypes.Address) gigatypes.Hash { | ||
| account := v.accountData(addr[:]) | ||
| if account == nil { | ||
| account, ok := v.accountRow(addr) | ||
| if !ok { | ||
| return gigatypes.Hash{} | ||
| } | ||
| codeHash := gigatypes.Hash(*account.GetCodeHash()) | ||
| codeHash := gigatypes.Hash(account.CodeHash()) | ||
| if codeHash == (gigatypes.Hash{}) { | ||
| // A row only exists while some field is non-zero (see AccountData.IsDelete), and the code hash | ||
| // is not that field here, so this account has a nonce or a balance and no code — the case EVM | ||
|
|
@@ -121,14 +122,25 @@ func (v *flatKVStateView) GetCodeHash(addr gigatypes.Address) gigatypes.Hash { | |
|
|
||
| // GetStorage returns the value at key in addr's storage, or the zero hash when the slot is unset. | ||
| func (v *flatKVStateView) GetStorage(addr gigatypes.Address, key gigatypes.Hash) gigatypes.Hash { | ||
| storage := v.storageData(ktype.StorageKey(ktype.Address(addr), ktype.Slot(key))) | ||
| if storage == nil { | ||
| var buf [physKeyBufLen]byte | ||
| physKey := ktype.AppendEVMPhysicalKey(buf[:0], keys.EVMKeyStorage, addr[:]) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [suggestion] |
||
| physKey = append(physKey, key[:]...) | ||
| raw, found := v.readRow(v.blockView.StorageView(), physKey) | ||
| if !found { | ||
| return gigatypes.Hash{} | ||
| } | ||
| return gigatypes.Hash(*storage.GetValue()) | ||
| storage, err := vtype.ParseStorageRow(raw) | ||
| if err != nil { | ||
| panic(fmt.Sprintf("flatkv: parse storage %x at height %d: %v", physKey, v.blockView.BlockHeight(), err)) | ||
| } | ||
| if storage.IsDelete() { | ||
| return gigatypes.Hash{} | ||
| } | ||
| return gigatypes.Hash(storage.Value()) | ||
| } | ||
|
|
||
| // GetCode returns addr's contract code, or nil when it has none. | ||
| // GetCode returns addr's contract code, or nil when it has none. The slice aliases the store's row | ||
| // and is valid until the view is closed. | ||
| func (v *flatKVStateView) GetCode(addr gigatypes.Address) []byte { | ||
| code := v.codeData(addr[:]) | ||
| if code == nil { | ||
|
|
@@ -142,6 +154,28 @@ func (v *flatKVStateView) GetCodeSize(addr gigatypes.Address) int { | |
| return len(v.GetCode(addr)) | ||
| } | ||
|
|
||
| // physKeyBufLen holds the longest EVM physical key: "evm/" + kind byte + address + slot. | ||
| const physKeyBufLen = len(keys.EVMStoreKey) + 2 + ktype.AddressLen + ktype.SlotLen | ||
|
|
||
| // accountRow returns addr's account row, or false when no account exists in this block. The row | ||
| // aliases store memory and is valid until the view is closed. | ||
| func (v *flatKVStateView) accountRow(addr gigatypes.Address) (vtype.AccountRow, bool) { | ||
| var buf [physKeyBufLen]byte | ||
| physKey := ktype.AppendEVMPhysicalKey(buf[:0], ktype.EVMKeyAccount, addr[:]) | ||
| raw, found := v.readRow(v.blockView.AccountView(), physKey) | ||
| if !found { | ||
| return vtype.AccountRow{}, false | ||
| } | ||
| account, err := vtype.ParseAccountRow(raw) | ||
| if err != nil { | ||
| panic(fmt.Sprintf("flatkv: parse account %x at height %d: %v", addr, v.blockView.BlockHeight(), err)) | ||
| } | ||
| if account.IsDelete() { | ||
| return vtype.AccountRow{}, false | ||
| } | ||
| return account, true | ||
| } | ||
|
|
||
| // accountData returns the account row for the 20-byte address in keyBytes, or nil when no account | ||
| // exists in this block. | ||
| func (v *flatKVStateView) accountData(keyBytes []byte) *vtype.AccountData { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| package flatkv | ||
|
|
||
| import ( | ||
| "path/filepath" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/sei-protocol/sei-chain/sei-db/common/keys" | ||
| "github.com/sei-protocol/sei-chain/sei-db/proto" | ||
| gigatypes "github.com/sei-protocol/sei-chain/sei-db/state_db/giga/types" | ||
| "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/config" | ||
| ) | ||
|
|
||
| // benchView opens a view over a store holding one account with a nonce, a balance, a storage slot and | ||
| // 8 KiB of code, so each benchmark measures a cache-hit read. | ||
| func benchView(b *testing.B) (gigatypes.StateView, gigatypes.Address) { | ||
| b.Helper() | ||
| cfg := config.DefaultTestConfig(&testing.T{}) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [suggestion] |
||
| cfg.DataDir = filepath.Join(b.TempDir(), "flatkv") | ||
| s, err := newCommitStoreWithWAL(b.Context(), cfg) | ||
| require.NoError(b, err) | ||
| require.NoError(b, s.LoadLatest()) | ||
| b.Cleanup(func() { require.NoError(b, s.Close()) }) | ||
| addr := addrN(1) | ||
| code := make([]byte, 8*1024) | ||
| for i := range code { | ||
| code[i] = byte(i) | ||
| } | ||
| cs := namedCS(noncePair(addr, 7), storagePair(addr, slotN(1), padLeft32(0xaa)), codePair(addr, code)) | ||
| cs.Changeset.Pairs = append(cs.Changeset.Pairs, &proto.KVPair{ | ||
| Key: keys.BuildEVMKey(keys.EVMKeyBalance, addr[:]), | ||
| Value: padLeft32(0x05), | ||
| }) | ||
| require.NoError(b, s.CommitStateChanges(1, []*proto.NamedChangeSet{cs})) | ||
| v := s.OpenView() | ||
| b.Cleanup(v.Close) | ||
| return v, gigaAddr(addr) | ||
| } | ||
|
|
||
| func BenchmarkStateViewGetStorage(b *testing.B) { | ||
| v, addr := benchView(b) | ||
| slot := gigatypes.Hash(slotN(1)) | ||
| require.Equal(b, gigatypes.Hash(padLeft32(0xaa)), v.GetStorage(addr, slot)) | ||
| b.ReportAllocs() | ||
| b.ResetTimer() | ||
| for i := 0; i < b.N; i++ { | ||
| v.GetStorage(addr, slot) | ||
| } | ||
| } | ||
|
|
||
| func BenchmarkStateViewGetBalance(b *testing.B) { | ||
| v, addr := benchView(b) | ||
| require.Equal(b, gigatypes.Hash(padLeft32(0x05)), v.GetBalance(addr)) | ||
| b.ReportAllocs() | ||
| b.ResetTimer() | ||
| for i := 0; i < b.N; i++ { | ||
| v.GetBalance(addr) | ||
| } | ||
| } | ||
|
|
||
| func BenchmarkStateViewGetCode(b *testing.B) { | ||
| v, addr := benchView(b) | ||
| require.Len(b, v.GetCode(addr), 8*1024) | ||
| b.ReportAllocs() | ||
| b.ResetTimer() | ||
| for i := 0; i < b.N; i++ { | ||
| v.GetCode(addr) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -75,8 +75,7 @@ func DeserializeCodeData(data []byte) (*CodeData, error) { | |
| version, codeBytecodeStart, len(data)) | ||
| } | ||
|
|
||
| bytecode := make([]byte, len(data)-codeBytecodeStart) | ||
| copy(bytecode, data[codeBytecodeStart:]) | ||
| bytecode := data[codeBytecodeStart:] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [suggestion] The returned bytecode now aliases |
||
|
|
||
| return &CodeData{ | ||
| version: version, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| package vtype | ||
|
|
||
| import ( | ||
| "encoding/binary" | ||
| "errors" | ||
| "fmt" | ||
| ) | ||
|
|
||
| // AccountRow is a read-only view over a serialized account row, in either its compact or full form. | ||
| // It aliases the bytes it was parsed from rather than copying them. | ||
| type AccountRow struct { | ||
| data []byte | ||
| } | ||
|
|
||
| // ParseAccountRow validates data as an account row and returns a view over it. | ||
| func ParseAccountRow(data []byte) (AccountRow, error) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [suggestion] |
||
| if len(data) == 0 { | ||
| return AccountRow{}, errors.New("data is empty") | ||
| } | ||
| if version := AccountDataVersion(data[accountVersionStart]); version != AccountDataVersion0 { | ||
| return AccountRow{}, fmt.Errorf("unsupported serialization version: %d", version) | ||
| } | ||
| if len(data) != accountDataLength && len(data) != accountCompactLength { | ||
| return AccountRow{}, fmt.Errorf("data length should be %d or %d, got %d", | ||
| accountCompactLength, accountDataLength, len(data)) | ||
| } | ||
| return AccountRow{data: data}, nil | ||
| } | ||
|
|
||
| // Balance returns the account's balance. | ||
| func (r AccountRow) Balance() Balance { | ||
| return Balance(r.data[accountBalanceStart:accountNonceStart]) | ||
| } | ||
|
|
||
| // Nonce returns the account's nonce. | ||
| func (r AccountRow) Nonce() uint64 { | ||
| return binary.BigEndian.Uint64(r.data[accountNonceStart:accountCodeHashStart]) | ||
| } | ||
|
|
||
| // CodeHash returns the account's code hash, or the zero hash for a compact row. | ||
| func (r AccountRow) CodeHash() CodeHash { | ||
| if len(r.data) < accountDataLength { | ||
| return CodeHash{} | ||
| } | ||
| return CodeHash(r.data[accountCodeHashStart:accountDataLength]) | ||
| } | ||
|
|
||
| // IsDelete reports whether the row is a tombstone: every field other than the version and block | ||
| // height is zero. | ||
| func (r AccountRow) IsDelete() bool { | ||
| for _, b := range r.data[accountBalanceStart:] { | ||
| if b != 0 { | ||
| return false | ||
| } | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| // StorageRow is a read-only view over a serialized storage row. It aliases the bytes it was parsed | ||
| // from rather than copying them. | ||
| type StorageRow struct { | ||
| data []byte | ||
| } | ||
|
|
||
| // ParseStorageRow validates data as a storage row and returns a view over it. | ||
| func ParseStorageRow(data []byte) (StorageRow, error) { | ||
| if len(data) == 0 { | ||
| return StorageRow{}, errors.New("data is empty") | ||
| } | ||
| if version := StorageDataVersion(data[storageVersionStart]); version != StorageDataVersion0 { | ||
| return StorageRow{}, fmt.Errorf("unsupported serialization version: %d", version) | ||
| } | ||
| if len(data) != storageDataLength { | ||
| return StorageRow{}, fmt.Errorf("data length should be %d, got %d", storageDataLength, len(data)) | ||
| } | ||
| return StorageRow{data: data}, nil | ||
| } | ||
|
|
||
| // Value returns the storage slot value. | ||
| func (r StorageRow) Value() [StorageValueLength]byte { | ||
| return [StorageValueLength]byte(r.data[storageValueStart:storageDataLength]) | ||
| } | ||
|
|
||
| // IsDelete reports whether the row is a tombstone: the value is all zeros. | ||
| func (r StorageRow) IsDelete() bool { | ||
| for _, b := range r.data[storageValueStart:] { | ||
| if b != 0 { | ||
| return false | ||
| } | ||
| } | ||
| return true | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[suggestion]
AppendEVMPhysicalKeyduplicatesEVMPhysicalKey's codehash/balance canonicalization, prefix-byte lookup, and layout.EVMPhysicalKeycan become a one-liner over it —return AppendEVMPhysicalKey(make([]byte, 0, len(keys.EVMStoreKey)+2+len(strippedKey)), kind, strippedKey)— which keeps its single allocation and itsnil-on-unknown-kind return, and leaves one place where the physical key layout is defined. Right now a change to the layout has to be made twice, and the two can diverge silently since nothing asserts they agree.