Skip to content

Commit 1bd7ed0

Browse files
authored
improve profile discovery (#12)
* introduce tags for debug and trace methods * rework profile discovery and add light and heavy profiles for EVM * update readme
1 parent e4aa35d commit 1bd7ed0

File tree

9 files changed

+178
-23
lines changed

9 files changed

+178
-23
lines changed

README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,32 @@ After the test is finished, the tool will automatically quit.
103103
- `--autoquit`: This flag tells the Chainbench tool to automatically quit after the test has finished. This is useful for running the benchmark in an automated environment where manual intervention is not desired.
104104
- `--help`: This flag displays the help message.
105105

106+
By default, Chainbench uses `constant_pacing` wait time with a value of 10 seconds.
107+
This means a load test with 100 users will have a theoretical maximum of 10 rps. This allows us to have a target rps in a test,
108+
and the result rps lower than the target rps means performance deterioration.
109+
106110
### Profiles
107-
Profiles are located in the [`profile`](chainbench/profile) directory. For a tutorial on how to create custom profiles, please refer to [this document](docs/PROFILE.md).
111+
Default profiles are located in the [`profile`](chainbench/profile) directory. For a tutorial on how to create custom profiles, please refer to [this document](docs/PROFILE.md).
112+
113+
You can also use the `--profile-dir` flag to specify a custom directory with profiles. For example:
114+
```shell
115+
chainbench start --profile-dir /path/to/profiles --profile my-profile --users 50 --workers 2 --test-time 12h --target https://node-url --headless --autoquit
116+
```
117+
This will run a load test using `/path/to/profiles/my-profile.py` profile.
118+
119+
It's possible to group profiles into directories. For example, you can create a directory called `bsc` and put all the BSC profiles there. Then you can run a load test using the following command:
120+
```shell
121+
chainbench start --profile-dir /path/to/profiles --profile bsc.my-profile --users 50 --workers 2 --test-time 12h --target https://node-url --headless --autoquit
122+
```
123+
124+
Chainbench will look for the profile in `/path/to/profiles/bsc/my-profile.py`. Currently, only one level of nesting is supported.
125+
126+
There are built-in `evm.light` and `evm.heavy` profiles for EVM-compatible chains.
127+
128+
Here's an example of how to run a load test for Ethereum using the `evm.light` profile:
129+
```shell
130+
chainbench start --profile evm.light --users 50 --workers 2 --test-time 12h --target https://node-url --headless --autoquit
131+
```
108132

109133
### Web UI Mode
110134

chainbench/main.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import os
23
import shlex
34
import subprocess
45
import sys
@@ -9,6 +10,7 @@
910
from chainbench.util.cli import (
1011
ContextData,
1112
ensure_results_dir,
13+
get_base_path,
1214
get_master_command,
1315
get_profile_path,
1416
get_worker_command,
@@ -48,6 +50,9 @@ def cli(ctx: click.Context):
4850
help="Profile to run",
4951
show_default=True,
5052
)
53+
@click.option(
54+
"--profile-dir", default=None, type=click.Path(), help="Profile directory"
55+
)
5156
@click.option("--host", default=MASTER_HOST, help="Host to run on", show_default=True)
5257
@click.option("--port", default=MASTER_PORT, help="Port to run on", show_default=True)
5358
@click.option(
@@ -76,6 +81,7 @@ def cli(ctx: click.Context):
7681
def start(
7782
ctx: click.Context,
7883
profile: str,
84+
profile_dir: Path | None,
7985
host: str,
8086
port: int,
8187
workers: int,
@@ -102,7 +108,10 @@ def start(
102108
click.echo("Target is required when running in headless mode")
103109
sys.exit(1)
104110

105-
profile_path = get_profile_path(profile, __file__)
111+
if not profile_dir:
112+
profile_dir = get_base_path(__file__)
113+
114+
profile_path = get_profile_path(profile_dir, profile)
106115

107116
if not profile_path.exists():
108117
click.echo(f"Profile file {profile_path} does not exist")
@@ -131,12 +140,17 @@ def start(
131140
workers=workers,
132141
headless=headless,
133142
target=target,
143+
# TODO: Add support for tags in the CLI
144+
exclude_tags=["trace", "debug"],
134145
)
135146
if headless:
136147
click.echo(f"Starting master in headless mode for {profile}")
137148
else:
138149
click.echo(f"Starting master for {profile}")
139-
master_args = shlex.split(master_command)
150+
151+
is_posix = os.name == "posix"
152+
153+
master_args = shlex.split(master_command, posix=is_posix)
140154
master_process = subprocess.Popen(master_args)
141155
ctx.obj.master = master_process
142156
# Start the Locust workers
@@ -150,12 +164,13 @@ def start(
150164
target=target,
151165
worker_id=worker_id,
152166
log_level=log_level,
167+
# TODO: Add support for tags in the CLI
168+
exclude_tags=["trace", "debug"],
153169
)
154-
worker_args = shlex.split(worker_command)
170+
worker_args = shlex.split(worker_command, posix=is_posix)
155171
worker_process = subprocess.Popen(worker_args)
156172
ctx.obj.workers.append(worker_process)
157173
click.echo(f"Starting worker {worker_id + 1} for {profile}")
158-
# Print out the URL to access the test
159174
if headless:
160175
click.echo(f"Running test in headless mode for {profile}")
161176
ctx.obj.notifier.notify(
@@ -164,13 +179,15 @@ def start(
164179
tags=["loudspeaker"],
165180
)
166181
else:
182+
# Print out the URL to access the test
167183
click.echo(f"Run test: http://127.0.0.1:8089 {profile}")
168184

169185
for process in ctx.obj.workers:
170186
process.wait()
171187

172188
if autoquit:
173-
click.echo("Quitting when test is finished")
189+
ctx.obj.master.wait()
190+
click.echo("Quitting...")
174191
ctx.obj.master.terminate()
175192

176193
ctx.obj.notifier.notify(

chainbench/profile/ethereum.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"Others" : 12
1616
```
1717
"""
18-
from locust import task
18+
from locust import tag, task
1919

2020
from chainbench.user.evm import EVMBenchUser
2121

@@ -91,8 +91,8 @@ def get_logs_task(self):
9191
params=self._get_logs_params_factory(),
9292
),
9393

94-
# TODO: introduce tags to make it possible to filter out unsupported methods
95-
# @task(3)
94+
@tag("debug")
95+
@task(3)
9696
def trace_transaction_task(self):
9797
self.make_call(
9898
name="trace_transaction",

chainbench/profile/evm/heavy.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""
2+
Ethereum profile (heavy mode).
3+
"""
4+
from locust import task
5+
6+
from chainbench.user.evm import EVMBenchUser
7+
8+
9+
class EthereumHeavyProfile(EVMBenchUser):
10+
@task
11+
def trace_transaction_task(self):
12+
self.make_call(
13+
name="trace_transaction",
14+
method="debug_traceTransaction",
15+
params=[],
16+
),
17+
18+
@task
19+
def block_task(self):
20+
self.make_call(
21+
name="block",
22+
method="trace_block",
23+
params=self._block_by_number_params_factory(),
24+
),
25+
26+
@task
27+
def get_logs_task(self):
28+
self.make_call(
29+
name="get_logs",
30+
method="eth_getLogs",
31+
params=self._get_logs_params_factory(),
32+
),

chainbench/profile/evm/light.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""
2+
Ethereum profile (light mode).
3+
"""
4+
from locust import task
5+
6+
from chainbench.user.evm import EVMBenchUser
7+
8+
9+
class EthereumLightProfile(EVMBenchUser):
10+
@task
11+
def get_transaction_receipt_task(self):
12+
self.make_call(
13+
name="get_transaction_receipt",
14+
method="eth_getTransactionReceipt",
15+
params=self._transaction_by_hash_params_factory(),
16+
),
17+
18+
@task
19+
def block_number_task(self):
20+
self.make_call(
21+
name="block_number",
22+
method="eth_blockNumber",
23+
params=[],
24+
),
25+
26+
@task
27+
def get_balance_task(self):
28+
self.make_call(
29+
name="get_balance",
30+
method="eth_getBalance",
31+
params=self._get_balance_params_factory_latest(),
32+
),
33+
34+
@task
35+
def chain_id_task(self):
36+
self.make_call(
37+
name="chain_id",
38+
method="eth_chainId",
39+
params=[],
40+
),
41+
42+
@task
43+
def get_block_by_number_task(self):
44+
self.make_call(
45+
name="get_block_by_number",
46+
method="eth_getBlockByNumber",
47+
params=self._block_by_number_params_factory(),
48+
),
49+
50+
@task
51+
def get_transaction_by_hash_task(self):
52+
self.make_call(
53+
name="get_transaction_by_hash",
54+
method="eth_getTransactionByHash",
55+
params=self._transaction_by_hash_params_factory(),
56+
),
57+
58+
@task
59+
def client_version_task(self):
60+
self.make_call(
61+
name="client_version",
62+
method="web3_clientVersion",
63+
params=[],
64+
),

chainbench/profile/polygon.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"Others" : 9
1919
```
2020
"""
21-
from locust import task
21+
from locust import tag, task
2222

2323
from chainbench.user.evm import EVMBenchUser
2424

@@ -94,8 +94,8 @@ def get_balance_task(self):
9494
params=self._get_balance_params_factory_latest(),
9595
),
9696

97-
# TODO: introduce tags to make it possible to filter out unsupported methods
98-
# @task(2)
97+
@tag("trace")
98+
@task(2)
9999
def block_task(self):
100100
self.make_call(
101101
name="block",

chainbench/user/base.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@ def on_stop(self):
3636

3737
def check_fatal(self, response: RestResponseContextManager):
3838
if response.status_code == 401:
39-
self.logger.critical(f"⛔️ Unauthorized request to {response.url}")
39+
self.logger.critical(f"Unauthorized request to {response.url}")
4040
elif response.status_code == 404:
41-
self.logger.critical(f"⛔️ Not found: {response.url}")
41+
self.logger.critical(f"Not found: {response.url}")
4242
elif 500 <= response.status_code <= 599:
4343
self.logger.critical(
44-
f"⛔️ Got internal server error when requesting {response.url}"
44+
f"Got internal server error when requesting {response.url}"
4545
)
4646
elif 300 <= response.status_code <= 399:
47-
self.logger.critical(f"⛔️ Redirect error: {response.url}")
47+
self.logger.critical(f"Redirect error: {response.url}")
4848

4949
def check_response(self, response: RestResponseContextManager):
5050
"""Check the response for errors."""

chainbench/util/cli.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,20 @@
66
from chainbench.util.notify import NoopNotifier, Notifier
77

88

9-
def get_profile_path(profile: str, src_path: str | Path) -> Path:
10-
"""Get profile path."""
9+
def get_base_path(src_path: str | Path) -> Path:
10+
"""Get base path."""
1111
curr_path = Path(src_path).resolve()
12-
profile_path = curr_path.parent / f"profile/{profile}.py"
12+
base_path = curr_path.parent / "profile"
13+
return base_path
14+
15+
16+
def get_profile_path(base_path: Path, profile: str) -> Path:
17+
"""Get profile path."""
18+
subdir, _, profile = profile.rpartition(".")
19+
if subdir:
20+
profile_path = base_path / subdir / f"{profile}.py"
21+
else:
22+
profile_path = base_path / f"{profile}.py"
1323
return profile_path
1424

1525

@@ -42,6 +52,7 @@ def get_master_command(
4252
results_path: Path,
4353
target: str | None = None,
4454
headless: bool = False,
55+
exclude_tags: list[str] | None = None,
4556
) -> str:
4657
"""Generate master command."""
4758
command = (
@@ -59,6 +70,9 @@ def get_master_command(
5970
if headless:
6071
command += " --headless"
6172

73+
if exclude_tags:
74+
command += f" --exclude-tags {' '.join(exclude_tags)}"
75+
6276
return command
6377

6478

@@ -71,6 +85,7 @@ def get_worker_command(
7185
target: str | None = None,
7286
headless: bool = False,
7387
worker_id: int = 0,
88+
exclude_tags: list[str] | None = None,
7489
) -> str:
7590
"""Generate worker command."""
7691
command = (
@@ -84,6 +99,9 @@ def get_worker_command(
8499
if headless:
85100
command += " --headless"
86101

102+
if exclude_tags:
103+
command += f" --exclude-tags {' '.join(exclude_tags)}"
104+
87105
return command
88106

89107

chainbench/util/event.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ def on_test_stop(environment, **_kwargs):
3232
if not isinstance(runner, MasterRunner):
3333
# Print worker details to the log
3434
logger.info(
35-
f"🏁 Worker[{runner.worker_index:02d}]: Tests completed in "
35+
f"Worker[{runner.worker_index:02d}]: Tests completed in "
3636
f"{Timer.get_time_diff(runner.worker_index):>.3f} seconds"
3737
)
3838
else:
3939
# Print master details to the log
40-
logger.info("🏁 Master: The test is stopped")
40+
logger.info("Master: The test is stopped")
4141

4242

4343
# Listener for the init event
@@ -51,11 +51,11 @@ def on_init(environment, **_kwargs):
5151

5252
if isinstance(environment.runner, MasterRunner):
5353
# Print master details to the log
54-
logger.info("🤖 I'm a master. Running tests for %s", host_under_test)
54+
logger.info("I'm a master. Running tests for %s", host_under_test)
5555

5656
if isinstance(environment.runner, WorkerRunner):
5757
# Print worker details to the log
58-
logger.info("🤖 I'm a worker. Running tests for %s", host_under_test)
58+
logger.info("I'm a worker. Running tests for %s", host_under_test)
5959
logger.info("Initializing test data...")
6060
for user in environment.runner.user_classes:
6161
if not hasattr(user, "test_data"):

0 commit comments

Comments
 (0)