datasette/perf/request-response/tools/serve_variant.py
Claude 2b2f896ab2
Add end-to-end server results to the performance report
Real datasette serve runs (uvicorn, keep-alive load, 5 interleaved rounds)
comparing cf113a5 with the combined patches, with and without httptools
and the access log. Adds the load generator and server harness.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8mEgRS8swZfYqwqrVMeo5
2026-10-13 01:28:50 +00:00

146 lines
4.6 KiB
Python

"""
Run exactly `datasette serve ...` (datasette.cli.cli), but let environment
variables tweak how uvicorn is configured, WITHOUT editing datasette source:
E2E_HTTP=h11|httptools|auto -> uvicorn.Config(http=...)
E2E_ACCESS_LOG=0 -> uvicorn.Config(access_log=False)
E2E_LOOP=uvloop -> asyncio.run(..., loop_factory=uvloop.new_event_loop)
E2E_GC_FREEZE=1 -> gc.collect(); gc.freeze() just before uvicorn serves
E2E_SWITCH=0.0005 -> sys.setswitchinterval(0.0005) (GIL convoy experiment)
E2E_CONTENT_LENGTH=1 -> (experiment) Response.asgi_send adds a
content-length header, so h11 uses
Content-Length framing instead of chunked
E2E_COALESCE=1 -> (experiment) uvicorn h11 cycle buffers the
status line+headers and writes them together
with the body in one transport write
Usage: python serve_variant.py serve fixtures.db -p 8801 --secret s ...
"""
import asyncio
import gc
import os
import sys
import uvicorn
_orig_config_init = uvicorn.Config.__init__
def _config_init(self, app, **kw):
http = os.environ.get("E2E_HTTP")
if http:
kw["http"] = http
if os.environ.get("E2E_ACCESS_LOG") == "0":
kw["access_log"] = False
_orig_config_init(self, app, **kw)
uvicorn.Config.__init__ = _config_init
if os.environ.get("E2E_GC_FREEZE") == "1":
_orig_serve = uvicorn.Server.serve
async def _serve(self, sockets=None):
gc.collect()
gc.freeze()
return await _orig_serve(self, sockets)
uvicorn.Server.serve = _serve
if os.environ.get("E2E_LOOP") == "uvloop":
import uvloop
_orig_run = asyncio.run
def _run(main, **kw):
kw.setdefault("loop_factory", uvloop.new_event_loop)
return _orig_run(main, **kw)
asyncio.run = _run
if os.environ.get("E2E_CONTENT_LENGTH") == "1":
from datasette.utils import asgi as _dsasgi
async def _asgi_send(self, send):
headers = {}
headers.update(self.headers)
headers["content-type"] = self.content_type
body = self.body
if not isinstance(body, bytes):
body = body.encode("utf-8")
raw_headers = [
[key.encode("utf-8"), value.encode("utf-8")]
for key, value in headers.items()
]
if not any(k.lower() == "content-length" for k in headers):
raw_headers.append([b"content-length", str(len(body)).encode("latin-1")])
for set_cookie in self._set_cookie_headers:
raw_headers.append([b"set-cookie", set_cookie.encode("utf-8")])
await send(
{
"type": "http.response.start",
"status": self.status,
"headers": raw_headers,
}
)
await send({"type": "http.response.body", "body": body})
_dsasgi.Response.asgi_send = _asgi_send
if os.environ.get("E2E_COALESCE") == "1":
from uvicorn.protocols.http import h11_impl as _h11_impl
class _BufferedTransport:
__slots__ = ("_buf", "_t")
def __init__(self, t):
self._t = t
self._buf = []
def write(self, data):
if data:
self._buf.append(data)
def flush(self):
if self._buf:
buf = self._buf
self._buf = []
self._t.write(buf[0] if len(buf) == 1 else b"".join(buf))
def close(self):
self.flush()
self._t.close()
def __getattr__(self, name):
return getattr(self._t, name)
_orig_cycle_send = _h11_impl.RequestResponseCycle.send
async def _cycle_send(self, message):
t = self.transport
if not isinstance(t, _BufferedTransport):
t = self.transport = _BufferedTransport(t)
await _orig_cycle_send(self, message)
if message["type"] != "http.response.start":
t.flush()
_h11_impl.RequestResponseCycle.send = _cycle_send
if os.environ.get("E2E_STATIC_CHUNK"):
# (experiment) asgi_static(root_path, chunk_size=4096, ...) -> bigger chunks
from datasette.utils import asgi as _dsasgi2
_d = list(_dsasgi2.asgi_static.__defaults__)
_d[0] = int(os.environ["E2E_STATIC_CHUNK"])
_dsasgi2.asgi_static.__defaults__ = tuple(_d)
if os.environ.get("E2E_SWITCH"):
# (experiment) shorter GIL switch interval (default 0.005s)
sys.setswitchinterval(float(os.environ["E2E_SWITCH"]))
from datasette.cli import cli
if __name__ == "__main__":
sys.argv[0] = "datasette"
cli(sys.argv[1:])