Skip to content
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

perf: optimize iteration on nested cache context #13881

Merged
merged 36 commits into from
Dec 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
1874a29
Optimize iteration on nested cache context
yihuang Nov 16, 2022
0c60aa1
use btree directly
yihuang Nov 16, 2022
08454af
add comments
yihuang Nov 16, 2022
48fb0a7
move to internal package
yihuang Nov 16, 2022
1bd5213
changelog
yihuang Nov 16, 2022
1f629ae
fix lint
yihuang Nov 16, 2022
d9bec0d
update comments
yihuang Nov 16, 2022
a5d2f20
Update store/cachekv/internal/btree.go
yihuang Nov 16, 2022
6d301f4
no locks
yihuang Nov 16, 2022
4be8ecf
Merge remote-tracking branch 'fork/optim_cachekv' into optim_cachekv
yihuang Nov 16, 2022
cf19f22
add contract comment about concurrency
yihuang Nov 16, 2022
5eed697
close iterators properly
yihuang Nov 16, 2022
99888dd
cleanup with review suggestions
yihuang Nov 16, 2022
f9f06e3
Merge branch 'main' into optim_cachekv
yihuang Nov 16, 2022
c5dca14
Update CHANGELOG.md
yihuang Nov 17, 2022
4cd470e
review suggestions
yihuang Nov 17, 2022
353c55f
Merge branch 'main' into optim_cachekv
yihuang Nov 17, 2022
09d3d78
Update store/cachekv/internal/btree.go
yihuang Nov 17, 2022
4a27e1c
Merge branch 'main' into optim_cachekv
yihuang Nov 18, 2022
06abe3b
Merge branch 'main' into optim_cachekv
tac0turtle Nov 18, 2022
a2daac8
Merge branch 'main' into optim_cachekv
yihuang Nov 21, 2022
e9b4ae3
Merge branch 'main' into optim_cachekv
tac0turtle Nov 23, 2022
6b34092
enable btree lock and close iterator in unit tests
yihuang Nov 24, 2022
4811bec
close iterator in unit test
yihuang Nov 24, 2022
c1526a3
Merge branch 'main' into optim_cachekv
yihuang Nov 30, 2022
b64963a
remove the btree lock for now
yihuang Nov 30, 2022
01312d8
Merge remote-tracking branch 'fork/optim_cachekv' into optim_cachekv
yihuang Nov 30, 2022
2288dd3
Merge branch 'main' into optim_cachekv
yihuang Dec 1, 2022
3a2ce61
Merge branch 'main' into optim_cachekv
yihuang Dec 6, 2022
8993c98
Merge branch 'main' into optim_cachekv
tac0turtle Dec 7, 2022
816f0cb
Merge branch 'main' into optim_cachekv
yihuang Dec 15, 2022
9fea8b4
add deadlock unit test case
yihuang Dec 15, 2022
81bd8c9
Merge branch 'main' into optim_cachekv
tac0turtle Dec 15, 2022
91ee101
Merge branch 'main' into optim_cachekv
alexanderbez Dec 15, 2022
7a0c065
Merge branch 'main' into optim_cachekv
tac0turtle Dec 15, 2022
b2eeaf8
Merge branch 'main' into optim_cachekv
tac0turtle Dec 16, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
* [#13794](https://github.com/cosmos/cosmos-sdk/pull/13794) `types/module.Manager` now supports the
`cosmossdk.io/core/appmodule.AppModule` API via the new `NewManagerFromMap` constructor.
* [#14019](https://github.com/cosmos/cosmos-sdk/issues/14019) Remove the interface casting to allow other implementations of a `CommitMultiStore`.
* [#13881](https://github.com/cosmos/cosmos-sdk/pull/13881) Optimize iteration on nested cached KV stores and other operations in general.

### State Machine Breaking

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ require (
github.com/tendermint/go-amino v0.16.0
github.com/tendermint/tendermint v0.37.0-rc2
github.com/tendermint/tm-db v0.6.7
github.com/tidwall/btree v1.5.2
tac0turtle marked this conversation as resolved.
Show resolved Hide resolved
golang.org/x/crypto v0.4.0
golang.org/x/exp v0.0.0-20221019170559-20944726eadf
google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,8 @@ github.com/tendermint/tendermint v0.37.0-rc2 h1:2n1em+jfbhSv6QnBj8F6KHCpbIzZCB8K
github.com/tendermint/tendermint v0.37.0-rc2/go.mod h1:uYQO9DRNPeZROa9X3hJOZpYcVREDC2/HST+EiU5g2+A=
github.com/tendermint/tm-db v0.6.7 h1:fE00Cbl0jayAoqlExN6oyQJ7fR/ZtoVOmvPJ//+shu8=
github.com/tendermint/tm-db v0.6.7/go.mod h1:byQDzFkZV1syXr/ReXS808NxA2xvyuuVgXOJ/088L6I=
github.com/tidwall/btree v1.5.2 h1:5eA83Gfki799V3d3bJo9sWk+yL2LRoTEah3O/SA6/8w=
github.com/tidwall/btree v1.5.2/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
Expand Down
1 change: 1 addition & 0 deletions simapp/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ require (
github.com/tendermint/btcd v0.1.1 // indirect
github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15 // indirect
github.com/tendermint/go-amino v0.16.0 // indirect
github.com/tidwall/btree v1.5.2 // indirect
github.com/ulikunitz/xz v0.5.8 // indirect
github.com/zondax/hid v0.9.1 // indirect
github.com/zondax/ledger-go v0.14.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions simapp/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,8 @@ github.com/tendermint/tendermint v0.37.0-rc2 h1:2n1em+jfbhSv6QnBj8F6KHCpbIzZCB8K
github.com/tendermint/tendermint v0.37.0-rc2/go.mod h1:uYQO9DRNPeZROa9X3hJOZpYcVREDC2/HST+EiU5g2+A=
github.com/tendermint/tm-db v0.6.7 h1:fE00Cbl0jayAoqlExN6oyQJ7fR/ZtoVOmvPJ//+shu8=
github.com/tendermint/tm-db v0.6.7/go.mod h1:byQDzFkZV1syXr/ReXS808NxA2xvyuuVgXOJ/088L6I=
github.com/tidwall/btree v1.5.2 h1:5eA83Gfki799V3d3bJo9sWk+yL2LRoTEah3O/SA6/8w=
github.com/tidwall/btree v1.5.2/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
Expand Down
161 changes: 161 additions & 0 deletions store/cachekv/benchmark_test.go
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"))
}

odeke-em marked this conversation as resolved.
Show resolved Hide resolved
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)
}
}
80 changes: 80 additions & 0 deletions store/cachekv/internal/btree.go
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.
tac0turtle marked this conversation as resolved.
Show resolved Hide resolved
//
// 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just do len comparison. No need for nil checks.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's only to emulate the interface contract in tm-db: https://github.com/tendermint/tm-db/blob/master/memdb.go#L179
it's not used by cachekv, only used in unit tests currently.

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}
}
Loading