-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
benchmark async vs. sync http providers #2002
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Add basic provider benchmarking infrastructure |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
import argparse | ||
import asyncio | ||
from collections import ( | ||
defaultdict, | ||
) | ||
import logging | ||
import sys | ||
import timeit | ||
from typing import ( | ||
Any, | ||
Callable, | ||
Dict, | ||
Union, | ||
) | ||
|
||
from web3 import ( | ||
AsyncHTTPProvider, | ||
HTTPProvider, | ||
Web3, | ||
) | ||
from web3.eth import ( | ||
AsyncEth, | ||
) | ||
from web3.tools.benchmark.node import ( | ||
GethBenchmarkFixture, | ||
) | ||
from web3.tools.benchmark.reporting import ( | ||
print_entry, | ||
print_footer, | ||
print_header, | ||
) | ||
from web3.tools.benchmark.utils import ( | ||
wait_for_aiohttp, | ||
wait_for_http, | ||
) | ||
|
||
parser = argparse.ArgumentParser() | ||
parser.add_argument( | ||
"--num-calls", type=int, default=10, help="The number of RPC calls to make", | ||
) | ||
|
||
# TODO - layers to test: | ||
# contract.functions.method(...).call() | ||
# w3.eth.call(...) | ||
# HTTPProvider.make_request(...) | ||
|
||
|
||
def build_web3_http(endpoint_uri: str) -> Web3: | ||
wait_for_http(endpoint_uri) | ||
_web3 = Web3(HTTPProvider(endpoint_uri), middlewares=[]) | ||
return _web3 | ||
|
||
|
||
async def build_async_w3_http(endpoint_uri: str) -> Web3: | ||
await wait_for_aiohttp(endpoint_uri) | ||
_web3 = Web3( | ||
AsyncHTTPProvider(endpoint_uri), # type: ignore | ||
middlewares=[], | ||
modules={"async_eth": (AsyncEth,)}, | ||
) | ||
return _web3 | ||
|
||
|
||
def sync_benchmark(func: Callable[..., Any], n: int) -> Union[float, str]: | ||
try: | ||
starttime = timeit.default_timer() | ||
for _ in range(n): | ||
func() | ||
endtime = timeit.default_timer() | ||
execution_time = endtime - starttime | ||
return execution_time | ||
except Exception: | ||
return "N/A" | ||
|
||
|
||
async def async_benchmark(func: Callable[..., Any], n: int) -> Union[float, str]: | ||
try: | ||
starttime = timeit.default_timer() | ||
for result in asyncio.as_completed([func() for _ in range(n)]): | ||
await result | ||
execution_time = timeit.default_timer() - starttime | ||
return execution_time | ||
except Exception: | ||
return "N/A" | ||
|
||
|
||
def main(logger: logging.Logger, num_calls: int) -> None: | ||
fixture = GethBenchmarkFixture() | ||
for built_fixture in fixture.build(): | ||
for process in built_fixture: | ||
w3_http = build_web3_http(fixture.endpoint_uri) | ||
loop = asyncio.get_event_loop() | ||
async_w3_http = loop.run_until_complete(build_async_w3_http(fixture.endpoint_uri)) | ||
|
||
methods = [ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe worth pulling this out into a constant? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's probably a good way to clean this up a bit, but the methods rely on the providers built within this context. Will revisit next iteration. |
||
{ | ||
"name": "eth_gasPrice", | ||
"params": {}, | ||
"exec": lambda: w3_http.eth.gas_price, | ||
"async_exec": lambda: async_w3_http.async_eth.gas_price, | ||
}, | ||
{ | ||
"name": "eth_blockNumber", | ||
"params": {}, | ||
"exec": lambda: w3_http.eth.block_number, | ||
"async_exec": lambda: (_ for _ in ()).throw(Exception("not implemented yet")), | ||
}, | ||
{ | ||
"name": "eth_getBlock", | ||
"params": {}, | ||
"exec": lambda: w3_http.eth.get_block("1"), | ||
"async_exec": lambda: (_ for _ in ()).throw(Exception("not implemented yet")), | ||
}, | ||
] | ||
|
||
def benchmark(method: Dict[str, Any]) -> None: | ||
outcomes: Dict[str, Union[str, float]] = defaultdict(lambda: "N/A") | ||
outcomes["name"] = method["name"] | ||
outcomes["HTTPProvider"] = sync_benchmark(method["exec"], num_calls,) | ||
outcomes["AsyncHTTPProvider"] = loop.run_until_complete( | ||
async_benchmark(method["async_exec"], num_calls) | ||
) | ||
print_entry(logger, outcomes) | ||
|
||
print_header(logger, num_calls) | ||
|
||
for method in methods: | ||
benchmark(method) | ||
|
||
print_footer(logger) | ||
|
||
|
||
if __name__ == "__main__": | ||
args = parser.parse_args() | ||
|
||
logger = logging.getLogger() | ||
logger.setLevel(logging.INFO) | ||
logger.addHandler(logging.StreamHandler(sys.stdout)) | ||
|
||
main(logger, args.num_calls) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import os | ||
import socket | ||
from subprocess import ( | ||
PIPE, | ||
Popen, | ||
check_output, | ||
) | ||
from tempfile import ( | ||
TemporaryDirectory, | ||
) | ||
from typing import ( | ||
Any, | ||
Generator, | ||
Sequence, | ||
) | ||
import zipfile | ||
|
||
from geth.install import ( | ||
get_executable_path, | ||
install_geth, | ||
) | ||
|
||
from web3.tools.benchmark.utils import ( | ||
kill_proc_gracefully, | ||
) | ||
|
||
GETH_FIXTURE_ZIP = "geth-1.10.1-fixture.zip" | ||
|
||
|
||
class GethBenchmarkFixture: | ||
def __init__(self) -> None: | ||
self.rpc_port = self._rpc_port() | ||
self.endpoint_uri = self._endpoint_uri() | ||
self.geth_binary = self._geth_binary() | ||
|
||
def build(self) -> Generator[Any, None, None]: | ||
with TemporaryDirectory() as base_dir: | ||
zipfile_path = os.path.abspath( | ||
os.path.join( | ||
os.path.dirname(__file__), | ||
"../../../tests/integration/", | ||
GETH_FIXTURE_ZIP, | ||
) | ||
) | ||
tmp_datadir = os.path.join(str(base_dir), "datadir") | ||
with zipfile.ZipFile(zipfile_path, "r") as zip_ref: | ||
zip_ref.extractall(tmp_datadir) | ||
self.datadir = tmp_datadir | ||
|
||
genesis_file = os.path.join(self.datadir, "genesis.json") | ||
|
||
yield self._geth_process(self.datadir, genesis_file, self.rpc_port) | ||
|
||
def _rpc_port(self) -> str: | ||
sock = socket.socket() | ||
sock.bind(("127.0.0.1", 0)) | ||
port = sock.getsockname()[1] | ||
sock.close() | ||
return str(port) | ||
|
||
def _endpoint_uri(self) -> str: | ||
return "http://localhost:{0}".format(self.rpc_port) | ||
|
||
def _geth_binary(self) -> str: | ||
if "GETH_BINARY" in os.environ: | ||
return os.environ["GETH_BINARY"] | ||
elif "GETH_VERSION" in os.environ: | ||
geth_version = os.environ["GETH_VERSION"] | ||
_geth_binary = get_executable_path(geth_version) | ||
if not os.path.exists(_geth_binary): | ||
install_geth(geth_version) | ||
assert os.path.exists(_geth_binary) | ||
return _geth_binary | ||
else: | ||
return "geth" | ||
|
||
def _geth_command_arguments(self, datadir: str) -> Sequence[str]: | ||
return ( | ||
self.geth_binary, | ||
"--datadir", | ||
str(datadir), | ||
"--nodiscover", | ||
"--fakepow", | ||
"--http", | ||
"--http.port", | ||
self.rpc_port, | ||
"--http.api", | ||
"admin,eth,net,web3,personal,miner", | ||
"--ipcdisable", | ||
"--allow-insecure-unlock", | ||
) | ||
|
||
def _geth_process( | ||
self, datadir: str, genesis_file: str, rpc_port: str | ||
) -> Generator[Any, None, None]: | ||
init_datadir_command = ( | ||
self.geth_binary, | ||
"--datadir", | ||
str(datadir), | ||
"init", | ||
str(genesis_file), | ||
) | ||
check_output( | ||
init_datadir_command, stdin=PIPE, stderr=PIPE, | ||
) | ||
proc = Popen( | ||
self._geth_command_arguments(datadir), | ||
stdin=PIPE, | ||
stdout=PIPE, | ||
stderr=PIPE, | ||
) | ||
try: | ||
yield proc | ||
finally: | ||
kill_proc_gracefully(proc) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
from logging import ( | ||
Logger, | ||
) | ||
from typing import ( | ||
Any, | ||
Dict, | ||
) | ||
|
||
|
||
def print_header(logger: Logger, num_calls: int) -> None: | ||
logger.info( | ||
"|{:^26}|{:^20}|{:^20}|{:^20}|{:^20}|".format( | ||
f"Method ({num_calls} calls)", | ||
"HTTPProvider", | ||
"AsyncHTTProvider", | ||
"IPCProvider", | ||
"WebsocketProvider", | ||
) | ||
) | ||
logger.info("-" * 112) | ||
|
||
|
||
def print_entry(logger: Logger, method_benchmarks: Dict[str, Any],) -> None: | ||
logger.info( | ||
"|{:^26}|{:^20.10}|{:^20.10}|{:^20.10}|{:^20.10}|".format( | ||
method_benchmarks["name"], | ||
method_benchmarks["HTTPProvider"], | ||
method_benchmarks["AsyncHTTPProvider"], | ||
method_benchmarks["IPCProvider"], | ||
method_benchmarks["WebsocketProvider"], | ||
) | ||
) | ||
|
||
|
||
def print_footer(logger: Logger) -> None: | ||
logger.info("-" * 112) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just curious - what's the reasoning behind testing 5, 50, and 100 calls here and not just checking one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once the suite is more mature, we should have vectors for benchmarking (the average of) 1 call vs. 1 call; this would be useful for comparing the HTTPProvider vs. IPCProvider, for example. I think making many calls is the best way to showcase the difference between AsyncHTTPProvider vs. HTTPProvider performance.