diff --git a/testground/benchmark/benchmark/stateless.py b/testground/benchmark/benchmark/stateless.py index bc4c612efe..e903508f1f 100644 --- a/testground/benchmark/benchmark/stateless.py +++ b/testground/benchmark/benchmark/stateless.py @@ -9,7 +9,7 @@ from pathlib import Path from typing import List -import fire +import click import requests import tomlkit @@ -38,149 +38,193 @@ ECHO_SERVER_PORT = 26659 -class CLI: - def gen( - self, - outdir: str, - hostname_template=HOSTNAME_TEMPLATE, - options={}, - ): - print("options", options) - validators = options.get("validators", 3) - fullnodes = options.get("fullnodes", 7) - num_accounts = options.get("num_accounts", 10) - num_txs = options.get("num_txs", 1000) - config_patch = options.get("config", {}) - app_patch = options.get("app", {}) - genesis_patch = options.get("genesis", {}) - outdir = Path(outdir) - cli = ChainCommand(LOCAL_CRONOSD_PATH) - (outdir / VALIDATOR_GROUP).mkdir(parents=True, exist_ok=True) - (outdir / FULLNODE_GROUP).mkdir(parents=True, exist_ok=True) - - peers = [] - for i in range(validators): - print("init validator", i) - ip = hostname_template.format(index=i) - peers.append(init_node_local(cli, outdir, VALIDATOR_GROUP, i, ip)) - for i in range(fullnodes): - print("init fullnode", i) - ip = hostname_template.format(index=i + validators) - peers.append(init_node_local(cli, outdir, FULLNODE_GROUP, i, ip)) - - print("prepare genesis") - # use a full node directory to prepare the genesis file - genesis = gen_genesis(cli, outdir / FULLNODE_GROUP / "0", peers, genesis_patch) - - print("patch genesis") - # write genesis file and patch config files - for i in range(validators): - patch_configs_local( - peers, genesis, outdir, VALIDATOR_GROUP, i, config_patch, app_patch - ) - for i in range(fullnodes): - patch_configs_local( - peers, - genesis, - outdir, - FULLNODE_GROUP, - i, - config_patch, - app_patch, - ) - - print("write config") - cfg = { - "validators": validators, - "fullnodes": fullnodes, - "num_accounts": num_accounts, - "num_txs": num_txs, - "validator-generate-load": options.get("validator-generate-load", True), - } - (outdir / "config.json").write_text(json.dumps(cfg)) - - def patchimage( - self, - toimage, - src, - dst="/data", - fromimage="ghcr.io/crypto-org-chain/cronos-testground:latest", - ): - """ - combine data directory with an exiting image to produce a new image - """ - with tempfile.TemporaryDirectory() as tmpdir: - tmpdir = Path(tmpdir) - shutil.copytree(src, tmpdir / "out") - content = f"""FROM {fromimage} +@click.group() +def cli(): + pass + + +def validate_json(ctx, param, value): + try: + return json.loads(value) + except json.JSONDecodeError: + raise click.BadParameter("must be a valid JSON string") + + +@cli.command() +@click.argument("outdir") +@click.option("--hostname-template", default=HOSTNAME_TEMPLATE) +@click.option("--validators", default=3) +@click.option("--fullnodes", default=7) +@click.option("--num-accounts", default=10) +@click.option("--num-txs", default=1000) +@click.option("--config-patch", default="{}", callback=validate_json) +@click.option("--app-patch", default="{}", callback=validate_json) +@click.option("--genesis-patch", default="{}", callback=validate_json) +@click.option("--validator-generate-load/--no-validator-generate-load", default=True) +def gen(**kwargs): + return _gen(**kwargs) + + +@cli.command() +@click.argument("options", callback=validate_json) +def generic_gen(options: dict): + return _gen(**options) + + +def _gen( + outdir: str, + hostname_template: str = HOSTNAME_TEMPLATE, + validators: int = 3, + fullnodes: int = 7, + num_accounts: int = 10, + num_txs: int = 1000, + validator_generate_load: bool = True, + config_patch: dict = None, + app_patch: dict = None, + genesis_patch: dict = None, +): + config_patch = config_patch or {} + app_patch = app_patch or {} + genesis_patch = genesis_patch or {} + + outdir = Path(outdir) + cli = ChainCommand(LOCAL_CRONOSD_PATH) + (outdir / VALIDATOR_GROUP).mkdir(parents=True, exist_ok=True) + (outdir / FULLNODE_GROUP).mkdir(parents=True, exist_ok=True) + + config_patch = ( + json.loads(config_patch) if isinstance(config_patch, str) else config_patch + ) + app_patch = json.loads(app_patch) if isinstance(app_patch, str) else app_patch + genesis_patch = ( + json.loads(genesis_patch) if isinstance(genesis_patch, str) else genesis_patch + ) + + peers = [] + for i in range(validators): + print("init validator", i) + ip = hostname_template.format(index=i) + peers.append(init_node_local(cli, outdir, VALIDATOR_GROUP, i, ip)) + for i in range(fullnodes): + print("init fullnode", i) + ip = hostname_template.format(index=i + validators) + peers.append(init_node_local(cli, outdir, FULLNODE_GROUP, i, ip)) + + print("prepare genesis") + # use a full node directory to prepare the genesis file + genesis = gen_genesis(cli, outdir / FULLNODE_GROUP / "0", peers, genesis_patch) + + print("patch genesis") + # write genesis file and patch config files + for i in range(validators): + patch_configs_local( + peers, genesis, outdir, VALIDATOR_GROUP, i, config_patch, app_patch + ) + for i in range(fullnodes): + patch_configs_local( + peers, + genesis, + outdir, + FULLNODE_GROUP, + i, + config_patch, + app_patch, + ) + + print("write config") + cfg = { + "validators": validators, + "fullnodes": fullnodes, + "num_accounts": num_accounts, + "num_txs": num_txs, + "validator-generate-load": validator_generate_load, + } + (outdir / "config.json").write_text(json.dumps(cfg)) + + +@cli.command() +@click.argument("toimage") +@click.argument("src") +@click.option("--dst", default="/data") +@click.option( + "--fromimage", default="ghcr.io/crypto-org-chain/cronos-testground:latest" +) +def patchimage(toimage, src, dst, fromimage): + """ + combine data directory with an exiting image to produce a new image + """ + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + shutil.copytree(src, tmpdir / "out") + content = f"""FROM {fromimage} ADD ./out {dst} """ - print(content) - (tmpdir / "Dockerfile").write_text(content) - subprocess.run(["docker", "build", "-t", toimage, tmpdir]) - - def run( - self, - outdir: str = "/outputs", - datadir: str = "/data", - cronosd=CONTAINER_CRONOSD_PATH, - global_seq=None, - ): - run_echo_server(ECHO_SERVER_PORT) - - datadir = Path(datadir) - cfg = json.loads((datadir / "config.json").read_text()) - - if global_seq is None: - global_seq = node_index() - - validators = cfg["validators"] - group = VALIDATOR_GROUP if global_seq < validators else FULLNODE_GROUP - group_seq = global_seq if group == VALIDATOR_GROUP else global_seq - validators - print("node role", global_seq, group, group_seq) - - home = datadir / group / str(group_seq) - - # wait for persistent peers to be ready - wait_for_peers(home) - - print("start node") - logfile = open(home / "node.log", "ab", buffering=0) - proc = subprocess.Popen( - [cronosd, "start", "--home", str(home)], - stdout=logfile, - ) + print(content) + (tmpdir / "Dockerfile").write_text(content) + subprocess.run(["docker", "build", "-t", toimage, tmpdir]) - cli = ChainCommand(cronosd) - wait_for_port(26657) - wait_for_port(8545) - wait_for_block(cli, 3) - if group == FULLNODE_GROUP or cfg.get("validator-generate-load", True): - wait_for_w3() - generate_load( - cli, cfg["num_accounts"], cfg["num_txs"], home=home, output="json" - ) +@cli.command() +@click.option("--outdir", default="/outputs") +@click.option("--datadir", default="/data") +@click.option("--cronosd", default=CONTAINER_CRONOSD_PATH) +@click.option("--global-seq", default=None) +def run(outdir: str, datadir: str, cronosd, global_seq): + run_echo_server(ECHO_SERVER_PORT) - # node quit when the chain is idle or halted for a while - detect_idle_halted(20, 20) + datadir = Path(datadir) + cfg = json.loads((datadir / "config.json").read_text()) - with (home / "block_stats.log").open("w") as logfile: - dump_block_stats(logfile) + if global_seq is None: + global_seq = node_index() - proc.kill() - proc.wait(20) + validators = cfg["validators"] + group = VALIDATOR_GROUP if global_seq < validators else FULLNODE_GROUP + group_seq = global_seq if group == VALIDATOR_GROUP else global_seq - validators + print("node role", global_seq, group, group_seq) - # collect outputs - output = Path("/data.tar.bz2") - with tarfile.open(output, "x:bz2") as tar: - tar.add(home, arcname="data", filter=output_filter(group, group_seq)) - outdir = Path(outdir) - if outdir.exists(): - assert outdir.is_dir() - filename = outdir / f"{group}_{group_seq}.tar.bz2" - filename.unlink(missing_ok=True) - shutil.copy(output, filename) + home = datadir / group / str(group_seq) + + # wait for persistent peers to be ready + wait_for_peers(home) + + print("start node") + logfile = open(home / "node.log", "ab", buffering=0) + proc = subprocess.Popen( + [cronosd, "start", "--home", str(home)], + stdout=logfile, + ) + + cli = ChainCommand(cronosd) + wait_for_port(26657) + wait_for_port(8545) + wait_for_block(cli, 3) + + if group == FULLNODE_GROUP or cfg.get("validator-generate-load", True): + wait_for_w3() + generate_load( + cli, cfg["num_accounts"], cfg["num_txs"], home=home, output="json" + ) + + # node quit when the chain is idle or halted for a while + detect_idle_halted(20, 20) + + with (home / "block_stats.log").open("w") as logfile: + dump_block_stats(logfile) + + proc.kill() + proc.wait(20) + + # collect outputs + output = Path("/data.tar.bz2") + with tarfile.open(output, "x:bz2") as tar: + tar.add(home, arcname="data", filter=output_filter(group, group_seq)) + outdir = Path(outdir) + if outdir.exists(): + assert outdir.is_dir() + filename = outdir / f"{group}_{group_seq}.tar.bz2" + filename.unlink(missing_ok=True) + shutil.copy(output, filename) def output_filter(group, group_seq: int): @@ -321,9 +365,5 @@ def dump_block_stats(fp): print("block", i, txs, timestamp, file=fp) -def main(): - fire.Fire(CLI) - - if __name__ == "__main__": - main() + cli() diff --git a/testground/benchmark/poetry.lock b/testground/benchmark/poetry.lock index 0147734545..f53b1f83ee 100644 --- a/testground/benchmark/poetry.lock +++ b/testground/benchmark/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -497,6 +497,20 @@ files = [ {file = "ckzg-1.0.2.tar.gz", hash = "sha256:4295acc380f8d42ebea4a4a0a68c424a322bb335a33bad05c72ead8cbb28d118"}, ] +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" @@ -802,20 +816,6 @@ dev = ["build (>=0.9.0)", "bumpversion (>=0.5.3)", "eth-hash[pycryptodome]", "hy docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)"] test = ["hypothesis (>=4.43.0)", "mypy (==1.5.1)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] -[[package]] -name = "fire" -version = "0.6.0" -description = "A library for automatically generating command line interfaces." -optional = false -python-versions = "*" -files = [ - {file = "fire-0.6.0.tar.gz", hash = "sha256:54ec5b996ecdd3c0309c800324a0703d6da512241bc73b553db959d98de0aa66"}, -] - -[package.dependencies] -six = "*" -termcolor = "*" - [[package]] name = "frozenlist" version = "1.4.1" @@ -1814,20 +1814,6 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] -[[package]] -name = "termcolor" -version = "2.4.0" -description = "ANSI color formatting for output in terminal" -optional = false -python-versions = ">=3.8" -files = [ - {file = "termcolor-2.4.0-py3-none-any.whl", hash = "sha256:9297c0df9c99445c2412e832e882a7884038a25617c60cea2ad69488d4040d63"}, - {file = "termcolor-2.4.0.tar.gz", hash = "sha256:aab9e56047c8ac41ed798fa36d892a37aca6b3e9159f3e0c24bc64a9b3ac7b7a"}, -] - -[package.extras] -tests = ["pytest", "pytest-cov"] - [[package]] name = "tomlkit" version = "0.12.5" @@ -2115,4 +2101,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "671465e724b62315282a119e91a4594710b2270f36a6f578665220668c48a2d6" +content-hash = "58618047dc3ced34b3e62b0115fa8b419d82b4f81357055905e4abc5bbd5e71c" diff --git a/testground/benchmark/pyproject.toml b/testground/benchmark/pyproject.toml index d0675a59d5..d19f457cd6 100644 --- a/testground/benchmark/pyproject.toml +++ b/testground/benchmark/pyproject.toml @@ -14,8 +14,8 @@ tomlkit = "^0" web3 = "^6" hexbytes = "^0" bech32 = "^1" -fire = "^0" requests = "^2.32" +click = "^8.1.7" [tool.poetry.dev-dependencies] pytest = "^8.2" @@ -26,7 +26,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] testground-testcase = "benchmark.main:main" -stateless-testcase = "benchmark.stateless:main" +stateless-testcase = "benchmark.stateless:cli" [tool.black] line-length = 88