From bcc800ed8aabe30ec1ed5018903ab4639c18fb84 Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 11 Feb 2026 21:32:54 +0100 Subject: [PATCH 01/13] chore(deps): update phpbench to stable ^1.4 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 6d2fa7106..28cbc5208 100644 --- a/composer.json +++ b/composer.json @@ -72,7 +72,7 @@ "nyholm/psr7": "^1.8", "patrickbussmann/oauth2-apple": "^0.3", "phpat/phpat": "^0.11.0", - "phpbench/phpbench": "84.x-dev", + "phpbench/phpbench": "^1.4", "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^12.5.8", "predis/predis": "^3.0.0", From eb178db6bf4811efe52f1f5dafb547ec4544cd78 Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 11 Feb 2026 21:33:00 +0100 Subject: [PATCH 02/13] chore(bench): configure runner paths and extension --- phpbench.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/phpbench.json b/phpbench.json index a78fe30fd..193bd46a5 100644 --- a/phpbench.json +++ b/phpbench.json @@ -1,4 +1,9 @@ { - "$schema": "./vendor/phpbench/phpbench/phpbench.schema.json", - "runner.bootstrap": "vendor/autoload.php" + "$schema": "./vendor/phpbench/phpbench/phpbench.schema.json", + "runner.bootstrap": "vendor/autoload.php", + "runner.path": "tests/Benchmark", + "runner.file_pattern": "*Bench.php", + "core.extensions": [ + "Tests\\Tempest\\Benchmark\\Extension\\MarkdownExtension" + ] } From da1f51a7bd4883affbdf30bbce834878fd93054a Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 11 Feb 2026 21:33:06 +0100 Subject: [PATCH 03/13] ci: add benchmark workflows for pull requests --- .github/workflows/benchmark-comment.yml | 66 +++++++++++++++++++++++++ .github/workflows/benchmark.yml | 59 ++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 .github/workflows/benchmark-comment.yml create mode 100644 .github/workflows/benchmark.yml diff --git a/.github/workflows/benchmark-comment.yml b/.github/workflows/benchmark-comment.yml new file mode 100644 index 000000000..64d079e8e --- /dev/null +++ b/.github/workflows/benchmark-comment.yml @@ -0,0 +1,66 @@ +name: Benchmark Comment + +on: + workflow_run: + workflows: [Benchmark] + types: [completed] + +jobs: + comment: + name: Post Benchmark Results + runs-on: ubuntu-latest + permissions: + actions: read + pull-requests: write + if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' + + steps: + - name: Download benchmark artifact + uses: actions/download-artifact@v7 + with: + name: benchmark-result + path: benchmark-artifact/ + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + + - name: Read artifact data + id: bench + run: | + echo "pr-number=$(cat benchmark-artifact/pr-number.txt)" >> "$GITHUB_OUTPUT" + echo "head-ref=$(cat benchmark-artifact/head-ref.txt)" >> "$GITHUB_OUTPUT" + echo "base-ref=$(cat benchmark-artifact/base-ref.txt)" >> "$GITHUB_OUTPUT" + echo "base-sha=$(cat benchmark-artifact/base-sha.txt)" >> "$GITHUB_OUTPUT" + echo "head-sha=$(cat benchmark-artifact/head-sha.txt)" >> "$GITHUB_OUTPUT" + { + echo "result<> "$GITHUB_OUTPUT" + + - name: Find existing comment + uses: peter-evans/find-comment@v4 + id: find-comment + with: + issue-number: ${{ steps.bench.outputs.pr-number }} + comment-author: github-actions[bot] + body-includes: "## Benchmark Results" + + - name: Post benchmark results + uses: peter-evans/create-or-update-comment@v5 + with: + issue-number: ${{ steps.bench.outputs.pr-number }} + comment-id: ${{ steps.find-comment.outputs.comment-id }} + edit-mode: replace + body: | + ## Benchmark Results + + Comparison of `${{ steps.bench.outputs.head-ref }}` against `${{ steps.bench.outputs.base-ref }}` (`${{ steps.bench.outputs.base-sha }}`). + +
+ Open to see the benchmark results + + ${{ steps.bench.outputs.result }} + +
+ + Generated by phpbench against commit ${{ steps.bench.outputs.head-sha }} diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 000000000..446924ec1 --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,59 @@ +name: Benchmark + +on: + pull_request: + branches: [main] + +concurrency: + group: benchmark-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + benchmark: + name: Performance Regression Check + runs-on: ubuntu-latest + + steps: + - name: Checkout PR branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.4 + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo + coverage: none + + - name: Install dependencies + uses: ramsey/composer-install@v3 + + - name: Benchmark base branch + run: | + git checkout ${{ github.event.pull_request.base.sha }} + composer install --no-interaction --prefer-dist --quiet + vendor/bin/phpbench run --tag=base --store --progress=none + + - name: Benchmark PR branch + run: | + git checkout ${{ github.event.pull_request.head.sha }} + composer install --no-interaction --prefer-dist --quiet + vendor/bin/phpbench run --tag=pr --store --ref=base --report=aggregate --progress=none --output=markdown | tee benchmark-result.txt || true + + - name: Prepare artifact + run: | + mkdir -p benchmark-artifact + cp benchmark-result.txt benchmark-artifact/ + echo "${{ github.event.pull_request.number }}" > benchmark-artifact/pr-number.txt + echo "${{ github.head_ref }}" > benchmark-artifact/head-ref.txt + echo "${{ github.base_ref }}" > benchmark-artifact/base-ref.txt + echo "${{ github.event.pull_request.base.sha }}" > benchmark-artifact/base-sha.txt + echo "${{ github.event.pull_request.head.sha }}" > benchmark-artifact/head-sha.txt + + - name: Upload benchmark artifact + uses: actions/upload-artifact@v6 + with: + name: benchmark-result + path: benchmark-artifact/ + retention-days: 1 From f076c6a80290d7ed920541421cd480be82fdd918 Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 11 Feb 2026 21:33:12 +0100 Subject: [PATCH 04/13] test(bench): add markdown renderer extension --- .../Benchmark/Extension/MarkdownExtension.php | 37 +++++ .../Benchmark/Extension/MarkdownRenderer.php | 132 ++++++++++++++++++ 2 files changed, 169 insertions(+) create mode 100644 tests/Benchmark/Extension/MarkdownExtension.php create mode 100644 tests/Benchmark/Extension/MarkdownRenderer.php diff --git a/tests/Benchmark/Extension/MarkdownExtension.php b/tests/Benchmark/Extension/MarkdownExtension.php new file mode 100644 index 000000000..e0108766e --- /dev/null +++ b/tests/Benchmark/Extension/MarkdownExtension.php @@ -0,0 +1,37 @@ +register( + MarkdownRenderer::class, + function (Container $container) { + return new MarkdownRenderer( + $container->get(ConsoleExtension::SERVICE_OUTPUT_STD), + $container->get(ExpressionExtension::SERVICE_PLAIN_PRINTER), + ); + }, + [ + ReportExtension::TAG_REPORT_RENDERER => [ + 'name' => 'markdown', + ], + ], + ); + } +} diff --git a/tests/Benchmark/Extension/MarkdownRenderer.php b/tests/Benchmark/Extension/MarkdownRenderer.php new file mode 100644 index 000000000..6d86d9477 --- /dev/null +++ b/tests/Benchmark/Extension/MarkdownRenderer.php @@ -0,0 +1,132 @@ +renderContent($reports); + $file = $config['file']; + + if ($file === null) { + $this->output->write($content); + + return; + } + + $this->writeFile($file, $content); + } + + public function configure(OptionsResolver $options): void + { + $options->setDefaults([ + 'file' => null, + ]); + $options->setAllowedTypes('file', ['null', 'string']); + } + + private function renderContent(Reports $reports): string + { + $lines = []; + + foreach ($reports->tables() as $table) { + array_push($lines, ...$this->renderTable($table)); + } + + return implode("\n", $lines) . "\n"; + } + + private function renderTable(Table $table): array + { + $lines = []; + $title = $table->title(); + + if ($title !== null && $title !== '') { + $lines[] = "## {$title}"; + $lines[] = ''; + } + + $columns = $table->columnNames(); + + if ($columns === []) { + return $lines; + } + + $lines[] = $this->renderRow($columns); + $lines[] = $this->renderSeparatorRow($columns); + + foreach ($table as $row) { + $lines[] = $this->renderDataRow($row); + } + + $lines[] = ''; + + return $lines; + } + + private function renderRow(array $cells): string + { + return '| ' . implode(' | ', $cells) . ' |'; + } + + private function renderSeparatorRow(array $columns): string + { + return $this->renderRow(array_map( + fn (string $column): string => str_repeat('-', max(3, mb_strlen($column))), + $columns, + )); + } + + private function renderDataRow(TableRow $row): string + { + $cells = array_map($this->formatCell(...), iterator_to_array($row)); + + return $this->renderRow($cells); + } + + private function formatCell(Node $node): string + { + return str_replace('|', '\\|', trim($this->printer->print($node))); + } + + private function writeFile(string $file, string $content): void + { + $this->createDirectory(dirname($file)); + + if (file_put_contents($file, $content) === false) { + throw new RuntimeException(sprintf('Could not write to file "%s"', $file)); + } + + $this->output->writeln("Written markdown report to: {$file}"); + } + + private function createDirectory(string $directory): void + { + if (is_dir($directory)) { + return; + } + + if (! mkdir($directory, 0o777, true) && ! is_dir($directory)) { + throw new RuntimeException(sprintf('Could not create directory "%s"', $directory)); + } + } +} From f28b4893304581029d592de4e76fae26adb26c82 Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 11 Feb 2026 21:33:17 +0100 Subject: [PATCH 05/13] test(bench): add kernel boot benchmark --- tests/Benchmark/Kernel/KernelBootBench.php | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tests/Benchmark/Kernel/KernelBootBench.php diff --git a/tests/Benchmark/Kernel/KernelBootBench.php b/tests/Benchmark/Kernel/KernelBootBench.php new file mode 100644 index 000000000..f3e46234e --- /dev/null +++ b/tests/Benchmark/Kernel/KernelBootBench.php @@ -0,0 +1,26 @@ +root = dirname(__DIR__, 3); + } + + #[Revs(10)] + #[Warmup(2)] + public function benchBoot(): void + { + FrameworkKernel::boot(root: $this->root); + } +} From b68037909b405e9e5e9539bf9c62cc0f0e74933f Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 11 Feb 2026 21:35:41 +0100 Subject: [PATCH 06/13] test(bench): add container resolution benchmark --- tests/Benchmark/Container/ContainerBench.php | 104 +++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 tests/Benchmark/Container/ContainerBench.php diff --git a/tests/Benchmark/Container/ContainerBench.php b/tests/Benchmark/Container/ContainerBench.php new file mode 100644 index 000000000..5bc63165b --- /dev/null +++ b/tests/Benchmark/Container/ContainerBench.php @@ -0,0 +1,104 @@ +container = new GenericContainer(); + } + + #[BeforeMethods('setUp')] + #[Revs(1000)] + #[Warmup(10)] + public function benchAutowireSimple(): void + { + $this->container->get(ContainerObjectA::class); + } + + #[BeforeMethods('setUp')] + #[Revs(1000)] + #[Warmup(10)] + public function benchAutowireNested(): void + { + $this->container->get(ContainerObjectB::class); + } + + #[BeforeMethods('setUp')] + #[Revs(1000)] + #[Warmup(10)] + public function benchSingletonResolution(): void + { + $this->container->singleton(ContainerObjectA::class, new ContainerObjectA()); + $this->container->get(ContainerObjectA::class); + } + + #[BeforeMethods('setUp')] + #[Revs(1000)] + #[Warmup(10)] + public function benchSingletonAttribute(): void + { + $this->container->get(ClassWithSingletonAttribute::class); + } + + #[BeforeMethods('setUp')] + #[Revs(1000)] + #[Warmup(10)] + public function benchDefinitionResolution(): void + { + $this->container->register(ContainerObjectA::class, fn () => new ContainerObjectA()); + $this->container->get(ContainerObjectA::class); + } + + #[BeforeMethods('setUp')] + #[Revs(1000)] + #[Warmup(10)] + public function benchInitializerResolution(): void + { + $this->container->addInitializer(ContainerObjectDInitializer::class); + $this->container->get(ContainerObjectD::class); + } + + #[BeforeMethods('setUp')] + #[Revs(1000)] + #[Warmup(10)] + public function benchDynamicInitializerResolution(): void + { + $this->container->addInitializer(ContainerObjectEInitializer::class); + $this->container->get(ContainerObjectE::class); + } + + #[BeforeMethods('setUp')] + #[Revs(1000)] + #[Warmup(10)] + public function benchClosureSingletonResolution(): void + { + $this->container->singleton(ContainerObjectA::class, fn () => new ContainerObjectA()); + $this->container->get(ContainerObjectA::class); + } + + #[BeforeMethods('setUp')] + #[Revs(1000)] + #[Warmup(10)] + public function benchInvokeClosure(): void + { + $this->container->invoke(fn (ContainerObjectA $a) => $a); + } +} From 77f83552ebd4df70aca8f1ffdb9f1b7b1d3a89c0 Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 11 Feb 2026 21:53:07 +0100 Subject: [PATCH 07/13] chore: add .phpbench/ to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0e321ab34..78921a9b7 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ .tempest/ .cache/ .idea/ +.phpbench/ build/ sessions/ vendor/ From 0e6f23548e0ea266d2348ae77c62ad018a64ce2c Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 11 Feb 2026 21:53:13 +0100 Subject: [PATCH 08/13] test(bench): add iterations to existing benchmarks --- tests/Benchmark/Container/ContainerBench.php | 10 ++++++++++ tests/Benchmark/Http/RouteConfigBench.php | 2 ++ .../Routing/Construction/RouteConfiguratorBench.php | 3 +++ .../Http/Routing/Matching/GenericRouteMatcherBench.php | 2 ++ tests/Benchmark/Kernel/KernelBootBench.php | 2 ++ 5 files changed, 19 insertions(+) diff --git a/tests/Benchmark/Container/ContainerBench.php b/tests/Benchmark/Container/ContainerBench.php index 5bc63165b..0317fe9c3 100644 --- a/tests/Benchmark/Container/ContainerBench.php +++ b/tests/Benchmark/Container/ContainerBench.php @@ -5,6 +5,7 @@ namespace Tests\Tempest\Benchmark\Container; use PhpBench\Attributes\BeforeMethods; +use PhpBench\Attributes\Iterations; use PhpBench\Attributes\Revs; use PhpBench\Attributes\Warmup; use Tempest\Container\GenericContainer; @@ -26,6 +27,7 @@ public function setUp(): void } #[BeforeMethods('setUp')] + #[Iterations(5)] #[Revs(1000)] #[Warmup(10)] public function benchAutowireSimple(): void @@ -34,6 +36,7 @@ public function benchAutowireSimple(): void } #[BeforeMethods('setUp')] + #[Iterations(5)] #[Revs(1000)] #[Warmup(10)] public function benchAutowireNested(): void @@ -42,6 +45,7 @@ public function benchAutowireNested(): void } #[BeforeMethods('setUp')] + #[Iterations(5)] #[Revs(1000)] #[Warmup(10)] public function benchSingletonResolution(): void @@ -51,6 +55,7 @@ public function benchSingletonResolution(): void } #[BeforeMethods('setUp')] + #[Iterations(5)] #[Revs(1000)] #[Warmup(10)] public function benchSingletonAttribute(): void @@ -59,6 +64,7 @@ public function benchSingletonAttribute(): void } #[BeforeMethods('setUp')] + #[Iterations(5)] #[Revs(1000)] #[Warmup(10)] public function benchDefinitionResolution(): void @@ -68,6 +74,7 @@ public function benchDefinitionResolution(): void } #[BeforeMethods('setUp')] + #[Iterations(5)] #[Revs(1000)] #[Warmup(10)] public function benchInitializerResolution(): void @@ -77,6 +84,7 @@ public function benchInitializerResolution(): void } #[BeforeMethods('setUp')] + #[Iterations(5)] #[Revs(1000)] #[Warmup(10)] public function benchDynamicInitializerResolution(): void @@ -86,6 +94,7 @@ public function benchDynamicInitializerResolution(): void } #[BeforeMethods('setUp')] + #[Iterations(5)] #[Revs(1000)] #[Warmup(10)] public function benchClosureSingletonResolution(): void @@ -95,6 +104,7 @@ public function benchClosureSingletonResolution(): void } #[BeforeMethods('setUp')] + #[Iterations(5)] #[Revs(1000)] #[Warmup(10)] public function benchInvokeClosure(): void diff --git a/tests/Benchmark/Http/RouteConfigBench.php b/tests/Benchmark/Http/RouteConfigBench.php index 7e328bf93..1cb2e2c60 100644 --- a/tests/Benchmark/Http/RouteConfigBench.php +++ b/tests/Benchmark/Http/RouteConfigBench.php @@ -4,6 +4,7 @@ namespace Tests\Tempest\Benchmark\Http; +use PhpBench\Attributes\Iterations; use PhpBench\Attributes\Revs; use PhpBench\Attributes\Warmup; use Tempest\Router\RouteConfig; @@ -19,6 +20,7 @@ public function __construct() $this->config = self::makeRouteConfig(); } + #[Iterations(5)] #[Revs(1000)] #[Warmup(10)] public function benchSerialization(): void diff --git a/tests/Benchmark/Http/Routing/Construction/RouteConfiguratorBench.php b/tests/Benchmark/Http/Routing/Construction/RouteConfiguratorBench.php index a1e8d3f4f..6bd75947a 100644 --- a/tests/Benchmark/Http/Routing/Construction/RouteConfiguratorBench.php +++ b/tests/Benchmark/Http/Routing/Construction/RouteConfiguratorBench.php @@ -5,6 +5,7 @@ namespace Tests\Tempest\Benchmark\Http\Routing\Construction; use PhpBench\Attributes\BeforeMethods; +use PhpBench\Attributes\Iterations; use PhpBench\Attributes\Revs; use PhpBench\Attributes\Warmup; use Tempest\Router\Routing\Construction\RouteConfigurator; @@ -20,6 +21,7 @@ public function __construct() } #[BeforeMethods('setupRouteConfig')] + #[Iterations(5)] #[Revs(1000)] #[Warmup(10)] public function benchRouteConfigConstructionToConfig(): void @@ -27,6 +29,7 @@ public function benchRouteConfigConstructionToConfig(): void $this->subject->toRouteConfig(); } + #[Iterations(5)] #[Revs(1000)] #[Warmup(10)] public function benchRouteConfigConstructionRouteAdding(): void diff --git a/tests/Benchmark/Http/Routing/Matching/GenericRouteMatcherBench.php b/tests/Benchmark/Http/Routing/Matching/GenericRouteMatcherBench.php index 17ea69b5b..95794f418 100644 --- a/tests/Benchmark/Http/Routing/Matching/GenericRouteMatcherBench.php +++ b/tests/Benchmark/Http/Routing/Matching/GenericRouteMatcherBench.php @@ -5,6 +5,7 @@ namespace Tests\Tempest\Benchmark\Http\Routing\Matching; use Generator; +use PhpBench\Attributes\Iterations; use PhpBench\Attributes\ParamProviders; use PhpBench\Attributes\Revs; use PhpBench\Attributes\Warmup; @@ -26,6 +27,7 @@ public function __construct() $this->matcher = new GenericRouteMatcher($config); } + #[Iterations(5)] #[ParamProviders('provideDynamicMatchingCases')] #[Revs(1000)] #[Warmup(10)] diff --git a/tests/Benchmark/Kernel/KernelBootBench.php b/tests/Benchmark/Kernel/KernelBootBench.php index f3e46234e..77ad297ed 100644 --- a/tests/Benchmark/Kernel/KernelBootBench.php +++ b/tests/Benchmark/Kernel/KernelBootBench.php @@ -4,6 +4,7 @@ namespace Tests\Tempest\Benchmark\Kernel; +use PhpBench\Attributes\Iterations; use PhpBench\Attributes\Revs; use PhpBench\Attributes\Warmup; use Tempest\Core\FrameworkKernel; @@ -17,6 +18,7 @@ public function __construct() $this->root = dirname(__DIR__, 3); } + #[Iterations(5)] #[Revs(10)] #[Warmup(2)] public function benchBoot(): void From d9521e4f7050cbfa33a4f88d2eb7feaed6ae1cc3 Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 11 Feb 2026 21:53:17 +0100 Subject: [PATCH 09/13] test(bench): add view rendering benchmark --- .../View/Fixtures/component.view.php | 6 ++ .../View/Fixtures/control-flow.view.php | 21 ++++ .../View/Fixtures/expressions.view.php | 10 ++ tests/Benchmark/View/Fixtures/plain.view.php | 9 ++ .../View/Fixtures/x-bench-layout.view.php | 10 ++ tests/Benchmark/View/ViewRenderBench.php | 98 +++++++++++++++++++ 6 files changed, 154 insertions(+) create mode 100644 tests/Benchmark/View/Fixtures/component.view.php create mode 100644 tests/Benchmark/View/Fixtures/control-flow.view.php create mode 100644 tests/Benchmark/View/Fixtures/expressions.view.php create mode 100644 tests/Benchmark/View/Fixtures/plain.view.php create mode 100644 tests/Benchmark/View/Fixtures/x-bench-layout.view.php create mode 100644 tests/Benchmark/View/ViewRenderBench.php diff --git a/tests/Benchmark/View/Fixtures/component.view.php b/tests/Benchmark/View/Fixtures/component.view.php new file mode 100644 index 000000000..266fa2900 --- /dev/null +++ b/tests/Benchmark/View/Fixtures/component.view.php @@ -0,0 +1,6 @@ + +

{{ $heading }}

+
    +
  • {{ $item }}
  • +
+
diff --git a/tests/Benchmark/View/Fixtures/control-flow.view.php b/tests/Benchmark/View/Fixtures/control-flow.view.php new file mode 100644 index 000000000..f516d98e0 --- /dev/null +++ b/tests/Benchmark/View/Fixtures/control-flow.view.php @@ -0,0 +1,21 @@ + + + {{ $title }} + + +
+

{{ $heading }}

+
+ +
    +
  • {{ $item }}
  • +
+ +
+

Admin panel

+
+
+

Regular user

+
+ + diff --git a/tests/Benchmark/View/Fixtures/expressions.view.php b/tests/Benchmark/View/Fixtures/expressions.view.php new file mode 100644 index 000000000..e245824cf --- /dev/null +++ b/tests/Benchmark/View/Fixtures/expressions.view.php @@ -0,0 +1,10 @@ + + + {{ $title }} + + +

{{ $heading }}

+

Welcome, {{ $name }}. You have {{ $count }} notifications.

+
{{ $footer }}
+ + diff --git a/tests/Benchmark/View/Fixtures/plain.view.php b/tests/Benchmark/View/Fixtures/plain.view.php new file mode 100644 index 000000000..0cb92bab4 --- /dev/null +++ b/tests/Benchmark/View/Fixtures/plain.view.php @@ -0,0 +1,9 @@ + + + Benchmark + + +

Hello World

+

This is a plain static template with no dynamic content.

+ + diff --git a/tests/Benchmark/View/Fixtures/x-bench-layout.view.php b/tests/Benchmark/View/Fixtures/x-bench-layout.view.php new file mode 100644 index 000000000..ac6a48123 --- /dev/null +++ b/tests/Benchmark/View/Fixtures/x-bench-layout.view.php @@ -0,0 +1,10 @@ + + + <?= $title ?? 'Benchmark' ?> + + + +
+
Footer
+ + diff --git a/tests/Benchmark/View/ViewRenderBench.php b/tests/Benchmark/View/ViewRenderBench.php new file mode 100644 index 000000000..c3e953bcf --- /dev/null +++ b/tests/Benchmark/View/ViewRenderBench.php @@ -0,0 +1,98 @@ +fixturesPath = __DIR__ . '/Fixtures'; + + $this->renderer = TempestViewRenderer::make(); + + $this->rendererWithComponents = TempestViewRenderer::make( + viewConfig: new ViewConfig()->addViewComponents( + $this->fixturesPath . '/x-bench-layout.view.php', + ), + ); + } + + #[BeforeMethods('setUp')] + #[Iterations(5)] + #[Revs(1000)] + #[Warmup(10)] + public function benchPlainHtml(): void + { + $this->renderer->render( + new GenericView($this->fixturesPath . '/plain.view.php'), + ); + } + + #[BeforeMethods('setUp')] + #[Iterations(5)] + #[Revs(1000)] + #[Warmup(10)] + public function benchExpressions(): void + { + $view = new GenericView($this->fixturesPath . '/expressions.view.php'); + $view->data( + title: 'Benchmark', + heading: 'Hello World', + name: 'Tempest', + count: '42', + footer: 'Copyright 2025', + ); + + $this->renderer->render($view); + } + + #[BeforeMethods('setUp')] + #[Iterations(5)] + #[Revs(1000)] + #[Warmup(10)] + public function benchControlFlow(): void + { + $view = new GenericView($this->fixturesPath . '/control-flow.view.php'); + $view->data( + title: 'Benchmark', + heading: 'Hello World', + showHeader: true, + isAdmin: false, + items: ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'], + ); + + $this->renderer->render($view); + } + + #[BeforeMethods('setUp')] + #[Iterations(5)] + #[Revs(1000)] + #[Warmup(10)] + public function benchViewComponent(): void + { + $view = new GenericView($this->fixturesPath . '/component.view.php'); + $view->data( + title: 'Benchmark', + heading: 'Hello World', + items: ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'], + ); + + $this->rendererWithComponents->render($view); + } +} From c51499c89ff84b5361cc370747d40bd8bf104c03 Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 11 Feb 2026 22:08:57 +0100 Subject: [PATCH 10/13] test(bench): add router dispatch benchmark --- tests/Benchmark/Http/GenericRouterBench.php | 137 ++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 tests/Benchmark/Http/GenericRouterBench.php diff --git a/tests/Benchmark/Http/GenericRouterBench.php b/tests/Benchmark/Http/GenericRouterBench.php new file mode 100644 index 000000000..ff31c0dcd --- /dev/null +++ b/tests/Benchmark/Http/GenericRouterBench.php @@ -0,0 +1,137 @@ +routeConfig = self::makeRouteConfig(); + + $this->routeConfig->middleware = new Middleware( + HandleRouteExceptionMiddleware::class, + MatchRouteMiddleware::class, + ); + + $this->container = new GenericContainer(); + + $matcher = new GenericRouteMatcher($this->routeConfig); + + $this->container->singleton(Container::class, fn () => $this->container); + $this->container->singleton(RouteMatcher::class, fn () => $matcher); + $this->container->singleton(RouteConfig::class, fn () => $this->routeConfig); + + $this->router = new GenericRouter($this->container, $this->routeConfig); + } + + #[BeforeMethods('setUp')] + #[Iterations(5)] + #[ParamProviders('provideDispatchCases')] + #[Revs(1000)] + #[Warmup(10)] + public function benchDispatch(array $params): void + { + $this->router->dispatch( + new GenericRequest(Method::GET, $params['uri']), + ); + } + + #[BeforeMethods('setUp')] + #[Iterations(5)] + #[ParamProviders('provideDispatchCases')] + #[Revs(1000)] + #[Warmup(10)] + public function benchDispatchWithoutMiddleware(array $params): void + { + $this->routeConfig->middleware = new Middleware( + MatchRouteMiddleware::class, + ); + + $router = new GenericRouter($this->container, $this->routeConfig); + + $router->dispatch( + new GenericRequest(Method::GET, $params['uri']), + ); + } + + public function provideDispatchCases(): Generator + { + yield 'Static route' => ['uri' => '/test/5']; + yield 'Dynamic route' => ['uri' => '/test/key/5/edit']; + yield 'Dynamic short' => ['uri' => '/test/key/50']; + } + + public function handle(): Ok + { + return new Ok('OK'); + } + + public function handleWithParam(string $id): Ok + { + return new Ok('OK'); + } + + private static function makeRouteConfig(): RouteConfig + { + $handler = new MethodReflector(new ReflectionMethod(self::class, 'handle')); + $handlerWithParam = new MethodReflector(new ReflectionMethod(self::class, 'handleWithParam')); + + $configurator = new RouteConfigurator(); + + foreach (range(1, 100) as $i) { + $configurator->addRoute(DiscoveredRoute::fromRoute( + new \Tempest\Router\Get("/test/{$i}"), + [], + $handler, + )); + $configurator->addRoute(DiscoveredRoute::fromRoute( + new \Tempest\Router\Get("/test/{id}/{$i}"), + [], + $handlerWithParam, + )); + $configurator->addRoute(DiscoveredRoute::fromRoute( + new \Tempest\Router\Get("/test/{id}/{$i}/delete"), + [], + $handlerWithParam, + )); + $configurator->addRoute(DiscoveredRoute::fromRoute( + new \Tempest\Router\Get("/test/{id}/{$i}/edit"), + [], + $handlerWithParam, + )); + } + + return $configurator->toRouteConfig(); + } +} From 03f0396c945ee383af697ef8cd787a99f669c0eb Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 11 Feb 2026 22:31:46 +0100 Subject: [PATCH 11/13] chore(bench): add composer bench script --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 28cbc5208..d7548d935 100644 --- a/composer.json +++ b/composer.json @@ -273,6 +273,7 @@ "composer phpunit", "composer phpstan", "composer exceptions:build" - ] + ], + "bench": "vendor/bin/phpbench run --report=aggregate" } } From 9ee720d792533bca35357e0adc4628d6a65f920c Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 11 Feb 2026 22:43:44 +0100 Subject: [PATCH 12/13] style(bench): import Get attribute --- tests/Benchmark/Http/GenericRouterBench.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/Benchmark/Http/GenericRouterBench.php b/tests/Benchmark/Http/GenericRouterBench.php index ff31c0dcd..f769baaaa 100644 --- a/tests/Benchmark/Http/GenericRouterBench.php +++ b/tests/Benchmark/Http/GenericRouterBench.php @@ -19,6 +19,7 @@ use Tempest\Http\Responses\Ok; use Tempest\Reflection\MethodReflector; use Tempest\Router\GenericRouter; +use Tempest\Router\Get; use Tempest\Router\HandleRouteExceptionMiddleware; use Tempest\Router\MatchRouteMiddleware; use Tempest\Router\RouteConfig; @@ -111,22 +112,22 @@ private static function makeRouteConfig(): RouteConfig foreach (range(1, 100) as $i) { $configurator->addRoute(DiscoveredRoute::fromRoute( - new \Tempest\Router\Get("/test/{$i}"), + new Get("/test/{$i}"), [], $handler, )); $configurator->addRoute(DiscoveredRoute::fromRoute( - new \Tempest\Router\Get("/test/{id}/{$i}"), + new Get("/test/{id}/{$i}"), [], $handlerWithParam, )); $configurator->addRoute(DiscoveredRoute::fromRoute( - new \Tempest\Router\Get("/test/{id}/{$i}/delete"), + new Get("/test/{id}/{$i}/delete"), [], $handlerWithParam, )); $configurator->addRoute(DiscoveredRoute::fromRoute( - new \Tempest\Router\Get("/test/{id}/{$i}/edit"), + new Get("/test/{id}/{$i}/edit"), [], $handlerWithParam, )); From cde2c4725c1c484e71142f378cfe410343e9c79a Mon Sep 17 00:00:00 2001 From: Mark Date: Thu, 12 Feb 2026 14:27:03 +0100 Subject: [PATCH 13/13] chore: add benchmark workflow CODEOWNERS --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a329b727c..abcb81262 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,6 +11,8 @@ /.github/workflows/trigger-tempest-app-qa.yml @brendt /.github/workflows/trigger-tempest-clean-qa.yml @brendt /.github/workflows/validate-packages.yml @aidan-casey +/.github/workflows/benchmark.yml @xHeaven +/.github/workflows/benchmark-comment.yml @xHeaven /src/ @brendt /docs/ @brendt @innocenzi /packages/auth/ @innocenzi