diff --git a/scripts/build/build.mk b/scripts/build/build.mk index 581ee1dbe06d..da13b0e95e23 100644 --- a/scripts/build/build.mk +++ b/scripts/build/build.mk @@ -70,6 +70,11 @@ ifeq (bls12381,$(findstring bls12381,$(COSMOS_BUILD_OPTIONS))) build_tags += bls12381 endif +# benchmark module +ifeq (benchmark,$(findstring benchmark,$(COSMOS_BUILD_OPTIONS))) + build_tags += benchmark +endif + whitespace := whitespace += $(whitespace) comma := , diff --git a/simapp/app_config.go b/simapp/app_config.go index 45846b673aba..1eb1d5b16dd0 100644 --- a/simapp/app_config.go +++ b/simapp/app_config.go @@ -12,7 +12,6 @@ import ( authmodulev1 "cosmossdk.io/api/cosmos/auth/module/v1" authzmodulev1 "cosmossdk.io/api/cosmos/authz/module/v1" bankmodulev1 "cosmossdk.io/api/cosmos/bank/module/v1" - benchmarkmodulev1 "cosmossdk.io/api/cosmos/benchmark/module/v1" circuitmodulev1 "cosmossdk.io/api/cosmos/circuit/module/v1" consensusmodulev1 "cosmossdk.io/api/cosmos/consensus/module/v1" distrmodulev1 "cosmossdk.io/api/cosmos/distribution/module/v1" @@ -109,7 +108,7 @@ var ( } // application configuration (used by depinject) - appConfig = appconfig.Compose(&appv1alpha1.Config{ + appConfig = &appv1alpha1.Config{ Modules: []*appv1alpha1.ModuleConfig{ { Name: runtime.ModuleName, @@ -299,20 +298,6 @@ var ( Name: bankv2types.ModuleName, Config: appconfig.WrapAny(&bankmodulev2.Module{}), }, - { - Name: benchmark.ModuleName, - Config: appconfig.WrapAny(&benchmarkmodulev1.Module{ - GenesisParams: &benchmarkmodulev1.GeneratorParams{ - Seed: 34, - BucketCount: 1, - GenesisCount: 100, - KeyMean: 64, - KeyStdDev: 12, - ValueMean: 1024, - ValueStdDev: 256, - }, - }), - }, }, - }) + } ) diff --git a/simapp/app_di.go b/simapp/app_di.go index 90de295e8b7d..e57e2899d1c0 100644 --- a/simapp/app_di.go +++ b/simapp/app_di.go @@ -13,6 +13,7 @@ import ( "cosmossdk.io/core/registry" corestore "cosmossdk.io/core/store" "cosmossdk.io/depinject" + "cosmossdk.io/depinject/appconfig" _ "cosmossdk.io/indexer/postgres" // register the postgres indexer "cosmossdk.io/log" "cosmossdk.io/x/accounts" @@ -96,7 +97,7 @@ func init() { // AppConfig returns the default app config. func AppConfig() depinject.Config { return depinject.Configs( - appConfig, // Alternatively use appconfig.LoadYAML(AppConfigYAML) + appconfig.Compose(appConfig), // Alternatively use appconfig.LoadYAML(AppConfigYAML) depinject.Provide( ProvideExampleMintFn, // optional: override the mint module's mint function with epoched minting ), diff --git a/simapp/benchmark.go b/simapp/benchmark.go new file mode 100644 index 000000000000..64bb9e039ac8 --- /dev/null +++ b/simapp/benchmark.go @@ -0,0 +1,30 @@ +//go:build benchmark + +package simapp + +import ( + appv1alpha1 "cosmossdk.io/api/cosmos/app/v1alpha1" + benchmarkmodulev1 "cosmossdk.io/api/cosmos/benchmark/module/v1" + "cosmossdk.io/depinject/appconfig" + benchmark "cosmossdk.io/tools/benchmark/module" +) + +func init() { + // WARNING! + // Enabling this module will produce 3M keys in the genesis state for the benchmark module. + // Will also enable processing of benchmark transactions which can easily overwhelm the system. + appConfig.Modules = append(appConfig.Modules, &appv1alpha1.ModuleConfig{ + Name: benchmark.ModuleName, + Config: appconfig.WrapAny(&benchmarkmodulev1.Module{ + GenesisParams: &benchmarkmodulev1.GeneratorParams{ + Seed: 34, + BucketCount: 3, + GenesisCount: 3_000_000, + KeyMean: 64, + KeyStdDev: 12, + ValueMean: 1024, + ValueStdDev: 256, + }, + }), + }) +} diff --git a/simapp/v2/app_config.go b/simapp/v2/app_config.go index b21565822ee0..b8271884fb5e 100644 --- a/simapp/v2/app_config.go +++ b/simapp/v2/app_config.go @@ -12,7 +12,6 @@ import ( authmodulev1 "cosmossdk.io/api/cosmos/auth/module/v1" authzmodulev1 "cosmossdk.io/api/cosmos/authz/module/v1" bankmodulev1 "cosmossdk.io/api/cosmos/bank/module/v1" - benchmarkmodulev1 "cosmossdk.io/api/cosmos/benchmark/module/v1" circuitmodulev1 "cosmossdk.io/api/cosmos/circuit/module/v1" consensusmodulev1 "cosmossdk.io/api/cosmos/consensus/module/v1" distrmodulev1 "cosmossdk.io/api/cosmos/distribution/module/v1" @@ -109,7 +108,7 @@ var ( } // ModuleConfig is the application module configuration used by depinject - ModuleConfig = appconfig.Compose(&appv1alpha1.Config{ + ModuleConfig = &appv1alpha1.Config{ Modules: []*appv1alpha1.ModuleConfig{ { Name: runtime.ModuleName, @@ -304,20 +303,6 @@ var ( Name: bankv2types.ModuleName, Config: appconfig.WrapAny(&bankmodulev2.Module{}), }, - { - Name: benchmark.ModuleName, - Config: appconfig.WrapAny(&benchmarkmodulev1.Module{ - GenesisParams: &benchmarkmodulev1.GeneratorParams{ - Seed: 34, - BucketCount: 1, - GenesisCount: 100, - KeyMean: 64, - KeyStdDev: 12, - ValueMean: 1024, - ValueStdDev: 256, - }, - }), - }, }, - }) + } ) diff --git a/simapp/v2/app_di.go b/simapp/v2/app_di.go index e78708807b33..601d641ad060 100644 --- a/simapp/v2/app_di.go +++ b/simapp/v2/app_di.go @@ -10,6 +10,7 @@ import ( "cosmossdk.io/core/server" "cosmossdk.io/core/transaction" "cosmossdk.io/depinject" + "cosmossdk.io/depinject/appconfig" _ "cosmossdk.io/indexer/postgres" // register the postgres indexer "cosmossdk.io/log" "cosmossdk.io/runtime/v2" @@ -49,7 +50,7 @@ type SimApp[T transaction.Tx] struct { // AppConfig returns the default app config. func AppConfig() depinject.Config { return depinject.Configs( - ModuleConfig, // Alternatively use appconfig.LoadYAML(AppConfigYAML) + appconfig.Compose(ModuleConfig), // Alternatively use appconfig.LoadYAML(AppConfigYAML) runtime.DefaultServiceBindings(), codec.DefaultProviders, depinject.Provide( diff --git a/simapp/v2/benchmark.go b/simapp/v2/benchmark.go new file mode 100644 index 000000000000..25e5f9956003 --- /dev/null +++ b/simapp/v2/benchmark.go @@ -0,0 +1,30 @@ +//go:build benchmark + +package simapp + +import ( + appv1alpha1 "cosmossdk.io/api/cosmos/app/v1alpha1" + benchmarkmodulev1 "cosmossdk.io/api/cosmos/benchmark/module/v1" + "cosmossdk.io/depinject/appconfig" + benchmark "cosmossdk.io/tools/benchmark/module" +) + +func init() { + // WARNING! + // Enabling this module will produce 3M keys in the genesis state for the benchmark module. + // Will also enable processing of benchmark transactions which can easily overwhelm the system. + ModuleConfig.Modules = append(ModuleConfig.Modules, &appv1alpha1.ModuleConfig{ + Name: benchmark.ModuleName, + Config: appconfig.WrapAny(&benchmarkmodulev1.Module{ + GenesisParams: &benchmarkmodulev1.GeneratorParams{ + Seed: 34, + BucketCount: 3, + GenesisCount: 3_000_000, + KeyMean: 64, + KeyStdDev: 12, + ValueMean: 1024, + ValueStdDev: 256, + }, + }), + }) +} diff --git a/tools/benchmark/generator/gen.go b/tools/benchmark/generator/gen.go index 1d473781cbab..c8c4e2c00c27 100644 --- a/tools/benchmark/generator/gen.go +++ b/tools/benchmark/generator/gen.go @@ -16,6 +16,7 @@ import ( "cosmossdk.io/tools/benchmark" ) +// Options is the configuration for the generator. type Options struct { *module.GeneratorParams // HomeDir is for reading/writing state @@ -27,6 +28,8 @@ type Options struct { DeleteWeight float64 } +// State is the state of the generator. +// It can be marshaled and unmarshaled to/from a binary format. type State struct { Src interface { rand.Source @@ -36,6 +39,7 @@ type State struct { Keys [][]Payload } +// Marshal writes the state to w. func (s *State) Marshal(w io.Writer) error { srcBz, err := s.Src.MarshalBinary() if err != nil { @@ -65,6 +69,7 @@ func (s *State) Marshal(w io.Writer) error { return nil } +// Unmarshal reads the state from r. func (s *State) Unmarshal(r io.Reader) error { srcBz := make([]byte, 20) if _, err := r.Read(srcBz); err != nil { @@ -95,6 +100,9 @@ func (s *State) Unmarshal(r io.Reader) error { return nil } +// Generator generates operations for a benchmark transaction. +// The generator is stateful, keeping track of which keys have been inserted +// so that meaningful gets and deletes can be generated. type Generator struct { Options @@ -104,6 +112,7 @@ type Generator struct { type opt func(*Generator) +// NewGenerator creates a new generator with the given options. func NewGenerator(opts Options, f ...opt) *Generator { g := &Generator{ Options: opts, @@ -118,6 +127,9 @@ func NewGenerator(opts Options, f ...opt) *Generator { return g } +// WithGenesis sets the generator state to the genesis seed. +// When the generator is created, it will sync to genesis state. +// The benchmark client needs to do this so that it can generate meaningful tx operations. func WithGenesis() func(*Generator) { return func(g *Generator) { // sync state to genesis seed @@ -130,6 +142,7 @@ func WithGenesis() func(*Generator) { } } +// WithSeed sets the seed for the generator. func WithSeed(seed uint64) func(*Generator) { return func(g *Generator) { g.state.Src = rand.NewPCG(seed, seed>>32) @@ -137,6 +150,7 @@ func WithSeed(seed uint64) func(*Generator) { } } +// Load loads the generator state from disk. func (g *Generator) Load() error { f := fmt.Sprintf("%s/data/generator_state.bin", g.HomeDir) r, err := os.Open(f) @@ -149,16 +163,22 @@ func (g *Generator) Load() error { return g.state.Unmarshal(r) } +// Payload is a 2-tuple of seed and length. +// A seed is uint64 which is used to generate a byte slice of size length. type Payload [2]uint64 +// Seed returns the seed in the payload. func (p Payload) Seed() uint64 { return p[0] } +// Length returns the length in the payload. func (p Payload) Length() uint64 { return p[1] } +// Bytes returns the byte slice generated from the seed and length. +// The underlying byte slice is deterministically generated using the (very fast) xxhash algorithm. func (p Payload) Bytes() []byte { return Bytes(p.Seed(), p.Length()) } @@ -171,6 +191,7 @@ func NewPayload(seed, length uint64) Payload { return Payload{seed, length} } +// KV is a key-value pair with a store key. type KV struct { StoreKey uint64 Key Payload @@ -194,6 +215,9 @@ func (g *Generator) setKey(bucket uint64, payload Payload) { g.state.Keys[bucket] = append(g.state.Keys[bucket], payload) } +// GenesisSet returns a sequence of key-value pairs for the genesis state. +// It is called by the server during InitGenesis to generate and set the initial state. +// The client uses WithGenesis to sync to the genesis state. func (g *Generator) GenesisSet() iter.Seq[*KV] { return func(yield func(*KV) bool) { for range g.GenesisCount { @@ -209,9 +233,12 @@ func (g *Generator) GenesisSet() iter.Seq[*KV] { } } +// Next generates the next benchmark operation. +// The operation is one of insert, update, get, or delete. +// The tx client calls this function to deterministically generate the next operation. func (g *Generator) Next() (uint64, *benchmark.Op, error) { if g.InsertWeight+g.UpdateWeight+g.GetWeight+g.DeleteWeight != 1 { - return 0, nil, fmt.Errorf("probabilities must sum to 1") + return 0, nil, fmt.Errorf("weights must sum to 1") } var ( @@ -265,6 +292,7 @@ func (g *Generator) Next() (uint64, *benchmark.Op, error) { return bucket, op, nil } +// NormUint64 returns a random uint64 with a normal distribution. func (g *Generator) NormUint64(mean, stdDev uint64) uint64 { return uint64(g.rand.NormFloat64()*float64(stdDev) + float64(mean)) } @@ -277,6 +305,7 @@ func (g *Generator) getLength(mean, stdDev uint64) uint64 { return length } +// UintN returns a random uint64 in the range [0, n). func (g *Generator) UintN(n uint64) uint64 { return g.rand.Uint64N(n) } @@ -305,6 +334,7 @@ func encodeUint64(x uint64) []byte { const maxStoreKeyGenIterations = 100 +// StoreKeys deterministically generates a set of unique store keys from seed. func StoreKeys(prefix string, seed, count uint64) ([]string, error) { r := rand.New(rand.NewPCG(seed, seed>>32)) keys := make([]string, count) @@ -323,10 +353,13 @@ func StoreKeys(prefix string, seed, count uint64) ([]string, error) { keys[i] = sk seen[sk] = struct{}{} i++ + j++ } return keys, nil } +// Bytes generates a byte slice of length length from seed. +// The byte slice is deterministically generated using the (very fast) xxhash algorithm. func Bytes(seed, length uint64) []byte { b := make([]byte, length) rounds := length / 8 diff --git a/tools/benchmark/module/depinject.go b/tools/benchmark/module/depinject.go index ffb9b653aa28..d09d98c895e1 100644 --- a/tools/benchmark/module/depinject.go +++ b/tools/benchmark/module/depinject.go @@ -17,7 +17,6 @@ const ModuleName = "benchmark" const maxStoreKeyGenIterations = 100 func init() { - // TODO try depinject gogo API appconfig.RegisterModule( &modulev1.Module{}, appconfig.Provide( diff --git a/tools/benchmark/module/module.go b/tools/benchmark/module/module.go index dfe1423b2d7b..ad40eb04e9d9 100644 --- a/tools/benchmark/module/module.go +++ b/tools/benchmark/module/module.go @@ -3,8 +3,6 @@ package module import ( "context" "encoding/json" - "fmt" - "github.com/cosmos/cosmos-sdk/types/msgservice" "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/spf13/cobra" @@ -67,8 +65,7 @@ func (a *AppModule) InitGenesis(ctx context.Context, _ json.RawMessage) error { for kv := range g.GenesisSet() { i++ if i%100_000 == 0 { - fmt.Printf("init genesis: %d/%d\n", i, a.genesisParams.GenesisCount) - a.log.Warn("init genesis", "progress", i, "total", a.genesisParams.GenesisCount) + a.log.Warn("benchmark: init genesis", "progress", i, "total", a.genesisParams.GenesisCount) } sk := a.storeKeys[kv.StoreKey] key := gen.Bytes(kv.Key.Seed(), kv.Key.Length()) diff --git a/tools/benchmark/proto/buf.yaml b/tools/benchmark/proto/buf.yaml index 19fd0b6afb95..ae83b8045617 100644 --- a/tools/benchmark/proto/buf.yaml +++ b/tools/benchmark/proto/buf.yaml @@ -1,5 +1,5 @@ version: v1 -name: buf.build/mods/consensus +name: buf.build/mods/benchmark deps: - buf.build/cosmos/cosmos-sdk # pin the Cosmos SDK version - buf.build/cometbft/cometbft:4a62c99d422068a5165429b62a7eb824df46cca9 # CometBFT v0.38 diff --git a/tools/benchmark/proto/cosmos/benchmark/module/v1/module.proto b/tools/benchmark/proto/cosmos/benchmark/module/v1/module.proto index 2e22994cdf9b..c0bc93f0dfb5 100644 --- a/tools/benchmark/proto/cosmos/benchmark/module/v1/module.proto +++ b/tools/benchmark/proto/cosmos/benchmark/module/v1/module.proto @@ -15,18 +15,29 @@ message Module { // GenesisParams defines the genesis parameters for the benchmark module. message GeneratorParams { + // seed is the seed for the random number generator. uint64 seed = 1; + // bucket_count is the number of store keys to uniformly distribute genesis_count keys across. uint64 bucket_count = 2; + // key_mean is the mean size (in normal distribution) of keys in each bucket. uint64 key_mean = 3; + // key_std_dev is the standard deviation of key sizes in each bucket. uint64 key_std_dev = 4; + // value_mean is the mean size (in normal distribution) of values in each bucket. uint64 value_mean = 6; + // value_std_dev is the standard deviation of value sizes in each bucket. uint64 value_std_dev = 7; + // genesis_count is the number of keys to insert in the store, distributed across all buckets. uint64 genesis_count = 8; + // insert_weight is the weight of insert operations. float insert_weight = 9; + // update_weight is the weight of update operations. float update_weight = 10; + // get_weight is the weight of get operations. float get_weight = 12; + // delete_weight is the weight of delete operations. float delete_weight = 11; } \ No newline at end of file