Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ Edit `my-config.json`:
"accounts": {
"count": 100,
"newAccountRate": 0.1
},
"settings": {
"workers": 5,
"tps": 100,
"statsInterval": "10s",
"bufferSize": 1000,
"trackUserLatency": true
}
}
```
Expand Down Expand Up @@ -89,7 +96,8 @@ Edit `my-config.json`:
"endpoints": ["http://localhost:8545"],
"chainId": 1329,
"scenarios": [...],
"accounts": {...}
"accounts": {...},
"settings": {...}
}
```

Expand All @@ -110,6 +118,31 @@ Edit `my-config.json`:
}
```

### Settings
```json
"settings": {
"workers": 5,
"tps": 100,
"statsInterval": "10s",
"bufferSize": 1000,
"trackUserLatency": true
}
```

**Settings Precedence**: CLI flags > Config file settings > Default values

Available settings:
- `workers`: Number of workers per endpoint
- `tps`: Transactions per second (0 = unlimited)
- `statsInterval`: Stats logging interval (e.g., "10s", "5m")
- `bufferSize`: Buffer size per worker
- `dryRun`: Simulate without sending transactions
- `debug`: Enable debug logging
- `trackReceipts`: Track transaction receipts
- `trackBlocks`: Track block statistics
- `trackUserLatency`: Track user latency metrics
- `prewarm`: Prewarm accounts before test

## Available Scenarios

- **EVMTransfer**: Simple ETH transfers
Expand Down
28 changes: 23 additions & 5 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,32 @@
package config

import "math/big"
import (
"math/big"
"time"
)

// LoadConfig stores the configuration for load-related settings.
type LoadConfig struct {
ChainID int64 `json:"chain_id,omitempty"`
ChainID int64 `json:"chainId,omitempty"`
Endpoints []string `json:"endpoints"`
Accounts *AccountConfig `json:"accounts,omitempty"`
Scenarios []Scenario `json:"scenarios,omitempty"`
MockDeploy bool `json:"mock_deploy,omitempty"`
MockDeploy bool `json:"mockDeploy,omitempty"`
Settings *Settings `json:"settings,omitempty"`
}

// Settings stores CLI-configurable settings that can be specified in config file
type Settings struct {
Workers *int `json:"workers,omitempty"`
TPS *float64 `json:"tps,omitempty"`
StatsInterval *time.Duration `json:"statsInterval,omitempty"`
BufferSize *int `json:"bufferSize,omitempty"`
DryRun *bool `json:"dryRun,omitempty"`
Debug *bool `json:"debug,omitempty"`
TrackReceipts *bool `json:"trackReceipts,omitempty"`
TrackBlocks *bool `json:"trackBlocks,omitempty"`
TrackUserLatency *bool `json:"trackUserLatency,omitempty"`
Prewarm *bool `json:"prewarm,omitempty"`
}

// GetChainID returns the chain ID as a big.Int.
Expand All @@ -18,8 +36,8 @@ func (c *LoadConfig) GetChainID() *big.Int {

// AccountConfig stores the configuration for account generation.
type AccountConfig struct {
NewAccountRate float64 `json:"new_account_rate,omitempty"`
Accounts int `json:"accounts,omitempty"`
NewAccountRate float64 `json:"newAccountRate,omitempty"`
Accounts int `json:"count,omitempty"`
}

// Scenario represents each scenario in the load configuration.
Expand Down
164 changes: 133 additions & 31 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,105 @@ var (
trackUserLatency bool
)

// ResolvedSettings holds the final resolved settings after applying precedence
type ResolvedSettings struct {
Workers int
TPS float64
StatsInterval time.Duration
BufferSize int
DryRun bool
Debug bool
TrackReceipts bool
TrackBlocks bool
TrackUserLatency bool
Prewarm bool
}

// resolveSettings applies precedence: CLI > Config > Default
func resolveSettings(cfg *config.LoadConfig, cmd *cobra.Command) ResolvedSettings {
settings := ResolvedSettings{
// Default values
Workers: 1,
TPS: 0,
StatsInterval: 10 * time.Second,
BufferSize: 1000,
DryRun: false,
Debug: false,
TrackReceipts: false,
TrackBlocks: false,
TrackUserLatency: false,
Prewarm: false,
}

// Apply config values if present
if cfg.Settings != nil {
if cfg.Settings.Workers != nil {
settings.Workers = *cfg.Settings.Workers
}
if cfg.Settings.TPS != nil {
settings.TPS = *cfg.Settings.TPS
}
if cfg.Settings.StatsInterval != nil {
settings.StatsInterval = *cfg.Settings.StatsInterval
}
if cfg.Settings.BufferSize != nil {
settings.BufferSize = *cfg.Settings.BufferSize
}
if cfg.Settings.DryRun != nil {
settings.DryRun = *cfg.Settings.DryRun
}
if cfg.Settings.Debug != nil {
settings.Debug = *cfg.Settings.Debug
}
if cfg.Settings.TrackReceipts != nil {
settings.TrackReceipts = *cfg.Settings.TrackReceipts
}
if cfg.Settings.TrackBlocks != nil {
settings.TrackBlocks = *cfg.Settings.TrackBlocks
}
if cfg.Settings.TrackUserLatency != nil {
settings.TrackUserLatency = *cfg.Settings.TrackUserLatency
}
if cfg.Settings.Prewarm != nil {
settings.Prewarm = *cfg.Settings.Prewarm
}
}

// Apply CLI values if explicitly set (CLI wins over config)
if cmd.Flags().Changed("workers") {
settings.Workers = workers
}
if cmd.Flags().Changed("tps") {
settings.TPS = tps
}
if cmd.Flags().Changed("stats-interval") {
settings.StatsInterval = statsInterval
}
if cmd.Flags().Changed("buffer-size") {
settings.BufferSize = bufferSize
}
if cmd.Flags().Changed("dry-run") {
settings.DryRun = dryRun
}
if cmd.Flags().Changed("debug") {
settings.Debug = debug
}
if cmd.Flags().Changed("track-receipts") {
settings.TrackReceipts = trackReceipts
}
if cmd.Flags().Changed("track-blocks") {
settings.TrackBlocks = trackBlocks
}
if cmd.Flags().Changed("track-user-latency") {
settings.TrackUserLatency = trackUserLatency
}
if cmd.Flags().Changed("prewarm") {
settings.Prewarm = prewarm
}

return settings
}

var rootCmd = &cobra.Command{
Use: "seiload",
Short: "Sei Chain Load Test v2",
Expand Down Expand Up @@ -88,42 +187,45 @@ func runLoadTest(ctx context.Context, cmd *cobra.Command, args []string) error {
return fmt.Errorf("failed to load config: %w", err)
}

// Resolve settings with precedence: CLI > Config > Default
settings := resolveSettings(cfg, cmd)

log.Printf("🚀 Starting Sei Chain Load Test v2")
log.Printf("📁 Config file: %s", configFile)
log.Printf("🎯 Endpoints: %d", len(cfg.Endpoints))
log.Printf("👥 Workers per endpoint: %d", workers)
log.Printf("🔧 Total workers: %d", len(cfg.Endpoints)*workers)
log.Printf("👥 Workers per endpoint: %d", settings.Workers)
log.Printf("🔧 Total workers: %d", len(cfg.Endpoints)*settings.Workers)
log.Printf("📊 Scenarios: %d", len(cfg.Scenarios))
log.Printf("⏱️ Stats interval: %v", statsInterval)
log.Printf("📦 Buffer size per worker: %d", bufferSize)
if tps > 0 {
log.Printf("📈 Transactions per second: %.2f", tps)
log.Printf("⏱️ Stats interval: %v", settings.StatsInterval)
log.Printf("📦 Buffer size per worker: %d", settings.BufferSize)
if settings.TPS > 0 {
log.Printf("📈 Transactions per second: %.2f", settings.TPS)
}
if dryRun {
if settings.DryRun {
log.Printf("📝 Dry run: enabled")
}
if trackReceipts {
if settings.TrackReceipts {
log.Printf("📝 Track receipts: enabled")
}
if trackBlocks {
if settings.TrackBlocks {
log.Printf("📝 Track blocks: enabled")
}
if prewarm {
if settings.Prewarm {
log.Printf("📝 Prewarm: enabled")
}
if trackUserLatency {
if settings.TrackUserLatency {
log.Printf("📝 Track user latency: enabled")
}
log.Println()

// Enable mock deployment in dry-run mode
if dryRun {
if settings.DryRun {
cfg.MockDeploy = true
}

// Create statistics collector and logger
collector := stats.NewCollector()
logger := stats.NewLogger(collector, statsInterval, debug)
logger := stats.NewLogger(collector, settings.StatsInterval, settings.Debug)

err = service.Run(ctx, func(ctx context.Context, s service.Scope) error {
// Create the generator from the config struct
Expand All @@ -133,14 +235,14 @@ func runLoadTest(ctx context.Context, cmd *cobra.Command, args []string) error {
}

// Create the sender from the config struct
snd, err := sender.NewShardedSender(cfg, bufferSize, workers)
snd, err := sender.NewShardedSender(cfg, settings.BufferSize, settings.Workers)
if err != nil {
return fmt.Errorf("failed to create sender: %w", err)
}

// Create and start block collector if endpoints are available
var blockCollector *stats.BlockCollector
if len(cfg.Endpoints) > 0 && trackBlocks {
if len(cfg.Endpoints) > 0 && settings.TrackBlocks {
blockCollector = stats.NewBlockCollector()
collector.SetBlockCollector(blockCollector)
s.SpawnBgNamed("block collector", func() error {
Expand All @@ -149,24 +251,24 @@ func runLoadTest(ctx context.Context, cmd *cobra.Command, args []string) error {
}

// Create and start user latency tracker if endpoints are available
if len(cfg.Endpoints) > 0 && trackUserLatency {
userLatencyTracker := stats.NewUserLatencyTracker(statsInterval)
if len(cfg.Endpoints) > 0 && settings.TrackUserLatency {
userLatencyTracker := stats.NewUserLatencyTracker(settings.StatsInterval)
s.SpawnBgNamed("user latency tracker", func() error {
return userLatencyTracker.Run(ctx, cfg.Endpoints[0])
})
}

// Enable dry-run mode in sender if specified
if dryRun {
if settings.DryRun {
snd.SetDryRun(true)
}
if debug {
if settings.Debug {
snd.SetDebug(true)
}
if trackReceipts {
if settings.TrackReceipts {
snd.SetTrackReceipts(true)
}
if trackBlocks {
if settings.TrackBlocks {
snd.SetTrackBlocks(true)
}

Expand All @@ -175,17 +277,17 @@ func runLoadTest(ctx context.Context, cmd *cobra.Command, args []string) error {

// Create dispatcher
dispatcher := sender.NewDispatcher(gen, snd)
if tps > 0 {
if settings.TPS > 0 {
// Convert TPS to interval: 1/tps seconds = (1/tps) * 1e9 nanoseconds
intervalNs := int64((1.0 / tps) * 1e9)
intervalNs := int64((1.0 / settings.TPS) * 1e9)
dispatcher.SetRateLimit(time.Duration(intervalNs))
}

// Set statistics collector for dispatcher
dispatcher.SetStatsCollector(collector)

// Set up prewarming if enabled
if prewarm {
if settings.Prewarm {
log.Printf("🔥 Creating prewarm generator...")
prewarmGen := generator.NewPrewarmGenerator(cfg, gen)
dispatcher.SetPrewarmGenerator(prewarmGen)
Expand All @@ -198,7 +300,7 @@ func runLoadTest(ctx context.Context, cmd *cobra.Command, args []string) error {
log.Printf("✅ Connected to %d endpoints", snd.GetNumShards())

// Perform prewarming if enabled (before starting logger to avoid logging prewarm transactions)
if prewarm {
if settings.Prewarm {
if err := dispatcher.Prewarm(ctx); err != nil {
return fmt.Errorf("failed to prewarm accounts: %w", err)
}
Expand All @@ -216,20 +318,20 @@ func runLoadTest(ctx context.Context, cmd *cobra.Command, args []string) error {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

log.Printf("📈 Logging statistics every %v (Press Ctrl+C to stop)", statsInterval)
if dryRun {
log.Printf("📈 Logging statistics every %v (Press Ctrl+C to stop)", settings.StatsInterval)
if settings.DryRun {
log.Printf("📝 Dry-run mode: Simulating requests without sending")
}
if debug {
if settings.Debug {
log.Printf("🐛 Debug mode: Each transaction will be logged")
}
if trackReceipts {
if settings.TrackReceipts {
log.Printf("📝 Track receipts mode: Receipts will be tracked")
}
if trackBlocks {
if settings.TrackBlocks {
log.Printf("📝 Track blocks mode: Block data will be collected")
}
if trackUserLatency {
if settings.TrackUserLatency {
log.Printf("📝 Track user latency mode: User latency will be tracked")
}
log.Print(strings.Repeat("=", 60))
Expand Down
Loading
Loading