Skip to content
Merged
Changes from 1 commit
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
81dc810
refactor: move spamoor benchmark into testify suite in test/e2e/bench…
chatton Feb 25, 2026
cc56590
merge main into cian/bench-refactor
chatton Feb 25, 2026
18fc15a
fix: correct BENCH_JSON_OUTPUT path for spamoor benchmark
chatton Feb 25, 2026
fccd9db
fix: place package pattern before test binary flags in benchmark CI
chatton Feb 25, 2026
ae525ca
fix: adjust evm-binary path for benchmark subpackage working directory
chatton Feb 25, 2026
039eaf7
wip: erc20 benchmark test
chatton Feb 25, 2026
85c9d2d
fix: exclude benchmark subpackage from make test-e2e
chatton Feb 25, 2026
fe8d166
fix: replace FilterLogs with header iteration and optimize spamoor co…
chatton Feb 26, 2026
99e3e42
fix: improve benchmark measurement window and reliability
chatton Feb 26, 2026
e4e06c5
fix: address PR review feedback for benchmark suite
chatton Mar 2, 2026
1c3b560
Merge branch 'main' into cian/bench-refactor
chatton Mar 2, 2026
03b9239
chore: specify http
chatton Mar 2, 2026
fe3ca23
chore: filter out benchmark tests from test-e2e
chatton Mar 2, 2026
8752fee
Merge branch 'main' into cian/bench-refactor
chatton Mar 2, 2026
06f532b
Merge branch 'cian/bench-refactor' into cian/erc20-benchmark
chatton Mar 2, 2026
560974a
Merge remote-tracking branch 'origin/main' into cian/erc20-benchmark
chatton Mar 2, 2026
054f2c6
refactor: centralize reth config and lower ERC20 spammer count
chatton Mar 2, 2026
26bb117
chore: collect all traces at once
chatton Mar 2, 2026
b88cae3
chore: self review
chatton Mar 2, 2026
272ab71
refactor: extract benchmark helpers to slim down ERC20 test body
chatton Mar 2, 2026
676e0d1
docs: add detailed documentation to benchmark helper methods
chatton Mar 2, 2026
f4949a1
ci: add ERC20 throughput benchmark job
chatton Mar 2, 2026
4c7b7e1
chore: remove span assertions
chatton Mar 2, 2026
6f56c80
chore: adding gas burner test
chatton Mar 2, 2026
40ff689
feat: allowing for env vars to be set for external resources
chatton Mar 3, 2026
4a7af54
chore: adding private key as env var
chatton Mar 3, 2026
d2aeebc
chore: use host networking for spamoor in external mode
chatton Mar 4, 2026
7a85f3b
fix: pin tastora to correct commit with host network support
chatton Mar 4, 2026
f42ffe0
chore: change service name
chatton Mar 4, 2026
53d12e1
chore: scope victoria traces queries to current test run
chatton Mar 4, 2026
bdd7132
chore: paginate victoria traces queries
chatton Mar 4, 2026
0fe81a1
chore: reduce tryCollectSpans timeout to 15s for victoria provider
chatton Mar 4, 2026
2756ce3
chore: increase gas burner spammers from 4 to 8
chatton Mar 4, 2026
8802838
chore: revert gas burner spammers back to 4
chatton Mar 4, 2026
2f27ab9
chore: switch victoria traces to LogsQL API
chatton Mar 4, 2026
67c00d3
fix: use _stream filter syntax for LogsQL query
chatton Mar 4, 2026
f7a8c5f
chore: tune gasburner config for 100M gas limit blocks
chatton Mar 4, 2026
1308af4
chore: match spamoor slot-duration to deployed 250ms block time
chatton Mar 4, 2026
3b4f5d1
chore: increase gasburner tx count for sustained load
chatton Mar 4, 2026
c0de7a3
chore: use 8 spammers for sustained gasburner load
chatton Mar 4, 2026
f5d0eaa
chore: revert to 4 spammers to avoid nonce collisions
chatton Mar 4, 2026
6c4ca40
chore: bump gasburner fees to 1000/500 gwei to clear stale txs
chatton Mar 4, 2026
2d4ab9a
chore: adding support for rich flowchart display
chatton Mar 5, 2026
9bd2381
adding host name to span
chatton Mar 10, 2026
dffc429
chore: add re-broadcast
chatton Mar 10, 2026
7fac0a3
chore: adding flowchart
chatton Mar 10, 2026
4445cde
chore: bump fee
chatton Mar 10, 2026
64633d8
chore: bump wallet amounts
chatton Mar 10, 2026
f6100a2
chore: increase throughput
chatton Mar 10, 2026
51754b8
chore: 4x tx volume
chatton Mar 10, 2026
f1e68d2
chore: fewer tx, higher gas
chatton Mar 10, 2026
a3325dd
chore: remove amount
chatton Mar 10, 2026
aea69a2
Bumped from 50/20 ETH to 500/200 ETH per wallet
chatton Mar 11, 2026
1468615
Merge remote-tracking branch 'origin/main' into cian/enable-tests-to-…
chatton Mar 12, 2026
ae26530
feat: parameterize benchmark config via env vars and add engine span …
chatton Mar 12, 2026
8866358
refactor: extract benchConfig and benchmarkResult types
chatton Mar 12, 2026
87348de
chore: cleaning up flowchart dissplay and fixing UI link error
chatton Mar 12, 2026
1990e85
refactor: address PR review feedback on benchmark harness
chatton Mar 18, 2026
0a4c8a4
Merge branch 'main' into cian/enable-tests-to-run-without-infra
chatton Mar 18, 2026
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
Prev Previous commit
Next Next commit
docs: add detailed documentation to benchmark helper methods
  • Loading branch information
chatton committed Mar 2, 2026
commit 676e0d146d4ad909388e5c9355f93c83ee37c88b
123 changes: 83 additions & 40 deletions test/e2e/benchmark/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,53 +17,63 @@ import (
)

// blockMetrics holds aggregated gas and transaction data across a range of blocks.
// Only blocks containing at least one transaction are counted in BlockCount,
// GasPerBlock, and TxPerBlock. All blocks (including empty) are counted in
// TotalBlockCount and contribute to BlockIntervals.
type blockMetrics struct {
StartBlock uint64
EndBlock uint64
BlockCount int // non-empty blocks only
TotalBlockCount int // all blocks in range including empty
TotalGasUsed uint64
TotalTxCount int
GasPerBlock []uint64 // non-empty blocks only
TxPerBlock []int // non-empty blocks only
BlockIntervals []time.Duration // intervals between all consecutive blocks (for drift)
FirstBlockTime time.Time
LastBlockTime time.Time
StartBlock uint64
EndBlock uint64
BlockCount int // non-empty blocks only
TotalBlockCount int // all blocks in range including empty
TotalGasUsed uint64 // cumulative gas across non-empty blocks
TotalTxCount int // cumulative tx count across non-empty blocks
GasPerBlock []uint64 // per-block gas for non-empty blocks only
TxPerBlock []int // per-block tx count for non-empty blocks only
BlockIntervals []time.Duration // time between all consecutive blocks (including empty)
FirstBlockTime time.Time // timestamp of the first non-empty block
LastBlockTime time.Time // timestamp of the last non-empty block
}

// steadyStateDuration returns the time between the first and last active blocks.
// steadyStateDuration returns the wall-clock time between the first and last
// non-empty blocks. This excludes warm-up (empty blocks before the first tx)
// and cool-down (empty blocks after the last tx) from throughput calculations.
func (m *blockMetrics) steadyStateDuration() time.Duration {
if m.FirstBlockTime.IsZero() || m.LastBlockTime.IsZero() {
return 0
}
return m.LastBlockTime.Sub(m.FirstBlockTime)
}

// avgGasPerBlock returns the mean gas used per block.
// avgGasPerBlock returns TotalGasUsed / BlockCount, i.e. the mean gas used
// per non-empty block.
func (m *blockMetrics) avgGasPerBlock() float64 {
if m.BlockCount == 0 {
return 0
}
return float64(m.TotalGasUsed) / float64(m.BlockCount)
}

// avgTxPerBlock returns the mean transaction count per block.
// avgTxPerBlock returns TotalTxCount / BlockCount, i.e. the mean transaction
// count per non-empty block.
func (m *blockMetrics) avgTxPerBlock() float64 {
if m.BlockCount == 0 {
return 0
}
return float64(m.TotalTxCount) / float64(m.BlockCount)
}

// nonEmptyRatio returns the percentage of blocks that contained transactions.
// nonEmptyRatio returns (BlockCount / TotalBlockCount) * 100, i.e. the
// percentage of blocks in the range that contained at least one transaction.
func (m *blockMetrics) nonEmptyRatio() float64 {
if m.TotalBlockCount == 0 {
return 0
}
return float64(m.BlockCount) / float64(m.TotalBlockCount) * 100
}

// blockIntervalStats returns p50, p99, and max of block intervals.
// blockIntervalStats computes percentile statistics over the time gaps between
// all consecutive blocks (including empty ones). This measures block production
// cadence and jitter rather than transaction throughput.
func (m *blockMetrics) blockIntervalStats() (p50, p99, max time.Duration) {
if len(m.BlockIntervals) == 0 {
return 0, 0, 0
Expand All @@ -78,7 +88,8 @@ func (m *blockMetrics) blockIntervalStats() (p50, p99, max time.Duration) {
time.Duration(sorted[len(sorted)-1])
}

// gasPerBlockStats returns p50 and p99 of gas used per non-empty block.
// gasPerBlockStats returns the 50th and 99th percentile of gas used across
// non-empty blocks. Shows the distribution of per-block gas consumption.
func (m *blockMetrics) gasPerBlockStats() (p50, p99 float64) {
if len(m.GasPerBlock) == 0 {
return 0, 0
Expand All @@ -91,7 +102,8 @@ func (m *blockMetrics) gasPerBlockStats() (p50, p99 float64) {
return percentile(sorted, 0.50), percentile(sorted, 0.99)
}

// txPerBlockStats returns p50 and p99 of tx count per non-empty block.
// txPerBlockStats returns the 50th and 99th percentile of transaction counts
// across non-empty blocks. Shows the distribution of per-block tx throughput.
func (m *blockMetrics) txPerBlockStats() (p50, p99 float64) {
if len(m.TxPerBlock) == 0 {
return 0, 0
Expand All @@ -104,8 +116,9 @@ func (m *blockMetrics) txPerBlockStats() (p50, p99 float64) {
return percentile(sorted, 0.50), percentile(sorted, 0.99)
}

// percentile returns the p-th percentile from a pre-sorted float64 slice
// using linear interpolation.
// percentile returns the p-th percentile from a pre-sorted float64 slice using
// linear interpolation between adjacent ranks. For example, p=0.50 returns the
// median and p=0.99 returns the value below which 99% of observations fall.
func percentile(sorted []float64, p float64) float64 {
if len(sorted) == 0 {
return 0
Expand All @@ -123,7 +136,8 @@ func percentile(sorted []float64, p float64) float64 {
return sorted[lower] + frac*(sorted[upper]-sorted[lower])
}

// mgasPerSec calculates the achieved throughput in megagas per second.
// mgasPerSec computes totalGasUsed / elapsed / 1e6, giving throughput in
// millions of gas units per second (MGas/s).
func mgasPerSec(totalGasUsed uint64, elapsed time.Duration) float64 {
if elapsed <= 0 {
return 0
Expand Down Expand Up @@ -227,25 +241,41 @@ func waitForDrain(ctx context.Context, log func(string, ...any), client *ethclie
}
}

// blockMetricsSummary holds the computed stats from a blockMetrics measurement window.
// blockMetricsSummary holds all derived statistics from a blockMetrics measurement
// window. Every field is computed from the raw block data by summarize().
type blockMetricsSummary struct {
SteadyState time.Duration
AchievedMGas float64
AchievedTPS float64
IntervalP50 time.Duration
IntervalP99 time.Duration
IntervalMax time.Duration
GasP50 float64
GasP99 float64
TxP50 float64
TxP99 float64
AvgGas float64
AvgTx float64
BlocksPerSec float64
// SteadyState is the wall-clock duration between the first and last non-empty
// blocks, used as the denominator for throughput calculations.
SteadyState time.Duration
// AchievedMGas is total gas / steady-state seconds / 1e6 (megagas per second).
AchievedMGas float64
// AchievedTPS is total tx count / steady-state seconds.
AchievedTPS float64
// IntervalP50, IntervalP99, IntervalMax are percentile and max statistics
// over the time between all consecutive blocks (including empty).
IntervalP50 time.Duration
IntervalP99 time.Duration
IntervalMax time.Duration
// GasP50, GasP99 are the 50th/99th percentile of gas used per non-empty block.
GasP50 float64
GasP99 float64
// TxP50, TxP99 are the 50th/99th percentile of tx count per non-empty block.
TxP50 float64
TxP99 float64
// AvgGas is the mean gas per non-empty block (TotalGasUsed / BlockCount).
AvgGas float64
// AvgTx is the mean tx count per non-empty block (TotalTxCount / BlockCount).
AvgTx float64
// BlocksPerSec is non-empty blocks / steady-state seconds.
BlocksPerSec float64
// NonEmptyRatio is (non-empty blocks / total blocks) * 100.
NonEmptyRatio float64
}

// summarize computes all derived stats from the raw block metrics.
// summarize computes all derived statistics from the raw block-level data in one
// pass. It delegates to the individual stat methods (steadyStateDuration,
// blockIntervalStats, gasPerBlockStats, etc.) and packages the results into a
// blockMetricsSummary for logging and result-writing.
func (m *blockMetrics) summarize() *blockMetricsSummary {
ss := m.steadyStateDuration()
intervalP50, intervalP99, intervalMax := m.blockIntervalStats()
Expand Down Expand Up @@ -275,7 +305,10 @@ func (m *blockMetrics) summarize() *blockMetricsSummary {
}
}

// log prints a concise summary of the block metrics to the test log.
// log prints the block range, interval stats, per-block gas/tx stats, and
// throughput (MGas/s + TPS) to the test log. startBlock/endBlock and
// totalBlocks/nonEmptyBlocks are passed separately because they live on the
// raw blockMetrics, not the summary.
func (s *blockMetricsSummary) log(t testing.TB, startBlock, endBlock uint64, totalBlocks, nonEmptyBlocks int, wallClock time.Duration) {
t.Logf("block range: %d-%d (%d total, %d non-empty, %.1f%% non-empty)",
startBlock, endBlock, totalBlocks, nonEmptyBlocks, s.NonEmptyRatio)
Expand All @@ -287,7 +320,10 @@ func (s *blockMetricsSummary) log(t testing.TB, startBlock, endBlock uint64, tot
s.AchievedMGas, s.AchievedTPS, s.SteadyState.Round(time.Millisecond), wallClock.Round(time.Millisecond))
}

// entries returns all summary metrics as result writer entries with the given prefix.
// entries returns all summary metrics as result writer entries in the
// customSmallerIsBetter format expected by github-action-benchmark. Each entry
// is prefixed with the given label (e.g. "ERC20Throughput") so results from
// different tests are distinguishable in the same output file.
func (s *blockMetricsSummary) entries(prefix string) []entry {
return []entry{
{Name: prefix + " - MGas/s", Unit: "MGas/s", Value: s.AchievedMGas},
Expand All @@ -305,8 +341,15 @@ func (s *blockMetricsSummary) entries(prefix string) []entry {
}
}

// evNodeOverhead computes the overhead percentage of ev-node's ProduceBlock
// over the inner ExecuteTxs span. Returns the overhead and whether it was computable.
// evNodeOverhead computes the fraction of block production time spent outside
// EVM execution. It looks up the average durations of BlockExecutor.ProduceBlock
// (the outer span covering the full block lifecycle) and Executor.ExecuteTxs
// (the inner span covering only EVM tx execution), then returns:
//
// overhead% = (avgProduce - avgExecute) / avgProduce * 100
//
// This captures time spent on sequencing, DA submission, header construction,
// and other ev-node orchestration work. Returns false if either span is missing.
func evNodeOverhead(spans []e2e.TraceSpan) (float64, bool) {
stats := e2e.AggregateSpanStats(spans)
produce, ok := stats["BlockExecutor.ProduceBlock"]
Expand Down
Loading