Skip to content

Commit aa382b7

Browse files
feat(benchmark): Add CodSpeed benchmark suite (#219)
## Summary - add CodSpeed benchmarks and shared data/helpers - add CodSpeed workflow for sharded benchmark runs - add codspeed/pytest requirements files ## Testing - `PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 pytest -p pytest_codspeed.plugin benchmarks/ --codspeed`
1 parent 48beef3 commit aa382b7

13 files changed

+370
-2
lines changed

.github/workflows/benchmark.yaml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: Benchmark Suite
2+
3+
on:
4+
pull_request:
5+
branches: [master]
6+
paths:
7+
- ".github/workflows/benchmark.yaml"
8+
- "evmspec/**"
9+
- "benchmarks/**"
10+
- "setup.py"
11+
- "pyproject.toml"
12+
- "requirements-codspeed.txt"
13+
- "requirements-pytest.txt"
14+
push:
15+
branches: [master]
16+
paths:
17+
- ".github/workflows/benchmark.yaml"
18+
- "evmspec/**"
19+
- "benchmarks/**"
20+
- "setup.py"
21+
- "pyproject.toml"
22+
- "requirements-codspeed.txt"
23+
- "requirements-pytest.txt"
24+
workflow_dispatch:
25+
26+
concurrency:
27+
group: ${{ github.workflow }}-${{ github.ref }}
28+
cancel-in-progress: true
29+
30+
jobs:
31+
codspeed:
32+
name: Run CodSpeed Benchmarks
33+
runs-on: ubuntu-latest
34+
strategy:
35+
fail-fast: false
36+
steps:
37+
- uses: actions/checkout@v6
38+
39+
- name: Set up Python
40+
uses: actions/setup-python@v6
41+
with:
42+
python-version: "3.13"
43+
cache: pip
44+
cache-dependency-path: |
45+
pyproject.toml
46+
setup.py
47+
poetry.lock
48+
49+
- name: Install evmspec
50+
run: python -m pip install --upgrade pip && python -m pip install . -r requirements-codspeed.txt
51+
52+
- name: Run CodSpeed
53+
uses: CodSpeedHQ/action@v4
54+
with:
55+
mode: instrumentation
56+
run: pytest --codspeed benchmarks/

.github/workflows/pytest.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,5 +68,4 @@ jobs:
6868
env:
6969
PYTEST_NETWORK: mainnet
7070
ETHERSCAN_TOKEN: ${{ secrets.ETHERSCAN_TOKEN }}
71-
run: |
72-
poetry run pytest
71+
run: poetry run pytest tests

benchmarks/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Benchmarks for evmspec
2+
3+
This directory contains CodSpeed-friendly benchmarks for key hot paths in `evmspec`:
4+
5+
- `test_address_benchmarks.py`: Address checksum and construction.
6+
- `test_hexbytes_benchmarks.py`: `HexBytes32` construction.
7+
- `test_numeric_benchmarks.py`: `uint` hex parsing.
8+
- `test_decoding_benchmarks.py`: Transaction, receipt, and log decoding.
9+
- `test_block_benchmarks.py`: `TinyBlock.transactions` decoding.
10+
11+
## Running Benchmarks
12+
13+
Install the benchmark dependencies:
14+
15+
```
16+
pip install -r requirements-codspeed.txt
17+
```
18+
19+
Run the suite with CodSpeed:
20+
21+
```
22+
pytest benchmarks/ --codspeed
23+
```
24+
25+
## Contributing
26+
27+
- Add benchmarks for new public APIs or hot decode paths.
28+
- Keep inputs representative of real RPC payloads.
29+
- Each benchmark should stand alone (no paired reference/optimized variant).

benchmarks/__init__.py

Whitespace-only changes.

benchmarks/batch.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from typing import Any, Callable
2+
3+
4+
def batch(iterations: int, func: Callable[..., Any], *args: Any, **kwargs: Any) -> None:
5+
for _ in range(iterations):
6+
func(*args, **kwargs)

benchmarks/data.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import json
2+
3+
ADDRESS_CHECKSUM = "0x52908400098527886E0F7030069857D2E4169EE7"
4+
ADDRESS_LOWER = "0xde709f2102306220921060314715629080e2fb77"
5+
ADDRESS_ZERO = "0x0000000000000000000000000000000000000000"
6+
7+
ADDRESS_CASES = [
8+
ADDRESS_CHECKSUM,
9+
ADDRESS_LOWER,
10+
ADDRESS_ZERO,
11+
]
12+
ADDRESS_CASE_IDS = [
13+
"checksummed",
14+
"lower",
15+
"zero",
16+
]
17+
18+
HASH_1 = "0x" + "11" * 32
19+
HASH_2 = "0x" + "22" * 32
20+
HASH_3 = "0x" + "33" * 32
21+
HASH_4 = "0x" + "44" * 32
22+
23+
TOPIC_0 = "0x" + "aa" * 32
24+
TOPIC_1 = "0x" + "bb" * 32
25+
DATA_32 = "0x" + "00" * 32
26+
27+
HEXBYTES32_CASES = [
28+
HASH_1,
29+
bytes.fromhex("11" * 32),
30+
]
31+
HEXBYTES32_CASE_IDS = [
32+
"hexstr",
33+
"bytes",
34+
]
35+
36+
UINT_HEX_CASES = [
37+
"0x0",
38+
"0x1",
39+
"0xff",
40+
"0x7fffffffffffffff",
41+
]
42+
UINT_HEX_CASE_IDS = [
43+
"0",
44+
"1",
45+
"ff",
46+
"int64-max",
47+
]
48+
49+
LEGACY_TX = {
50+
"type": "0x0",
51+
"hash": HASH_1,
52+
"nonce": "0x1",
53+
"blockHash": HASH_2,
54+
"blockNumber": "0x10",
55+
"transactionIndex": "0x0",
56+
"from": ADDRESS_CHECKSUM,
57+
"to": ADDRESS_LOWER,
58+
"value": "0x0",
59+
"gas": "0x5208",
60+
"gasPrice": "0x3b9aca00",
61+
"input": "0x",
62+
"v": "0x1b",
63+
"r": HASH_3,
64+
"s": HASH_4,
65+
}
66+
67+
EIP1559_TX = {
68+
"type": "0x2",
69+
"hash": HASH_2,
70+
"nonce": "0x2",
71+
"blockHash": HASH_3,
72+
"blockNumber": "0x10",
73+
"transactionIndex": "0x1",
74+
"from": ADDRESS_LOWER,
75+
"to": ADDRESS_CHECKSUM,
76+
"value": "0x1",
77+
"gas": "0x5208",
78+
"gasPrice": "0x3b9aca00",
79+
"maxFeePerGas": "0x77359400",
80+
"maxPriorityFeePerGas": "0x3b9aca00",
81+
"input": "0x",
82+
"v": "0x0",
83+
"r": HASH_1,
84+
"s": HASH_2,
85+
"yParity": "0x1",
86+
"accessList": [],
87+
}
88+
89+
TX_LIST = [
90+
LEGACY_TX,
91+
EIP1559_TX,
92+
]
93+
94+
BLOCK_WITH_TXS = {
95+
"timestamp": "0x65f2",
96+
"transactions": TX_LIST,
97+
}
98+
99+
BLOCK_WITH_HASHES = {
100+
"timestamp": "0x65f2",
101+
"transactions": [HASH_1, HASH_2],
102+
}
103+
104+
LOG = {
105+
"address": ADDRESS_CHECKSUM,
106+
"topics": [TOPIC_0, TOPIC_1],
107+
"data": DATA_32,
108+
"removed": False,
109+
"blockNumber": "0x10",
110+
"transactionHash": HASH_1,
111+
"logIndex": "0x1",
112+
"transactionIndex": "0x0",
113+
}
114+
115+
RECEIPT = {
116+
"transactionHash": HASH_1,
117+
"blockNumber": "0x10",
118+
"contractAddress": None,
119+
"transactionIndex": "0x0",
120+
"status": "0x1",
121+
"gasUsed": "0x5208",
122+
"cumulativeGasUsed": "0x5208",
123+
"logs": [LOG],
124+
"effectiveGasPrice": "0x3b9aca00",
125+
"type": "0x2",
126+
}
127+
128+
129+
def _dump(obj: object) -> bytes:
130+
return json.dumps(obj, separators=(",", ":")).encode()
131+
132+
133+
RAW_LEGACY_TX = _dump(LEGACY_TX)
134+
RAW_EIP1559_TX = _dump(EIP1559_TX)
135+
RAW_TX_LIST = _dump(TX_LIST)
136+
RAW_BLOCK_WITH_TXS = _dump(BLOCK_WITH_TXS)
137+
RAW_BLOCK_WITH_HASHES = _dump(BLOCK_WITH_HASHES)
138+
RAW_LOG = _dump(LOG)
139+
RAW_RECEIPT = _dump(RECEIPT)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# mypy: disable-error-code=misc
2+
import pytest
3+
from pytest_codspeed import BenchmarkFixture
4+
5+
from benchmarks.batch import batch
6+
from benchmarks.data import ADDRESS_CASES, ADDRESS_CASE_IDS
7+
from evmspec.data import Address
8+
9+
10+
@pytest.mark.benchmark(group="address_checksum")
11+
@pytest.mark.parametrize("address", ADDRESS_CASES, ids=ADDRESS_CASE_IDS)
12+
def test_address_checksum(benchmark: BenchmarkFixture, address: str) -> None:
13+
benchmark(batch, 100, Address.checksum, address)
14+
15+
16+
@pytest.mark.benchmark(group="address_construct")
17+
@pytest.mark.parametrize("address", ADDRESS_CASES, ids=ADDRESS_CASE_IDS)
18+
def test_address_construct(benchmark: BenchmarkFixture, address: str) -> None:
19+
benchmark(batch, 100, Address, address)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# mypy: disable-error-code=misc
2+
import pytest
3+
from msgspec.json import Decoder
4+
from pytest_codspeed import BenchmarkFixture
5+
6+
from benchmarks.batch import batch
7+
from benchmarks.data import RAW_BLOCK_WITH_HASHES, RAW_BLOCK_WITH_TXS
8+
from evmspec.data import _decode_hook
9+
from evmspec.structs.block import TinyBlock
10+
11+
_decode_tinyblock = Decoder(type=TinyBlock, dec_hook=_decode_hook).decode
12+
13+
14+
def _decode_block_transactions(raw: bytes) -> None:
15+
block = _decode_tinyblock(raw)
16+
block.transactions
17+
18+
19+
@pytest.mark.benchmark(group="tinyblock_transactions")
20+
@pytest.mark.parametrize(
21+
"raw",
22+
[RAW_BLOCK_WITH_TXS, RAW_BLOCK_WITH_HASHES],
23+
ids=["full-transactions", "hashes-only"],
24+
)
25+
def test_tinyblock_transactions(benchmark: BenchmarkFixture, raw: bytes) -> None:
26+
benchmark(batch, 20, _decode_block_transactions, raw)
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# mypy: disable-error-code=misc
2+
import pytest
3+
from msgspec.json import Decoder
4+
from pytest_codspeed import BenchmarkFixture
5+
6+
from benchmarks.batch import batch
7+
from benchmarks.data import (
8+
RAW_EIP1559_TX,
9+
RAW_LEGACY_TX,
10+
RAW_LOG,
11+
RAW_RECEIPT,
12+
RAW_TX_LIST,
13+
)
14+
from evmspec.data import _decode_hook
15+
from evmspec.structs.log import Log
16+
from evmspec.structs.receipt import TransactionReceipt
17+
from evmspec.structs.transaction import Transaction, Transaction1559, TransactionLegacy
18+
19+
_decode_legacy_tx = Decoder(type=TransactionLegacy, dec_hook=_decode_hook).decode
20+
_decode_1559_tx = Decoder(type=Transaction1559, dec_hook=_decode_hook).decode
21+
_decode_transactions = Decoder(type=tuple[Transaction, ...], dec_hook=_decode_hook).decode
22+
_decode_receipt = Decoder(type=TransactionReceipt, dec_hook=_decode_hook).decode
23+
_decode_log = Decoder(type=Log, dec_hook=_decode_hook).decode
24+
25+
26+
def _decode_1559_access_list(raw: bytes) -> None:
27+
tx = _decode_1559_tx(raw)
28+
tx.accessList
29+
30+
31+
def _decode_receipt_logs(raw: bytes) -> None:
32+
receipt = _decode_receipt(raw)
33+
receipt.logs
34+
35+
36+
@pytest.mark.benchmark(group="decode_tx_legacy")
37+
def test_decode_tx_legacy(benchmark: BenchmarkFixture) -> None:
38+
benchmark(batch, 100, _decode_legacy_tx, RAW_LEGACY_TX)
39+
40+
41+
@pytest.mark.benchmark(group="decode_tx_1559")
42+
def test_decode_tx_1559(benchmark: BenchmarkFixture) -> None:
43+
benchmark(batch, 100, _decode_1559_tx, RAW_EIP1559_TX)
44+
45+
46+
@pytest.mark.benchmark(group="decode_tx_list")
47+
def test_decode_tx_list(benchmark: BenchmarkFixture) -> None:
48+
benchmark(batch, 50, _decode_transactions, RAW_TX_LIST)
49+
50+
51+
@pytest.mark.benchmark(group="decode_tx_access_list")
52+
def test_decode_tx_access_list(benchmark: BenchmarkFixture) -> None:
53+
benchmark(batch, 50, _decode_1559_access_list, RAW_EIP1559_TX)
54+
55+
56+
@pytest.mark.benchmark(group="decode_log")
57+
def test_decode_log(benchmark: BenchmarkFixture) -> None:
58+
benchmark(batch, 200, _decode_log, RAW_LOG)
59+
60+
61+
@pytest.mark.benchmark(group="decode_receipt_logs")
62+
def test_decode_receipt_logs(benchmark: BenchmarkFixture) -> None:
63+
benchmark(batch, 50, _decode_receipt_logs, RAW_RECEIPT)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# mypy: disable-error-code=misc
2+
import pytest
3+
from pytest_codspeed import BenchmarkFixture
4+
5+
from benchmarks.batch import batch
6+
from benchmarks.data import HEXBYTES32_CASES, HEXBYTES32_CASE_IDS
7+
from evmspec.data import HexBytes32
8+
9+
10+
@pytest.mark.benchmark(group="hexbytes32_construct")
11+
@pytest.mark.parametrize("value", HEXBYTES32_CASES, ids=HEXBYTES32_CASE_IDS)
12+
def test_hexbytes32_construct(benchmark: BenchmarkFixture, value: bytes | str) -> None:
13+
benchmark(batch, 100, HexBytes32, value)

0 commit comments

Comments
 (0)