zexus 1.6.8 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -5
- package/package.json +1 -1
- package/src/__init__.py +7 -0
- package/src/zexus/__init__.py +1 -1
- package/src/zexus/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/capability_system.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/debug_sanitizer.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/environment.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/error_reporter.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/input_validation.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/lexer.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/module_cache.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/module_manager.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/object.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/security.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/security_enforcement.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/syntax_validator.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/zexus_ast.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/zexus_token.cpython-312.pyc +0 -0
- package/src/zexus/access_control_system/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/access_control_system/__pycache__/access_control.cpython-312.pyc +0 -0
- package/src/zexus/advanced_types.py +17 -2
- package/src/zexus/blockchain/__init__.py +411 -0
- package/src/zexus/blockchain/accelerator.py +1160 -0
- package/src/zexus/blockchain/chain.py +660 -0
- package/src/zexus/blockchain/consensus.py +821 -0
- package/src/zexus/blockchain/contract_vm.py +1019 -0
- package/src/zexus/blockchain/crypto.py +79 -14
- package/src/zexus/blockchain/events.py +526 -0
- package/src/zexus/blockchain/loadtest.py +721 -0
- package/src/zexus/blockchain/monitoring.py +350 -0
- package/src/zexus/blockchain/mpt.py +716 -0
- package/src/zexus/blockchain/multichain.py +951 -0
- package/src/zexus/blockchain/multiprocess_executor.py +338 -0
- package/src/zexus/blockchain/network.py +886 -0
- package/src/zexus/blockchain/node.py +666 -0
- package/src/zexus/blockchain/rpc.py +1203 -0
- package/src/zexus/blockchain/rust_bridge.py +421 -0
- package/src/zexus/blockchain/storage.py +423 -0
- package/src/zexus/blockchain/tokens.py +750 -0
- package/src/zexus/blockchain/upgradeable.py +1004 -0
- package/src/zexus/blockchain/verification.py +1602 -0
- package/src/zexus/blockchain/wallet.py +621 -0
- package/src/zexus/capability_system.py +184 -9
- package/src/zexus/cli/__pycache__/main.cpython-312.pyc +0 -0
- package/src/zexus/cli/main.py +383 -34
- package/src/zexus/cli/zpm.py +1 -1
- package/src/zexus/compiler/__pycache__/bytecode.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/lexer.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/parser.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/semantic.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/zexus_ast.cpython-312.pyc +0 -0
- package/src/zexus/compiler/bytecode.py +124 -7
- package/src/zexus/compiler/compat_runtime.py +6 -2
- package/src/zexus/compiler/lexer.py +16 -5
- package/src/zexus/compiler/parser.py +108 -7
- package/src/zexus/compiler/semantic.py +18 -19
- package/src/zexus/compiler/zexus_ast.py +26 -1
- package/src/zexus/concurrency_system.py +79 -0
- package/src/zexus/config.py +54 -0
- package/src/zexus/crypto_bridge.py +244 -8
- package/src/zexus/dap/__init__.py +10 -0
- package/src/zexus/dap/__main__.py +4 -0
- package/src/zexus/dap/dap_server.py +391 -0
- package/src/zexus/dap/debug_engine.py +298 -0
- package/src/zexus/environment.py +112 -9
- package/src/zexus/evaluator/__pycache__/bytecode_compiler.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/core.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/expressions.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/functions.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/resource_limiter.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/statements.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/unified_execution.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/utils.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/bytecode_compiler.py +457 -37
- package/src/zexus/evaluator/core.py +644 -50
- package/src/zexus/evaluator/expressions.py +358 -62
- package/src/zexus/evaluator/functions.py +458 -20
- package/src/zexus/evaluator/resource_limiter.py +4 -4
- package/src/zexus/evaluator/statements.py +774 -122
- package/src/zexus/evaluator/unified_execution.py +573 -72
- package/src/zexus/evaluator/utils.py +14 -2
- package/src/zexus/evaluator_original.py +1 -1
- package/src/zexus/event_loop.py +186 -0
- package/src/zexus/lexer.py +742 -458
- package/src/zexus/lsp/__init__.py +1 -1
- package/src/zexus/lsp/definition_provider.py +163 -9
- package/src/zexus/lsp/server.py +22 -8
- package/src/zexus/lsp/symbol_provider.py +182 -9
- package/src/zexus/module_cache.py +239 -9
- package/src/zexus/module_manager.py +129 -1
- package/src/zexus/object.py +76 -6
- package/src/zexus/parser/__pycache__/parser.cpython-312.pyc +0 -0
- package/src/zexus/parser/__pycache__/strategy_context.cpython-312.pyc +0 -0
- package/src/zexus/parser/__pycache__/strategy_structural.cpython-312.pyc +0 -0
- package/src/zexus/parser/parser.py +1349 -408
- package/src/zexus/parser/strategy_context.py +755 -58
- package/src/zexus/parser/strategy_structural.py +121 -21
- package/src/zexus/persistence.py +15 -1
- package/src/zexus/renderer/__init__.py +61 -0
- package/src/zexus/renderer/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/backend.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/canvas.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/color_system.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/layout.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/main_renderer.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/painter.cpython-312.pyc +0 -0
- package/src/zexus/renderer/backend.py +261 -0
- package/src/zexus/renderer/canvas.py +78 -0
- package/src/zexus/renderer/color_system.py +201 -0
- package/src/zexus/renderer/graphics.py +31 -0
- package/src/zexus/renderer/layout.py +222 -0
- package/src/zexus/renderer/main_renderer.py +66 -0
- package/src/zexus/renderer/painter.py +30 -0
- package/src/zexus/renderer/tk_backend.py +208 -0
- package/src/zexus/renderer/web_backend.py +260 -0
- package/src/zexus/runtime/__init__.py +10 -2
- package/src/zexus/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/runtime/__pycache__/async_runtime.cpython-312.pyc +0 -0
- package/src/zexus/runtime/__pycache__/load_manager.cpython-312.pyc +0 -0
- package/src/zexus/runtime/file_flags.py +137 -0
- package/src/zexus/runtime/load_manager.py +368 -0
- package/src/zexus/safety/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/safety/__pycache__/memory_safety.cpython-312.pyc +0 -0
- package/src/zexus/security.py +424 -34
- package/src/zexus/stdlib/fs.py +23 -18
- package/src/zexus/stdlib/http.py +289 -186
- package/src/zexus/stdlib/sockets.py +207 -163
- package/src/zexus/stdlib/websockets.py +282 -0
- package/src/zexus/stdlib_integration.py +369 -2
- package/src/zexus/strategy_recovery.py +6 -3
- package/src/zexus/type_checker.py +423 -0
- package/src/zexus/virtual_filesystem.py +189 -2
- package/src/zexus/vm/__init__.py +113 -3
- package/src/zexus/vm/__pycache__/async_optimizer.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/bytecode.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/bytecode_converter.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/cache.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/compiler.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/gas_metering.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/jit.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/parallel_vm.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/vm.cpython-312.pyc +0 -0
- package/src/zexus/vm/async_optimizer.py +80 -6
- package/src/zexus/vm/binary_bytecode.py +659 -0
- package/src/zexus/vm/bytecode.py +59 -11
- package/src/zexus/vm/bytecode_converter.py +26 -12
- package/src/zexus/vm/cabi.c +1985 -0
- package/src/zexus/vm/cabi.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/cabi.h +127 -0
- package/src/zexus/vm/cache.py +561 -17
- package/src/zexus/vm/compiler.py +818 -51
- package/src/zexus/vm/fastops.c +15743 -0
- package/src/zexus/vm/fastops.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/fastops.pyx +288 -0
- package/src/zexus/vm/gas_metering.py +50 -9
- package/src/zexus/vm/jit.py +364 -20
- package/src/zexus/vm/native_jit_backend.py +1816 -0
- package/src/zexus/vm/native_runtime.cpp +1388 -0
- package/src/zexus/vm/native_runtime.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/optimizer.py +161 -11
- package/src/zexus/vm/parallel_vm.py +140 -45
- package/src/zexus/vm/peephole_optimizer.py +82 -4
- package/src/zexus/vm/profiler.py +38 -18
- package/src/zexus/vm/register_allocator.py +16 -5
- package/src/zexus/vm/register_vm.py +8 -5
- package/src/zexus/vm/vm.py +3581 -531
- package/src/zexus/vm/wasm_compiler.py +658 -0
- package/src/zexus/zexus_ast.py +137 -11
- package/src/zexus/zexus_token.py +16 -5
- package/src/zexus/zpm/installer.py +55 -15
- package/src/zexus/zpm/package_manager.py +1 -1
- package/src/zexus/zpm/registry.py +257 -28
- package/src/zexus.egg-info/PKG-INFO +16 -6
- package/src/zexus.egg-info/SOURCES.txt +129 -17
- package/src/zexus.egg-info/entry_points.txt +1 -0
- package/src/zexus.egg-info/requires.txt +4 -0
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Zexus Blockchain — Load Testing Framework
|
|
3
|
+
==========================================
|
|
4
|
+
|
|
5
|
+
A self-contained load testing tool that simulates realistic blockchain
|
|
6
|
+
workloads and validates throughput targets (e.g. 1 800 TPS).
|
|
7
|
+
|
|
8
|
+
The framework measures:
|
|
9
|
+
* **Throughput** — sustained transactions per second (TPS)
|
|
10
|
+
* **Latency** — per-transaction percentiles (p50, p95, p99, max)
|
|
11
|
+
* **Resource usage** — CPU, memory, GC pauses
|
|
12
|
+
* **Chain integrity** — all blocks are valid after the run
|
|
13
|
+
|
|
14
|
+
Usage
|
|
15
|
+
-----
|
|
16
|
+
::
|
|
17
|
+
|
|
18
|
+
from zexus.blockchain.loadtest import LoadTestRunner, LoadProfile
|
|
19
|
+
|
|
20
|
+
profile = LoadProfile(
|
|
21
|
+
target_tps=1800,
|
|
22
|
+
duration_seconds=30,
|
|
23
|
+
contract_count=8,
|
|
24
|
+
actions_per_contract=5,
|
|
25
|
+
)
|
|
26
|
+
runner = LoadTestRunner(profile)
|
|
27
|
+
report = runner.run()
|
|
28
|
+
report.print_summary()
|
|
29
|
+
|
|
30
|
+
CLI shortcut::
|
|
31
|
+
|
|
32
|
+
python -m zexus.blockchain.loadtest --tps 1800 --duration 30
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import gc
|
|
38
|
+
import hashlib
|
|
39
|
+
import json
|
|
40
|
+
import logging
|
|
41
|
+
import math
|
|
42
|
+
import os
|
|
43
|
+
import random
|
|
44
|
+
import statistics
|
|
45
|
+
import sys
|
|
46
|
+
import threading
|
|
47
|
+
import time
|
|
48
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
49
|
+
from dataclasses import dataclass, field
|
|
50
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
51
|
+
|
|
52
|
+
logger = logging.getLogger("zexus.blockchain.loadtest")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# =====================================================================
|
|
56
|
+
# Load profile — describes the workload
|
|
57
|
+
# =====================================================================
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class LoadProfile:
|
|
61
|
+
"""Configuration for a load test run."""
|
|
62
|
+
|
|
63
|
+
# ── Throughput target ────────────────────────────────────────
|
|
64
|
+
target_tps: int = 1_800
|
|
65
|
+
"""Target transactions per second to *attempt*."""
|
|
66
|
+
|
|
67
|
+
duration_seconds: int = 30
|
|
68
|
+
"""How long to sustain the load (seconds)."""
|
|
69
|
+
|
|
70
|
+
# ── Workload shape ───────────────────────────────────────────
|
|
71
|
+
contract_count: int = 8
|
|
72
|
+
"""Number of distinct contracts (parallelism factor)."""
|
|
73
|
+
|
|
74
|
+
actions_per_contract: int = 5
|
|
75
|
+
"""Number of unique action names per contract."""
|
|
76
|
+
|
|
77
|
+
batch_size: int = 200
|
|
78
|
+
"""Transactions submitted per batch call."""
|
|
79
|
+
|
|
80
|
+
# ── Transaction complexity ───────────────────────────────────
|
|
81
|
+
payload_bytes: int = 256
|
|
82
|
+
"""Average extra data per transaction (simulates calldata)."""
|
|
83
|
+
|
|
84
|
+
gas_limit: int = 100_000
|
|
85
|
+
"""Max gas per transaction."""
|
|
86
|
+
|
|
87
|
+
# ── Concurrency ──────────────────────────────────────────────
|
|
88
|
+
sender_count: int = 50
|
|
89
|
+
"""Number of distinct sender addresses (simulates wallets)."""
|
|
90
|
+
|
|
91
|
+
workers: int = 0
|
|
92
|
+
"""Thread-pool size for batch submission (0 = auto)."""
|
|
93
|
+
|
|
94
|
+
# ── Rust / accelerator settings ──────────────────────────────
|
|
95
|
+
use_rust: bool = True
|
|
96
|
+
"""Enable the Rust execution core if available."""
|
|
97
|
+
|
|
98
|
+
# ── Warm-up ──────────────────────────────────────────────────
|
|
99
|
+
warmup_seconds: int = 3
|
|
100
|
+
"""Warm-up period before measurements begin."""
|
|
101
|
+
|
|
102
|
+
# ── Seed ─────────────────────────────────────────────────────
|
|
103
|
+
seed: int = 42
|
|
104
|
+
"""Random seed for reproducible workloads."""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# =====================================================================
|
|
108
|
+
# Transaction generator
|
|
109
|
+
# =====================================================================
|
|
110
|
+
|
|
111
|
+
class TransactionGenerator:
|
|
112
|
+
"""Generates realistic-looking synthetic transactions."""
|
|
113
|
+
|
|
114
|
+
def __init__(self, profile: LoadProfile):
|
|
115
|
+
self._profile = profile
|
|
116
|
+
self._rng = random.Random(profile.seed)
|
|
117
|
+
|
|
118
|
+
# Pre-generate contract addresses
|
|
119
|
+
self._contracts = [
|
|
120
|
+
f"0x{hashlib.sha256(f'contract-{i}'.encode()).hexdigest()[:40]}"
|
|
121
|
+
for i in range(profile.contract_count)
|
|
122
|
+
]
|
|
123
|
+
# Pre-generate sender addresses
|
|
124
|
+
self._senders = [
|
|
125
|
+
f"0x{hashlib.sha256(f'sender-{i}'.encode()).hexdigest()[:40]}"
|
|
126
|
+
for i in range(profile.sender_count)
|
|
127
|
+
]
|
|
128
|
+
# Pre-generate action names per contract
|
|
129
|
+
self._actions: Dict[str, List[str]] = {}
|
|
130
|
+
for c in self._contracts:
|
|
131
|
+
self._actions[c] = [
|
|
132
|
+
f"action_{j}" for j in range(profile.actions_per_contract)
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
def generate_batch(self, size: int) -> List[Dict[str, Any]]:
|
|
136
|
+
"""Return *size* random transactions."""
|
|
137
|
+
txs: List[Dict[str, Any]] = []
|
|
138
|
+
for _ in range(size):
|
|
139
|
+
contract = self._rng.choice(self._contracts)
|
|
140
|
+
action = self._rng.choice(self._actions[contract])
|
|
141
|
+
sender = self._rng.choice(self._senders)
|
|
142
|
+
payload = os.urandom(self._profile.payload_bytes).hex()
|
|
143
|
+
txs.append({
|
|
144
|
+
"contract": contract,
|
|
145
|
+
"action": action,
|
|
146
|
+
"args": {
|
|
147
|
+
"value": self._rng.randint(0, 1_000_000),
|
|
148
|
+
"data": payload,
|
|
149
|
+
"nonce": self._rng.randint(0, 2**32),
|
|
150
|
+
},
|
|
151
|
+
"caller": sender,
|
|
152
|
+
"gas_limit": self._profile.gas_limit,
|
|
153
|
+
})
|
|
154
|
+
return txs
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def contracts(self) -> List[str]:
|
|
158
|
+
return list(self._contracts)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# =====================================================================
|
|
162
|
+
# Lightweight mock VM for load testing
|
|
163
|
+
# =====================================================================
|
|
164
|
+
|
|
165
|
+
class _MockContractVM:
|
|
166
|
+
"""Minimal VM that executes transactions at maximum speed.
|
|
167
|
+
|
|
168
|
+
Instead of running real contract bytecode, this mock performs
|
|
169
|
+
the same overhead that a real VM would (hashing, state lookup,
|
|
170
|
+
gas deduction) so that the measured TPS reflects genuine
|
|
171
|
+
system throughput for the *infrastructure* layer.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
def execute_action(
|
|
175
|
+
self,
|
|
176
|
+
contract: str,
|
|
177
|
+
action: str,
|
|
178
|
+
args: Dict[str, Any],
|
|
179
|
+
caller: str,
|
|
180
|
+
gas_limit: int = 100_000,
|
|
181
|
+
) -> Dict[str, Any]:
|
|
182
|
+
start = time.perf_counter()
|
|
183
|
+
# Simulate minimal hashing work (like a real state-transition
|
|
184
|
+
# that computes the new state hash)
|
|
185
|
+
data = f"{contract}:{action}:{caller}:{json.dumps(args, sort_keys=True)}"
|
|
186
|
+
result_hash = hashlib.sha256(data.encode()).hexdigest()
|
|
187
|
+
gas_used = max(21_000, len(data) * 8) # gas proportional to size
|
|
188
|
+
elapsed = time.perf_counter() - start
|
|
189
|
+
return {
|
|
190
|
+
"success": True,
|
|
191
|
+
"result": result_hash[:16],
|
|
192
|
+
"gas_used": min(gas_used, gas_limit),
|
|
193
|
+
"elapsed": elapsed,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# =====================================================================
|
|
198
|
+
# Resource sampler
|
|
199
|
+
# =====================================================================
|
|
200
|
+
|
|
201
|
+
class _ResourceSampler:
|
|
202
|
+
"""Background thread that periodically samples CPU & memory."""
|
|
203
|
+
|
|
204
|
+
def __init__(self, interval: float = 0.5):
|
|
205
|
+
self._interval = interval
|
|
206
|
+
self._samples: List[Dict[str, Any]] = []
|
|
207
|
+
self._stop = threading.Event()
|
|
208
|
+
self._thread: Optional[threading.Thread] = None
|
|
209
|
+
|
|
210
|
+
def start(self) -> None:
|
|
211
|
+
self._stop.clear()
|
|
212
|
+
self._thread = threading.Thread(target=self._loop, daemon=True)
|
|
213
|
+
self._thread.start()
|
|
214
|
+
|
|
215
|
+
def stop(self) -> List[Dict[str, Any]]:
|
|
216
|
+
self._stop.set()
|
|
217
|
+
if self._thread:
|
|
218
|
+
self._thread.join(timeout=2.0)
|
|
219
|
+
return list(self._samples)
|
|
220
|
+
|
|
221
|
+
def _loop(self) -> None:
|
|
222
|
+
try:
|
|
223
|
+
import resource as _resource
|
|
224
|
+
except ImportError:
|
|
225
|
+
_resource = None # type: ignore[assignment]
|
|
226
|
+
|
|
227
|
+
while not self._stop.is_set():
|
|
228
|
+
sample: Dict[str, Any] = {"ts": time.time()}
|
|
229
|
+
# Memory (RSS) from /proc/self/status (Linux) or resource module
|
|
230
|
+
try:
|
|
231
|
+
with open("/proc/self/status") as f:
|
|
232
|
+
for line in f:
|
|
233
|
+
if line.startswith("VmRSS:"):
|
|
234
|
+
sample["rss_kb"] = int(line.split()[1])
|
|
235
|
+
break
|
|
236
|
+
except Exception:
|
|
237
|
+
if _resource:
|
|
238
|
+
usage = _resource.getrusage(_resource.RUSAGE_SELF)
|
|
239
|
+
sample["rss_kb"] = usage.ru_maxrss
|
|
240
|
+
|
|
241
|
+
# GC stats
|
|
242
|
+
gc_stats = gc.get_stats()
|
|
243
|
+
sample["gc_collections"] = sum(s.get("collections", 0) for s in gc_stats)
|
|
244
|
+
|
|
245
|
+
self._samples.append(sample)
|
|
246
|
+
self._stop.wait(self._interval)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# =====================================================================
|
|
250
|
+
# Test report
|
|
251
|
+
# =====================================================================
|
|
252
|
+
|
|
253
|
+
@dataclass
|
|
254
|
+
class LoadTestReport:
|
|
255
|
+
"""Results of a load test run."""
|
|
256
|
+
|
|
257
|
+
profile: LoadProfile
|
|
258
|
+
total_transactions: int = 0
|
|
259
|
+
succeeded: int = 0
|
|
260
|
+
failed: int = 0
|
|
261
|
+
elapsed_seconds: float = 0.0
|
|
262
|
+
warmup_transactions: int = 0
|
|
263
|
+
|
|
264
|
+
# Throughput
|
|
265
|
+
sustained_tps: float = 0.0
|
|
266
|
+
peak_tps: float = 0.0
|
|
267
|
+
|
|
268
|
+
# Latency (seconds)
|
|
269
|
+
latency_p50: float = 0.0
|
|
270
|
+
latency_p95: float = 0.0
|
|
271
|
+
latency_p99: float = 0.0
|
|
272
|
+
latency_max: float = 0.0
|
|
273
|
+
latency_avg: float = 0.0
|
|
274
|
+
|
|
275
|
+
# Batch latencies
|
|
276
|
+
batch_latency_p50: float = 0.0
|
|
277
|
+
batch_latency_p95: float = 0.0
|
|
278
|
+
batch_latency_avg: float = 0.0
|
|
279
|
+
|
|
280
|
+
# Resource usage
|
|
281
|
+
peak_rss_mb: float = 0.0
|
|
282
|
+
avg_rss_mb: float = 0.0
|
|
283
|
+
gc_collections: int = 0
|
|
284
|
+
|
|
285
|
+
# Status
|
|
286
|
+
target_met: bool = False
|
|
287
|
+
rust_core_used: bool = False
|
|
288
|
+
error_rate: float = 0.0
|
|
289
|
+
|
|
290
|
+
# Raw data for post-analysis
|
|
291
|
+
per_second_tps: List[float] = field(default_factory=list)
|
|
292
|
+
resource_samples: List[Dict[str, Any]] = field(default_factory=list)
|
|
293
|
+
|
|
294
|
+
def print_summary(self) -> None:
|
|
295
|
+
"""Print a human-readable summary to stdout."""
|
|
296
|
+
line = "=" * 62
|
|
297
|
+
print(f"\n{line}")
|
|
298
|
+
print(f" ZEXUS LOAD TEST REPORT")
|
|
299
|
+
print(f"{line}")
|
|
300
|
+
print(f" Target TPS : {self.profile.target_tps:,}")
|
|
301
|
+
print(f" Duration : {self.elapsed_seconds:.1f}s "
|
|
302
|
+
f"(+ {self.profile.warmup_seconds}s warm-up)")
|
|
303
|
+
print(f" Contracts : {self.profile.contract_count}")
|
|
304
|
+
print(f" Batch size : {self.profile.batch_size}")
|
|
305
|
+
print(f" Rust core : {'YES' if self.rust_core_used else 'no (fallback)'}")
|
|
306
|
+
print(f"{line}")
|
|
307
|
+
print()
|
|
308
|
+
|
|
309
|
+
# Throughput
|
|
310
|
+
result = "PASS" if self.target_met else "FAIL"
|
|
311
|
+
print(f" Throughput")
|
|
312
|
+
print(f" Sustained TPS : {self.sustained_tps:,.0f} [{result}]")
|
|
313
|
+
print(f" Peak TPS : {self.peak_tps:,.0f}")
|
|
314
|
+
print(f" Total txns : {self.total_transactions:,}")
|
|
315
|
+
print(f" Succeeded : {self.succeeded:,}")
|
|
316
|
+
print(f" Failed : {self.failed:,}")
|
|
317
|
+
print(f" Error rate : {self.error_rate:.2%}")
|
|
318
|
+
print()
|
|
319
|
+
|
|
320
|
+
# Latency
|
|
321
|
+
print(f" Latency (per transaction)")
|
|
322
|
+
print(f" p50 : {self.latency_p50 * 1000:.2f} ms")
|
|
323
|
+
print(f" p95 : {self.latency_p95 * 1000:.2f} ms")
|
|
324
|
+
print(f" p99 : {self.latency_p99 * 1000:.2f} ms")
|
|
325
|
+
print(f" max : {self.latency_max * 1000:.2f} ms")
|
|
326
|
+
print(f" avg : {self.latency_avg * 1000:.2f} ms")
|
|
327
|
+
print()
|
|
328
|
+
|
|
329
|
+
# Batch latency
|
|
330
|
+
print(f" Latency (per batch of {self.profile.batch_size})")
|
|
331
|
+
print(f" p50 : {self.batch_latency_p50 * 1000:.1f} ms")
|
|
332
|
+
print(f" p95 : {self.batch_latency_p95 * 1000:.1f} ms")
|
|
333
|
+
print(f" avg : {self.batch_latency_avg * 1000:.1f} ms")
|
|
334
|
+
print()
|
|
335
|
+
|
|
336
|
+
# Resources
|
|
337
|
+
print(f" Resources")
|
|
338
|
+
print(f" Peak RSS : {self.peak_rss_mb:.1f} MB")
|
|
339
|
+
print(f" Avg RSS : {self.avg_rss_mb:.1f} MB")
|
|
340
|
+
print(f" GC collections : {self.gc_collections}")
|
|
341
|
+
print()
|
|
342
|
+
|
|
343
|
+
# Per-second TPS histogram (text sparkline)
|
|
344
|
+
if self.per_second_tps:
|
|
345
|
+
_max = max(self.per_second_tps) or 1
|
|
346
|
+
bars = ""
|
|
347
|
+
for tps in self.per_second_tps:
|
|
348
|
+
level = int(tps / _max * 7)
|
|
349
|
+
bars += " ▁▂▃▄▅▆▇"[min(level, 7)]
|
|
350
|
+
print(f" TPS sparkline : [{bars}]")
|
|
351
|
+
print()
|
|
352
|
+
|
|
353
|
+
print(f"{line}")
|
|
354
|
+
if self.target_met:
|
|
355
|
+
print(f" RESULT: TARGET MET — {self.sustained_tps:,.0f} >= "
|
|
356
|
+
f"{self.profile.target_tps:,} TPS")
|
|
357
|
+
else:
|
|
358
|
+
print(f" RESULT: TARGET NOT MET — {self.sustained_tps:,.0f} < "
|
|
359
|
+
f"{self.profile.target_tps:,} TPS")
|
|
360
|
+
print(f"{line}\n")
|
|
361
|
+
|
|
362
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
363
|
+
"""Serialize to a JSON-friendly dict."""
|
|
364
|
+
return {
|
|
365
|
+
"target_tps": self.profile.target_tps,
|
|
366
|
+
"sustained_tps": round(self.sustained_tps, 1),
|
|
367
|
+
"peak_tps": round(self.peak_tps, 1),
|
|
368
|
+
"total_transactions": self.total_transactions,
|
|
369
|
+
"succeeded": self.succeeded,
|
|
370
|
+
"failed": self.failed,
|
|
371
|
+
"elapsed_seconds": round(self.elapsed_seconds, 2),
|
|
372
|
+
"target_met": self.target_met,
|
|
373
|
+
"rust_core_used": self.rust_core_used,
|
|
374
|
+
"error_rate": round(self.error_rate, 4),
|
|
375
|
+
"latency_ms": {
|
|
376
|
+
"p50": round(self.latency_p50 * 1000, 3),
|
|
377
|
+
"p95": round(self.latency_p95 * 1000, 3),
|
|
378
|
+
"p99": round(self.latency_p99 * 1000, 3),
|
|
379
|
+
"max": round(self.latency_max * 1000, 3),
|
|
380
|
+
"avg": round(self.latency_avg * 1000, 3),
|
|
381
|
+
},
|
|
382
|
+
"batch_latency_ms": {
|
|
383
|
+
"p50": round(self.batch_latency_p50 * 1000, 2),
|
|
384
|
+
"p95": round(self.batch_latency_p95 * 1000, 2),
|
|
385
|
+
"avg": round(self.batch_latency_avg * 1000, 2),
|
|
386
|
+
},
|
|
387
|
+
"resources": {
|
|
388
|
+
"peak_rss_mb": round(self.peak_rss_mb, 1),
|
|
389
|
+
"avg_rss_mb": round(self.avg_rss_mb, 1),
|
|
390
|
+
"gc_collections": self.gc_collections,
|
|
391
|
+
},
|
|
392
|
+
"per_second_tps": [round(t, 1) for t in self.per_second_tps],
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# =====================================================================
|
|
397
|
+
# Load test runner
|
|
398
|
+
# =====================================================================
|
|
399
|
+
|
|
400
|
+
class LoadTestRunner:
|
|
401
|
+
"""Executes a load test against the Zexus blockchain stack.
|
|
402
|
+
|
|
403
|
+
The runner simulates a steady stream of transactions at the
|
|
404
|
+
configured ``target_tps``, dispatching them in batches via the
|
|
405
|
+
``ExecutionAccelerator`` (which in turn uses the Rust core if it's
|
|
406
|
+
compiled). It collects per-transaction latencies, per-second TPS
|
|
407
|
+
counters, and resource samples, then produces a
|
|
408
|
+
:class:`LoadTestReport`.
|
|
409
|
+
"""
|
|
410
|
+
|
|
411
|
+
def __init__(
|
|
412
|
+
self,
|
|
413
|
+
profile: Optional[LoadProfile] = None,
|
|
414
|
+
contract_vm: Any = None,
|
|
415
|
+
):
|
|
416
|
+
self.profile = profile or LoadProfile()
|
|
417
|
+
self._vm = contract_vm or _MockContractVM()
|
|
418
|
+
self._tx_gen = TransactionGenerator(self.profile)
|
|
419
|
+
|
|
420
|
+
def run(self) -> LoadTestReport:
|
|
421
|
+
"""Execute the full load test and return a report."""
|
|
422
|
+
p = self.profile
|
|
423
|
+
report = LoadTestReport(profile=p)
|
|
424
|
+
workers = p.workers or min(32, max(4, p.contract_count))
|
|
425
|
+
|
|
426
|
+
# ── Check Rust core ───────────────────────────────────────
|
|
427
|
+
try:
|
|
428
|
+
from .rust_bridge import rust_core_available
|
|
429
|
+
report.rust_core_used = p.use_rust and rust_core_available()
|
|
430
|
+
except ImportError:
|
|
431
|
+
report.rust_core_used = False
|
|
432
|
+
|
|
433
|
+
# ── Build the accelerator ─────────────────────────────────
|
|
434
|
+
try:
|
|
435
|
+
from .accelerator import ExecutionAccelerator
|
|
436
|
+
accel = ExecutionAccelerator(
|
|
437
|
+
contract_vm=self._vm,
|
|
438
|
+
rust_core=p.use_rust,
|
|
439
|
+
batch_workers=workers,
|
|
440
|
+
)
|
|
441
|
+
except ImportError:
|
|
442
|
+
accel = None # type: ignore[assignment]
|
|
443
|
+
|
|
444
|
+
logger.info(
|
|
445
|
+
"Starting load test: target=%d TPS, duration=%ds, "
|
|
446
|
+
"contracts=%d, batch=%d, rust=%s",
|
|
447
|
+
p.target_tps, p.duration_seconds, p.contract_count,
|
|
448
|
+
p.batch_size, report.rust_core_used,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# ── Resource sampler ──────────────────────────────────────
|
|
452
|
+
sampler = _ResourceSampler(interval=0.5)
|
|
453
|
+
sampler.start()
|
|
454
|
+
|
|
455
|
+
# ── Timing ────────────────────────────────────────────────
|
|
456
|
+
tx_latencies: List[float] = []
|
|
457
|
+
batch_latencies: List[float] = []
|
|
458
|
+
per_second_counts: Dict[int, int] = {}
|
|
459
|
+
total_sent = 0
|
|
460
|
+
total_ok = 0
|
|
461
|
+
total_fail = 0
|
|
462
|
+
|
|
463
|
+
# Token-bucket rate limiter: track cumulative "debt" to
|
|
464
|
+
# compensate for sleep() granularity and processing time.
|
|
465
|
+
batches_per_sec = max(1, p.target_tps / p.batch_size)
|
|
466
|
+
inter_batch_delay = 1.0 / batches_per_sec
|
|
467
|
+
|
|
468
|
+
run_start = time.time()
|
|
469
|
+
warmup_end = run_start + p.warmup_seconds
|
|
470
|
+
test_end = warmup_end + p.duration_seconds
|
|
471
|
+
is_warmup = True
|
|
472
|
+
next_batch_time = time.perf_counter() # token-bucket deadline
|
|
473
|
+
|
|
474
|
+
try:
|
|
475
|
+
while True:
|
|
476
|
+
now = time.time()
|
|
477
|
+
if now >= test_end:
|
|
478
|
+
break
|
|
479
|
+
if is_warmup and now >= warmup_end:
|
|
480
|
+
is_warmup = False
|
|
481
|
+
# Reset counters after warm-up
|
|
482
|
+
tx_latencies.clear()
|
|
483
|
+
batch_latencies.clear()
|
|
484
|
+
per_second_counts.clear()
|
|
485
|
+
total_sent = 0
|
|
486
|
+
total_ok = 0
|
|
487
|
+
total_fail = 0
|
|
488
|
+
logger.info("Warm-up complete, measuring...")
|
|
489
|
+
|
|
490
|
+
# Generate & submit a batch
|
|
491
|
+
batch = self._tx_gen.generate_batch(p.batch_size)
|
|
492
|
+
batch_start = time.perf_counter()
|
|
493
|
+
|
|
494
|
+
if accel:
|
|
495
|
+
result = accel.execute_batch(batch)
|
|
496
|
+
batch_elapsed_inner = time.perf_counter() - batch_start
|
|
497
|
+
# Extract per-tx latencies from receipts, or derive
|
|
498
|
+
# from batch time when Rust doesn't report them
|
|
499
|
+
found_latency = False
|
|
500
|
+
for r in (result.receipts if hasattr(result, 'receipts') else []):
|
|
501
|
+
if isinstance(r, dict) and r.get("elapsed", 0.0) > 0:
|
|
502
|
+
tx_latencies.append(r["elapsed"])
|
|
503
|
+
found_latency = True
|
|
504
|
+
if not found_latency and hasattr(result, 'receipts') and result.receipts:
|
|
505
|
+
# Derive per-tx latency from batch wall time
|
|
506
|
+
per_tx = batch_elapsed_inner / max(len(result.receipts), 1)
|
|
507
|
+
tx_latencies.extend([per_tx] * len(result.receipts))
|
|
508
|
+
ok = result.succeeded if hasattr(result, 'succeeded') else 0
|
|
509
|
+
fail = result.failed if hasattr(result, 'failed') else 0
|
|
510
|
+
else:
|
|
511
|
+
# Direct VM fallback
|
|
512
|
+
ok = 0
|
|
513
|
+
fail = 0
|
|
514
|
+
for tx in batch:
|
|
515
|
+
t0 = time.perf_counter()
|
|
516
|
+
try:
|
|
517
|
+
res = self._vm.execute_action(
|
|
518
|
+
contract=tx["contract"],
|
|
519
|
+
action=tx["action"],
|
|
520
|
+
args=tx["args"],
|
|
521
|
+
caller=tx["caller"],
|
|
522
|
+
gas_limit=tx.get("gas_limit", 100_000),
|
|
523
|
+
)
|
|
524
|
+
lat = time.perf_counter() - t0
|
|
525
|
+
tx_latencies.append(lat)
|
|
526
|
+
if res.get("success"):
|
|
527
|
+
ok += 1
|
|
528
|
+
else:
|
|
529
|
+
fail += 1
|
|
530
|
+
except Exception:
|
|
531
|
+
fail += 1
|
|
532
|
+
|
|
533
|
+
batch_elapsed = time.perf_counter() - batch_start
|
|
534
|
+
batch_latencies.append(batch_elapsed)
|
|
535
|
+
|
|
536
|
+
if not is_warmup:
|
|
537
|
+
total_sent += len(batch)
|
|
538
|
+
total_ok += ok
|
|
539
|
+
total_fail += fail
|
|
540
|
+
# Track per-second TPS
|
|
541
|
+
sec = int(time.time() - warmup_end)
|
|
542
|
+
per_second_counts[sec] = per_second_counts.get(sec, 0) + len(batch)
|
|
543
|
+
|
|
544
|
+
# Token-bucket throttle: sleep only until next_batch_time
|
|
545
|
+
next_batch_time += inter_batch_delay
|
|
546
|
+
sleep_time = next_batch_time - time.perf_counter()
|
|
547
|
+
if sleep_time > 0.0001:
|
|
548
|
+
time.sleep(sleep_time)
|
|
549
|
+
|
|
550
|
+
except KeyboardInterrupt:
|
|
551
|
+
logger.warning("Load test interrupted")
|
|
552
|
+
|
|
553
|
+
# ── Stop resource sampler ─────────────────────────────────
|
|
554
|
+
resource_data = sampler.stop()
|
|
555
|
+
|
|
556
|
+
# ── Compute report ────────────────────────────────────────
|
|
557
|
+
run_end = time.time()
|
|
558
|
+
elapsed = max(0.001, run_end - warmup_end - max(0, run_end - test_end))
|
|
559
|
+
report.elapsed_seconds = elapsed
|
|
560
|
+
report.total_transactions = total_sent
|
|
561
|
+
report.succeeded = total_ok
|
|
562
|
+
report.failed = total_fail
|
|
563
|
+
report.error_rate = total_fail / max(total_sent, 1)
|
|
564
|
+
|
|
565
|
+
# Throughput
|
|
566
|
+
report.sustained_tps = total_sent / elapsed
|
|
567
|
+
if per_second_counts:
|
|
568
|
+
report.per_second_tps = [
|
|
569
|
+
per_second_counts.get(s, 0)
|
|
570
|
+
for s in range(max(per_second_counts.keys()) + 1)
|
|
571
|
+
]
|
|
572
|
+
report.peak_tps = max(report.per_second_tps) if report.per_second_tps else 0
|
|
573
|
+
|
|
574
|
+
# Latency percentiles
|
|
575
|
+
if tx_latencies:
|
|
576
|
+
tx_latencies.sort()
|
|
577
|
+
report.latency_avg = statistics.mean(tx_latencies)
|
|
578
|
+
report.latency_p50 = _percentile(tx_latencies, 50)
|
|
579
|
+
report.latency_p95 = _percentile(tx_latencies, 95)
|
|
580
|
+
report.latency_p99 = _percentile(tx_latencies, 99)
|
|
581
|
+
report.latency_max = tx_latencies[-1]
|
|
582
|
+
|
|
583
|
+
if batch_latencies:
|
|
584
|
+
batch_latencies.sort()
|
|
585
|
+
report.batch_latency_avg = statistics.mean(batch_latencies)
|
|
586
|
+
report.batch_latency_p50 = _percentile(batch_latencies, 50)
|
|
587
|
+
report.batch_latency_p95 = _percentile(batch_latencies, 95)
|
|
588
|
+
|
|
589
|
+
# Resources
|
|
590
|
+
rss_values = [s.get("rss_kb", 0) for s in resource_data if s.get("rss_kb")]
|
|
591
|
+
if rss_values:
|
|
592
|
+
report.peak_rss_mb = max(rss_values) / 1024.0
|
|
593
|
+
report.avg_rss_mb = statistics.mean(rss_values) / 1024.0
|
|
594
|
+
gc_vals = [s.get("gc_collections", 0) for s in resource_data]
|
|
595
|
+
if gc_vals:
|
|
596
|
+
report.gc_collections = max(gc_vals) - min(gc_vals)
|
|
597
|
+
|
|
598
|
+
report.resource_samples = resource_data
|
|
599
|
+
report.target_met = report.sustained_tps >= p.target_tps
|
|
600
|
+
|
|
601
|
+
logger.info(
|
|
602
|
+
"Load test complete: %d txns in %.1fs = %.0f TPS (%s)",
|
|
603
|
+
total_sent, elapsed, report.sustained_tps,
|
|
604
|
+
"PASS" if report.target_met else "FAIL",
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
return report
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
# =====================================================================
|
|
611
|
+
# Helpers
|
|
612
|
+
# =====================================================================
|
|
613
|
+
|
|
614
|
+
def _percentile(sorted_data: List[float], pct: float) -> float:
|
|
615
|
+
"""Compute the *pct*-th percentile from pre-sorted data."""
|
|
616
|
+
if not sorted_data:
|
|
617
|
+
return 0.0
|
|
618
|
+
k = (len(sorted_data) - 1) * pct / 100.0
|
|
619
|
+
f = math.floor(k)
|
|
620
|
+
c = math.ceil(k)
|
|
621
|
+
if f == c:
|
|
622
|
+
return sorted_data[int(k)]
|
|
623
|
+
return sorted_data[f] * (c - k) + sorted_data[c] * (k - f)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
# =====================================================================
|
|
627
|
+
# Convenience: quick benchmark
|
|
628
|
+
# =====================================================================
|
|
629
|
+
|
|
630
|
+
def quick_benchmark(
|
|
631
|
+
target_tps: int = 1_800,
|
|
632
|
+
duration: int = 10,
|
|
633
|
+
contracts: int = 8,
|
|
634
|
+
use_rust: bool = True,
|
|
635
|
+
) -> LoadTestReport:
|
|
636
|
+
"""Run a quick benchmark with sensible defaults.
|
|
637
|
+
|
|
638
|
+
::
|
|
639
|
+
|
|
640
|
+
from zexus.blockchain.loadtest import quick_benchmark
|
|
641
|
+
report = quick_benchmark()
|
|
642
|
+
report.print_summary()
|
|
643
|
+
"""
|
|
644
|
+
profile = LoadProfile(
|
|
645
|
+
target_tps=target_tps,
|
|
646
|
+
duration_seconds=duration,
|
|
647
|
+
contract_count=contracts,
|
|
648
|
+
use_rust=use_rust,
|
|
649
|
+
warmup_seconds=2,
|
|
650
|
+
)
|
|
651
|
+
runner = LoadTestRunner(profile)
|
|
652
|
+
return runner.run()
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
# =====================================================================
|
|
656
|
+
# CLI entry-point
|
|
657
|
+
# =====================================================================
|
|
658
|
+
|
|
659
|
+
def _cli_main() -> None:
|
|
660
|
+
"""Simple CLI for running load tests."""
|
|
661
|
+
import argparse
|
|
662
|
+
|
|
663
|
+
parser = argparse.ArgumentParser(
|
|
664
|
+
description="Zexus Blockchain Load Tester",
|
|
665
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
666
|
+
epilog=(
|
|
667
|
+
"Examples:\n"
|
|
668
|
+
" python -m zexus.blockchain.loadtest --tps 1800 --duration 30\n"
|
|
669
|
+
" python -m zexus.blockchain.loadtest --tps 5000 --contracts 16 --batch 500\n"
|
|
670
|
+
" python -m zexus.blockchain.loadtest --no-rust # Pure Python baseline\n"
|
|
671
|
+
),
|
|
672
|
+
)
|
|
673
|
+
parser.add_argument("--tps", type=int, default=1_800,
|
|
674
|
+
help="Target TPS (default: 1800)")
|
|
675
|
+
parser.add_argument("--duration", type=int, default=30,
|
|
676
|
+
help="Test duration in seconds (default: 30)")
|
|
677
|
+
parser.add_argument("--contracts", type=int, default=8,
|
|
678
|
+
help="Number of contracts (default: 8)")
|
|
679
|
+
parser.add_argument("--batch", type=int, default=200,
|
|
680
|
+
help="Batch size (default: 200)")
|
|
681
|
+
parser.add_argument("--workers", type=int, default=0,
|
|
682
|
+
help="Worker threads (0=auto)")
|
|
683
|
+
parser.add_argument("--warmup", type=int, default=3,
|
|
684
|
+
help="Warm-up seconds (default: 3)")
|
|
685
|
+
parser.add_argument("--senders", type=int, default=50,
|
|
686
|
+
help="Distinct sender addresses (default: 50)")
|
|
687
|
+
parser.add_argument("--no-rust", action="store_true",
|
|
688
|
+
help="Disable Rust core (pure-Python baseline)")
|
|
689
|
+
parser.add_argument("--json", type=str, default=None,
|
|
690
|
+
help="Write JSON report to file")
|
|
691
|
+
parser.add_argument("-v", "--verbose", action="store_true",
|
|
692
|
+
help="Enable debug logging")
|
|
693
|
+
|
|
694
|
+
args = parser.parse_args()
|
|
695
|
+
|
|
696
|
+
level = logging.DEBUG if args.verbose else logging.INFO
|
|
697
|
+
logging.basicConfig(level=level, format="%(levelname)s %(name)s: %(message)s")
|
|
698
|
+
|
|
699
|
+
profile = LoadProfile(
|
|
700
|
+
target_tps=args.tps,
|
|
701
|
+
duration_seconds=args.duration,
|
|
702
|
+
contract_count=args.contracts,
|
|
703
|
+
batch_size=args.batch,
|
|
704
|
+
workers=args.workers,
|
|
705
|
+
warmup_seconds=args.warmup,
|
|
706
|
+
sender_count=args.senders,
|
|
707
|
+
use_rust=not args.no_rust,
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
runner = LoadTestRunner(profile)
|
|
711
|
+
report = runner.run()
|
|
712
|
+
report.print_summary()
|
|
713
|
+
|
|
714
|
+
if args.json:
|
|
715
|
+
with open(args.json, "w") as f:
|
|
716
|
+
json.dump(report.to_dict(), f, indent=2)
|
|
717
|
+
print(f"JSON report written to {args.json}")
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
if __name__ == "__main__":
|
|
721
|
+
_cli_main()
|