diff --git a/README.md b/README.md index cbed63a..a6b5e05 100644 --- a/README.md +++ b/README.md @@ -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 } } ``` @@ -89,7 +96,8 @@ Edit `my-config.json`: "endpoints": ["http://localhost:8545"], "chainId": 1329, "scenarios": [...], - "accounts": {...} + "accounts": {...}, + "settings": {...} } ``` @@ -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 diff --git a/config/config.go b/config/config.go index 500523b..2014f66 100644 --- a/config/config.go +++ b/config/config.go @@ -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. @@ -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. diff --git a/main.go b/main.go index 042c769..da13b28 100644 --- a/main.go +++ b/main.go @@ -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", @@ -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 @@ -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 { @@ -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) } @@ -175,9 +277,9 @@ 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)) } @@ -185,7 +287,7 @@ func runLoadTest(ctx context.Context, cmd *cobra.Command, args []string) error { 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) @@ -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) } @@ -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)) diff --git a/profiles/local.json b/profiles/local.json index f12eaa7..060759f 100644 --- a/profiles/local.json +++ b/profiles/local.json @@ -1,10 +1,11 @@ { - "chain_id": 713714, + "chainId": 713714, "endpoints": [ "http://127.0.0.1:8545" ], "accounts": { - "accounts": 5000 + "count": 5000, + "newAccountRate": 0.0 }, "scenarios": [ { @@ -27,5 +28,17 @@ "name": "ERC721", "weight": 1 } - ] + ], + "settings": { + "workers": 1, + "tps": 0, + "statsInterval": "10s", + "bufferSize": 1000, + "dryRun": false, + "debug": false, + "trackReceipts": false, + "trackBlocks": false, + "trackUserLatency": false, + "prewarm": false + } }