"""Count socket syscalls for identical tiny requests; not a throughput benchmark."""
import argparse
import hashlib
import http.client
import json
import os
from pathlib import Path
import signal
import socket
import subprocess
import sys
import time
root = Path(__file__).resolve().parents[1]
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--server', choices=['skirr', 'peregrine', 'weft'], required=True)
parser.add_argument('--baseline', type=Path)
parser.add_argument('--app', choices=['raw', 'fastapi'], default='raw')
parser.add_argument('--output', type=Path, required=True)
a = parser.parse_args()
source = a.baseline / 'python' if a.baseline else root / 'python'
out = a.output.resolve()
out.mkdir(parents=True, exist_ok=True)
with socket.socket() as sock:
sock.bind(('127.0.0.1', 0))
port = sock.getsockname()[1]
env = dict(os.environ, PYTHONPATH=str(source) + os.pathsep + str(root / 'bench'))
command = ['strace', '-f', '-qq', '-yy', '-e', 'trace=network,read,write,writev,epoll_ctl,epoll_wait',
'-o', str(out / 'syscalls.txt'), sys.executable, str(root / 'bench/server.py'),
a.server, '--app', a.app, '--port', str(port), '--workers', '1', '--cpus', '0']
(out / 'command.json').write_text(json.dumps(command, indent=2))
path, expected = ('/json', b'{"hello":"world"}') if a.app == 'fastapi' else ('/tiny', b'Hello world!')
process = subprocess.Popen(command, env=env, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, start_new_session=True)
try:
for _ in range(100):
client = http.client.HTTPConnection('127.0.0.1', port, timeout=.5)
try:
client.request('GET', path)
response = client.getresponse()
assert response.status == 200 and response.read() == expected
break
except OSError:
time.sleep(.05)
finally:
client.close()
else:
raise RuntimeError('Server did not start')
client = http.client.HTTPConnection('127.0.0.1', port, timeout=5)
try:
for _ in range(1000):
client.request('GET', path)
response = client.getresponse()
assert response.status == 200 and response.read() == expected
finally:
client.close()
finally:
os.killpg(process.pid, signal.SIGINT)
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
os.killpg(process.pid, signal.SIGKILL)
process.wait()
lines = (out / 'syscalls.txt').read_text().splitlines()
reads = [line for line in lines if '<TCP:' in line and
('read(' in line or 'recvfrom(' in line)]
writes = [line for line in lines if '<TCP:' in line and
any(name in line for name in ('write(', 'sendto(', 'writev('))]
summary = dict(server=a.server, app=a.app, requests=1001, socket_reads=len(reads),
epoll_out_modifications=sum("epoll_ctl(" in line and "EPOLL_CTL_MOD" in line and "EPOLLOUT" in line for line in lines),
socket_error_queries=sum("getsockopt(" in line and "SO_ERROR" in line for line in lines),
nodelay_pushes=sum("setsockopt(" in line and "TCP_NODELAY" in line for line in lines),
source_sha256={p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in (a.baseline or root).joinpath("src").glob("*.rs")} if a.server == "skirr" else {},
binary_sha256=hashlib.sha256((source/"skirr/_native.so").read_bytes()).hexdigest() if a.server == "skirr" else None,
socket_read_EAGAIN=sum('EAGAIN' in line for line in reads),
socket_writes=len(writes), diagnostic_only=True,
includes_startup_and_shutdown=True)
(out / 'summary.json').write_text(json.dumps(summary, indent=2))
print(summary)