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
87 changes: 87 additions & 0 deletions .github/workflows/pgo-profile.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: Refresh PGO profile
# Regenerates caddy/frankenphp/default.pgo by hammering frankenphp with wrk in
# both regular and worker mode (see build-pgo.sh) and opens a PR with the
# new merged profile. Direct `go build` and the Dockerfile auto-detect it;
# xcaddy users still need an explicit --pgo flag.
concurrency:
cancel-in-progress: false
group: ${{ github.workflow }}-${{ github.ref }}
on:
push:
branches:
- main
paths:
- "**/*.go"
- "frankenphp.c"
- "profiles/**"
- ".github/workflows/pgo-profile.yaml"
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
refresh:
environment: pgo
name: Generate PGO profile
runs-on: ubuntu-latest
env:
GOTOOLCHAIN: local
GOFLAGS: "-tags=nobadger,nomysql,nopgx"
LIBRARY_PATH: ${{ github.workspace }}/watcher/target/lib
BENCH_SEC: "30"
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
# zizmor: ignore[artipacked]
# persist-credentials is intentionally left enabled because this
# workflow needs to push a branch via git push (mirrors translate.yaml).
- uses: actions/setup-go@v6
with:
go-version: "1.26"
cache-dependency-path: |
go.sum
caddy/go.sum
- uses: shivammathur/setup-php@v2
with:
php-version: "8.5"
ini-file: development
coverage: none
tools: none
env:
phpts: ts
debug: true
- name: Install e-dant/watcher
uses: ./.github/actions/watcher
- name: Set CGO flags
run: echo "CGO_CFLAGS=-I${PWD}/watcher/target/include $(php-config --includes)" >> "${GITHUB_ENV}"
- name: Install wrk
run: sudo apt-get update && sudo apt-get install -y wrk
- name: Generate profile
run: ./profiles/build-pgo.sh
- name: Show pprof summary
run: |
go install github.com/google/pprof@latest
"$(go env GOPATH)/bin/pprof" -top -cum -nodecount=25 caddy/frankenphp/default.pgo || true
- name: Open PR with refreshed profile
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ACTOR: ${{ github.actor }}
ACTOR_ID: ${{ github.actor_id }}
RUN_ID: ${{ github.run_id }}
SOURCE_SHA: ${{ github.sha }}
BENCH_SEC: ${{ env.BENCH_SEC }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
BRANCH="pgo/refresh-$RUN_ID"
git checkout -b "$BRANCH"
git add caddy/frankenphp/default.pgo
git diff --cached --quiet && exit 0
git commit -m "perf(pgo): refresh PGO profile" --author="$ACTOR <$ACTOR_ID+$ACTOR@users.noreply.github.com>"
git push origin "$BRANCH"
gh pr create \
--title "perf(pgo): refresh PGO profile" \
--body "Automated refresh of \`caddy/frankenphp/default.pgo\` from \`$SOURCE_SHA\`, generated with \`BENCH_SEC=$BENCH_SEC\` across every script in \`profiles/app/\` in both regular and worker mode." \
--label "perf" \
--label "bot"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/caddy/frankenphp/Build
/caddy/frankenphp/Caddyfile.test
/caddy/frankenphp/frankenphp
/caddy/frankenphp/frankenphp-pgo
/caddy/frankenphp/frankenphp.exe
/caddy/frankenphp/public
/dist
Expand Down
Binary file added caddy/frankenphp/default.pgo
Binary file not shown.
8 changes: 7 additions & 1 deletion frankenphp.c
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ __thread HashTable *sandboxed_env = NULL;
#ifndef PHP_WIN32
static bool is_forked_child = false;
static void frankenphp_fork_child(void) { is_forked_child = true; }

static void frankenphp_register_atfork(void) {
pthread_atfork(NULL, NULL, frankenphp_fork_child);
}
#endif

void frankenphp_update_local_thread_context(bool is_worker) {
Expand Down Expand Up @@ -711,7 +715,9 @@ PHP_FUNCTION(frankenphp_log) {
PHP_MINIT_FUNCTION(frankenphp) {
register_frankenphp_symbols(module_number);
#ifndef PHP_WIN32
pthread_atfork(NULL, NULL, frankenphp_fork_child);
/* MINIT runs once per ZTS thread — guard the atfork registration */
static pthread_once_t atfork_once = PTHREAD_ONCE_INIT;
pthread_once(&atfork_once, frankenphp_register_atfork);
#endif

zend_function *func;
Expand Down
15 changes: 15 additions & 0 deletions profiles/app/Caddyfile.regular
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# this is, intentionally, not an optimised configuration
# we're trying to cover as many codepaths as possible

{
skip_install_trust
admin localhost:22019
grace_period 0s
}

:22080 {
root * .
encode zstd br gzip

php_server
}
17 changes: 17 additions & 0 deletions profiles/app/Caddyfile.worker
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# this is, intentionally, not an optimised configuration
# we're trying to cover as many codepaths as possible

{
skip_install_trust
admin localhost:22019
grace_period 0s
}

:22080 {
root * .
encode zstd br gzip

php_server {
worker index.php
}
}
19 changes: 19 additions & 0 deletions profiles/app/cookies_headers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php
$cookieCount = count($_COOKIE);
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
$accept = $_SERVER['HTTP_ACCEPT'] ?? '';
$auth = $_SERVER['HTTP_AUTHORIZATION'] ?? '';

header('Content-Type: text/plain; charset=utf-8');
header('Cache-Control: private, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('X-Frame-Options: DENY');
header('X-Content-Type-Options: nosniff');
header('X-XSS-Protection: 1; mode=block');
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
header('Referrer-Policy: strict-origin-when-cross-origin');
header('Permissions-Policy: camera=(), microphone=(), geolocation=()');
header('Set-Cookie: trace=' . substr(md5((string)microtime(true)), 0, 16) . '; Path=/; HttpOnly; SameSite=Lax');
header('X-Request-Id: ' . substr(md5((string)microtime(true)), 0, 16));

echo "cookies=$cookieCount ua_len=" . strlen($ua) . " auth_len=" . strlen($auth) . "\n";
15 changes: 15 additions & 0 deletions profiles/app/file_io.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php
$file = '/tmp/benchmark_' . bin2hex(random_bytes(16)) . '.txt';

$data = str_repeat('Lorem ipsum dolor sit amet, consectetur adipiscing elit. ', 1000);
file_put_contents($file, $data);

$content = file_get_contents($file);

$lines = explode(' ', $content);
$filtered = array_filter($lines, fn($line) => strlen($line) > 5);
sort($filtered);

unlink($file);

echo "OK\n";
11 changes: 11 additions & 0 deletions profiles/app/frankenphp_log.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php
header('Content-Type: text/plain; charset=utf-8');

if (function_exists('frankenphp_log')) {
frankenphp_log('pgo profile request', FRANKENPHP_LOG_LEVEL_DEBUG, ['s' => 'fp_log']);
frankenphp_log('pgo profile request', FRANKENPHP_LOG_LEVEL_INFO);
} else {
error_log('frankenphp_log unavailable');
}

echo "ok\n";
2 changes: 2 additions & 0 deletions profiles/app/helloworld.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?php
echo "Hello World!";
21 changes: 21 additions & 0 deletions profiles/app/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

$handler = static function (): void {
$s = $_GET['s'] ?? '';
if (!preg_match('/^[a-z_]+$/', $s)) {
http_response_code(404);
return;
}
$file = __DIR__ . DIRECTORY_SEPARATOR . $s . '.php';
if (!is_file($file)) {
http_response_code(404);
return;
}
require $file;
};

if (isset($_SERVER['FRANKENPHP_WORKER'])) {
while (frankenphp_handle_request($handler)) {}
} else {
$handler();
}
7 changes: 7 additions & 0 deletions profiles/app/large_response.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php
// trigger encoder middleware in Caddy
header('Content-Type: text/plain; charset=utf-8');

$chunk = str_repeat('Lorem ipsum dolor sit amet, consectetur adipiscing elit. ', 64);
$payload = str_repeat($chunk, 64);
echo $payload;
37 changes: 37 additions & 0 deletions profiles/app/mandelbrot.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php
$start = microtime(true);

$width = 80;
$height = 40;
$maxIter = 100;

$xmin = -2.5;
$xmax = 1.0;
$ymin = -1.0;
$ymax = 1.0;

$chars = ' .:-=+*#%@';

for ($y = 0; $y < $height; $y++) {
for ($x = 0; $x < $width; $x++) {
$cx = $xmin + ($x / $width) * ($xmax - $xmin);
$cy = $ymin + ($y / $height) * ($ymax - $ymin);

$zx = 0;
$zy = 0;
$iter = 0;

while ($zx * $zx + $zy * $zy < 4 && $iter < $maxIter) {
$tmp = $zx * $zx - $zy * $zy + $cx;
$zy = 2 * $zx * $zy + $cy;
$zx = $tmp;
$iter++;
}

$charIndex = min(strlen($chars) - 1, (int)(($iter / $maxIter) * strlen($chars)));
echo $chars[$charIndex];
}
echo "\n";
}

printf("Mandelbrot %dx%d iter=%d time=%.4fs\n", $width, $height, $maxIter, microtime(true) - $start);
4 changes: 4 additions & 0 deletions profiles/app/post_body.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php
header('Content-Type: text/plain');
$body = file_get_contents('php://input');
echo "len=" . strlen($body);
5 changes: 5 additions & 0 deletions profiles/app/random.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php
$random = new \Random\Randomizer(new \Random\Engine\Xoshiro256StarStar());
for ($i = 0; $i < 50; $i++) {
echo $random->getBytesFromString("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", 1023), "\n";
}
12 changes: 12 additions & 0 deletions profiles/app/status_codes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php
$codes = [200, 201, 202, 204, 301, 302, 304, 400, 401, 403, 404, 410, 418, 500, 502, 503];
$code = $codes[mt_rand(0, count($codes) - 1)];

http_response_code($code);
header('Content-Type: text/plain; charset=utf-8');
header('X-Status: ' . $code);

if ($code === 204 || $code === 304) {
return;
}
echo "code=$code\n";
14 changes: 14 additions & 0 deletions profiles/app/streaming.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
header('Content-Type: text/plain; charset=utf-8');
header('X-Accel-Buffering: no');

while (ob_get_level() > 0) {
ob_end_flush();
}

for ($i = 0; $i < 32; $i++) {
echo "chunk-{$i} ";
echo str_repeat('x', 64);
echo "\n";
flush();
}
59 changes: 59 additions & 0 deletions profiles/benchmark.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/bin/bash
set -euo pipefail

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
APP="$ROOT/profiles/app"
MODE=${WORKER:+worker}
CADDYFILE="${1:-$APP/Caddyfile.${MODE:-regular}}"
FRANKENPHP_BIN="${FRANKENPHP_BIN:-$ROOT/caddy/frankenphp/frankenphp${PGO:+-pgo}}"
echo "$FRANKENPHP_BIN"
[ -x "$FRANKENPHP_BIN" ] || {
echo "FRANKENPHP_BIN not executable: $FRANKENPHP_BIN" >&2
exit 1
}

SCRIPTS=()
for f in "$APP"/*.php; do
n=$(basename "$f" .php)
[ "$n" = "index" ] || SCRIPTS+=("$n")
done

(cd "$APP" && exec "$FRANKENPHP_BIN" run --config "$CADDYFILE" >/dev/null 2>&1) &
SPID=$!
trap '
set +e
if [ -n "${SPID:-}" ] && kill -0 "$SPID" 2>/dev/null; then
kill "$SPID" 2>/dev/null
for _ in $(seq 1 20); do
kill -0 "$SPID" 2>/dev/null || break
sleep 0.1
done
kill -9 "$SPID" 2>/dev/null
fi
wait 2>/dev/null
exit 0
' EXIT
DEADLINE=$((SECONDS + 3))
until curl -fsS localhost:22019/config/ >/dev/null 2>&1; do
[ "$SECONDS" -ge "$DEADLINE" ] && {
echo "admin :22019 did not respond within 3s" >&2
exit 1
}
sleep 0.2
done

printf "%-20s %12s %10s %10s %10s\n" "script" "req/s" "avg" "p50" "p99"
sum=0
for s in "${SCRIPTS[@]}"; do
out=$(wrk -t4 -c256 -d"${BENCH_SEC:-8}s" --latency "http://localhost:22080/index.php?s=$s" 2>/dev/null || true)
read -r rps avg p50 p99 < <(awk '
/Requests\/sec:/ { rps = $2 }
/^ Latency / && !avg { avg = $2 }
/ 50%/ { p50 = $2 }
/ 99%/ { p99 = $2 }
END { print rps+0, avg, p50, p99 }
' <<<"$out")
printf "%-20s %12s %10s %10s %10s\n" "$s" "$rps" "$avg" "$p50" "$p99"
sum=$(awk -v a="$sum" -v b="$rps" 'BEGIN { print a + b }')
done
printf "%-20s %12s\n" "total req/s" "$sum"
Loading
Loading