Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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: 10 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,19 @@ func registerFlags(fs *flag.FlagSet, c *internal.Config) {
"mode",
runtime.RealmDeployment.String(),
fmt.Sprintf(
"the mode for the stress test. Possible modes: [%s, %s, %s]",
runtime.RealmDeployment.String(), runtime.PackageDeployment.String(), runtime.RealmCall.String(),
"the mode for the stress test. Possible modes: [%s, %s, %s, %s]",
runtime.RealmDeployment.String(), runtime.PackageDeployment.String(),
runtime.RealmCall.String(), runtime.Mixed.String(),
),
)

fs.StringVar(
&c.MixRatio,
"mix-ratio",
"",
"transaction mix ratios for MIXED mode, e.g., \"REALM_CALL:70,REALM_DEPLOYMENT:20,PACKAGE_DEPLOYMENT:10\"",
)

fs.StringVar(
&c.Output,
"output",
Expand Down
14 changes: 14 additions & 0 deletions internal/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package internal

import (
"errors"
"fmt"
"regexp"

"github.com/gnolang/gno/tm2/pkg/crypto/bip39"
Expand All @@ -15,6 +16,7 @@ var (
errInvalidSubaccounts = errors.New("invalid number of subaccounts specified")
errInvalidTransactions = errors.New("invalid number of transactions specified")
errInvalidBatchSize = errors.New("invalid batch size specified")
errMixRatioRequired = errors.New("mix-ratio is required for MIXED mode")
)

var (
Expand All @@ -31,6 +33,7 @@ type Config struct {
ChainID string // the chain ID of the cluster
Mnemonic string // the mnemonic for the keyring
Mode string // the stress test mode
MixRatio string // transaction mix ratios for MIXED mode
Output string // output path for results JSON, if any

SubAccounts uint64 // the number of sub-accounts in the run
Expand Down Expand Up @@ -71,5 +74,16 @@ func (cfg *Config) Validate() error {
return errInvalidBatchSize
}

// Validate mix ratio for MIXED mode
if runtime.Type(cfg.Mode) == runtime.Mixed {
if cfg.MixRatio == "" {
return errMixRatioRequired
}

if _, err := runtime.ParseMixRatio(cfg.MixRatio); err != nil {
return fmt.Errorf("invalid mix-ratio: %w", err)
}
}

return nil
}
68 changes: 68 additions & 0 deletions internal/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package internal

import (
"testing"

"github.com/gnolang/supernova/internal/runtime"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// validBaseConfig returns a config with all fields valid except Mode/MixRatio
func validBaseConfig() *Config {
return &Config{
URL: "http://localhost:26657",
ChainID: "dev",
Mnemonic: "source bonus chronic canvas draft south burst lottery " +
"vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast",
SubAccounts: 10,
Transactions: 100,
BatchSize: 100,
}
}

func TestConfig_Validate_MixedModeRequiresRatio(t *testing.T) {
t.Parallel()

cfg := validBaseConfig()
cfg.Mode = runtime.Mixed.String()
cfg.MixRatio = ""

err := cfg.Validate()
require.Error(t, err)
assert.ErrorIs(t, err, errMixRatioRequired)
}

func TestConfig_Validate_MixedModeInvalidRatio(t *testing.T) {
t.Parallel()

cfg := validBaseConfig()
cfg.Mode = runtime.Mixed.String()
cfg.MixRatio = "REALM_CALL:50" // only one type, needs at least 2

err := cfg.Validate()
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid mix-ratio")
}

func TestConfig_Validate_MixedModeValidRatio(t *testing.T) {
t.Parallel()

cfg := validBaseConfig()
cfg.Mode = runtime.Mixed.String()
cfg.MixRatio = "REALM_CALL:70,REALM_DEPLOYMENT:30"

err := cfg.Validate()
assert.NoError(t, err)
}

func TestConfig_Validate_NonMixedModeIgnoresRatio(t *testing.T) {
t.Parallel()

cfg := validBaseConfig()
cfg.Mode = runtime.RealmCall.String()
cfg.MixRatio = "this is invalid but should be ignored"

err := cfg.Validate()
assert.NoError(t, err)
}
29 changes: 25 additions & 4 deletions internal/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,26 @@ func NewPipeline(cfg *Config) (*Pipeline, error) {

// Execute runs the entire pipeline process
func (p *Pipeline) Execute(ctx context.Context) error {
var (
mode = runtime.Type(p.cfg.Mode)
mode := runtime.Type(p.cfg.Mode)

// Setup the context for mixed mode
if mode == runtime.Mixed {
mixConfig, err := runtime.ParseMixRatio(p.cfg.MixRatio)
if err != nil {
return fmt.Errorf("unable to parse mix ratio: %w", err)
}

ctx = runtime.WithMixConfig(ctx, mixConfig)
}

txRuntime, err := runtime.GetRuntime(ctx, mode)
if err != nil {
return fmt.Errorf("unable to get runtime: %w", err)
}

var (
txBatcher = batcher.NewBatcher(ctx, p.cli)
txCollector = collector.NewCollector(ctx, p.cli)
txRuntime = runtime.GetRuntime(ctx, mode)
)

// Initialize the accounts for the runtime
Expand Down Expand Up @@ -229,7 +243,14 @@ func prepareRuntime(

signCB := runtime.SignTransactionsCb(chainID, deployer, deployerKey)

if mode != runtime.RealmCall {
needsPredeploy := mode == runtime.RealmCall

if mode == runtime.Mixed {
mixConfig := runtime.GetMixConfig(ctx)
needsPredeploy = mixConfig != nil && mixConfig.HasType(runtime.RealmCall)
}

if !needsPredeploy {
return txRuntime.CalculateRuntimeCosts(deployer, cli.EstimateGas, signCB, currentMaxGas, gasPrice, transactions)
}

Expand Down
157 changes: 157 additions & 0 deletions internal/runtime/mix_ratio.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package runtime

import (
"errors"
"fmt"
"strconv"
"strings"
)

var (
errEmptyMixRatio = errors.New("mix ratio cannot be empty")
errInvalidRatioFormat = errors.New("invalid ratio format, expected TYPE:PERCENTAGE")
errInvalidPercentage = errors.New("percentage must be a positive integer between 1 and 100")
errUnknownType = errors.New("unknown runtime type in mix ratio")
errDuplicateType = errors.New("duplicate runtime type in mix ratio")
errMixedInMix = errors.New("MIXED type cannot be used in mix ratio")
errRatioSumNot100 = errors.New("mix ratio percentages must sum to 100")
errInsufficientTypes = errors.New("mix ratio must contain at least 2 types")
)

type mixRatio struct {
Type Type
Percentage int
}

type MixConfig struct {
Ratios []mixRatio

This comment was marked as resolved.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

}

// ParseMixRatio parses a mix ratio string into a MixConfig
// Example input: "REALM_CALL:70,REALM_DEPLOYMENT:20,PACKAGE_DEPLOYMENT:10"
func ParseMixRatio(input string) (*MixConfig, error) {
if strings.TrimSpace(input) == "" {
return nil, errEmptyMixRatio
}

parts := strings.Split(input, ",")
if len(parts) < 2 {
return nil, errInsufficientTypes
}

config := &MixConfig{
Ratios: make([]mixRatio, 0, len(parts)),
}

seenTypes := make(map[Type]bool)

for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}

ratio, err := parseRatioPart(part)
if err != nil {
return nil, err
}

if seenTypes[ratio.Type] {
return nil, fmt.Errorf("%w: %s", errDuplicateType, ratio.Type)
}

seenTypes[ratio.Type] = true

config.Ratios = append(config.Ratios, ratio)
}

if err := config.Validate(); err != nil {
return nil, err
}

return config, nil
}

// parseRatioPart parses a single TYPE:PERCENTAGE part of the mix ratio
// Example input: "REALM_CALL:70" and ensures validity
func parseRatioPart(part string) (mixRatio, error) {
colonIdx := strings.LastIndex(part, ":")
if colonIdx == -1 {
return mixRatio{}, fmt.Errorf("%w: %s", errInvalidRatioFormat, part)
}

typeName := strings.TrimSpace(part[:colonIdx])
percentageStr := strings.TrimSpace(part[colonIdx+1:])

percentage, err := strconv.Atoi(percentageStr)
if err != nil || percentage < 1 || percentage > 100 {
return mixRatio{}, fmt.Errorf("%w: %s", errInvalidPercentage, percentageStr)
}

runtimeType := Type(typeName)

if runtimeType == Mixed {
return mixRatio{}, errMixedInMix
}

if !IsMixableRuntime(runtimeType) {
return mixRatio{}, fmt.Errorf("%w: %s", errUnknownType, typeName)
}

return mixRatio{
Type: runtimeType,
Percentage: percentage,
}, nil
}

// Validate checks the MixConfig for correctness
// ensuring at least two types and that percentages sum to 100
func (mc *MixConfig) Validate() error {
if len(mc.Ratios) < 2 {
return errInsufficientTypes
}

sum := 0
for _, ratio := range mc.Ratios {
sum += ratio.Percentage
}

if sum != 100 {
return fmt.Errorf("%w: got %d", errRatioSumNot100, sum)
}

return nil
}

// HasType checks if the MixConfig includes the specified runtime type
// For example in: REALM_CALL:70,REALM_DEPLOYMENT:30, HasType(REALM_CALL) returns true
// but HasType(PACKAGE_DEPLOYMENT) returns false
func (mc *MixConfig) HasType(t Type) bool {
for _, ratio := range mc.Ratios {
if ratio.Type == t {
return true
}
}

return false
}

// CalculateTransactionCounts computes the number of transactions
// for each runtime type based on the total and the defined ratios
func (mc *MixConfig) CalculateTransactionCounts(total uint64) map[Type]uint64 {
counts := make(map[Type]uint64)

var allocated uint64

for i, ratio := range mc.Ratios {
if i == len(mc.Ratios)-1 {
counts[ratio.Type] = total - allocated
} else {
count := (total * uint64(ratio.Percentage)) / 100
counts[ratio.Type] = count
allocated += count
}
}

return counts
}
Loading
Loading