Skip to content

Commit be23f0a

Browse files
authored
perf: use PGO to improve FrankenPHP's go performance (7-8% Hello World throughput) (#2361)
- [x] add basic benchmark php files and script to generate pgo profiles (requires wrk) - [x] actual proper benchmarking - [x] github action to auto-sync pgo profiles on merge to main - [ ] use pgo in builds (to be done after this PR is merged) closes #2360 Edit: patched wrk source and compiled into a static musl binary, in case you can't get wrk from package manager (like on rhel): https://files.henderkes.com/x86_64-linux/wrk https://files.henderkes.com/aarch64-linux/wrk
1 parent 3740118 commit be23f0a

21 files changed

Lines changed: 368 additions & 1 deletion

.github/workflows/pgo-profile.yaml

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
name: Refresh PGO profile
2+
# Regenerates caddy/frankenphp/default.pgo by hammering frankenphp with wrk in
3+
# both regular and worker mode (see build-pgo.sh) and opens a PR with the
4+
# new merged profile. Direct `go build` and the Dockerfile auto-detect it;
5+
# xcaddy users still need an explicit --pgo flag.
6+
concurrency:
7+
cancel-in-progress: false
8+
group: ${{ github.workflow }}-${{ github.ref }}
9+
on:
10+
push:
11+
branches:
12+
- main
13+
paths:
14+
- "**/*.go"
15+
- "frankenphp.c"
16+
- "profiles/**"
17+
- ".github/workflows/pgo-profile.yaml"
18+
workflow_dispatch:
19+
permissions:
20+
contents: write
21+
pull-requests: write
22+
jobs:
23+
refresh:
24+
environment: pgo
25+
name: Generate PGO profile
26+
runs-on: ubuntu-latest
27+
env:
28+
GOTOOLCHAIN: local
29+
GOFLAGS: "-tags=nobadger,nomysql,nopgx"
30+
LIBRARY_PATH: ${{ github.workspace }}/watcher/target/lib
31+
BENCH_SEC: "30"
32+
steps:
33+
- uses: actions/checkout@v6
34+
with:
35+
fetch-depth: 0
36+
# zizmor: ignore[artipacked]
37+
# persist-credentials is intentionally left enabled because this
38+
# workflow needs to push a branch via git push (mirrors translate.yaml).
39+
- uses: actions/setup-go@v6
40+
with:
41+
go-version: "1.26"
42+
cache-dependency-path: |
43+
go.sum
44+
caddy/go.sum
45+
- uses: shivammathur/setup-php@v2
46+
with:
47+
php-version: "8.5"
48+
ini-file: development
49+
coverage: none
50+
tools: none
51+
env:
52+
phpts: ts
53+
debug: true
54+
- name: Install e-dant/watcher
55+
uses: ./.github/actions/watcher
56+
- name: Set CGO flags
57+
run: echo "CGO_CFLAGS=-I${PWD}/watcher/target/include $(php-config --includes)" >> "${GITHUB_ENV}"
58+
- name: Install wrk
59+
run: sudo apt-get update && sudo apt-get install -y wrk
60+
- name: Generate profile
61+
run: ./profiles/build-pgo.sh
62+
- name: Show pprof summary
63+
run: |
64+
go install github.com/google/pprof@latest
65+
"$(go env GOPATH)/bin/pprof" -top -cum -nodecount=25 caddy/frankenphp/default.pgo || true
66+
- name: Open PR with refreshed profile
67+
env:
68+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
69+
ACTOR: ${{ github.actor }}
70+
ACTOR_ID: ${{ github.actor_id }}
71+
RUN_ID: ${{ github.run_id }}
72+
SOURCE_SHA: ${{ github.sha }}
73+
BENCH_SEC: ${{ env.BENCH_SEC }}
74+
run: |
75+
git config user.name "github-actions[bot]"
76+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
77+
BRANCH="pgo/refresh-$RUN_ID"
78+
git checkout -b "$BRANCH"
79+
git add caddy/frankenphp/default.pgo
80+
git diff --cached --quiet && exit 0
81+
git commit -m "perf(pgo): refresh PGO profile" --author="$ACTOR <$ACTOR_ID+$ACTOR@users.noreply.github.com>"
82+
git push origin "$BRANCH"
83+
gh pr create \
84+
--title "perf(pgo): refresh PGO profile" \
85+
--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." \
86+
--label "perf" \
87+
--label "bot"

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/caddy/frankenphp/Build
22
/caddy/frankenphp/Caddyfile.test
33
/caddy/frankenphp/frankenphp
4+
/caddy/frankenphp/frankenphp-pgo
45
/caddy/frankenphp/frankenphp.exe
56
/caddy/frankenphp/public
67
/dist

caddy/frankenphp/default.pgo

282 KB
Binary file not shown.

frankenphp.c

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ __thread HashTable *sandboxed_env = NULL;
9696
#ifndef PHP_WIN32
9797
static bool is_forked_child = false;
9898
static void frankenphp_fork_child(void) { is_forked_child = true; }
99+
100+
static void frankenphp_register_atfork(void) {
101+
pthread_atfork(NULL, NULL, frankenphp_fork_child);
102+
}
99103
#endif
100104

101105
/* Best-effort force-kill for stuck PHP threads.
@@ -862,7 +866,9 @@ static const zend_function_entry frankenphp_test_hook_functions[] = {
862866
PHP_MINIT_FUNCTION(frankenphp) {
863867
register_frankenphp_symbols(module_number);
864868
#ifndef PHP_WIN32
865-
pthread_atfork(NULL, NULL, frankenphp_fork_child);
869+
/* MINIT runs once per ZTS thread — guard the atfork registration */
870+
static pthread_once_t atfork_once = PTHREAD_ONCE_INIT;
871+
pthread_once(&atfork_once, frankenphp_register_atfork);
866872
#endif
867873

868874
#ifdef FRANKENPHP_TEST

profiles/app/Caddyfile.regular

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# this is, intentionally, not an optimised configuration
2+
# we're trying to cover as many codepaths as possible
3+
4+
{
5+
skip_install_trust
6+
admin localhost:22019
7+
grace_period 0s
8+
}
9+
10+
:22080 {
11+
root * .
12+
encode zstd br gzip
13+
14+
php_server
15+
}

profiles/app/Caddyfile.worker

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# this is, intentionally, not an optimised configuration
2+
# we're trying to cover as many codepaths as possible
3+
4+
{
5+
skip_install_trust
6+
admin localhost:22019
7+
grace_period 0s
8+
}
9+
10+
:22080 {
11+
root * .
12+
encode zstd br gzip
13+
14+
php_server {
15+
worker index.php
16+
}
17+
}

profiles/app/cookies_headers.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
$cookieCount = count($_COOKIE);
3+
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
4+
$accept = $_SERVER['HTTP_ACCEPT'] ?? '';
5+
$auth = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
6+
7+
header('Content-Type: text/plain; charset=utf-8');
8+
header('Cache-Control: private, no-cache, must-revalidate, max-age=0');
9+
header('Pragma: no-cache');
10+
header('X-Frame-Options: DENY');
11+
header('X-Content-Type-Options: nosniff');
12+
header('X-XSS-Protection: 1; mode=block');
13+
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
14+
header('Referrer-Policy: strict-origin-when-cross-origin');
15+
header('Permissions-Policy: camera=(), microphone=(), geolocation=()');
16+
header('Set-Cookie: trace=' . substr(md5((string)microtime(true)), 0, 16) . '; Path=/; HttpOnly; SameSite=Lax');
17+
header('X-Request-Id: ' . substr(md5((string)microtime(true)), 0, 16));
18+
19+
echo "cookies=$cookieCount ua_len=" . strlen($ua) . " auth_len=" . strlen($auth) . "\n";

profiles/app/file_io.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
$file = '/tmp/benchmark_' . bin2hex(random_bytes(16)) . '.txt';
3+
4+
$data = str_repeat('Lorem ipsum dolor sit amet, consectetur adipiscing elit. ', 1000);
5+
file_put_contents($file, $data);
6+
7+
$content = file_get_contents($file);
8+
9+
$lines = explode(' ', $content);
10+
$filtered = array_filter($lines, fn($line) => strlen($line) > 5);
11+
sort($filtered);
12+
13+
unlink($file);
14+
15+
echo "OK\n";

profiles/app/frankenphp_log.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
header('Content-Type: text/plain; charset=utf-8');
3+
4+
if (function_exists('frankenphp_log')) {
5+
frankenphp_log('pgo profile request', FRANKENPHP_LOG_LEVEL_DEBUG, ['s' => 'fp_log']);
6+
frankenphp_log('pgo profile request', FRANKENPHP_LOG_LEVEL_INFO);
7+
} else {
8+
error_log('frankenphp_log unavailable');
9+
}
10+
11+
echo "ok\n";

profiles/app/helloworld.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<?php
2+
echo "Hello World!";

0 commit comments

Comments
 (0)