Commit 77feb65a authored by Elvis Pranskevichus's avatar Elvis Pranskevichus

Add support for HTTP benchmarks

parent 0ab494ea
#!/usr/bin/env python3
import argparse
import json
import os
import subprocess
import sys
import tempfile
def abort(msg):
print(msg, file=sys.stdout)
sys.exit(1)
luascript = '''
function done(summary, latency, requests)
tpl = [[
{
"messages": %d,
"transfer": %.2f,
"rps": %.2f,
"latency_min": %.3f,
"latency_mean": %.3f,
"latency_max": %.3f,
"latency_std": %.3f,
"latency_cv": %.2f,
"latency_percentiles": [%s]
}]]
transfer = (summary.bytes / (1024 * 1024)) / (summary.duration / 1000000)
rps = summary.requests / (summary.duration / 1000000)
latency_percentiles = {}
percentiles = {25, 50, 75, 90, 99, 99.99}
for i, percentile in ipairs(percentiles) do
table.insert(
latency_percentiles,
string.format("[%.2f, %.3f]", percentile,
latency:percentile(percentile) / 1000)
)
end
out = string.format(tpl, summary.requests, transfer, rps,
latency.min / 1000, latency.mean / 1000,
latency.max / 1000, latency.stdev / 1000,
(latency.stdev / latency.mean) * 100,
table.concat(latency_percentiles, ','))
io.stderr:write(out)
end
'''
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--msize', default=1000, type=int,
help='message size in bytes')
parser.add_argument('--duration', '-T', default=30, type=int,
help='duration of test in seconds')
parser.add_argument('--concurrency', default=3, type=int,
help='request concurrency')
parser.add_argument('--addr', default='127.0.0.1:25000', type=str,
help='server address')
parser.add_argument('--output-format', default='text', type=str,
help='output format', choices=['text', 'json'])
args = parser.parse_args()
unix = False
if args.addr.startswith('file:'):
abort('Unix sockets are not supported')
with tempfile.NamedTemporaryFile(mode='w+t', delete=False) as luaf:
luaf.write(luascript)
lua_script_path = luaf.name
wrk = ['wrk', '--latency', '--duration={}s'.format(args.duration),
'--connections={}'.format(args.concurrency),
'--script={}'.format(lua_script_path),
'http://{}/{}'.format(args.addr, args.msize)]
try:
wrk_run = subprocess.Popen(wrk, universal_newlines=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
out, data_json = wrk_run.communicate()
finally:
os.unlink(lua_script_path)
if args.output_format == 'json':
print(data_json)
else:
data = json.loads(data_json)
data['latency_percentiles'] = '; '.join(
'{}% under {}ms'.format(*v) for v in data['latency_percentiles'])
output = '''\
{messages} {size}KiB messages in {duration} seconds
Latency: min {latency_min}ms; max {latency_max}ms; mean {latency_mean}ms; \
std: {latency_std}ms ({latency_cv}%)
Latency distribtion: {latency_percentiles}
Requests/sec: {rps}
Transfer/sec: {transfer}MiB
'''.format(duration=args.duration, size=round(args.msize / 1024, 2), **data)
print(output)
...@@ -34,8 +34,8 @@ unix_address = 'file:{_socket}/server.sock'.format(_socket=_socket) ...@@ -34,8 +34,8 @@ unix_address = 'file:{_socket}/server.sock'.format(_socket=_socket)
tcp_client = echo_client + ['--addr={}'.format(tcp_address)] tcp_client = echo_client + ['--addr={}'.format(tcp_address)]
unix_client = echo_client + ['--addr={}'.format(unix_address)] unix_client = echo_client + ['--addr={}'.format(unix_address)]
http_client = ['./http_client', '--output-format=json',
http_client = "wrk --latency -d 30 -c 200 -t 4 http://127.0.0.1:25000/{msize}" '--addr={}'.format(tcp_address)]
benchmarks = [{ benchmarks = [{
'name': 'tcpecho-gevent', 'name': 'tcpecho-gevent',
...@@ -143,6 +143,38 @@ benchmarks = [{ ...@@ -143,6 +143,38 @@ benchmarks = [{
'--streams', '--uvloop'], '--streams', '--uvloop'],
'server_address': unix_address, 'server_address': unix_address,
'client': unix_client, 'client': unix_client,
}, {
'name': 'http-asyncio-aiohttp',
'title': 'HTTP server (asyncio/aiohttp)',
'server': python + ['/usr/src/servers/asyncio_http_server.py',
'--type=asyncio+aiohttp',
'--addr=0.0.0.0:25000'],
'server_address': tcp_address,
'client': http_client,
}, {
'name': 'http-asyncio-httptools',
'title': 'HTTP server (asyncio/httptools)',
'server': python + ['/usr/src/servers/asyncio_http_server.py',
'--type=asyncio+httptools',
'--addr=0.0.0.0:25000'],
'server_address': tcp_address,
'client': http_client,
}, {
'name': 'http-uvloop-aiohttp',
'title': 'HTTP server (uvloop/aiohttp)',
'server': python + ['/usr/src/servers/asyncio_http_server.py',
'--type=uvloop+aiohttp',
'--addr=0.0.0.0:25000'],
'server_address': tcp_address,
'client': http_client,
}, {
'name': 'http-uvloop-httptools',
'title': 'HTTP server (uvloop/httptools)',
'server': python + ['/usr/src/servers/asyncio_http_server.py',
'--type=uvloop+httptools',
'--addr=0.0.0.0:25000'],
'server_address': tcp_address,
'client': http_client,
}] }]
...@@ -175,7 +207,7 @@ def start_and_wait_for_server(server_cmd, address, timeout=60): ...@@ -175,7 +207,7 @@ def start_and_wait_for_server(server_cmd, address, timeout=60):
sock.settimeout(time.monotonic() - start) sock.settimeout(time.monotonic() - start)
try: try:
sock.connect(addr) sock.connect(addr)
sock.sendall(b'ping') sock.sendall(b'GET / HTTP/1.0\r\n\r\n')
if sock.recv(4): if sock.recv(4):
print('Server is up and running.') print('Server is up and running.')
else: else:
......
...@@ -5,6 +5,7 @@ import aiohttp.server ...@@ -5,6 +5,7 @@ import aiohttp.server
from aiohttp import web from aiohttp import web
import sys import sys
import httptools
import uvloop import uvloop
from socket import * from socket import *
...@@ -32,9 +33,6 @@ class HttpResponse: ...@@ -32,9 +33,6 @@ class HttpResponse:
self._headers_sent = False self._headers_sent = False
def write(self, data): def write(self, data):
if isinstance(data, str):
data = data.encode()
self._protocol._transport.writelines([ self._protocol._transport.writelines([
'HTTP/{} 200 OK\r\n'.format( 'HTTP/{} 200 OK\r\n'.format(
self._request._version).encode('latin-1'), self._request._version).encode('latin-1'),
...@@ -45,9 +43,6 @@ class HttpResponse: ...@@ -45,9 +43,6 @@ class HttpResponse:
]) ])
RESP = b'Hello World' * 512
class HttpProtocol(asyncio.Protocol): class HttpProtocol(asyncio.Protocol):
__slots__ = ('_loop', __slots__ = ('_loop',
...@@ -83,6 +78,8 @@ class HttpProtocol(asyncio.Protocol): ...@@ -83,6 +78,8 @@ class HttpProtocol(asyncio.Protocol):
def connection_made(self, transport): def connection_made(self, transport):
self._transport = transport self._transport = transport
sock = transport.get_extra_info('socket')
sock.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1)
def connection_lost(self, exc): def connection_lost(self, exc):
self._current_request = self._current_parser = None self._current_request = self._current_parser = None
...@@ -96,7 +93,13 @@ class HttpProtocol(asyncio.Protocol): ...@@ -96,7 +93,13 @@ class HttpProtocol(asyncio.Protocol):
self._current_parser.feed_data(data) self._current_parser.feed_data(data)
def handle(self, request, response): def handle(self, request, response):
response.write(RESP) parsed_url = httptools.parse_url(self._current_url)
payload_size = parsed_url.path.decode('ascii')[1:]
if not payload_size:
payload_size = 1024
else:
payload_size = int(payload_size)
response.write(b'X' * payload_size)
def abort(msg): def abort(msg):
...@@ -105,12 +108,12 @@ def abort(msg): ...@@ -105,12 +108,12 @@ def abort(msg):
def aiohttp_server(loop, addr): def aiohttp_server(loop, addr):
PAYLOAD = b'<h1>Hello, World!</h1>'
async def handle(request): async def handle(request):
return web.Response(body=PAYLOAD) payload_size = int(request.match_info.get('size', 1024))
return web.Response(body=b'X' * payload_size)
app = web.Application(loop=loop) app = web.Application(loop=loop)
app.router.add_route('GET', '/{size}', handle)
app.router.add_route('GET', '/', handle) app.router.add_route('GET', '/', handle)
handler = app.make_handler() handler = app.make_handler()
server = loop.create_server(handler, *addr) server = loop.create_server(handler, *addr)
...@@ -118,6 +121,10 @@ def aiohttp_server(loop, addr): ...@@ -118,6 +121,10 @@ def aiohttp_server(loop, addr):
return server return server
def httptools_server(loop, addr):
return loop.create_server(lambda: HttpProtocol(loop=loop), *addr)
if __name__ == '__main__': if __name__ == '__main__':
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--type', default='asyncio+aiohttp', action='store') parser.add_argument('--type', default='asyncio+aiohttp', action='store')
...@@ -132,7 +139,7 @@ if __name__ == '__main__': ...@@ -132,7 +139,7 @@ if __name__ == '__main__':
else: else:
server_type = args.type server_type = args.type
if server_type == 'aiohttp': if server_type in {'aiohttp', 'httptools'}:
if not loop_type: if not loop_type:
loop_type = 'asyncio' loop_type = 'asyncio'
else: else:
......
...@@ -3,5 +3,5 @@ aiohttp==0.21.5 ...@@ -3,5 +3,5 @@ aiohttp==0.21.5
gevent==1.1.1 gevent==1.1.1
tornado==4.3 tornado==4.3
Twisted==16.1.1 Twisted==16.1.1
httptools==0.0.4 httptools==0.0.5
uvloop==0.4.7 uvloop==0.4.7
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment