-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
perf: optimize iteration on nested cache context (#13881)
Co-authored-by: Aleksandr Bezobchuk <[email protected]> Co-authored-by: Marko <[email protected]> Closes #10310
- Loading branch information
Showing
16 changed files
with
663 additions
and
101 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
package cachekv_test | ||
|
||
import ( | ||
fmt "fmt" | ||
"testing" | ||
|
||
"github.com/cosmos/cosmos-sdk/store" | ||
storetypes "github.com/cosmos/cosmos-sdk/store/types" | ||
sdk "github.com/cosmos/cosmos-sdk/types" | ||
"github.com/tendermint/tendermint/libs/log" | ||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types" | ||
dbm "github.com/tendermint/tm-db" | ||
) | ||
|
||
func DoBenchmarkDeepContextStack(b *testing.B, depth int) { | ||
begin := []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} | ||
end := []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff} | ||
key := storetypes.NewKVStoreKey("test") | ||
|
||
db := dbm.NewMemDB() | ||
cms := store.NewCommitMultiStore(db) | ||
cms.MountStoreWithDB(key, storetypes.StoreTypeIAVL, db) | ||
cms.LoadLatestVersion() | ||
ctx := sdk.NewContext(cms, tmproto.Header{}, false, log.NewNopLogger()) | ||
|
||
var stack ContextStack | ||
stack.Reset(ctx) | ||
|
||
for i := 0; i < depth; i++ { | ||
stack.Snapshot() | ||
|
||
store := stack.CurrentContext().KVStore(key) | ||
store.Set(begin, []byte("value")) | ||
} | ||
|
||
store := stack.CurrentContext().KVStore(key) | ||
|
||
b.ResetTimer() | ||
for i := 0; i < b.N; i++ { | ||
it := store.Iterator(begin, end) | ||
it.Valid() | ||
it.Key() | ||
it.Value() | ||
it.Next() | ||
it.Close() | ||
} | ||
} | ||
|
||
func BenchmarkDeepContextStack1(b *testing.B) { | ||
DoBenchmarkDeepContextStack(b, 1) | ||
} | ||
|
||
func BenchmarkDeepContextStack3(b *testing.B) { | ||
DoBenchmarkDeepContextStack(b, 3) | ||
} | ||
func BenchmarkDeepContextStack10(b *testing.B) { | ||
DoBenchmarkDeepContextStack(b, 10) | ||
} | ||
|
||
func BenchmarkDeepContextStack13(b *testing.B) { | ||
DoBenchmarkDeepContextStack(b, 13) | ||
} | ||
|
||
// cachedContext is a pair of cache context and its corresponding commit method. | ||
// They are obtained from the return value of `context.CacheContext()`. | ||
type cachedContext struct { | ||
ctx sdk.Context | ||
commit func() | ||
} | ||
|
||
// ContextStack manages the initial context and a stack of cached contexts, | ||
// to support the `StateDB.Snapshot` and `StateDB.RevertToSnapshot` methods. | ||
// | ||
// Copied from an old version of ethermint | ||
type ContextStack struct { | ||
// Context of the initial state before transaction execution. | ||
// It's the context used by `StateDB.CommitedState`. | ||
initialCtx sdk.Context | ||
cachedContexts []cachedContext | ||
} | ||
|
||
// CurrentContext returns the top context of cached stack, | ||
// if the stack is empty, returns the initial context. | ||
func (cs *ContextStack) CurrentContext() sdk.Context { | ||
l := len(cs.cachedContexts) | ||
if l == 0 { | ||
return cs.initialCtx | ||
} | ||
return cs.cachedContexts[l-1].ctx | ||
} | ||
|
||
// Reset sets the initial context and clear the cache context stack. | ||
func (cs *ContextStack) Reset(ctx sdk.Context) { | ||
cs.initialCtx = ctx | ||
if len(cs.cachedContexts) > 0 { | ||
cs.cachedContexts = []cachedContext{} | ||
} | ||
} | ||
|
||
// IsEmpty returns true if the cache context stack is empty. | ||
func (cs *ContextStack) IsEmpty() bool { | ||
return len(cs.cachedContexts) == 0 | ||
} | ||
|
||
// Commit commits all the cached contexts from top to bottom in order and clears the stack by setting an empty slice of cache contexts. | ||
func (cs *ContextStack) Commit() { | ||
// commit in order from top to bottom | ||
for i := len(cs.cachedContexts) - 1; i >= 0; i-- { | ||
if cs.cachedContexts[i].commit == nil { | ||
panic(fmt.Sprintf("commit function at index %d should not be nil", i)) | ||
} else { | ||
cs.cachedContexts[i].commit() | ||
} | ||
} | ||
cs.cachedContexts = []cachedContext{} | ||
} | ||
|
||
// CommitToRevision commit the cache after the target revision, | ||
// to improve efficiency of db operations. | ||
func (cs *ContextStack) CommitToRevision(target int) error { | ||
if target < 0 || target >= len(cs.cachedContexts) { | ||
return fmt.Errorf("snapshot index %d out of bound [%d..%d)", target, 0, len(cs.cachedContexts)) | ||
} | ||
|
||
// commit in order from top to bottom | ||
for i := len(cs.cachedContexts) - 1; i > target; i-- { | ||
if cs.cachedContexts[i].commit == nil { | ||
return fmt.Errorf("commit function at index %d should not be nil", i) | ||
} | ||
cs.cachedContexts[i].commit() | ||
} | ||
cs.cachedContexts = cs.cachedContexts[0 : target+1] | ||
|
||
return nil | ||
} | ||
|
||
// Snapshot pushes a new cached context to the stack, | ||
// and returns the index of it. | ||
func (cs *ContextStack) Snapshot() int { | ||
i := len(cs.cachedContexts) | ||
ctx, commit := cs.CurrentContext().CacheContext() | ||
cs.cachedContexts = append(cs.cachedContexts, cachedContext{ctx: ctx, commit: commit}) | ||
return i | ||
} | ||
|
||
// RevertToSnapshot pops all the cached contexts after the target index (inclusive). | ||
// the target should be snapshot index returned by `Snapshot`. | ||
// This function panics if the index is out of bounds. | ||
func (cs *ContextStack) RevertToSnapshot(target int) { | ||
if target < 0 || target >= len(cs.cachedContexts) { | ||
panic(fmt.Errorf("snapshot index %d out of bound [%d..%d)", target, 0, len(cs.cachedContexts))) | ||
} | ||
cs.cachedContexts = cs.cachedContexts[:target] | ||
} | ||
|
||
// RevertAll discards all the cache contexts. | ||
func (cs *ContextStack) RevertAll() { | ||
if len(cs.cachedContexts) > 0 { | ||
cs.RevertToSnapshot(0) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
package internal | ||
|
||
import ( | ||
"bytes" | ||
"errors" | ||
|
||
"github.com/tidwall/btree" | ||
) | ||
|
||
const ( | ||
// The approximate number of items and children per B-tree node. Tuned with benchmarks. | ||
// copied from memdb. | ||
bTreeDegree = 32 | ||
) | ||
|
||
var errKeyEmpty = errors.New("key cannot be empty") | ||
|
||
// BTree implements the sorted cache for cachekv store, | ||
// we don't use MemDB here because cachekv is used extensively in sdk core path, | ||
// we need it to be as fast as possible, while `MemDB` is mainly used as a mocking db in unit tests. | ||
// | ||
// We choose tidwall/btree over google/btree here because it provides API to implement step iterator directly. | ||
type BTree struct { | ||
tree btree.BTreeG[item] | ||
} | ||
|
||
// NewBTree creates a wrapper around `btree.BTreeG`. | ||
func NewBTree() *BTree { | ||
return &BTree{tree: *btree.NewBTreeGOptions(byKeys, btree.Options{ | ||
Degree: bTreeDegree, | ||
// Contract: cachekv store must not be called concurrently | ||
NoLocks: true, | ||
})} | ||
} | ||
|
||
func (bt *BTree) Set(key, value []byte) { | ||
bt.tree.Set(newItem(key, value)) | ||
} | ||
|
||
func (bt *BTree) Get(key []byte) []byte { | ||
i, found := bt.tree.Get(newItem(key, nil)) | ||
if !found { | ||
return nil | ||
} | ||
return i.value | ||
} | ||
|
||
func (bt *BTree) Delete(key []byte) { | ||
bt.tree.Delete(newItem(key, nil)) | ||
} | ||
|
||
func (bt *BTree) Iterator(start, end []byte) (*memIterator, error) { | ||
if (start != nil && len(start) == 0) || (end != nil && len(end) == 0) { | ||
return nil, errKeyEmpty | ||
} | ||
return NewMemIterator(start, end, bt, make(map[string]struct{}), true), nil | ||
} | ||
|
||
func (bt *BTree) ReverseIterator(start, end []byte) (*memIterator, error) { | ||
if (start != nil && len(start) == 0) || (end != nil && len(end) == 0) { | ||
return nil, errKeyEmpty | ||
} | ||
return NewMemIterator(start, end, bt, make(map[string]struct{}), false), nil | ||
} | ||
|
||
// item is a btree item with byte slices as keys and values | ||
type item struct { | ||
key []byte | ||
value []byte | ||
} | ||
|
||
// byKeys compares the items by key | ||
func byKeys(a, b item) bool { | ||
return bytes.Compare(a.key, b.key) == -1 | ||
} | ||
|
||
// newItem creates a new pair item. | ||
func newItem(key, value []byte) item { | ||
return item{key: key, value: value} | ||
} |
Oops, something went wrong.