Skip to content

Commit 4ba788f

Browse files
tclemospraetoriansentryclaudeminhd-vu
authored
feat: gas manager for loadtest; add plot command (#755)
* loadtest: WIP first version of a gas limiter based on a oscillation curve * add traces; add gas limiter readme; fix infinte loadtest loop; * add gas pricer; add flags to gasmanager; rename to gasmanager package * add tx-gas-chart command; adjusts to gas manager * refactoring loadtest gas price manager waves and tx-gas-chart; * refactoring loadtest gas price manager waves and tx-gas-chart; * gas manager and tx-gas-chart refactoring and docs; * adds usage documentation for tx-gas-chart * improves documentation for gasmanager package * refactoring * make gen-doc * small doc formating * fix: nil pointer dereference in contract-call loadtest mode Fix panic caused by signing a nil transaction in loadTestContractCall. The transaction was being built and assigned to `tx`, but the code was incorrectly trying to sign `rtx` which was declared but never assigned. This caused a segmentation fault when using the `contract-call` mode: panic: runtime error: invalid memory address or nil pointer dereference at cmd/loadtest/loadtest.go:1987 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * loadtest: remove infinite; gas provider wait first new block to provide gas * make gen-doc * WIP * tx-gas-chart: make it compatible with op-stack deposit txs * chart header fix * fix merge conflicts * tx-gas-chart: allow start block to be zero * fix codeql issues * add unit tests to TestGetSenderFromTx * fix: correct variation formula in DynamicGasPriceStrategy * fix: add context cancellation support to GasVault.SpendOrWaitAvailableBudget * fix: use passed context instead of creating new root context in runLoadTest * fix: add validation for empty gas prices in DynamicGasPriceStrategy * fix: use cmd.Context() instead of context.Background() in txgaschart * loadtest: revert debug comments for random mode * loadtest: add comment for better explanation * loadtest: make lint * txgaschart: improve usage.md readability * docs: update README to reflect context cancellation changes and fix markdown formatting * fix: prevent precision loss and overflow in average block gas calculation * test: add comprehensive unit tests for GasVault * test: add comprehensive unit tests for gas pricing strategies * test: add comprehensive unit tests for OscillatingGasProvider * docs: add lessons learned to CLAUDE.md for future sessions * fix: remove impossible int > MaxInt64 check flagged by staticcheck * make gen-doc * chore: combine like files * fix: default block range * refactor: simplify code and improve Go idioms - Use cmp.Compare for sorting instead of if/else chains - Use named type gasPriceCount instead of repeated anonymous struct - Replace time.Sleep in select with proper time.Ticker pattern - Embed sync.Mutex directly instead of using pointer - Return copy from GetGasPrice to prevent mutation of internal state - Simplify wave computeWave methods by removing redundant casts and constants - Capitalize log messages for consistency - Fix parseFlags to check start block after defaulting (fixes startup error) * rename: tx-gas-chart command to plot - Rename cmd/txgaschart directory to cmd/plot - Update package name and command Use field - Update all imports and documentation references - Regenerate documentation * fix: use best practices * refactor(plot): simplify chart code and improve tooltips * feat: migrate from gonum/plot to go-echarts * fix: go.sum * fix: ci --------- Co-authored-by: John Hilliard <praetoriansentry@gmail.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Minh Vu <mvu@polygon.technology> Co-authored-by: Minh Vu <minhd_vu@yahoo.com>
1 parent 71365f4 commit 4ba788f

34 files changed

Lines changed: 4687 additions & 129 deletions

CLAUDE.md

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,152 @@ go func() {
317317
}()
318318
```
319319

320+
## Additional Patterns & Lessons Learned
321+
322+
### Random Variation Formula
323+
When implementing random variation within a percentage range:
324+
325+
```go
326+
// ✅ DO: Correct uniform distribution
327+
variationMin := 1.0 - variation // e.g., 0.7 for ±30%
328+
variationMax := 1.0 + variation // e.g., 1.3 for ±30%
329+
factor := variationMin + rand.Float64() * (variationMax - variationMin)
330+
// Result: uniformly distributed between 0.7 and 1.3
331+
332+
// ❌ DON'T: Incorrect - produces wrong range
333+
factor := variationMin + rand.Float64() * variationMax
334+
// Result: distributed between 0.7 and 2.0 (not ±30%!)
335+
```
336+
337+
### Type Conversion Safety (big.Int and int64)
338+
When converting between types with different ranges:
339+
340+
```go
341+
// ✅ DO: Check bounds before conversion
342+
numBlocks := len(blocks)
343+
if numBlocks == 0 {
344+
avgGasUsed = 0
345+
} else if numBlocks > math.MaxInt64 {
346+
log.Warn().Msg("value exceeds int64 max")
347+
avgGasUsed = 0
348+
} else {
349+
result := new(big.Int).Div(total, big.NewInt(int64(numBlocks)))
350+
if result.IsUint64() {
351+
avgGasUsed = result.Uint64()
352+
} else {
353+
avgGasUsed = math.MaxUint64
354+
log.Warn().Msg("result exceeds uint64 max")
355+
}
356+
}
357+
358+
// ❌ DON'T: Blindly convert without checking
359+
avgGasUsed = new(big.Int).Div(total, big.NewInt(int64(len(blocks)))).Uint64()
360+
// Can panic if len(blocks) > MaxInt64 or result > MaxUint64
361+
```
362+
363+
### Blocking Operations Must Accept Context
364+
Any operation that can block must accept `context.Context` for cancellation:
365+
366+
```go
367+
// ✅ DO: Accept context, use ticker, handle cancellation
368+
func (v *Vault) SpendOrWait(ctx context.Context, amount uint64) error {
369+
ticker := time.NewTicker(100 * time.Millisecond)
370+
defer ticker.Stop()
371+
372+
for {
373+
if v.trySpend(amount) {
374+
return nil
375+
}
376+
select {
377+
case <-ticker.C:
378+
continue
379+
case <-ctx.Done():
380+
return ctx.Err()
381+
}
382+
}
383+
}
384+
385+
// ❌ DON'T: Use bare time.Sleep in infinite loop
386+
func (v *Vault) SpendOrWait(amount uint64) {
387+
for {
388+
if v.trySpend(amount) {
389+
break
390+
}
391+
time.Sleep(100 * time.Millisecond) // Cannot be cancelled!
392+
}
393+
}
394+
```
395+
396+
### Constructor Validation
397+
Validate inputs in constructors and return errors:
398+
399+
```go
400+
// ✅ DO: Validate and return error
401+
func NewDynamicPricer(config Config) (*DynamicPricer, error) {
402+
if len(config.Prices) == 0 {
403+
return nil, fmt.Errorf("config.Prices cannot be empty")
404+
}
405+
return &DynamicPricer{config: config}, nil
406+
}
407+
408+
// ❌ DON'T: Panic on invalid input or allow invalid state
409+
func NewDynamicPricer(config Config) *DynamicPricer {
410+
// Will panic on first use if Prices is empty
411+
return &DynamicPricer{config: config}
412+
}
413+
```
414+
415+
### Documentation Must Match Implementation
416+
Keep documentation (especially README files) synchronized with code:
417+
418+
**Critical to document:**
419+
- Function signatures including context parameters and return types
420+
- Error return conditions (not just success path)
421+
- Async/timing behavior (e.g., "vault has zero budget until first block header received")
422+
- Cancellation behavior
423+
424+
**Example:**
425+
```markdown
426+
<!-- ✅ DO: Accurate signature and behavior -->
427+
- **`SpendOrWaitAvailableBudget(context.Context, uint64) error`**:
428+
Attempts to spend gas; blocks if insufficient budget is available or until context is cancelled.
429+
Returns nil on success, ctx.Err() if cancelled.
430+
431+
<!-- ❌ DON'T: Outdated or incomplete -->
432+
- **`SpendOrWaitAvailableBudget(uint64)`**:
433+
Attempts to spend gas; blocks if insufficient budget is available.
434+
```
435+
436+
### Test Coverage Requirements
437+
When adding new components, always include comprehensive unit tests:
438+
439+
**Required test categories:**
440+
- Basic functionality (creation, getters, setters)
441+
- Edge cases (zero values, nil inputs, overflow, division by zero)
442+
- Error conditions (invalid configs, failed operations)
443+
- Concurrency (multiple goroutines, race conditions)
444+
- Context cancellation (immediate cancel, timeout, graceful shutdown)
445+
- Blocking/waiting behavior (wait then succeed, wait then timeout)
446+
447+
**Pattern for concurrent tests:**
448+
```go
449+
func TestConcurrentAccess(t *testing.T) {
450+
component := NewComponent()
451+
const numGoroutines = 100
452+
var wg sync.WaitGroup
453+
454+
for i := 0; i < numGoroutines; i++ {
455+
wg.Add(1)
456+
go func() {
457+
defer wg.Done()
458+
// Perform concurrent operations
459+
}()
460+
}
461+
wg.Wait()
462+
// Verify final state
463+
}
464+
```
465+
320466
## Code Style
321467

322468
### Cobra Flags

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ Note: Do not modify this section! It is auto-generated by `cobra` using `make ge
8383

8484
- [polycli parseethwallet](doc/polycli_parseethwallet.md) - Extract the private key from an eth wallet.
8585

86+
- [polycli plot](doc/polycli_plot.md) - Plot a chart of transaction gas prices and limits.
87+
8688
- [polycli publish](doc/polycli_publish.md) - Publish transactions to the network with high-throughput.
8789

8890
- [polycli report](doc/polycli_report.md) - Generate a report analyzing a range of blocks from an Ethereum-compatible blockchain.

cmd/loadtest/cmd.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ var cfg = &config.Config{
3434
// uniswapCfg holds UniswapV3-specific configuration.
3535
var uniswapCfg = &config.UniswapV3Config{}
3636

37+
// gasManagerCfg holds gas manager configuration.
38+
var gasManagerCfg = &config.GasManagerConfig{}
39+
3740
// LoadtestCmd represents the loadtest command.
3841
var LoadtestCmd = &cobra.Command{
3942
Use: "loadtest",
@@ -55,6 +58,8 @@ var LoadtestCmd = &cobra.Command{
5558
return cfg.Validate()
5659
},
5760
RunE: func(cmd *cobra.Command, args []string) error {
61+
// Attach gas manager config.
62+
cfg.GasManager = gasManagerCfg
5863
return loadtest.Run(cmd.Context(), cfg)
5964
},
6065
}
@@ -69,9 +74,10 @@ var uniswapv3Cmd = &cobra.Command{
6974
return uniswapCfg.Validate()
7075
},
7176
RunE: func(cmd *cobra.Command, args []string) error {
72-
// Override mode to uniswapv3 and attach UniswapV3 config.
77+
// Override mode to uniswapv3 and attach configs.
7378
cfg.Modes = []string{"v3"}
7479
cfg.UniswapV3 = uniswapCfg
80+
cfg.GasManager = gasManagerCfg
7581

7682
return loadtest.Run(cmd.Context(), cfg)
7783
},
@@ -115,6 +121,24 @@ func initPersistentFlags() {
115121
pf.BoolVar(&cfg.LegacyTxMode, "legacy", false, "send a legacy transaction instead of an EIP1559 transaction")
116122
pf.BoolVar(&cfg.FireAndForget, "fire-and-forget", false, "send transactions and load without waiting for it to be mined")
117123
pf.BoolVar(&cfg.FireAndForget, "send-only", false, "alias for --fire-and-forget")
124+
125+
initGasManagerFlags()
126+
}
127+
128+
func initGasManagerFlags() {
129+
pf := LoadtestCmd.PersistentFlags()
130+
131+
// Oscillation wave
132+
pf.StringVar(&gasManagerCfg.OscillationWave, "gas-manager-oscillation-wave", "flat", "type of oscillation wave (flat | sine | square | triangle | sawtooth)")
133+
pf.Uint64Var(&gasManagerCfg.Target, "gas-manager-target", 30_000_000, "target gas limit for oscillation wave")
134+
pf.Uint64Var(&gasManagerCfg.Period, "gas-manager-period", 1, "period in blocks for oscillation wave")
135+
pf.Uint64Var(&gasManagerCfg.Amplitude, "gas-manager-amplitude", 0, "amplitude for oscillation wave")
136+
137+
// Pricing strategy
138+
pf.StringVar(&gasManagerCfg.PriceStrategy, "gas-manager-price-strategy", "estimated", "gas price strategy (estimated | fixed | dynamic)")
139+
pf.Uint64Var(&gasManagerCfg.FixedGasPriceWei, "gas-manager-fixed-gas-price-wei", 300000000, "fixed gas price in wei")
140+
pf.StringVar(&gasManagerCfg.DynamicGasPricesWei, "gas-manager-dynamic-gas-prices-wei", "0,1000000,0,10000000,0,100000000", "comma-separated gas prices in wei for dynamic strategy")
141+
pf.Float64Var(&gasManagerCfg.DynamicGasPricesVariation, "gas-manager-dynamic-gas-prices-variation", 0.3, "variation percentage for dynamic strategy")
118142
}
119143

120144
func initFlags() {

cmd/loadtest/loadtestUsage.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,24 @@ Here is a simple example that runs 1000 requests at a max rate of 1 request per
5252
$ polycli loadtest --verbosity 700 --chain-id 1256 --concurrency 1 --requests 1000 --rate-limit 1 --mode t --rpc-url http://localhost:8888
5353
```
5454

55+
### Gas Manager
56+
57+
The loadtest command includes a gas manager for controlling transaction gas limits and pricing. Use the `--gas-manager-*` flags to:
58+
59+
- **Oscillate gas limits** with wave patterns (flat, sine, square, triangle, sawtooth)
60+
- **Control gas pricing** with strategies (estimated, fixed, dynamic)
61+
62+
Example with sine wave oscillation:
63+
```bash
64+
$ polycli loadtest --rpc-url http://localhost:8545 \
65+
--gas-manager-oscillation-wave sine \
66+
--gas-manager-target 20000000 \
67+
--gas-manager-amplitude 10000000 \
68+
--gas-manager-period 100
69+
```
70+
71+
See [Gas Manager README](../../loadtest/gasmanager/README.md) for detailed documentation.
72+
5573
### Load Test Contract
5674

5775
The codebase has a contract that used for load testing. It's written in Solidity. The workflow for modifying this contract is.

0 commit comments

Comments
 (0)