Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
3e96e45
http_connection: keep multishot armed for the connection lifetime
EdmondDantes May 1, 2026
64a968f
examples: add multi-worker server + smoke test + perf analysis doc
EdmondDantes May 1, 2026
7138699
docs: add coroutine overhead analysis vs Swoole
EdmondDantes May 1, 2026
24bdbf7
http: drop libc format_converter from the response hot path
EdmondDantes May 1, 2026
fa4b64e
examples: add minimal-server.php for per-thread bench
EdmondDantes May 1, 2026
c5fd2c9
http: fire-and-forget plaintext response write
EdmondDantes May 2, 2026
dc548c0
http_server: split PHP wrapper from refcounted C-state
EdmondDantes May 2, 2026
29fd57e
http: slab arena for http_connection_t with embedded alive list
EdmondDantes May 2, 2026
4a14ec8
http: per-worker periodic deadline watchdog
EdmondDantes May 2, 2026
14f3b1f
http: switch deadline tracking from zend_hrtime ns to ZEND_ASYNC_NOW ms
EdmondDantes May 2, 2026
ffee257
docs: record step 1-3 perf results
EdmondDantes May 2, 2026
7015c9f
http: coalesce zend_hrtime — pass timestamps as parameters
EdmondDantes May 2, 2026
53e1602
docs: PLAN.md — record step 1-4 results, refresh next-steps list
EdmondDantes May 2, 2026
f4c66b8
http: gate per-request hrtime stamps behind sample_stamps_enabled
EdmondDantes May 2, 2026
3d07c42
docs: PERF_2026_05_02_STEP_4_1.md — confirm stamp gate fires
EdmondDantes May 2, 2026
cba6418
http: vectored writev for large responses (>= 1 KiB)
EdmondDantes May 2, 2026
8f42920
docs: PLAN.md — mark step 10 done, link perf doc
EdmondDantes May 2, 2026
7d499d4
http: use cached fcall_info_cache for handler invocation
EdmondDantes May 2, 2026
20570b9
docs: PLAN.md — mark step 6 done
EdmondDantes May 2, 2026
1b954c1
docs: PLAN.md — record TLS handshake regression as Шаг 0 blocker
EdmondDantes May 2, 2026
5b11bf0
tls: clear per-conn alloc_cb to fix handshake regression
EdmondDantes May 2, 2026
ebe58a0
http: inline sample_stamps gate and request counter
EdmondDantes May 2, 2026
6dc826b
http: coarse clock for long-window drain decisions
EdmondDantes May 2, 2026
df60fc9
tls: zero-copy WRITE_EX for ciphertext send path
EdmondDantes May 2, 2026
66af11b
http: batched fire-and-forget plaintext send for HTTP/2 commit
EdmondDantes May 3, 2026
a5153e2
docs: PLAN.md — record step 7 (H/2 throttle + batched send) results
EdmondDantes May 3, 2026
3e7e518
http2/3: pool stream slots, embed http_request_t at offset 0
EdmondDantes May 3, 2026
661690f
http: latch parse_error to avoid double-count on multishot tail
EdmondDantes May 3, 2026
4321bd3
tests: rehydrate unit + phpt suite vs current source tree
EdmondDantes May 3, 2026
9ed7c8e
refactor: remove dead server-less/pool-less paths, purge noise comments
EdmondDantes May 3, 2026
db6e148
build: compile tls_layer.c only when OpenSSL is present
EdmondDantes May 3, 2026
f1b1a65
http: add getPath() / getQuery() / getQueryParam() with lazy URI parsing
EdmondDantes May 3, 2026
39c66de
docs: CHANGELOG — document getPath/getQuery/getQueryParam
EdmondDantes May 3, 2026
292e26c
refactor: remove HTTP2 and HTTP3 feature guards from header files
EdmondDantes May 4, 2026
f2acd16
http: sweep arena alive list on server free to fix shutdown leak
EdmondDantes May 4, 2026
6c1c14e
ci: use two-dot diff for touched-files list
EdmondDantes May 4, 2026
6f83ce3
ci: also fetch HEAD_SHA for touched-files diff
EdmondDantes May 4, 2026
5d6dc9d
ci: update coverage regression check to issue a warning instead of fa…
EdmondDantes May 4, 2026
1d6ebc9
docs: update TODO.md with remaining tasks for HTTP/3 implementation
EdmondDantes May 4, 2026
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
11 changes: 7 additions & 4 deletions .github/workflows/build-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -572,8 +572,12 @@ jobs:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -eux
# actions/checkout for pull_request checks out the merge ref,
# so neither BASE nor HEAD is guaranteed to be present locally —
# fetch both as explicit objects before diffing.
git fetch --no-tags --depth=200 origin "$BASE_SHA" || true
git diff --name-only "$BASE_SHA"..."$HEAD_SHA" > /tmp/touched.txt
git fetch --no-tags --depth=200 origin "$HEAD_SHA" || true
git diff --name-only "$BASE_SHA" "$HEAD_SHA" > /tmp/touched.txt
wc -l /tmp/touched.txt
head -50 /tmp/touched.txt

Expand Down Expand Up @@ -646,11 +650,10 @@ jobs:
owner, repo, issue_number, body: tagged });
}

- name: Fail on coverage regression
- name: Warn on coverage regression
if: matrix.debug == false && github.event_name == 'pull_request' && steps.compare.outputs.exit != '0'
run: |
echo "Coverage gate failed (exit=${{ steps.compare.outputs.exit }})." >&2
exit 1
echo "::warning::Coverage regression detected (exit=${{ steps.compare.outputs.exit }}). See coverage comment for details."

- name: Refresh baseline on main
if: matrix.debug == false && github.event_name == 'push' && github.ref == 'refs/heads/main'
Expand Down
20 changes: 19 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,25 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [0.2.0] - 2026-05-04

### Added

- **`HttpRequest::getPath()`** — returns the URI path without the query
string (e.g. `/search` from `/search?q=hello`). Works identically for
HTTP/1.1, HTTP/2 (`:path` pseudo-header), and HTTP/3.
- **`HttpRequest::getQuery(): array`** — returns all query parameters as
an associative array, equivalent to `$_GET`. Supports percent-decoding,
`+`-as-space, and PHP array notation (`foo[]`, `foo[bar]`).
- **`HttpRequest::getQueryParam(string $name, mixed $default = null): mixed`** —
returns a single query parameter by name, or `$default` (null unless
overridden) when the parameter is absent.

All three methods share a single lazy parse: the URI is split into path
and query string on the first call and the result is cached in the
request struct for subsequent accesses. The query parser delegates to
`php_default_treat_data(PARSE_STRING, …)` — the same function PHP uses
to populate `$_GET`.

### Fixed

Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ set(CORE_SOURCES
# Source files - Core subsystem
set(CORE_SUBSYSTEM_SOURCES
src/core/http_connection.c
src/core/conn_arena.c
src/core/http_protocol_handlers.c
src/core/http_protocol_strategy.c
src/core/tls_layer.c
Expand Down
27 changes: 27 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# TODO — HTTP Server Performance Backlog

## Step 1 — HTTP/3 hot path

Most of this is already done: handler coroutines do not suspend on write, UDP_SEGMENT / GSO batching is implemented in `src/http3/http3_listener.c:584` (`http3_listener_send_gso`), and Linux uses direct `sendmsg(MSG_DONTWAIT)` bypassing libuv.

Remaining:
- `MSG_ZEROCOPY` for large QUIC DATA frames (> 8 KB) — can be addressed together with Step 3

## Step 2 — Full TLS optimization (deferred)

When revisited:
- `setsockopt(TCP_ULP, "tls")` — kernel TLS offload (kTLS)
- `SSL_sendfile` for large responses
- Reduce memcpy between BIO ring buffers
- Switch to socket-BIO to eliminate the extra copy layer

## Step 3 — Zero-copy for large responses

**Goal**: avoid CPU cost of copying large response bodies into kernel on send.

- Threshold-based: apply only when `len > 16 KB` (page-pin overhead makes it harmful for small responses)
- Add a flag to `ZEND_ASYNC_IO_WRITE_EX` to request zero-copy mode
- `libuv_reactor.c`: direct `send(MSG_ZEROCOPY)` bypassing libuv, drain error queue via `recvmsg(MSG_ERRQUEUE)` to invoke `free_cb`
- `iouring_reactor.c`: `IORING_OP_SEND_ZC`

**Expected effect**: 10–30% CPU saving on large-body responses; more significant on NUMA under L3 cache bandwidth pressure.
5 changes: 3 additions & 2 deletions config.m4
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ if test "$PHP_HTTP_SERVER" != "no"; then
src/http_server_config.c
src/http_server_class.c
src/core/http_connection.c
src/core/conn_arena.c
src/http1/http_parser.c
src/http1/http1_stream.c
src/formats/multipart_parser.c
Expand All @@ -347,7 +348,6 @@ if test "$PHP_HTTP_SERVER" != "no"; then
src/uploaded_file.c
src/core/http_protocol_handlers.c
src/core/http_protocol_strategy.c
src/core/tls_layer.c
src/core/http_known_strings.c
src/log/http_log.c
src/log/trace_context.c
Expand All @@ -357,7 +357,7 @@ if test "$PHP_HTTP_SERVER" != "no"; then
dnl handshake/decrypt coroutine). Compiled only when OpenSSL is present
dnl so non-TLS builds stay smaller.
if test "$_http_server_openssl_ok" = "yes"; then
http_server_sources="$http_server_sources src/core/http_connection_tls.c"
http_server_sources="$http_server_sources src/core/tls_layer.c src/core/http_connection_tls.c"
fi

dnl HTTP/2 sources compile only when --enable-http2 succeeded and
Expand All @@ -383,6 +383,7 @@ if test "$PHP_HTTP_SERVER" != "no"; then
src/http3/http3_callbacks.c
src/http3/http3_dispatch.c
src/http3/http3_stream.c
src/http3/http3_stream_pool.c
"
fi

Expand Down
19 changes: 19 additions & 0 deletions examples/certs/server.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDCTCCAfGgAwIBAgIUbEz52reYuYYb26TfVwBpyW3YACUwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDMwNzEzNTYwNFoXDTM2MDMw
NDEzNTYwNFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAtJGyejZirwk/Yh5v0VwTY7YLBn+zLmHTcmkZeSiSwIUb
P6y8vRigFoQHKSrqaS9WB49EXpuxwFTzxY0zPDjg/boJEVaPp+ct1YxS7DY93zQo
0CSZCG/PYsw2SeXpBE93/UCajDJp3vHZXPi2tuT/zL/+dy8Si/r+ln9W8YiNINJm
0Y2AnHgSoFDAt6sZY4B6R/FsHY3oeYlKIxIJIHZvwFpY4o1/Mr1Q8ZbfsGONEFGk
Fsp/xB9FVFFNlM5mf3rNoRHfXZb6QykHMW2P9nXEx2cnG57xLaHhZAEzrilQ6/6G
sI3ypxTaQCQinyz/l6yeG+uBbe4slXPHty6ue1RYeQIDAQABo1MwUTAdBgNVHQ4E
FgQU+unU067fqVnaWvCOXgh2TIF55TUwHwYDVR0jBBgwFoAU+unU067fqVnaWvCO
Xgh2TIF55TUwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAalzP
zEpDH1858MTdbLf21D08DsW2BxXiZ2KsFVhE+UW4+utfGdi4s3qXbu2C4H4A4us7
rmUYAXLZW9uZumV5yy/CfXbw3E9vy/kKztH6qNVaMfmEZNa4aNcCKziwef+/3x7e
zI6o5pyw3ffhw9c8QIqD7ULBKLHdBKvE+NaTAzQNKWJoDDPd1HdeLkIzz4nm0xPR
Zc8xAhIfOlniYzZ+y3NpCd7kOhfpYwMQY8mjP7cLYIQsoS2gD9reYxs2dvp/T8Y+
skU1doO8r4ryU/wR60XXQs4ihykA8z/QUxqCgm8q4YPl8sKHUKjN9+fc0QRQCbKR
g6nb97XFdZV9IZqtqQ==
-----END CERTIFICATE-----
28 changes: 28 additions & 0 deletions examples/certs/server.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC0kbJ6NmKvCT9i
Hm/RXBNjtgsGf7MuYdNyaRl5KJLAhRs/rLy9GKAWhAcpKuppL1YHj0Rem7HAVPPF
jTM8OOD9ugkRVo+n5y3VjFLsNj3fNCjQJJkIb89izDZJ5ekET3f9QJqMMmne8dlc
+La25P/Mv/53LxKL+v6Wf1bxiI0g0mbRjYCceBKgUMC3qxljgHpH8Wwdjeh5iUoj
Egkgdm/AWljijX8yvVDxlt+wY40QUaQWyn/EH0VUUU2UzmZ/es2hEd9dlvpDKQcx
bY/2dcTHZycbnvEtoeFkATOuKVDr/oawjfKnFNpAJCKfLP+XrJ4b64Ft7iyVc8e3
Lq57VFh5AgMBAAECggEAEvtsCUf1WNQ+iwiLFbW5vhYxk4XSJtKW4WSmDmQTBVUY
17lHgBN6JNPNUukVimg1AYdwlweECKWFmONumuqZ0GKBuIZihLKbUWM2hmlvWKsJ
jVQDmGz0nry8Ckm2lMLr6L4lYQ6drZe8E3d78b8iGvql/A6BQyDoKZcKY0rJF0WJ
3/mFIrLWELxv1xbJEVnfIQX57NEAPnFcL1VmnDhjpYcQrlDDIHkBiI2goGcdxUQo
sxI029VAQt/37xlvMf3gDcoxFuzSPnqPeqjdy0vK3raSGF+JQPpXAzJZRkEFkDi4
j4ojJtcdEPDOxpm7A+o+LhbIBG463oCaEODbjf0lWQKBgQDYcGaku1a1BGfMFyZ6
d28DKkamCSEtu09c77FjJWkQf71Kxa7f88KUZEnIUkRCmg/0bNvwjdlY+oL2OfXt
xf1EC6dlLzdSKjQhqGPHex/515xqt2bY9PE8Q4jQvsuN0QtmGqguup6n2Q4B0MuI
56LhKkwCg47q7qo/RKgiqku8FQKBgQDVkt+C7wKIhhkgydCCmkkltOVt96FJhlY3
tr8WZc6iwMN3/7imEwkBRgHuu/lroTuAKtYhciFuO112XFY4zAeT44zQ1EmUGGgR
w4MWTZvnWAD036vy0kdze35xvRlcbb7o6cY6yv23L0xjrOawdoID1DbDEvB+9B01
08oaFOUv1QKBgE1kU6+PtT9g5eSaWo3r6uwMz9pK5Ww+z/ABXUKAfAMESiFUcmVt
+iOpgKB6miHeiNnzmul3L0KbwPxeWUu+QgN0z1Rk/7kHkkB+v77yjcp+iFW4YrQt
UZ0k4OUTdCGvoA3QdlbPMDAAcvu/Nygq+5jb0PYNKKtkz8dzu1M55X7JAoGAAnkz
+3k3J0ueSOHtd0XAKR6iNZbTmF1k7DpClkjRjtL6sI4Wnl3EEe60oQYuSk/Qt5hH
aJXAy10GpRNGsFu0jsLo45ZBz+REeEgyYXS+pHxBbpSUkjhbOXwpp7mP7KEcv+fN
Musc6x0yHklnVo3YzaCMjc/PVzkOiYwNYCXzzY0CgYA8c9kQzLxAjQrns+7AYhap
+p6SN4vksHdQgfy2gcc2/MtvwKgD38DL8Gapvkf/vzItTPja3vdYhcILLOb3oarM
198aeahujWgkfgvovllWrIhZrNkrpbA/Wak3hbxAWrPZEdTm2MclJBg/+2z4nAI5
RYpItECfqAvggb+3GH9phA==
-----END PRIVATE KEY-----
48 changes: 48 additions & 0 deletions examples/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Multi-worker TrueAsync server smoke test, built on the official image.
#
# The base image ships php-true-async + true_async_server + ngtcp2/nghttp3
# libraries. We add curl/wrk/nc/gcc, build the h3client harness, drop the
# entrypoint and certs in, and run.
#
# docker build -t tas-smoke -f examples/docker/Dockerfile .
# docker run --rm -p 18080:8080 -p 18443:8443 -p 18443:8443/udp tas-smoke
# docker run --rm tas-smoke /app/smoke-test.sh

FROM trueasync/php-true-async:0.7.0-alpha.2-php8.6

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
curl wrk bash netcat-openbsd nghttp2-client \
gcc libc6-dev pkg-config libssl-dev \
&& rm -rf /var/lib/apt/lists/*

COPY examples/multi-worker.php /app/multi-worker.php
COPY examples/docker/smoke-test.sh /app/smoke-test.sh
COPY examples/certs/ /certs/
COPY tests/h3client/h3client.c /tmp/h3client.c

# Build h3client against the base image's ngtcp2/nghttp3 + ossl_crypto.
RUN gcc /tmp/h3client.c -o /app/h3client \
$(pkg-config --cflags --libs libngtcp2 libnghttp3 openssl) \
-lngtcp2_crypto_ossl \
&& rm /tmp/h3client.c \
&& chmod +x /app/smoke-test.sh

# Long-running CLI server -> opcache + JIT.
RUN { \
echo 'opcache.enable=1'; \
echo 'opcache.enable_cli=1'; \
echo 'opcache.jit=1255'; \
echo 'opcache.jit_buffer_size=128M'; \
echo 'opcache.memory_consumption=256'; \
echo 'opcache.validate_timestamps=0'; \
echo 'memory_limit=512M'; \
} > /etc/php.d/99-benchmark.ini

ENV PORT=8080 TLS_PORT=8443 H3_PORT=8443 \
TLS_CERT=/certs/server.crt TLS_KEY=/certs/server.key \
WORKERS=0
EXPOSE 8080 8443 8443/udp

CMD ["php", "/app/multi-worker.php"]
128 changes: 128 additions & 0 deletions examples/docker/smoke-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
#!/usr/bin/env bash
# In-container smoke test: starts multi-worker.php in the background, hits
# every endpoint, validates keep-alive + pipelining + multi-worker dispatch,
# runs a short wrk burst, then stops.

set -euo pipefail

PORT="${PORT:-8080}"
TLS_PORT="${TLS_PORT:-8443}"
H3_PORT="${H3_PORT:-$TLS_PORT}"
TLS_CERT="${TLS_CERT:-/certs/server.crt}"
TLS_KEY="${TLS_KEY:-/certs/server.key}"
WORKERS="${WORKERS:-0}" # 0 => auto via available_parallelism()
H3CLIENT="${H3CLIENT:-/app/h3client}"
LOG="$(mktemp)"

TLS_AVAILABLE=0
[[ -r "$TLS_CERT" && -r "$TLS_KEY" ]] && TLS_AVAILABLE=1

echo ">> WORKERS=$WORKERS (0 = auto) PORT=$PORT TLS_PORT=$TLS_PORT TLS=$TLS_AVAILABLE"
WORKERS="$WORKERS" PORT="$PORT" TLS_PORT="$TLS_PORT" H3_PORT="$H3_PORT" \
TLS_CERT="$TLS_CERT" TLS_KEY="$TLS_KEY" \
php /app/multi-worker.php >"$LOG" 2>&1 &
SERVER_PID=$!
cleanup() {
kill "$SERVER_PID" 2>/dev/null || true
wait "$SERVER_PID" 2>/dev/null || true
echo ">> server log tail:"
tail -n 5 "$LOG" | sed 's/^/ /'
}
trap cleanup EXIT

ready=0
for _ in $(seq 1 50); do
curl -fsS "http://127.0.0.1:$PORT/" >/dev/null 2>&1 && { ready=1; break; }
sleep 0.1
done
[[ "$ready" == 1 ]] || { echo "server failed to start" >&2; exit 1; }

grep -E "cpu sources|workers ·" "$LOG" | sed 's/^/ /'

pass() { echo " PASS $1"; }
fail() { echo " FAIL $1: $2" >&2; exit 1; }
expect() {
local label="$1" want="$2" got="$3"
[[ "$got" == "$want" ]] && pass "$label" || fail "$label" "want [$want] got [$got]"
}

expect "GET /" "ok" "$(curl -fsS "http://127.0.0.1:$PORT/")"
expect "GET /pipeline" "ok" "$(curl -fsS "http://127.0.0.1:$PORT/pipeline")"
expect "GET /baseline sum" "6" "$(curl -fsS "http://127.0.0.1:$PORT/baseline?a=1&b=2&c=3")"
expect "POST /baseline body" "15" "$(curl -fsS -X POST -d '10' "http://127.0.0.1:$PORT/baseline?n=5")"
expect "GET /baseline empty" "0" "$(curl -fsS "http://127.0.0.1:$PORT/baseline")"

JSON="$(curl -fsS "http://127.0.0.1:$PORT/json/3")"
[[ "$JSON" == *'"count":3'* && "$JSON" == *'"items"'* ]] \
&& pass "GET /json/3 shape" || fail "json shape" "$JSON"

JSON_BIG="$(curl -fsS "http://127.0.0.1:$PORT/json/9999")"
[[ "$JSON_BIG" == *'"count":64'* ]] \
&& pass "GET /json/9999 clamped to 64" || fail "json clamp" "$JSON_BIG"

STATUS="$(curl -sS -o /dev/null -w '%{http_code}' "http://127.0.0.1:$PORT/missing")"
expect "404 unknown path" "404" "$STATUS"

CT="$(curl -sSI "http://127.0.0.1:$PORT/json/1" | awk -F': *' 'tolower($1)=="content-type"{print $2}' | tr -d '\r')"
expect "json content-type" "application/json" "$CT"

KA_SUM="$(curl -fsS -w '%{num_connects}\n' \
-o /dev/null "http://127.0.0.1:$PORT/" \
-o /dev/null "http://127.0.0.1:$PORT/" \
-o /dev/null "http://127.0.0.1:$PORT/" \
-o /dev/null "http://127.0.0.1:$PORT/" \
-o /dev/null "http://127.0.0.1:$PORT/" \
| awk '{s+=$1} END{print s}')"
expect "keep-alive (1 connect for 5 requests)" "1" "$KA_SUM"

if command -v nc >/dev/null; then
PIPE="$(printf 'GET /pipeline HTTP/1.1\r\nHost: x\r\n\r\nGET /pipeline HTTP/1.1\r\nHost: x\r\n\r\nGET /pipeline HTTP/1.1\r\nHost: x\r\n\r\nGET /pipeline HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n' \
| timeout 3 nc -q1 127.0.0.1 "$PORT" 2>/dev/null | grep -c '^ok' || true)"
[[ "$PIPE" -ge 4 ]] \
&& pass "HTTP/1.1 pipelining (4 responses)" \
|| fail "pipelining" "got $PIPE 'ok' bodies"
fi

DISTINCT="$(for _ in $(seq 1 64); do
curl -fsS "http://127.0.0.1:$PORT/pid"
done | awk '{print $2}' | sort -u | wc -l)"
[[ "$DISTINCT" -ge 2 ]] \
&& pass "REUSEPORT spread across workers ($DISTINCT distinct counters)" \
|| fail "multi-worker" "only $DISTINCT distinct counter(s)"

if [[ "$TLS_AVAILABLE" == 1 ]]; then
echo ">> TLS / HTTP/2 / HTTP/3 checks"

H2="$(curl -sSk --http2 -w '%{http_code} %{http_version}' -o /dev/null "https://127.0.0.1:$TLS_PORT/")"
expect "HTTP/2 ALPN handshake + 200" "200 2" "$H2"

H2_BODY="$(curl -sSk --http2 "https://127.0.0.1:$TLS_PORT/json/2")"
[[ "$H2_BODY" == *'"count":2'* ]] \
&& pass "HTTP/2 body roundtrip" || fail "h2 body" "$H2_BODY"

ALTSVC="$(curl -sSkI --http2 "https://127.0.0.1:$TLS_PORT/" | awk -F': *' 'tolower($1)=="alt-svc"{print $2}' | tr -d '\r"')"
[[ "$ALTSVC" == *"h3="* ]] \
&& pass "Alt-Svc advertises h3 ($ALTSVC)" \
|| fail "alt-svc" "got [$ALTSVC]"

if [[ -x "$H3CLIENT" ]]; then
H3_OUT="$("$H3CLIENT" 127.0.0.1 "$H3_PORT" / 2>&1)"
if [[ "$H3_OUT" == *"STATUS=200"* && "$H3_OUT" == *"ok"* ]]; then
pass "HTTP/3 GET / -> 200, body=ok"
else
fail "HTTP/3" "$H3_OUT"
fi
H3_JSON="$("$H3CLIENT" 127.0.0.1 "$H3_PORT" /json/3 2>&1)"
[[ "$H3_JSON" == *'"count":3'* ]] \
&& pass "HTTP/3 GET /json/3" || fail "h3 json" "$H3_JSON"
else
echo " SKIP HTTP/3 (h3client not built)"
fi
fi

if command -v wrk >/dev/null; then
echo ">> 5s wrk -t4 -c64"
wrk -t4 -c64 -d5s "http://127.0.0.1:$PORT/" | grep -E "Requests/sec|Latency|requests in" | sed 's/^/ /'
fi

echo ">> done"
23 changes: 23 additions & 0 deletions examples/minimal-server.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
/**
* Minimal handler — equivalent of Swoole's
* $http->on('request', fn($req,$res) => $res->end('ok'));
*
* Used for fair-apples-to-apples per-thread profiling.
* Run with WORKERS=1 to compare against Swoole reactor=1+worker=1.
*/

use TrueAsync\HttpServer;
use TrueAsync\HttpServerConfig;

$config = (new HttpServerConfig())
->addListener('0.0.0.0', (int)(getenv('PORT') ?: 8080))
->setBacklog(2048)
->setReadTimeout(15)
->setWriteTimeout(15)
->setKeepAliveTimeout(60);

$server = new HttpServer($config);
$server->addHttpHandler(fn($req, $res) => $res->setBody('ok'));
fprintf(STDERR, "[minimal-server] :%d pid=%d\n", (int)(getenv('PORT') ?: 8080), getmypid());
$server->start();
Loading
Loading