method-economics.py: host-local method cost-vs-pay analysis
Each gateway serves its own regional market, so the eviction economics (cost_ratio = time_share/call_share at flat per-call pay) are computed where they apply: against the local dshackle's histograms. Silent on non-gateway hosts. Offenders = candidates for per-upstream method-groups/methods disables (operator eviction rule 2026-07-10). sendRawTransaction et al [protected]. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
91
method-economics.py
Executable file
91
method-economics.py
Executable file
@@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""method-economics.py - HOST-LOCAL method cost-vs-pay analysis for the dshackle
|
||||||
|
gateway on this machine. dRPC pays flat per call, so a method's pay share equals
|
||||||
|
its call-count share while its cost is its serving-time share:
|
||||||
|
|
||||||
|
cost_ratio = time_share / call_share (== mean_method / mean_chain)
|
||||||
|
|
||||||
|
Methods with cost_ratio >> 1 and a meaningful time share are subsidized by the
|
||||||
|
cheap calls they slow down (and they poison the chain's latency scoring).
|
||||||
|
Prints one line per offender; silent when this host runs no dshackle.
|
||||||
|
|
||||||
|
Usage: ./method-economics.py [--ratio 8] [--time-share 0.10] [--min-calls 20] [--all]
|
||||||
|
Run from the controller: ./run <host> ./method-economics.py
|
||||||
|
Window = since this dshackle's last restart (counters reset on restart).
|
||||||
|
"""
|
||||||
|
import collections
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
RATIO_MIN = 8.0
|
||||||
|
TIME_SHARE_MIN = 0.10
|
||||||
|
MIN_CALLS = 20
|
||||||
|
MIN_CHAIN_CALLS = 500
|
||||||
|
# Never-evict (inherent cost, core function): flagged [protected], not evictable.
|
||||||
|
PROTECTED = {'eth_sendRawTransaction', 'eth_sendTransaction'}
|
||||||
|
|
||||||
|
args = sys.argv[1:]
|
||||||
|
def argval(name, default):
|
||||||
|
return float(args[args.index(name) + 1]) if name in args else default
|
||||||
|
RATIO_MIN = argval('--ratio', RATIO_MIN)
|
||||||
|
TIME_SHARE_MIN = argval('--time-share', TIME_SHARE_MIN)
|
||||||
|
MIN_CALLS = int(argval('--min-calls', MIN_CALLS))
|
||||||
|
SHOW_ALL = '--all' in args
|
||||||
|
|
||||||
|
def dshackle_ip():
|
||||||
|
try:
|
||||||
|
out = subprocess.run(['docker', 'inspect', 'rpc-dshackle-1', '--format',
|
||||||
|
'{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}'],
|
||||||
|
capture_output=True, text=True, timeout=10).stdout.strip()
|
||||||
|
return out.split()[0] if out.split() else None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ip = dshackle_ip()
|
||||||
|
if not ip:
|
||||||
|
return # not a gateway host - stay silent
|
||||||
|
try:
|
||||||
|
metrics = subprocess.run(['curl', '-s', '--max-time', '15', f'http://{ip}:8081/metrics'],
|
||||||
|
capture_output=True, text=True, timeout=20).stdout
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
S = collections.Counter(); C = collections.Counter()
|
||||||
|
for line in metrics.splitlines():
|
||||||
|
m = re.match(r'dshackle_request_grpc_native_response_seconds_(sum|count)'
|
||||||
|
r'\{chain="([A-Z_]+)",method="([^"]+)"\} ([0-9.eE+]+)', line)
|
||||||
|
if m:
|
||||||
|
kind, chain, meth, v = m.groups()
|
||||||
|
(S if kind == 'sum' else C)[(chain, meth)] += float(v)
|
||||||
|
chain_calls = collections.Counter(); chain_time = collections.Counter()
|
||||||
|
for (chain, meth), c in C.items():
|
||||||
|
chain_calls[chain] += c
|
||||||
|
chain_time[chain] += S[(chain, meth)]
|
||||||
|
rows = []
|
||||||
|
for (chain, meth), c in C.items():
|
||||||
|
if c < MIN_CALLS or chain_calls[chain] < MIN_CHAIN_CALLS or chain_time[chain] <= 0:
|
||||||
|
continue
|
||||||
|
call_share = c / chain_calls[chain]
|
||||||
|
time_share = S[(chain, meth)] / chain_time[chain]
|
||||||
|
ratio = time_share / call_share if call_share else 0
|
||||||
|
if SHOW_ALL or (ratio >= RATIO_MIN and time_share >= TIME_SHARE_MIN):
|
||||||
|
rows.append({'chain': chain, 'method': meth,
|
||||||
|
'protected': meth in PROTECTED,
|
||||||
|
'calls': int(c),
|
||||||
|
'call_share_pct': round(100 * call_share, 2),
|
||||||
|
'time_share_pct': round(100 * time_share, 1),
|
||||||
|
'mean_ms': round(1000 * S[(chain, meth)] / c, 1),
|
||||||
|
'cost_ratio': round(ratio, 1)})
|
||||||
|
rows.sort(key=lambda r: -r['cost_ratio'] * r['time_share_pct'])
|
||||||
|
for r in rows:
|
||||||
|
tag = ' [protected]' if r['protected'] else ''
|
||||||
|
print(f"{r['chain']:18s} {r['method']:30s}{tag} calls={r['calls']:<7d} "
|
||||||
|
f"call%={r['call_share_pct']:<6.2f} time%={r['time_share_pct']:<5.1f} "
|
||||||
|
f"mean={r['mean_ms']:.1f}ms ratio={r['cost_ratio']}")
|
||||||
|
if '--json' in args:
|
||||||
|
print(json.dumps(rows))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user