Skip to content
Merged
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
5 changes: 5 additions & 0 deletions frameworks/symfony-spawn-franken/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
APP_ENV=prod
APP_DEBUG=0
APP_SECRET=benchmark_httparena_secret
DATABASE_URL=pgsql://bench:bench@localhost:5432/benchmark
DEFAULT_URI=http://localhost
35 changes: 35 additions & 0 deletions frameworks/symfony-spawn-franken/Caddyfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
admin off
auto_https off
frankenphp
}

:8080 {
encode br gzip
root * /app/public
php_server {
index off
file_server off
worker {
file /app/worker.php
num {$WORKERS:0}
async
match /*
}
}
}

:8443 {
tls /certs/server.crt /certs/server.key
root * /app/public
php_server {
index off
file_server off
worker {
file /app/worker.php
num {$WORKERS:0}
async
match /*
}
}
}
41 changes: 41 additions & 0 deletions frameworks/symfony-spawn-franken/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
FROM trueasync/php-true-async:latest-frankenphp

COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer

WORKDIR /app

COPY composer.json ./

RUN APP_ENV=prod composer install --no-dev --optimize-autoloader --no-scripts --no-interaction

COPY . .

RUN APP_ENV=prod APP_DEBUG=0 APP_SECRET=benchmark \
DATABASE_URL=pgsql://bench:bench@localhost:5432/benchmark \
DEFAULT_URI=http://localhost \
php bin/console cache:warmup

RUN echo '\
opcache.enable=1\n\
opcache.enable_cli=1\n\
opcache.jit=1255\n\
opcache.jit_buffer_size=128M\n\
opcache.memory_consumption=256\n\
opcache.max_accelerated_files=10000\n\
opcache.validate_timestamps=0\n\
memory_limit=2048M\n\
' >> /etc/php.d/99-benchmark.ini

ENV GODEBUG=cgocheck=0
ENV GOGC=1000
ENV APP_ENV=prod
ENV APP_DEBUG=0
ENV APP_SECRET=benchmark
ENV DATABASE_URL=pgsql://bench:bench@localhost:5432/benchmark
ENV DEFAULT_URI=http://localhost

COPY Caddyfile /etc/caddy/Caddyfile

EXPOSE 8080 8443/tcp 8443/udp

CMD ["frankenphp", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
65 changes: 65 additions & 0 deletions frameworks/symfony-spawn-franken/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
"type": "project",
"license": "MIT",
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
"php": ">=8.6",
"ext-ctype": "*",
"ext-iconv": "*",
"ext-pcntl": "*",
"ext-pdo": "*",
"doctrine/dbal": "^4.4",
"doctrine/doctrine-bundle": "^2.18",
"symfony/console": "7.4.*",
"symfony/dotenv": "7.4.*",
"symfony/flex": "^2",
"symfony/framework-bundle": "7.4.*",
"symfony/runtime": "7.4.*",
"symfony/yaml": "7.4.*",
"yangusik/symfony-spawn": "*"
},
"config": {
"allow-plugins": {
"php-http/discovery": true,
"symfony/flex": true,
"symfony/runtime": true
},
"sort-packages": true
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"replace": {
"symfony/polyfill-ctype": "*",
"symfony/polyfill-iconv": "*",
"symfony/polyfill-php72": "*",
"symfony/polyfill-php73": "*",
"symfony/polyfill-php74": "*",
"symfony/polyfill-php80": "*",
"symfony/polyfill-php81": "*",
"symfony/polyfill-php82": "*"
},
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"
],
"post-update-cmd": [
"@auto-scripts"
]
},
"conflict": {
"symfony/symfony": "*"
},
"extra": {
"symfony": {
"allow-contrib": false,
"require": "7.4.*"
}
}
}
7 changes: 7 additions & 0 deletions frameworks/symfony-spawn-franken/config/bundles.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Spawn\Symfony\TrueAsyncBundle::class => ['all' => true],
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
doctrine:
dbal:
driver_class: Spawn\Symfony\Database\TrueAsyncPgsqlDriver
url: '%env(DATABASE_URL)%'
server_version: '17'
use_savepoints: true

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
framework:
secret: '%env(APP_SECRET)%'
http_method_override: false
handle_all_throwables: true
php_errors:
log: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
true_async:
db_pool:
enabled: true
min: 4
max: 64
healthcheck_interval: 30
5 changes: 5 additions & 0 deletions frameworks/symfony-spawn-franken/config/routes.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
controllers:
resource:
path: ../src/Controller/
namespace: App\Controller
type: attribute
7 changes: 7 additions & 0 deletions frameworks/symfony-spawn-franken/config/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
services:
_defaults:
autowire: true
autoconfigure: true

App\:
resource: '../src/'
28 changes: 28 additions & 0 deletions frameworks/symfony-spawn-franken/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"display_name": "symfony-spawn-franken",
"language": "PHP",
"type": "tuned",
"engine": "frankenphp",
"description": "Symfony with symfony-spawn bundle: coroutine-per-request isolation via TrueAsync PHP core, Doctrine DBAL connection pooling, and FrankenPHP.",
"repo": "https://github.com/yangusik/symfony-spawn",
"enabled": true,
"tests": [
"baseline",
"pipelined",
"limited-conn",
"json",
"json-comp",
"upload",
"static",
"async-db",
"api-4",
"api-16",
"baseline-h2",
"static-h2",
"baseline-h3",
"static-h3"
],
"maintainers": [
"YanGusik"
]
}
9 changes: 9 additions & 0 deletions frameworks/symfony-spawn-franken/public/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

use App\Kernel;

require_once dirname(__DIR__).'/vendor/autoload_runtime.php';

return static function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<?php

declare(strict_types=1);

namespace App\Controller;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\ParameterType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class BenchmarkController
{
private static array $dataset = [];
private static array $staticFiles = [];
private static bool $dataLoaded = false;

private const MIME_TYPES = [
'css' => 'text/css',
'js' => 'application/javascript',
'html' => 'text/html',
'woff2' => 'font/woff2',
'svg' => 'image/svg+xml',
'webp' => 'image/webp',
'json' => 'application/json',
];

public function __construct(private readonly Connection $connection)
{
if (self::$dataLoaded) {
return;
}

self::$dataset = json_decode(file_get_contents('/data/dataset.json'), true);

$dir = '/data/static';
if (is_dir($dir)) {
foreach (scandir($dir) as $file) {
if ($file === '.' || $file === '..') continue;
if (str_ends_with($file, '.br') || str_ends_with($file, '.gz')) continue;
$base = $dir . '/' . $file;
$ext = pathinfo($file, PATHINFO_EXTENSION);
self::$staticFiles[$file] = [
'data' => file_get_contents($base),
'mime' => self::MIME_TYPES[$ext] ?? 'application/octet-stream',
'br' => file_exists($base . '.br') ? file_get_contents($base . '.br') : null,
'gz' => file_exists($base . '.gz') ? file_get_contents($base . '.gz') : null,
];
}
}

self::$dataLoaded = true;
}

#[Route('/baseline11', methods: ['GET', 'POST'])]
#[Route('/baseline2', methods: ['GET', 'POST'])]
public function baseline(Request $request): Response
{
$sum = array_sum($request->query->all());
if ($request->isMethod('POST')) {
$sum += (int) $request->getContent();
}
return new Response((string) $sum, 200, ['Content-Type' => 'text/plain']);
}

#[Route('/pipeline')]
public function pipeline(): Response
{
return new Response('ok', 200, ['Content-Type' => 'text/plain']);
}

#[Route('/json/{count}', requirements: ['count' => '\d+'])]
public function json(int $count, Request $request): Response
{
$count = max(0, min($count, count(self::$dataset)));
$m = (int) ($request->query->get('m', 1) ?: 1);
$items = [];
for ($i = 0; $i < $count; $i++) {
$item = self::$dataset[$i];
$item['total'] = $item['price'] * $item['quantity'] * $m;
$items[] = $item;
}
return new Response(
json_encode(['items' => $items, 'count' => $count], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
200,
['Content-Type' => 'application/json']
);
}

#[Route('/upload', methods: ['POST'])]
public function upload(Request $request): Response
{
return new Response((string) strlen($request->getContent()), 200, ['Content-Type' => 'text/plain']);
}

#[Route('/async-db')]
public function asyncDb(Request $request): Response
{
$min = (int) ($request->query->get('min', 10));
$max = (int) ($request->query->get('max', 50));
$limit = max(1, min(50, (int) ($request->query->get('limit', 50))));

try {
$stmt = $this->connection->prepare(
'SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN ? AND ? LIMIT ?'
);
$stmt->bindValue(1, $min);
$stmt->bindValue(2, $max);
$stmt->bindValue(3, $limit, ParameterType::INTEGER);
$result = $stmt->executeQuery();
$rows = $result->fetchAllAssociative();

$items = array_map(static function (array $row): array {
$row['active'] = (bool) $row['active'];
$row['tags'] = json_decode($row['tags'], true);
$row['rating'] = [
'score' => (int) $row['rating_score'],
'count' => (int) $row['rating_count'],
];
unset($row['rating_score'], $row['rating_count']);
return $row;
}, $rows);

return new Response(
json_encode(['items' => $items, 'count' => count($items)]),
200,
['Content-Type' => 'application/json']
);
} catch (\Throwable) {
return new Response('{"items":[],"count":0}', 200, ['Content-Type' => 'application/json']);
}
}

#[Route('/static/{file}', requirements: ['file' => '.+'])]
public function static(string $file, Request $request): Response
{
if (!isset(self::$staticFiles[$file])) {
return new Response('Not Found', 404, ['Content-Type' => 'text/plain']);
}

$f = self::$staticFiles[$file];
$ae = $request->headers->get('Accept-Encoding', '');
$headers = ['Content-Type' => $f['mime']];

if ($f['br'] !== null && str_contains($ae, 'br')) {
$headers['Content-Encoding'] = 'br';
return new Response($f['br'], 200, $headers);
}

if ($f['gz'] !== null && str_contains($ae, 'gzip')) {
$headers['Content-Encoding'] = 'gzip';
return new Response($f['gz'], 200, $headers);
}

return new Response($f['data'], 200, $headers);
}
}
Loading