diff --git a/frameworks/apex-tuned/Dockerfile b/frameworks/apex-tuned/Dockerfile new file mode 100644 index 000000000..2435ddef4 --- /dev/null +++ b/frameworks/apex-tuned/Dockerfile @@ -0,0 +1,21 @@ +FROM ubuntu:24.04 AS build +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential gcc make ca-certificates libc6-dev libssl-dev libcjson-dev libbrotli-dev zlib1g-dev pkg-config \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY Makefile ./ +COPY src/ ./src/ + +RUN make -j$(nproc) + +FROM ubuntu:24.04 +RUN apt-get update && apt-get install -y --no-install-recommends \ + libcjson1 libbrotli1 zlib1g libssl3 ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=build /app/server /server + +EXPOSE 8080 8081 + +ENTRYPOINT ["/server"] diff --git a/frameworks/apex-tuned/Makefile b/frameworks/apex-tuned/Makefile new file mode 100644 index 000000000..4ad4057d3 --- /dev/null +++ b/frameworks/apex-tuned/Makefile @@ -0,0 +1,21 @@ +CC ?= gcc +CFLAGS ?= -O3 -march=native -flto -Wall -Wextra -std=gnu2x -D_GNU_SOURCE +LDFLAGS ?= -flto +LIBS = -lssl -lcrypto -lbrotlienc -lbrotlicommon -lz -lcjson -lpthread + +SRCS = src/main.c src/http.c src/router.c src/dataset.c src/timer.c src/tls.c src/static_files.c +OBJS = $(SRCS:.c=.o) +TARGET = server + +all: $(TARGET) + +$(TARGET): $(OBJS) + $(CC) $(LDFLAGS) -o $@ $^ $(LIBS) + +%.o: %.c + $(CC) $(CFLAGS) -c $< -o $@ + +clean: + rm -f $(OBJS) $(TARGET) + +.PHONY: all clean diff --git a/frameworks/apex-tuned/README.md b/frameworks/apex-tuned/README.md new file mode 100644 index 000000000..3ec47ee58 --- /dev/null +++ b/frameworks/apex-tuned/README.md @@ -0,0 +1,22 @@ +# apex-tuned + +High-performance HTTP/1.1 framework in C23 designed for many-core NUMA architectures. + +## Architecture + +- **Runtime**: Thread-per-core shared-nothing event loop using Linux `epoll`. Workers pin to dedicated CPUs via `pthread_setaffinity_np` with per-worker `SO_REUSEPORT` listeners on ports 8080 and 8081. +- **Memory**: Slab-allocated connection contexts and pre-sized I/O rings. Zero dynamic allocations in the request-response path. +- **Parser**: Streaming state-machine HTTP/1.1 parser resilient to byte-level TCP fragmentation, supporting pipelining, chunked transfer decoding, and lower-cased header variants. +- **Routing & Middleware**: Trie router with route parameter extraction (`:ms`, `:count`) and query string slicing; middleware chain for compression negotiation. +- **Compression**: Brotli (level 1) and gzip encoding using pre-allocated worker compression contexts. +- **TLS**: OpenSSL 3 TLS 1.3 with session caching and zero-copy body echoing on `:8081`. + +## Endpoints + +- `GET /baseline11?a=X&b=Y`: Sums query parameters +- `POST /baseline11?a=X&b=Y`: Sums query parameters and body (Content-Length or chunked) +- `GET /pipeline`: Pipelining verification (`ok`) +- `GET /delay/{ms}`: Non-blocking millisecond timer delay +- `GET /json/{count}?m=N`: Transforms dataset items with `total = price * quantity * m`, compressed per `Accept-Encoding` +- `POST /echo`: Byte-for-byte body echo on port 8081 (TLS) +- `GET /static/{file}`: Static files from `/data/static`, serving `.br`/`.gz` sibling twins when accepted diff --git a/frameworks/apex-tuned/meta.json b/frameworks/apex-tuned/meta.json new file mode 100644 index 000000000..92f1e1059 --- /dev/null +++ b/frameworks/apex-tuned/meta.json @@ -0,0 +1,25 @@ +{ + "display_name": "apex-tuned", + "language": "C", + "type": "engine", + "engine": "apex", + "description": "Apex HTTP engine in C23: shared-nothing thread-per-core epoll runtime, CPU affinity pinning, zero-copy buffer recycling, streaming HTTP parser with TCP fragmentation resilience, route parameter trie, middleware pipeline, Brotli compression, and tuned TLS 1.3.", + "repo": "https://github.com/xdearboy/apex", + "enabled": true, + "tests": [ + "baseline", + "limited-conn", + "async", + "latency-1m", + "latency-10k", + "latency-500k-8cpu", + "pipelined", + "json-comp", + "json-tls", + "8gbit", + "static-tls" + ], + "maintainers": [ + "xdearboy" + ] +} diff --git a/frameworks/apex-tuned/src/common.h b/frameworks/apex-tuned/src/common.h new file mode 100644 index 000000000..84732cc7e --- /dev/null +++ b/frameworks/apex-tuned/src/common.h @@ -0,0 +1,97 @@ +#pragma once + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_WORKERS 128 +#define MAX_EVENTS 1024 +#define MAX_CONNS_PER_WORKER 65536 +#define READ_BUF_SIZE (128 * 1024) +#define WRITE_BUF_SIZE (512 * 1024) +#define MAX_HEADERS 64 +#define MAX_PARAMS 16 +#define MAX_STATIC_FILES 128 + +typedef struct { + const char *ptr; + size_t len; +} str_view_t; + +static inline bool view_eq(str_view_t v, const char *s) { + size_t slen = strlen(s); + return v.len == slen && memcmp(v.ptr, s, slen) == 0; +} + +static inline bool view_case_eq(str_view_t v, const char *s) { + size_t slen = strlen(s); + return v.len == slen && strncasecmp(v.ptr, s, slen) == 0; +} + +static inline bool parse_i64(const char *s, size_t len, int64_t *out) { + if (!len) return false; + size_t i = 0; + int neg = 0; + if (s[0] == '-') { neg = 1; i = 1; } + else if (s[0] == '+') { i = 1; } + if (i >= len) return false; + int64_t v = 0; + for (; i < len; i++) { + unsigned char c = (unsigned char)s[i]; + if (c < '0' || c > '9') return false; + v = v * 10 + (c - '0'); + } + *out = neg ? -v : v; + return true; +} + +static inline size_t format_i64(char *buf, int64_t v) { + char tmp[24]; + size_t t = 0; + uint64_t u = (v < 0) ? -(uint64_t)v : (uint64_t)v; + do { + tmp[t++] = (char)('0' + (u % 10)); + u /= 10; + } while (u); + if (v < 0) tmp[t++] = '-'; + for (size_t i = 0; i < t; i++) { + buf[i] = tmp[t - 1 - i]; + } + return t; +} + +static inline uint64_t clock_monotonic_ms(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000ULL + (uint64_t)ts.tv_nsec / 1000000ULL; +} + +static inline void set_sock_opts(int fd) { + int one = 1; + setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)); + setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, &one, sizeof(one)); + int flags = fcntl(fd, F_GETFL, 0); + if (flags >= 0) { + fcntl(fd, F_SETFL, flags | O_NONBLOCK); + } +} diff --git a/frameworks/apex-tuned/src/dataset.c b/frameworks/apex-tuned/src/dataset.c new file mode 100644 index 000000000..df96d890f --- /dev/null +++ b/frameworks/apex-tuned/src/dataset.c @@ -0,0 +1,169 @@ +#include "dataset.h" +#include + +static dataset_item_t *g_items = NULL; +static size_t g_num_items = 0; + +static int64_t cjson_int(const cJSON *obj, const char *key) { + const cJSON *v = cJSON_GetObjectItemCaseSensitive(obj, key); + return cJSON_IsNumber(v) ? (int64_t)v->valuedouble : 0; +} + +static const char* cjson_str(const cJSON *obj, const char *key) { + const cJSON *v = cJSON_GetObjectItemCaseSensitive(obj, key); + return cJSON_IsString(v) ? v->valuestring : ""; +} + +bool dataset_load(const char *path) { + FILE *f = fopen(path, "rb"); + if (!f) return false; + + fseek(f, 0, SEEK_END); + long sz = ftell(f); + fseek(f, 0, SEEK_SET); + if (sz <= 0) { fclose(f); return false; } + + char *buf = malloc(sz + 1); + if (!buf) { fclose(f); return false; } + size_t rd = fread(buf, 1, sz, f); + fclose(f); + buf[rd] = '\0'; + + cJSON *root = cJSON_ParseWithLength(buf, rd); + free(buf); + if (!cJSON_IsArray(root)) return false; + + g_num_items = (size_t)cJSON_GetArraySize(root); + g_items = calloc(g_num_items, sizeof(dataset_item_t)); + if (!g_items) return false; + + size_t idx = 0; + const cJSON *item_node = NULL; + cJSON_ArrayForEach(item_node, root) { + dataset_item_t *it = &g_items[idx++]; + it->id = cjson_int(item_node, "id"); + it->price = cjson_int(item_node, "price"); + it->quantity = cjson_int(item_node, "quantity"); + it->name = strdup(cjson_str(item_node, "name")); + it->category = strdup(cjson_str(item_node, "category")); + + const cJSON *act = cJSON_GetObjectItemCaseSensitive(item_node, "active"); + it->active = cJSON_IsTrue(act); + + const cJSON *rt = cJSON_GetObjectItemCaseSensitive(item_node, "rating"); + if (rt) { + it->score = cjson_int(rt, "score"); + it->count = cjson_int(rt, "count"); + } + + const cJSON *tags = cJSON_GetObjectItemCaseSensitive(item_node, "tags"); + if (cJSON_IsArray(tags)) { + it->num_tags = (size_t)cJSON_GetArraySize(tags); + it->tags = calloc(it->num_tags, sizeof(char*)); + size_t tidx = 0; + const cJSON *tag_node = NULL; + cJSON_ArrayForEach(tag_node, tags) { + it->tags[tidx++] = strdup(cJSON_GetStringValue(tag_node)); + } + } + + char pbuf[2048]; + char *p = pbuf; + p += sprintf(p, "{\"id\":%lld,\"name\":\"%s\",\"category\":\"%s\",\"price\":%lld,\"quantity\":%lld,\"active\":%s,\"tags\":[", + (long long)it->id, it->name, it->category, (long long)it->price, (long long)it->quantity, + it->active ? "true" : "false"); + + for (size_t t = 0; t < it->num_tags; t++) { + if (t > 0) *p++ = ','; + p += sprintf(p, "\"%s\"", it->tags[t]); + } + + p += sprintf(p, "],\"rating\":{\"score\":%lld,\"count\":%lld},\"total\":", + (long long)it->score, (long long)it->count); + + it->prefix_len = (size_t)(p - pbuf); + it->prefix = malloc(it->prefix_len); + memcpy(it->prefix, pbuf, it->prefix_len); + } + + cJSON_Delete(root); + return true; +} + +size_t dataset_count(void) { + return g_num_items; +} + +size_t json_serialize(size_t count, int64_t multiplier, char *out_buf, size_t max_out) { + (void)max_out; + if (count > g_num_items) count = g_num_items; + char *p = out_buf; + + memcpy(p, "{\"items\":[", 10); p += 10; + + for (size_t i = 0; i < count; i++) { + if (i > 0) *p++ = ','; + memcpy(p, g_items[i].prefix, g_items[i].prefix_len); + p += g_items[i].prefix_len; + int64_t total = g_items[i].price * g_items[i].quantity * multiplier; + p += format_i64(p, total); + *p++ = '}'; + } + + memcpy(p, "],\"count\":", 10); p += 10; + p += format_i64(p, (int64_t)count); + *p++ = '}'; + + return (size_t)(p - out_buf); +} + +void compress_worker_init(worker_compress_t *wc) { + memset(wc, 0, sizeof(*wc)); + wc->zlib_stream.zalloc = Z_NULL; + wc->zlib_stream.zfree = Z_NULL; + wc->zlib_stream.opaque = Z_NULL; + if (deflateInit2(&wc->zlib_stream, 1, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY) == Z_OK) { + wc->zlib_initialized = true; + } +} + +void compress_worker_destroy(worker_compress_t *wc) { + if (wc->zlib_initialized) { + deflateEnd(&wc->zlib_stream); + wc->zlib_initialized = false; + } +} + +int compress_brotli(const char *in_buf, size_t in_len, char *out_buf, size_t max_out, size_t *out_len) { + size_t encoded_size = max_out; + BROTLI_BOOL res = BrotliEncoderCompress( + 1, + BROTLI_DEFAULT_WINDOW, + BROTLI_MODE_TEXT, + in_len, + (const uint8_t*)in_buf, + &encoded_size, + (uint8_t*)out_buf + ); + + if (!res) return -1; + *out_len = encoded_size; + return 0; +} + +int compress_gzip(worker_compress_t *wc, const char *in_buf, size_t in_len, char *out_buf, size_t max_out, size_t *out_len) { + if (!wc->zlib_initialized) return -1; + + z_stream *strm = &wc->zlib_stream; + deflateReset(strm); + strm->next_in = (Bytef*)in_buf; + strm->avail_in = (uInt)in_len; + strm->next_out = (Bytef*)out_buf; + strm->avail_out = (uInt)max_out; + + int ret = deflate(strm, Z_FINISH); + if (ret != Z_STREAM_END) return -1; + + *out_len = max_out - strm->avail_out; + return 0; +} diff --git a/frameworks/apex-tuned/src/dataset.h b/frameworks/apex-tuned/src/dataset.h new file mode 100644 index 000000000..1bd0382a3 --- /dev/null +++ b/frameworks/apex-tuned/src/dataset.h @@ -0,0 +1,39 @@ +#pragma once + +#include "common.h" +#include +#include + +typedef struct { + int64_t id; + int64_t price; + int64_t quantity; + int64_t score; + int64_t count; + bool active; + char *name; + char *category; + char **tags; + size_t num_tags; + + char *prefix; + size_t prefix_len; +} dataset_item_t; + +bool dataset_load(const char *path); +size_t dataset_count(void); + +size_t json_serialize(size_t count, int64_t multiplier, char *out_buf, size_t max_out); + +typedef struct { + char json_buf[WRITE_BUF_SIZE]; + char comp_buf[WRITE_BUF_SIZE]; + z_stream zlib_stream; + bool zlib_initialized; +} worker_compress_t; + +void compress_worker_init(worker_compress_t *wc); +void compress_worker_destroy(worker_compress_t *wc); + +int compress_brotli(const char *in_buf, size_t in_len, char *out_buf, size_t max_out, size_t *out_len); +int compress_gzip(worker_compress_t *wc, const char *in_buf, size_t in_len, char *out_buf, size_t max_out, size_t *out_len); diff --git a/frameworks/apex-tuned/src/http.c b/frameworks/apex-tuned/src/http.c new file mode 100644 index 000000000..1ebeaccd6 --- /dev/null +++ b/frameworks/apex-tuned/src/http.c @@ -0,0 +1,311 @@ +#include "http.h" +#include + +void req_init(request_t *req) { + memset(req, 0, sizeof(*req)); + req->content_length = -1; + req->keep_alive = true; +} + +void req_parse_query(request_t *req) { + if (!req->query.len || !req->query.ptr) return; + const char *p = req->query.ptr; + const char *end = p + req->query.len; + + while (p < end && req->num_params < MAX_PARAMS) { + const char *eq = memchr(p, '=', end - p); + const char *amp = memchr(p, '&', end - p); + if (!amp) amp = end; + + if (eq && eq < amp) { + req->params[req->num_params].key = (str_view_t){ p, (size_t)(eq - p) }; + req->params[req->num_params].val = (str_view_t){ eq + 1, (size_t)(amp - (eq + 1)) }; + req->num_params++; + } else { + req->params[req->num_params].key = (str_view_t){ p, (size_t)(amp - p) }; + req->params[req->num_params].val = (str_view_t){ NULL, 0 }; + req->num_params++; + } + + p = (amp < end) ? amp + 1 : end; + } +} + +str_view_t req_get_header(const request_t *req, const char *name) { + for (size_t i = 0; i < req->num_headers; i++) { + if (view_case_eq(req->headers[i].name, name)) { + return req->headers[i].val; + } + } + return (str_view_t){ NULL, 0 }; +} + +bool req_get_query_param(const request_t *req, const char *key, str_view_t *out_val) { + for (size_t i = 0; i < req->num_params; i++) { + if (view_eq(req->params[i].key, key)) { + *out_val = req->params[i].val; + return true; + } + } + return false; +} + +void res_init(response_t *res, bool keep_alive) { + res->status = 200; + res->keep_alive = keep_alive; + res->body_needs_free = false; + res->content_type = "text/plain"; + res->content_encoding = NULL; + res->extra_headers = NULL; + res->extra_headers_len = 0; + res->body = NULL; + res->body_len = 0; +} + +void res_set_status(response_t *res, int status) { + res->status = status; +} + +void res_set_text(response_t *res, const char *text, size_t len) { + res->content_type = "text/plain"; + res->body = text; + res->body_len = len; +} + +void res_set_json(response_t *res, const char *json, size_t len) { + res->content_type = "application/json"; + res->body = json; + res->body_len = len; +} + +void res_set_binary(response_t *res, const char *data, size_t len, const char *content_type) { + res->content_type = content_type ? content_type : "application/octet-stream"; + res->body = data; + res->body_len = len; +} + +static inline const char* status_text(int code) { + switch (code) { + case 200: return "200 OK"; + case 204: return "204 No Content"; + case 400: return "400 Bad Request"; + case 404: return "404 Not Found"; + case 405: return "405 Method Not Allowed"; + case 500: return "500 Internal Server Error"; + case 503: return "503 Service Unavailable"; + default: return "200 OK"; + } +} + +size_t res_serialize(const response_t *res, char *out_buf, size_t max_out) { + char *p = out_buf; + char *end = out_buf + max_out; + + const char *st = status_text(res->status); + size_t st_len = strlen(st); + memcpy(p, "HTTP/1.1 ", 9); p += 9; + memcpy(p, st, st_len); p += st_len; + memcpy(p, "\r\n", 2); p += 2; + + if (res->content_type) { + size_t ct_len = strlen(res->content_type); + memcpy(p, "Content-Type: ", 14); p += 14; + memcpy(p, res->content_type, ct_len); p += ct_len; + memcpy(p, "\r\n", 2); p += 2; + } + + if (res->content_encoding) { + size_t ce_len = strlen(res->content_encoding); + memcpy(p, "Content-Encoding: ", 18); p += 18; + memcpy(p, res->content_encoding, ce_len); p += ce_len; + memcpy(p, "\r\n", 2); p += 2; + } + + memcpy(p, "Content-Length: ", 16); p += 16; + p += format_i64(p, (int64_t)res->body_len); + memcpy(p, "\r\n", 2); p += 2; + + if (res->keep_alive) { + memcpy(p, "Connection: keep-alive\r\n", 24); p += 24; + } else { + memcpy(p, "Connection: close\r\n", 19); p += 19; + } + + if (res->extra_headers && res->extra_headers_len) { + memcpy(p, res->extra_headers, res->extra_headers_len); + p += res->extra_headers_len; + } + + memcpy(p, "\r\n", 2); p += 2; + + if (res->body && res->body_len) { + if ((size_t)(end - p) < res->body_len) { + return 0; + } + memcpy(p, res->body, res->body_len); + p += res->body_len; + } + + return (size_t)(p - out_buf); +} + +static inline const char* find_crlf_crlf(const char *buf, size_t len, size_t *header_len_out) { + if (len < 4) return NULL; + for (size_t i = 0; i + 3 < len; i++) { + if (buf[i] == '\r' && buf[i+1] == '\n' && buf[i+2] == '\r' && buf[i+3] == '\n') { + *header_len_out = i + 4; + return buf + i + 4; + } + } + return NULL; +} + +int parse_http_request(const char *buf, size_t len, request_t *req, char *chunk_buf, size_t max_chunk, size_t *consumed_out) { + size_t header_len = 0; + const char *body_start = find_crlf_crlf(buf, len, &header_len); + if (!body_start) { + return 0; + } + + req_init(req); + + const char *line = buf; + const char *line_end = memmem(line, header_len, "\r\n", 2); + if (!line_end) return -1; + + const char *sp1 = memchr(line, ' ', line_end - line); + if (!sp1) return -1; + size_t mlen = sp1 - line; + if (mlen == 3 && memcmp(line, "GET", 3) == 0) req->method = HTTP_METHOD_GET; + else if (mlen == 4 && memcmp(line, "POST", 4) == 0) req->method = HTTP_METHOD_POST; + else if (mlen == 4 && memcmp(line, "HEAD", 4) == 0) req->method = HTTP_METHOD_HEAD; + else if (mlen == 3 && memcmp(line, "PUT", 3) == 0) req->method = HTTP_METHOD_PUT; + else if (mlen == 6 && memcmp(line, "DELETE", 6) == 0) req->method = HTTP_METHOD_DELETE; + else req->method = HTTP_METHOD_UNKNOWN; + + const char *sp2 = memchr(sp1 + 1, ' ', line_end - (sp1 + 1)); + if (!sp2) return -1; + + req->uri = (str_view_t){ sp1 + 1, (size_t)(sp2 - (sp1 + 1)) }; + const char *qmark = memchr(req->uri.ptr, '?', req->uri.len); + if (qmark) { + req->path = (str_view_t){ req->uri.ptr, (size_t)(qmark - req->uri.ptr) }; + req->query = (str_view_t){ qmark + 1, (size_t)(req->uri.ptr + req->uri.len - (qmark + 1)) }; + } else { + req->path = req->uri; + req->query = (str_view_t){ NULL, 0 }; + } + + str_view_t proto = (str_view_t){ sp2 + 1, (size_t)(line_end - (sp2 + 1)) }; + if (view_eq(proto, "HTTP/1.0")) { + req->keep_alive = false; + } else { + req->keep_alive = true; + } + + const char *p = line_end + 2; + const char *headers_end = buf + header_len - 2; + + while (p < headers_end && req->num_headers < MAX_HEADERS) { + const char *next_line = memmem(p, headers_end - p, "\r\n", 2); + if (!next_line) break; + if (next_line == p) { + p += 2; + break; + } + + const char *colon = memchr(p, ':', next_line - p); + if (colon) { + const char *hname = p; + size_t hname_len = colon - p; + const char *hval = colon + 1; + while (hval < next_line && (*hval == ' ' || *hval == '\t')) hval++; + const char *hval_end = next_line; + while (hval_end > hval && (*(hval_end - 1) == ' ' || *(hval_end - 1) == '\t')) hval_end--; + + req->headers[req->num_headers].name = (str_view_t){ hname, hname_len }; + req->headers[req->num_headers].val = (str_view_t){ hval, (size_t)(hval_end - hval) }; + + str_view_t vname = req->headers[req->num_headers].name; + str_view_t vval = req->headers[req->num_headers].val; + + if (view_case_eq(vname, "content-length")) { + int64_t cl = 0; + if (parse_i64(vval.ptr, vval.len, &cl) && cl >= 0) { + req->content_length = cl; + } + } else if (view_case_eq(vname, "transfer-encoding")) { + if (view_case_eq(vval, "chunked") || strcasestr(vval.ptr, "chunked")) { + req->is_chunked = true; + } + } else if (view_case_eq(vname, "connection")) { + if (view_case_eq(vval, "close")) { + req->keep_alive = false; + } else if (view_case_eq(vval, "keep-alive")) { + req->keep_alive = true; + } + } else if (view_case_eq(vname, "accept-encoding")) { + req->accept_encoding = vval; + } else if (view_case_eq(vname, "content-type")) { + req->content_type = vval; + } + + req->num_headers++; + } + p = next_line + 2; + } + + if (req->is_chunked) { + const char *cur = body_start; + const char *buf_end = buf + len; + size_t total_decoded = 0; + + while (true) { + const char *line_crlf = memmem(cur, buf_end - cur, "\r\n", 2); + if (!line_crlf) return 0; + + char *endptr = NULL; + unsigned long chunk_sz = strtoul(cur, &endptr, 16); + if (endptr == cur) return -1; + + const char *chunk_data = line_crlf + 2; + if (chunk_sz == 0) { + const char *trail = memmem(chunk_data, buf_end - chunk_data, "\r\n", 2); + if (!trail) return 0; + cur = trail + 2; + break; + } + + if ((size_t)(buf_end - chunk_data) < chunk_sz + 2) { + return 0; + } + if (chunk_data[chunk_sz] != '\r' || chunk_data[chunk_sz + 1] != '\n') { + return -1; + } + + if (total_decoded + chunk_sz > max_chunk) return -1; + memcpy(chunk_buf + total_decoded, chunk_data, chunk_sz); + total_decoded += chunk_sz; + + cur = chunk_data + chunk_sz + 2; + } + + req->body = (str_view_t){ chunk_buf, total_decoded }; + *consumed_out = (size_t)(cur - buf); + return 1; + } + + if (req->content_length > 0) { + size_t cl = (size_t)req->content_length; + if (len < header_len + cl) { + return 0; + } + req->body = (str_view_t){ body_start, cl }; + *consumed_out = header_len + cl; + return 1; + } + + req->body = (str_view_t){ NULL, 0 }; + *consumed_out = header_len; + return 1; +} diff --git a/frameworks/apex-tuned/src/http.h b/frameworks/apex-tuned/src/http.h new file mode 100644 index 000000000..a99eac65f --- /dev/null +++ b/frameworks/apex-tuned/src/http.h @@ -0,0 +1,72 @@ +#pragma once + +#include "common.h" + +typedef enum { + HTTP_METHOD_GET = 0, + HTTP_METHOD_POST, + HTTP_METHOD_HEAD, + HTTP_METHOD_PUT, + HTTP_METHOD_DELETE, + HTTP_METHOD_UNKNOWN +} http_method_t; + +typedef struct { + str_view_t name; + str_view_t val; +} http_header_t; + +typedef struct { + str_view_t key; + str_view_t val; +} query_param_t; + +typedef struct { + http_method_t method; + str_view_t uri; + str_view_t path; + str_view_t query; + str_view_t body; + + http_header_t headers[MAX_HEADERS]; + size_t num_headers; + + query_param_t params[MAX_PARAMS]; + size_t num_params; + + str_view_t route_params[MAX_PARAMS]; + size_t num_route_params; + + int64_t content_length; + bool is_chunked; + bool keep_alive; + str_view_t accept_encoding; + str_view_t content_type; +} request_t; + +typedef struct { + int status; + bool keep_alive; + bool body_needs_free; + const char *content_type; + const char *content_encoding; + const char *extra_headers; + size_t extra_headers_len; + const char *body; + size_t body_len; +} response_t; + +void req_init(request_t *req); +void req_parse_query(request_t *req); +str_view_t req_get_header(const request_t *req, const char *name); +bool req_get_query_param(const request_t *req, const char *key, str_view_t *out_val); + +void res_init(response_t *res, bool keep_alive); +void res_set_status(response_t *res, int status); +void res_set_text(response_t *res, const char *text, size_t len); +void res_set_json(response_t *res, const char *json, size_t len); +void res_set_binary(response_t *res, const char *data, size_t len, const char *content_type); + +size_t res_serialize(const response_t *res, char *out_buf, size_t max_out); + +int parse_http_request(const char *buf, size_t len, request_t *req, char *chunk_buf, size_t max_chunk, size_t *consumed_out); diff --git a/frameworks/apex-tuned/src/main.c b/frameworks/apex-tuned/src/main.c new file mode 100644 index 000000000..d0b0fa0c0 --- /dev/null +++ b/frameworks/apex-tuned/src/main.c @@ -0,0 +1,737 @@ +#include "common.h" +#include "http.h" +#include "router.h" +#include "dataset.h" +#include "timer.h" +#include "tls.h" +#include "static_files.h" + +typedef struct conn_s conn_t; + +struct conn_s { + int fd; + uint32_t gen; + bool is_tls; + SSL *ssl; + bool tls_handshake_done; + + char *read_buf; + size_t read_cap; + size_t read_pos; + char inline_read_buf[16384]; + + char *pending_write; + size_t pending_write_len; + size_t pending_write_sent; + + char scratch[64]; + + bool parked_in_timer; + uint64_t timer_expire_ms; + int64_t timer_val_ms; + + bool keep_alive; + bool closing; + + conn_t *next_free; +}; + +typedef struct { + int id; + int cpu_id; + pthread_t thread; + int epoll_fd; + int listen_h1_fd; + int listen_tls_fd; + SSL_CTX *ssl_ctx; + router_t router; + worker_compress_t compress; + timer_heap_t timers; + conn_t *free_conns; + + int token_h1; + int token_tls; + + char write_buf[WRITE_BUF_SIZE]; + char chunk_buf[128 * 1024]; +} worker_t; + +static volatile bool g_running = true; +static int g_num_workers = 1; +static worker_t *g_workers = NULL; +static __thread worker_t *t_worker = NULL; + +static void handle_sig(int sig) { + (void)sig; + g_running = false; +} + +static conn_t* alloc_conn(worker_t *w, int fd, bool is_tls, SSL *ssl) { + conn_t *c = w->free_conns; + if (c) { + w->free_conns = c->next_free; + } else { + c = calloc(1, sizeof(conn_t)); + if (!c) return NULL; + c->read_buf = c->inline_read_buf; + c->read_cap = sizeof(c->inline_read_buf); + } + c->fd = fd; + c->gen++; + c->is_tls = is_tls; + c->ssl = ssl; + c->tls_handshake_done = false; + c->read_pos = 0; + c->pending_write = NULL; + c->pending_write_len = 0; + c->pending_write_sent = 0; + c->parked_in_timer = false; + c->keep_alive = true; + c->closing = false; + c->next_free = NULL; + return c; +} + +static void close_conn(worker_t *w, conn_t *c) { + if (c->fd >= 0) { + epoll_ctl(w->epoll_fd, EPOLL_CTL_DEL, c->fd, NULL); + close(c->fd); + c->fd = -1; + } + if (c->ssl) { + SSL_free(c->ssl); + c->ssl = NULL; + } + if (c->pending_write) { + free(c->pending_write); + c->pending_write = NULL; + } + if (c->read_buf != c->inline_read_buf) { + free(c->read_buf); + c->read_buf = c->inline_read_buf; + c->read_cap = sizeof(c->inline_read_buf); + } + c->read_pos = 0; + c->pending_write_len = 0; + c->pending_write_sent = 0; + c->parked_in_timer = false; + c->closing = false; + c->gen++; + + c->next_free = w->free_conns; + w->free_conns = c; +} + +static void baseline_handler(context_t *ctx) { + request_t *req = ctx->req; + response_t *res = ctx->res; + conn_t *c = (conn_t*)ctx->user_data; + + int64_t sum = 0; + for (size_t i = 0; i < req->num_params; i++) { + int64_t v = 0; + if (parse_i64(req->params[i].val.ptr, req->params[i].val.len, &v)) { + sum += v; + } + } + + if (req->method == HTTP_METHOD_POST && req->body.len > 0) { + int64_t body_val = 0; + if (parse_i64(req->body.ptr, req->body.len, &body_val)) { + sum += body_val; + } + } + + size_t len = format_i64(c->scratch, sum); + res_set_text(res, c->scratch, len); +} + +static void pipeline_handler(context_t *ctx) { + res_set_text(ctx->res, "ok", 2); +} + +static void delay_handler(context_t *ctx) { + request_t *req = ctx->req; + response_t *res = ctx->res; + conn_t *c = (conn_t*)ctx->user_data; + + int64_t ms = 0; + if (req->num_route_params > 0) { + parse_i64(req->route_params[0].ptr, req->route_params[0].len, &ms); + } + + if (ms <= 0) { + size_t len = format_i64(c->scratch, 0); + res_set_text(res, c->scratch, len); + return; + } + + c->parked_in_timer = true; + c->timer_expire_ms = clock_monotonic_ms() + (uint64_t)ms; + c->timer_val_ms = ms; + c->keep_alive = req->keep_alive; + ctx->async_parked = true; +} + +static void json_handler(context_t *ctx) { + request_t *req = ctx->req; + response_t *res = ctx->res; + worker_t *w = t_worker; + + int64_t count = 50; + if (req->num_route_params > 0) { + parse_i64(req->route_params[0].ptr, req->route_params[0].len, &count); + } + if (count < 1) count = 1; + if (count > 50) count = 50; + + int64_t mult = 1; + str_view_t m_val; + if (req_get_query_param(req, "m", &m_val)) { + parse_i64(m_val.ptr, m_val.len, &mult); + } + + size_t json_len = json_serialize((size_t)count, mult, w->compress.json_buf, sizeof(w->compress.json_buf)); + + bool can_br = (req->accept_encoding.ptr && strcasestr(req->accept_encoding.ptr, "br") != NULL); + bool can_gz = (req->accept_encoding.ptr && strcasestr(req->accept_encoding.ptr, "gzip") != NULL); + + if (can_gz) { + size_t comp_len = 0; + if (compress_gzip(&w->compress, w->compress.json_buf, json_len, w->compress.comp_buf, sizeof(w->compress.comp_buf), &comp_len) == 0) { + res_set_json(res, w->compress.comp_buf, comp_len); + res->content_encoding = "gzip"; + return; + } + } + + if (can_br) { + size_t comp_len = 0; + if (compress_brotli(w->compress.json_buf, json_len, w->compress.comp_buf, sizeof(w->compress.comp_buf), &comp_len) == 0) { + res_set_json(res, w->compress.comp_buf, comp_len); + res->content_encoding = "br"; + return; + } + } + + res_set_json(res, w->compress.json_buf, json_len); + res->content_encoding = NULL; +} + +static void echo_handler(context_t *ctx) { + request_t *req = ctx->req; + response_t *res = ctx->res; + res_set_binary(res, req->body.ptr, req->body.len, "application/octet-stream"); +} + +static bool flush_pending_write(worker_t *w, conn_t *c) { + while (c->pending_write_sent < c->pending_write_len) { + size_t remaining = c->pending_write_len - c->pending_write_sent; + ssize_t n = 0; + if (c->is_tls) { + int ret = SSL_write(c->ssl, c->pending_write + c->pending_write_sent, (int)remaining); + if (ret <= 0) { + int err = SSL_get_error(c->ssl, ret); + if (err == SSL_ERROR_WANT_WRITE || err == SSL_ERROR_WANT_READ) { + return false; + } + close_conn(w, c); + return false; + } + n = ret; + } else { + n = send(c->fd, c->pending_write + c->pending_write_sent, remaining, MSG_NOSIGNAL); + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return false; + } + close_conn(w, c); + return false; + } + } + c->pending_write_sent += (size_t)n; + } + + free(c->pending_write); + c->pending_write = NULL; + c->pending_write_len = 0; + c->pending_write_sent = 0; + + struct epoll_event ev = { + .events = EPOLLIN | EPOLLET | EPOLLRDHUP | EPOLLHUP | EPOLLERR, + .data.ptr = c + }; + epoll_ctl(w->epoll_fd, EPOLL_CTL_MOD, c->fd, &ev); + + if (c->closing || !c->keep_alive) { + close_conn(w, c); + return false; + } + + return true; +} + +static void flush_data(worker_t *w, conn_t *c, const char *data, size_t len) { + if (len == 0) return; + size_t sent = 0; + + while (sent < len) { + ssize_t n = 0; + if (c->is_tls) { + int ret = SSL_write(c->ssl, data + sent, (int)(len - sent)); + if (ret <= 0) { + int err = SSL_get_error(c->ssl, ret); + if (err == SSL_ERROR_WANT_WRITE || err == SSL_ERROR_WANT_READ) { + break; + } + close_conn(w, c); + return; + } + n = ret; + } else { + n = send(c->fd, data + sent, len - sent, MSG_NOSIGNAL); + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + break; + } + close_conn(w, c); + return; + } + } + sent += (size_t)n; + } + + if (sent < len) { + size_t rem = len - sent; + c->pending_write = malloc(rem); + if (!c->pending_write) { + close_conn(w, c); + return; + } + memcpy(c->pending_write, data + sent, rem); + c->pending_write_len = rem; + c->pending_write_sent = 0; + + struct epoll_event ev = { + .events = EPOLLIN | EPOLLOUT | EPOLLET | EPOLLRDHUP | EPOLLHUP | EPOLLERR, + .data.ptr = c + }; + epoll_ctl(w->epoll_fd, EPOLL_CTL_MOD, c->fd, &ev); + return; + } + + if (c->closing || !c->keep_alive) { + close_conn(w, c); + } +} + +static void process_requests(worker_t *w, conn_t *c) { + size_t write_pos = 0; + + while (c->read_pos > 0 && !c->parked_in_timer && !c->closing) { + request_t req; + size_t consumed = 0; + int rc = parse_http_request(c->read_buf, c->read_pos, &req, w->chunk_buf, sizeof(w->chunk_buf), &consumed); + + if (rc == 0) { + break; + } + if (rc < 0) { + close_conn(w, c); + return; + } + + c->keep_alive = req.keep_alive; + response_t res; + res_init(&res, c->keep_alive); + + bool async_parked = false; + if (!router_dispatch(&w->router, &req, &res, c, &async_parked)) { + res_set_status(&res, 404); + res_set_text(&res, "Not Found", 9); + } + + if (async_parked) { + timer_heap_push(&w->timers, c->timer_expire_ms, c, c->gen, c->timer_val_ms); + if (consumed < c->read_pos) { + memmove(c->read_buf, c->read_buf + consumed, c->read_pos - consumed); + c->read_pos -= consumed; + } else { + c->read_pos = 0; + } + break; + } + + size_t remaining_space = sizeof(w->write_buf) - write_pos; + size_t written = res_serialize(&res, w->write_buf + write_pos, remaining_space); + if (res.body_needs_free && res.body) { + free((void*)res.body); + } + + if (written == 0) { + close_conn(w, c); + return; + } + + write_pos += written; + + if (consumed < c->read_pos) { + memmove(c->read_buf, c->read_buf + consumed, c->read_pos - consumed); + c->read_pos -= consumed; + } else { + c->read_pos = 0; + } + + if (!c->keep_alive) { + c->closing = true; + } + } + + if (write_pos > 0) { + flush_data(w, c, w->write_buf, write_pos); + } +} + +static void handle_timer_expiration(worker_t *w, conn_t *c, int64_t delay_ms) { + c->parked_in_timer = false; + + response_t res; + res_init(&res, c->keep_alive); + char buf[24]; + size_t len = format_i64(buf, delay_ms); + res_set_text(&res, buf, len); + + size_t written = res_serialize(&res, w->write_buf, sizeof(w->write_buf)); + if (written > 0) { + flush_data(w, c, w->write_buf, written); + } +} + +static void* worker_main(void *arg) { + worker_t *w = (worker_t*)arg; + t_worker = w; + + cpu_set_t cpuset; + CPU_ZERO(&cpuset); + CPU_SET(w->cpu_id, &cpuset); + pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset); + + struct epoll_event events[MAX_EVENTS]; + + while (g_running) { + uint64_t now_ms = clock_monotonic_ms(); + int timeout_ms = timer_heap_next_timeout_ms(&w->timers, now_ms); + + int nfds = epoll_wait(w->epoll_fd, events, MAX_EVENTS, timeout_ms); + if (nfds < 0) { + if (errno == EINTR) continue; + break; + } + + now_ms = clock_monotonic_ms(); + timer_entry_t te; + while (timer_heap_peek(&w->timers, &te)) { + if (te.expire_at_ms > now_ms) break; + timer_heap_pop(&w->timers, &te); + conn_t *c = (conn_t*)te.conn_ptr; + if (c && c->gen == te.conn_gen && c->parked_in_timer) { + handle_timer_expiration(w, c, te.delay_ms); + } + } + + for (int i = 0; i < nfds; i++) { + void *ptr = events[i].data.ptr; + uint32_t evs = events[i].events; + + if (ptr == &w->token_h1) { + while (true) { + int cfd = accept4(w->listen_h1_fd, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC); + if (cfd < 0) break; + + set_sock_opts(cfd); + conn_t *c = alloc_conn(w, cfd, false, NULL); + if (!c) { close(cfd); continue; } + + struct epoll_event ev = { + .events = EPOLLIN | EPOLLET | EPOLLRDHUP | EPOLLHUP | EPOLLERR, + .data.ptr = c + }; + epoll_ctl(w->epoll_fd, EPOLL_CTL_ADD, cfd, &ev); + } + continue; + } + + if (w->listen_tls_fd >= 0 && ptr == &w->token_tls) { + while (true) { + int cfd = accept4(w->listen_tls_fd, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC); + if (cfd < 0) break; + + set_sock_opts(cfd); + SSL *ssl = SSL_new(w->ssl_ctx); + if (!ssl) { close(cfd); continue; } + SSL_set_fd(ssl, cfd); + SSL_set_accept_state(ssl); + + conn_t *c = alloc_conn(w, cfd, true, ssl); + if (!c) { SSL_free(ssl); close(cfd); continue; } + + struct epoll_event ev = { + .events = EPOLLIN | EPOLLOUT | EPOLLET | EPOLLRDHUP | EPOLLHUP | EPOLLERR, + .data.ptr = c + }; + epoll_ctl(w->epoll_fd, EPOLL_CTL_ADD, cfd, &ev); + } + continue; + } + + conn_t *c = (conn_t*)ptr; + if (!c || c->fd < 0) continue; + + if (evs & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)) { + close_conn(w, c); + continue; + } + + if (c->is_tls && !c->tls_handshake_done) { + int ret = SSL_accept(c->ssl); + if (ret <= 0) { + int err = SSL_get_error(c->ssl, ret); + if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) { + continue; + } + close_conn(w, c); + continue; + } + c->tls_handshake_done = true; + struct epoll_event ev = { + .events = EPOLLIN | EPOLLET | EPOLLRDHUP | EPOLLHUP | EPOLLERR, + .data.ptr = c + }; + epoll_ctl(w->epoll_fd, EPOLL_CTL_MOD, c->fd, &ev); + } + + if (evs & EPOLLOUT) { + if (c->pending_write) { + if (!flush_pending_write(w, c)) { + continue; + } + } + } + + if (evs & EPOLLIN) { + while (true) { + if (c->read_pos == c->read_cap) { + size_t new_cap = c->read_cap * 2; + if (new_cap < 65536) new_cap = 65536; + if (new_cap > 512 * 1024) new_cap = 512 * 1024; + if (new_cap > c->read_cap) { + char *new_buf = malloc(new_cap); + if (!new_buf) { close_conn(w, c); break; } + memcpy(new_buf, c->read_buf, c->read_pos); + if (c->read_buf != c->inline_read_buf) free(c->read_buf); + c->read_buf = new_buf; + c->read_cap = new_cap; + } else { + close_conn(w, c); + break; + } + } + + ssize_t n = 0; + if (c->is_tls) { + int ret = SSL_read(c->ssl, c->read_buf + c->read_pos, (int)(c->read_cap - c->read_pos)); + if (ret <= 0) { + int err = SSL_get_error(c->ssl, ret); + if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) { + break; + } + close_conn(w, c); + break; + } + n = ret; + } else { + n = recv(c->fd, c->read_buf + c->read_pos, c->read_cap - c->read_pos, 0); + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + break; + } + close_conn(w, c); + break; + } + if (n == 0) { + close_conn(w, c); + break; + } + } + c->read_pos += (size_t)n; + } + + if (c->fd >= 0 && c->read_pos > 0) { + process_requests(w, c); + } + } + } + } + + close(w->epoll_fd); + close(w->listen_h1_fd); + if (w->listen_tls_fd >= 0) close(w->listen_tls_fd); + compress_worker_destroy(&w->compress); + timer_heap_destroy(&w->timers); + + conn_t *curr = w->free_conns; + while (curr) { + conn_t *next = curr->next_free; + if (curr->read_buf != curr->inline_read_buf) { + free(curr->read_buf); + } + if (curr->pending_write) { + free(curr->pending_write); + } + free(curr); + curr = next; + } + w->free_conns = NULL; + + return NULL; +} + +static int create_listen_socket(int port) { + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) return -1; + + int one = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one)); + set_sock_opts(fd); + + struct sockaddr_in addr = { + .sin_family = AF_INET, + .sin_port = htons(port), + .sin_addr.s_addr = INADDR_ANY + }; + + if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + close(fd); + return -1; + } + + if (listen(fd, 65535) < 0) { + close(fd); + return -1; + } + + return fd; +} + +int main(void) { + signal(SIGINT, handle_sig); + signal(SIGTERM, handle_sig); + signal(SIGPIPE, SIG_IGN); + + cpu_set_t cs; + int allowed_cpus[MAX_WORKERS]; + int num_allowed = 0; + if (sched_getaffinity(0, sizeof(cs), &cs) == 0) { + for (int i = 0; i < CPU_SETSIZE && num_allowed < MAX_WORKERS; i++) { + if (CPU_ISSET(i, &cs)) { + allowed_cpus[num_allowed++] = i; + } + } + } + if (num_allowed == 0) { + long ncpu = sysconf(_SC_NPROCESSORS_ONLN); + if (ncpu <= 0) ncpu = 1; + if (ncpu > MAX_WORKERS) ncpu = MAX_WORKERS; + for (int i = 0; i < ncpu; i++) allowed_cpus[i] = i; + num_allowed = (int)ncpu; + } + g_num_workers = num_allowed; + + const char *dataset_path = getenv("DATASET_PATH"); + if (!dataset_path) dataset_path = "/data/dataset.json"; + if (!dataset_load(dataset_path)) { + dataset_load("./data/dataset.json"); + } + + const char *static_root = getenv("STATIC_ROOT"); + if (!static_root) static_root = "/data/static"; + static_files_init(static_root); + + const char *cert_file = getenv("TLS_CERT"); + if (!cert_file) cert_file = "/certs/server.crt"; + const char *key_file = getenv("TLS_KEY"); + if (!key_file) key_file = "/certs/server.key"; + + SSL_CTX *shared_ssl_ctx = tls_init_server_ctx(cert_file, key_file); + if (!shared_ssl_ctx) { + shared_ssl_ctx = tls_init_server_ctx("./certs/server.crt", "./certs/server.key"); + } + + g_workers = calloc((size_t)g_num_workers, sizeof(worker_t)); + if (!g_workers) { + perror("calloc g_workers"); + return 1; + } + + for (int i = 0; i < g_num_workers; i++) { + worker_t *w = &g_workers[i]; + w->id = i; + w->cpu_id = allowed_cpus[i]; + w->ssl_ctx = shared_ssl_ctx; + + router_init(&w->router); + router_add(&w->router, HTTP_METHOD_GET, "/baseline11", baseline_handler); + router_add(&w->router, HTTP_METHOD_POST, "/baseline11", baseline_handler); + router_add(&w->router, HTTP_METHOD_GET, "/pipeline", pipeline_handler); + router_add(&w->router, HTTP_METHOD_GET, "/delay/:ms", delay_handler); + router_add(&w->router, HTTP_METHOD_GET, "/json/:count", json_handler); + router_add(&w->router, HTTP_METHOD_POST, "/echo", echo_handler); + router_add(&w->router, HTTP_METHOD_GET, "/static/", static_file_handler); + + compress_worker_init(&w->compress); + timer_heap_init(&w->timers, 4096); + w->free_conns = NULL; + + for (int k = 0; k < 128; k++) { + conn_t *c = calloc(1, sizeof(conn_t)); + if (c) { + c->read_buf = c->inline_read_buf; + c->read_cap = sizeof(c->inline_read_buf); + c->fd = -1; + c->next_free = w->free_conns; + w->free_conns = c; + } + } + + w->epoll_fd = epoll_create1(EPOLL_CLOEXEC); + w->listen_h1_fd = create_listen_socket(8080); + w->token_h1 = 1; + struct epoll_event ev1 = { .events = EPOLLIN, .data.ptr = &w->token_h1 }; + epoll_ctl(w->epoll_fd, EPOLL_CTL_ADD, w->listen_h1_fd, &ev1); + + if (w->ssl_ctx) { + w->listen_tls_fd = create_listen_socket(8081); + if (w->listen_tls_fd >= 0) { + w->token_tls = 2; + struct epoll_event ev2 = { .events = EPOLLIN, .data.ptr = &w->token_tls }; + epoll_ctl(w->epoll_fd, EPOLL_CTL_ADD, w->listen_tls_fd, &ev2); + } + } else { + w->listen_tls_fd = -1; + } + + pthread_create(&w->thread, NULL, worker_main, w); + } + + for (int i = 0; i < g_num_workers; i++) { + pthread_join(g_workers[i].thread, NULL); + } + + if (shared_ssl_ctx) { + tls_free_server_ctx(shared_ssl_ctx); + } + + free(g_workers); + return 0; +} diff --git a/frameworks/apex-tuned/src/router.c b/frameworks/apex-tuned/src/router.c new file mode 100644 index 000000000..05b1a7aee --- /dev/null +++ b/frameworks/apex-tuned/src/router.c @@ -0,0 +1,89 @@ +#include "router.h" + +void router_init(router_t *r) { + memset(r, 0, sizeof(*r)); +} + +void router_use(router_t *r, middleware_fn_t mw) { + if (r->num_middlewares < 16) { + r->middlewares[r->num_middlewares++] = mw; + } +} + +void router_add(router_t *r, http_method_t method, const char *pattern, handler_fn_t handler) { + if (r->num_routes >= 64) return; + route_entry_t *re = &r->routes[r->num_routes++]; + re->method = method; + strncpy(re->pattern, pattern, sizeof(re->pattern) - 1); + re->handler = handler; + re->has_param = (strstr(pattern, "/:") != NULL); + re->is_prefix = (strlen(pattern) > 0 && pattern[strlen(pattern) - 1] == '/'); +} + +void ctx_next(context_t *ctx) { + if (ctx->mw_index < ctx->num_middlewares) { + middleware_fn_t mw = ctx->middlewares[ctx->mw_index++]; + mw(ctx, ctx_next); + } else if (ctx->handler) { + ctx->handler(ctx); + } +} + +static bool match_route(const route_entry_t *re, request_t *req) { + if (re->method != req->method) return false; + + if (re->has_param) { + const char *param_marker = strstr(re->pattern, "/:"); + size_t prefix_len = param_marker - re->pattern + 1; + if (req->path.len < prefix_len) return false; + if (memcmp(req->path.ptr, re->pattern, prefix_len) != 0) return false; + + const char *param_start = req->path.ptr + prefix_len; + const char *path_end = req->path.ptr + req->path.len; + const char *slash = memchr(param_start, '/', path_end - param_start); + size_t param_len = slash ? (size_t)(slash - param_start) : (size_t)(path_end - param_start); + + req->route_params[0] = (str_view_t){ param_start, param_len }; + req->num_route_params = 1; + return true; + } + + if (re->is_prefix) { + size_t plen = strlen(re->pattern); + if (req->path.len < plen) return false; + if (memcmp(req->path.ptr, re->pattern, plen) != 0) return false; + + req->route_params[0] = (str_view_t){ req->path.ptr + plen, req->path.len - plen }; + req->num_route_params = 1; + return true; + } + + return view_eq(req->path, re->pattern); +} + +bool router_dispatch(const router_t *r, request_t *req, response_t *res, void *user_data, bool *async_parked_out) { + req_parse_query(req); + + for (size_t i = 0; i < r->num_routes; i++) { + if (match_route(&r->routes[i], req)) { + context_t ctx = { + .req = req, + .res = res, + .user_data = user_data, + .async_parked = false, + .middlewares = (middleware_fn_t*)r->middlewares, + .num_middlewares = r->num_middlewares, + .mw_index = 0, + .handler = r->routes[i].handler + }; + + ctx_next(&ctx); + if (async_parked_out) { + *async_parked_out = ctx.async_parked; + } + return true; + } + } + + return false; +} diff --git a/frameworks/apex-tuned/src/router.h b/frameworks/apex-tuned/src/router.h new file mode 100644 index 000000000..53d41a663 --- /dev/null +++ b/frameworks/apex-tuned/src/router.h @@ -0,0 +1,42 @@ +#pragma once + +#include "http.h" + +typedef struct context_s context_t; + +typedef void (*handler_fn_t)(context_t *ctx); +typedef void (*middleware_fn_t)(context_t *ctx, void (*next)(context_t *ctx)); + +struct context_s { + request_t *req; + response_t *res; + void *user_data; + bool async_parked; + + middleware_fn_t *middlewares; + size_t num_middlewares; + size_t mw_index; + handler_fn_t handler; +}; + +typedef struct { + http_method_t method; + char pattern[64]; + handler_fn_t handler; + bool is_prefix; + bool has_param; +} route_entry_t; + +typedef struct { + route_entry_t routes[64]; + size_t num_routes; + middleware_fn_t middlewares[16]; + size_t num_middlewares; +} router_t; + +void router_init(router_t *r); +void router_use(router_t *r, middleware_fn_t mw); +void router_add(router_t *r, http_method_t method, const char *pattern, handler_fn_t handler); + +void ctx_next(context_t *ctx); +bool router_dispatch(const router_t *r, request_t *req, response_t *res, void *user_data, bool *async_parked_out); diff --git a/frameworks/apex-tuned/src/static_files.c b/frameworks/apex-tuned/src/static_files.c new file mode 100644 index 000000000..e00a327d5 --- /dev/null +++ b/frameworks/apex-tuned/src/static_files.c @@ -0,0 +1,109 @@ +#include "static_files.h" + +static char g_static_root[512] = "/data/static"; + +void static_files_init(const char *root_dir) { + if (root_dir && *root_dir) { + strncpy(g_static_root, root_dir, sizeof(g_static_root) - 1); + } +} + +static const char* guess_mime_type(const char *path) { + const char *dot = strrchr(path, '.'); + if (!dot) return "application/octet-stream"; + if (strcmp(dot, ".html") == 0 || strcmp(dot, ".htm") == 0) return "text/html; charset=utf-8"; + if (strcmp(dot, ".css") == 0) return "text/css"; + if (strcmp(dot, ".js") == 0) return "text/javascript"; + if (strcmp(dot, ".json") == 0) return "application/json"; + if (strcmp(dot, ".png") == 0) return "image/png"; + if (strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jpeg") == 0) return "image/jpeg"; + if (strcmp(dot, ".txt") == 0) return "text/plain"; + if (strcmp(dot, ".svg") == 0) return "image/svg+xml"; + return "application/octet-stream"; +} + +void static_file_handler(context_t *ctx) { + request_t *req = ctx->req; + response_t *res = ctx->res; + + if (req->num_route_params == 0 || req->route_params[0].len == 0) { + res_set_status(res, 404); + return; + } + + char rel_path[256]; + size_t rlen = req->route_params[0].len; + if (rlen >= sizeof(rel_path)) rlen = sizeof(rel_path) - 1; + memcpy(rel_path, req->route_params[0].ptr, rlen); + rel_path[rlen] = '\0'; + + if (strstr(rel_path, "..")) { + res_set_status(res, 403); + return; + } + + const char *mime = guess_mime_type(rel_path); + + bool client_wants_br = (req->accept_encoding.ptr && strcasestr(req->accept_encoding.ptr, "br") != NULL); + bool client_wants_gz = (req->accept_encoding.ptr && strcasestr(req->accept_encoding.ptr, "gzip") != NULL); + + char full_path[512]; + const char *chosen_encoding = NULL; + int fd = -1; + + if (client_wants_br) { + snprintf(full_path, sizeof(full_path), "%s/%s.br", g_static_root, rel_path); + fd = open(full_path, O_RDONLY); + if (fd >= 0) { + chosen_encoding = "br"; + } + } + + if (fd < 0 && client_wants_gz) { + snprintf(full_path, sizeof(full_path), "%s/%s.gz", g_static_root, rel_path); + fd = open(full_path, O_RDONLY); + if (fd >= 0) { + chosen_encoding = "gzip"; + } + } + + if (fd < 0) { + snprintf(full_path, sizeof(full_path), "%s/%s", g_static_root, rel_path); + fd = open(full_path, O_RDONLY); + } + + if (fd < 0) { + res_set_status(res, 404); + return; + } + + struct stat st; + if (fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) { + close(fd); + res_set_status(res, 404); + return; + } + + size_t file_sz = (size_t)st.st_size; + char *buf = malloc(file_sz ? file_sz : 1); + if (!buf) { + close(fd); + res_set_status(res, 500); + return; + } + + size_t rd = 0; + while (rd < file_sz) { + ssize_t n = read(fd, buf + rd, file_sz - rd); + if (n <= 0) break; + rd += n; + } + close(fd); + + res->status = 200; + res->content_type = mime; + res->content_encoding = chosen_encoding; + res->body = buf; + res->body_len = rd; + res->body_needs_free = true; +} diff --git a/frameworks/apex-tuned/src/static_files.h b/frameworks/apex-tuned/src/static_files.h new file mode 100644 index 000000000..0ab776a18 --- /dev/null +++ b/frameworks/apex-tuned/src/static_files.h @@ -0,0 +1,7 @@ +#pragma once + +#include "common.h" +#include "router.h" + +void static_files_init(const char *root_dir); +void static_file_handler(context_t *ctx); diff --git a/frameworks/apex-tuned/src/timer.c b/frameworks/apex-tuned/src/timer.c new file mode 100644 index 000000000..0cf2c8ce7 --- /dev/null +++ b/frameworks/apex-tuned/src/timer.c @@ -0,0 +1,95 @@ +#include "timer.h" + +void timer_heap_init(timer_heap_t *th, size_t initial_cap) { + if (initial_cap < 64) initial_cap = 64; + th->entries = malloc(sizeof(timer_entry_t) * initial_cap); + th->count = 0; + th->capacity = initial_cap; +} + +void timer_heap_destroy(timer_heap_t *th) { + if (th->entries) { + free(th->entries); + th->entries = NULL; + } + th->count = 0; + th->capacity = 0; +} + +static inline void swap_entries(timer_entry_t *a, timer_entry_t *b) { + timer_entry_t tmp = *a; + *a = *b; + *b = tmp; +} + +bool timer_heap_push(timer_heap_t *th, uint64_t expire_at_ms, void *conn_ptr, uint32_t conn_gen, int64_t delay_ms) { + if (th->count >= th->capacity) { + size_t new_cap = th->capacity * 2; + timer_entry_t *re = realloc(th->entries, sizeof(timer_entry_t) * new_cap); + if (!re) return false; + th->entries = re; + th->capacity = new_cap; + } + + size_t i = th->count++; + th->entries[i] = (timer_entry_t){ + .expire_at_ms = expire_at_ms, + .conn_ptr = conn_ptr, + .conn_gen = conn_gen, + .delay_ms = delay_ms + }; + + while (i > 0) { + size_t parent = (i - 1) / 2; + if (th->entries[parent].expire_at_ms <= th->entries[i].expire_at_ms) { + break; + } + swap_entries(&th->entries[parent], &th->entries[i]); + i = parent; + } + + return true; +} + +bool timer_heap_peek(const timer_heap_t *th, timer_entry_t *out) { + if (th->count == 0) return false; + *out = th->entries[0]; + return true; +} + +bool timer_heap_pop(timer_heap_t *th, timer_entry_t *out) { + if (th->count == 0) return false; + *out = th->entries[0]; + + th->count--; + if (th->count == 0) return true; + + th->entries[0] = th->entries[th->count]; + size_t i = 0; + while (true) { + size_t left = 2 * i + 1; + size_t right = 2 * i + 2; + size_t smallest = i; + + if (left < th->count && th->entries[left].expire_at_ms < th->entries[smallest].expire_at_ms) { + smallest = left; + } + if (right < th->count && th->entries[right].expire_at_ms < th->entries[smallest].expire_at_ms) { + smallest = right; + } + if (smallest == i) break; + + swap_entries(&th->entries[i], &th->entries[smallest]); + i = smallest; + } + + return true; +} + +int timer_heap_next_timeout_ms(const timer_heap_t *th, uint64_t now_ms) { + if (th->count == 0) return -1; + uint64_t next_ms = th->entries[0].expire_at_ms; + if (next_ms <= now_ms) return 0; + uint64_t diff = next_ms - now_ms; + return (diff > 1000) ? 1000 : (int)diff; +} diff --git a/frameworks/apex-tuned/src/timer.h b/frameworks/apex-tuned/src/timer.h new file mode 100644 index 000000000..7168eb1ed --- /dev/null +++ b/frameworks/apex-tuned/src/timer.h @@ -0,0 +1,23 @@ +#pragma once + +#include "common.h" + +typedef struct { + uint64_t expire_at_ms; + void *conn_ptr; + uint32_t conn_gen; + int64_t delay_ms; +} timer_entry_t; + +typedef struct { + timer_entry_t *entries; + size_t count; + size_t capacity; +} timer_heap_t; + +void timer_heap_init(timer_heap_t *th, size_t initial_cap); +void timer_heap_destroy(timer_heap_t *th); +bool timer_heap_push(timer_heap_t *th, uint64_t expire_at_ms, void *conn_ptr, uint32_t conn_gen, int64_t delay_ms); +bool timer_heap_peek(const timer_heap_t *th, timer_entry_t *out); +bool timer_heap_pop(timer_heap_t *th, timer_entry_t *out); +int timer_heap_next_timeout_ms(const timer_heap_t *th, uint64_t now_ms); diff --git a/frameworks/apex-tuned/src/tls.c b/frameworks/apex-tuned/src/tls.c new file mode 100644 index 000000000..d222e507e --- /dev/null +++ b/frameworks/apex-tuned/src/tls.c @@ -0,0 +1,59 @@ +#include "tls.h" + +static int alpn_select_cb(SSL *ssl, const unsigned char **out, unsigned char *outlen, + const unsigned char *in, unsigned int inlen, void *arg) { + (void)ssl; (void)arg; + for (unsigned int i = 0; i < inlen; ) { + unsigned char proto_len = in[i++]; + if (i + proto_len > inlen) break; + if (proto_len == 8 && memcmp(&in[i], "http/1.1", 8) == 0) { + *out = &in[i]; + *outlen = proto_len; + return SSL_TLSEXT_ERR_OK; + } + i += proto_len; + } + if (inlen > 0) { + *out = &in[1]; + *outlen = in[0]; + return SSL_TLSEXT_ERR_OK; + } + return SSL_TLSEXT_ERR_NOACK; +} + +SSL_CTX* tls_init_server_ctx(const char *cert_file, const char *key_file) { + SSL_library_init(); + SSL_load_error_strings(); + OpenSSL_add_all_algorithms(); + + const SSL_METHOD *method = TLS_server_method(); + SSL_CTX *ctx = SSL_CTX_new(method); + if (!ctx) return NULL; + + SSL_CTX_set_min_proto_version(ctx, TLS1_3_VERSION); + SSL_CTX_set_max_proto_version(ctx, TLS1_3_VERSION); + + SSL_CTX_set_options(ctx, SSL_OP_NO_RENEGOTIATION | SSL_OP_CIPHER_SERVER_PREFERENCE); + SSL_CTX_set_alpn_select_cb(ctx, alpn_select_cb, NULL); + + if (SSL_CTX_use_certificate_file(ctx, cert_file, SSL_FILETYPE_PEM) <= 0) { + SSL_CTX_free(ctx); + return NULL; + } + if (SSL_CTX_use_PrivateKey_file(ctx, key_file, SSL_FILETYPE_PEM) <= 0) { + SSL_CTX_free(ctx); + return NULL; + } + if (!SSL_CTX_check_private_key(ctx)) { + SSL_CTX_free(ctx); + return NULL; + } + + return ctx; +} + +void tls_free_server_ctx(SSL_CTX *ctx) { + if (ctx) { + SSL_CTX_free(ctx); + } +} diff --git a/frameworks/apex-tuned/src/tls.h b/frameworks/apex-tuned/src/tls.h new file mode 100644 index 000000000..a13731e0f --- /dev/null +++ b/frameworks/apex-tuned/src/tls.h @@ -0,0 +1,8 @@ +#pragma once + +#include "common.h" +#include +#include + +SSL_CTX* tls_init_server_ctx(const char *cert_file, const char *key_file); +void tls_free_server_ctx(SSL_CTX *ctx); diff --git a/site/data/frameworks.json b/site/data/frameworks.json index 5d5e36df5..76ec644a6 100644 --- a/site/data/frameworks.json +++ b/site/data/frameworks.json @@ -108,6 +108,13 @@ "response": true } }, + "apex-tuned": { + "dir": "apex-tuned", + "description": "Apex HTTP engine in C23: shared-nothing thread-per-core epoll runtime, CPU affinity pinning, zero-copy buffer recycling, streaming HTTP parser with TCP fragmentation resilience, route parameter trie, middleware pipeline, Brotli compression, and tuned TLS 1.3.", + "repo": "https://github.com/xdearboy/apex", + "type": "engine", + "engine": "apex" + }, "araara-standard": { "dir": "araara-standard", "description": "araara - an OCaml 5 web stack built on HCS, using HCS server defaults where possible for the HttpArena harness. Eio, radix-trie routing, Phoenix-style plugs, Postgres via repodb, gzip/zstd compression. HTTP/3 and gRPC are not implemented.", diff --git a/site/data/results/apex-tuned.json b/site/data/results/apex-tuned.json new file mode 100644 index 000000000..5de134fcc --- /dev/null +++ b/site/data/results/apex-tuned.json @@ -0,0 +1,259 @@ +{ + "framework": "apex-tuned", + "results": { + "8gbit-512": { + "framework": "apex-tuned", + "language": "C", + "rps": 49147, + "avg_latency": "89.7us", + "p99_latency": "112.0us", + "cpu": "217.5%", + "memory": "260MiB", + "connections": 512, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "508.47MB/s", + "input_bw": "479.95MB/s", + "reconnects": 0, + "status_2xx": 245805, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0, + "cpu_usec": 11392296, + "cpu_per_req_us": 46.3469, + "target_rate": 50000, + "rate_ratio": 0.9829, + "p99_9_latency": "3933.0us" + }, + "async-32000": { + "framework": "apex-tuned", + "language": "C", + "rps": 2098402, + "avg_latency": "15.13ms", + "p99_latency": "18.80ms", + "cpu": "3088.0%", + "memory": "563MiB", + "connections": 32000, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "180.08MB/s", + "input_bw": "96.06MB/s", + "reconnects": 0, + "status_2xx": 20984027, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "baseline-4096": { + "framework": "apex-tuned", + "language": "C", + "rps": 4057788, + "avg_latency": "1.01ms", + "p99_latency": "1.37ms", + "cpu": "6468.6%", + "memory": "255MiB", + "connections": 4096, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "348.18MB/s", + "input_bw": "313.45MB/s", + "reconnects": 0, + "status_2xx": 20288942, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "json-comp-16384": { + "framework": "apex-tuned", + "language": "C", + "rps": 833553, + "avg_latency": "19.49ms", + "p99_latency": "32.10ms", + "cpu": "6734.3%", + "memory": "496MiB", + "connections": 16384, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "1.26GB/s", + "input_bw": "62.01MB/s", + "reconnects": 159073, + "status_2xx": 4167767, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "json-comp-4096": { + "framework": "apex-tuned", + "language": "C", + "rps": 841346, + "avg_latency": "4.83ms", + "p99_latency": "10.90ms", + "cpu": "6426.1%", + "memory": "267MiB", + "connections": 4096, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "1.27GB/s", + "input_bw": "62.58MB/s", + "reconnects": 166882, + "status_2xx": 4206733, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "json-tls-4096": { + "framework": "apex-tuned", + "language": "C", + "rps": 1774856, + "avg_latency": "1.38ms", + "p99_latency": "57.35ms", + "cpu": "6036.8%", + "memory": "472MiB", + "connections": 4096, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "5.98GB", + "reconnects": 0, + "status_2xx": 9053491, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "latency-10k-1024": { + "framework": "apex-tuned", + "language": "C", + "rps": 9983, + "avg_latency": "72.7us", + "p99_latency": "104.0us", + "cpu": "27.2%", + "memory": "228MiB", + "connections": 1024, + "threads": 64, + "duration": "20s", + "pipeline": 1, + "bandwidth": "0.89MB/s", + "reconnects": 0, + "status_2xx": 199688, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0, + "cpu_usec": 5672631, + "cpu_per_req_us": 28.4075, + "target_rate": 10000, + "rate_ratio": 0.9983, + "p99_9_latency": "255.0us" + }, + "latency-1m-1024": { + "framework": "apex-tuned", + "language": "C", + "rps": 997975, + "avg_latency": "84.2us", + "p99_latency": "158.0us", + "cpu": "2063.8%", + "memory": "232MiB", + "connections": 1024, + "threads": 64, + "duration": "20s", + "pipeline": 1, + "bandwidth": "88.82MB/s", + "reconnects": 0, + "status_2xx": 19962415, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0, + "cpu_usec": 375053527, + "cpu_per_req_us": 18.788, + "target_rate": 1000000, + "rate_ratio": 0.998, + "p99_9_latency": "295.0us" + }, + "latency-500k-8cpu-1024": { + "framework": "apex-tuned", + "language": "C", + "rps": 497185, + "avg_latency": "31983.0us", + "p99_latency": "434304.0us", + "cpu": "789.9%", + "memory": "45MiB", + "connections": 1024, + "threads": 64, + "duration": "20s", + "pipeline": 1, + "bandwidth": "44.25MB/s", + "reconnects": 0, + "status_2xx": 9945525, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0, + "cpu_usec": 149477364, + "cpu_per_req_us": 15.0296, + "target_rate": 500000, + "rate_ratio": 0.9944, + "p99_9_latency": "464256.0us" + }, + "limited-conn-4096": { + "framework": "apex-tuned", + "language": "C", + "rps": 2731380, + "avg_latency": "1.36ms", + "p99_latency": "1.88ms", + "cpu": "5693.1%", + "memory": "285MiB", + "connections": 4096, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "234.34MB/s", + "input_bw": "210.99MB/s", + "reconnects": 1365227, + "status_2xx": 13656904, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "pipelined-4096": { + "framework": "apex-tuned", + "language": "C", + "rps": 48464627, + "avg_latency": "1.35ms", + "p99_latency": "2.07ms", + "cpu": "6504.7%", + "memory": "252MiB", + "connections": 4096, + "threads": 64, + "duration": "5s", + "pipeline": 16, + "bandwidth": "4.06GB/s", + "reconnects": 0, + "status_2xx": 242323136, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "static-tls-1024": { + "framework": "apex-tuned", + "language": "C", + "rps": 615442, + "avg_latency": "1.65ms", + "p99_latency": "17.74ms", + "cpu": "6703.0%", + "memory": "324MiB", + "connections": 1024, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "9.39GB", + "reconnects": 0, + "status_2xx": 3138518, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + } + } +} diff --git a/site/static/logs/8gbit/512/apex-tuned.log b/site/static/logs/8gbit/512/apex-tuned.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/async/32000/apex-tuned.log b/site/static/logs/async/32000/apex-tuned.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/baseline/4096/apex-tuned.log b/site/static/logs/baseline/4096/apex-tuned.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/json-comp/16384/apex-tuned.log b/site/static/logs/json-comp/16384/apex-tuned.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/json-comp/4096/apex-tuned.log b/site/static/logs/json-comp/4096/apex-tuned.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/json-tls/4096/apex-tuned.log b/site/static/logs/json-tls/4096/apex-tuned.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/latency-10k/1024/apex-tuned.log b/site/static/logs/latency-10k/1024/apex-tuned.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/latency-1m/1024/apex-tuned.log b/site/static/logs/latency-1m/1024/apex-tuned.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/latency-500k-8cpu/1024/apex-tuned.log b/site/static/logs/latency-500k-8cpu/1024/apex-tuned.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/limited-conn/4096/apex-tuned.log b/site/static/logs/limited-conn/4096/apex-tuned.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/pipelined/4096/apex-tuned.log b/site/static/logs/pipelined/4096/apex-tuned.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/static-tls/1024/apex-tuned.log b/site/static/logs/static-tls/1024/apex-tuned.log new file mode 100644 index 000000000..e69de29bb