72 lines
3.4 KiB
Python
72 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Measure fresh-connection task admission bursts against an isolated local API.
|
|
|
|
The planner is deliberately idle: receipts must not wait for planning. Latency
|
|
is reported as evidence, never asserted as a machine-independent unit-test SLA.
|
|
"""
|
|
import argparse
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
import http.client
|
|
import json
|
|
import math
|
|
from pathlib import Path
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
|
|
ROOT=Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0,str(ROOT/'coordinator'))
|
|
from robot_bt_coordinator.backends import ManualBackend
|
|
from robot_bt_coordinator.http_api import make_server
|
|
from robot_bt_coordinator.service import Coordinator
|
|
|
|
|
|
def probe(clients=10,rounds=3):
|
|
if not 1<=clients<=128 or not 1<=rounds<=100:
|
|
raise ValueError('clients must be 1..128 and rounds 1..100')
|
|
with tempfile.TemporaryDirectory(prefix='bt-http-probe-') as folder:
|
|
coordinator=Coordinator(str(Path(folder)/'tasks.db'),ManualBackend(),{'probe_robot'})
|
|
server=make_server(coordinator,'127.0.0.1',0,'local-probe-token')
|
|
thread=threading.Thread(target=server.serve_forever,daemon=True)
|
|
thread.start()
|
|
samples=[]
|
|
try:
|
|
for iteration in range(rounds):
|
|
gate=threading.Barrier(clients)
|
|
def submit(index):
|
|
connection=http.client.HTTPConnection('127.0.0.1',server.server_port,timeout=10)
|
|
request=dict(client_request_id=f'probe-{iteration}-{index}',robot_id='probe_robot',instruction='local admission probe',known_info={})
|
|
gate.wait(timeout=15)
|
|
started=time.perf_counter()
|
|
try:
|
|
connection.request('POST','/v1/tasks',json.dumps(request),{'Authorization':'Bearer local-probe-token','Content-Type':'application/json'})
|
|
response=connection.getresponse()
|
|
body=json.loads(response.read())
|
|
return dict(elapsed_ms=(time.perf_counter()-started)*1000,http_status=response.status,task_id=body.get('task_id'),status=body.get('status'))
|
|
finally:connection.close()
|
|
with ThreadPoolExecutor(max_workers=clients) as pool:
|
|
samples.extend(pool.map(submit,range(clients)))
|
|
successful=all(s['http_status']==202 and s['status']=='QUEUED' for s in samples)
|
|
successful=successful and len({s['task_id'] for s in samples})==clients*rounds
|
|
durations=sorted(s['elapsed_ms'] for s in samples)
|
|
return dict(clients=clients,rounds=rounds,requests=len(samples),all_accepted=successful,
|
|
p50_ms=durations[math.ceil(len(durations)*.5)-1],
|
|
p95_ms=durations[math.ceil(len(durations)*.95)-1],max_ms=max(durations),
|
|
listen_backlog=server.request_queue_size,planner_started=bool(coordinator.backend.plans),samples=samples)
|
|
finally:
|
|
server.shutdown();server.server_close();thread.join();coordinator.close()
|
|
|
|
|
|
def main():
|
|
parser=argparse.ArgumentParser()
|
|
parser.add_argument('--clients',type=int,default=10)
|
|
parser.add_argument('--rounds',type=int,default=3)
|
|
args=parser.parse_args()
|
|
result=probe(args.clients,args.rounds)
|
|
print(json.dumps(result,indent=2))
|
|
if not result['all_accepted'] or result['planner_started']:raise SystemExit(1)
|
|
|
|
|
|
if __name__=='__main__':main()
|