From 2c6938dee5c088d22d47a45a1de728aaf8668e8c Mon Sep 17 00:00:00 2001 From: rob Date: Fri, 10 Jul 2026 15:01:06 +0000 Subject: [PATCH] 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 --- method-economics.py | 91 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100755 method-economics.py diff --git a/method-economics.py b/method-economics.py new file mode 100755 index 00000000..55df3360 --- /dev/null +++ b/method-economics.py @@ -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 ./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()