From 01d1faa8b00845dbc452d511155398eb32fb9357 Mon Sep 17 00:00:00 2001 From: Erwin Wee Date: Mon, 22 Jul 2024 15:57:53 +0800 Subject: [PATCH 01/21] wss initial --- chainbench/user/__init__.py | 5 +- chainbench/user/http.py | 196 +-------------------------- chainbench/user/jsonrpc.py | 140 +++++++++++++++++++ chainbench/user/protocol/evm.py | 3 +- chainbench/user/protocol/solana.py | 3 +- chainbench/user/protocol/starknet.py | 2 +- chainbench/user/wss.py | 53 ++++++++ chainbench/util/jsonrpc.py | 64 +++++++++ poetry.lock | 18 ++- pyproject.toml | 1 + 10 files changed, 285 insertions(+), 200 deletions(-) create mode 100644 chainbench/user/jsonrpc.py create mode 100644 chainbench/user/wss.py create mode 100644 chainbench/util/jsonrpc.py diff --git a/chainbench/user/__init__.py b/chainbench/user/__init__.py index 4e971b5..5a3babd 100644 --- a/chainbench/user/__init__.py +++ b/chainbench/user/__init__.py @@ -2,7 +2,8 @@ from chainbench.util.event import setup_event_listeners from .common import get_subclass_tasks -from .http import HttpUser, JsonRpcUser +from .http import HttpUser +from .wss import WssUser # importing plugins here as all profiles depend on it import locust_plugins # isort: skip # noqa @@ -13,8 +14,8 @@ "EthBeaconUser", "EvmUser", "HttpUser", - "JsonRpcUser", "SolanaUser", "StarkNetUser", + "WssUser", "get_subclass_tasks", ] diff --git a/chainbench/user/http.py b/chainbench/user/http.py index 0a308d4..0e4a6f5 100644 --- a/chainbench/user/http.py +++ b/chainbench/user/http.py @@ -1,47 +1,14 @@ -import json import logging -import random import typing as t -from locust import FastHttpUser, TaskSet, tag, task +from locust import FastHttpUser, TaskSet from locust.contrib.fasthttp import ResponseContextManager from chainbench.test_data import TestData +from chainbench.util.jsonrpc import expand_to_list from chainbench.util.rng import RNGManager -class RpcCall: - def __init__(self, method: str, params: list[t.Any] | dict | None = None) -> None: - self.method = method - self.params = params - - -def expand_rpc_calls(rpc_calls_weighted: dict[t.Callable[[], RpcCall], int]) -> list[RpcCall]: - rpc_call_methods_weighted: dict[RpcCall, int] = {} - for rpc_call_method, weight in rpc_calls_weighted.items(): - rpc_call_methods_weighted[rpc_call_method()] = weight - - expanded_rpc_calls: list[RpcCall] = expand_to_list(rpc_call_methods_weighted) - return expanded_rpc_calls - - -def expand_to_list(items_weighted: dict[t.Any, int] | list[t.Any | tuple[t.Any, int]]) -> list[t.Any]: - expanded_items_list: list[t.Any] = [] - if isinstance(items_weighted, dict): - items_weighted = list(items_weighted.items()) - - if isinstance(items_weighted, list): - for rpc_call in items_weighted: - if isinstance(rpc_call, tuple): - rpc_call, count = rpc_call - for _ in range(count): - expanded_items_list.append(rpc_call) - else: - expanded_items_list.append(rpc_call) - - return expanded_items_list - - class HttpUser(FastHttpUser): """Extension of FastHttpUser for Chainbench.""" @@ -121,162 +88,3 @@ def get(self, name: str, params: t.Optional[dict] = None, path: str = "") -> Res return response -class JsonRpcUser(HttpUser): - """Extension of HttpUser to provide JsonRPC support.""" - - abstract = True - rpc_path = "" - rpc_error_code_exclusions: list[int] = [] - rpc_calls: dict[t.Callable, int] = {} # To be populated in the subclass load profile - calls_per_batch = 10 # default requests to include in a batch request - - def __init__(self, environment: t.Any): - self.calls_per_batch = environment.parsed_options.batch_size - super().__init__(environment) - - @tag("single") - @task - def rpc_call_task(self) -> None: - self.method_to_task_function(self.environment.parsed_options.method)(self) - - @tag("batch") - @task - def batch_rpc_call_task(self) -> None: - rpc_calls = {getattr(self, method.__name__): weight for method, weight in self.rpc_calls.items()} - self.make_random_batch_rpc_call( - rpc_calls, - calls_per_batch=self.calls_per_batch, - ) - - @tag("batch_single") - @task - def batch_single_rpc_call_task(self) -> None: - rpc_call: RpcCall = self.method_to_rpc_call(self.environment.parsed_options.method)(self) - rpc_calls = [rpc_call for _ in range(self.calls_per_batch)] - self.make_batch_rpc_call( - rpc_calls, - ) - - @classmethod - def method_to_rpc_call(cls, method: str) -> t.Callable: - method_name = cls.method_to_function_name(method) - return getattr(cls, method_name) - - def check_json_rpc_response(self, response: ResponseContextManager, name: str) -> None: - CHUNK_SIZE = 1024 - if response.text is None: - self.logger.error(f"Response for {name} is empty") - response.failure(f"Response for {name} is empty") - return - data = response.text[:CHUNK_SIZE] - if "jsonrpc" not in data: - self.logger.error(f"Response for {name} is not a JSON-RPC: {response.text}") - response.failure(f"Response for {name} is not a JSON-RPC") - return - - if "error" in data: - response_js: list | dict = response.json() - if isinstance(response_js, dict): - response_js = [response_js] - if isinstance(response_js, list): - for response_js_item in response_js: - if "error" in response_js_item: - if "code" in response_js_item["error"]: - self.logger.error(f"Response for {name} has a JSON-RPC error: {response.text}") - if response_js_item["error"]["code"] not in self.rpc_error_code_exclusions: - response.failure( - f"Response for {name} has a JSON-RPC error {response_js_item['error']['code']} - " - f"{response_js_item['error']['message']}" - ) - return - response.failure("Unspecified JSON-RPC error") - self.logger.error(f"Unspecified JSON-RPC error: {response.text}") - return - # TODO: handle multiple errors in batch response properly - - if "result" not in data: - response.failure(f"Response for {name} call has no result") - self.logger.error(f"Response for {name} call has no result: {response.text}") - - def make_rpc_call( - self, - rpc_call: RpcCall | None = None, - method: str | None = None, - params: list[t.Any] | dict | None = None, - name: str = "", - path: str = "", - ) -> None: - """Make a JSON-RPC call.""" - if rpc_call is not None: - method = rpc_call.method - params = rpc_call.params - - if name == "" and method is not None: - name = method - - with self.client.request( - "POST", self.rpc_path + path, json=generate_request_body(method, params), name=name, catch_response=True - ) as response: - self.check_http_error(response) - self.check_json_rpc_response(response, name=name) - - def make_batch_rpc_call(self, rpc_calls: list[RpcCall], name: str = "", path: str = "") -> None: - """Make a Batch JSON-RPC call.""" - - if name == "": - name = f"Batch RPC ({len(rpc_calls)})" - - headers = {"Content-Type": "application/json", "accept": "application/json"} - - with self.client.request( - "POST", - self.rpc_path + path, - data=generate_batch_request_body(rpc_calls), - name=name, - catch_response=True, - headers=headers, - ) as response: - self.check_http_error(response) - self.check_json_rpc_response(response, name=name) - - def make_random_batch_rpc_call( - self, - weighted_rpc_calls: dict[t.Callable[[], RpcCall], int], - calls_per_batch: int, - name: str = "", - path: str = "", - ) -> None: - """Make a Batch JSON-RPC call.""" - rpc_calls: list[RpcCall] = expand_rpc_calls(weighted_rpc_calls) - random_rpc_calls: list[RpcCall] = random.choices(rpc_calls, k=calls_per_batch) - - self.make_batch_rpc_call(random_rpc_calls, name=name, path=path) - - -def generate_request_body( - method: str | None = None, params: list | dict | None = None, request_id: int | None = None, version: str = "2.0" -) -> dict: - """Generate a JSON-RPC request body.""" - - if params is None: - params = [] - - if request_id is None: - request_id = random.randint(1, 100000000) - - return { - "jsonrpc": version, - "method": method, - "params": params, - "id": request_id, - } - - -def generate_batch_request_body(rpc_calls: list[RpcCall], version: str = "2.0") -> str: - """Generate a batch JSON-RPC request body.""" - return json.dumps( - [ - generate_request_body(rpc_calls[i].method, rpc_calls[i].params, request_id=i, version=version) - for i in range(1, len(rpc_calls)) - ] - ) diff --git a/chainbench/user/jsonrpc.py b/chainbench/user/jsonrpc.py new file mode 100644 index 0000000..f4d101b --- /dev/null +++ b/chainbench/user/jsonrpc.py @@ -0,0 +1,140 @@ +import random +import typing as t + +from locust import tag, task +from locust.contrib.fasthttp import ResponseContextManager + +from chainbench.user.http import HttpUser +from chainbench.util.jsonrpc import RpcCall, generate_request_body, generate_batch_request_body, expand_rpc_calls + + +class JsonRpcUser(HttpUser): + """Extension of HttpUser to provide JsonRPC support.""" + + abstract = True + rpc_path = "" + rpc_error_code_exclusions: list[int] = [] + rpc_calls: dict[t.Callable, int] = {} # To be populated in the subclass load profile + calls_per_batch = 10 # default requests to include in a batch request + + def __init__(self, environment: t.Any): + self.calls_per_batch = environment.parsed_options.batch_size + super().__init__(environment) + + @tag("single") + @task + def rpc_call_task(self) -> None: + self.method_to_task_function(self.environment.parsed_options.method)(self) + + @tag("batch") + @task + def batch_rpc_call_task(self) -> None: + rpc_calls = {getattr(self, method.__name__): weight for method, weight in self.rpc_calls.items()} + self.make_random_batch_rpc_call( + rpc_calls, + calls_per_batch=self.calls_per_batch, + ) + + @tag("batch_single") + @task + def batch_single_rpc_call_task(self) -> None: + rpc_call: RpcCall = self.method_to_rpc_call(self.environment.parsed_options.method)(self) + rpc_calls = [rpc_call for _ in range(self.calls_per_batch)] + self.make_batch_rpc_call( + rpc_calls, + ) + + @classmethod + def method_to_rpc_call(cls, method: str) -> t.Callable: + method_name = cls.method_to_function_name(method) + return getattr(cls, method_name) + + def check_json_rpc_response(self, response: ResponseContextManager, name: str) -> None: + CHUNK_SIZE = 1024 + if response.text is None: + self.logger.error(f"Response for {name} is empty") + response.failure(f"Response for {name} is empty") + return + data = response.text[:CHUNK_SIZE] + if "jsonrpc" not in data: + self.logger.error(f"Response for {name} is not a JSON-RPC: {response.text}") + response.failure(f"Response for {name} is not a JSON-RPC") + return + + if "error" in data: + response_js: list | dict = response.json() + if isinstance(response_js, dict): + response_js = [response_js] + if isinstance(response_js, list): + for response_js_item in response_js: + if "error" in response_js_item: + if "code" in response_js_item["error"]: + self.logger.error(f"Response for {name} has a JSON-RPC error: {response.text}") + if response_js_item["error"]["code"] not in self.rpc_error_code_exclusions: + response.failure( + f"Response for {name} has a JSON-RPC error {response_js_item['error']['code']} - " + f"{response_js_item['error']['message']}" + ) + return + response.failure("Unspecified JSON-RPC error") + self.logger.error(f"Unspecified JSON-RPC error: {response.text}") + return + # TODO: handle multiple errors in batch response properly + + if "result" not in data: + response.failure(f"Response for {name} call has no result") + self.logger.error(f"Response for {name} call has no result: {response.text}") + + def make_rpc_call( + self, + rpc_call: RpcCall | None = None, + method: str | None = None, + params: list[t.Any] | dict | None = None, + name: str = "", + path: str = "", + ) -> None: + """Make a JSON-RPC call.""" + if rpc_call is not None: + method = rpc_call.method + params = rpc_call.params + + if name == "" and method is not None: + name = method + + with self.client.request( + "POST", self.rpc_path + path, json=generate_request_body(method, params), name=name, catch_response=True + ) as response: + self.check_http_error(response) + self.check_json_rpc_response(response, name=name) + + def make_batch_rpc_call(self, rpc_calls: list[RpcCall], name: str = "", path: str = "") -> None: + """Make a Batch JSON-RPC call.""" + + if name == "": + name = f"Batch RPC ({len(rpc_calls)})" + + headers = {"Content-Type": "application/json", "accept": "application/json"} + + with self.client.request( + "POST", + self.rpc_path + path, + data=generate_batch_request_body(rpc_calls), + name=name, + catch_response=True, + headers=headers, + ) as response: + self.check_http_error(response) + self.check_json_rpc_response(response, name=name) + + def make_random_batch_rpc_call( + self, + weighted_rpc_calls: dict[t.Callable[[], RpcCall], int], + calls_per_batch: int, + name: str = "", + path: str = "", + ) -> None: + """Make a Batch JSON-RPC call.""" + rpc_calls: list[RpcCall] = expand_rpc_calls(weighted_rpc_calls) + random_rpc_calls: list[RpcCall] = random.choices(rpc_calls, k=calls_per_batch) + + self.make_batch_rpc_call(random_rpc_calls, name=name, path=path) diff --git a/chainbench/user/protocol/evm.py b/chainbench/user/protocol/evm.py index fd746ca..bca3fff 100644 --- a/chainbench/user/protocol/evm.py +++ b/chainbench/user/protocol/evm.py @@ -8,7 +8,8 @@ Tx, TxHash, ) -from chainbench.user.http import JsonRpcUser, RpcCall +from chainbench.user.jsonrpc import JsonRpcUser +from chainbench.util.jsonrpc import RpcCall from chainbench.user.tag import tag from chainbench.util.rng import RNG diff --git a/chainbench/user/protocol/solana.py b/chainbench/user/protocol/solana.py index d2fc63e..8632343 100644 --- a/chainbench/user/protocol/solana.py +++ b/chainbench/user/protocol/solana.py @@ -4,7 +4,8 @@ from solders.message import Message from chainbench.test_data import Account, BlockNumber, SolanaTestData, TxHash -from chainbench.user.http import JsonRpcUser, RpcCall +from chainbench.user.jsonrpc import JsonRpcUser +from chainbench.util.jsonrpc import RpcCall from chainbench.util.rng import RNG diff --git a/chainbench/user/protocol/starknet.py b/chainbench/user/protocol/starknet.py index 710a808..f4fed6d 100644 --- a/chainbench/user/protocol/starknet.py +++ b/chainbench/user/protocol/starknet.py @@ -2,7 +2,7 @@ from chainbench.test_data import StarkNetTestData from chainbench.test_data.blockchain import Account, TxHash -from chainbench.user.http import JsonRpcUser +from chainbench.user.jsonrpc import JsonRpcUser from chainbench.util.rng import RNG diff --git a/chainbench/user/wss.py b/chainbench/user/wss.py new file mode 100644 index 0000000..1e7ad4d --- /dev/null +++ b/chainbench/user/wss.py @@ -0,0 +1,53 @@ +import json +import logging +import time + +import websocket +import gevent +from locust import User +from websocket import WebSocket + + +class WssUser(User): + abstract = True + + def __init__(self, environment): + super().__init__(environment) + self.ws: WebSocket | None = None + self.ws_greenlet = None + self.start_time = None + self.name = None + + def on_start(self) -> None: + self.connect(self.environment.parsed_options.host) + + def on_stop(self) -> None: + self.ws_greenlet.kill() + self.ws.close() + + def connect(self, host: str, **kwargs): + self.ws = websocket.create_connection(host, **kwargs) + self.ws_greenlet = gevent.spawn(self.receive_loop) + + def on_message(self, message): # override this method in your subclass for custom handling + response_time = ((time.time_ns() - self.start_time) / 1_000_000).__round__() + self.environment.events.request.fire( + request_type="WSR", + name=self.name, + response_time=response_time, + response_length=len(message), + exception=None, + ) + + def receive_loop(self): + while True: + message = self.ws.recv() + logging.debug(f"WSR: {message}") + self.on_message(message) + + def send(self, body, name): + json_body = json.dumps(body) + logging.debug(f"WSS: {json_body}") + self.start_time = time.time_ns() + self.name = name + self.ws.send(json_body) diff --git a/chainbench/util/jsonrpc.py b/chainbench/util/jsonrpc.py new file mode 100644 index 0000000..443f3aa --- /dev/null +++ b/chainbench/util/jsonrpc.py @@ -0,0 +1,64 @@ +import json +import random +import typing as t + + +class RpcCall: + def __init__(self, method: str, params: list[t.Any] | dict | None = None) -> None: + self.method = method + self.params = params + + +def generate_request_body( + method: str | None = None, params: list | dict | None = None, request_id: int | None = None, version: str = "2.0" +) -> dict: + """Generate a JSON-RPC request body.""" + + if params is None: + params = [] + + if request_id is None: + request_id = random.randint(1, 100000000) + + return { + "jsonrpc": version, + "method": method, + "params": params, + "id": request_id, + } + + +def generate_batch_request_body(rpc_calls: list[RpcCall], version: str = "2.0") -> str: + """Generate a batch JSON-RPC request body.""" + return json.dumps( + [ + generate_request_body(rpc_calls[i].method, rpc_calls[i].params, request_id=i, version=version) + for i in range(1, len(rpc_calls)) + ] + ) + + +def expand_rpc_calls(rpc_calls_weighted: dict[t.Callable[[], RpcCall], int]) -> list[RpcCall]: + rpc_call_methods_weighted: dict[RpcCall, int] = {} + for rpc_call_method, weight in rpc_calls_weighted.items(): + rpc_call_methods_weighted[rpc_call_method()] = weight + + expanded_rpc_calls: list[RpcCall] = expand_to_list(rpc_call_methods_weighted) + return expanded_rpc_calls + + +def expand_to_list(items_weighted: dict[t.Any, int] | list[t.Any | tuple[t.Any, int]]) -> list[t.Any]: + expanded_items_list: list[t.Any] = [] + if isinstance(items_weighted, dict): + items_weighted = list(items_weighted.items()) + + if isinstance(items_weighted, list): + for rpc_call in items_weighted: + if isinstance(rpc_call, tuple): + rpc_call, count = rpc_call + for _ in range(count): + expanded_items_list.append(rpc_call) + else: + expanded_items_list.append(rpc_call) + + return expanded_items_list diff --git a/poetry.lock b/poetry.lock index eeb20bb..15692ad 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1603,6 +1603,22 @@ platformdirs = ">=3.9.1,<5" docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +[[package]] +name = "websocket-client" +version = "1.8.0" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.8" +files = [ + {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, + {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, +] + +[package.extras] +docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + [[package]] name = "werkzeug" version = "3.0.2" @@ -1694,4 +1710,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "3544f0c3a01ea63a80fb62f87c5aaa2b4f5b4c60591229df1ffff9d002aee8b5" +content-hash = "2f24f6c908616b71e22e43315ab2bc78b92a8c30fb45f2ff7bc2eb1ac58282e3" diff --git a/pyproject.toml b/pyproject.toml index 37c4040..eda90fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ locust-plugins = {extras = ["dashboards"], version = "^4.4.2"} tenacity = "^8.2.2" base58 = "^2.1.1" solders = "^0.21.0" +websocket-client = "^1.8.0" [tool.poetry.group.dev.dependencies] black = ">=23.1,<25.0" From babf1b3de9665e5723b2411dba7092d62688e612 Mon Sep 17 00:00:00 2001 From: Erwin Wee Date: Sat, 27 Jul 2024 00:23:07 +0800 Subject: [PATCH 02/21] wip --- chainbench/profile/sandbox.py | 41 ++++++++++++ chainbench/user/http.py | 2 - chainbench/user/protocol/evm.py | 7 +- chainbench/user/wss.py | 112 ++++++++++++++++++++++++++------ chainbench/util/event.py | 85 +++++++++++++----------- 5 files changed, 185 insertions(+), 62 deletions(-) create mode 100644 chainbench/profile/sandbox.py diff --git a/chainbench/profile/sandbox.py b/chainbench/profile/sandbox.py new file mode 100644 index 0000000..4e6608d --- /dev/null +++ b/chainbench/profile/sandbox.py @@ -0,0 +1,41 @@ +import random + +from locust import constant_pacing, task + +from chainbench.user import WssUser, SolanaUser + + +# TODO: Update Oasis profile to new format and update tutorial in documentation + + +class SandboxProfile(WssUser, SolanaUser): + wait_time = constant_pacing(1) + + @task + def dummy_task(self): + pass + + # + # @task + # def eth_block_number(self): + # self.send( + # { + # "jsonrpc": "2.0", + # "method": "eth_blockNumber", + # "params": [], + # "id": random.Random().randint(0, 100000000) + # }, + # "eth_blockNumber" + # ) + # + # @task + # def eth_get_logs(self): + # self.send( + # { + # "jsonrpc": "2.0", + # "method": "eth_getLogs", + # "params": self._get_logs_params_factory(self.rng.get_rng()), + # "id": random.Random().randint(0, 100000000) + # }, + # "eth_getLogs" + # ) diff --git a/chainbench/user/http.py b/chainbench/user/http.py index 0e4a6f5..f2c0c55 100644 --- a/chainbench/user/http.py +++ b/chainbench/user/http.py @@ -6,7 +6,6 @@ from chainbench.test_data import TestData from chainbench.util.jsonrpc import expand_to_list -from chainbench.util.rng import RNGManager class HttpUser(FastHttpUser): @@ -15,7 +14,6 @@ class HttpUser(FastHttpUser): abstract = True test_data: TestData = TestData() logger = logging.getLogger(__name__) - rng = RNGManager() connection_timeout = 120 network_timeout = 360 diff --git a/chainbench/user/protocol/evm.py b/chainbench/user/protocol/evm.py index bca3fff..d48459b 100644 --- a/chainbench/user/protocol/evm.py +++ b/chainbench/user/protocol/evm.py @@ -11,12 +11,13 @@ from chainbench.user.jsonrpc import JsonRpcUser from chainbench.util.jsonrpc import RpcCall from chainbench.user.tag import tag -from chainbench.util.rng import RNG +from chainbench.util.rng import RNG, RNGManager -class EvmBaseUser(JsonRpcUser): +class EvmBaseUser: abstract = True test_data = EvmTestData() + rng = RNGManager() _default_trace_timeout = "120s" @@ -395,7 +396,7 @@ def web3_sha3(self) -> RpcCall: return RpcCall(method="web3_sha3", params=[self.test_data.get_random_tx_hash(self.rng.get_rng())]) -class EvmUser(EvmRpcMethods): +class EvmUser(EvmRpcMethods, JsonRpcUser): abstract = True @staticmethod diff --git a/chainbench/user/wss.py b/chainbench/user/wss.py index 1e7ad4d..31190ae 100644 --- a/chainbench/user/wss.py +++ b/chainbench/user/wss.py @@ -1,9 +1,12 @@ import json import logging +import random import time +from json import JSONDecodeError import websocket import gevent +from gevent import Greenlet from locust import User from websocket import WebSocket @@ -13,41 +16,112 @@ class WssUser(User): def __init__(self, environment): super().__init__(environment) - self.ws: WebSocket | None = None - self.ws_greenlet = None - self.start_time = None - self.name = None + self._ws: WebSocket | None = None + self._ws_greenlet: Greenlet | None = None + self._requests = {} + self._running = False + self.subscriptions = {} def on_start(self) -> None: - self.connect(self.environment.parsed_options.host) + self._running = True + host: str = self.environment.parsed_options.host + if host.startswith("ws") or host.startswith("wss"): + self.connect(self.environment.parsed_options.host) + else: + raise ValueError("Invalid host provided. Expected ws or wss protocol") + self.subscribe_all() def on_stop(self) -> None: - self.ws_greenlet.kill() - self.ws.close() + self._running = False + self.unsubscribe_all() + logging.debug("Unsubscribed from all subscriptions") def connect(self, host: str, **kwargs): - self.ws = websocket.create_connection(host, **kwargs) - self.ws_greenlet = gevent.spawn(self.receive_loop) + self._ws = websocket.create_connection(host, **kwargs) + self._ws_greenlet = gevent.spawn(self.receive_loop) - def on_message(self, message): # override this method in your subclass for custom handling - response_time = ((time.time_ns() - self.start_time) / 1_000_000).__round__() + def subscribe_all(self): + subscribe_methods = ["blockSubscribe"] + for method in subscribe_methods: + self.subscribe(method) + + def subscribe(self, method: str): + request_id = random.Random().randint(0, 1000000) + self.send({"id": request_id, "jsonrpc": "2.0", "method": method, + "params": ["all", { + "commitment": "confirmed", + "encoding": "base64", + "showRewards": True, + "transactionDetails": "full", + "maxSupportedTransactionVersion": 0 + }]}, "block_subscribe") + self._requests[request_id].update({"subscription": method}) + + def unsubscribe_all(self): + subscription_ids = list(self.subscriptions.keys()) + for subscription_id in subscription_ids: + self.send({"id": random.Random().randint(0, 1000000), "jsonrpc": "2.0", "method": "blockUnsubscribe", + "params": [subscription_id]}, "block_unsubscribe") + + def on_message(self, message): + try: + response = json.loads(message) + if "method" in response: + self.check_subscriptions(response, message) + else: + self.check_requests(response, message) + except JSONDecodeError: + self.environment.events.request.fire( + request_type="WSS", + name="JSONDecodeError", + response_time=None, + response_length=len(message), + exception=None, + ) + + def check_requests(self, response, message): + if "id" not in response: + logging.error("Received message without id") + logging.error(response) + return + if response["id"] not in self._requests: + logging.error("Received message with unknown id") + logging.error(response) + return + request = self._requests.pop(response["id"]) + if request["name"] == "blockSubscribe": + self.subscriptions.update({response["result"]: request["subscription"]}) self.environment.events.request.fire( - request_type="WSR", - name=self.name, - response_time=response_time, + request_type="WSS", + name=request["name"], + response_time=((time.time_ns() - request["start_time"]) / 1_000_000).__round__(), response_length=len(message), exception=None, ) + def check_subscriptions(self, response, message): + if response["method"] == "blockNotification": + if "params" in response: + if "subscription" in response["params"]: + self.environment.events.request.fire( + request_type="WSS Sub", + name="blockNotification", + response_time=time.time().__round__() - response["params"]["result"]["value"]["block"]["blockTime"], + response_length=len(message), + exception=None, + ) + def receive_loop(self): - while True: - message = self.ws.recv() + while self._running: + logging.debug(f"self.requests: {self._requests}") + message = self._ws.recv() logging.debug(f"WSR: {message}") self.on_message(message) + else: + self._ws.close() def send(self, body, name): + self._requests.update({body["id"]: {"name": name, "start_time": time.time_ns()}}) json_body = json.dumps(body) logging.debug(f"WSS: {json_body}") - self.start_time = time.time_ns() - self.name = name - self.ws.send(json_body) + self._ws.send(json_body) diff --git a/chainbench/util/event.py b/chainbench/util/event.py index e751049..63d4840 100644 --- a/chainbench/util/event.py +++ b/chainbench/util/event.py @@ -187,48 +187,57 @@ def on_init(environment: Environment, **_kwargs): logger.info(f"Initializing test data for {test_data_class_name}") print(f"Initializing test data for {test_data_class_name}") if environment.host: - user_test_data.init_http_client(environment.host) - if isinstance(user_test_data, EvmTestData): - chain_id: ChainId = user_test_data.fetch_chain_id() - user_test_data.init_network(chain_id) - logger.info(f"Target endpoint network is {user_test_data.network.name}") - print(f"Target endpoint network is {user_test_data.network.name}") - test_data["chain_id"] = {test_data_class_name: chain_id} + if environment.host.startswith("wss"): + user_test_data.init_http_client("https://nd-195-027-150.p2pify.com/681543cc4d120809ae5a1c973ac798e8") + else: + user_test_data.init_http_client(environment.host) + # if isinstance(user_test_data, EvmTestData): + # chain_id: ChainId = user_test_data.fetch_chain_id() + # user_test_data.init_network(chain_id) + # logger.info(f"Target endpoint network is {user_test_data.network.name}") + # print(f"Target endpoint network is {user_test_data.network.name}") + # test_data["chain_id"] = {test_data_class_name: chain_id} if environment.parsed_options: user_test_data.init_data(environment.parsed_options) test_data[test_data_class_name] = user_test_data.data.to_json() send_msg_to_workers(environment.runner, "test_data", test_data) - print("Fetching blocks...") - if environment.parsed_options.use_latest_blocks: - print(f"Using latest {user_test_data.data.size.blocks_len} blocks as test data") - logger.info(f"Using latest {user_test_data.data.size.blocks_len} blocks as test data") - for block_number in range( - user_test_data.data.block_range.start, user_test_data.data.block_range.end + 1 - ): - try: - block = user_test_data.fetch_block(block_number) - except (BlockNotFoundError, InvalidBlockError): - block = user_test_data.fetch_latest_block() - user_test_data.data.push_block(block) - block_data = {test_data_class_name: block.to_json()} - send_msg_to_workers(environment.runner, "block_data", block_data) - print(user_test_data.data.stats(), end="\r") - else: - print(user_test_data.data.stats(), end="\r") - print("\n") # new line after progress display upon exiting loop - else: - while user_test_data.data.size.blocks_len > len(user_test_data.data.blocks): - try: - block = user_test_data.fetch_random_block(user_test_data.data.block_numbers) - except (BlockNotFoundError, InvalidBlockError): - continue - user_test_data.data.push_block(block) - block_data = {test_data_class_name: block.to_json()} - send_msg_to_workers(environment.runner, "block_data", block_data) - print(user_test_data.data.stats(), end="\r") - else: - print(user_test_data.data.stats(), end="\r") - print("\n") # new line after progress display upon exiting loop + # print("Fetching blocks...") + # if environment.parsed_options.use_latest_blocks: + # print(f"Using latest {user_test_data.data.size.blocks_len} blocks as test data") + # logger.info(f"Using latest {user_test_data.data.size.blocks_len} blocks as test data") + # for block_number in range( + # user_test_data.data.block_range.start, user_test_data.data.block_range.end + 1 + # ): + # block = None + # try: + # block = user_test_data.fetch_block(block_number) + # except (BlockNotFoundError, InvalidBlockError): + # pass + # while block is None: + # try: + # block = user_test_data.fetch_latest_block() + # except (BlockNotFoundError, InvalidBlockError): + # pass + # user_test_data.data.push_block(block) + # block_data = {test_data_class_name: block.to_json()} + # send_msg_to_workers(environment.runner, "block_data", block_data) + # print(user_test_data.data.stats(), end="\r") + # else: + # print(user_test_data.data.stats(), end="\r") + # print("\n") # new line after progress display upon exiting loop + # else: + # while user_test_data.data.size.blocks_len > len(user_test_data.data.blocks): + # try: + # block = user_test_data.fetch_random_block(user_test_data.data.block_numbers) + # except (BlockNotFoundError, InvalidBlockError): + # continue + # user_test_data.data.push_block(block) + # block_data = {test_data_class_name: block.to_json()} + # send_msg_to_workers(environment.runner, "block_data", block_data) + # print(user_test_data.data.stats(), end="\r") + # else: + # print(user_test_data.data.stats(), end="\r") + # print("\n") # new line after progress display upon exiting loop logger.info("Test data is ready") send_msg_to_workers(environment.runner, "release_lock", {}) user_test_data.release_lock() From 59c431a90d3277c6851530b954368304a58b1184 Mon Sep 17 00:00:00 2001 From: Erwin Wee Date: Wed, 31 Jul 2024 18:13:54 +0800 Subject: [PATCH 03/21] try orjson --- chainbench/test_data/blockchain.py | 2 +- chainbench/test_data/ethereum.py | 2 +- chainbench/test_data/evm.py | 2 +- chainbench/test_data/solana.py | 2 +- chainbench/test_data/starknet.py | 2 +- chainbench/tools/discovery/rpc.py | 2 +- chainbench/user/wss.py | 6 +- chainbench/util/http.py | 4 +- chainbench/util/jsonrpc.py | 2 +- chainbench/util/monitor.py | 2 +- poetry.lock | 114 ++++++++++++++++++++++++++++- pyproject.toml | 2 + 12 files changed, 128 insertions(+), 14 deletions(-) diff --git a/chainbench/test_data/blockchain.py b/chainbench/test_data/blockchain.py index cd76266..946f2db 100644 --- a/chainbench/test_data/blockchain.py +++ b/chainbench/test_data/blockchain.py @@ -1,4 +1,4 @@ -import json +import orjson as json import logging import typing as t from argparse import Namespace diff --git a/chainbench/test_data/ethereum.py b/chainbench/test_data/ethereum.py index 34bc10d..a85c8ef 100644 --- a/chainbench/test_data/ethereum.py +++ b/chainbench/test_data/ethereum.py @@ -1,4 +1,4 @@ -import json +import orjson as json import logging import typing as t from argparse import Namespace diff --git a/chainbench/test_data/evm.py b/chainbench/test_data/evm.py index 2948d36..7487f92 100644 --- a/chainbench/test_data/evm.py +++ b/chainbench/test_data/evm.py @@ -1,4 +1,4 @@ -import json +import orjson as json import logging import typing as t from argparse import Namespace diff --git a/chainbench/test_data/solana.py b/chainbench/test_data/solana.py index f054247..0b67be7 100644 --- a/chainbench/test_data/solana.py +++ b/chainbench/test_data/solana.py @@ -1,4 +1,4 @@ -import json +import orjson as json import logging import typing as t from argparse import Namespace diff --git a/chainbench/test_data/starknet.py b/chainbench/test_data/starknet.py index ad91596..4123063 100644 --- a/chainbench/test_data/starknet.py +++ b/chainbench/test_data/starknet.py @@ -1,4 +1,4 @@ -import json +import orjson as json import logging import typing as t diff --git a/chainbench/tools/discovery/rpc.py b/chainbench/tools/discovery/rpc.py index b26910e..35510e8 100644 --- a/chainbench/tools/discovery/rpc.py +++ b/chainbench/tools/discovery/rpc.py @@ -1,4 +1,4 @@ -import json +import orjson as json import os from dataclasses import dataclass from pathlib import Path diff --git a/chainbench/user/wss.py b/chainbench/user/wss.py index 31190ae..50f990c 100644 --- a/chainbench/user/wss.py +++ b/chainbench/user/wss.py @@ -1,8 +1,8 @@ -import json +import orjson as json import logging import random import time -from json import JSONDecodeError +from orjson import JSONDecodeError import websocket import gevent @@ -50,7 +50,7 @@ def subscribe(self, method: str): self.send({"id": request_id, "jsonrpc": "2.0", "method": method, "params": ["all", { "commitment": "confirmed", - "encoding": "base64", + "encoding": "jsonParsed", "showRewards": True, "transactionDetails": "full", "maxSupportedTransactionVersion": 0 diff --git a/chainbench/util/http.py b/chainbench/util/http.py index 1a7f95e..075b4eb 100644 --- a/chainbench/util/http.py +++ b/chainbench/util/http.py @@ -1,10 +1,10 @@ -import json +import orjson as json import logging import typing as t from base64 import b64encode from enum import IntEnum from functools import cached_property -from json import JSONDecodeError +from orjson import JSONDecodeError from secrets import token_hex from geventhttpclient import URL, HTTPClient diff --git a/chainbench/util/jsonrpc.py b/chainbench/util/jsonrpc.py index 443f3aa..6b18726 100644 --- a/chainbench/util/jsonrpc.py +++ b/chainbench/util/jsonrpc.py @@ -1,4 +1,4 @@ -import json +import orjson as json import random import typing as t diff --git a/chainbench/util/monitor.py b/chainbench/util/monitor.py index 4e4f0dd..e30caa6 100644 --- a/chainbench/util/monitor.py +++ b/chainbench/util/monitor.py @@ -1,7 +1,7 @@ import csv import logging from datetime import datetime, timedelta -from json import JSONDecodeError +from orjson import JSONDecodeError from pathlib import Path from time import sleep diff --git a/poetry.lock b/poetry.lock index 15692ad..c1f4747 100644 --- a/poetry.lock +++ b/poetry.lock @@ -999,6 +999,58 @@ files = [ {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, ] +[[package]] +name = "msgspec" +version = "0.18.6" +description = "A fast serialization and validation library, with builtin support for JSON, MessagePack, YAML, and TOML." +optional = false +python-versions = ">=3.8" +files = [ + {file = "msgspec-0.18.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:77f30b0234eceeff0f651119b9821ce80949b4d667ad38f3bfed0d0ebf9d6d8f"}, + {file = "msgspec-0.18.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a76b60e501b3932782a9da039bd1cd552b7d8dec54ce38332b87136c64852dd"}, + {file = "msgspec-0.18.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06acbd6edf175bee0e36295d6b0302c6de3aaf61246b46f9549ca0041a9d7177"}, + {file = "msgspec-0.18.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40a4df891676d9c28a67c2cc39947c33de516335680d1316a89e8f7218660410"}, + {file = "msgspec-0.18.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a6896f4cd5b4b7d688018805520769a8446df911eb93b421c6c68155cdf9dd5a"}, + {file = "msgspec-0.18.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3ac4dd63fd5309dd42a8c8c36c1563531069152be7819518be0a9d03be9788e4"}, + {file = "msgspec-0.18.6-cp310-cp310-win_amd64.whl", hash = "sha256:fda4c357145cf0b760000c4ad597e19b53adf01382b711f281720a10a0fe72b7"}, + {file = "msgspec-0.18.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e77e56ffe2701e83a96e35770c6adb655ffc074d530018d1b584a8e635b4f36f"}, + {file = "msgspec-0.18.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5351afb216b743df4b6b147691523697ff3a2fc5f3d54f771e91219f5c23aaa"}, + {file = "msgspec-0.18.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3232fabacef86fe8323cecbe99abbc5c02f7698e3f5f2e248e3480b66a3596b"}, + {file = "msgspec-0.18.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3b524df6ea9998bbc99ea6ee4d0276a101bcc1aa8d14887bb823914d9f60d07"}, + {file = "msgspec-0.18.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:37f67c1d81272131895bb20d388dd8d341390acd0e192a55ab02d4d6468b434c"}, + {file = "msgspec-0.18.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d0feb7a03d971c1c0353de1a8fe30bb6579c2dc5ccf29b5f7c7ab01172010492"}, + {file = "msgspec-0.18.6-cp311-cp311-win_amd64.whl", hash = "sha256:41cf758d3f40428c235c0f27bc6f322d43063bc32da7b9643e3f805c21ed57b4"}, + {file = "msgspec-0.18.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d86f5071fe33e19500920333c11e2267a31942d18fed4d9de5bc2fbab267d28c"}, + {file = "msgspec-0.18.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce13981bfa06f5eb126a3a5a38b1976bddb49a36e4f46d8e6edecf33ccf11df1"}, + {file = "msgspec-0.18.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e97dec6932ad5e3ee1e3c14718638ba333befc45e0661caa57033cd4cc489466"}, + {file = "msgspec-0.18.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad237100393f637b297926cae1868b0d500f764ccd2f0623a380e2bcfb2809ca"}, + {file = "msgspec-0.18.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db1d8626748fa5d29bbd15da58b2d73af25b10aa98abf85aab8028119188ed57"}, + {file = "msgspec-0.18.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d70cb3d00d9f4de14d0b31d38dfe60c88ae16f3182988246a9861259c6722af6"}, + {file = "msgspec-0.18.6-cp312-cp312-win_amd64.whl", hash = "sha256:1003c20bfe9c6114cc16ea5db9c5466e49fae3d7f5e2e59cb70693190ad34da0"}, + {file = "msgspec-0.18.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f7d9faed6dfff654a9ca7d9b0068456517f63dbc3aa704a527f493b9200b210a"}, + {file = "msgspec-0.18.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9da21f804c1a1471f26d32b5d9bc0480450ea77fbb8d9db431463ab64aaac2cf"}, + {file = "msgspec-0.18.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46eb2f6b22b0e61c137e65795b97dc515860bf6ec761d8fb65fdb62aa094ba61"}, + {file = "msgspec-0.18.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8355b55c80ac3e04885d72db515817d9fbb0def3bab936bba104e99ad22cf46"}, + {file = "msgspec-0.18.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9080eb12b8f59e177bd1eb5c21e24dd2ba2fa88a1dbc9a98e05ad7779b54c681"}, + {file = "msgspec-0.18.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cc001cf39becf8d2dcd3f413a4797c55009b3a3cdbf78a8bf5a7ca8fdb76032c"}, + {file = "msgspec-0.18.6-cp38-cp38-win_amd64.whl", hash = "sha256:fac5834e14ac4da1fca373753e0c4ec9c8069d1fe5f534fa5208453b6065d5be"}, + {file = "msgspec-0.18.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:974d3520fcc6b824a6dedbdf2b411df31a73e6e7414301abac62e6b8d03791b4"}, + {file = "msgspec-0.18.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fd62e5818731a66aaa8e9b0a1e5543dc979a46278da01e85c3c9a1a4f047ef7e"}, + {file = "msgspec-0.18.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7481355a1adcf1f08dedd9311193c674ffb8bf7b79314b4314752b89a2cf7f1c"}, + {file = "msgspec-0.18.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6aa85198f8f154cf35d6f979998f6dadd3dc46a8a8c714632f53f5d65b315c07"}, + {file = "msgspec-0.18.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e24539b25c85c8f0597274f11061c102ad6b0c56af053373ba4629772b407be"}, + {file = "msgspec-0.18.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c61ee4d3be03ea9cd089f7c8e36158786cd06e51fbb62529276452bbf2d52ece"}, + {file = "msgspec-0.18.6-cp39-cp39-win_amd64.whl", hash = "sha256:b5c390b0b0b7da879520d4ae26044d74aeee5144f83087eb7842ba59c02bc090"}, + {file = "msgspec-0.18.6.tar.gz", hash = "sha256:a59fc3b4fcdb972d09138cb516dbde600c99d07c38fd9372a6ef500d2d031b4e"}, +] + +[package.extras] +dev = ["attrs", "coverage", "furo", "gcovr", "ipython", "msgpack", "mypy", "pre-commit", "pyright", "pytest", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "tomli", "tomli-w"] +doc = ["furo", "ipython", "sphinx", "sphinx-copybutton", "sphinx-design"] +test = ["attrs", "msgpack", "mypy", "pyright", "pytest", "pyyaml", "tomli", "tomli-w"] +toml = ["tomli", "tomli-w"] +yaml = ["pyyaml"] + [[package]] name = "mypy" version = "1.9.0" @@ -1071,6 +1123,66 @@ files = [ [package.dependencies] setuptools = "*" +[[package]] +name = "orjson" +version = "3.10.6" +description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" +optional = false +python-versions = ">=3.8" +files = [ + {file = "orjson-3.10.6-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:fb0ee33124db6eaa517d00890fc1a55c3bfe1cf78ba4a8899d71a06f2d6ff5c7"}, + {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c1c4b53b24a4c06547ce43e5fee6ec4e0d8fe2d597f4647fc033fd205707365"}, + {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eadc8fd310edb4bdbd333374f2c8fec6794bbbae99b592f448d8214a5e4050c0"}, + {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61272a5aec2b2661f4fa2b37c907ce9701e821b2c1285d5c3ab0207ebd358d38"}, + {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57985ee7e91d6214c837936dc1608f40f330a6b88bb13f5a57ce5257807da143"}, + {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:633a3b31d9d7c9f02d49c4ab4d0a86065c4a6f6adc297d63d272e043472acab5"}, + {file = "orjson-3.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1c680b269d33ec444afe2bdc647c9eb73166fa47a16d9a75ee56a374f4a45f43"}, + {file = "orjson-3.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f759503a97a6ace19e55461395ab0d618b5a117e8d0fbb20e70cfd68a47327f2"}, + {file = "orjson-3.10.6-cp310-none-win32.whl", hash = "sha256:95a0cce17f969fb5391762e5719575217bd10ac5a189d1979442ee54456393f3"}, + {file = "orjson-3.10.6-cp310-none-win_amd64.whl", hash = "sha256:df25d9271270ba2133cc88ee83c318372bdc0f2cd6f32e7a450809a111efc45c"}, + {file = "orjson-3.10.6-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b1ec490e10d2a77c345def52599311849fc063ae0e67cf4f84528073152bb2ba"}, + {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d43d3feb8f19d07e9f01e5b9be4f28801cf7c60d0fa0d279951b18fae1932b"}, + {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3045267e98fe749408eee1593a142e02357c5c99be0802185ef2170086a863"}, + {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c27bc6a28ae95923350ab382c57113abd38f3928af3c80be6f2ba7eb8d8db0b0"}, + {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d27456491ca79532d11e507cadca37fb8c9324a3976294f68fb1eff2dc6ced5a"}, + {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05ac3d3916023745aa3b3b388e91b9166be1ca02b7c7e41045da6d12985685f0"}, + {file = "orjson-3.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1335d4ef59ab85cab66fe73fd7a4e881c298ee7f63ede918b7faa1b27cbe5212"}, + {file = "orjson-3.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4bbc6d0af24c1575edc79994c20e1b29e6fb3c6a570371306db0993ecf144dc5"}, + {file = "orjson-3.10.6-cp311-none-win32.whl", hash = "sha256:450e39ab1f7694465060a0550b3f6d328d20297bf2e06aa947b97c21e5241fbd"}, + {file = "orjson-3.10.6-cp311-none-win_amd64.whl", hash = "sha256:227df19441372610b20e05bdb906e1742ec2ad7a66ac8350dcfd29a63014a83b"}, + {file = "orjson-3.10.6-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ea2977b21f8d5d9b758bb3f344a75e55ca78e3ff85595d248eee813ae23ecdfb"}, + {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6f3d167d13a16ed263b52dbfedff52c962bfd3d270b46b7518365bcc2121eed"}, + {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f710f346e4c44a4e8bdf23daa974faede58f83334289df80bc9cd12fe82573c7"}, + {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7275664f84e027dcb1ad5200b8b18373e9c669b2a9ec33d410c40f5ccf4b257e"}, + {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0943e4c701196b23c240b3d10ed8ecd674f03089198cf503105b474a4f77f21f"}, + {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:446dee5a491b5bc7d8f825d80d9637e7af43f86a331207b9c9610e2f93fee22a"}, + {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:64c81456d2a050d380786413786b057983892db105516639cb5d3ee3c7fd5148"}, + {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"}, + {file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"}, + {file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"}, + {file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"}, + {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"}, + {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"}, + {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2c116072a8533f2fec435fde4d134610f806bdac20188c7bd2081f3e9e0133f"}, + {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6eeb13218c8cf34c61912e9df2de2853f1d009de0e46ea09ccdf3d757896af0a"}, + {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:965a916373382674e323c957d560b953d81d7a8603fbeee26f7b8248638bd48b"}, + {file = "orjson-3.10.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:03c95484d53ed8e479cade8628c9cea00fd9d67f5554764a1110e0d5aa2de96e"}, + {file = "orjson-3.10.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e060748a04cccf1e0a6f2358dffea9c080b849a4a68c28b1b907f272b5127e9b"}, + {file = "orjson-3.10.6-cp38-none-win32.whl", hash = "sha256:738dbe3ef909c4b019d69afc19caf6b5ed0e2f1c786b5d6215fbb7539246e4c6"}, + {file = "orjson-3.10.6-cp38-none-win_amd64.whl", hash = "sha256:d40f839dddf6a7d77114fe6b8a70218556408c71d4d6e29413bb5f150a692ff7"}, + {file = "orjson-3.10.6-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:697a35a083c4f834807a6232b3e62c8b280f7a44ad0b759fd4dce748951e70db"}, + {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd502f96bf5ea9a61cbc0b2b5900d0dd68aa0da197179042bdd2be67e51a1e4b"}, + {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f215789fb1667cdc874c1b8af6a84dc939fd802bf293a8334fce185c79cd359b"}, + {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2debd8ddce948a8c0938c8c93ade191d2f4ba4649a54302a7da905a81f00b56"}, + {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5410111d7b6681d4b0d65e0f58a13be588d01b473822483f77f513c7f93bd3b2"}, + {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb1f28a137337fdc18384079fa5726810681055b32b92253fa15ae5656e1dddb"}, + {file = "orjson-3.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bf2fbbce5fe7cd1aa177ea3eab2b8e6a6bc6e8592e4279ed3db2d62e57c0e1b2"}, + {file = "orjson-3.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:79b9b9e33bd4c517445a62b90ca0cc279b0f1f3970655c3df9e608bc3f91741a"}, + {file = "orjson-3.10.6-cp39-none-win32.whl", hash = "sha256:30b0a09a2014e621b1adf66a4f705f0809358350a757508ee80209b2d8dae219"}, + {file = "orjson-3.10.6-cp39-none-win_amd64.whl", hash = "sha256:49e3bc615652617d463069f91b867a4458114c5b104e13b7ae6872e5f79d0844"}, + {file = "orjson-3.10.6.tar.gz", hash = "sha256:e54b63d0a7c6c54a5f5f726bc93a2078111ef060fec4ecbf34c5db800ca3b3a7"}, +] + [[package]] name = "packaging" version = "24.0" @@ -1710,4 +1822,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "2f24f6c908616b71e22e43315ab2bc78b92a8c30fb45f2ff7bc2eb1ac58282e3" +content-hash = "a80cdd558be49d13a97f751575bd9e059e68aa281a98f3137d5690a77422d8a6" diff --git a/pyproject.toml b/pyproject.toml index eda90fe..51981ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,8 @@ tenacity = "^8.2.2" base58 = "^2.1.1" solders = "^0.21.0" websocket-client = "^1.8.0" +orjson = "^3.10.6" +msgspec = "^0.18.6" [tool.poetry.group.dev.dependencies] black = ">=23.1,<25.0" From 306eda2f223c20e28233caaa26882f4fdc28a6ae Mon Sep 17 00:00:00 2001 From: Erwin Wee Date: Wed, 31 Jul 2024 18:20:19 +0800 Subject: [PATCH 04/21] formatting --- chainbench/profile/sandbox.py | 5 +--- chainbench/test_data/blockchain.py | 2 +- chainbench/test_data/ethereum.py | 2 +- chainbench/test_data/evm.py | 3 +- chainbench/test_data/solana.py | 2 +- chainbench/test_data/starknet.py | 3 +- chainbench/tools/discovery/rpc.py | 2 +- chainbench/user/http.py | 2 -- chainbench/user/jsonrpc.py | 7 ++++- chainbench/user/protocol/evm.py | 2 +- chainbench/user/wss.py | 46 +++++++++++++++++++++--------- chainbench/util/event.py | 4 ++- chainbench/util/http.py | 6 ++-- chainbench/util/jsonrpc.py | 3 +- chainbench/util/monitor.py | 2 +- 15 files changed, 57 insertions(+), 34 deletions(-) diff --git a/chainbench/profile/sandbox.py b/chainbench/profile/sandbox.py index 4e6608d..aeae317 100644 --- a/chainbench/profile/sandbox.py +++ b/chainbench/profile/sandbox.py @@ -1,9 +1,6 @@ -import random - from locust import constant_pacing, task -from chainbench.user import WssUser, SolanaUser - +from chainbench.user import SolanaUser, WssUser # TODO: Update Oasis profile to new format and update tutorial in documentation diff --git a/chainbench/test_data/blockchain.py b/chainbench/test_data/blockchain.py index 946f2db..8836283 100644 --- a/chainbench/test_data/blockchain.py +++ b/chainbench/test_data/blockchain.py @@ -1,9 +1,9 @@ -import orjson as json import logging import typing as t from argparse import Namespace from dataclasses import dataclass +import orjson as json from gevent.lock import Semaphore as GeventSemaphore from tenacity import retry, stop_after_attempt diff --git a/chainbench/test_data/ethereum.py b/chainbench/test_data/ethereum.py index a85c8ef..81df057 100644 --- a/chainbench/test_data/ethereum.py +++ b/chainbench/test_data/ethereum.py @@ -1,9 +1,9 @@ -import orjson as json import logging import typing as t from argparse import Namespace from dataclasses import dataclass +import orjson as json from tenacity import retry, stop_after_attempt, wait_fixed from chainbench.test_data.blockchain import ( diff --git a/chainbench/test_data/evm.py b/chainbench/test_data/evm.py index 7487f92..3f443c8 100644 --- a/chainbench/test_data/evm.py +++ b/chainbench/test_data/evm.py @@ -1,9 +1,10 @@ -import orjson as json import logging import typing as t from argparse import Namespace from dataclasses import dataclass +import orjson as json + from chainbench.util.rng import RNG, get_rng from .blockchain import ( diff --git a/chainbench/test_data/solana.py b/chainbench/test_data/solana.py index 0b67be7..eb71845 100644 --- a/chainbench/test_data/solana.py +++ b/chainbench/test_data/solana.py @@ -1,9 +1,9 @@ -import orjson as json import logging import typing as t from argparse import Namespace from dataclasses import dataclass +import orjson as json from tenacity import retry, stop_after_attempt from chainbench.util.rng import RNG, get_rng diff --git a/chainbench/test_data/starknet.py b/chainbench/test_data/starknet.py index 4123063..1dbfcf7 100644 --- a/chainbench/test_data/starknet.py +++ b/chainbench/test_data/starknet.py @@ -1,7 +1,8 @@ -import orjson as json import logging import typing as t +import orjson as json + from .blockchain import ( Account, BlockHash, diff --git a/chainbench/tools/discovery/rpc.py b/chainbench/tools/discovery/rpc.py index 35510e8..27d6cf6 100644 --- a/chainbench/tools/discovery/rpc.py +++ b/chainbench/tools/discovery/rpc.py @@ -1,9 +1,9 @@ -import orjson as json import os from dataclasses import dataclass from pathlib import Path from typing import Iterator +import orjson as json from tenacity import retry, retry_if_exception_type, wait_exponential from chainbench.util.http import HttpClient, HttpErrorLevel diff --git a/chainbench/user/http.py b/chainbench/user/http.py index f2c0c55..277b786 100644 --- a/chainbench/user/http.py +++ b/chainbench/user/http.py @@ -84,5 +84,3 @@ def get(self, name: str, params: t.Optional[dict] = None, path: str = "") -> Res with self.client.request("GET", path, params=params, name=name, catch_response=True) as response: self.check_http_error(response) return response - - diff --git a/chainbench/user/jsonrpc.py b/chainbench/user/jsonrpc.py index f4d101b..e48883e 100644 --- a/chainbench/user/jsonrpc.py +++ b/chainbench/user/jsonrpc.py @@ -5,7 +5,12 @@ from locust.contrib.fasthttp import ResponseContextManager from chainbench.user.http import HttpUser -from chainbench.util.jsonrpc import RpcCall, generate_request_body, generate_batch_request_body, expand_rpc_calls +from chainbench.util.jsonrpc import ( + RpcCall, + expand_rpc_calls, + generate_batch_request_body, + generate_request_body, +) class JsonRpcUser(HttpUser): diff --git a/chainbench/user/protocol/evm.py b/chainbench/user/protocol/evm.py index d48459b..284ade9 100644 --- a/chainbench/user/protocol/evm.py +++ b/chainbench/user/protocol/evm.py @@ -9,8 +9,8 @@ TxHash, ) from chainbench.user.jsonrpc import JsonRpcUser -from chainbench.util.jsonrpc import RpcCall from chainbench.user.tag import tag +from chainbench.util.jsonrpc import RpcCall from chainbench.util.rng import RNG, RNGManager diff --git a/chainbench/user/wss.py b/chainbench/user/wss.py index 50f990c..6818a9a 100644 --- a/chainbench/user/wss.py +++ b/chainbench/user/wss.py @@ -1,13 +1,13 @@ -import orjson as json import logging import random import time -from orjson import JSONDecodeError -import websocket import gevent +import orjson as json +import websocket from gevent import Greenlet from locust import User +from orjson import JSONDecodeError from websocket import WebSocket @@ -47,21 +47,38 @@ def subscribe_all(self): def subscribe(self, method: str): request_id = random.Random().randint(0, 1000000) - self.send({"id": request_id, "jsonrpc": "2.0", "method": method, - "params": ["all", { - "commitment": "confirmed", - "encoding": "jsonParsed", - "showRewards": True, - "transactionDetails": "full", - "maxSupportedTransactionVersion": 0 - }]}, "block_subscribe") + self.send( + { + "id": request_id, + "jsonrpc": "2.0", + "method": method, + "params": [ + "all", + { + "commitment": "confirmed", + "encoding": "jsonParsed", + "showRewards": True, + "transactionDetails": "full", + "maxSupportedTransactionVersion": 0, + }, + ], + }, + "block_subscribe", + ) self._requests[request_id].update({"subscription": method}) def unsubscribe_all(self): subscription_ids = list(self.subscriptions.keys()) for subscription_id in subscription_ids: - self.send({"id": random.Random().randint(0, 1000000), "jsonrpc": "2.0", "method": "blockUnsubscribe", - "params": [subscription_id]}, "block_unsubscribe") + self.send( + { + "id": random.Random().randint(0, 1000000), + "jsonrpc": "2.0", + "method": "blockUnsubscribe", + "params": [subscription_id], + }, + "block_unsubscribe", + ) def on_message(self, message): try: @@ -106,7 +123,8 @@ def check_subscriptions(self, response, message): self.environment.events.request.fire( request_type="WSS Sub", name="blockNotification", - response_time=time.time().__round__() - response["params"]["result"]["value"]["block"]["blockTime"], + response_time=time.time().__round__() + - response["params"]["result"]["value"]["block"]["blockTime"], response_length=len(message), exception=None, ) diff --git a/chainbench/util/event.py b/chainbench/util/event.py index 63d4840..76d24af 100644 --- a/chainbench/util/event.py +++ b/chainbench/util/event.py @@ -188,7 +188,9 @@ def on_init(environment: Environment, **_kwargs): print(f"Initializing test data for {test_data_class_name}") if environment.host: if environment.host.startswith("wss"): - user_test_data.init_http_client("https://nd-195-027-150.p2pify.com/681543cc4d120809ae5a1c973ac798e8") + user_test_data.init_http_client( + "https://nd-195-027-150.p2pify.com/681543cc4d120809ae5a1c973ac798e8" + ) else: user_test_data.init_http_client(environment.host) # if isinstance(user_test_data, EvmTestData): diff --git a/chainbench/util/http.py b/chainbench/util/http.py index 075b4eb..02e6b34 100644 --- a/chainbench/util/http.py +++ b/chainbench/util/http.py @@ -1,14 +1,14 @@ -import orjson as json import logging import typing as t from base64 import b64encode from enum import IntEnum from functools import cached_property -from orjson import JSONDecodeError from secrets import token_hex +import orjson as json from geventhttpclient import URL, HTTPClient from geventhttpclient.response import HTTPSocketPoolResponse +from orjson import JSONDecodeError logger = logging.getLogger(__name__) @@ -142,7 +142,7 @@ def post( headers.update({"Accept": "application/json"}) headers.update(self._general_headers) if isinstance(data, dict): - body = json.dumps(data).encode("utf-8") + body = json.dumps(data) elif isinstance(data, bytes): body = data else: diff --git a/chainbench/util/jsonrpc.py b/chainbench/util/jsonrpc.py index 6b18726..d3c4ad9 100644 --- a/chainbench/util/jsonrpc.py +++ b/chainbench/util/jsonrpc.py @@ -1,7 +1,8 @@ -import orjson as json import random import typing as t +import orjson as json + class RpcCall: def __init__(self, method: str, params: list[t.Any] | dict | None = None) -> None: diff --git a/chainbench/util/monitor.py b/chainbench/util/monitor.py index e30caa6..4098435 100644 --- a/chainbench/util/monitor.py +++ b/chainbench/util/monitor.py @@ -1,11 +1,11 @@ import csv import logging from datetime import datetime, timedelta -from orjson import JSONDecodeError from pathlib import Path from time import sleep from locust.util.timespan import parse_timespan +from orjson import JSONDecodeError from .http import HttpClient From fe5a592d320ce24d065d8abd90dc89ba29a512c6 Mon Sep 17 00:00:00 2001 From: Erwin Wee Date: Wed, 31 Jul 2024 18:24:00 +0800 Subject: [PATCH 05/21] Update blockchain.py --- chainbench/test_data/blockchain.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/chainbench/test_data/blockchain.py b/chainbench/test_data/blockchain.py index 8836283..99f0795 100644 --- a/chainbench/test_data/blockchain.py +++ b/chainbench/test_data/blockchain.py @@ -5,6 +5,7 @@ import orjson as json from gevent.lock import Semaphore as GeventSemaphore +from orjson.orjson import OPT_SORT_KEYS from tenacity import retry, stop_after_attempt from chainbench.util.http import HttpClient @@ -91,7 +92,7 @@ def __init__(self, size: Size, start: BlockNumber = 0, end: BlockNumber = 0): self.block_numbers: list[BlockNumber] = [] def to_json(self) -> str: - return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True) + return json.dumps(self, default=lambda o: o.__dict__, option=OPT_SORT_KEYS).decode("utf-8") def push_block(self, block: B) -> None: if block.block_number in self.block_numbers: From eff6756b73df1a0436b0342c909aa4cb4b299be3 Mon Sep 17 00:00:00 2001 From: Erwin Wee Date: Wed, 31 Jul 2024 19:11:47 +0800 Subject: [PATCH 06/21] try msgspec --- chainbench/user/wss.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/chainbench/user/wss.py b/chainbench/user/wss.py index 6818a9a..d7259ac 100644 --- a/chainbench/user/wss.py +++ b/chainbench/user/wss.py @@ -3,11 +3,11 @@ import time import gevent -import orjson as json +from msgspec import json import websocket from gevent import Greenlet from locust import User -from orjson import JSONDecodeError +from json import JSONDecodeError from websocket import WebSocket @@ -82,7 +82,7 @@ def unsubscribe_all(self): def on_message(self, message): try: - response = json.loads(message) + response = json.decode(message) if "method" in response: self.check_subscriptions(response, message) else: @@ -93,7 +93,8 @@ def on_message(self, message): name="JSONDecodeError", response_time=None, response_length=len(message), - exception=None, + exception=JSONDecodeError, + response=message, ) def check_requests(self, response, message): @@ -140,6 +141,6 @@ def receive_loop(self): def send(self, body, name): self._requests.update({body["id"]: {"name": name, "start_time": time.time_ns()}}) - json_body = json.dumps(body) + json_body = json.encode(body) logging.debug(f"WSS: {json_body}") self._ws.send(json_body) From ee98d93f17041dbc11b775790b4a82725fbde775 Mon Sep 17 00:00:00 2001 From: Erwin Wee Date: Wed, 31 Jul 2024 23:22:39 +0800 Subject: [PATCH 07/21] use orjson --- chainbench/user/wss.py | 56 ++++++++++++++++++++++++-------------- chainbench/util/jsonrpc.py | 2 +- poetry.lock | 54 +----------------------------------- pyproject.toml | 1 - 4 files changed, 37 insertions(+), 76 deletions(-) diff --git a/chainbench/user/wss.py b/chainbench/user/wss.py index d7259ac..3a79ed9 100644 --- a/chainbench/user/wss.py +++ b/chainbench/user/wss.py @@ -3,17 +3,18 @@ import time import gevent -from msgspec import json -import websocket +import orjson as json from gevent import Greenlet from locust import User -from json import JSONDecodeError -from websocket import WebSocket +from orjson import JSONDecodeError +from tenacity import retry, wait_fixed, retry_if_exception_type, stop_after_attempt +from websocket import WebSocket, WebSocketConnectionClosedException, create_connection class WssUser(User): abstract = True - + logger = logging.getLogger(__name__) + def __init__(self, environment): super().__init__(environment) self._ws: WebSocket | None = None @@ -34,11 +35,13 @@ def on_start(self) -> None: def on_stop(self) -> None: self._running = False self.unsubscribe_all() - logging.debug("Unsubscribed from all subscriptions") + self.logger.debug("Unsubscribed from all subscriptions") def connect(self, host: str, **kwargs): - self._ws = websocket.create_connection(host, **kwargs) + self._ws = create_connection(host, **kwargs) self._ws_greenlet = gevent.spawn(self.receive_loop) + if self._ws is not None: + self.logger.info(f"Connected to {host}") def subscribe_all(self): subscribe_methods = ["blockSubscribe"] @@ -82,7 +85,7 @@ def unsubscribe_all(self): def on_message(self, message): try: - response = json.decode(message) + response = json.loads(message) if "method" in response: self.check_subscriptions(response, message) else: @@ -99,12 +102,12 @@ def on_message(self, message): def check_requests(self, response, message): if "id" not in response: - logging.error("Received message without id") - logging.error(response) + self.logger.error("Received message without id") + self.logger.error(response) return if response["id"] not in self._requests: - logging.error("Received message with unknown id") - logging.error(response) + self.logger.error("Received message with unknown id") + self.logger.error(response) return request = self._requests.pop(response["id"]) if request["name"] == "blockSubscribe": @@ -131,16 +134,27 @@ def check_subscriptions(self, response, message): ) def receive_loop(self): - while self._running: - logging.debug(f"self.requests: {self._requests}") - message = self._ws.recv() - logging.debug(f"WSR: {message}") - self.on_message(message) - else: - self._ws.close() + try: + while self._running: + message = self._ws.recv() + self.logger.debug(f"WSR: {message}") + self.on_message(message) + else: + self._ws.close() + except WebSocketConnectionClosedException: + self.environment.events.request.fire( + request_type="WS", + name="WebSocket Connection", + response_time=None, + response_length=0, + exception=WebSocketConnectionClosedException, + ) + self._running = False + self.logger.error("Connection closed by server, trying to reconnect...") + self.on_start() def send(self, body, name): self._requests.update({body["id"]: {"name": name, "start_time": time.time_ns()}}) - json_body = json.encode(body) - logging.debug(f"WSS: {json_body}") + json_body = json.dumps(body) + self.logger.debug(f"WSS: {json_body}") self._ws.send(json_body) diff --git a/chainbench/util/jsonrpc.py b/chainbench/util/jsonrpc.py index d3c4ad9..2f3180f 100644 --- a/chainbench/util/jsonrpc.py +++ b/chainbench/util/jsonrpc.py @@ -29,7 +29,7 @@ def generate_request_body( } -def generate_batch_request_body(rpc_calls: list[RpcCall], version: str = "2.0") -> str: +def generate_batch_request_body(rpc_calls: list[RpcCall], version: str = "2.0") -> bytes: """Generate a batch JSON-RPC request body.""" return json.dumps( [ diff --git a/poetry.lock b/poetry.lock index c1f4747..1948ce4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -999,58 +999,6 @@ files = [ {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, ] -[[package]] -name = "msgspec" -version = "0.18.6" -description = "A fast serialization and validation library, with builtin support for JSON, MessagePack, YAML, and TOML." -optional = false -python-versions = ">=3.8" -files = [ - {file = "msgspec-0.18.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:77f30b0234eceeff0f651119b9821ce80949b4d667ad38f3bfed0d0ebf9d6d8f"}, - {file = "msgspec-0.18.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a76b60e501b3932782a9da039bd1cd552b7d8dec54ce38332b87136c64852dd"}, - {file = "msgspec-0.18.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06acbd6edf175bee0e36295d6b0302c6de3aaf61246b46f9549ca0041a9d7177"}, - {file = "msgspec-0.18.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40a4df891676d9c28a67c2cc39947c33de516335680d1316a89e8f7218660410"}, - {file = "msgspec-0.18.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a6896f4cd5b4b7d688018805520769a8446df911eb93b421c6c68155cdf9dd5a"}, - {file = "msgspec-0.18.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3ac4dd63fd5309dd42a8c8c36c1563531069152be7819518be0a9d03be9788e4"}, - {file = "msgspec-0.18.6-cp310-cp310-win_amd64.whl", hash = "sha256:fda4c357145cf0b760000c4ad597e19b53adf01382b711f281720a10a0fe72b7"}, - {file = "msgspec-0.18.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e77e56ffe2701e83a96e35770c6adb655ffc074d530018d1b584a8e635b4f36f"}, - {file = "msgspec-0.18.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5351afb216b743df4b6b147691523697ff3a2fc5f3d54f771e91219f5c23aaa"}, - {file = "msgspec-0.18.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3232fabacef86fe8323cecbe99abbc5c02f7698e3f5f2e248e3480b66a3596b"}, - {file = "msgspec-0.18.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3b524df6ea9998bbc99ea6ee4d0276a101bcc1aa8d14887bb823914d9f60d07"}, - {file = "msgspec-0.18.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:37f67c1d81272131895bb20d388dd8d341390acd0e192a55ab02d4d6468b434c"}, - {file = "msgspec-0.18.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d0feb7a03d971c1c0353de1a8fe30bb6579c2dc5ccf29b5f7c7ab01172010492"}, - {file = "msgspec-0.18.6-cp311-cp311-win_amd64.whl", hash = "sha256:41cf758d3f40428c235c0f27bc6f322d43063bc32da7b9643e3f805c21ed57b4"}, - {file = "msgspec-0.18.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d86f5071fe33e19500920333c11e2267a31942d18fed4d9de5bc2fbab267d28c"}, - {file = "msgspec-0.18.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce13981bfa06f5eb126a3a5a38b1976bddb49a36e4f46d8e6edecf33ccf11df1"}, - {file = "msgspec-0.18.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e97dec6932ad5e3ee1e3c14718638ba333befc45e0661caa57033cd4cc489466"}, - {file = "msgspec-0.18.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad237100393f637b297926cae1868b0d500f764ccd2f0623a380e2bcfb2809ca"}, - {file = "msgspec-0.18.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db1d8626748fa5d29bbd15da58b2d73af25b10aa98abf85aab8028119188ed57"}, - {file = "msgspec-0.18.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d70cb3d00d9f4de14d0b31d38dfe60c88ae16f3182988246a9861259c6722af6"}, - {file = "msgspec-0.18.6-cp312-cp312-win_amd64.whl", hash = "sha256:1003c20bfe9c6114cc16ea5db9c5466e49fae3d7f5e2e59cb70693190ad34da0"}, - {file = "msgspec-0.18.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f7d9faed6dfff654a9ca7d9b0068456517f63dbc3aa704a527f493b9200b210a"}, - {file = "msgspec-0.18.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9da21f804c1a1471f26d32b5d9bc0480450ea77fbb8d9db431463ab64aaac2cf"}, - {file = "msgspec-0.18.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46eb2f6b22b0e61c137e65795b97dc515860bf6ec761d8fb65fdb62aa094ba61"}, - {file = "msgspec-0.18.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8355b55c80ac3e04885d72db515817d9fbb0def3bab936bba104e99ad22cf46"}, - {file = "msgspec-0.18.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9080eb12b8f59e177bd1eb5c21e24dd2ba2fa88a1dbc9a98e05ad7779b54c681"}, - {file = "msgspec-0.18.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cc001cf39becf8d2dcd3f413a4797c55009b3a3cdbf78a8bf5a7ca8fdb76032c"}, - {file = "msgspec-0.18.6-cp38-cp38-win_amd64.whl", hash = "sha256:fac5834e14ac4da1fca373753e0c4ec9c8069d1fe5f534fa5208453b6065d5be"}, - {file = "msgspec-0.18.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:974d3520fcc6b824a6dedbdf2b411df31a73e6e7414301abac62e6b8d03791b4"}, - {file = "msgspec-0.18.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fd62e5818731a66aaa8e9b0a1e5543dc979a46278da01e85c3c9a1a4f047ef7e"}, - {file = "msgspec-0.18.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7481355a1adcf1f08dedd9311193c674ffb8bf7b79314b4314752b89a2cf7f1c"}, - {file = "msgspec-0.18.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6aa85198f8f154cf35d6f979998f6dadd3dc46a8a8c714632f53f5d65b315c07"}, - {file = "msgspec-0.18.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e24539b25c85c8f0597274f11061c102ad6b0c56af053373ba4629772b407be"}, - {file = "msgspec-0.18.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c61ee4d3be03ea9cd089f7c8e36158786cd06e51fbb62529276452bbf2d52ece"}, - {file = "msgspec-0.18.6-cp39-cp39-win_amd64.whl", hash = "sha256:b5c390b0b0b7da879520d4ae26044d74aeee5144f83087eb7842ba59c02bc090"}, - {file = "msgspec-0.18.6.tar.gz", hash = "sha256:a59fc3b4fcdb972d09138cb516dbde600c99d07c38fd9372a6ef500d2d031b4e"}, -] - -[package.extras] -dev = ["attrs", "coverage", "furo", "gcovr", "ipython", "msgpack", "mypy", "pre-commit", "pyright", "pytest", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "tomli", "tomli-w"] -doc = ["furo", "ipython", "sphinx", "sphinx-copybutton", "sphinx-design"] -test = ["attrs", "msgpack", "mypy", "pyright", "pytest", "pyyaml", "tomli", "tomli-w"] -toml = ["tomli", "tomli-w"] -yaml = ["pyyaml"] - [[package]] name = "mypy" version = "1.9.0" @@ -1822,4 +1770,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "a80cdd558be49d13a97f751575bd9e059e68aa281a98f3137d5690a77422d8a6" +content-hash = "5945688edff4feca256f9acf43dc575c29dd247b3e65a6082fda4d1008f3bccf" diff --git a/pyproject.toml b/pyproject.toml index 51981ae..95caf2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,6 @@ base58 = "^2.1.1" solders = "^0.21.0" websocket-client = "^1.8.0" orjson = "^3.10.6" -msgspec = "^0.18.6" [tool.poetry.group.dev.dependencies] black = ">=23.1,<25.0" From 8de228d83c6a0d4ec627c1fa0c1dbbe566f49e96 Mon Sep 17 00:00:00 2001 From: Erwin Wee Date: Thu, 1 Aug 2024 16:01:30 +0800 Subject: [PATCH 08/21] try jsonpath --- chainbench/user/wss.py | 44 ++++++++++++++++---------------------- chainbench/util/jsonrpc.py | 4 ++-- poetry.lock | 27 ++++++++++++++++++++++- pyproject.toml | 1 + 4 files changed, 48 insertions(+), 28 deletions(-) diff --git a/chainbench/user/wss.py b/chainbench/user/wss.py index 3a79ed9..3398154 100644 --- a/chainbench/user/wss.py +++ b/chainbench/user/wss.py @@ -1,20 +1,25 @@ import logging import random import time +import typing import gevent +import jsonpath_ng import orjson as json from gevent import Greenlet from locust import User from orjson import JSONDecodeError -from tenacity import retry, wait_fixed, retry_if_exception_type, stop_after_attempt from websocket import WebSocket, WebSocketConnectionClosedException, create_connection +def jsonpath_get_values(path, json_data) -> list[typing.Any]: + return [match.value for match in jsonpath_ng.parse(path).find(json_data)] + + class WssUser(User): abstract = True logger = logging.getLogger(__name__) - + def __init__(self, environment): super().__init__(environment) self._ws: WebSocket | None = None @@ -85,11 +90,16 @@ def unsubscribe_all(self): def on_message(self, message): try: - response = json.loads(message) - if "method" in response: - self.check_subscriptions(response, message) - else: - self.check_requests(response, message) + if jsonpath_get_values("id", message): + self.check_requests(message) + elif blockTime := jsonpath_get_values("params.result.value.block.blockTime", message): + self.environment.events.request.fire( + request_type="WSS Sub", + name="blockNotification", + response_time=time.time().__round__() - blockTime[0].value, + response_length=len(message), + exception=None, + ) except JSONDecodeError: self.environment.events.request.fire( request_type="WSS", @@ -100,11 +110,8 @@ def on_message(self, message): response=message, ) - def check_requests(self, response, message): - if "id" not in response: - self.logger.error("Received message without id") - self.logger.error(response) - return + def check_requests(self, message): + response = json.loads(message) if response["id"] not in self._requests: self.logger.error("Received message with unknown id") self.logger.error(response) @@ -120,19 +127,6 @@ def check_requests(self, response, message): exception=None, ) - def check_subscriptions(self, response, message): - if response["method"] == "blockNotification": - if "params" in response: - if "subscription" in response["params"]: - self.environment.events.request.fire( - request_type="WSS Sub", - name="blockNotification", - response_time=time.time().__round__() - - response["params"]["result"]["value"]["block"]["blockTime"], - response_length=len(message), - exception=None, - ) - def receive_loop(self): try: while self._running: diff --git a/chainbench/util/jsonrpc.py b/chainbench/util/jsonrpc.py index 2f3180f..74c691c 100644 --- a/chainbench/util/jsonrpc.py +++ b/chainbench/util/jsonrpc.py @@ -29,14 +29,14 @@ def generate_request_body( } -def generate_batch_request_body(rpc_calls: list[RpcCall], version: str = "2.0") -> bytes: +def generate_batch_request_body(rpc_calls: list[RpcCall], version: str = "2.0") -> str: """Generate a batch JSON-RPC request body.""" return json.dumps( [ generate_request_body(rpc_calls[i].method, rpc_calls[i].params, request_id=i, version=version) for i in range(1, len(rpc_calls)) ] - ) + ).decode("utf-8") def expand_rpc_calls(rpc_calls_weighted: dict[t.Callable[[], RpcCall], int]) -> list[RpcCall]: diff --git a/poetry.lock b/poetry.lock index 1948ce4..572ec06 100644 --- a/poetry.lock +++ b/poetry.lock @@ -797,6 +797,20 @@ files = [ {file = "jsonalias-0.1.1.tar.gz", hash = "sha256:64f04d935397d579fc94509e1fcb6212f2d081235d9d6395bd10baedf760a769"}, ] +[[package]] +name = "jsonpath-ng" +version = "1.6.1" +description = "A final implementation of JSONPath for Python that aims to be standard compliant, including arithmetic and binary comparison operators and providing clear AST for metaprogramming." +optional = false +python-versions = "*" +files = [ + {file = "jsonpath-ng-1.6.1.tar.gz", hash = "sha256:086c37ba4917304850bd837aeab806670224d3f038fe2833ff593a672ef0a5fa"}, + {file = "jsonpath_ng-1.6.1-py3-none-any.whl", hash = "sha256:8f22cd8273d7772eea9aaa84d922e0841aa36fdb8a2c6b7f6c3791a16a9bc0be"}, +] + +[package.dependencies] +ply = "*" + [[package]] name = "locust" version = "2.25.0" @@ -1168,6 +1182,17 @@ files = [ docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +[[package]] +name = "ply" +version = "3.11" +description = "Python Lex & Yacc" +optional = false +python-versions = "*" +files = [ + {file = "ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce"}, + {file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"}, +] + [[package]] name = "pre-commit" version = "3.7.0" @@ -1770,4 +1795,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "5945688edff4feca256f9acf43dc575c29dd247b3e65a6082fda4d1008f3bccf" +content-hash = "e0c0809fd8377c655054e61aa6a6f18f93c298aee2e3553762539ae9a7d6436b" diff --git a/pyproject.toml b/pyproject.toml index 95caf2c..508c3a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ base58 = "^2.1.1" solders = "^0.21.0" websocket-client = "^1.8.0" orjson = "^3.10.6" +jsonpath-ng = "^1.6.1" [tool.poetry.group.dev.dependencies] black = ">=23.1,<25.0" From 49c888ea9dfdbac876dd3ce7ad462a66ba30207d Mon Sep 17 00:00:00 2001 From: Erwin Wee Date: Thu, 1 Aug 2024 18:48:06 +0800 Subject: [PATCH 09/21] try msgspec struct --- chainbench/user/wss.py | 46 ++++++++++++++++++++++++-------- chainbench/util/jsonrpc.py | 1 + poetry.lock | 54 +++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 4 files changed, 90 insertions(+), 12 deletions(-) diff --git a/chainbench/user/wss.py b/chainbench/user/wss.py index 3398154..6e606a6 100644 --- a/chainbench/user/wss.py +++ b/chainbench/user/wss.py @@ -1,19 +1,41 @@ import logging import random import time -import typing import gevent -import jsonpath_ng -import orjson as json +import msgspec.json +import orjson from gevent import Greenlet from locust import User +from msgspec import Struct, json from orjson import JSONDecodeError from websocket import WebSocket, WebSocketConnectionClosedException, create_connection -def jsonpath_get_values(path, json_data) -> list[typing.Any]: - return [match.value for match in jsonpath_ng.parse(path).find(json_data)] +class Block(Struct): + blockTime: int + + +class JValue(Struct): + block: Block = None + + +class JResult(Struct): + value: JValue + + +class JParams(Struct): + result: JResult + + +class JsonRpcMessage(Struct): + jsonrpc: str + method: str = None + params: JParams = None + id: int = None + + +loads = msgspec.json.Decoder(JsonRpcMessage).decode class WssUser(User): @@ -90,13 +112,14 @@ def unsubscribe_all(self): def on_message(self, message): try: - if jsonpath_get_values("id", message): + parsed_json: JsonRpcMessage = loads(message) + if parsed_json.id is not None: self.check_requests(message) - elif blockTime := jsonpath_get_values("params.result.value.block.blockTime", message): + elif blockTime := parsed_json.params.result.value.block.blockTime: self.environment.events.request.fire( request_type="WSS Sub", name="blockNotification", - response_time=time.time().__round__() - blockTime[0].value, + response_time=time.time().__round__() - blockTime, response_length=len(message), exception=None, ) @@ -111,7 +134,7 @@ def on_message(self, message): ) def check_requests(self, message): - response = json.loads(message) + response = orjson.loads(message) if response["id"] not in self._requests: self.logger.error("Received message with unknown id") self.logger.error(response) @@ -131,7 +154,8 @@ def receive_loop(self): try: while self._running: message = self._ws.recv() - self.logger.debug(f"WSR: {message}") + self.logger.debug(f"WSR") + # self.logger.debug(f"WSR: {message}") self.on_message(message) else: self._ws.close() @@ -149,6 +173,6 @@ def receive_loop(self): def send(self, body, name): self._requests.update({body["id"]: {"name": name, "start_time": time.time_ns()}}) - json_body = json.dumps(body) + json_body = orjson.dumps(body) self.logger.debug(f"WSS: {json_body}") self._ws.send(json_body) diff --git a/chainbench/util/jsonrpc.py b/chainbench/util/jsonrpc.py index 74c691c..b59b0cc 100644 --- a/chainbench/util/jsonrpc.py +++ b/chainbench/util/jsonrpc.py @@ -1,6 +1,7 @@ import random import typing as t +import msgspec.json import orjson as json diff --git a/poetry.lock b/poetry.lock index 572ec06..d128e14 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1013,6 +1013,58 @@ files = [ {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, ] +[[package]] +name = "msgspec" +version = "0.18.6" +description = "A fast serialization and validation library, with builtin support for JSON, MessagePack, YAML, and TOML." +optional = false +python-versions = ">=3.8" +files = [ + {file = "msgspec-0.18.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:77f30b0234eceeff0f651119b9821ce80949b4d667ad38f3bfed0d0ebf9d6d8f"}, + {file = "msgspec-0.18.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a76b60e501b3932782a9da039bd1cd552b7d8dec54ce38332b87136c64852dd"}, + {file = "msgspec-0.18.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06acbd6edf175bee0e36295d6b0302c6de3aaf61246b46f9549ca0041a9d7177"}, + {file = "msgspec-0.18.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40a4df891676d9c28a67c2cc39947c33de516335680d1316a89e8f7218660410"}, + {file = "msgspec-0.18.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a6896f4cd5b4b7d688018805520769a8446df911eb93b421c6c68155cdf9dd5a"}, + {file = "msgspec-0.18.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3ac4dd63fd5309dd42a8c8c36c1563531069152be7819518be0a9d03be9788e4"}, + {file = "msgspec-0.18.6-cp310-cp310-win_amd64.whl", hash = "sha256:fda4c357145cf0b760000c4ad597e19b53adf01382b711f281720a10a0fe72b7"}, + {file = "msgspec-0.18.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e77e56ffe2701e83a96e35770c6adb655ffc074d530018d1b584a8e635b4f36f"}, + {file = "msgspec-0.18.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5351afb216b743df4b6b147691523697ff3a2fc5f3d54f771e91219f5c23aaa"}, + {file = "msgspec-0.18.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3232fabacef86fe8323cecbe99abbc5c02f7698e3f5f2e248e3480b66a3596b"}, + {file = "msgspec-0.18.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3b524df6ea9998bbc99ea6ee4d0276a101bcc1aa8d14887bb823914d9f60d07"}, + {file = "msgspec-0.18.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:37f67c1d81272131895bb20d388dd8d341390acd0e192a55ab02d4d6468b434c"}, + {file = "msgspec-0.18.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d0feb7a03d971c1c0353de1a8fe30bb6579c2dc5ccf29b5f7c7ab01172010492"}, + {file = "msgspec-0.18.6-cp311-cp311-win_amd64.whl", hash = "sha256:41cf758d3f40428c235c0f27bc6f322d43063bc32da7b9643e3f805c21ed57b4"}, + {file = "msgspec-0.18.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d86f5071fe33e19500920333c11e2267a31942d18fed4d9de5bc2fbab267d28c"}, + {file = "msgspec-0.18.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce13981bfa06f5eb126a3a5a38b1976bddb49a36e4f46d8e6edecf33ccf11df1"}, + {file = "msgspec-0.18.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e97dec6932ad5e3ee1e3c14718638ba333befc45e0661caa57033cd4cc489466"}, + {file = "msgspec-0.18.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad237100393f637b297926cae1868b0d500f764ccd2f0623a380e2bcfb2809ca"}, + {file = "msgspec-0.18.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db1d8626748fa5d29bbd15da58b2d73af25b10aa98abf85aab8028119188ed57"}, + {file = "msgspec-0.18.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d70cb3d00d9f4de14d0b31d38dfe60c88ae16f3182988246a9861259c6722af6"}, + {file = "msgspec-0.18.6-cp312-cp312-win_amd64.whl", hash = "sha256:1003c20bfe9c6114cc16ea5db9c5466e49fae3d7f5e2e59cb70693190ad34da0"}, + {file = "msgspec-0.18.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f7d9faed6dfff654a9ca7d9b0068456517f63dbc3aa704a527f493b9200b210a"}, + {file = "msgspec-0.18.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9da21f804c1a1471f26d32b5d9bc0480450ea77fbb8d9db431463ab64aaac2cf"}, + {file = "msgspec-0.18.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46eb2f6b22b0e61c137e65795b97dc515860bf6ec761d8fb65fdb62aa094ba61"}, + {file = "msgspec-0.18.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8355b55c80ac3e04885d72db515817d9fbb0def3bab936bba104e99ad22cf46"}, + {file = "msgspec-0.18.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9080eb12b8f59e177bd1eb5c21e24dd2ba2fa88a1dbc9a98e05ad7779b54c681"}, + {file = "msgspec-0.18.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cc001cf39becf8d2dcd3f413a4797c55009b3a3cdbf78a8bf5a7ca8fdb76032c"}, + {file = "msgspec-0.18.6-cp38-cp38-win_amd64.whl", hash = "sha256:fac5834e14ac4da1fca373753e0c4ec9c8069d1fe5f534fa5208453b6065d5be"}, + {file = "msgspec-0.18.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:974d3520fcc6b824a6dedbdf2b411df31a73e6e7414301abac62e6b8d03791b4"}, + {file = "msgspec-0.18.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fd62e5818731a66aaa8e9b0a1e5543dc979a46278da01e85c3c9a1a4f047ef7e"}, + {file = "msgspec-0.18.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7481355a1adcf1f08dedd9311193c674ffb8bf7b79314b4314752b89a2cf7f1c"}, + {file = "msgspec-0.18.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6aa85198f8f154cf35d6f979998f6dadd3dc46a8a8c714632f53f5d65b315c07"}, + {file = "msgspec-0.18.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e24539b25c85c8f0597274f11061c102ad6b0c56af053373ba4629772b407be"}, + {file = "msgspec-0.18.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c61ee4d3be03ea9cd089f7c8e36158786cd06e51fbb62529276452bbf2d52ece"}, + {file = "msgspec-0.18.6-cp39-cp39-win_amd64.whl", hash = "sha256:b5c390b0b0b7da879520d4ae26044d74aeee5144f83087eb7842ba59c02bc090"}, + {file = "msgspec-0.18.6.tar.gz", hash = "sha256:a59fc3b4fcdb972d09138cb516dbde600c99d07c38fd9372a6ef500d2d031b4e"}, +] + +[package.extras] +dev = ["attrs", "coverage", "furo", "gcovr", "ipython", "msgpack", "mypy", "pre-commit", "pyright", "pytest", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "tomli", "tomli-w"] +doc = ["furo", "ipython", "sphinx", "sphinx-copybutton", "sphinx-design"] +test = ["attrs", "msgpack", "mypy", "pyright", "pytest", "pyyaml", "tomli", "tomli-w"] +toml = ["tomli", "tomli-w"] +yaml = ["pyyaml"] + [[package]] name = "mypy" version = "1.9.0" @@ -1795,4 +1847,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "e0c0809fd8377c655054e61aa6a6f18f93c298aee2e3553762539ae9a7d6436b" +content-hash = "fc834247ce1e3e2e56ea81b5e880856ddaec614217fda49d4ea34369c5c4eba3" diff --git a/pyproject.toml b/pyproject.toml index 508c3a6..9a3878e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ solders = "^0.21.0" websocket-client = "^1.8.0" orjson = "^3.10.6" jsonpath-ng = "^1.6.1" +msgspec = "^0.18.6" [tool.poetry.group.dev.dependencies] black = ">=23.1,<25.0" From ee82220dc928fba0b72ed31cb91c15bdbbc75929 Mon Sep 17 00:00:00 2001 From: Erwin Wee Date: Fri, 2 Aug 2024 13:49:02 +0800 Subject: [PATCH 10/21] rename requests --- chainbench/user/wss.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chainbench/user/wss.py b/chainbench/user/wss.py index 6e606a6..4fb262d 100644 --- a/chainbench/user/wss.py +++ b/chainbench/user/wss.py @@ -93,7 +93,7 @@ def subscribe(self, method: str): }, ], }, - "block_subscribe", + "blockSubscribe", ) self._requests[request_id].update({"subscription": method}) @@ -107,7 +107,7 @@ def unsubscribe_all(self): "method": "blockUnsubscribe", "params": [subscription_id], }, - "block_unsubscribe", + "blockUnsubscribe", ) def on_message(self, message): From 983353818ce3ea66a49836ff36cb4ad628021d0f Mon Sep 17 00:00:00 2001 From: Erwin Wee Date: Tue, 6 Aug 2024 23:55:59 +0800 Subject: [PATCH 11/21] remove blockTime --- chainbench/profile/solana/get_block.py | 22 ++++++++++++++++++++++ chainbench/user/protocol/solana.py | 3 ++- chainbench/user/wss.py | 23 +++-------------------- 3 files changed, 27 insertions(+), 21 deletions(-) create mode 100644 chainbench/profile/solana/get_block.py diff --git a/chainbench/profile/solana/get_block.py b/chainbench/profile/solana/get_block.py new file mode 100644 index 0000000..f565e48 --- /dev/null +++ b/chainbench/profile/solana/get_block.py @@ -0,0 +1,22 @@ +from locust import task + +from chainbench.user.protocol.solana import SolanaUser +from chainbench.util.jsonrpc import RpcCall + + +class SolanaGetBlock(SolanaUser): + @task + def get_block_task(self): + self.make_rpc_call( + RpcCall( + method="getBlock", + params=[ + self.test_data.get_random_block_number(self.rng.get_rng()), + { + "encoding": "jsonParsed", + "transactionDetails": "full", + "maxSupportedTransactionVersion": 0, + }, + ] + ) + ) diff --git a/chainbench/user/protocol/solana.py b/chainbench/user/protocol/solana.py index 8632343..d0f879e 100644 --- a/chainbench/user/protocol/solana.py +++ b/chainbench/user/protocol/solana.py @@ -6,12 +6,13 @@ from chainbench.test_data import Account, BlockNumber, SolanaTestData, TxHash from chainbench.user.jsonrpc import JsonRpcUser from chainbench.util.jsonrpc import RpcCall -from chainbench.util.rng import RNG +from chainbench.util.rng import RNG, RNGManager class SolanaBaseUser(JsonRpcUser): abstract = True test_data = SolanaTestData() + rng = RNGManager() rpc_error_code_exclusions = [-32007] def _create_random_transaction_message(self, rng: RNG) -> Message: diff --git a/chainbench/user/wss.py b/chainbench/user/wss.py index 4fb262d..c19e043 100644 --- a/chainbench/user/wss.py +++ b/chainbench/user/wss.py @@ -12,26 +12,9 @@ from websocket import WebSocket, WebSocketConnectionClosedException, create_connection -class Block(Struct): - blockTime: int - - -class JValue(Struct): - block: Block = None - - -class JResult(Struct): - value: JValue - - -class JParams(Struct): - result: JResult - - class JsonRpcMessage(Struct): jsonrpc: str method: str = None - params: JParams = None id: int = None @@ -115,11 +98,11 @@ def on_message(self, message): parsed_json: JsonRpcMessage = loads(message) if parsed_json.id is not None: self.check_requests(message) - elif blockTime := parsed_json.params.result.value.block.blockTime: + elif parsed_json.method is not None: self.environment.events.request.fire( request_type="WSS Sub", - name="blockNotification", - response_time=time.time().__round__() - blockTime, + name=parsed_json.method, + response_time=None, response_length=len(message), exception=None, ) From d27db9b922e0a8aa1167e4116c55ae03029c6dec Mon Sep 17 00:00:00 2001 From: Erwin Wee Date: Wed, 7 Aug 2024 15:00:10 +0800 Subject: [PATCH 12/21] Update wss.py --- chainbench/user/wss.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/chainbench/user/wss.py b/chainbench/user/wss.py index c19e043..dce096b 100644 --- a/chainbench/user/wss.py +++ b/chainbench/user/wss.py @@ -12,9 +12,26 @@ from websocket import WebSocket, WebSocketConnectionClosedException, create_connection +class Block(Struct): + blockTime: int + + +class JValue(Struct): + block: Block = None + + +class JResult(Struct): + value: JValue + + +class JParams(Struct): + result: JResult + + class JsonRpcMessage(Struct): jsonrpc: str method: str = None + params: JParams = None id: int = None @@ -98,11 +115,11 @@ def on_message(self, message): parsed_json: JsonRpcMessage = loads(message) if parsed_json.id is not None: self.check_requests(message) - elif parsed_json.method is not None: + elif blockTime := parsed_json.params.result.value.block.blockTime: self.environment.events.request.fire( request_type="WSS Sub", name=parsed_json.method, - response_time=None, + response_time=time.time().__round__() - blockTime, response_length=len(message), exception=None, ) @@ -140,6 +157,7 @@ def receive_loop(self): self.logger.debug(f"WSR") # self.logger.debug(f"WSR: {message}") self.on_message(message) + gevent.sleep(0) else: self._ws.close() except WebSocketConnectionClosedException: From 00bff44cd89df9024c86a99348d43ca8cdde0535 Mon Sep 17 00:00:00 2001 From: Erwin Wee Date: Fri, 16 Aug 2024 18:02:54 +0800 Subject: [PATCH 13/21] fix 100% cpu --- chainbench/profile/sandbox.py | 41 +++++-- chainbench/user/__init__.py | 4 +- chainbench/user/jsonrpc.py | 8 +- chainbench/user/wss.py | 225 +++++++++++++++++++--------------- chainbench/util/cli.py | 2 +- chainbench/util/jsonrpc.py | 42 ++++--- chainbench/util/ws.py | 43 +++++++ poetry.lock | 79 +----------- pyproject.toml | 2 - 9 files changed, 233 insertions(+), 213 deletions(-) create mode 100644 chainbench/util/ws.py diff --git a/chainbench/profile/sandbox.py b/chainbench/profile/sandbox.py index aeae317..fa71324 100644 --- a/chainbench/profile/sandbox.py +++ b/chainbench/profile/sandbox.py @@ -1,16 +1,43 @@ -from locust import constant_pacing, task +from chainbench.user import SolanaUser, WssJrpcUser +from chainbench.user.wss import WSSubscription -from chainbench.user import SolanaUser, WssUser # TODO: Update Oasis profile to new format and update tutorial in documentation -class SandboxProfile(WssUser, SolanaUser): - wait_time = constant_pacing(1) +class SandboxProfile(WssJrpcUser, SolanaUser): + # wait_time = constant_pacing(1) + + subscriptions = [ + WSSubscription("blockSubscribe", [ + "all", + { + "commitment": "confirmed", + "encoding": "jsonParsed", + "showRewards": True, + "transactionDetails": "full", + "maxSupportedTransactionVersion": 0 + } + ], "blockUnsubscribe"), + # WSSubscription("slotSubscribe", [], "slotUnsubscribe") + ] + + # subscriptions = [ + # WSSubscription("eth_subscribe", ["newHeads"], "eth_unsubscribe"), + # WSSubscription("eth_subscribe", [ + # "logs", + # { + # "address": "0x8320fe7702b96808f7bbc0d4a888ed1468216cfd", + # "topics": ["0xd78a0cb8bb633d06981248b816e7bd33c2a35a6089241d099fa519e361cab902"] + # } + # ], "eth_unsubscribe"), + # WSSubscription("eth_subscribe", ["newPendingTransactions"], "eth_unsubscribe"), + # WSSubscription("eth_subscribe", ["syncing"], "eth_unsubscribe"), + # ] + # + # def get_notification_name(self, parsed_response: dict): + # return self.subscriptions[self.subscription_ids_to_index[parsed_response["params"]["subscription"]]].subscribe_rpc_call.params[0] - @task - def dummy_task(self): - pass # # @task diff --git a/chainbench/user/__init__.py b/chainbench/user/__init__.py index 5a3babd..acd82b7 100644 --- a/chainbench/user/__init__.py +++ b/chainbench/user/__init__.py @@ -3,7 +3,7 @@ from .common import get_subclass_tasks from .http import HttpUser -from .wss import WssUser +from .wss import WssJrpcUser # importing plugins here as all profiles depend on it import locust_plugins # isort: skip # noqa @@ -16,6 +16,6 @@ "HttpUser", "SolanaUser", "StarkNetUser", - "WssUser", + "WssJrpcUser", "get_subclass_tasks", ] diff --git a/chainbench/user/jsonrpc.py b/chainbench/user/jsonrpc.py index e48883e..c203ad7 100644 --- a/chainbench/user/jsonrpc.py +++ b/chainbench/user/jsonrpc.py @@ -9,7 +9,6 @@ RpcCall, expand_rpc_calls, generate_batch_request_body, - generate_request_body, ) @@ -99,15 +98,14 @@ def make_rpc_call( path: str = "", ) -> None: """Make a JSON-RPC call.""" - if rpc_call is not None: - method = rpc_call.method - params = rpc_call.params + if rpc_call is None: + rpc_call = RpcCall(method, params) if name == "" and method is not None: name = method with self.client.request( - "POST", self.rpc_path + path, json=generate_request_body(method, params), name=name, catch_response=True + "POST", self.rpc_path + path, json=rpc_call.request_body(), name=name, catch_response=True ) as response: self.check_http_error(response) self.check_json_rpc_response(response, name=name) diff --git a/chainbench/user/wss.py b/chainbench/user/wss.py index dce096b..40a4cae 100644 --- a/chainbench/user/wss.py +++ b/chainbench/user/wss.py @@ -1,131 +1,153 @@ import logging -import random import time import gevent -import msgspec.json -import orjson -from gevent import Greenlet -from locust import User -from msgspec import Struct, json +import orjson as json +from gevent import Greenlet, Timeout +from locust import User, task from orjson import JSONDecodeError from websocket import WebSocket, WebSocketConnectionClosedException, create_connection +from locust.env import Environment +from chainbench.util.jsonrpc import RpcCall -class Block(Struct): - blockTime: int +class WSSubscription: + def __init__( + self, + subscribe_method: str, + subscribe_params: dict | list, + unsubscribe_method: str + ): + self.subscribe_rpc_call: RpcCall = RpcCall(subscribe_method, subscribe_params) + self.unsubscribe_method: str = unsubscribe_method + self.subscribed: bool = False + self._subscription_id: int | str | None = None + @property + def subscription_id(self): + return self._subscription_id -class JValue(Struct): - block: Block = None + @subscription_id.setter + def subscription_id(self, value: int | str): + self._subscription_id = value + self.subscribed = True + @subscription_id.deleter + def subscription_id(self): + self._subscription_id = None + self.subscribed = False -class JResult(Struct): - value: JValue +class WSRequest: + def __init__(self, rpc_call: RpcCall, start_time: int, subscription_index: int = None): + self.rpc_call = rpc_call + self.start_time = start_time + self.subscription_index = subscription_index -class JParams(Struct): - result: JResult - -class JsonRpcMessage(Struct): - jsonrpc: str - method: str = None - params: JParams = None - id: int = None - - -loads = msgspec.json.Decoder(JsonRpcMessage).decode - - -class WssUser(User): +class WssJrpcUser(User): abstract = True logger = logging.getLogger(__name__) + # To be populated by subclass + subscriptions: list[WSSubscription] = [] + subscription_ids_to_index: dict[str | int, int] = {} + def __init__(self, environment): super().__init__(environment) self._ws: WebSocket | None = None self._ws_greenlet: Greenlet | None = None - self._requests = {} - self._running = False - self.subscriptions = {} + self._requests: dict[int, WSRequest] = {} + self._running: bool = False + + @task + def dummy_task(self): + gevent.sleep(3600) def on_start(self) -> None: self._running = True host: str = self.environment.parsed_options.host if host.startswith("ws") or host.startswith("wss"): - self.connect(self.environment.parsed_options.host) + self.connect(host) else: raise ValueError("Invalid host provided. Expected ws or wss protocol") self.subscribe_all() def on_stop(self) -> None: - self._running = False self.unsubscribe_all() + timeout = Timeout(30) + timeout.start() + try: + while self._requests: + gevent.sleep(1) + except Timeout: + self.logger.error("Timeout 30s - Failed to unsubscribe from all subscriptions") + timeout.close() + self._running = False self.logger.debug("Unsubscribed from all subscriptions") def connect(self, host: str, **kwargs): self._ws = create_connection(host, **kwargs) self._ws_greenlet = gevent.spawn(self.receive_loop) - if self._ws is not None: - self.logger.info(f"Connected to {host}") def subscribe_all(self): - subscribe_methods = ["blockSubscribe"] - for method in subscribe_methods: - self.subscribe(method) - - def subscribe(self, method: str): - request_id = random.Random().randint(0, 1000000) - self.send( - { - "id": request_id, - "jsonrpc": "2.0", - "method": method, - "params": [ - "all", - { - "commitment": "confirmed", - "encoding": "jsonParsed", - "showRewards": True, - "transactionDetails": "full", - "maxSupportedTransactionVersion": 0, - }, - ], - }, - "blockSubscribe", - ) - self._requests[request_id].update({"subscription": method}) + for i in range(len(self.subscriptions)): + self.subscribe(self.subscriptions[i], i) + + def subscribe(self, subscription: WSSubscription, index: int): + self.send(rpc_call=subscription.subscribe_rpc_call, subscription_index=index) def unsubscribe_all(self): - subscription_ids = list(self.subscriptions.keys()) - for subscription_id in subscription_ids: - self.send( - { - "id": random.Random().randint(0, 1000000), - "jsonrpc": "2.0", - "method": "blockUnsubscribe", - "params": [subscription_id], - }, - "blockUnsubscribe", - ) + for i in range(len(self.subscriptions)): + self.unsubscribe(self.subscriptions[i], i) + + def unsubscribe(self, subscription: WSSubscription, index: int): + params = [subscription.subscription_id] + self.send(method=subscription.unsubscribe_method, params=params, subscription_index=index) + + def get_notification_name(self, parsed_response: dict): + # Override this method to return the name of the notification if this is not correct + return parsed_response["method"] def on_message(self, message): try: - parsed_json: JsonRpcMessage = loads(message) - if parsed_json.id is not None: - self.check_requests(message) - elif blockTime := parsed_json.params.result.value.block.blockTime: + parsed_json: dict = json.loads(message) + if "error" in parsed_json: self.environment.events.request.fire( - request_type="WSS Sub", - name=parsed_json.method, - response_time=time.time().__round__() - blockTime, + request_type="WSJrpcErr", + name=f"JsonRPC Error {parsed_json['error']['code']}", + response_time=None, response_length=len(message), exception=None, + response=message, ) + return + if "id" not in parsed_json: + self.environment.events.request.fire( + request_type="WSNotif", + name=self.get_notification_name(parsed_json), + response_time=None, + response_length=len(message), + exception=None, + ) + return + if request := self.get_request(parsed_json): + if request.subscription_index is not None: + self.subscriptions[request.subscription_index].subscription_id = parsed_json["result"] + self.subscriptions[request.subscription_index].subscribed = "subscribed" + self.subscription_ids_to_index.update({parsed_json["result"]: request.subscription_index}) + self.environment.events.request.fire( + request_type="WSJrpc", + name=request.rpc_call.method, + response_time=((time.time_ns() - request.start_time) / 1_000_000).__round__(), + response_length=len(message), + exception=None, + ) + else: + self.logger.error("Received message with unknown id") except JSONDecodeError: self.environment.events.request.fire( - request_type="WSS", + request_type="WSErr", name="JSONDecodeError", response_time=None, response_length=len(message), @@ -133,36 +155,24 @@ def on_message(self, message): response=message, ) - def check_requests(self, message): - response = orjson.loads(message) - if response["id"] not in self._requests: + def get_request(self, json_response: dict): + if json_response["id"] not in self._requests: self.logger.error("Received message with unknown id") - self.logger.error(response) - return - request = self._requests.pop(response["id"]) - if request["name"] == "blockSubscribe": - self.subscriptions.update({response["result"]: request["subscription"]}) - self.environment.events.request.fire( - request_type="WSS", - name=request["name"], - response_time=((time.time_ns() - request["start_time"]) / 1_000_000).__round__(), - response_length=len(message), - exception=None, - ) + self.logger.error(json_response) + return None + return self._requests.pop(json_response["id"]) def receive_loop(self): try: while self._running: message = self._ws.recv() - self.logger.debug(f"WSR") - # self.logger.debug(f"WSR: {message}") + self.logger.debug(f"WSR: {message}") self.on_message(message) - gevent.sleep(0) else: self._ws.close() except WebSocketConnectionClosedException: self.environment.events.request.fire( - request_type="WS", + request_type="WSerr", name="WebSocket Connection", response_time=None, response_length=0, @@ -172,8 +182,25 @@ def receive_loop(self): self.logger.error("Connection closed by server, trying to reconnect...") self.on_start() - def send(self, body, name): - self._requests.update({body["id"]: {"name": name, "start_time": time.time_ns()}}) - json_body = orjson.dumps(body) + def send(self, rpc_call: RpcCall = None, method: str = None, params: dict | list = None, subscription_index: int = None): + self.logger.debug(f"Sending: {rpc_call or method}") + rpc = { + (None, None): None, + (None, method): RpcCall(method, params), + (rpc_call, None): rpc_call, + } + + rpc_call = rpc[(rpc_call, method)] + if rpc_call is None: + raise ValueError("Either rpc_call or method must be provided") + + if rpc_call is None and (method is not None): + rpc_call = RpcCall(method, params) + elif rpc_call is None and (method is None): + raise ValueError("Either rpc_call or method must be provided") + self._requests.update( + {rpc_call.request_id: WSRequest(rpc_call, start_time=time.time_ns(), subscription_index=subscription_index)} + ) + json_body = json.dumps(rpc_call.request_body()) self.logger.debug(f"WSS: {json_body}") self._ws.send(json_body) diff --git a/chainbench/util/cli.py b/chainbench/util/cli.py index a8f4016..b1edc85 100644 --- a/chainbench/util/cli.py +++ b/chainbench/util/cli.py @@ -156,7 +156,7 @@ def get_worker_command(self, worker_id: int = 0) -> str: """Generate worker command.""" command = ( f"locust -f {self.profile_path} --worker --master-host {self.host} --master-port {self.port} " - f"--logfile {self.results_path}/worker_{worker_id}.log --loglevel {self.log_level}" + f"--logfile {self.results_path}/worker_{worker_id}.log --loglevel {self.log_level} --stop-timeout 30" ) return self.get_extra_options(command) diff --git a/chainbench/util/jsonrpc.py b/chainbench/util/jsonrpc.py index b59b0cc..44fead9 100644 --- a/chainbench/util/jsonrpc.py +++ b/chainbench/util/jsonrpc.py @@ -1,40 +1,44 @@ import random import typing as t - -import msgspec.json import orjson as json class RpcCall: - def __init__(self, method: str, params: list[t.Any] | dict | None = None) -> None: + def __init__(self, method: str, params: list[t.Any] | dict | None = None, request_id: int = None) -> None: + self._request_id = request_id self.method = method self.params = params + @property + def request_id(self) -> int: + if self._request_id is None: + self._request_id = random.Random().randint(1, 100000000) + return self._request_id -def generate_request_body( - method: str | None = None, params: list | dict | None = None, request_id: int | None = None, version: str = "2.0" -) -> dict: - """Generate a JSON-RPC request body.""" + def request_body(self, request_id: int = None) -> dict: + """Generate a JSON-RPC request body.""" + if self.params is None: + self.params = [] - if params is None: - params = [] + if type(self.params) is dict: + self.params = [self.params] - if request_id is None: - request_id = random.randint(1, 100000000) + if request_id: + self._request_id = request_id - return { - "jsonrpc": version, - "method": method, - "params": params, - "id": request_id, - } + return { + "jsonrpc": "2.0", + "method": self.method, + "params": self.params, + "id": self._request_id, + } -def generate_batch_request_body(rpc_calls: list[RpcCall], version: str = "2.0") -> str: +def generate_batch_request_body(rpc_calls: list[RpcCall]) -> str: """Generate a batch JSON-RPC request body.""" return json.dumps( [ - generate_request_body(rpc_calls[i].method, rpc_calls[i].params, request_id=i, version=version) + rpc_calls[i].request_body(i) for i in range(1, len(rpc_calls)) ] ).decode("utf-8") diff --git a/chainbench/util/ws.py b/chainbench/util/ws.py new file mode 100644 index 0000000..188bba9 --- /dev/null +++ b/chainbench/util/ws.py @@ -0,0 +1,43 @@ +import asyncio +import orjson as json + +from autobahn.asyncio import WebSocketClientProtocol, WebSocketClientFactory +from autobahn.websocket.protocol import WebSocketProtocol + + +def create_protocol(on_connect=None, on_connecting=None, on_message=None, on_open=None, on_close=None): + member_functions = {} + if on_connect: + member_functions["onConnect"] = on_connect + if on_connecting: + member_functions["onConnecting"] = on_connecting + if on_message: + member_functions["onMessage"] = on_message + if on_open: + member_functions["onOpen"] = on_open + if on_close: + member_functions["onClose"] = on_close + return type("WsClientProtocol", (WebSocketClientProtocol,), member_functions) + + +class WebSocketClient: + def __init__(self, host, on_connect=None, on_connecting=None, on_message=None, on_open=None, on_close=None): + self.host = host + self.factory = WebSocketClientFactory(host) + self.factory.protocol = create_protocol(on_connect, on_connecting, on_message, on_open, on_close) + self._loop = asyncio.get_event_loop() + coro = self._loop.create_connection(self.factory, self.factory.host, self.factory.port, ssl=self.factory.isSecure) + _, self.protocol = self._loop.run_until_complete(coro) + + def connected(self): + return self.protocol.state == WebSocketProtocol.STATE_OPEN + + def run_forever(self): + print("run_forever") + self._loop.run_forever() + + def close(self): + self.protocol.sendClose() + + def send(self, data): + self.protocol.sendMessage(json.dumps(data), isBinary=False) diff --git a/poetry.lock b/poetry.lock index d128e14..1948ce4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -797,20 +797,6 @@ files = [ {file = "jsonalias-0.1.1.tar.gz", hash = "sha256:64f04d935397d579fc94509e1fcb6212f2d081235d9d6395bd10baedf760a769"}, ] -[[package]] -name = "jsonpath-ng" -version = "1.6.1" -description = "A final implementation of JSONPath for Python that aims to be standard compliant, including arithmetic and binary comparison operators and providing clear AST for metaprogramming." -optional = false -python-versions = "*" -files = [ - {file = "jsonpath-ng-1.6.1.tar.gz", hash = "sha256:086c37ba4917304850bd837aeab806670224d3f038fe2833ff593a672ef0a5fa"}, - {file = "jsonpath_ng-1.6.1-py3-none-any.whl", hash = "sha256:8f22cd8273d7772eea9aaa84d922e0841aa36fdb8a2c6b7f6c3791a16a9bc0be"}, -] - -[package.dependencies] -ply = "*" - [[package]] name = "locust" version = "2.25.0" @@ -1013,58 +999,6 @@ files = [ {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, ] -[[package]] -name = "msgspec" -version = "0.18.6" -description = "A fast serialization and validation library, with builtin support for JSON, MessagePack, YAML, and TOML." -optional = false -python-versions = ">=3.8" -files = [ - {file = "msgspec-0.18.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:77f30b0234eceeff0f651119b9821ce80949b4d667ad38f3bfed0d0ebf9d6d8f"}, - {file = "msgspec-0.18.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a76b60e501b3932782a9da039bd1cd552b7d8dec54ce38332b87136c64852dd"}, - {file = "msgspec-0.18.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06acbd6edf175bee0e36295d6b0302c6de3aaf61246b46f9549ca0041a9d7177"}, - {file = "msgspec-0.18.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40a4df891676d9c28a67c2cc39947c33de516335680d1316a89e8f7218660410"}, - {file = "msgspec-0.18.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a6896f4cd5b4b7d688018805520769a8446df911eb93b421c6c68155cdf9dd5a"}, - {file = "msgspec-0.18.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3ac4dd63fd5309dd42a8c8c36c1563531069152be7819518be0a9d03be9788e4"}, - {file = "msgspec-0.18.6-cp310-cp310-win_amd64.whl", hash = "sha256:fda4c357145cf0b760000c4ad597e19b53adf01382b711f281720a10a0fe72b7"}, - {file = "msgspec-0.18.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e77e56ffe2701e83a96e35770c6adb655ffc074d530018d1b584a8e635b4f36f"}, - {file = "msgspec-0.18.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5351afb216b743df4b6b147691523697ff3a2fc5f3d54f771e91219f5c23aaa"}, - {file = "msgspec-0.18.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3232fabacef86fe8323cecbe99abbc5c02f7698e3f5f2e248e3480b66a3596b"}, - {file = "msgspec-0.18.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3b524df6ea9998bbc99ea6ee4d0276a101bcc1aa8d14887bb823914d9f60d07"}, - {file = "msgspec-0.18.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:37f67c1d81272131895bb20d388dd8d341390acd0e192a55ab02d4d6468b434c"}, - {file = "msgspec-0.18.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d0feb7a03d971c1c0353de1a8fe30bb6579c2dc5ccf29b5f7c7ab01172010492"}, - {file = "msgspec-0.18.6-cp311-cp311-win_amd64.whl", hash = "sha256:41cf758d3f40428c235c0f27bc6f322d43063bc32da7b9643e3f805c21ed57b4"}, - {file = "msgspec-0.18.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d86f5071fe33e19500920333c11e2267a31942d18fed4d9de5bc2fbab267d28c"}, - {file = "msgspec-0.18.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce13981bfa06f5eb126a3a5a38b1976bddb49a36e4f46d8e6edecf33ccf11df1"}, - {file = "msgspec-0.18.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e97dec6932ad5e3ee1e3c14718638ba333befc45e0661caa57033cd4cc489466"}, - {file = "msgspec-0.18.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad237100393f637b297926cae1868b0d500f764ccd2f0623a380e2bcfb2809ca"}, - {file = "msgspec-0.18.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db1d8626748fa5d29bbd15da58b2d73af25b10aa98abf85aab8028119188ed57"}, - {file = "msgspec-0.18.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d70cb3d00d9f4de14d0b31d38dfe60c88ae16f3182988246a9861259c6722af6"}, - {file = "msgspec-0.18.6-cp312-cp312-win_amd64.whl", hash = "sha256:1003c20bfe9c6114cc16ea5db9c5466e49fae3d7f5e2e59cb70693190ad34da0"}, - {file = "msgspec-0.18.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f7d9faed6dfff654a9ca7d9b0068456517f63dbc3aa704a527f493b9200b210a"}, - {file = "msgspec-0.18.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9da21f804c1a1471f26d32b5d9bc0480450ea77fbb8d9db431463ab64aaac2cf"}, - {file = "msgspec-0.18.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46eb2f6b22b0e61c137e65795b97dc515860bf6ec761d8fb65fdb62aa094ba61"}, - {file = "msgspec-0.18.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8355b55c80ac3e04885d72db515817d9fbb0def3bab936bba104e99ad22cf46"}, - {file = "msgspec-0.18.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9080eb12b8f59e177bd1eb5c21e24dd2ba2fa88a1dbc9a98e05ad7779b54c681"}, - {file = "msgspec-0.18.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cc001cf39becf8d2dcd3f413a4797c55009b3a3cdbf78a8bf5a7ca8fdb76032c"}, - {file = "msgspec-0.18.6-cp38-cp38-win_amd64.whl", hash = "sha256:fac5834e14ac4da1fca373753e0c4ec9c8069d1fe5f534fa5208453b6065d5be"}, - {file = "msgspec-0.18.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:974d3520fcc6b824a6dedbdf2b411df31a73e6e7414301abac62e6b8d03791b4"}, - {file = "msgspec-0.18.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fd62e5818731a66aaa8e9b0a1e5543dc979a46278da01e85c3c9a1a4f047ef7e"}, - {file = "msgspec-0.18.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7481355a1adcf1f08dedd9311193c674ffb8bf7b79314b4314752b89a2cf7f1c"}, - {file = "msgspec-0.18.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6aa85198f8f154cf35d6f979998f6dadd3dc46a8a8c714632f53f5d65b315c07"}, - {file = "msgspec-0.18.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e24539b25c85c8f0597274f11061c102ad6b0c56af053373ba4629772b407be"}, - {file = "msgspec-0.18.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c61ee4d3be03ea9cd089f7c8e36158786cd06e51fbb62529276452bbf2d52ece"}, - {file = "msgspec-0.18.6-cp39-cp39-win_amd64.whl", hash = "sha256:b5c390b0b0b7da879520d4ae26044d74aeee5144f83087eb7842ba59c02bc090"}, - {file = "msgspec-0.18.6.tar.gz", hash = "sha256:a59fc3b4fcdb972d09138cb516dbde600c99d07c38fd9372a6ef500d2d031b4e"}, -] - -[package.extras] -dev = ["attrs", "coverage", "furo", "gcovr", "ipython", "msgpack", "mypy", "pre-commit", "pyright", "pytest", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "tomli", "tomli-w"] -doc = ["furo", "ipython", "sphinx", "sphinx-copybutton", "sphinx-design"] -test = ["attrs", "msgpack", "mypy", "pyright", "pytest", "pyyaml", "tomli", "tomli-w"] -toml = ["tomli", "tomli-w"] -yaml = ["pyyaml"] - [[package]] name = "mypy" version = "1.9.0" @@ -1234,17 +1168,6 @@ files = [ docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] -[[package]] -name = "ply" -version = "3.11" -description = "Python Lex & Yacc" -optional = false -python-versions = "*" -files = [ - {file = "ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce"}, - {file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"}, -] - [[package]] name = "pre-commit" version = "3.7.0" @@ -1847,4 +1770,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "fc834247ce1e3e2e56ea81b5e880856ddaec614217fda49d4ea34369c5c4eba3" +content-hash = "5945688edff4feca256f9acf43dc575c29dd247b3e65a6082fda4d1008f3bccf" diff --git a/pyproject.toml b/pyproject.toml index 9a3878e..95caf2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,8 +20,6 @@ base58 = "^2.1.1" solders = "^0.21.0" websocket-client = "^1.8.0" orjson = "^3.10.6" -jsonpath-ng = "^1.6.1" -msgspec = "^0.18.6" [tool.poetry.group.dev.dependencies] black = ">=23.1,<25.0" From 6fb1c46fde41b09bff537928b78886603c9d27ed Mon Sep 17 00:00:00 2001 From: Erwin Wee Date: Fri, 16 Aug 2024 18:51:14 +0800 Subject: [PATCH 14/21] add wsaccel and skip_utf8_validation --- chainbench/user/wss.py | 6 ++-- poetry.lock | 67 +++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/chainbench/user/wss.py b/chainbench/user/wss.py index 40a4cae..c19b2d9 100644 --- a/chainbench/user/wss.py +++ b/chainbench/user/wss.py @@ -1,5 +1,6 @@ import logging import time +from socket import socket import gevent import orjson as json @@ -7,7 +8,6 @@ from locust import User, task from orjson import JSONDecodeError from websocket import WebSocket, WebSocketConnectionClosedException, create_connection -from locust.env import Environment from chainbench.util.jsonrpc import RpcCall @@ -86,8 +86,8 @@ def on_stop(self) -> None: self._running = False self.logger.debug("Unsubscribed from all subscriptions") - def connect(self, host: str, **kwargs): - self._ws = create_connection(host, **kwargs) + def connect(self, host: str): + self._ws = create_connection(host, skip_utf8_validation=True) self._ws_greenlet = gevent.spawn(self.receive_loop) def subscribe_all(self): diff --git a/poetry.lock b/poetry.lock index 1948ce4..20c4e29 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1696,6 +1696,71 @@ MarkupSafe = ">=2.1.1" [package.extras] watchdog = ["watchdog (>=2.3)"] +[[package]] +name = "wsaccel" +version = "0.6.6" +description = "Accelerator for ws4py and AutobahnPython" +optional = false +python-versions = "*" +files = [ + {file = "wsaccel-0.6.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:670857dd7a95a900bbb338d3a51b59d9cabc4797dbefa93ae56cbc249dedf8f3"}, + {file = "wsaccel-0.6.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1377c8458ad4d21fc756dcffe9d768c68944bd09b278a1846758719c8e00288"}, + {file = "wsaccel-0.6.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c1a5937edaf38532597df61ce089deaef33417b4396484deff6f2b1fa22c62b"}, + {file = "wsaccel-0.6.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10a8eba84ef89b24e09bfdd94f74c90679810ea9508986178f2d858f55040029"}, + {file = "wsaccel-0.6.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:458b1576f8e3ff650aae35301cb717504c7fe7f1414565d8a4a19489d83df316"}, + {file = "wsaccel-0.6.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f069d60be03312ece51b75d0ea6839c41625930feaef8acd94716edaef74d053"}, + {file = "wsaccel-0.6.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62f3c7920167261cebf7b2ddd1d4a3e0e3b43f762dd9740ef71a1f2530081b3f"}, + {file = "wsaccel-0.6.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3aae728bdb5ec46442f7f4062bde6c62e2382a32ccd1fd8b3138b638021a2a36"}, + {file = "wsaccel-0.6.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:51ce50e3c7c7a1fc69f3b7920238d9cf9673d48d1402dc5ee71eb8426457ddc5"}, + {file = "wsaccel-0.6.6-cp310-cp310-win32.whl", hash = "sha256:d20505d3c0cbc7d34efcf9e7cc5b8f5f1dc4b055f0be09e15f528f387d72a633"}, + {file = "wsaccel-0.6.6-cp310-cp310-win_amd64.whl", hash = "sha256:273cd5cd584c02f5026b0609430d8ba9e7f5b6d01bd0a6309cab258fed1d5f8d"}, + {file = "wsaccel-0.6.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:70139bf54c26b8a7e5158bd7dc03cd0cc3af55facf6b63c55384610fc711c3ea"}, + {file = "wsaccel-0.6.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9c0ee2fc0e4cacb333e4ca00f7f55457c715d84244a8f034fdcb851abc3635ca"}, + {file = "wsaccel-0.6.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:414d9d802b59f5f41bbc2f001e2a9228957007c3684e33e64479e76b22909af3"}, + {file = "wsaccel-0.6.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f899f79d0881be740396efca9c01a776473c4f0eb59861a55c2c6e32b6781a74"}, + {file = "wsaccel-0.6.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d87200bbdf9759826dad33c2c322ccd2954b93431db707b83a80886f0b5910e5"}, + {file = "wsaccel-0.6.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a918d4e0b4042715a311706cd91aa5dcc1cf9bba5090cf7c2884769abb39c9cf"}, + {file = "wsaccel-0.6.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bcd6b7f5f8b6fc8378ad3d30f328b5091fbdc62b8b46bd952dacd5a64688707f"}, + {file = "wsaccel-0.6.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e5a758e1a9916df05053334978ed74c3241fa665ad789f8b962e977131e076ca"}, + {file = "wsaccel-0.6.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c2a1a929512a9f82504aa739d4790e3c78c800abd07c3b9bd1e046228c5a967e"}, + {file = "wsaccel-0.6.6-cp311-cp311-win32.whl", hash = "sha256:00445ea583128055896c359aa0fad1bcbc44bc85b7857db1e34a06301d232f20"}, + {file = "wsaccel-0.6.6-cp311-cp311-win_amd64.whl", hash = "sha256:ab1a1ee11161a9b882ebf6553bb1401713e86e72547a6f2d8f0bc467f672c911"}, + {file = "wsaccel-0.6.6-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4b3cafeb2f69e2b9606755da0098b2b0e70a6f37cb5f374d195b92d12d605f30"}, + {file = "wsaccel-0.6.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7d5c4535d153979c1196d7a23389a8e93129b738a4bb839fb1e77d9f2b08bb41"}, + {file = "wsaccel-0.6.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:59a77171798eac6ba372c4dc935a5ea98fe46461c5b70554de50bf500c735dd9"}, + {file = "wsaccel-0.6.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc51bae5016d2ef69cf8a71d5009f0f90dd043bcf1867f8d5b7cf7dab2d790c8"}, + {file = "wsaccel-0.6.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0440f6c8a0334336c76536c8f9985db0ac5f430afe5ca5827a4f2c4aaba11df"}, + {file = "wsaccel-0.6.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:610245b574804b4432c8aad906cc9c3c6c7315977290d8d323aee3f95b267651"}, + {file = "wsaccel-0.6.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b5a2a6e53bf9b184f70a63dfd595af7d534db29691e2a11324e720212ada7953"}, + {file = "wsaccel-0.6.6-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fd06a4c6539de32cf669da1629bd6837c6ac84a2454cc78a043441f18f8aeb5a"}, + {file = "wsaccel-0.6.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7bf2731f53907e89f4fd152149d3ce02c35f7c5c3755eedcad09d4f793ff75f8"}, + {file = "wsaccel-0.6.6-cp312-cp312-win32.whl", hash = "sha256:3e7b37678d316bb00126f97961bc1f4774fc7680dafa23bdea40a15beaf6a6bb"}, + {file = "wsaccel-0.6.6-cp312-cp312-win_amd64.whl", hash = "sha256:5b9b7cbee58ea12da45e817a01f20ba45ebd37d258aee4ab11106b91980961a3"}, + {file = "wsaccel-0.6.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:229c27d182591e76028d8fa2b4cc73286e18b7c882f34aa47d75a1c065b5514a"}, + {file = "wsaccel-0.6.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:587d3bbd9a9b8bf38fb8dcc32f59e23a60096da8900bbb1512fbc65fdf06c992"}, + {file = "wsaccel-0.6.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:09999ae0727897e47c958150311de25eba28dc52a0b5568a7d7a35b54e536e73"}, + {file = "wsaccel-0.6.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c21b7101b156a91df598e73dbc1b1a6c85224d609351f72991aa68f69aca873a"}, + {file = "wsaccel-0.6.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47b9c529dbabf0b647b091486f8f473abc10c927e897253a0038830b5aa57494"}, + {file = "wsaccel-0.6.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60d7a1c6e3aa5f6cc144a789d5313a861f2fd860bf62e0a468eef2262bd805a0"}, + {file = "wsaccel-0.6.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:79a5cc1b3172601ac874ca048e9cf7ecb6209b61b6c3543822ac7640851c58a7"}, + {file = "wsaccel-0.6.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4f28a380eda05efbaae39701828d501539520938e1316ce4dc94bb0020cdfba1"}, + {file = "wsaccel-0.6.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d68c0638777d24ed1dfbef67b89894b887f0b24aac7e0106dad236337b6212ec"}, + {file = "wsaccel-0.6.6-cp38-cp38-win32.whl", hash = "sha256:fe774891fe89ce6e2e570501bf9af9cb8760b42991b3b588159ac38d8f058adf"}, + {file = "wsaccel-0.6.6-cp38-cp38-win_amd64.whl", hash = "sha256:4cd7f5f376efd07d93eeb4d0848c1b5b2805c93cee734cd79f2903cfd4d8a9a4"}, + {file = "wsaccel-0.6.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:56eae811dc5a13f4a0e7c1e8d567132846f7f320c4fa80964d5def7b98873eae"}, + {file = "wsaccel-0.6.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:73a5bb52cbbd19b19dd87232f2cead62c01fa7895b42f08ec266c847d4703561"}, + {file = "wsaccel-0.6.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0dd4a5e4c4b80c9d2b0b037b552a576193d3f6e9e812347763907217d92c2702"}, + {file = "wsaccel-0.6.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2638c5e69df9e86e12310a26eadc3bf6c536eb32304496e4a58914e735b303"}, + {file = "wsaccel-0.6.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0718cb37e17b258eb5e5804ae4d86dcc63073a6be5090c7c5e8993c0e0ec754c"}, + {file = "wsaccel-0.6.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:238298b9c5b9b821aa94703bbbf34e0e92a4ae67f08f2b82a28ecb293fbade73"}, + {file = "wsaccel-0.6.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1ba9afd0613b852c119a93d7dbd1ab02244b41f8ea619a117bdb0926ebfcc845"}, + {file = "wsaccel-0.6.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ca7ecf0a24ece61540c9b12488e60224c43de4b9ad21218074b5e998ea3219b2"}, + {file = "wsaccel-0.6.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8acad029668184e2a4dc3ebeefca4c0b71cb5d1081ebd933b674d6a0fc790f38"}, + {file = "wsaccel-0.6.6-cp39-cp39-win32.whl", hash = "sha256:90bb18a1b440230ef40f918449c4ca3219625500cba0423489a258a732e5e0b6"}, + {file = "wsaccel-0.6.6-cp39-cp39-win_amd64.whl", hash = "sha256:3798ba3af378753fa5166b68462dc11ad740ba47a321699d923d4eedaf50c20b"}, + {file = "wsaccel-0.6.6.tar.gz", hash = "sha256:18efec0a7182587ba97102b4cd8df7b4f665f45d7ca36f19783f5f081ea114ea"}, +] + [[package]] name = "zope-event" version = "5.0" @@ -1770,4 +1835,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "5945688edff4feca256f9acf43dc575c29dd247b3e65a6082fda4d1008f3bccf" +content-hash = "11afa278640c190f98cb3334cfa24c630e83a52ceb5a505a62e8564563bbb8a3" diff --git a/pyproject.toml b/pyproject.toml index 95caf2c..9176943 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ base58 = "^2.1.1" solders = "^0.21.0" websocket-client = "^1.8.0" orjson = "^3.10.6" +wsaccel = "^0.6.6" [tool.poetry.group.dev.dependencies] black = ">=23.1,<25.0" From 2d40774c1e9a7aa0bd12e90beb2144dff5372ee5 Mon Sep 17 00:00:00 2001 From: Erwin Wee Date: Fri, 16 Aug 2024 22:06:04 +0800 Subject: [PATCH 15/21] fix stuff --- chainbench/user/__init__.py | 2 + chainbench/user/wss.py | 5 +- chainbench/util/event.py | 93 ++++++++++++++++++------------------- chainbench/util/ws.py | 43 ----------------- 4 files changed, 48 insertions(+), 95 deletions(-) delete mode 100644 chainbench/util/ws.py diff --git a/chainbench/user/__init__.py b/chainbench/user/__init__.py index acd82b7..8b2bb74 100644 --- a/chainbench/user/__init__.py +++ b/chainbench/user/__init__.py @@ -3,6 +3,7 @@ from .common import get_subclass_tasks from .http import HttpUser +from .jsonrpc import JsonRpcUser from .wss import WssJrpcUser # importing plugins here as all profiles depend on it @@ -14,6 +15,7 @@ "EthBeaconUser", "EvmUser", "HttpUser", + "JsonRpcUser", "SolanaUser", "StarkNetUser", "WssJrpcUser", diff --git a/chainbench/user/wss.py b/chainbench/user/wss.py index c19b2d9..f49ada7 100644 --- a/chainbench/user/wss.py +++ b/chainbench/user/wss.py @@ -1,6 +1,5 @@ import logging import time -from socket import socket import gevent import orjson as json @@ -166,7 +165,7 @@ def receive_loop(self): try: while self._running: message = self._ws.recv() - self.logger.debug(f"WSR: {message}") + self.logger.debug(f"WSResp: {message}") self.on_message(message) else: self._ws.close() @@ -202,5 +201,5 @@ def send(self, rpc_call: RpcCall = None, method: str = None, params: dict | list {rpc_call.request_id: WSRequest(rpc_call, start_time=time.time_ns(), subscription_index=subscription_index)} ) json_body = json.dumps(rpc_call.request_body()) - self.logger.debug(f"WSS: {json_body}") + self.logger.debug(f"WSReq: {json_body}") self._ws.send(json_body) diff --git a/chainbench/util/event.py b/chainbench/util/event.py index 76d24af..85af343 100644 --- a/chainbench/util/event.py +++ b/chainbench/util/event.py @@ -187,59 +187,54 @@ def on_init(environment: Environment, **_kwargs): logger.info(f"Initializing test data for {test_data_class_name}") print(f"Initializing test data for {test_data_class_name}") if environment.host: - if environment.host.startswith("wss"): - user_test_data.init_http_client( - "https://nd-195-027-150.p2pify.com/681543cc4d120809ae5a1c973ac798e8" - ) - else: - user_test_data.init_http_client(environment.host) - # if isinstance(user_test_data, EvmTestData): - # chain_id: ChainId = user_test_data.fetch_chain_id() - # user_test_data.init_network(chain_id) - # logger.info(f"Target endpoint network is {user_test_data.network.name}") - # print(f"Target endpoint network is {user_test_data.network.name}") - # test_data["chain_id"] = {test_data_class_name: chain_id} + user_test_data.init_http_client(environment.host) + if isinstance(user_test_data, EvmTestData): + chain_id: ChainId = user_test_data.fetch_chain_id() + user_test_data.init_network(chain_id) + logger.info(f"Target endpoint network is {user_test_data.network.name}") + print(f"Target endpoint network is {user_test_data.network.name}") + test_data["chain_id"] = {test_data_class_name: chain_id} if environment.parsed_options: user_test_data.init_data(environment.parsed_options) test_data[test_data_class_name] = user_test_data.data.to_json() send_msg_to_workers(environment.runner, "test_data", test_data) - # print("Fetching blocks...") - # if environment.parsed_options.use_latest_blocks: - # print(f"Using latest {user_test_data.data.size.blocks_len} blocks as test data") - # logger.info(f"Using latest {user_test_data.data.size.blocks_len} blocks as test data") - # for block_number in range( - # user_test_data.data.block_range.start, user_test_data.data.block_range.end + 1 - # ): - # block = None - # try: - # block = user_test_data.fetch_block(block_number) - # except (BlockNotFoundError, InvalidBlockError): - # pass - # while block is None: - # try: - # block = user_test_data.fetch_latest_block() - # except (BlockNotFoundError, InvalidBlockError): - # pass - # user_test_data.data.push_block(block) - # block_data = {test_data_class_name: block.to_json()} - # send_msg_to_workers(environment.runner, "block_data", block_data) - # print(user_test_data.data.stats(), end="\r") - # else: - # print(user_test_data.data.stats(), end="\r") - # print("\n") # new line after progress display upon exiting loop - # else: - # while user_test_data.data.size.blocks_len > len(user_test_data.data.blocks): - # try: - # block = user_test_data.fetch_random_block(user_test_data.data.block_numbers) - # except (BlockNotFoundError, InvalidBlockError): - # continue - # user_test_data.data.push_block(block) - # block_data = {test_data_class_name: block.to_json()} - # send_msg_to_workers(environment.runner, "block_data", block_data) - # print(user_test_data.data.stats(), end="\r") - # else: - # print(user_test_data.data.stats(), end="\r") - # print("\n") # new line after progress display upon exiting loop + print("Fetching blocks...") + if environment.parsed_options.use_latest_blocks: + print(f"Using latest {user_test_data.data.size.blocks_len} blocks as test data") + logger.info(f"Using latest {user_test_data.data.size.blocks_len} blocks as test data") + for block_number in range( + user_test_data.data.block_range.start, user_test_data.data.block_range.end + 1 + ): + block = None + try: + block = user_test_data.fetch_block(block_number) + except (BlockNotFoundError, InvalidBlockError): + pass + while block is None: + try: + block = user_test_data.fetch_latest_block() + except (BlockNotFoundError, InvalidBlockError): + pass + user_test_data.data.push_block(block) + block_data = {test_data_class_name: block.to_json()} + send_msg_to_workers(environment.runner, "block_data", block_data) + print(user_test_data.data.stats(), end="\r") + else: + print(user_test_data.data.stats(), end="\r") + print("\n") # new line after progress display upon exiting loop + else: + while user_test_data.data.size.blocks_len > len(user_test_data.data.blocks): + try: + block = user_test_data.fetch_random_block(user_test_data.data.block_numbers) + except (BlockNotFoundError, InvalidBlockError): + continue + user_test_data.data.push_block(block) + block_data = {test_data_class_name: block.to_json()} + send_msg_to_workers(environment.runner, "block_data", block_data) + print(user_test_data.data.stats(), end="\r") + else: + print(user_test_data.data.stats(), end="\r") + print("\n") # new line after progress display upon exiting loop logger.info("Test data is ready") send_msg_to_workers(environment.runner, "release_lock", {}) user_test_data.release_lock() diff --git a/chainbench/util/ws.py b/chainbench/util/ws.py deleted file mode 100644 index 188bba9..0000000 --- a/chainbench/util/ws.py +++ /dev/null @@ -1,43 +0,0 @@ -import asyncio -import orjson as json - -from autobahn.asyncio import WebSocketClientProtocol, WebSocketClientFactory -from autobahn.websocket.protocol import WebSocketProtocol - - -def create_protocol(on_connect=None, on_connecting=None, on_message=None, on_open=None, on_close=None): - member_functions = {} - if on_connect: - member_functions["onConnect"] = on_connect - if on_connecting: - member_functions["onConnecting"] = on_connecting - if on_message: - member_functions["onMessage"] = on_message - if on_open: - member_functions["onOpen"] = on_open - if on_close: - member_functions["onClose"] = on_close - return type("WsClientProtocol", (WebSocketClientProtocol,), member_functions) - - -class WebSocketClient: - def __init__(self, host, on_connect=None, on_connecting=None, on_message=None, on_open=None, on_close=None): - self.host = host - self.factory = WebSocketClientFactory(host) - self.factory.protocol = create_protocol(on_connect, on_connecting, on_message, on_open, on_close) - self._loop = asyncio.get_event_loop() - coro = self._loop.create_connection(self.factory, self.factory.host, self.factory.port, ssl=self.factory.isSecure) - _, self.protocol = self._loop.run_until_complete(coro) - - def connected(self): - return self.protocol.state == WebSocketProtocol.STATE_OPEN - - def run_forever(self): - print("run_forever") - self._loop.run_forever() - - def close(self): - self.protocol.sendClose() - - def send(self, data): - self.protocol.sendMessage(json.dumps(data), isBinary=False) From 92782c177839680648f2549346f350a6af85375b Mon Sep 17 00:00:00 2001 From: Erwin Wee Date: Fri, 16 Aug 2024 23:13:18 +0800 Subject: [PATCH 16/21] linting and formatting --- chainbench/profile/solana/get_block.py | 22 -------------- chainbench/user/jsonrpc.py | 4 ++- chainbench/user/protocol/evm.py | 2 +- chainbench/user/wss.py | 41 +++++++++++++++----------- chainbench/util/jsonrpc.py | 12 +++----- 5 files changed, 32 insertions(+), 49 deletions(-) delete mode 100644 chainbench/profile/solana/get_block.py diff --git a/chainbench/profile/solana/get_block.py b/chainbench/profile/solana/get_block.py deleted file mode 100644 index f565e48..0000000 --- a/chainbench/profile/solana/get_block.py +++ /dev/null @@ -1,22 +0,0 @@ -from locust import task - -from chainbench.user.protocol.solana import SolanaUser -from chainbench.util.jsonrpc import RpcCall - - -class SolanaGetBlock(SolanaUser): - @task - def get_block_task(self): - self.make_rpc_call( - RpcCall( - method="getBlock", - params=[ - self.test_data.get_random_block_number(self.rng.get_rng()), - { - "encoding": "jsonParsed", - "transactionDetails": "full", - "maxSupportedTransactionVersion": 0, - }, - ] - ) - ) diff --git a/chainbench/user/jsonrpc.py b/chainbench/user/jsonrpc.py index c203ad7..c044632 100644 --- a/chainbench/user/jsonrpc.py +++ b/chainbench/user/jsonrpc.py @@ -98,8 +98,10 @@ def make_rpc_call( path: str = "", ) -> None: """Make a JSON-RPC call.""" - if rpc_call is None: + if rpc_call is None and method is not None: rpc_call = RpcCall(method, params) + else: + raise ValueError("Either rpc_call or method must be provided") if name == "" and method is not None: name = method diff --git a/chainbench/user/protocol/evm.py b/chainbench/user/protocol/evm.py index 284ade9..5c6eee4 100644 --- a/chainbench/user/protocol/evm.py +++ b/chainbench/user/protocol/evm.py @@ -16,7 +16,7 @@ class EvmBaseUser: abstract = True - test_data = EvmTestData() + test_data: EvmTestData = EvmTestData() rng = RNGManager() _default_trace_timeout = "120s" diff --git a/chainbench/user/wss.py b/chainbench/user/wss.py index f49ada7..2a465c1 100644 --- a/chainbench/user/wss.py +++ b/chainbench/user/wss.py @@ -5,18 +5,15 @@ import orjson as json from gevent import Greenlet, Timeout from locust import User, task +from locust.env import Environment from orjson import JSONDecodeError from websocket import WebSocket, WebSocketConnectionClosedException, create_connection + from chainbench.util.jsonrpc import RpcCall class WSSubscription: - def __init__( - self, - subscribe_method: str, - subscribe_params: dict | list, - unsubscribe_method: str - ): + def __init__(self, subscribe_method: str, subscribe_params: dict | list, unsubscribe_method: str): self.subscribe_rpc_call: RpcCall = RpcCall(subscribe_method, subscribe_params) self.unsubscribe_method: str = unsubscribe_method self.subscribed: bool = False @@ -38,7 +35,7 @@ def subscription_id(self): class WSRequest: - def __init__(self, rpc_call: RpcCall, start_time: int, subscription_index: int = None): + def __init__(self, rpc_call: RpcCall, start_time: int, subscription_index: int | None = None): self.rpc_call = rpc_call self.start_time = start_time self.subscription_index = subscription_index @@ -52,7 +49,7 @@ class WssJrpcUser(User): subscriptions: list[WSSubscription] = [] subscription_ids_to_index: dict[str | int, int] = {} - def __init__(self, environment): + def __init__(self, environment: Environment): super().__init__(environment) self._ws: WebSocket | None = None self._ws_greenlet: Greenlet | None = None @@ -108,7 +105,7 @@ def get_notification_name(self, parsed_response: dict): # Override this method to return the name of the notification if this is not correct return parsed_response["method"] - def on_message(self, message): + def on_message(self, message: str | bytes): try: parsed_json: dict = json.loads(message) if "error" in parsed_json: @@ -181,15 +178,24 @@ def receive_loop(self): self.logger.error("Connection closed by server, trying to reconnect...") self.on_start() - def send(self, rpc_call: RpcCall = None, method: str = None, params: dict | list = None, subscription_index: int = None): + def send( + self, + rpc_call: RpcCall | None = None, + method: str | None = None, + params: dict | list | None = None, + subscription_index: int | None = None, + ): + def _get_args(): + if rpc_call: + return rpc_call + elif method: + return RpcCall(method, params) + else: + raise ValueError("Either rpc_call or method must be provided") + + rpc_call = _get_args() self.logger.debug(f"Sending: {rpc_call or method}") - rpc = { - (None, None): None, - (None, method): RpcCall(method, params), - (rpc_call, None): rpc_call, - } - rpc_call = rpc[(rpc_call, method)] if rpc_call is None: raise ValueError("Either rpc_call or method must be provided") @@ -202,4 +208,5 @@ def send(self, rpc_call: RpcCall = None, method: str = None, params: dict | list ) json_body = json.dumps(rpc_call.request_body()) self.logger.debug(f"WSReq: {json_body}") - self._ws.send(json_body) + if self._ws: + self._ws.send(json_body) diff --git a/chainbench/util/jsonrpc.py b/chainbench/util/jsonrpc.py index 44fead9..9099695 100644 --- a/chainbench/util/jsonrpc.py +++ b/chainbench/util/jsonrpc.py @@ -1,10 +1,11 @@ import random import typing as t + import orjson as json class RpcCall: - def __init__(self, method: str, params: list[t.Any] | dict | None = None, request_id: int = None) -> None: + def __init__(self, method: str, params: list[t.Any] | dict | None = None, request_id: int | None = None) -> None: self._request_id = request_id self.method = method self.params = params @@ -15,7 +16,7 @@ def request_id(self) -> int: self._request_id = random.Random().randint(1, 100000000) return self._request_id - def request_body(self, request_id: int = None) -> dict: + def request_body(self, request_id: int | None = None) -> dict: """Generate a JSON-RPC request body.""" if self.params is None: self.params = [] @@ -36,12 +37,7 @@ def request_body(self, request_id: int = None) -> dict: def generate_batch_request_body(rpc_calls: list[RpcCall]) -> str: """Generate a batch JSON-RPC request body.""" - return json.dumps( - [ - rpc_calls[i].request_body(i) - for i in range(1, len(rpc_calls)) - ] - ).decode("utf-8") + return json.dumps([rpc_calls[i].request_body(i) for i in range(1, len(rpc_calls))]).decode("utf-8") def expand_rpc_calls(rpc_calls_weighted: dict[t.Callable[[], RpcCall], int]) -> list[RpcCall]: From 1b1bd7abdea6a5212a6e2fcc22ab87707c7513b1 Mon Sep 17 00:00:00 2001 From: Erwin Wee Date: Fri, 16 Aug 2024 23:15:35 +0800 Subject: [PATCH 17/21] Delete sandbox.py --- chainbench/profile/sandbox.py | 65 ----------------------------------- 1 file changed, 65 deletions(-) delete mode 100644 chainbench/profile/sandbox.py diff --git a/chainbench/profile/sandbox.py b/chainbench/profile/sandbox.py deleted file mode 100644 index fa71324..0000000 --- a/chainbench/profile/sandbox.py +++ /dev/null @@ -1,65 +0,0 @@ -from chainbench.user import SolanaUser, WssJrpcUser -from chainbench.user.wss import WSSubscription - - -# TODO: Update Oasis profile to new format and update tutorial in documentation - - -class SandboxProfile(WssJrpcUser, SolanaUser): - # wait_time = constant_pacing(1) - - subscriptions = [ - WSSubscription("blockSubscribe", [ - "all", - { - "commitment": "confirmed", - "encoding": "jsonParsed", - "showRewards": True, - "transactionDetails": "full", - "maxSupportedTransactionVersion": 0 - } - ], "blockUnsubscribe"), - # WSSubscription("slotSubscribe", [], "slotUnsubscribe") - ] - - # subscriptions = [ - # WSSubscription("eth_subscribe", ["newHeads"], "eth_unsubscribe"), - # WSSubscription("eth_subscribe", [ - # "logs", - # { - # "address": "0x8320fe7702b96808f7bbc0d4a888ed1468216cfd", - # "topics": ["0xd78a0cb8bb633d06981248b816e7bd33c2a35a6089241d099fa519e361cab902"] - # } - # ], "eth_unsubscribe"), - # WSSubscription("eth_subscribe", ["newPendingTransactions"], "eth_unsubscribe"), - # WSSubscription("eth_subscribe", ["syncing"], "eth_unsubscribe"), - # ] - # - # def get_notification_name(self, parsed_response: dict): - # return self.subscriptions[self.subscription_ids_to_index[parsed_response["params"]["subscription"]]].subscribe_rpc_call.params[0] - - - # - # @task - # def eth_block_number(self): - # self.send( - # { - # "jsonrpc": "2.0", - # "method": "eth_blockNumber", - # "params": [], - # "id": random.Random().randint(0, 100000000) - # }, - # "eth_blockNumber" - # ) - # - # @task - # def eth_get_logs(self): - # self.send( - # { - # "jsonrpc": "2.0", - # "method": "eth_getLogs", - # "params": self._get_logs_params_factory(self.rng.get_rng()), - # "id": random.Random().randint(0, 100000000) - # }, - # "eth_getLogs" - # ) From c06970f916150609f76b085d04a00346bce5721c Mon Sep 17 00:00:00 2001 From: Erwin Wee Date: Tue, 20 Aug 2024 23:35:55 +0800 Subject: [PATCH 18/21] fix stuff --- chainbench/profile/ethereum/subscriptions.py | 16 ++++++++++++++++ chainbench/test_data/blockchain.py | 2 +- chainbench/user/__init__.py | 4 ++-- chainbench/user/jsonrpc.py | 2 +- chainbench/user/protocol/ethereum.py | 6 ++++++ chainbench/user/protocol/evm.py | 6 +++--- chainbench/user/protocol/solana.py | 4 ++-- chainbench/user/protocol/starknet.py | 4 ++-- chainbench/user/wss.py | 3 +++ 9 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 chainbench/profile/ethereum/subscriptions.py diff --git a/chainbench/profile/ethereum/subscriptions.py b/chainbench/profile/ethereum/subscriptions.py new file mode 100644 index 0000000..1c42a79 --- /dev/null +++ b/chainbench/profile/ethereum/subscriptions.py @@ -0,0 +1,16 @@ +from chainbench.user import EvmUser, WssJrpcUser +from chainbench.user.protocol.ethereum import EthSubscribe + + +class EthSubscriptions(WssJrpcUser, EvmUser): + subscriptions = [ + EthSubscribe(["newHeads"]), + # logs subscription for approve method signature + EthSubscribe(["logs", {"topics": ["0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925"]}]), + EthSubscribe(["newPendingTransactions"]), + ] + + def get_notification_name(self, parsed_response: dict): + return self.get_subscription( + subscription_id=parsed_response["params"]["subscription"] + ).subscribe_rpc_call.params[0] diff --git a/chainbench/test_data/blockchain.py b/chainbench/test_data/blockchain.py index 99f0795..054735f 100644 --- a/chainbench/test_data/blockchain.py +++ b/chainbench/test_data/blockchain.py @@ -47,7 +47,7 @@ class Block: block_number: BlockNumber def to_json(self) -> str: - return json.dumps(self.__dict__) + return json.dumps(self.__dict__).decode("utf-8") @dataclass diff --git a/chainbench/user/__init__.py b/chainbench/user/__init__.py index 8b2bb74..36208d5 100644 --- a/chainbench/user/__init__.py +++ b/chainbench/user/__init__.py @@ -3,7 +3,7 @@ from .common import get_subclass_tasks from .http import HttpUser -from .jsonrpc import JsonRpcUser +from .jsonrpc import JrpcHttpUser from .wss import WssJrpcUser # importing plugins here as all profiles depend on it @@ -15,7 +15,7 @@ "EthBeaconUser", "EvmUser", "HttpUser", - "JsonRpcUser", + "JrpcHttpUser", "SolanaUser", "StarkNetUser", "WssJrpcUser", diff --git a/chainbench/user/jsonrpc.py b/chainbench/user/jsonrpc.py index c044632..a7f78ad 100644 --- a/chainbench/user/jsonrpc.py +++ b/chainbench/user/jsonrpc.py @@ -12,7 +12,7 @@ ) -class JsonRpcUser(HttpUser): +class JrpcHttpUser(HttpUser): """Extension of HttpUser to provide JsonRPC support.""" abstract = True diff --git a/chainbench/user/protocol/ethereum.py b/chainbench/user/protocol/ethereum.py index b4736b7..da59d98 100644 --- a/chainbench/user/protocol/ethereum.py +++ b/chainbench/user/protocol/ethereum.py @@ -5,6 +5,7 @@ from chainbench.test_data.ethereum import EthBeaconTestData from chainbench.user.http import HttpUser +from chainbench.user.wss import WSSubscription logger = logging.getLogger(__name__) @@ -329,3 +330,8 @@ class TestEthMethod(EthBeaconUser): @task def run_task(self) -> None: self.method_to_task_function(self.environment.parsed_options.method)() + + +class EthSubscribe(WSSubscription): + def __init__(self, params: dict | list): + super().__init__("eth_subscribe", params, "eth_unsubscribe") diff --git a/chainbench/user/protocol/evm.py b/chainbench/user/protocol/evm.py index 5c6eee4..e310465 100644 --- a/chainbench/user/protocol/evm.py +++ b/chainbench/user/protocol/evm.py @@ -8,13 +8,13 @@ Tx, TxHash, ) -from chainbench.user.jsonrpc import JsonRpcUser +from chainbench.user.jsonrpc import JrpcHttpUser from chainbench.user.tag import tag from chainbench.util.jsonrpc import RpcCall from chainbench.util.rng import RNG, RNGManager -class EvmBaseUser: +class EvmBaseUser(JrpcHttpUser): abstract = True test_data: EvmTestData = EvmTestData() rng = RNGManager() @@ -396,7 +396,7 @@ def web3_sha3(self) -> RpcCall: return RpcCall(method="web3_sha3", params=[self.test_data.get_random_tx_hash(self.rng.get_rng())]) -class EvmUser(EvmRpcMethods, JsonRpcUser): +class EvmUser(EvmRpcMethods): abstract = True @staticmethod diff --git a/chainbench/user/protocol/solana.py b/chainbench/user/protocol/solana.py index d0f879e..4659f8f 100644 --- a/chainbench/user/protocol/solana.py +++ b/chainbench/user/protocol/solana.py @@ -4,12 +4,12 @@ from solders.message import Message from chainbench.test_data import Account, BlockNumber, SolanaTestData, TxHash -from chainbench.user.jsonrpc import JsonRpcUser +from chainbench.user.jsonrpc import JrpcHttpUser from chainbench.util.jsonrpc import RpcCall from chainbench.util.rng import RNG, RNGManager -class SolanaBaseUser(JsonRpcUser): +class SolanaBaseUser(JrpcHttpUser): abstract = True test_data = SolanaTestData() rng = RNGManager() diff --git a/chainbench/user/protocol/starknet.py b/chainbench/user/protocol/starknet.py index f4fed6d..774162b 100644 --- a/chainbench/user/protocol/starknet.py +++ b/chainbench/user/protocol/starknet.py @@ -2,11 +2,11 @@ from chainbench.test_data import StarkNetTestData from chainbench.test_data.blockchain import Account, TxHash -from chainbench.user.jsonrpc import JsonRpcUser +from chainbench.user.jsonrpc import JrpcHttpUser from chainbench.util.rng import RNG -class StarkNetUser(JsonRpcUser): +class StarkNetUser(JrpcHttpUser): abstract = True test_data = StarkNetTestData() diff --git a/chainbench/user/wss.py b/chainbench/user/wss.py index 2a465c1..828b6e6 100644 --- a/chainbench/user/wss.py +++ b/chainbench/user/wss.py @@ -56,6 +56,9 @@ def __init__(self, environment: Environment): self._requests: dict[int, WSRequest] = {} self._running: bool = False + def get_subscription(self, subscription_id: str | int): + return self.subscriptions[self.subscription_ids_to_index[subscription_id]] + @task def dummy_task(self): gevent.sleep(3600) From 927de97e51cb9cc279ec1bcaf07938ff20433c49 Mon Sep 17 00:00:00 2001 From: Erwin Wee Date: Tue, 20 Aug 2024 23:47:01 +0800 Subject: [PATCH 19/21] Update wss.py --- chainbench/user/wss.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chainbench/user/wss.py b/chainbench/user/wss.py index 828b6e6..c2c13ea 100644 --- a/chainbench/user/wss.py +++ b/chainbench/user/wss.py @@ -210,6 +210,6 @@ def _get_args(): {rpc_call.request_id: WSRequest(rpc_call, start_time=time.time_ns(), subscription_index=subscription_index)} ) json_body = json.dumps(rpc_call.request_body()) - self.logger.debug(f"WSReq: {json_body}") + self.logger.debug(f"WSReq: {json_body.decode('utf-8')}") if self._ws: self._ws.send(json_body) From 3f092f1f8b9a45b91ee4dc81fe2eef622bcf2d4e Mon Sep 17 00:00:00 2001 From: Erwin Wee Date: Tue, 20 Aug 2024 23:51:35 +0800 Subject: [PATCH 20/21] fix mypy issues --- chainbench/profile/solana/get_program_accounts_shark.py | 2 +- chainbench/profile/solana/get_program_accounts_stake.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/chainbench/profile/solana/get_program_accounts_shark.py b/chainbench/profile/solana/get_program_accounts_shark.py index 62370e6..e44c450 100644 --- a/chainbench/profile/solana/get_program_accounts_shark.py +++ b/chainbench/profile/solana/get_program_accounts_shark.py @@ -1,6 +1,6 @@ from locust import task -from chainbench.user.http import RpcCall +from chainbench.user.jsonrpc import RpcCall from chainbench.user.protocol.solana import SolanaUser diff --git a/chainbench/profile/solana/get_program_accounts_stake.py b/chainbench/profile/solana/get_program_accounts_stake.py index a618084..10cefea 100644 --- a/chainbench/profile/solana/get_program_accounts_stake.py +++ b/chainbench/profile/solana/get_program_accounts_stake.py @@ -1,6 +1,6 @@ from locust import task -from chainbench.user.http import RpcCall +from chainbench.user.jsonrpc import RpcCall from chainbench.user.protocol.solana import SolanaUser From 5201059c86df212cfc9f6f59e65c3907d2605c72 Mon Sep 17 00:00:00 2001 From: Erwin Wee Date: Wed, 21 Aug 2024 00:32:39 +0800 Subject: [PATCH 21/21] fix issues --- chainbench/profile/ethereum/subscriptions.py | 4 +- chainbench/user/jsonrpc.py | 13 ++- chainbench/user/wss.py | 2 +- chainbench/util/event.py | 111 ++++++++++--------- chainbench/util/jsonrpc.py | 2 +- 5 files changed, 67 insertions(+), 65 deletions(-) diff --git a/chainbench/profile/ethereum/subscriptions.py b/chainbench/profile/ethereum/subscriptions.py index 1c42a79..079c1ab 100644 --- a/chainbench/profile/ethereum/subscriptions.py +++ b/chainbench/profile/ethereum/subscriptions.py @@ -1,8 +1,8 @@ -from chainbench.user import EvmUser, WssJrpcUser +from chainbench.user import WssJrpcUser from chainbench.user.protocol.ethereum import EthSubscribe -class EthSubscriptions(WssJrpcUser, EvmUser): +class EthSubscriptions(WssJrpcUser): subscriptions = [ EthSubscribe(["newHeads"]), # logs subscription for approve method signature diff --git a/chainbench/user/jsonrpc.py b/chainbench/user/jsonrpc.py index a7f78ad..cc110c0 100644 --- a/chainbench/user/jsonrpc.py +++ b/chainbench/user/jsonrpc.py @@ -98,13 +98,14 @@ def make_rpc_call( path: str = "", ) -> None: """Make a JSON-RPC call.""" - if rpc_call is None and method is not None: - rpc_call = RpcCall(method, params) + if rpc_call is None: + if method is None: + raise ValueError("Either rpc_call or method must be provided") + else: + rpc_call = RpcCall(method, params) + name = method else: - raise ValueError("Either rpc_call or method must be provided") - - if name == "" and method is not None: - name = method + name = rpc_call.method with self.client.request( "POST", self.rpc_path + path, json=rpc_call.request_body(), name=name, catch_response=True diff --git a/chainbench/user/wss.py b/chainbench/user/wss.py index c2c13ea..bf2bf8f 100644 --- a/chainbench/user/wss.py +++ b/chainbench/user/wss.py @@ -165,7 +165,7 @@ def receive_loop(self): try: while self._running: message = self._ws.recv() - self.logger.debug(f"WSResp: {message}") + self.logger.debug(f"WSResp: {message.strip()}") self.on_message(message) else: self._ws.close() diff --git a/chainbench/util/event.py b/chainbench/util/event.py index 85af343..cdbfbcb 100644 --- a/chainbench/util/event.py +++ b/chainbench/util/event.py @@ -179,65 +179,66 @@ def on_init(environment: Environment, **_kwargs): test_data: dict[str, t.Any] = {} for user in environment.runner.user_classes: if not hasattr(user, "test_data"): - raise AttributeError(f"{user} class does not have 'test_data' attribute") - user_test_data: TestData = getattr(user, "test_data") - test_data_class_name: str = type(user_test_data).__name__ - if test_data_class_name in test_data: - continue - logger.info(f"Initializing test data for {test_data_class_name}") - print(f"Initializing test data for {test_data_class_name}") - if environment.host: - user_test_data.init_http_client(environment.host) - if isinstance(user_test_data, EvmTestData): - chain_id: ChainId = user_test_data.fetch_chain_id() - user_test_data.init_network(chain_id) - logger.info(f"Target endpoint network is {user_test_data.network.name}") - print(f"Target endpoint network is {user_test_data.network.name}") - test_data["chain_id"] = {test_data_class_name: chain_id} - if environment.parsed_options: - user_test_data.init_data(environment.parsed_options) - test_data[test_data_class_name] = user_test_data.data.to_json() - send_msg_to_workers(environment.runner, "test_data", test_data) - print("Fetching blocks...") - if environment.parsed_options.use_latest_blocks: - print(f"Using latest {user_test_data.data.size.blocks_len} blocks as test data") - logger.info(f"Using latest {user_test_data.data.size.blocks_len} blocks as test data") - for block_number in range( - user_test_data.data.block_range.start, user_test_data.data.block_range.end + 1 - ): - block = None - try: - block = user_test_data.fetch_block(block_number) - except (BlockNotFoundError, InvalidBlockError): - pass - while block is None: + logger.warning(f"{user} class does not have 'test_data' attribute") + else: + user_test_data: TestData = getattr(user, "test_data") + test_data_class_name: str = type(user_test_data).__name__ + if test_data_class_name in test_data: + continue + logger.info(f"Initializing test data for {test_data_class_name}") + print(f"Initializing test data for {test_data_class_name}") + if environment.host: + user_test_data.init_http_client(environment.host) + if isinstance(user_test_data, EvmTestData): + chain_id: ChainId = user_test_data.fetch_chain_id() + user_test_data.init_network(chain_id) + logger.info(f"Target endpoint network is {user_test_data.network.name}") + print(f"Target endpoint network is {user_test_data.network.name}") + test_data["chain_id"] = {test_data_class_name: chain_id} + if environment.parsed_options: + user_test_data.init_data(environment.parsed_options) + test_data[test_data_class_name] = user_test_data.data.to_json() + send_msg_to_workers(environment.runner, "test_data", test_data) + print("Fetching blocks...") + if environment.parsed_options.use_latest_blocks: + print(f"Using latest {user_test_data.data.size.blocks_len} blocks as test data") + logger.info(f"Using latest {user_test_data.data.size.blocks_len} blocks as test data") + for block_number in range( + user_test_data.data.block_range.start, user_test_data.data.block_range.end + 1 + ): + block = None try: - block = user_test_data.fetch_latest_block() + block = user_test_data.fetch_block(block_number) except (BlockNotFoundError, InvalidBlockError): pass - user_test_data.data.push_block(block) - block_data = {test_data_class_name: block.to_json()} - send_msg_to_workers(environment.runner, "block_data", block_data) - print(user_test_data.data.stats(), end="\r") + while block is None: + try: + block = user_test_data.fetch_latest_block() + except (BlockNotFoundError, InvalidBlockError): + pass + user_test_data.data.push_block(block) + block_data = {test_data_class_name: block.to_json()} + send_msg_to_workers(environment.runner, "block_data", block_data) + print(user_test_data.data.stats(), end="\r") + else: + print(user_test_data.data.stats(), end="\r") + print("\n") # new line after progress display upon exiting loop else: - print(user_test_data.data.stats(), end="\r") - print("\n") # new line after progress display upon exiting loop - else: - while user_test_data.data.size.blocks_len > len(user_test_data.data.blocks): - try: - block = user_test_data.fetch_random_block(user_test_data.data.block_numbers) - except (BlockNotFoundError, InvalidBlockError): - continue - user_test_data.data.push_block(block) - block_data = {test_data_class_name: block.to_json()} - send_msg_to_workers(environment.runner, "block_data", block_data) - print(user_test_data.data.stats(), end="\r") - else: - print(user_test_data.data.stats(), end="\r") - print("\n") # new line after progress display upon exiting loop - logger.info("Test data is ready") - send_msg_to_workers(environment.runner, "release_lock", {}) - user_test_data.release_lock() + while user_test_data.data.size.blocks_len > len(user_test_data.data.blocks): + try: + block = user_test_data.fetch_random_block(user_test_data.data.block_numbers) + except (BlockNotFoundError, InvalidBlockError): + continue + user_test_data.data.push_block(block) + block_data = {test_data_class_name: block.to_json()} + send_msg_to_workers(environment.runner, "block_data", block_data) + print(user_test_data.data.stats(), end="\r") + else: + print(user_test_data.data.stats(), end="\r") + print("\n") # new line after progress display upon exiting loop + logger.info("Test data is ready") + send_msg_to_workers(environment.runner, "release_lock", {}) + user_test_data.release_lock() except Exception as e: logger.error(f"Failed to init test data: {e.__class__.__name__}: {e}. Exiting...") print(f"Failed to init test data:\n {e.__class__.__name__}: {e}. Exiting...") diff --git a/chainbench/util/jsonrpc.py b/chainbench/util/jsonrpc.py index 9099695..0fab3be 100644 --- a/chainbench/util/jsonrpc.py +++ b/chainbench/util/jsonrpc.py @@ -31,7 +31,7 @@ def request_body(self, request_id: int | None = None) -> dict: "jsonrpc": "2.0", "method": self.method, "params": self.params, - "id": self._request_id, + "id": self.request_id, }