From 6041e7cfa6e1fbd0da2ded87c2f76c11761de649 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sat, 8 Oct 2016 15:21:40 -0700 Subject: [PATCH] Performance improvements to response and moved tests around --- sanic/response.py | 36 ++++---- sanic/server.py | 16 ++-- .../aiohttp}/python.aiohttp.py | 0 tests/{ => performance/golang}/golang.http.go | 3 +- tests/performance/sanic/http_response.py | 33 +++++++ tests/performance/sanic/simple_server.py | 85 +++++++++++++++++++ tests/python.sanic.py | 83 ------------------ 7 files changed, 143 insertions(+), 113 deletions(-) rename tests/{ => performance/aiohttp}/python.aiohttp.py (100%) rename tests/{ => performance/golang}/golang.http.go (79%) create mode 100644 tests/performance/sanic/http_response.py create mode 100644 tests/performance/sanic/simple_server.py delete mode 100644 tests/python.sanic.py diff --git a/sanic/response.py b/sanic/response.py index cb2ac57f..97be7d9d 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -17,36 +17,30 @@ STATUS_CODES = { 503: 'Service Unavailable', 504: 'Gateway Timeout', } + class HTTPResponse: __slots__ = ('body', 'status', 'content_type') - def __init__(self, body='', status=200, content_type='text/plain'): + def __init__(self, body=None, status=200, content_type='text/plain', body_bytes=b''): self.content_type = content_type - self.body = body + + if not body is None: + self.body = body.encode('utf-8') + else: + self.body = body_bytes + self.status = status - @property - def body_bytes(self): - body_type = type(self.body) - if body_type is str: - body = self.body.encode('utf-8') - elif body_type is bytes: - body = self.body - else: - body = b'Unable to interpret body' - - return body - def output(self, version="1.1", keep_alive=False): - body = self.body_bytes + # This is all returned in a kind-of funky way + # We tried to make this as fast as possible in pure python return b''.join([ - 'HTTP/{} {} {}\r\n'.format(version, self.status, STATUS_CODES.get(self.status, 'FAIL')).encode('latin-1'), - 'Content-Type: {}\r\n'.format(self.content_type).encode('latin-1'), - 'Content-Length: {}\r\n'.format(len(body)).encode('latin-1'), - 'Connection: {}\r\n'.format('keep-alive' if keep_alive else 'close').encode('latin-1'), + 'HTTP/{} {} {}\r\n'.format(version, self.status, STATUS_CODES.get(self.status, 'FAIL')).encode(), + b'Content-Type: ', self.content_type.encode(), b'\r\n', + b'Content-Length: ', str(len(self.body)).encode(), b'\r\n', + b'Connection: ', ('keep-alive' if keep_alive else 'close').encode(), b'\r\n', b'\r\n', - body, - #b'\r\n' + self.body, ]) def json(body, status=200): diff --git a/sanic/server.py b/sanic/server.py index e8e8fd1e..fb381547 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -95,19 +95,19 @@ class HttpProtocol(asyncio.Protocol): def on_body(self, body): self.request.body = body def on_message_complete(self): - self.loop.create_task(self.get_response(self.request)) + self.loop.create_task(self.get_response()) # -------------------------------------------- # # Responding # -------------------------------------------- # - async def get_response(self, request): + async def get_response(self): try: - handler = self.sanic.router.get(request) + handler = self.sanic.router.get(self.request) if handler is None: raise ServerError("'None' was returned while requesting a handler from the router") - response = handler(request) + response = handler(self.request) # Check if the handler is asynchronous if isawaitable(response): @@ -115,20 +115,20 @@ class HttpProtocol(asyncio.Protocol): except Exception as e: try: - response = self.sanic.error_handler.response(request, e) + response = self.sanic.error_handler.response(self.request, e) except Exception as e: if self.sanic.debug: response = HTTPResponse("Error while handling error: {}\nStack: {}".format(e, format_exc())) else: response = HTTPResponse("An error occured while handling an error") - self.write_response(request, response) + self.write_response(response) - def write_response(self, request, response): + def write_response(self, response): #print("response - {} - {}".format(self.n, self.request)) try: keep_alive = self.parser.should_keep_alive() - self.transport.write(response.output(request.version, keep_alive)) + self.transport.write(response.output(self.request.version, keep_alive)) #print("KA - {}".format(self.parser.should_keep_alive())) if not keep_alive: self.transport.close() diff --git a/tests/python.aiohttp.py b/tests/performance/aiohttp/python.aiohttp.py similarity index 100% rename from tests/python.aiohttp.py rename to tests/performance/aiohttp/python.aiohttp.py diff --git a/tests/golang.http.go b/tests/performance/golang/golang.http.go similarity index 79% rename from tests/golang.http.go rename to tests/performance/golang/golang.http.go index c022c00e..fb13cc8b 100644 --- a/tests/golang.http.go +++ b/tests/performance/golang/golang.http.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "os" "net/http" ) @@ -11,5 +12,5 @@ func handler(w http.ResponseWriter, r *http.Request) { func main() { http.HandleFunc("/", handler) - http.ListenAndServe(":8000", nil) + http.ListenAndServe(":" + os.Args[1], nil) } diff --git a/tests/performance/sanic/http_response.py b/tests/performance/sanic/http_response.py new file mode 100644 index 00000000..d17967e2 --- /dev/null +++ b/tests/performance/sanic/http_response.py @@ -0,0 +1,33 @@ +import asyncpg +import sys +import os +import inspect + +currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) +sys.path.insert(0,currentdir + '/../../../') + +import timeit + +from sanic.response import json + +print(json({ "test":True }).output()) + +print("Running New 100,000 times") +times = 0 +total_time = 0 +for n in range(6): + time = timeit.timeit('json({ "test":True }).output()', setup='from sanic.response import json', number=100000) + print("Took {} seconds".format(time)) + total_time += time + times += 1 +print("Average: {}".format(total_time/times)) + +print("Running Old 100,000 times") +times = 0 +total_time = 0 +for n in range(6): + time = timeit.timeit('json({ "test":True }).output_old()', setup='from sanic.response import json', number=100000) + print("Took {} seconds".format(time)) + total_time += time + times += 1 +print("Average: {}".format(total_time/times)) \ No newline at end of file diff --git a/tests/performance/sanic/simple_server.py b/tests/performance/sanic/simple_server.py new file mode 100644 index 00000000..3109dcc4 --- /dev/null +++ b/tests/performance/sanic/simple_server.py @@ -0,0 +1,85 @@ +import sys +import os +import inspect + +currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) +sys.path.insert(0,currentdir + '/../../../') + +from sanic import Sanic +from sanic.response import json, text +from sanic.exceptions import ServerError + +app = Sanic("test") + +@app.route("/") +async def test(request): + return json({ "test": True }) + +@app.route("/sync") +def test(request): + return json({ "test": True }) + + + + +@app.route("/text") +def rtext(request): + return text("yeehaww") + +@app.route("/exception") +def exception(request): + raise ServerError("yep") + +@app.route("/exception/async") +async def test(request): + raise ServerError("asunk") + +@app.route("/post_json") +def post_json(request): + return json({ "received": True, "message": request.json }) + +@app.route("/query_string") +def query_string(request): + return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string }) + +import sys +app.run(host="0.0.0.0", port=sys.argv[1],)#, on_start=setup) + + + +# import asyncio_redis +# import asyncpg +# async def setup(sanic, loop): +# sanic.conn = [] +# sanic.redis = [] +# for x in range(10): +# sanic.conn.append(await asyncpg.connect(user='postgres', password='zomgdev', database='postgres', host='192.168.99.100')) +# for n in range(30): +# connection = await asyncio_redis.Connection.create(host='192.168.99.100', port=6379) +# sanic.redis.append(connection) + + +# c=0 +# @app.route("/postgres") +# async def postgres(request): +# global c +# values = await app.conn[c].fetch('''SELECT * FROM players''') +# c += 1 +# if c == 10: +# c = 0 +# return text("yep") + +# r=0 +# @app.route("/redis") +# async def redis(request): +# global r +# try: +# values = await app.redis[r].get('my_key') +# except asyncio_redis.exceptions.ConnectionLostError: +# app.redis[r] = await asyncio_redis.Connection.create(host='127.0.0.1', port=6379) +# values = await app.redis[r].get('my_key') + +# r += 1 +# if r == 30: +# r = 0 +# return text(values) diff --git a/tests/python.sanic.py b/tests/python.sanic.py deleted file mode 100644 index 43df5b9e..00000000 --- a/tests/python.sanic.py +++ /dev/null @@ -1,83 +0,0 @@ -import asyncpg -import sys -import os -import inspect - -currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) -parentdir = os.path.dirname(currentdir) -sys.path.insert(0,parentdir) - -from sanic import Sanic -from sanic.response import json, text -from sanic.exceptions import ServerError - -app = Sanic("test") - -@app.route("/") -async def test(request): - return json({ "test": True }) - - - -import asyncio_redis -import asyncpg -async def setup(sanic, loop): - sanic.conn = [] - sanic.redis = [] - for x in range(10): - sanic.conn.append(await asyncpg.connect(user='postgres', password='zomgdev', database='postgres', host='192.168.99.100')) - for n in range(30): - connection = await asyncio_redis.Connection.create(host='192.168.99.100', port=6379) - sanic.redis.append(connection) - - -c=0 -@app.route("/postgres") -async def postgres(request): - global c - values = await app.conn[c].fetch('''SELECT * FROM players''') - c += 1 - if c == 10: - c = 0 - return text("yep") - -r=0 -@app.route("/redis") -async def redis(request): - global r - try: - values = await app.redis[r].get('my_key') - except asyncio_redis.exceptions.ConnectionLostError: - app.redis[r] = await asyncio_redis.Connection.create(host='127.0.0.1', port=6379) - values = await app.redis[r].get('my_key') - - r += 1 - if r == 30: - r = 0 - return text(values) - - - - -@app.route("/text") -def rtext(request): - return text("yeehaww") - -@app.route("/exception") -def exception(request): - raise ServerError("yep") - -@app.route("/exception/async") -async def test(request): - raise ServerError("asunk") - -@app.route("/post_json") -def post_json(request): - return json({ "received": True, "message": request.json }) - -@app.route("/query_string") -def query_string(request): - return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string }) - -import sys -app.run(host="0.0.0.0", port=sys.argv[1])#, on_start=setup)