File size: 8,813 Bytes
da8c484 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 | #!/usr/bin/env python3
"""
scripts/test_sendpath.py — prove the send path is inside a slot. Or that it isn't.
venv/bin/python scripts/test_sendpath.py
────────────────────────────────────────────────────────────────────────────
WHY THIS EXISTS, AND HOW IT DIFFERS FROM /bench
────────────────────────────────────────────────────────────────────────────
Operator: "how you improve the speed 400 ms — make test show us."
Fair, and the honest answer was that /pipeline could not show it. A
pipeline trace is only written when a real signal reaches the executor,
and nothing has reached it since the fix — so /pipeline still displays
three attempts from four days ago at 1.19s, measured on code that no
longer exists. A screen that cannot be refreshed cannot be evidence.
/bench does not answer it either. /bench times each stage in ISOLATION on
a fresh connection: it tells you the network is fine, which was never the
question. The question is whether the CACHES work, and an isolated timing
deliberately avoids them.
So this walks the real thing. Every function called below is the exact
function solana_executor calls on a live send — `_cached_mint_safety`,
`_cached_priority_fee`, `_cached_reserve`, `_cached_alt_accounts`,
`_fresh_blockhash` — through the same hot_state cache, with the same keys.
Each is timed TWICE:
COLD the first call, cache empty, the network round trip
WARM the second call, which is what a real trade actually pays
The gap between those two columns IS the fix. `freeze` was 758 ms and
51% of the send path; if the warm column does not show it near zero, the
warm cache is not working and this prints that in as many words.
────────────────────────────────────────────────────────────────────────────
WHAT IT NEVER DOES
────────────────────────────────────────────────────────────────────────────
No key is loaded. No transaction is built, signed, simulated or sent. No
bundle reaches Jito. It reads public state over RPC and times the reads.
Safe to run while the bot is live — it shares the same warm cache, so it
leaves the caches hotter than it found them.
"""
from __future__ import annotations
import argparse
import asyncio
import os
import sys
import time
from pathlib import Path
from typing import Any, Callable, Optional
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from modules.env_file import load_env_file
load_env_file()
_G, _Y, _R, _B, _0 = "\033[32m", "\033[33m", "\033[31m", "\033[1m", "\033[0m"
_SLOT_MS = 400.0
async def _time(fn: Callable[[], Any]) -> tuple[Optional[float], str]:
"""One call, in milliseconds. A failure returns None, never a zero.
A stage that errors and reports 0 ms is the single most misleading
number this script could print — it would look like the fastest stage
on the page.
"""
t0 = time.perf_counter()
try:
result = fn()
if asyncio.iscoroutine(result):
await result
except Exception as exc: # noqa: BLE001
return None, str(exc)[:70]
return (time.perf_counter() - t0) * 1000.0, ""
async def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--warm-runs", type=int, default=3,
help="warm samples per stage (default 3, the median is used)")
args = ap.parse_args()
from constants import SOLANA_TOKENS
from modules.solana_executor import (
_cached_alt_accounts, _cached_mint_safety, _cached_priority_fee,
_cached_reserve, _fresh_blockhash, get_shared_client,
)
rpc = (os.getenv("SOLANA_RPC_PRIMARY") or os.getenv("SOLANA_RPC_URL") or "").strip()
if not rpc:
print("\n no RPC configured (SOLANA_RPC_PRIMARY) — nothing to measure\n")
return 1
client = get_shared_client()
sol, usdc = SOLANA_TOKENS["SOL"], SOLANA_TOKENS["USDC"]
# Exactly the calls a send makes, with the same cache keys.
stages: list[tuple[str, Callable[[], Any], bool]] = [
("freeze SOL", lambda: _cached_mint_safety(client, rpc, "SOL", sol["mint"]), True),
("freeze USDC", lambda: _cached_mint_safety(client, rpc, "USDC", usdc["mint"]), True),
("priority fee", lambda: _cached_priority_fee(client, rpc), True),
("reserve", lambda: _cached_reserve(client, "USDC", usdc["mint"], rpc), True),
("alt tables", lambda: _cached_alt_accounts(client, rpc, []), True),
# Not cacheable in the same sense — a blockhash has a real
# expiry — but hot_state serves it behind a short TTL, so the
# caller should not be paying the round trip either.
("blockhash", lambda: _fresh_blockhash(client, rpc), False),
]
print()
print(f"{_B} ⏱ send-path test — the real cached calls, nothing signed{_0}")
print(" COLD = cache empty · WARM = what a live trade actually pays")
print(" " + "─" * 64)
print(f" {'stage':<16}{'COLD':>10}{'WARM':>10} saved")
print(" " + "─" * 64)
warm_total = 0.0
cold_total = 0.0
broken: list[str] = []
for name, fn, cacheable in stages:
cold, err = await _time(fn)
if cold is None:
broken.append(f"{name}: {err}")
print(f" {name:<16}{_R}{'failed':>10}{_0} {err[:30]}")
continue
warms: list[float] = []
for _ in range(max(1, args.warm_runs)):
w, werr = await _time(fn)
if w is not None:
warms.append(w)
if not warms:
broken.append(f"{name}: warm call failed")
continue
warm = sorted(warms)[len(warms) // 2]
cold_total += cold
warm_total += warm
saved = cold - warm
# Colour the WARM column, because that is the number a trade pays.
colour = _G if warm < 20 else (_Y if warm < 100 else _R)
flag = "" if cacheable else " (short TTL by design)"
print(f" {name:<16}{cold:>9.0f}ms{colour}{warm:>9.0f}ms{_0}"
f" {saved:>6.0f}ms{flag}")
print(" " + "─" * 64)
print(f" {'TOTAL':<16}{cold_total:>9.0f}ms{warm_total:>9.0f}ms"
f" {cold_total - warm_total:>6.0f}ms")
print()
if broken:
print(f" {_Y}Some stages could not be measured:{_0}")
for b in broken:
print(f" {b}")
print(" The totals above exclude them, so they understate the real")
print(" pipeline. Fix these before reading this as a pass.")
print()
return 1
# A live send also pays two quotes and two swap-instruction builds.
# Those are NOT cacheable — they are the market — so /bench is the
# right measure for them and its figure is used here rather than
# re-measuring it badly.
quotes_ms = float(os.getenv("SENDPATH_QUOTE_BUDGET_MS", "120"))
projected = warm_total + quotes_ms
print(f" cached state, warm {warm_total:>7.0f} ms")
print(f" + quotes and swap-ix {quotes_ms:>7.0f} ms "
f"(uncacheable — /bench measures these)")
print(f" {_B}= signal to signed {projected:>7.0f} ms{_0}")
print(f" a Solana slot is {_SLOT_MS:>7.0f} ms")
print()
if projected <= _SLOT_MS:
print(f" {_G}✓ {projected:.0f} ms — inside one slot.{_0} The warm cache is")
print(" working. Latency is not what stops a trade; /observe for")
print(" whether the round trip is positive at all.")
else:
print(f" {_R}✗ {projected:.0f} ms — longer than a slot.{_0}")
print(" The warm column above names which stage is still paying a")
print(" round trip. A cached stage over 100 ms warm means the cache")
print(" is missing — check hot_state's TTL for it and that")
print(" warm_mint_safety ran at startup.")
print()
# The freeze stage specifically, because it was 51% of the pipeline and
# is the thing the operator asked to see proven.
print(f" {_B}The one that mattered:{_0} `freeze` was 758 ms p50 and 51% of the")
print(" send path. Its WARM column above is what a trade pays now.")
print()
return 0
if __name__ == "__main__":
raise SystemExit(asyncio.run(main()))
|