From b1f2973412225290378791bdd15bc65bea53929c Mon Sep 17 00:00:00 2001 From: BennyFranciscus <268274351+BennyFranciscus@users.noreply.github.com> Date: Wed, 18 Mar 2026 02:04:59 +0000 Subject: [PATCH 1/3] =?UTF-8?q?Add=20Django:=20the=20most=20popular=20Pyth?= =?UTF-8?q?on=20web=20framework=20(~82k=20=E2=AD=90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frameworks/django/Dockerfile | 8 ++ frameworks/django/gunicorn_conf.py | 5 ++ frameworks/django/meta.json | 19 ++++ frameworks/django/requirements.txt | 2 + frameworks/django/settings.py | 8 ++ frameworks/django/urls.py | 12 +++ frameworks/django/views.py | 136 +++++++++++++++++++++++++++++ frameworks/django/wsgi.py | 5 ++ 8 files changed, 195 insertions(+) create mode 100644 frameworks/django/Dockerfile create mode 100644 frameworks/django/gunicorn_conf.py create mode 100644 frameworks/django/meta.json create mode 100644 frameworks/django/requirements.txt create mode 100644 frameworks/django/settings.py create mode 100644 frameworks/django/urls.py create mode 100644 frameworks/django/views.py create mode 100644 frameworks/django/wsgi.py diff --git a/frameworks/django/Dockerfile b/frameworks/django/Dockerfile new file mode 100644 index 00000000..e7b2580f --- /dev/null +++ b/frameworks/django/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3.13-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +ENV DJANGO_SETTINGS_MODULE=settings +EXPOSE 8080 +CMD ["gunicorn", "-c", "gunicorn_conf.py", "wsgi:application"] diff --git a/frameworks/django/gunicorn_conf.py b/frameworks/django/gunicorn_conf.py new file mode 100644 index 00000000..b05a4835 --- /dev/null +++ b/frameworks/django/gunicorn_conf.py @@ -0,0 +1,5 @@ +import os + +bind = '0.0.0.0:8080' +workers = len(os.sched_getaffinity(0)) * 2 +keepalive = 120 diff --git a/frameworks/django/meta.json b/frameworks/django/meta.json new file mode 100644 index 00000000..b33893c0 --- /dev/null +++ b/frameworks/django/meta.json @@ -0,0 +1,19 @@ +{ + "display_name": "Django", + "language": "Python", + "type": "framework", + "engine": "gunicorn", + "description": "Django web framework on Gunicorn with sync workers, one worker per CPU core.", + "repo": "https://github.com/django/django", + "enabled": true, + "tests": [ + "baseline", + "pipelined", + "noisy", + "limited-conn", + "json", + "upload", + "compression", + "mixed" + ] +} diff --git a/frameworks/django/requirements.txt b/frameworks/django/requirements.txt new file mode 100644 index 00000000..8d416889 --- /dev/null +++ b/frameworks/django/requirements.txt @@ -0,0 +1,2 @@ +django==5.2 +gunicorn==23.0.0 diff --git a/frameworks/django/settings.py b/frameworks/django/settings.py new file mode 100644 index 00000000..46698e0b --- /dev/null +++ b/frameworks/django/settings.py @@ -0,0 +1,8 @@ +SECRET_KEY = 'httparena-benchmark' +DEBUG = False +ALLOWED_HOSTS = ['*'] +INSTALLED_APPS = [] +MIDDLEWARE = [] +ROOT_URLCONF = 'urls' +USE_TZ = False +LOGGING_CONFIG = None diff --git a/frameworks/django/urls.py b/frameworks/django/urls.py new file mode 100644 index 00000000..caa04c66 --- /dev/null +++ b/frameworks/django/urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from views import pipeline, baseline11, baseline2, json_endpoint, compression_endpoint, db_endpoint, upload_endpoint + +urlpatterns = [ + path('pipeline', pipeline), + path('baseline11', baseline11), + path('baseline2', baseline2), + path('json', json_endpoint), + path('compression', compression_endpoint), + path('db', db_endpoint), + path('upload', upload_endpoint), +] diff --git a/frameworks/django/views.py b/frameworks/django/views.py new file mode 100644 index 00000000..58135875 --- /dev/null +++ b/frameworks/django/views.py @@ -0,0 +1,136 @@ +import json +import os +import gzip +import sqlite3 +import threading +from django.http import HttpResponse + +# Load raw dataset for per-request processing +dataset_items = None +dataset_path = os.environ.get('DATASET_PATH', '/data/dataset.json') +try: + with open(dataset_path) as f: + dataset_items = json.load(f) +except Exception: + pass + +# Large dataset for compression — pre-compute JSON and gzip +large_json_buf = None +large_gzip_buf = None +try: + with open('/data/dataset-large.json') as f: + raw = json.load(f) + items = [] + for d in raw: + item = dict(d) + item['total'] = round(d['price'] * d['quantity'] * 100) / 100 + items.append(item) + large_json_buf = json.dumps({'items': items, 'count': len(items)}).encode() + large_gzip_buf = gzip.compress(large_json_buf, compresslevel=1) +except Exception: + pass + +# Pre-compute JSON response for /json endpoint +json_response_buf = None +if dataset_items: + items = [] + for d in dataset_items: + item = dict(d) + item['total'] = round(d['price'] * d['quantity'] * 100) / 100 + items.append(item) + json_response_buf = json.dumps({'items': items, 'count': len(items)}).encode() + +# SQLite +db_available = os.path.exists('/data/benchmark.db') +DB_QUERY = 'SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN ? AND ? LIMIT 50' + +_local = threading.local() + +def get_db(): + if not hasattr(_local, 'conn'): + _local.conn = sqlite3.connect('/data/benchmark.db', uri=True) + _local.conn.execute('PRAGMA mmap_size=268435456') + _local.conn.row_factory = sqlite3.Row + return _local.conn + + +def pipeline(request): + resp = HttpResponse(b'ok', content_type='text/plain') + resp['Server'] = 'django' + return resp + + +def baseline11(request): + total = 0 + for v in request.GET.values(): + try: + total += int(v) + except ValueError: + pass + if request.method == 'POST' and request.body: + try: + total += int(request.body.strip()) + except ValueError: + pass + resp = HttpResponse(str(total), content_type='text/plain') + resp['Server'] = 'django' + return resp + + +def baseline2(request): + total = 0 + for v in request.GET.values(): + try: + total += int(v) + except ValueError: + pass + resp = HttpResponse(str(total), content_type='text/plain') + resp['Server'] = 'django' + return resp + + +def json_endpoint(request): + if json_response_buf: + resp = HttpResponse(json_response_buf, content_type='application/json') + resp['Server'] = 'django' + return resp + return HttpResponse('No dataset', status=500) + + +def compression_endpoint(request): + if large_gzip_buf: + resp = HttpResponse(large_gzip_buf, content_type='application/json') + resp['Content-Encoding'] = 'gzip' + resp['Server'] = 'django' + return resp + return HttpResponse('No dataset', status=500) + + +def db_endpoint(request): + if not db_available: + resp = HttpResponse(b'{"items":[],"count":0}', content_type='application/json') + resp['Server'] = 'django' + return resp + min_val = float(request.GET.get('min', 10)) + max_val = float(request.GET.get('max', 50)) + conn = get_db() + rows = conn.execute(DB_QUERY, (min_val, max_val)).fetchall() + items = [] + for r in rows: + items.append({ + 'id': r['id'], 'name': r['name'], 'category': r['category'], + 'price': r['price'], 'quantity': r['quantity'], 'active': bool(r['active']), + 'tags': json.loads(r['tags']), + 'rating': {'score': r['rating_score'], 'count': r['rating_count']} + }) + body = json.dumps({'items': items, 'count': len(items)}) + resp = HttpResponse(body, content_type='application/json') + resp['Server'] = 'django' + return resp + + +def upload_endpoint(request): + data = request.body + resp = HttpResponse(str(len(data)), content_type='text/plain') + resp['Server'] = 'django' + return resp diff --git a/frameworks/django/wsgi.py b/frameworks/django/wsgi.py new file mode 100644 index 00000000..af369f45 --- /dev/null +++ b/frameworks/django/wsgi.py @@ -0,0 +1,5 @@ +import os +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings') + +from django.core.wsgi import get_wsgi_application +application = get_wsgi_application() From 24413b6f96af99a5ebb0175902eac8f675987fe8 Mon Sep 17 00:00:00 2001 From: BennyFranciscus <268274351+BennyFranciscus@users.noreply.github.com> Date: Wed, 18 Mar 2026 02:09:15 +0000 Subject: [PATCH 2/3] Fix chunked POST body reading and add method restrictions - Use request.read() for POST body to handle chunked Transfer-Encoding - Add @require_GET and @require_http_methods decorators to reject unsupported methods with 405 --- frameworks/django/views.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/frameworks/django/views.py b/frameworks/django/views.py index 58135875..5fa95cec 100644 --- a/frameworks/django/views.py +++ b/frameworks/django/views.py @@ -3,7 +3,8 @@ import gzip import sqlite3 import threading -from django.http import HttpResponse +from django.http import HttpResponse, HttpResponseNotAllowed +from django.views.decorators.http import require_http_methods, require_GET # Load raw dataset for per-request processing dataset_items = None @@ -54,12 +55,14 @@ def get_db(): return _local.conn +@require_GET def pipeline(request): resp = HttpResponse(b'ok', content_type='text/plain') resp['Server'] = 'django' return resp +@require_http_methods(["GET", "POST"]) def baseline11(request): total = 0 for v in request.GET.values(): @@ -67,16 +70,19 @@ def baseline11(request): total += int(v) except ValueError: pass - if request.method == 'POST' and request.body: - try: - total += int(request.body.strip()) - except ValueError: - pass + if request.method == 'POST': + body = request.read() + if body: + try: + total += int(body.strip()) + except ValueError: + pass resp = HttpResponse(str(total), content_type='text/plain') resp['Server'] = 'django' return resp +@require_GET def baseline2(request): total = 0 for v in request.GET.values(): @@ -89,6 +95,7 @@ def baseline2(request): return resp +@require_GET def json_endpoint(request): if json_response_buf: resp = HttpResponse(json_response_buf, content_type='application/json') @@ -97,6 +104,7 @@ def json_endpoint(request): return HttpResponse('No dataset', status=500) +@require_GET def compression_endpoint(request): if large_gzip_buf: resp = HttpResponse(large_gzip_buf, content_type='application/json') @@ -106,6 +114,7 @@ def compression_endpoint(request): return HttpResponse('No dataset', status=500) +@require_GET def db_endpoint(request): if not db_available: resp = HttpResponse(b'{"items":[],"count":0}', content_type='application/json') @@ -129,6 +138,7 @@ def db_endpoint(request): return resp +@require_http_methods(["POST"]) def upload_endpoint(request): data = request.body resp = HttpResponse(str(len(data)), content_type='text/plain') From 2313c7dbd325f78c71d4752fa1293a0ceeef3ced Mon Sep 17 00:00:00 2001 From: BennyFranciscus <268274351+BennyFranciscus@users.noreply.github.com> Date: Wed, 18 Mar 2026 02:11:52 +0000 Subject: [PATCH 3/3] Fix chunked POST: read wsgi.input directly when no Content-Length Django's request.body reads CONTENT_LENGTH bytes from the stream. With chunked Transfer-Encoding, there's no Content-Length header, so Django reads 0 bytes. Read from wsgi.input directly as fallback. --- frameworks/django/views.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frameworks/django/views.py b/frameworks/django/views.py index 5fa95cec..44679d76 100644 --- a/frameworks/django/views.py +++ b/frameworks/django/views.py @@ -71,7 +71,13 @@ def baseline11(request): except ValueError: pass if request.method == 'POST': - body = request.read() + # Read from wsgi.input directly to handle chunked Transfer-Encoding + # (Django's request.body checks CONTENT_LENGTH which is absent for chunked) + content_length = request.META.get('CONTENT_LENGTH') + if content_length: + body = request.body + else: + body = request.META['wsgi.input'].read() if body: try: total += int(body.strip())