Skip to content

Commit 005e06c

Browse files
authored
feat: add TestDeFiSimulation benchmark for Uniswap V2 workload (#3187)
* feat: add TestDeFiSimulation benchmark for Uniswap V2 workload * refactor: use benchConfig for TestDeFiSimulation spamoor parameters Replace hardcoded spammer config with benchConfig fields so all parameters are controllable via BENCH_* env vars. Add pair_count and rebroadcast as configurable options for the uniswap-swaps scenario. * ci: add DeFi simulation benchmark to CI workflow
1 parent a44ae05 commit 005e06c

2 files changed

Lines changed: 135 additions & 0 deletions

File tree

.github/workflows/benchmark.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,28 @@ jobs:
100100
-run='^TestSpamoorSuite$/^TestERC20Throughput$' -v -timeout=15m \
101101
./benchmark/ --evm-binary=../../../build/evm
102102
103+
defi-benchmark:
104+
name: DeFi Simulation Benchmark
105+
runs-on: ubuntu-latest
106+
timeout-minutes: 30
107+
steps:
108+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
109+
- name: Set up Go
110+
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
111+
with:
112+
go-version-file: ./go.mod
113+
- name: Set up Docker Buildx
114+
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
115+
- name: Install just
116+
uses: extractions/setup-just@v3
117+
- name: Build binaries
118+
run: just build-evm build-da
119+
- name: Run DeFi simulation test
120+
run: |
121+
cd test/e2e && go test -tags evm \
122+
-run='^TestSpamoorSuite$/^TestDeFiSimulation$' -v -timeout=15m \
123+
./benchmark/ --evm-binary=../../../build/evm
124+
103125
# single job to push all results to gh-pages sequentially, avoiding race conditions
104126
publish-benchmarks:
105127
name: Publish Benchmark Results
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
//go:build evm
2+
3+
package benchmark
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"time"
9+
10+
"github.com/celestiaorg/tastora/framework/docker/evstack/spamoor"
11+
)
12+
13+
// TestDeFiSimulation measures throughput under a realistic DeFi workload using
14+
// Uniswap V2 swaps. Each tx involves deep call chains, event emission, and
15+
// multi-contract storage operations — representative of production L2 traffic.
16+
//
17+
// Shares system configuration with TestERC20Throughput (100ms blocks, 100M gas,
18+
// 25ms scrape) so the only variable is workload type. The gap between ERC20
19+
// and Uniswap MGas/s shows the cost of workload complexity.
20+
//
21+
// Primary metrics: MGas/s, TPS.
22+
// Diagnostic metrics: per-span latency breakdown, ev-node overhead %.
23+
func (s *SpamoorSuite) TestDeFiSimulation() {
24+
cfg := newBenchConfig("ev-node-defi")
25+
26+
t := s.T()
27+
ctx := t.Context()
28+
cfg.log(t)
29+
w := newResultWriter(t, "DeFiSimulation")
30+
defer w.flush()
31+
32+
e := s.setupEnv(cfg)
33+
34+
uniswapConfig := map[string]any{
35+
"throughput": cfg.Throughput,
36+
"total_count": cfg.CountPerSpammer,
37+
"max_pending": 50000,
38+
"max_wallets": cfg.MaxWallets,
39+
"pair_count": envInt("BENCH_PAIR_COUNT", 1),
40+
"rebroadcast": envInt("BENCH_REBROADCAST", 0),
41+
"base_fee": 20,
42+
"tip_fee": 2,
43+
"refill_amount": "10000000000000000000", // 10 ETH (swaps need ETH for WETH wrapping and router approvals)
44+
"refill_balance": "5000000000000000000", // 5 ETH
45+
"refill_interval": 600,
46+
}
47+
48+
s.Require().NoError(deleteAllSpammers(e.spamoorAPI), "failed to delete stale spammers")
49+
50+
// launch all spammers before recording startBlock so warm-up
51+
// (Uniswap contract deploys + liquidity provision + wallet funding)
52+
// is excluded from the measurement window.
53+
var spammerIDs []int
54+
for i := range cfg.NumSpammers {
55+
name := fmt.Sprintf("bench-defi-%d", i)
56+
id, err := e.spamoorAPI.CreateSpammer(name, spamoor.ScenarioUniswapSwaps, uniswapConfig, true)
57+
s.Require().NoError(err, "failed to create spammer %s", name)
58+
spammerIDs = append(spammerIDs, id)
59+
t.Cleanup(func() { _ = e.spamoorAPI.DeleteSpammer(id) })
60+
}
61+
62+
// give spammers time to deploy contracts and provision liquidity,
63+
// then verify none failed during warmup.
64+
time.Sleep(5 * time.Second)
65+
requireSpammersRunning(t, e.spamoorAPI, spammerIDs)
66+
67+
// wait for warmup transactions (contract deploys, liquidity adds) to land
68+
// before recording start block.
69+
pollSentTotal := func() (float64, error) {
70+
metrics, mErr := e.spamoorAPI.GetMetrics()
71+
if mErr != nil {
72+
return 0, mErr
73+
}
74+
return sumCounter(metrics["spamoor_transactions_sent_total"]), nil
75+
}
76+
waitForMetricTarget(t, "spamoor_transactions_sent_total (warmup)", pollSentTotal, float64(cfg.WarmupTxs), cfg.WaitTimeout)
77+
78+
// reset trace window to exclude warmup spans
79+
e.traces.resetStartTime()
80+
81+
startHeader, err := e.ethClient.HeaderByNumber(ctx, nil)
82+
s.Require().NoError(err, "failed to get start block header")
83+
startBlock := startHeader.Number.Uint64()
84+
loadStart := time.Now()
85+
t.Logf("start block: %d (after warmup)", startBlock)
86+
87+
// wait for all transactions to be sent
88+
waitForMetricTarget(t, "spamoor_transactions_sent_total", pollSentTotal, float64(cfg.totalCount()), cfg.WaitTimeout)
89+
90+
// wait for pending txs to drain
91+
drainCtx, drainCancel := context.WithTimeout(ctx, 30*time.Second)
92+
defer drainCancel()
93+
if err := waitForDrain(drainCtx, t.Logf, e.ethClient, 10); err != nil {
94+
t.Logf("warning: %v", err)
95+
}
96+
wallClock := time.Since(loadStart)
97+
98+
endHeader, err := e.ethClient.HeaderByNumber(ctx, nil)
99+
s.Require().NoError(err, "failed to get end block header")
100+
endBlock := endHeader.Number.Uint64()
101+
t.Logf("end block: %d (range %d blocks)", endBlock, endBlock-startBlock)
102+
103+
// collect block-level gas/tx metrics
104+
bm, err := collectBlockMetrics(ctx, e.ethClient, startBlock, endBlock)
105+
s.Require().NoError(err, "failed to collect block metrics")
106+
107+
traces := s.collectTraces(e, cfg.ServiceName)
108+
109+
result := newBenchmarkResult("DeFiSimulation", bm, traces)
110+
s.Require().Greater(result.summary.SteadyState, time.Duration(0), "expected non-zero steady-state duration")
111+
result.log(t, wallClock)
112+
w.addEntries(result.entries())
113+
}

0 commit comments

Comments
 (0)