diff --git a/Dockerfile b/Dockerfile index 3253f8b..f462f91 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,15 @@ +# chainbench-grpc (Solana Yellowstone gRPC tool) build stage. +# Builds the Rust binary from a pinned ref so `chainbench grpc` can shell out to +# it. protoc is vendored by the crate, so no extra system deps are needed. +# NOTE: assumes the repo is reachable anonymously. If it stays private, pass a +# token via BuildKit secret (`--mount=type=secret,id=gh_token`) instead. +FROM rust:1.90-bookworm AS grpc +ARG CHAINBENCH_GRPC_REF=v0.4.0 +RUN git clone --depth 1 --branch "${CHAINBENCH_GRPC_REF}" \ + https://github.com/CSFeo/chainbench-grpc /src \ + && cd /src \ + && cargo build --release + # build stage FROM python:3.10-bookworm AS venv @@ -21,6 +33,9 @@ ENV PATH /app/venv/bin:$PATH RUN apt-get update && apt-get install -y tini htop nano curl +# Bundle the chainbench-grpc binary on PATH (used by `chainbench grpc`). +COPY --from=grpc /src/target/release/chainbench-grpc /usr/local/bin/chainbench-grpc + WORKDIR /app COPY . ./ RUN python -m pip install . diff --git a/README.md b/README.md index dfc9954..13bdff7 100644 --- a/README.md +++ b/README.md @@ -256,6 +256,31 @@ chainbench list clients ``` If you don't specify the `--clients` option, the tool will default to Ethereum JSON-RPC Specification (eth). +### Solana Yellowstone gRPC benchmarks + +`chainbench grpc` benchmarks Solana Yellowstone (Geyser) gRPC endpoints. It delegates +to the bundled [`chainbench-grpc`](https://github.com/CSFeo/chainbench-grpc) binary +(shipped on `PATH` in the Docker image), so flags after the mode are passed straight +through to that tool: + +```shell +# absolute latency of a single endpoint +chainbench grpc latency --url https://grpc.example.com --token YOUR_TOKEN + +# compare two endpoints (win rate + relative latency) +chainbench grpc race -u https://ep1 -t t1 -u https://ep2 -t t2 --transactions 5000 + +# stream throughput / slot lifecycle / full report +chainbench grpc throughput --url https://grpc.example.com --duration 60 +chainbench grpc slots --url https://grpc.example.com --target-slots 100 + +# the binary renders its own help +chainbench grpc latency --help +``` + +Modes: `race`, `latency`, `throughput`, `slots`, `full`. When running outside the +Docker image, install the `chainbench-grpc` binary and ensure it is on `PATH`. + ## License This project is licensed under the [Apache 2.0 License](LICENSE). diff --git a/chainbench/grpc.py b/chainbench/grpc.py new file mode 100644 index 0000000..aea229d --- /dev/null +++ b/chainbench/grpc.py @@ -0,0 +1,60 @@ +"""``chainbench grpc`` — Solana Yellowstone gRPC benchmarks. + +Thin pass-through to the ``chainbench-grpc`` Rust binary (shipped on ``PATH`` in +the ChainBench Docker image). It forwards stdout/stderr and the process exit +code; the binary owns its own flags, validation, and ``--help``. Pass tool flags +after the mode, e.g.:: + + chainbench grpc latency --url https://grpc.example.com --token TOKEN + chainbench grpc race -u https://a -t t1 -u https://b -t t2 --transactions 5000 + chainbench grpc latency --help # rendered by the binary +""" + +import shutil +import subprocess +import sys + +import click + +GRPC_BIN = "chainbench-grpc" +GRPC_REPO = "https://github.com/CSFeo/chainbench-grpc" +MODES = ("race", "latency", "throughput", "slots", "full") + + +def _run(mode: str, args: tuple[str, ...]) -> None: + exe = shutil.which(GRPC_BIN) + if exe is None: + raise click.ClickException( + f"`{GRPC_BIN}` not found on PATH. It ships in the ChainBench Docker " + f"image; for local use install it from {GRPC_REPO}" + ) + completed = subprocess.run([exe, mode, *args]) + sys.exit(completed.returncode) + + +@click.group( + name="grpc", + help=( + "Solana Yellowstone gRPC benchmarks (delegates to the chainbench-grpc " + "binary). Flags after the mode are passed through to the binary, e.g. " + "`grpc latency --url URL --token TOKEN`." + ), +) +def grpc() -> None: + pass + + +def _register(mode: str) -> None: + @grpc.command( + name=mode, + help=f"Run chainbench-grpc `{mode}` (flags are passed through to the binary).", + context_settings={"ignore_unknown_options": True, "allow_extra_args": True}, + add_help_option=False, # let the binary render its own --help + ) + @click.argument("args", nargs=-1, type=click.UNPROCESSED) + def _cmd(args: tuple[str, ...]) -> None: + _run(mode, args) + + +for _mode in MODES: + _register(_mode) diff --git a/chainbench/main.py b/chainbench/main.py index 99328c5..80a0b92 100644 --- a/chainbench/main.py +++ b/chainbench/main.py @@ -11,6 +11,7 @@ from locust.argument_parser import parse_locustfile_paths from locust.util.load_locustfile import load_locustfile +from chainbench.grpc import grpc as grpc_group from chainbench.user import EvmUser, SolanaUser, get_subclass_tasks from chainbench.user.common import all_method_classes, all_methods from chainbench.util.cli import ( @@ -45,6 +46,9 @@ def cli(ctx: Context): ctx.obj = ContextData() +cli.add_command(grpc_group) + + def validate_method(ctx: Context, param: Parameter, value: str) -> str: if value is not None: if value not in all_methods.keys():