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: Amortize clearing unsorted cache entries (Juno genesis fix) #12885

Merged
merged 10 commits into from
Aug 18, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
* [#12187](https://github.com/cosmos/cosmos-sdk/pull/12187) Add batch operation for x/nft module.
* [#12693](https://github.com/cosmos/cosmos-sdk/pull/12693) Make sure the order of each node is consistent when emitting proto events.
* [#12455](https://github.com/cosmos/cosmos-sdk/pull/12455) Show attempts count in error for signing.
* [#12886](https://github.com/cosmos/cosmos-sdk/pull/12886) Amortize cost of processing cache KV store

### State Machine Breaking

Expand Down
43 changes: 43 additions & 0 deletions store/cachekv/search_benchmark_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package cachekv

import (
db "github.com/tendermint/tm-db"
"strconv"
"testing"
)

func BenchmarkLargeUnsortedMisses(b *testing.B) {
for i := 0; i < b.N; i++ {
b.StopTimer()
store := generateStore()
b.StartTimer()

for k := 0; k < 10000; k++ {
// cache has A + Z values
// these are within range, but match nothing
store.dirtyItems([]byte("B1"), []byte("B2"))
}
}
}

func generateStore() *Store {
cache := map[string]*cValue{}
unsorted := map[string]struct{}{}
for i := 0; i < 5000; i++ {
key := "A" + strconv.Itoa(i)
unsorted[key] = struct{}{}
cache[key] = &cValue{}
}

for i := 0; i < 5000; i++ {
key := "Z" + strconv.Itoa(i)
unsorted[key] = struct{}{}
cache[key] = &cValue{}
}

return &Store{
cache: cache,
unsortedCache: unsorted,
sortedCache: db.NewMemDB(),
}
}
16 changes: 15 additions & 1 deletion store/cachekv/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/cosmos/cosmos-sdk/store/tracekv"
"github.com/cosmos/cosmos-sdk/store/types"
"github.com/cosmos/cosmos-sdk/types/kv"
"github.com/tendermint/tendermint/libs/math"
)

// cValue represents a cached value.
Expand Down Expand Up @@ -278,6 +279,8 @@ const (
stateAlreadySorted
)

const minSortSize = 1024

// Constructs a slice of dirty items, to use w/ memIterator.
func (store *Store) dirtyItems(start, end []byte) {
startStr, endStr := conv.UnsafeBytesToStr(start), conv.UnsafeBytesToStr(end)
Expand All @@ -294,7 +297,7 @@ func (store *Store) dirtyItems(start, end []byte) {
// O(N^2) overhead.
// Even without that, too many range checks eventually becomes more expensive
// than just not having the cache.
if n < 1024 {
if n < minSortSize {
for key := range store.unsortedCache {
// dbm.IsKeyInDomain is nil safe and returns true iff key is greater than start
if dbm.IsKeyInDomain(conv.UnsafeStrToBytes(key), start, end) {
Expand Down Expand Up @@ -331,6 +334,17 @@ func (store *Store) dirtyItems(start, end []byte) {
endIndex = len(strL) - 1
}

// Since we spent cycles to sort the values, we should process and remove a reasonable amount
// ensure start to end is at least minSortSize in size
// if below minSortSize, expand it to cover additional values
// this amortizes the cost of processing elements across multiple calls
if endIndex-startIndex < minSortSize {
endIndex = math.MinInt(startIndex+minSortSize, len(strL)-1)
if endIndex-startIndex < minSortSize {
startIndex = math.MaxInt(endIndex-minSortSize, 0)
}
}

kvL := make([]*kv.Pair, 0)
for i := startIndex; i <= endIndex; i++ {
key := strL[i]
Expand Down