Skip to content

Latest commit

 

History

10 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Stoke

Lire en français

A PHP application server. libphp is linked into a Rust binary, with a custom SAPI and hyper on top. No FastCGI, no reverse proxy, no process manager.

Proof of concept. It is tested like production software. 31 conformance cases diffed byte for byte against php-fpm, 21 fault injections, 6 streaming checks, 13 unit tests, a 10 minute soak. All of it passes. It is still not production software. One contributor, no deployment history, no security review, no HTTP/3. Use FrankenPHP or php-fpm for anything real.

Results

Symfony 7.2, prod, warm cache, 6 workers, 64 connections, median of 3 runs.

stack route rps p50 p99 memory
stoke worker / 59 386 0.96 ms 4.30 ms 81 MB
frankenphp worker / 24 574 2.31 ms 7.59 ms 196 MB
stoke worker /json 100 KB 8 971 6.47 ms 18.31 ms 83 MB
frankenphp worker /json 100 KB 7 278 8.50 ms 16.31 ms 199 MB
stoke classic / 4 857 13.29 ms 28.82 ms 50 MB
php-fpm + nginx / 4 046 15.53 ms 25.93 ms 40 MB

CPU per request: stoke 105 µs, FrankenPHP 252 µs, php-fpm 1 952 µs. The p99 on large responses changes sides between runs, so treat those two as tied.

Measured in a Docker Desktop VM on an Apple M4. The numbers are only valid relative to each other. Every stack ran in the same container, on the same application, in the same session.

Why it is fast

PHP runs on the thread hyper hands the request to. No channel, no wakeup. The engine is not thread-safe, so the server forks instead of using threads.

The app is booted before forking. Workers share its heap copy-on-write: one worker costs 38 MB, sixteen cost 44 MB.

$_SERVER barely changes between requests, so it is copied from a persistent hashtable instead of registered entry by entry.

Running it

docker compose -f docker/compose.yml up -d dev
docker compose -f docker/compose.yml exec dev cargo build --release

STOKE_ROOT=/app/public stoke                                      # classic
STOKE_ROOT=/app/public STOKE_WORKER_SCRIPT=/app/worker.php stoke   # worker

A worker script returns a callable:

<?php
$kernel = new Kernel('prod', false);
$kernel->boot();

return function () use ($kernel) {
    $request = Request::createFromGlobals();
    $response = $kernel->handle($request);
    $response->send();
    $kernel->terminate($request, $response);
};

Worker scripts written for FrankenPHP run unmodified. frankenphp_handle_request($handler) is accepted and takes the callable.

Return an array instead to get a per-worker start hook. It runs once in each forked worker. That is where you reopen anything the pre-fork bootstrap left shared:

return [
    'on_worker_start' => fn () => $pool->reconnect(),
    'handler' => fn () => $kernel->handle(Request::createFromGlobals())->send(),
];

Settings come from the environment. STOKE_CONFIG points at a file using the same names without the prefix (workers = 4). The environment wins over the file.

variable default
STOKE_LISTEN 0.0.0.0:8080
STOKE_ROOT /app/public document root
STOKE_WORKERS CPU count worker processes
STOKE_WORKER_SCRIPT enables worker mode
STOKE_FRONT_CONTROLLER classic-mode fallback script
STOKE_TLS_CERT / STOKE_TLS_KEY enables TLS, ALPN negotiates HTTP/2
STOKE_STREAM 0 send output as PHP produces it. Needed for SSE and long polling, costs about 10% throughput
STOKE_STATIC 1 serve static files
STOKE_MAX_BODY 8 MiB request body limit
STOKE_LOOP_MAX 0 recycle a worker after N requests
STOKE_PREFORK_BOOT 1 boot the app before forking, so workers share its memory
STOKE_INI extra php.ini directives, comma separated
STOKE_CONFIG path to a settings file

HTTP/1.1 and HTTP/2 share the port, with or without TLS.

The test suites run with sh tests/run-all.sh inside the dev container.

Scope

HTTP/3 and automatic certificates are not planned. That is the proxy's job. No point rebuilding Caddy.

Two things to know. With STOKE_STREAM=1, a client that stops reading blocks its worker until max_execution_time. Every unbuffered server does that. With STOKE_PREFORK_BOOT=1, workers share whatever the bootstrap opened. The on_worker_start hook is there to reopen it, otherwise set the flag to 0.

About

PHP application server written in Rust

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages