瀏覽代碼

Added a benchmark test for the bareASGI Python ASGI web framework (#7623)

* Added a benchmark test for the bareASGI Python ASGI web framework

* Added faster json encoder to handler

Co-authored-by: Rob Blackbourn <[email protected]>
Rob Blackbourn 2 年之前
父節點
當前提交
b8f4703f26

+ 22 - 0
frameworks/Python/bareasgi/README.md

@@ -0,0 +1,22 @@
+# bareASGI Benchmark Test
+
+This is the bareASGI portion of a [benchmarking tests suite](../../)
+comparing a variety of web development platforms.
+
+The information below is specific to bareASGI. For further guidance,
+review the [documentation](https://github.com/TechEmpower/FrameworkBenchmarks/wiki).
+Also note that there is additional information provided in
+the [Python README](../).
+
+## Description
+
+[bareASGI](https://github.com/rob-blackbourn/bareASGI) is a lightweight ASGI web server framework.
+
+## Test Paths & Sources
+
+All of the test implementations are located within a single file ([app.py](app.py)).
+
+## Resources
+
+* [bareASGI on GitHub](https://github.com/rob-blackbourn/bareASGI)
+* [ASGI specification](https://asgi.readthedocs.io/en/latest/)

+ 136 - 0
frameworks/Python/bareasgi/app.py

@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+from random import randint, sample
+import os
+import os.path
+from typing import Any, Dict, List, Tuple
+from urllib.parse import parse_qs
+
+import asyncpg
+import jinja2
+import orjson
+
+from bareasgi import Application, HttpRequest, HttpResponse, LifespanRequest
+from bareasgi_jinja2 import add_jinja2, Jinja2TemplateProvider
+
+GET_WORLD = "SELECT id, randomnumber FROM world WHERE id = $1"
+UPDATE_WORLD = "UPDATE world SET randomNumber = $2 WHERE id = $1"
+GET_FORTUNES = "SELECT * FROM fortune"
+ADDITIONAL_ROW = (0, "Additional fortune added at request time.")
+
+
+async def on_startup(_request: LifespanRequest) -> None:
+    app.info['db'] = await asyncpg.create_pool(
+        user=os.getenv("PGUSER", "benchmarkdbuser"),
+        password=os.getenv("PGPASS", "benchmarkdbpass"),
+        database="hello_world",
+        host="tfb-database",
+        port=5432,
+    )
+
+
+async def on_shutdown(_request: LifespanRequest) -> None:
+    await app.info['db'].close()
+
+
+async def handle_json_request(_request: HttpRequest) -> HttpResponse:
+    return HttpResponse.from_json(
+        {"message": "Hello, World!"},
+        encode_bytes=orjson.dumps
+    )
+
+
+async def handle_plaintext_request(_request: HttpRequest) -> HttpResponse:
+    return HttpResponse.from_text("Hello, World!")
+
+
+async def handle_db_request(_request: HttpRequest) -> HttpResponse:
+    key = randint(1, 10000)
+
+    async with app.info['db'].acquire() as conn:
+        number = await conn.fetchval(GET_WORLD, key)
+
+    return HttpResponse.from_json(
+        {"id": key, "randomNumber": number},
+        encode_bytes=orjson.dumps
+    )
+
+
+def get_query_count(request: HttpRequest):
+    try:
+        qs = parse_qs(request.scope["query_string"])
+        num_queries = int(qs.get(b'queries', (1, ))[0])
+    except ValueError:
+        num_queries = 1
+    if num_queries < 1:
+        return 1
+    if num_queries > 500:
+        return 500
+        
+    return num_queries
+
+
+async def handle_queries_request(request: HttpRequest) -> HttpResponse:
+    queries = get_query_count(request)
+
+    worlds: List[Dict[str, Any]] = []
+    async with app.info['db'].acquire() as conn:
+        pst = await conn.prepare(GET_WORLD)
+        for key in sample(range(1, 10000), queries):
+            number = await pst.fetchval(key)
+            worlds.append({"id": key, "randomNumber": number})
+
+    return HttpResponse.from_json(
+        worlds,
+        encode_bytes=orjson.dumps
+    )
+
+
+async def handle_updates_request(request: HttpRequest) -> HttpResponse:
+    queries = get_query_count(request)
+    updates = [(row_id, randint(1, 10000)) for row_id in sample(range(1, 10000), queries)]
+    updates.sort()
+    worlds = [{'id': row_id, 'randomNumber': number} for row_id, number in updates]
+
+    async with app.info['db'].acquire() as connection:
+        statement = await connection.prepare(GET_WORLD)
+        for row_id, number in updates:
+            await statement.fetchval(row_id)
+        await connection.executemany(UPDATE_WORLD, updates)
+
+    return HttpResponse.from_json(
+        worlds,
+        encode_bytes=orjson.dumps
+    )
+
+async def handle_fortunes_request(request: HttpRequest) -> HttpResponse:
+    async with app.info['db'].acquire() as conn:
+        rows = await conn.fetch(GET_FORTUNES)
+    rows.append(ADDITIONAL_ROW)
+    rows.sort(key=lambda row: row[1])
+
+    return await Jinja2TemplateProvider.apply(
+        request,
+        "fortune.html",
+        { "fortunes": rows }
+    )
+
+app = Application(
+    startup_handlers=[on_startup],
+    shutdown_handlers=[on_shutdown]
+)
+
+here = os.path.abspath(os.path.dirname(__file__))
+env = jinja2.Environment(
+    loader=jinja2.FileSystemLoader(os.path.join(here, 'templates')),
+    autoescape=jinja2.select_autoescape(['html', 'xml']),
+    enable_async=True
+)
+
+add_jinja2(app, env)
+
+app.http_router.add({"GET"}, "/json", handle_json_request)
+app.http_router.add({"GET"}, "/plaintext", handle_plaintext_request)
+app.http_router.add({"GET"}, "/db", handle_db_request)
+app.http_router.add({"GET"}, "/queries", handle_queries_request)
+app.http_router.add({"GET"}, "/updates", handle_updates_request)
+app.http_router.add({"GET"}, "/fortunes", handle_fortunes_request)

+ 11 - 0
frameworks/Python/bareasgi/bareasgi.dockerfile

@@ -0,0 +1,11 @@
+FROM python:3.10
+
+ADD ./ /bareasgi
+
+WORKDIR /bareasgi
+
+RUN pip install -r /bareasgi/requirements.txt
+
+EXPOSE 8080
+
+CMD hypercorn app:app --config=file:hypercorn_conf.py

+ 30 - 0
frameworks/Python/bareasgi/benchmark_config.json

@@ -0,0 +1,30 @@
+{
+  "framework": "bareasgi",
+  "tests": [
+    {
+      "default": {
+        "json_url": "/json",
+        "plaintext_url": "/plaintext",
+        "fortune_url": "/fortunes",
+        "db_url": "/db",
+        "query_url": "/queries?queries=",
+        "update_url": "/updates?queries=",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Micro",
+        "database": "Postgres",
+        "framework": "bareasgi",
+        "language": "Python",
+        "flavor": "Python3",
+        "orm": "Raw",
+        "platform": "asyncio",
+        "webserver": "Hypercorn",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "bareASGI",
+        "notes": "",
+        "versus": "None"
+      }
+    }
+  ]
+}

+ 19 - 0
frameworks/Python/bareasgi/config.toml

@@ -0,0 +1,19 @@
+[framework]
+name = "bareasgi"
+
+[main]
+urls.plaintext = "/plaintext"
+urls.json = "/json"
+urls.db = "/db"
+urls.query = "/queries?queries="
+urls.update = "/updates?queries="
+urls.fortune = "/fortunes"
+approach = "Realistic"
+classification = "Micro"
+database = "Postgres"
+database_os = "Linux"
+os = "Linux"
+orm = "Raw"
+platform = "None"
+webserver = "None"
+versus = "None"

+ 13 - 0
frameworks/Python/bareasgi/hypercorn_conf.py

@@ -0,0 +1,13 @@
+import multiprocessing
+import os
+
+_is_travis = os.environ.get('TRAVIS') == 'true'
+
+workers = multiprocessing.cpu_count()
+if _is_travis:
+    workers = 2
+
+bind = ["0.0.0.0:8080"]
+keep_alive_timeout = 120
+loglevel = "error"
+worker_class = "uvloop"

+ 17 - 0
frameworks/Python/bareasgi/requirements.txt

@@ -0,0 +1,17 @@
+asyncpg==0.26.0
+bareasgi==4.3.0
+bareasgi-jinja2==4.0.1
+bareutils==4.0.2
+h11==0.14.0
+h2==4.1.0
+hpack==4.0.0
+hypercorn==0.14.3
+hyperframe==6.0.1
+jetblack-asgi-typing==0.4.0
+Jinja2==3.1.2
+MarkupSafe==2.1.1
+orjson==3.8.0
+priority==2.0.0
+toml==0.10.2
+uvloop==0.17.0
+wsproto==1.2.0

+ 10 - 0
frameworks/Python/bareasgi/templates/fortune.html

@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head><title>Fortunes</title></head>
+<body>
+<table>
+<tr><th>id</th><th>message</th></tr>
+{% for fortune in fortunes %}<tr><td>{{ fortune[0] }}</td><td>{{ fortune[1]|e }}</td></tr>
+{% endfor %}</table>
+</body>
+</html>