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

feat(sdk.Context): Context.KVStore/TransientStore improve performance #14203

Closed
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
12 changes: 8 additions & 4 deletions baseapp/abci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,8 @@ func TestABCI_CheckTx(t *testing.T) {
require.Empty(t, r.GetEvents())
}

checkStateStore := getCheckStateCtx(suite.baseApp).KVStore(capKey1)
ctx := getCheckStateCtx(suite.baseApp)
checkStateStore := ctx.KVStore(capKey1)
storedCounter := getIntFromStore(t, checkStateStore, counterKey)

// ensure AnteHandler ran
Expand All @@ -625,7 +626,8 @@ func TestABCI_CheckTx(t *testing.T) {
suite.baseApp.EndBlock(abci.RequestEndBlock{})
suite.baseApp.Commit()

checkStateStore = getCheckStateCtx(suite.baseApp).KVStore(capKey1)
stateCtx := getCheckStateCtx(suite.baseApp)
checkStateStore = stateCtx.KVStore(capKey1)
storedBytes := checkStateStore.Get(counterKey)
require.Nil(t, storedBytes)
}
Expand Down Expand Up @@ -697,7 +699,8 @@ func TestABCI_DeliverTx_MultiMsg(t *testing.T) {
res := suite.baseApp.DeliverTx(abci.RequestDeliverTx{Tx: txBytes})
require.True(t, res.IsOK(), fmt.Sprintf("%v", res))

store := getDeliverStateCtx(suite.baseApp).KVStore(capKey1)
ctx := getDeliverStateCtx(suite.baseApp)
store := ctx.KVStore(capKey1)

// tx counter only incremented once
txCounter := getIntFromStore(t, store, anteKey)
Expand Down Expand Up @@ -725,7 +728,8 @@ func TestABCI_DeliverTx_MultiMsg(t *testing.T) {
res = suite.baseApp.DeliverTx(abci.RequestDeliverTx{Tx: txBytes})
require.True(t, res.IsOK(), fmt.Sprintf("%v", res))

store = getDeliverStateCtx(suite.baseApp).KVStore(capKey1)
ctx = getDeliverStateCtx(suite.baseApp)
store = ctx.KVStore(capKey1)

// tx counter only incremented once
txCounter = getIntFromStore(t, store, anteKey)
Expand Down
8 changes: 4 additions & 4 deletions store/gaskv/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ var _ types.KVStore = &Store{}
// KVStore interface.
type Store struct {
gasMeter types.GasMeter
gasConfig types.GasConfig
gasConfig *types.GasConfig
parent types.KVStore
}

// NewStore returns a reference to a new GasKVStore.
func NewStore(parent types.KVStore, gasMeter types.GasMeter, gasConfig types.GasConfig) *Store {
func NewStore(parent types.KVStore, gasMeter types.GasMeter, gasConfig *types.GasConfig) *Store {
kvs := &Store{
gasMeter: gasMeter,
gasConfig: gasConfig,
Expand Down Expand Up @@ -108,11 +108,11 @@ func (gs *Store) iterator(start, end []byte, ascending bool) types.Iterator {

type gasIterator struct {
gasMeter types.GasMeter
gasConfig types.GasConfig
gasConfig *types.GasConfig
parent types.Iterator
}

func newGasIterator(gasMeter types.GasMeter, gasConfig types.GasConfig, parent types.Iterator) types.Iterator {
func newGasIterator(gasMeter types.GasMeter, gasConfig *types.GasConfig, parent types.Iterator) types.Iterator {
return &gasIterator{
gasMeter: gasMeter,
gasConfig: gasConfig,
Expand Down
8 changes: 4 additions & 4 deletions store/types/gas.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,8 @@ type GasConfig struct {
}

// KVGasConfig returns a default gas config for KVStores.
func KVGasConfig() GasConfig {
return GasConfig{
func KVGasConfig() *GasConfig {
return &GasConfig{
HasCost: 1000,
DeleteCost: 1000,
ReadCostFlat: 1000,
Expand All @@ -241,8 +241,8 @@ func KVGasConfig() GasConfig {
}

// TransientGasConfig returns a default gas config for TransientStores.
func TransientGasConfig() GasConfig {
return GasConfig{
func TransientGasConfig() *GasConfig {
return &GasConfig{
HasCost: 100,
DeleteCost: 100,
ReadCostFlat: 100,
Expand Down
2 changes: 1 addition & 1 deletion store/types/gas_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func TestAddUint64Overflow(t *testing.T) {
func TestTransientGasConfig(t *testing.T) {
t.Parallel()
config := TransientGasConfig()
require.Equal(t, config, GasConfig{
require.Equal(t, config, &GasConfig{
HasCost: 100,
DeleteCost: 100,
ReadCostFlat: 100,
Expand Down
25 changes: 14 additions & 11 deletions types/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ type Context struct {
consParams *tmproto.ConsensusParams
eventManager *EventManager
priority int64 // The tx priority, only relevant in CheckTx
kvGasConfig storetypes.GasConfig
transientKVGasConfig storetypes.GasConfig
kvGasConfig *storetypes.GasConfig
transientKVGasConfig *storetypes.GasConfig
}

// Proposed rename, not done to avoid API breakage
Expand All @@ -62,8 +62,8 @@ func (c Context) IsReCheckTx() bool { return c.recheckT
func (c Context) MinGasPrices() DecCoins { return c.minGasPrice }
func (c Context) EventManager() *EventManager { return c.eventManager }
func (c Context) Priority() int64 { return c.priority }
func (c Context) KVGasConfig() storetypes.GasConfig { return c.kvGasConfig }
func (c Context) TransientKVGasConfig() storetypes.GasConfig { return c.transientKVGasConfig }
func (c Context) KVGasConfig() storetypes.GasConfig { return *c.kvGasConfig }
func (c Context) TransientKVGasConfig() storetypes.GasConfig { return *c.transientKVGasConfig }

// clone the header before returning
func (c Context) BlockHeader() tmproto.Header {
Expand Down Expand Up @@ -203,14 +203,14 @@ func (c Context) WithBlockGasMeter(meter GasMeter) Context {
// WithKVGasConfig returns a Context with an updated gas configuration for
// the KVStore
func (c Context) WithKVGasConfig(gasConfig storetypes.GasConfig) Context {
c.kvGasConfig = gasConfig
c.kvGasConfig = &gasConfig
return c
}

// WithTransientKVGasConfig returns a Context with an updated gas configuration for
// the transient KVStore
func (c Context) WithTransientKVGasConfig(gasConfig storetypes.GasConfig) Context {
c.transientKVGasConfig = gasConfig
c.transientKVGasConfig = &gasConfig
return c
}

Expand Down Expand Up @@ -277,21 +277,24 @@ func (c Context) Value(key interface{}) interface{} {
// ----------------------------------------------------------------------------

// KVStore fetches a KVStore from the MultiStore.
func (c Context) KVStore(key storetypes.StoreKey) KVStore {
return gaskv.NewStore(c.MultiStore().GetKVStore(key), c.GasMeter(), c.kvGasConfig)
// NOTE: Uses pointer receiver to save on execution time.
func (c *Context) KVStore(key storetypes.StoreKey) KVStore {
kv := c.ms.GetKVStore(key)
return gaskv.NewStore(kv, c.gasMeter, c.kvGasConfig)
}
Comment on lines -280 to 284

Choose a reason for hiding this comment

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

Callers who invoke c.KVStore will get a KVStore value with a kvGasConfig that is no longer a copy of the config data, but instead is a pointer to the same storetypes.GasConfig value of the Context from which the KVStore was derived. This is a fundamental change to the semantics of the KVStore method, and of the Context API in general. It's important to establish the safety of large changes like this with comprehensive test coverage.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

+1 if we need testing for this but old behaviour is retained


// TransientStore fetches a TransientStore from the MultiStore.
func (c Context) TransientStore(key storetypes.StoreKey) KVStore {
return gaskv.NewStore(c.MultiStore().GetKVStore(key), c.GasMeter(), c.transientKVGasConfig)
// NOTE: Uses pointer receiver to save on execution time.
func (c *Context) TransientStore(key storetypes.StoreKey) KVStore {
return gaskv.NewStore(c.ms.GetKVStore(key), c.gasMeter, c.transientKVGasConfig)
}

// CacheContext returns a new Context with the multi-store cached and a new
// EventManager. The cached context is written to the context when writeCache
// is called. Note, events are automatically emitted on the parent context's
// EventManager when the caller executes the write.
func (c Context) CacheContext() (cc Context, writeCache func()) {
cms := c.MultiStore().CacheMultiStore()
cms := c.ms.CacheMultiStore()
cc = c.WithMultiStore(cms).WithEventManager(NewEventManager())

testinginprod marked this conversation as resolved.
Show resolved Hide resolved
writeCache = func() {
Expand Down
18 changes: 18 additions & 0 deletions types/context_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package types_test

import (
"github.com/cosmos/cosmos-sdk/store/types"
"github.com/cosmos/cosmos-sdk/testutil"
"testing"
)

func BenchmarkContext_KVStore(b *testing.B) {
key := types.NewKVStoreKey(b.Name() + "_TestCacheContext")

ctx := testutil.DefaultContext(key, types.NewTransientStoreKey("transient_"+b.Name()))

b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ctx.KVStore(key)
}
}
3 changes: 2 additions & 1 deletion x/group/internal/orm/testsupport.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ func NewGasCountingMockContext() *GasCountingMockContext {
}

func (g GasCountingMockContext) KVStore(store sdk.KVStore) sdk.KVStore {
return gaskv.NewStore(store, g.GasMeter, storetypes.KVGasConfig())
gasConfig := storetypes.KVGasConfig()
return gaskv.NewStore(store, g.GasMeter, gasConfig)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why does this offer any performance improvement?

Copy link
Contributor Author

@testinginprod testinginprod Dec 8, 2022

Choose a reason for hiding this comment

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

the perf improvement is this: https://github.com/cosmos/cosmos-sdk/pull/14203/files#diff-abad3e7f81deb0a089f7e974e60a081e67a7b21e5c6ba5c7b2be3785a66445afR15

this allows us to save time because of alloc + copy, gasKV config is static for a specific context, meaning that as the context is running the gas configuration (how much we spend on each Read/Write/Iter/etc) does not change, so this is why the copy is unnecessary. To further support this point, the only way to modify the GasConfig is by calling WithGasConfig in Context which yields to a Context copy which means the behaviour before my changes is that: changes to GasConfig are not propagated to existing gaskv instances. My perf improvement retains this behaviour.

My benches showed it saved a further 10ns

Copy link

@peterbourgon peterbourgon Dec 8, 2022

Choose a reason for hiding this comment

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

this allows us to save time because of alloc + copy

Cool, where can we see the benchmark results that demonstrate the perf increases? It would be super cool if changing the representation of this ~50 byte struct from a value to a pointer managed to produce performance benefits from the reduced copy costs that outweighed the additional costs it incurs on the allocator and GC.

}

func (g GasCountingMockContext) GasConsumed() storetypes.Gas {
Expand Down