Skip to content

Commit 55cd3cb

Browse files
authored
benchmark async vs. sync http (#2002)
1 parent 474a7ef commit 55cd3cb

File tree

10 files changed

+384
-4
lines changed

10 files changed

+384
-4
lines changed

.circleci/config.yml

+9
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,14 @@ jobs:
560560
environment:
561561
TOXENV: py39-wheel-cli
562562

563+
benchmark:
564+
<<: *geth_steps
565+
docker:
566+
- image: circleci/python:3.9
567+
environment:
568+
TOXENV: benchmark
569+
GETH_VERSION: v1.10.1
570+
563571
workflows:
564572
version: 2.1
565573
test:
@@ -571,6 +579,7 @@ workflows:
571579
- py39-core
572580
- lint
573581
- docs
582+
- benchmark
574583
- py36-ens
575584
- py36-ethpm
576585
- py36-integration-goethereum-ipc

Makefile

+3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ test:
3434
test-all:
3535
tox
3636

37+
benchmark:
38+
tox -e benchmark
39+
3740
build-docs:
3841
sphinx-apidoc -o docs/ . setup.py "web3/utils/*" "*conftest*" "tests" "ethpm"
3942
$(MAKE) -C docs clean

newsfragments/2002.misc.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add basic provider benchmarking infrastructure

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"click>=5.1",
2222
"configparser==3.5.0",
2323
"contextlib2>=0.5.4",
24-
"py-geth>=2.4.0,<3",
24+
"py-geth>=3.0.0,<4",
2525
"py-solc>=0.4.0",
2626
"pytest>=4.4.0,<5.0.0",
2727
"sphinx>=3.0,<4",

tox.ini

+11-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ envlist=
66
py{36,37,38,39}-integration-{goethereum,ethtester,parity}
77
lint
88
docs
9+
benchmark
910
py{36,37,38,39}-wheel-cli
1011

1112
[isort]
@@ -63,9 +64,16 @@ basepython =
6364
basepython=python
6465
extras=linter
6566
commands=
66-
flake8 {toxinidir}/web3 {toxinidir}/ens {toxinidir}/ethpm {toxinidir}/tests --exclude {toxinidir}/ethpm/ethpm-spec
67-
isort --recursive --check-only --diff {toxinidir}/web3/ {toxinidir}/ens/ {toxinidir}/ethpm/ {toxinidir}/tests/
68-
mypy -p web3 -p ethpm -p ens --config-file {toxinidir}/mypy.ini
67+
flake8 {toxinidir}/web3 {toxinidir}/ens {toxinidir}/ethpm {toxinidir}/tests --exclude {toxinidir}/ethpm/ethpm-spec
68+
isort --recursive --check-only --diff {toxinidir}/web3/ {toxinidir}/ens/ {toxinidir}/ethpm/ {toxinidir}/tests/
69+
mypy -p web3 -p ethpm -p ens --config-file {toxinidir}/mypy.ini
70+
71+
[testenv:benchmark]
72+
basepython=python
73+
commands=
74+
python {toxinidir}/web3/tools/benchmark/main.py --num-calls 5
75+
python {toxinidir}/web3/tools/benchmark/main.py --num-calls 50
76+
python {toxinidir}/web3/tools/benchmark/main.py --num-calls 100
6977

7078
[common-wheel-cli]
7179
deps=wheel

web3/tools/benchmark/__init__.py

Whitespace-only changes.

web3/tools/benchmark/main.py

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import argparse
2+
import asyncio
3+
from collections import (
4+
defaultdict,
5+
)
6+
import logging
7+
import sys
8+
import timeit
9+
from typing import (
10+
Any,
11+
Callable,
12+
Dict,
13+
Union,
14+
)
15+
16+
from web3 import (
17+
AsyncHTTPProvider,
18+
HTTPProvider,
19+
Web3,
20+
)
21+
from web3.eth import (
22+
AsyncEth,
23+
)
24+
from web3.tools.benchmark.node import (
25+
GethBenchmarkFixture,
26+
)
27+
from web3.tools.benchmark.reporting import (
28+
print_entry,
29+
print_footer,
30+
print_header,
31+
)
32+
from web3.tools.benchmark.utils import (
33+
wait_for_aiohttp,
34+
wait_for_http,
35+
)
36+
37+
parser = argparse.ArgumentParser()
38+
parser.add_argument(
39+
"--num-calls", type=int, default=10, help="The number of RPC calls to make",
40+
)
41+
42+
# TODO - layers to test:
43+
# contract.functions.method(...).call()
44+
# w3.eth.call(...)
45+
# HTTPProvider.make_request(...)
46+
47+
48+
def build_web3_http(endpoint_uri: str) -> Web3:
49+
wait_for_http(endpoint_uri)
50+
_web3 = Web3(HTTPProvider(endpoint_uri), middlewares=[])
51+
return _web3
52+
53+
54+
async def build_async_w3_http(endpoint_uri: str) -> Web3:
55+
await wait_for_aiohttp(endpoint_uri)
56+
_web3 = Web3(
57+
AsyncHTTPProvider(endpoint_uri), # type: ignore
58+
middlewares=[],
59+
modules={"async_eth": (AsyncEth,)},
60+
)
61+
return _web3
62+
63+
64+
def sync_benchmark(func: Callable[..., Any], n: int) -> Union[float, str]:
65+
try:
66+
starttime = timeit.default_timer()
67+
for _ in range(n):
68+
func()
69+
endtime = timeit.default_timer()
70+
execution_time = endtime - starttime
71+
return execution_time
72+
except Exception:
73+
return "N/A"
74+
75+
76+
async def async_benchmark(func: Callable[..., Any], n: int) -> Union[float, str]:
77+
try:
78+
starttime = timeit.default_timer()
79+
for result in asyncio.as_completed([func() for _ in range(n)]):
80+
await result
81+
execution_time = timeit.default_timer() - starttime
82+
return execution_time
83+
except Exception:
84+
return "N/A"
85+
86+
87+
def main(logger: logging.Logger, num_calls: int) -> None:
88+
fixture = GethBenchmarkFixture()
89+
for built_fixture in fixture.build():
90+
for process in built_fixture:
91+
w3_http = build_web3_http(fixture.endpoint_uri)
92+
loop = asyncio.get_event_loop()
93+
async_w3_http = loop.run_until_complete(build_async_w3_http(fixture.endpoint_uri))
94+
95+
methods = [
96+
{
97+
"name": "eth_gasPrice",
98+
"params": {},
99+
"exec": lambda: w3_http.eth.gas_price,
100+
"async_exec": lambda: async_w3_http.async_eth.gas_price,
101+
},
102+
{
103+
"name": "eth_blockNumber",
104+
"params": {},
105+
"exec": lambda: w3_http.eth.block_number,
106+
"async_exec": lambda: (_ for _ in ()).throw(Exception("not implemented yet")),
107+
},
108+
{
109+
"name": "eth_getBlock",
110+
"params": {},
111+
"exec": lambda: w3_http.eth.get_block("1"),
112+
"async_exec": lambda: (_ for _ in ()).throw(Exception("not implemented yet")),
113+
},
114+
]
115+
116+
def benchmark(method: Dict[str, Any]) -> None:
117+
outcomes: Dict[str, Union[str, float]] = defaultdict(lambda: "N/A")
118+
outcomes["name"] = method["name"]
119+
outcomes["HTTPProvider"] = sync_benchmark(method["exec"], num_calls,)
120+
outcomes["AsyncHTTPProvider"] = loop.run_until_complete(
121+
async_benchmark(method["async_exec"], num_calls)
122+
)
123+
print_entry(logger, outcomes)
124+
125+
print_header(logger, num_calls)
126+
127+
for method in methods:
128+
benchmark(method)
129+
130+
print_footer(logger)
131+
132+
133+
if __name__ == "__main__":
134+
args = parser.parse_args()
135+
136+
logger = logging.getLogger()
137+
logger.setLevel(logging.INFO)
138+
logger.addHandler(logging.StreamHandler(sys.stdout))
139+
140+
main(logger, args.num_calls)

web3/tools/benchmark/node.py

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import os
2+
import socket
3+
from subprocess import (
4+
PIPE,
5+
Popen,
6+
check_output,
7+
)
8+
from tempfile import (
9+
TemporaryDirectory,
10+
)
11+
from typing import (
12+
Any,
13+
Generator,
14+
Sequence,
15+
)
16+
import zipfile
17+
18+
from geth.install import (
19+
get_executable_path,
20+
install_geth,
21+
)
22+
23+
from web3.tools.benchmark.utils import (
24+
kill_proc_gracefully,
25+
)
26+
27+
GETH_FIXTURE_ZIP = "geth-1.10.1-fixture.zip"
28+
29+
30+
class GethBenchmarkFixture:
31+
def __init__(self) -> None:
32+
self.rpc_port = self._rpc_port()
33+
self.endpoint_uri = self._endpoint_uri()
34+
self.geth_binary = self._geth_binary()
35+
36+
def build(self) -> Generator[Any, None, None]:
37+
with TemporaryDirectory() as base_dir:
38+
zipfile_path = os.path.abspath(
39+
os.path.join(
40+
os.path.dirname(__file__),
41+
"../../../tests/integration/",
42+
GETH_FIXTURE_ZIP,
43+
)
44+
)
45+
tmp_datadir = os.path.join(str(base_dir), "datadir")
46+
with zipfile.ZipFile(zipfile_path, "r") as zip_ref:
47+
zip_ref.extractall(tmp_datadir)
48+
self.datadir = tmp_datadir
49+
50+
genesis_file = os.path.join(self.datadir, "genesis.json")
51+
52+
yield self._geth_process(self.datadir, genesis_file, self.rpc_port)
53+
54+
def _rpc_port(self) -> str:
55+
sock = socket.socket()
56+
sock.bind(("127.0.0.1", 0))
57+
port = sock.getsockname()[1]
58+
sock.close()
59+
return str(port)
60+
61+
def _endpoint_uri(self) -> str:
62+
return "http://localhost:{0}".format(self.rpc_port)
63+
64+
def _geth_binary(self) -> str:
65+
if "GETH_BINARY" in os.environ:
66+
return os.environ["GETH_BINARY"]
67+
elif "GETH_VERSION" in os.environ:
68+
geth_version = os.environ["GETH_VERSION"]
69+
_geth_binary = get_executable_path(geth_version)
70+
if not os.path.exists(_geth_binary):
71+
install_geth(geth_version)
72+
assert os.path.exists(_geth_binary)
73+
return _geth_binary
74+
else:
75+
return "geth"
76+
77+
def _geth_command_arguments(self, datadir: str) -> Sequence[str]:
78+
return (
79+
self.geth_binary,
80+
"--datadir",
81+
str(datadir),
82+
"--nodiscover",
83+
"--fakepow",
84+
"--http",
85+
"--http.port",
86+
self.rpc_port,
87+
"--http.api",
88+
"admin,eth,net,web3,personal,miner",
89+
"--ipcdisable",
90+
"--allow-insecure-unlock",
91+
)
92+
93+
def _geth_process(
94+
self, datadir: str, genesis_file: str, rpc_port: str
95+
) -> Generator[Any, None, None]:
96+
init_datadir_command = (
97+
self.geth_binary,
98+
"--datadir",
99+
str(datadir),
100+
"init",
101+
str(genesis_file),
102+
)
103+
check_output(
104+
init_datadir_command, stdin=PIPE, stderr=PIPE,
105+
)
106+
proc = Popen(
107+
self._geth_command_arguments(datadir),
108+
stdin=PIPE,
109+
stdout=PIPE,
110+
stderr=PIPE,
111+
)
112+
try:
113+
yield proc
114+
finally:
115+
kill_proc_gracefully(proc)

web3/tools/benchmark/reporting.py

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from logging import (
2+
Logger,
3+
)
4+
from typing import (
5+
Any,
6+
Dict,
7+
)
8+
9+
10+
def print_header(logger: Logger, num_calls: int) -> None:
11+
logger.info(
12+
"|{:^26}|{:^20}|{:^20}|{:^20}|{:^20}|".format(
13+
f"Method ({num_calls} calls)",
14+
"HTTPProvider",
15+
"AsyncHTTProvider",
16+
"IPCProvider",
17+
"WebsocketProvider",
18+
)
19+
)
20+
logger.info("-" * 112)
21+
22+
23+
def print_entry(logger: Logger, method_benchmarks: Dict[str, Any],) -> None:
24+
logger.info(
25+
"|{:^26}|{:^20.10}|{:^20.10}|{:^20.10}|{:^20.10}|".format(
26+
method_benchmarks["name"],
27+
method_benchmarks["HTTPProvider"],
28+
method_benchmarks["AsyncHTTPProvider"],
29+
method_benchmarks["IPCProvider"],
30+
method_benchmarks["WebsocketProvider"],
31+
)
32+
)
33+
34+
35+
def print_footer(logger: Logger) -> None:
36+
logger.info("-" * 112)

0 commit comments

Comments
 (0)