205 lines
7.4 KiB
Python
Executable File
205 lines
7.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
import concurrent.futures
|
|
import socket
|
|
import json
|
|
import sys
|
|
import time
|
|
|
|
DEFAULT_HOST = "127.0.0.1"
|
|
DEFAULT_PORT = 57001
|
|
DEFAULT_END_HEIGHT = 914000
|
|
|
|
SCAN_KEY = "3c8b12d524c72d91dad33573c18f17dddb8f45e8d60c711c49a5a7992e321364"
|
|
SPEND_KEY = "0377dd40dfd0da11369dc6bddf6b3bf4f0474383a8beb6e523dddabc0f966734a6"
|
|
|
|
# (descriptive label, short label, blocks)
|
|
PERIODS = [
|
|
("2 hours", "2h", 12),
|
|
("1 day", "1d", 144),
|
|
("1 week", "1w", 1008),
|
|
("2 weeks", "2w", 2016),
|
|
("1 month", "1m", 4320),
|
|
("3 months", "3m", 12960),
|
|
("6 months", "6m", 25920),
|
|
("1 year", "1y", 52560),
|
|
("2 years", "2y", 105120),
|
|
]
|
|
|
|
# Transaction counts at endHeight=914000 (for display only)
|
|
TRANSACTION_COUNTS = {
|
|
"2h": 8207,
|
|
"1d": 127804,
|
|
"1w": 751769,
|
|
"2w": 1709358,
|
|
"1m": 4240572,
|
|
"3m": 13558435,
|
|
"6m": 26103759,
|
|
"1y": 59578156,
|
|
"2y": 132994804,
|
|
}
|
|
|
|
|
|
def scan(host, port, start_range):
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.connect((host, port))
|
|
sock.settimeout(600)
|
|
|
|
sock.sendall(json.dumps({
|
|
"jsonrpc": "2.0", "method": "server.version",
|
|
"params": ["benchmark", "1.4"], "id": 1
|
|
}).encode() + b"\n")
|
|
|
|
req = json.dumps({
|
|
"jsonrpc": "2.0", "method": "blockchain.silentpayments.subscribe",
|
|
"params": {
|
|
"scan_private_key": SCAN_KEY,
|
|
"spend_public_key": SPEND_KEY,
|
|
"start": start_range,
|
|
}, "id": 2
|
|
}).encode() + b"\n"
|
|
|
|
t0 = time.monotonic()
|
|
sock.sendall(req)
|
|
|
|
buf = b""
|
|
while True:
|
|
data = sock.recv(8192)
|
|
if not data:
|
|
break
|
|
buf += data
|
|
while b"\n" in buf:
|
|
line, buf = buf.split(b"\n", 1)
|
|
msg = json.loads(line)
|
|
if "params" in msg and msg["params"].get("progress", 0) >= 1.0:
|
|
elapsed = time.monotonic() - t0
|
|
sock.close()
|
|
return elapsed, len(msg["params"].get("history", []))
|
|
if msg.get("id") == 2 and "error" in msg:
|
|
sock.close()
|
|
raise RuntimeError(msg["error"].get("message", str(msg["error"])))
|
|
|
|
elapsed = time.monotonic() - t0
|
|
sock.close()
|
|
return elapsed, 0
|
|
|
|
|
|
def format_time(seconds):
|
|
ms = round(seconds * 1000)
|
|
if ms < 1000:
|
|
return f"{ms}ms"
|
|
s = ms // 1000
|
|
remainder_ms = ms % 1000
|
|
if s < 60:
|
|
return f"{s}s {remainder_ms}ms"
|
|
m = s // 60
|
|
remainder_s = s % 60
|
|
return f"{m}m {remainder_s}s"
|
|
|
|
|
|
def format_number(n):
|
|
return f"{n:,}"
|
|
|
|
|
|
def run_benchmarks(host, port, end_height, markdown, clients, max_periods=0):
|
|
period_list = PERIODS[:max_periods] if max_periods > 0 else PERIODS
|
|
periods = []
|
|
for desc, short, blocks in period_list:
|
|
start = end_height - blocks
|
|
txns = TRANSACTION_COUNTS.get(short) if end_height == DEFAULT_END_HEIGHT else None
|
|
periods.append((desc, short, blocks, start, f"{start}-{end_height}", txns))
|
|
|
|
# Warmup scan (first scan on a new connection has overhead from
|
|
# precompute table loading in the DuckDB read pool)
|
|
print("Warming up...", end="", flush=True)
|
|
scan(host, port, periods[0][4])
|
|
print(" done.\n")
|
|
|
|
results = []
|
|
for desc, short, blocks, start, height_range, txns in periods:
|
|
sys.stdout.write(f" Scanning {short}...")
|
|
sys.stdout.flush()
|
|
if clients == 1:
|
|
elapsed, count = scan(host, port, height_range)
|
|
else:
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=clients) as pool:
|
|
futures = [pool.submit(scan, host, port, height_range) for _ in range(clients)]
|
|
thread_results = [f.result() for f in futures]
|
|
elapsed = max(e for e, c in thread_results)
|
|
tps = round(clients * txns / elapsed) if txns and elapsed > 0 else None
|
|
results.append((desc, blocks, start, end_height, txns, elapsed, tps))
|
|
sys.stdout.write(f" {format_time(elapsed)}\n")
|
|
sys.stdout.flush()
|
|
|
|
print()
|
|
print_results(results, markdown, clients)
|
|
|
|
|
|
def print_results(results, markdown, clients=1):
|
|
show_txns = any(r[4] is not None for r in results)
|
|
tps_label = f"Transactions/sec ({clients} clients)" if clients > 1 else "Transactions/sec"
|
|
|
|
if markdown:
|
|
if show_txns:
|
|
print(f"| | Blocks | Start | End | Transactions | Time | {tps_label} |")
|
|
print("|---|--------|-------|-----|--------------|------|------------------|")
|
|
for desc, blocks, start, end, txns, elapsed, tps in results:
|
|
print(f"| {desc} | {blocks} | {start} | {end} | {format_number(txns)} | {format_time(elapsed)} | {format_number(tps)} |")
|
|
else:
|
|
print("| | Blocks | Start | End | Time |")
|
|
print("|---|--------|-------|-----|------|")
|
|
for desc, blocks, start, end, txns, elapsed, tps in results:
|
|
print(f"| {desc} | {blocks} | {start} | {end} | {format_time(elapsed)} |")
|
|
else:
|
|
if show_txns:
|
|
h = ("", "Blocks", "Start", "End", "Transactions", "Time", tps_label)
|
|
rows = [(desc, str(blocks), str(start), str(end), format_number(txns), format_time(elapsed), format_number(tps)) for desc, blocks, start, end, txns, elapsed, tps in results]
|
|
else:
|
|
h = ("", "Blocks", "Start", "End", "Time")
|
|
rows = [(desc, str(blocks), str(start), str(end), format_time(elapsed)) for desc, blocks, start, end, txns, elapsed, tps in results]
|
|
|
|
widths = [max(len(h[i]), max(len(r[i]) for r in rows)) for i in range(len(h))]
|
|
|
|
def fmt_row(vals):
|
|
parts = []
|
|
for i, v in enumerate(vals):
|
|
if i == 0:
|
|
parts.append(f"{v:<{widths[i]}}")
|
|
else:
|
|
parts.append(f"{v:>{widths[i]}}")
|
|
print(" " + " ".join(parts))
|
|
|
|
fmt_row(h)
|
|
fmt_row(tuple("─" * w for w in widths))
|
|
for r in rows:
|
|
fmt_row(r)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Benchmark Frigate Silent Payments scanning performance.",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="Examples:\n python3 benchmark.py\n python3 benchmark.py --end-height 920000 --markdown\n python3 benchmark.py --host 192.168.1.10 --port 57001\n python3 benchmark.py --clients 4",
|
|
)
|
|
parser.add_argument("--host", default=DEFAULT_HOST, help=f"server host (default: {DEFAULT_HOST})")
|
|
parser.add_argument("--port", type=int, default=DEFAULT_PORT, help=f"server port (default: {DEFAULT_PORT})")
|
|
parser.add_argument("--end-height", type=int, default=DEFAULT_END_HEIGHT, help=f"end block height (default: {DEFAULT_END_HEIGHT})")
|
|
parser.add_argument("--markdown", action="store_true", help="output as markdown table")
|
|
parser.add_argument("--clients", type=int, default=1, help="number of concurrent clients per scan period (default: 1)")
|
|
parser.add_argument("--periods", type=int, default=0, help="number of periods to run (default: all)")
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
run_benchmarks(args.host, args.port, args.end_height, args.markdown, args.clients, args.periods)
|
|
except ConnectionRefusedError:
|
|
print(f"Error: could not connect to {args.host}:{args.port}", file=sys.stderr)
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
print(f"Error: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|