Skip to content

Commit 177c49e

Browse files
authored
Feature/add solana profile (#34)
- refactor test data generation to happen only once in the master process, and then use custom messaging to send test data to all workers. - remove duplicate code from monitor.py, use existing function from locust.util.timespan instead. - added test_data module for solana to generate solana test data - added user module for solana for creating rpc requests - added solana locust profile - updated dependencies - updated README.md
1 parent 676b68c commit 177c49e

File tree

16 files changed

+524
-161
lines changed

16 files changed

+524
-161
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Chainbench lets you to easily define profiles for any EVM-compatible chain.
3535
You can use not only hard-coded values but also real chain data to generate dynamic call parameters.
3636

3737
Main features:
38-
- Built-in profiles for Ethereum, Binance Smart Chain, Polygon, Oasis, and Avalanche
38+
- Built-in profiles for Ethereum, Binance Smart Chain, Polygon, Oasis, Avalanche and Solana
3939
- Support for custom profiles
4040
- Dynamic call params generation using real chain data
4141
- Headless and web UI modes

chainbench/main.py

Lines changed: 13 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -56,28 +56,18 @@ def cli(ctx: click.Context):
5656
help="Profile to run",
5757
show_default=True,
5858
)
59-
@click.option(
60-
"-d", "--profile-dir", default=None, type=click.Path(), help="Profile directory"
61-
)
62-
@click.option(
63-
"-H", "--host", default=MASTER_HOST, help="Host to run on", show_default=True
64-
)
65-
@click.option(
66-
"-P", "--port", default=MASTER_PORT, help="Port to run on", show_default=True
67-
)
59+
@click.option("-d", "--profile-dir", default=None, type=click.Path(), help="Profile directory")
60+
@click.option("-H", "--host", default=MASTER_HOST, help="Host to run on", show_default=True)
61+
@click.option("-P", "--port", default=MASTER_PORT, help="Port to run on", show_default=True)
6862
@click.option(
6963
"-w",
7064
"--workers",
7165
default=WORKER_COUNT,
7266
help="Number of workers to run",
7367
show_default=True,
7468
)
75-
@click.option(
76-
"-t", "--test-time", default=TEST_TIME, help="Test time", show_default=True
77-
)
78-
@click.option(
79-
"-u", "--users", default=USERS, help="Target number of users", show_default=True
80-
)
69+
@click.option("-t", "--test-time", default=TEST_TIME, help="Test time", show_default=True)
70+
@click.option("-u", "--users", default=USERS, help="Target number of users", show_default=True)
8171
@click.option(
8272
"-r",
8373
"--spawn-rate",
@@ -116,29 +106,20 @@ def cli(ctx: click.Context):
116106
"-E",
117107
"--exclude-tags",
118108
default=[],
119-
help="Exclude tasks tagged with custom tags from the test. "
120-
"You may specify this option multiple times",
109+
help="Exclude tasks tagged with custom tags from the test. " "You may specify this option multiple times",
121110
multiple=True,
122111
)
123-
@click.option(
124-
"--timescale", is_flag=True, help="Export data to PG with timescale extension"
125-
)
126-
@click.option(
127-
"--pg-host", default=None, help="Hostname of PG instance with timescale extension"
128-
)
112+
@click.option("--timescale", is_flag=True, help="Export data to PG with timescale extension")
113+
@click.option("--pg-host", default=None, help="Hostname of PG instance with timescale extension")
129114
@click.option(
130115
"--pg-port",
131116
default=5432,
132117
help="Port of PG instance with timescale extension",
133118
show_default=True,
134119
)
135-
@click.option(
136-
"--pg-username", default="postgres", help="PG username", show_default=True
137-
)
120+
@click.option("--pg-username", default="postgres", help="PG username", show_default=True)
138121
@click.option("--pg-password", default=None, help="PG password")
139-
@click.option(
140-
"--use-recent-blocks", is_flag=True, help="Uses recent blocks for test data"
141-
)
122+
@click.option("--use-recent-blocks", is_flag=True, help="Uses recent blocks for test data")
142123
@click.pass_context
143124
def start(
144125
ctx: click.Context,
@@ -179,9 +160,7 @@ def start(
179160
click.echo("Target is required when running in headless mode")
180161
sys.exit(1)
181162

182-
if timescale and any(
183-
pg_arg is None for pg_arg in (pg_host, pg_port, pg_username, pg_password)
184-
):
163+
if timescale and any(pg_arg is None for pg_arg in (pg_host, pg_port, pg_username, pg_password)):
185164
click.echo(
186165
"PG connection parameters are required "
187166
"when --timescale flag is used: pg_host, pg_port, pg_username, pg_password"
@@ -201,9 +180,7 @@ def start(
201180

202181
click.echo(f"Results directory: {results_dir}")
203182

204-
results_path = ensure_results_dir(
205-
profile=profile, parent_dir=results_dir, run_id=run_id
206-
)
183+
results_path = ensure_results_dir(profile=profile, parent_dir=results_dir, run_id=run_id)
207184

208185
click.echo(f"Results will be saved to {results_path}")
209186

@@ -299,9 +276,7 @@ def start(
299276
click.echo("Quitting...")
300277
ctx.obj.master.terminate()
301278

302-
ctx.obj.notifier.notify(
303-
title="Test finished", message=f"Test finished for {profile}", tags=["tada"]
304-
)
279+
ctx.obj.notifier.notify(title="Test finished", message=f"Test finished for {profile}", tags=["tada"])
305280

306281

307282
if __name__ == "__main__":

chainbench/profile/solana/general.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"""
2+
Solana profile.
3+
4+
Chart:
5+
```mermaid
6+
%%{init: {'theme':'forest'}}%%
7+
pie title Methods Distribution
8+
"getAccountInfo" : 53
9+
"getBlock" : 9
10+
"getTokenAccountsByOwner" : 8
11+
"getMultipleAccounts" : 8
12+
"getTransaction" : 7
13+
"getSignaturesForAddress" : 4
14+
"getLatestBlockhash" : 4
15+
"getBalance" : 4
16+
"Others" : 3
17+
```
18+
"""
19+
from locust import constant_pacing, tag, task
20+
21+
from chainbench.user.solana import SolanaBenchUser
22+
from chainbench.util.rng import get_rng
23+
24+
25+
class SolanaProfile(SolanaBenchUser):
26+
wait_time = constant_pacing(2)
27+
28+
@task(1000)
29+
def get_account_info_task(self):
30+
self.make_call(
31+
method="getAccountInfo",
32+
params=self._get_account_info_params_factory(get_rng()),
33+
),
34+
35+
@task(175)
36+
def get_block_task(self):
37+
self.make_call(
38+
method="getBlock",
39+
params=self._get_block_params_factory(get_rng()),
40+
),
41+
42+
@task(150)
43+
def get_token_accounts_by_owner(self):
44+
self.make_call(
45+
method="getTokenAccountsByOwner",
46+
params=self._get_token_accounts_by_owner_params_factory(get_rng()),
47+
),
48+
49+
@task(150)
50+
def get_multiple_accounts(self):
51+
self.make_call(
52+
method="getMultipleAccounts",
53+
params=self._get_multiple_accounts_params_factory(get_rng()),
54+
),
55+
56+
@task(130)
57+
def get_transaction(self):
58+
self.make_call(
59+
method="getTransaction",
60+
params=self._get_transaction_params_factory(get_rng()),
61+
),
62+
63+
@task(75)
64+
def get_signatures_for_address(self):
65+
self.make_call(
66+
method="getSignaturesForAddress",
67+
params=self._get_signatures_for_address_params_factory(get_rng()),
68+
),
69+
70+
@task(75)
71+
def get_latest_blockhash(self):
72+
self.make_call(
73+
method="getLatestBlockhash",
74+
params=[],
75+
),
76+
77+
@task(75)
78+
def get_balance(self):
79+
self.make_call(
80+
method="getBalance",
81+
params=self._get_balance_params_factory(get_rng()),
82+
),
83+
84+
@task(20)
85+
def get_slot(self):
86+
self.make_call(
87+
method="getSlot",
88+
params=[],
89+
),
90+
91+
@task(15)
92+
def get_block_height(self):
93+
self.make_call(
94+
method="getBlockHeight",
95+
params=[],
96+
),
97+
98+
@task(5)
99+
@tag("get-program-accounts")
100+
def get_program_accounts(self):
101+
self.make_call(
102+
method="getProgramAccounts",
103+
params=[
104+
"SharkXwkS3h24fJ2LZvgG5tPbsH3BKQYuAtKdqskf1f",
105+
{"encoding": "base64", "commitment": "confirmed"},
106+
],
107+
),
108+
109+
@task(4)
110+
def get_signature_statuses(self):
111+
self.make_call(
112+
method="getSignatureStatuses",
113+
params=self._get_signature_statuses_params_factory(get_rng()),
114+
),
115+
116+
@task(3)
117+
def get_recent_blockhash(self):
118+
self.make_call(
119+
method="getRecentBlockhash",
120+
params=[],
121+
),
122+
123+
@task(2)
124+
def get_blocks(self):
125+
self.make_call(
126+
method="getBlocks",
127+
params=self._get_blocks_params_factory(get_rng()),
128+
),
129+
130+
@task(2)
131+
def get_epoch_info(self):
132+
self.make_call(
133+
method="getEpochInfo",
134+
params=[],
135+
),
136+
137+
@task(2)
138+
def get_confirmed_signatures_for_address2(self):
139+
self.make_call(
140+
method="getConfirmedSignaturesForAddress2",
141+
params=self._get_confirmed_signatures_for_address2_params_factory(get_rng()),
142+
),

chainbench/test_data/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from .base import BaseTestData
22
from .dummy import DummyTestData
33
from .evm import EVMTestData
4+
from .solana import SolanaTestData
45

56
__all__ = [
67
"BaseTestData",
78
"DummyTestData",
89
"EVMTestData",
10+
"SolanaTestData",
911
]

chainbench/test_data/base.py

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import json
12
import logging
23
import typing as t
4+
from argparse import Namespace
35
from dataclasses import dataclass, field
46
from secrets import token_hex
57

@@ -29,6 +31,24 @@ class BlockchainData:
2931
tx_hashes: TxHashes = field(default_factory=list)
3032
accounts: Accounts = field(default_factory=list)
3133

34+
def to_json(self):
35+
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4)
36+
37+
def from_json(self, json_data):
38+
data = json.loads(json_data)
39+
self.start_block_number = data["start_block_number"]
40+
self.end_block_number = data["end_block_number"]
41+
self.blocks = data["blocks"]
42+
self.txs = data["txs"]
43+
self.tx_hashes = data["tx_hashes"]
44+
self.accounts = data["accounts"]
45+
46+
47+
class ChainInfo(t.TypedDict):
48+
name: str
49+
start_block: int
50+
end_block: int
51+
3252

3353
class BaseTestData:
3454
def __init__(self, rpc_version: str = "2.0"):
@@ -44,11 +64,11 @@ def __init__(self, rpc_version: str = "2.0"):
4464

4565
self._data: BlockchainData | None = None
4666

47-
def update(self, host_url: str, use_recent_blocks: bool = False) -> BlockchainData:
67+
def update(self, host_url: str, parsed_options: Namespace) -> BlockchainData:
4868
self._logger.info("Updating data")
4969
self._host = host_url
5070
self._logger.debug("Host: %s", self._host)
51-
data = self._get_init_data(use_recent_blocks)
71+
data = self._get_init_data(parsed_options)
5272
self._logger.info("Data fetched")
5373
self._logger.debug("Data: %s", data)
5474
self._data = data
@@ -57,9 +77,16 @@ def update(self, host_url: str, use_recent_blocks: bool = False) -> BlockchainDa
5777
self._logger.info("Lock released")
5878
return data
5979

60-
def _get_init_data(self, use_recent_blocks) -> BlockchainData:
80+
def _get_init_data(self, parsed_options) -> BlockchainData:
6181
raise NotImplementedError
6282

83+
def init_data_from_json(self, json_data: str):
84+
self._data = BlockchainData()
85+
self._data.from_json(json_data)
86+
self._logger.info("Data updated. Releasing lock")
87+
self._lock.release()
88+
self._logger.info("Lock released")
89+
6390
@property
6491
def initialized(self) -> bool:
6592
return self._data is not None
@@ -78,6 +105,18 @@ def data(self) -> BlockchainData:
78105

79106
return self._data
80107

108+
@staticmethod
109+
def _parse_hex_to_int(value: str) -> int:
110+
return int(value, 16)
111+
112+
@staticmethod
113+
def _append_if_not_none(data, val):
114+
if val is not None:
115+
if isinstance(data, list):
116+
data.append(val)
117+
elif isinstance(data, set):
118+
data.add(val)
119+
81120
def _make_body(self, method: str, params: list[t.Any] | None = None):
82121
if params is None:
83122
params = []
@@ -98,9 +137,7 @@ def _make_call(self, method: str, params: list[t.Any] | None = None):
98137
json=self._make_body(method, params),
99138
)
100139

101-
self._logger.debug(
102-
f"Making call to {self.host} with method {method} and params {params}"
103-
)
140+
self._logger.debug(f"Making call to {self.host} with method {method} and params {params}")
104141
self._logger.debug(f"Response: {response.text}")
105142

106143
response.raise_for_status()
@@ -152,9 +189,7 @@ def get_random_tx_hash(self, rng: RNG | None = None) -> TxHash:
152189
rng = get_rng()
153190
return rng.random.choice(self.tx_hashes)
154191

155-
def get_random_recent_block_number(
156-
self, n: int, rng: RNG | None = None
157-
) -> BlockNumber:
192+
def get_random_recent_block_number(self, n: int, rng: RNG | None = None) -> BlockNumber:
158193
if rng is None:
159194
rng = get_rng()
160195
return rng.random.randint(

0 commit comments

Comments
 (0)