Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions caddysnake.c
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ MapKeyVal *MapKeyVal_new(size_t capacity) {
new_map->capacity = capacity;
new_map->keys = malloc(sizeof(char *) * capacity);
new_map->values = malloc(sizeof(char *) * capacity);
new_map->keysLen = malloc(sizeof(int) * capacity);
new_map->valuesLen = malloc(sizeof(int) * capacity);
return new_map;
}

Expand All @@ -108,6 +110,15 @@ static void MapKeyVal_append(MapKeyVal *map, char *key, char *value) {
map->length++;
}

static void MapKeyVal_append_v2(MapKeyVal *map, char *key, int keyLen,
char *value, int valueLen) {
map->keys[map->length] = key;
map->keysLen[map->length] = keyLen;
map->values[map->length] = value;
map->valuesLen[map->length] = valueLen;
map->length++;
}

typedef struct {
PyObject_HEAD WsgiApp *app;
int64_t request_id;
Expand Down Expand Up @@ -605,6 +616,7 @@ void AsgiEvent_set(AsgiEvent *self, const char *body, size_t body_len,
Py_DECREF(self->request_body);
}
self->request_body = PyBytes_FromStringAndSize(body, body_len);
free(body);
}
self->more_body = more_body;
PyObject *set_fn = NULL;
Expand Down Expand Up @@ -725,7 +737,16 @@ static void AsgiEvent_websocket_disconnect(AsgiEvent *self, PyObject *data) {

static PyObject *AsgiEvent_receive_start(AsgiEvent *self, PyObject *args) {
PyObject *result = Py_False;
if (asgi_receive_start(self->request_id, self) == 1) {

size_t cbuf_size = 1 << 13;
char *cbuf = malloc(cbuf_size);

PyThreadState *_save = PyEval_SaveThread();
uint8_t receive_result =
asgi_receive_start(self->request_id, self, cbuf, cbuf_size);
PyEval_RestoreThread(_save);

if (receive_result == 1) {
Py_INCREF(self->event_ts_receive);
result = self->event_ts_receive;
}
Expand Down Expand Up @@ -823,7 +844,8 @@ static void AsgiEvent_response_start(AsgiEvent *self, PyObject *data) {
MapKeyVal *http_headers = MapKeyVal_new(headers_count);

PyObject *key, *value, *item;
size_t len = 0;
size_t keyLen = 0;
size_t valueLen = 0;
while ((item = PyIter_Next(iterator))) {
// if (!PyTuple_Check(item) || PyTuple_Size(item) != 2) {
// PyErr_SetString(PyExc_RuntimeError,
Expand All @@ -837,8 +859,9 @@ static void AsgiEvent_response_start(AsgiEvent *self, PyObject *data) {
// }
key = PyTuple_GetItem(item, 0);
value = PyTuple_GetItem(item, 1);
MapKeyVal_append(http_headers, copy_pybytes(key, &len),
copy_pybytes(value, &len));
char *keyBuf = copy_pybytes(key, &keyLen);
char *valueBuf = copy_pybytes(value, &valueLen);
MapKeyVal_append_v2(http_headers, keyBuf, keyLen, valueBuf, valueLen);
Py_DECREF(item);
}
Py_DECREF(iterator);
Expand All @@ -856,7 +879,10 @@ static void AsgiEvent_write_body(AsgiEvent *self, PyObject *data) {
PyObject *pybody = PyDict_GetItemString(data, "body");
size_t body_len = 0;
char *body = copy_pybytes(pybody, &body_len);

PyThreadState *_save = PyEval_SaveThread();
asgi_send_response(self->request_id, body, body_len, send_more_body, self);
PyEval_RestoreThread(_save);
}

static void AsgiEvent_websocket_accept(AsgiEvent *self, PyObject *data) {
Expand Down
47 changes: 36 additions & 11 deletions caddysnake.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"net/url"
"os"
"path/filepath"
"reflect"
"regexp"
"strconv"
"strings"
Expand All @@ -29,12 +30,16 @@
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/gorilla/websocket"
"go.uber.org/zap"

// debug on Linux
_ "github.com/ianlancetaylor/cgosymbolizer"

Check failure on line 35 in caddysnake.go

View workflow job for this annotation

GitHub Actions / Lint

no required module provides package github.com/ianlancetaylor/cgosymbolizer; to add it:

Check failure on line 35 in caddysnake.go

View workflow job for this annotation

GitHub Actions / tests (3.10)

no required module provides package github.com/ianlancetaylor/cgosymbolizer; to add it:

Check failure on line 35 in caddysnake.go

View workflow job for this annotation

GitHub Actions / tests (3.10)

no required module provides package github.com/ianlancetaylor/cgosymbolizer; to add it:

Check failure on line 35 in caddysnake.go

View workflow job for this annotation

GitHub Actions / tests (3.11)

no required module provides package github.com/ianlancetaylor/cgosymbolizer; to add it:

Check failure on line 35 in caddysnake.go

View workflow job for this annotation

GitHub Actions / tests (3.11)

no required module provides package github.com/ianlancetaylor/cgosymbolizer; to add it:

Check failure on line 35 in caddysnake.go

View workflow job for this annotation

GitHub Actions / tests (3.13)

no required module provides package github.com/ianlancetaylor/cgosymbolizer; to add it:

Check failure on line 35 in caddysnake.go

View workflow job for this annotation

GitHub Actions / tests (3.13)

no required module provides package github.com/ianlancetaylor/cgosymbolizer; to add it:

Check failure on line 35 in caddysnake.go

View workflow job for this annotation

GitHub Actions / tests (3.12)

no required module provides package github.com/ianlancetaylor/cgosymbolizer; to add it:

Check failure on line 35 in caddysnake.go

View workflow job for this annotation

GitHub Actions / tests (3.12)

no required module provides package github.com/ianlancetaylor/cgosymbolizer; to add it:
)

//go:embed caddysnake.py
var caddysnake_py string

var SIZE_OF_CHAR_POINTER = unsafe.Sizeof((*C.char)(nil))
var SIZE_OF_INT_POINTER = unsafe.Sizeof(C.int(0))

// MapKeyVal wraps the same structure defined in the C layer
type MapKeyVal struct {
Expand Down Expand Up @@ -85,7 +90,25 @@
headerValuePtr := unsafe.Pointer(uintptr(unsafe.Pointer(m.m.values)) + uintptr(pos)*SIZE_OF_CHAR_POINTER)
headerName := *(**C.char)(headerNamePtr)
headerValue := *(**C.char)(headerValuePtr)
return C.GoString(headerName), C.GoString(headerValue)

headerNameLenPtr := unsafe.Pointer(uintptr(unsafe.Pointer(m.m.keysLen)) + uintptr(pos)*SIZE_OF_INT_POINTER)
headerValueLenPtr := unsafe.Pointer(uintptr(unsafe.Pointer(m.m.valuesLen)) + uintptr(pos)*SIZE_OF_INT_POINTER)
headerNameLen := int(*(*C.int)(headerNameLenPtr))
headerValueLen := int(*(*C.int)(headerValueLenPtr))

hdr := reflect.StringHeader{
Data: uintptr(unsafe.Pointer(headerName)),
Len: headerNameLen,
}
headerNameStr := *(*string)(unsafe.Pointer(&hdr))

hdr2 := reflect.StringHeader{
Data: uintptr(unsafe.Pointer(headerValue)),
Len: headerValueLen,
}
headerValueStr := *(*string)(unsafe.Pointer(&hdr2))

return headerNameStr, headerValueStr
}

func (m *MapKeyVal) Len() int {
Expand Down Expand Up @@ -1036,21 +1059,20 @@
h.event = event
}

func (h *AsgiRequestHandler) readBody(event *C.AsgiEvent) {
func (h *AsgiRequestHandler) readBody(event *C.AsgiEvent, cbuf *C.char, cbufSize C.size_t) {
var bodyStr *C.char
var bodyLen C.size_t
var moreBody C.uint8_t
if !h.completedBody {
buffer := make([]byte, 1<<16)
buffer := unsafe.Slice((*byte)(unsafe.Pointer(cbuf)), cbufSize)
n, err := h.r.Body.Read(buffer)
if err != nil && err != io.EOF {
h.done <- err
return
}
h.completedBody = (err == io.EOF)
buffer = append(buffer[:n], 0)
bodyStr = (*C.char)(unsafe.Pointer(&buffer[0]))
bodyLen = C.size_t(len(buffer) - 1) // -1 to remove null-terminator
bodyStr = cbuf
bodyLen = C.size_t(n)
}

if h.completedBody {
Expand All @@ -1064,9 +1086,9 @@
})
}

func (h *AsgiRequestHandler) ReceiveStart(event *C.AsgiEvent) C.uint8_t {
func (h *AsgiRequestHandler) ReceiveStart(event *C.AsgiEvent, cbuf *C.char, cbufSize C.size_t) C.uint8_t {
h.operations <- AsgiOperations{op: func() {
h.readBody(event)
h.readBody(event, cbuf, cbufSize)
}}
return C.uint8_t(1)
}
Expand All @@ -1082,6 +1104,7 @@
wsConn, err := upgrader.Upgrade(h.w, h.r, headers)
if err != nil {
h.websocketState = WS_DISCONNECTED
h.websocketConn.Close()
C.AsgiEvent_websocket_set_disconnected(event)
C.AsgiEvent_set(event, nil, 0, C.uint8_t(0), C.uint8_t(1))
return
Expand Down Expand Up @@ -1126,6 +1149,8 @@

h.w.WriteHeader(int(statusCode))

h.w.(http.Flusher).Flush()

pythonMainThread.do(func() {
C.AsgiEvent_set(event, nil, 0, C.uint8_t(0), C.uint8_t(1))
})
Expand All @@ -1135,7 +1160,7 @@
func (h *AsgiRequestHandler) SendResponse(body *C.char, bodyLen C.size_t, moreBody C.uint8_t, event *C.AsgiEvent) {
h.operations <- AsgiOperations{op: func() {
defer C.free(unsafe.Pointer(body))
bodyBytes := C.GoBytes(unsafe.Pointer(body), C.int(bodyLen))
bodyBytes := unsafe.Slice((*byte)(unsafe.Pointer(body)), bodyLen)
h.accumulatedResponseSize += len(bodyBytes)
_, err := h.w.Write(bodyBytes)
if f, ok := h.w.(http.Flusher); ok {
Expand Down Expand Up @@ -1210,7 +1235,7 @@
}

//export asgi_receive_start
func asgi_receive_start(requestID C.uint64_t, event *C.AsgiEvent) C.uint8_t {
func asgi_receive_start(requestID C.uint64_t, event *C.AsgiEvent, cbuf *C.char, cbufSize C.size_t) C.uint8_t {
h := asgiState.GetHandler(uint64(requestID))
if h == nil || h.completedResponse {
return C.uint8_t(0)
Expand All @@ -1221,7 +1246,7 @@
return h.HandleWebsocket(event)
}

return h.ReceiveStart(event)
return h.ReceiveStart(event, cbuf, cbufSize)
}

//export asgi_set_headers
Expand Down
4 changes: 3 additions & 1 deletion caddysnake.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ typedef struct {
size_t capacity;
char **keys;
char **values;
int *keysLen;
int *valuesLen;
} MapKeyVal;
MapKeyVal *MapKeyVal_new(size_t);
void MapKeyVal_free(MapKeyVal *map);
Expand Down Expand Up @@ -43,7 +45,7 @@ void AsgiEvent_websocket_set_disconnected(AsgiEvent *);
void AsgiEvent_cleanup(AsgiEvent *);
void AsgiApp_cleanup(AsgiApp *);

extern uint8_t asgi_receive_start(uint64_t, AsgiEvent *);
extern uint8_t asgi_receive_start(uint64_t, AsgiEvent *, char *, size_t);
extern void asgi_send_response(uint64_t, char *, size_t, uint8_t, AsgiEvent *);
extern void asgi_send_response_websocket(uint64_t, char *, size_t, uint8_t,
AsgiEvent *);
Expand Down
17 changes: 17 additions & 0 deletions tests/benchmarks/Caddyfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
http_port 9080
admin off
auto_https off
log {
level info
}
}
:9080 {
route /* {
python {
module_asgi "main:app"
venv "./venv"
lifespan on
}
}
}
23 changes: 23 additions & 0 deletions tests/benchmarks/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
FROM python:3.13-bullseye

# Install go version 1.24
RUN apt-get update && \
apt-get install -y wget bc wrk git && \
wget https://go.dev/dl/go1.24.0.linux-arm64.tar.gz && \
tar -C /usr/local -xzf go1.24.0.linux-arm64.tar.gz && \
rm go1.24.0.linux-arm64.tar.gz &&\
pip install fastapi uvicorn hypercorn granian asyncpg pydantic

ENV PATH=$PATH:/usr/local/go/bin
ENV GODEBUG=cgocheck=0

RUN go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest

COPY . /caddy-snake

RUN cd /caddy-snake &&\
# git apply tests/benchmarks/caddysnake.patch &&\
cd /usr/local/bin &&\
CGO_ENABLED=1 /root/go/bin/xcaddy build --with github.com/mliezun/caddy-snake=/caddy-snake

WORKDIR /caddy-snake/tests/benchmarks
50 changes: 50 additions & 0 deletions tests/benchmarks/benchmark.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/bin/bash

set -euo pipefail

HOST=localhost
DURATION=20s
THREADS=2
CONNECTIONS=20
RESULTS_FILE="results.txt"


function run_benchmark() {
SERVER_CMD="$1"
SERVER_NAME="$2"
PORT="$3"
URL="http://$HOST:$PORT/pastes"
LOG_FILE="${SERVER_NAME}_log.txt"
echo "=== Starting $SERVER_NAME ===" | tee -a "$RESULTS_FILE"
# Start server in background
eval "$SERVER_CMD" > "$LOG_FILE" 2>&1 &
# Wait for server to be ready
sleep 10
for i in {1..30}; do
if curl -s "$URL" -o /dev/null; then
break
fi
sleep 1
done
echo "Running wrk benchmark on $SERVER_NAME..." | tee -a "$RESULTS_FILE"
wrk -t$THREADS -c$CONNECTIONS -d$DURATION -s "wrk_post.lua" "$URL" | tee -a "$RESULTS_FILE"
# Find and kill the server process by command line
ps aux | grep "$SERVER_CMD" | grep -v grep | awk '{print $2}' | xargs -r kill
echo "=== Finished $SERVER_NAME ===" | tee -a "$RESULTS_FILE"
}

# Clean previous results
rm -f "$RESULTS_FILE"

run_benchmark "./caddy run --config Caddyfile" "caddy" 9080
run_benchmark "uvicorn main:app --host 0.0.0.0 --port 9081" "uvicorn" 9081
run_benchmark "hypercorn main:app --bind 0.0.0.0:9082" "hypercorn" 9082
run_benchmark "granian --interface asgi --host 0.0.0.0 --port 9083 main:app" "granian" 9083

echo "=== Benchmark Summary ===" | tee -a "$RESULTS_FILE"
grep -E 'Running wrk benchmark|Requests/sec|Latency' "$RESULTS_FILE" | tee -a "$RESULTS_FILE"

# Print best performer
BEST=$(grep 'Requests/sec' "$RESULTS_FILE" | awk '{print $2" "$3}' | sort -nr | head -1)
BEST_SERVER=$(grep -B2 "$BEST" "$RESULTS_FILE" | head -1 | awk '{print $3}')
echo -e "Best performer: $BEST_SERVER with $BEST requests/sec" | tee -a "$RESULTS_FILE"
32 changes: 32 additions & 0 deletions tests/benchmarks/benchmark_v2.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/bin/bash

set -euo pipefail

SIZE=$1

HOST="[2001:bc8:1210:6e9e:dc00:ff:fe7a:770f]"
DURATION=20s
THREADS=2
CONNECTIONS=20
RESULTS_FILE="results.txt"


function run_benchmark() {
SERVER_NAME="$1"
PORT="$2"
URL="http://$HOST:$PORT/pastes"
LOG_FILE="${SERVER_NAME}_log.txt"
echo "Running wrk benchmark on $SERVER_NAME..." | tee -a "$RESULTS_FILE"
wrk -t$THREADS -c$CONNECTIONS -d$DURATION -s "wrk_post_$SIZE.lua" "$URL" | tee -a "$RESULTS_FILE"
}

# Clean previous results
rm -f "$RESULTS_FILE"

run_benchmark "caddy" 9080
run_benchmark "uvicorn" 9081
run_benchmark "hypercorn" 9082
run_benchmark "granian" 9083

echo "=== Benchmark Summary ===" | tee -a "$RESULTS_FILE"
grep -E 'Running wrk benchmark|Requests/sec|Latency' "$RESULTS_FILE" | tee -a "$RESULTS_FILE"
29 changes: 29 additions & 0 deletions tests/benchmarks/caddysnake.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
diff --git a/caddysnake.go b/caddysnake.go
index bf24510..63f73af 100644
--- a/caddysnake.go
+++ b/caddysnake.go
@@ -16,6 +16,7 @@ import (
"os"
"path/filepath"
"regexp"
+ "runtime/pprof"
"strconv"
"strings"
"sync"
@@ -169,6 +170,8 @@ func (CaddySnake) CaddyModule() caddy.ModuleInfo {

// Provision sets up the module.
func (f *CaddySnake) Provision(ctx caddy.Context) error {
+ cpu_prof, _ := os.Create("cpu_profile.pprof")
+ pprof.StartCPUProfile(cpu_prof)
f.logger = ctx.Logger(f)
if f.ModuleWsgi != "" {
w, err := NewWsgi(f.ModuleWsgi, f.WorkingDir, f.VenvPath)
@@ -200,6 +203,7 @@ func (m *CaddySnake) Validate() error {

// Cleanup frees resources uses by module
func (m *CaddySnake) Cleanup() error {
+ defer pprof.StopCPUProfile()
if m != nil && m.app != nil {
m.logger.Info("cleaning up module")
return m.app.Cleanup()
Loading
Loading