diff --git a/frameworks/symfony-spawn-tas/.env b/frameworks/symfony-spawn-tas/.env new file mode 100644 index 000000000..2b8a6747a --- /dev/null +++ b/frameworks/symfony-spawn-tas/.env @@ -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 diff --git a/frameworks/symfony-spawn-tas/Dockerfile b/frameworks/symfony-spawn-tas/Dockerfile new file mode 100644 index 000000000..8b58d9542 --- /dev/null +++ b/frameworks/symfony-spawn-tas/Dockerfile @@ -0,0 +1,45 @@ +FROM trueasync/php-true-async:0.7.0-alpha.3-php8.6-alpine + +RUN apk add --no-cache git openssh-client + +COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer + +WORKDIR /app + +COPY composer.json ./ + +RUN APP_ENV=prod COMPOSER_MAX_PARALLEL_HTTP=1 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 printf "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\ +post_max_size=32M\n\ +upload_max_filesize=32M\n\ +zlib.output_compression_level=6\n\ +zlib.output_compression=On\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 +ENV APP_RUNTIME='Spawn\Symfony\Runtime\TrueAsyncRuntime' + +EXPOSE 8080 8443/tcp 8443/udp + +CMD ["php", "public/index.php"] diff --git a/frameworks/symfony-spawn-tas/composer.json b/frameworks/symfony-spawn-tas/composer.json new file mode 100644 index 000000000..6ed13177c --- /dev/null +++ b/frameworks/symfony-spawn-tas/composer.json @@ -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": "dev-master" + }, + "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.*" + } + } +} diff --git a/frameworks/symfony-spawn-tas/config/bundles.php b/frameworks/symfony-spawn-tas/config/bundles.php new file mode 100644 index 000000000..7ee07cc31 --- /dev/null +++ b/frameworks/symfony-spawn-tas/config/bundles.php @@ -0,0 +1,7 @@ + ['all' => true], + Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], + Spawn\Symfony\TrueAsyncBundle::class => ['all' => true], +]; diff --git a/frameworks/symfony-spawn-tas/config/packages/doctrine.yaml b/frameworks/symfony-spawn-tas/config/packages/doctrine.yaml new file mode 100644 index 000000000..86df03148 --- /dev/null +++ b/frameworks/symfony-spawn-tas/config/packages/doctrine.yaml @@ -0,0 +1,7 @@ +doctrine: + dbal: + driver_class: Spawn\Symfony\Database\TrueAsyncPgsqlDriver + url: '%env(DATABASE_URL)%' + server_version: '17' + use_savepoints: true + diff --git a/frameworks/symfony-spawn-tas/config/packages/framework.yaml b/frameworks/symfony-spawn-tas/config/packages/framework.yaml new file mode 100644 index 000000000..bafc204cf --- /dev/null +++ b/frameworks/symfony-spawn-tas/config/packages/framework.yaml @@ -0,0 +1,6 @@ +framework: + secret: '%env(APP_SECRET)%' + http_method_override: false + handle_all_throwables: true + php_errors: + log: true diff --git a/frameworks/symfony-spawn-tas/config/packages/true_async.yaml b/frameworks/symfony-spawn-tas/config/packages/true_async.yaml new file mode 100644 index 000000000..c547c2ec0 --- /dev/null +++ b/frameworks/symfony-spawn-tas/config/packages/true_async.yaml @@ -0,0 +1,6 @@ +true_async: + db_pool: + enabled: true + min: 4 + max: 64 + healthcheck_interval: 30 diff --git a/frameworks/symfony-spawn-tas/config/routes.yaml b/frameworks/symfony-spawn-tas/config/routes.yaml new file mode 100644 index 000000000..41ef8140b --- /dev/null +++ b/frameworks/symfony-spawn-tas/config/routes.yaml @@ -0,0 +1,5 @@ +controllers: + resource: + path: ../src/Controller/ + namespace: App\Controller + type: attribute diff --git a/frameworks/symfony-spawn-tas/config/services.yaml b/frameworks/symfony-spawn-tas/config/services.yaml new file mode 100644 index 000000000..1972e6531 --- /dev/null +++ b/frameworks/symfony-spawn-tas/config/services.yaml @@ -0,0 +1,7 @@ +services: + _defaults: + autowire: true + autoconfigure: true + + App\: + resource: '../src/' diff --git a/frameworks/symfony-spawn-tas/meta.json b/frameworks/symfony-spawn-tas/meta.json new file mode 100644 index 000000000..f40170dfb --- /dev/null +++ b/frameworks/symfony-spawn-tas/meta.json @@ -0,0 +1,25 @@ +{ + "display_name": "symfony-spawn-tas", + "language": "PHP", + "type": "tuned", + "engine": "C", + "description": "Symfony with symfony-spawn bundle: coroutine-per-request isolation via TrueAsync PHP core, Doctrine DBAL connection pooling, and TrueAsyncServer.", + "repo": "https://github.com/yangusik/symfony-spawn", + "enabled": true, + "tests": [ + "baseline", + "pipelined", + "limited-conn", + "json", + "upload", + "static", + "async-db", + "api-4", + "api-16", + "baseline-h3", + "static-h3" + ], + "maintainers": [ + "YanGusik" + ] +} diff --git a/frameworks/symfony-spawn-tas/public/index.php b/frameworks/symfony-spawn-tas/public/index.php new file mode 100644 index 000000000..c0037a8db --- /dev/null +++ b/frameworks/symfony-spawn-tas/public/index.php @@ -0,0 +1,9 @@ + '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); + } +} diff --git a/frameworks/symfony-spawn-tas/src/Kernel.php b/frameworks/symfony-spawn-tas/src/Kernel.php new file mode 100644 index 000000000..779cd1f2b --- /dev/null +++ b/frameworks/symfony-spawn-tas/src/Kernel.php @@ -0,0 +1,11 @@ +