diff --git a/README.md b/README.md index 2c31797..d093021 100644 --- a/README.md +++ b/README.md @@ -244,12 +244,15 @@ Compare the results! 👇 Framework | "hello world" (req/s) | 99% latency (ms) | redis save (req/s) | 99% latency (ms) ----------- | --------------------- | ---------------- | ------------------ | ---------------- -aiohttp | 14391.38 | 10.96 | 9470.74 | 12.94 -aiozmq | 15121.86 | 9.42 | 5904.84 | 21.57 -fastApi | 9590.96 | 18.31 | 6669.81 | 24.41 -sanic | 18790.49 | 8.69 | 12259.29 | 13.52 -zero(sync) | 24805.61 | 4.57 | 16498.83 | 7.80 -zero(async) | 22716.84 | 5.61 | 17446.19 | 7.24 +aiohttp | 14949.57 | 8.91 | 9753.87 | 13.75 +aiozmq | 13844.67 | 9.55 | 5239.14 | 30.92 +blacksheep | 32967.27 | 3.03 | 18010.67 | 6.79 +fastApi | 13154.96 | 9.07 | 8369.87 | 15.91 +sanic | 18793.08 | 5.88 | 12739.37 | 8.78 +zero(sync) | 28471.47 | 4.12 | 18114.84 | 6.69 +zero(async) | 29012.03 | 3.43 | 20956.48 | 5.80 + +Seems like blacksheep is the aster on hello world, but in more complex operations like saving to redis, zero is the winner! 🏆 # Roadmap 🗺 diff --git a/benchmarks/dockerize/Makefile b/benchmarks/dockerize/Makefile index 2cb8dc8..cb89f5d 100644 --- a/benchmarks/dockerize/Makefile +++ b/benchmarks/dockerize/Makefile @@ -1,7 +1,16 @@ SHELL := /bin/bash benchmark-aiohttp: cd aiohttp && ( \ - docker-compose up -d gateway server redis; \ + docker-compose up -d --build gateway server redis; \ + sleep 2; \ + docker-compose up wrk-hello; \ + docker-compose up wrk-order; \ + docker-compose down; \ + ) + +benchmark-blacksheep: + cd blacksheep && ( \ + docker-compose up -d --build gateway server redis; \ sleep 2; \ docker-compose up wrk-hello; \ docker-compose up wrk-order; \ @@ -10,7 +19,7 @@ benchmark-aiohttp: benchmark-aiozmq: cd aiozmq && ( \ - docker-compose up -d gateway server redis; \ + docker-compose up -d --build gateway server redis; \ sleep 2; \ docker-compose up wrk-hello; \ docker-compose up wrk-order; \ @@ -19,7 +28,7 @@ benchmark-aiozmq: benchmark-fastapi: cd fast_api && ( \ - docker-compose up -d gateway server redis; \ + docker-compose up -d --build gateway server redis; \ sleep 2; \ docker-compose up wrk-hello; \ docker-compose up wrk-order; \ @@ -28,7 +37,7 @@ benchmark-fastapi: benchmark-sanic: cd sanic && ( \ - docker-compose up -d gateway server redis; \ + docker-compose up -d --build gateway server redis; \ sleep 2; \ docker-compose up wrk-hello; \ docker-compose up wrk-order; \ diff --git a/benchmarks/dockerize/blacksheep/Dockerfile b/benchmarks/dockerize/blacksheep/Dockerfile new file mode 100755 index 0000000..521bd59 --- /dev/null +++ b/benchmarks/dockerize/blacksheep/Dockerfile @@ -0,0 +1,4 @@ +FROM python:3.9-slim + +COPY . . +RUN pip install -r requirements.txt \ No newline at end of file diff --git a/benchmarks/dockerize/blacksheep/docker-compose.yaml b/benchmarks/dockerize/blacksheep/docker-compose.yaml new file mode 100755 index 0000000..0bae88a --- /dev/null +++ b/benchmarks/dockerize/blacksheep/docker-compose.yaml @@ -0,0 +1,35 @@ +version: '3' + +services: + gateway: + build: . + container_name: gateway + command: uvicorn gateway:app --host 0.0.0.0 --port 8000 --workers 8 --log-level warning + ports: + - "8000:8000" + depends_on: + - server + - redis + server: + build: . + container_name: server + command: uvicorn server:app --host 0.0.0.0 --port 8011 --workers 8 --log-level warning + ports: + - "8011:8011" + # cpus: '0.50' + # mem_limit: 256m + redis: + image: eqalpha/keydb:latest + container_name: redis + ports: + - "6379:6379" + wrk-hello: + image: skandyla/wrk + command: -t 8 -c 64 -d 15s --latency http://gateway:8000/hello + depends_on: + - gateway + wrk-order: + image: skandyla/wrk + command: -t 8 -c 64 -d 15s --latency http://gateway:8000/order + depends_on: + - gateway diff --git a/benchmarks/dockerize/blacksheep/gateway.py b/benchmarks/dockerize/blacksheep/gateway.py new file mode 100644 index 0000000..f411cb6 --- /dev/null +++ b/benchmarks/dockerize/blacksheep/gateway.py @@ -0,0 +1,48 @@ +import logging +from typing import Optional + +from blacksheep import Application, JSONContent +from blacksheep import json as json_resp +from blacksheep import text +from blacksheep.client import ClientSession + +app = Application() +get = app.router.get + +logger = logging.getLogger(__name__) + +try: + import uvloop + + uvloop.install() +except ImportError: + logger.warning("Cannot use uvloop") + +session: Optional[ClientSession] = None + + +@get("/hello") +async def hello(): + global session + if session is None: + session = ClientSession() + + resp = await session.get("http://server:8011/hello") + txt = await resp.text() + return text(txt) + + +@get("/order") +async def order(): + global session + if session is None: + session = ClientSession() + + content = JSONContent( + data={ + "user_id": "1", + "items": ["apple", "python"], + } + ) + resp = await session.post("http://server:8011/order", content=content) + return json_resp(await resp.json()) diff --git a/benchmarks/dockerize/blacksheep/requirements.txt b/benchmarks/dockerize/blacksheep/requirements.txt new file mode 100644 index 0000000..5e7db40 --- /dev/null +++ b/benchmarks/dockerize/blacksheep/requirements.txt @@ -0,0 +1,7 @@ +blacksheep +uvicorn +PyJWT +aioredis +uvloop +msgpack +redis \ No newline at end of file diff --git a/benchmarks/dockerize/blacksheep/server.py b/benchmarks/dockerize/blacksheep/server.py new file mode 100644 index 0000000..4c32b05 --- /dev/null +++ b/benchmarks/dockerize/blacksheep/server.py @@ -0,0 +1,85 @@ +import logging +import uuid +from dataclasses import dataclass +from datetime import datetime +from typing import List + +from blacksheep import Application, FromJSON, json, text +from shared import Order, OrderResp, OrderStatus, async_save_order + +app = Application() +get = app.router.get +post = app.router.post + +logger = logging.getLogger(__name__) + +try: + import uvloop + + uvloop.install() +except ImportError: + logger.warning("Cannot use uvloop") + + +@get("/hello") +async def hello(): + return text("hello world") + + +@get("/order") +async def get_order(): + saved_order = await async_save_order( + Order( + id=str(uuid.uuid4()), + created_by="1", + items=["apple", "python"], + created_at=datetime.now().isoformat(), + status=OrderStatus.INITIATED, + ) + ) + + resp = OrderResp( + saved_order.id, + saved_order.status, + saved_order.items, + ) + return json( + { + "id": resp.order_id, + "status": resp.status, + "items": resp.items, + } + ) + + +@dataclass +class OrderReq: + user_id: str + items: List[str] + + +@post("/order") +async def order(req: FromJSON[OrderReq]): + body = req.value + saved_order = await async_save_order( + Order( + id=str(uuid.uuid4()), + created_by=body.user_id, + items=body.items, + created_at=datetime.now().isoformat(), + status=OrderStatus.INITIATED, + ) + ) + + resp = OrderResp( + saved_order.id, + saved_order.status, + saved_order.items, + ) + return json( + { + "id": resp.order_id, + "status": resp.status, + "items": resp.items, + } + ) diff --git a/benchmarks/dockerize/blacksheep/shared.py b/benchmarks/dockerize/blacksheep/shared.py new file mode 100644 index 0000000..2fb4b47 --- /dev/null +++ b/benchmarks/dockerize/blacksheep/shared.py @@ -0,0 +1,122 @@ +from dataclasses import dataclass +from datetime import datetime + +import aioredis +import msgpack +import redis + + +@dataclass +class Btype: + def pack(self): + return msgpack.packb(Btype.get_all_vars(self)) + + @classmethod + def unpack(cls, d): + return cls(**msgpack.unpackb(d, raw=False)) + + @staticmethod + def get_all_vars(obj): + values = vars(obj) + for k, v in values.items(): + if isinstance(v, Btype): + values[k] = vars(v) + return values + + +class OrderStatus: + INITIATED = 0 + PACKING = 1 + SHIPPED = 2 + DELIVERED = 3 + + +class Order(Btype): + # cache + orders = {} + + def __init__(self, id, items, created_by, created_at, status, updated_at=None): + self.id = id + self.items = items + self.created_by = created_by + self.created_at = created_at + self.status = status + self.updated_at = updated_at + + +@dataclass +class OrderResp(Btype): + order_id: str + status: int + items: list + + +@dataclass +class CreateOrderReq(Btype): + user_id: str + items: list + + +class SingletonMeta(type): + _instance = None + + def __call__(self): + if self._instance is None: + self._instance = super().__call__() + return self._instance + + +class AsyncRedisClient(metaclass=SingletonMeta): + def __init__(self): + self.client = aioredis.from_url("redis://redis:6379/0") + + async def set(self, key, val): + await self.client.set(key, val) + + async def get(self, key): + return await self.client.get(key) + + +async def async_save_order(order: Order) -> Order: + order.updated_at = datetime.now().isoformat() + r = AsyncRedisClient() + await r.set(order.id, order.pack()) + return order + + +async def async_get_order(id: str) -> Order: + r = AsyncRedisClient() + return Order.unpack(await r.get(id)) + + +class SingletonMeta(type): + _instance = None + + def __call__(self): + if self._instance is None: + self._instance = super().__call__() + return self._instance + + +class RedisClient(metaclass=SingletonMeta): + def __init__(self): + self.host = "0.0.0.0" + self.client = redis.StrictRedis().from_url("redis://redis:6379/0") + + def set(self, key, val): + self.client.set(key, val) + + def get(self, key): + return self.client.get(key) + + +def save_order(order: Order) -> Order: + order.updated_at = datetime.now().isoformat() + r = RedisClient() + r.set(order.id, order.pack()) + return order + + +def get_order(id: str) -> Order: + r = RedisClient() + return Order.unpack(r.get(id)) diff --git a/benchmarks/dockerize/sanic/gateway.py b/benchmarks/dockerize/sanic/gateway.py index f6dc633..32cf757 100644 --- a/benchmarks/dockerize/sanic/gateway.py +++ b/benchmarks/dockerize/sanic/gateway.py @@ -21,7 +21,7 @@ @app.route("/hello") -async def test(request): +async def hello(request): global session if session is None: session = ClientSession() @@ -31,7 +31,7 @@ async def test(request): @app.route("/order") -async def test(request): +async def order(request): global session if session is None: session = ClientSession() diff --git a/benchmarks/dockerize/zero/docker-compose.yaml b/benchmarks/dockerize/zero/docker-compose.yaml index 1f9b4b2..b1c8aa8 100755 --- a/benchmarks/dockerize/zero/docker-compose.yaml +++ b/benchmarks/dockerize/zero/docker-compose.yaml @@ -1,22 +1,33 @@ version: '3' services: - # aiohttp gateway + # # aiohttp gateway + # gateway: + # build: . + # container_name: gateway + # command: gunicorn gateway_aiohttp:app --bind 0.0.0.0:8000 --worker-class aiohttp.worker.GunicornWebWorker --workers 8 --log-level warning + # ports: + # - "8000:8000" + # depends_on: + # - server + # - redis + + # sanic gateway gateway: build: . container_name: gateway - command: gunicorn gateway_aiohttp:app --bind 0.0.0.0:8000 --worker-class aiohttp.worker.GunicornWebWorker --workers 8 --log-level warning + command: sanic gateway_sanic:app --host 0.0.0.0 --port 8000 --workers 8 --no-access-logs ports: - "8000:8000" depends_on: - server - redis - # sanic gateway + # # blacksheep gateway # gateway: # build: . # container_name: gateway - # command: sanic gateway_sanic:app --host 0.0.0.0 --port 8000 --workers 8 --no-access-logs + # command: uvicorn gateway_blacksheep:app --host 0.0.0.0 --port 8000 --workers 8 --log-level warning # ports: # - "8000:8000" # depends_on: diff --git a/benchmarks/dockerize/zero/gateway_blacksheep.py b/benchmarks/dockerize/zero/gateway_blacksheep.py new file mode 100644 index 0000000..eea8dd4 --- /dev/null +++ b/benchmarks/dockerize/zero/gateway_blacksheep.py @@ -0,0 +1,49 @@ +import logging + +from blacksheep import Application +from blacksheep import json as json_resp +from blacksheep import text + +from zero import AsyncZeroClient, ZeroClient + +# TODO: why we can't use uvloop? +try: + import uvloop + + uvloop.install() +except ImportError: + logging.warning("Cannot use uvloop") + pass + + +app = Application() +get = app.router.get + +client = ZeroClient("server", 5559) +async_client = AsyncZeroClient("server", 5559) + + +@get("/hello") +async def hello(): + resp = client.call("hello_world", None) + return text(resp) + + +@get("/async_hello") +async def async_hello(): + resp = await async_client.call("hello_world", None) + return text(resp) + + +@get("/order") +async def order(): + resp = client.call("save_order", {"user_id": "1", "items": ["apple", "python"]}) + return json_resp(resp) + + +@get("/async_order") +async def async_order(): + resp = await async_client.call( + "save_order", {"user_id": "1", "items": ["apple", "python"]} + ) + return json_resp(resp) diff --git a/benchmarks/dockerize/zero/gateway_sanic.py b/benchmarks/dockerize/zero/gateway_sanic.py index 171662d..7127f34 100644 --- a/benchmarks/dockerize/zero/gateway_sanic.py +++ b/benchmarks/dockerize/zero/gateway_sanic.py @@ -36,7 +36,7 @@ async def async_hello(request): @app.route("/order") async def order(request): resp = client.call("save_order", {"user_id": "1", "items": ["apple", "python"]}) - return json(await resp.json()) + return json(resp) @app.route("/async_order") @@ -44,17 +44,17 @@ async def async_order(request): resp = await async_client.call( "save_order", {"user_id": "1", "items": ["apple", "python"]} ) - return json(await resp.json()) + return json(resp) @app.route("/jwt") async def enc_dec_jwt(request): resp = await async_client.call("decode_jwt", {"user_id": "a1b2c3"}) - return json(await resp.json()) + return json(resp) @app.route("/echo") async def echo(request): big_list = ["hello world" for i in range(100_000)] resp = await async_client.call("echo", big_list) - return text(await resp.text()) + return text(resp) diff --git a/benchmarks/dockerize/zero/requirements.txt b/benchmarks/dockerize/zero/requirements.txt index b057e9a..78da89c 100644 --- a/benchmarks/dockerize/zero/requirements.txt +++ b/benchmarks/dockerize/zero/requirements.txt @@ -6,4 +6,6 @@ aiohttp gunicorn redis sanic -msgpack \ No newline at end of file +msgpack +uvicorn +blacksheep \ No newline at end of file diff --git a/benchmarks/local/zero/docker-compose.yaml b/benchmarks/local/zero/docker-compose.yaml index 1f9b4b2..0904ba7 100755 --- a/benchmarks/local/zero/docker-compose.yaml +++ b/benchmarks/local/zero/docker-compose.yaml @@ -1,17 +1,28 @@ version: '3' services: - # aiohttp gateway + # blacksheep gateway gateway: build: . container_name: gateway - command: gunicorn gateway_aiohttp:app --bind 0.0.0.0:8000 --worker-class aiohttp.worker.GunicornWebWorker --workers 8 --log-level warning + command: uvicorn gateway_blacksheep:app --host 0.0.0.0 --port 8000 --workers 8 --no-access-log ports: - "8000:8000" depends_on: - server - redis + # aiohttp gateway + # gateway: + # build: . + # container_name: gateway + # command: gunicorn gateway_aiohttp:app --bind 0.0.0.0:8000 --worker-class aiohttp.worker.GunicornWebWorker --workers 8 --log-level warning + # ports: + # - "8000:8000" + # depends_on: + # - server + # - redis + # sanic gateway # gateway: # build: . diff --git a/benchmarks/local/zero/gateway_blacksheep.py b/benchmarks/local/zero/gateway_blacksheep.py new file mode 100644 index 0000000..9b1e8d8 --- /dev/null +++ b/benchmarks/local/zero/gateway_blacksheep.py @@ -0,0 +1,35 @@ +from blacksheep import Application, json, text + +from zero import AsyncZeroClient, ZeroClient + +client = ZeroClient("server", 5559) +async_client = AsyncZeroClient("server", 5559) + +app = Application() +get = app.router.get + + +@get("/hello") +def hello(): + resp = client.call("hello_world", None) + return text(resp) + + +@get("/async_hello") +async def async_hello(): + resp = await async_client.call("hello_world", None) + return text(resp) + + +@get("/order") +def order(): + resp = client.call("save_order", {"user_id": "1", "items": ["apple", "python"]}) + return json(resp) + + +@get("/async_order") +async def async_order(): + resp = await async_client.call( + "save_order", {"user_id": "1", "items": ["apple", "python"]} + ) + return json(resp) diff --git a/benchmarks/local/zero/requirements.txt b/benchmarks/local/zero/requirements.txt index 3f22eee..8526f03 100644 --- a/benchmarks/local/zero/requirements.txt +++ b/benchmarks/local/zero/requirements.txt @@ -7,4 +7,6 @@ aiohttp gunicorn redis sanic -msgpack \ No newline at end of file +msgpack +blacksheep +uvicorn \ No newline at end of file