From 4176214a80dbb9f9d9872f457ea6851232d85675 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sun, 2 Aug 2026 00:09:17 -0400 Subject: [PATCH 1/8] Add compilation benchmark --- .../app/Console/Commands/BenchmarkCommand.php | 99 +++++++++++++++++-- 1 file changed, 93 insertions(+), 6 deletions(-) diff --git a/workbench/app/Console/Commands/BenchmarkCommand.php b/workbench/app/Console/Commands/BenchmarkCommand.php index 930637a3..dbad5f0e 100644 --- a/workbench/app/Console/Commands/BenchmarkCommand.php +++ b/workbench/app/Console/Commands/BenchmarkCommand.php @@ -9,6 +9,7 @@ use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\View; use Illuminate\Support\Str; +use Livewire\Blaze\Blaze; class BenchmarkCommand extends Command { @@ -106,6 +107,8 @@ protected function runBenchmark(): array $bladeTimes = []; $blazeTimes = []; + $bladeCompilationTimes = []; + $blazeCompilationTimes = []; $bar?->setMessage('Benchmarking...'); @@ -121,6 +124,20 @@ protected function runBenchmark(): array $bar?->advance(); } + // Compilation clears the view cache, so measure it only after all render rounds. + $this->measureCompilation(false); + $this->measureCompilation(true); + + for ($r = 0; $r < $this->rounds; $r++) { + if ($r % 2 === 0) { + $bladeCompilationTimes[] = $this->measureCompilation(false); + $blazeCompilationTimes[] = $this->measureCompilation(true); + } else { + $blazeCompilationTimes[] = $this->measureCompilation(true); + $bladeCompilationTimes[] = $this->measureCompilation(false); + } + } + $bar?->setMessage('Done!'); $bar?->finish(); @@ -132,15 +149,21 @@ protected function runBenchmark(): array // if neither its blade nor blaze time is an outlier. $keptRounds = $this->nonOutlierIndices(collect($bladeTimes)) ->intersect($this->nonOutlierIndices(collect($blazeTimes))) + ->intersect($this->nonOutlierIndices(collect($bladeCompilationTimes))) + ->intersect($this->nonOutlierIndices(collect($blazeCompilationTimes))) ->values(); $this->filteredRounds = $this->rounds - $keptRounds->count(); $bladeTimes = $keptRounds->map(fn ($r) => $bladeTimes[$r])->all(); $blazeTimes = $keptRounds->map(fn ($r) => $blazeTimes[$r])->all(); + $bladeCompilationTimes = $keptRounds->map(fn ($r) => $bladeCompilationTimes[$r])->all(); + $blazeCompilationTimes = $keptRounds->map(fn ($r) => $blazeCompilationTimes[$r])->all(); return [ 'blade_ms' => round(collect($bladeTimes)->median(), 2), 'blaze_ms' => round(collect($blazeTimes)->median(), 2), + 'blade_compilation_ms' => round(collect($bladeCompilationTimes)->median(), 2), + 'blaze_compilation_ms' => round(collect($blazeCompilationTimes)->median(), 2), ]; } @@ -216,13 +239,19 @@ protected function runMultipleAttempts(int $attempts): int // Filter outlier attempts — if either blade or blaze is an outlier, drop the whole attempt. $bladeValues = collect($allAttempts)->map(fn ($r) => $r['blade_ms']); $blazeValues = collect($allAttempts)->map(fn ($r) => $r['blaze_ms']); + $bladeCompilationValues = collect($allAttempts)->map(fn ($r) => $r['blade_compilation_ms']); + $blazeCompilationValues = collect($allAttempts)->map(fn ($r) => $r['blaze_compilation_ms']); $keptIndices = $this->nonOutlierIndices($bladeValues) ->intersect($this->nonOutlierIndices($blazeValues)) + ->intersect($this->nonOutlierIndices($bladeCompilationValues)) + ->intersect($this->nonOutlierIndices($blazeCompilationValues)) ->values(); $medianResult = [ 'blade_ms' => round($keptIndices->map(fn ($i) => $allAttempts[$i]['blade_ms'])->median(), 2), 'blaze_ms' => round($keptIndices->map(fn ($i) => $allAttempts[$i]['blaze_ms'])->median(), 2), + 'blade_compilation_ms' => round($keptIndices->map(fn ($i) => $allAttempts[$i]['blade_compilation_ms'])->median(), 2), + 'blaze_compilation_ms' => round($keptIndices->map(fn ($i) => $allAttempts[$i]['blaze_compilation_ms'])->median(), 2), ]; $results = [$benchmarkName => $medianResult]; @@ -246,20 +275,35 @@ protected function buildTable(array $results): array { $snapshot = $this->option('snapshot') ? null : $this->loadSnapshot(); - $headers = ['Blade', 'Blaze', 'Improvement']; + $headers = ['Benchmark', 'Blade', 'Blaze', 'Improvement']; - $rows = collect($results)->map(function ($result, $name) use ($snapshot) { + $rows = collect($results)->flatMap(function ($result, $name) use ($snapshot) { $blade = $this->formatTime($result['blade_ms']); $blaze = $this->formatTime($result['blaze_ms']); $improvement = $this->improvement($result).'%'; + $bladeCompilation = $this->formatTime($result['blade_compilation_ms']); + $blazeCompilation = $this->formatTime($result['blaze_compilation_ms']); + $compilationImprovement = $this->improvement($result, 'blade_compilation_ms', 'blaze_compilation_ms').'%'; if ($prev = $snapshot['benchmarks'][$name] ?? null) { $blade .= ' '.$this->formatChange($prev['blade_ms'], $result['blade_ms']); $blaze .= ' '.$this->formatChange($prev['blaze_ms'], $result['blaze_ms']); $improvement .= ' '.$this->formatImprovementChange($prev['improvement'], $this->improvement($result)); + + if (isset($prev['blade_compilation_ms'], $prev['blaze_compilation_ms'])) { + $bladeCompilation .= ' '.$this->formatChange($prev['blade_compilation_ms'], $result['blade_compilation_ms']); + $blazeCompilation .= ' '.$this->formatChange($prev['blaze_compilation_ms'], $result['blaze_compilation_ms']); + $compilationImprovement .= ' '.$this->formatImprovementChange( + $prev['compilation_improvement'], + $this->improvement($result, 'blade_compilation_ms', 'blaze_compilation_ms') + ); + } } - return [$blade, $blaze, $improvement]; + return [ + ['Render', $blade, $blaze, $improvement], + ['Compilation', $bladeCompilation, $blazeCompilation, $compilationImprovement], + ]; })->values()->all(); return [$headers, $rows, $snapshot]; @@ -413,6 +457,21 @@ protected function outputMarkdownAttempts(array $allAttempts, array $results, Co $rows[] = ['**Result**', "**{$blade}**", "**{$blaze}**", "**{$improvement}**"]; + $bladeCompilation = $this->formatTime($medianResult['blade_compilation_ms']); + $blazeCompilation = $this->formatTime($medianResult['blaze_compilation_ms']); + $compilationImprovement = $this->improvement($medianResult, 'blade_compilation_ms', 'blaze_compilation_ms').'%'; + + if (isset($snapshotData['blade_compilation_ms'], $snapshotData['blaze_compilation_ms'])) { + $bladeCompilation .= ' '.$this->formatChange($snapshotData['blade_compilation_ms'], $medianResult['blade_compilation_ms']); + $blazeCompilation .= ' '.$this->formatChange($snapshotData['blaze_compilation_ms'], $medianResult['blaze_compilation_ms']); + $compilationImprovement .= ' '.$this->formatImprovementChange( + $snapshotData['compilation_improvement'], + $this->improvement($medianResult, 'blade_compilation_ms', 'blaze_compilation_ms') + ); + } + + $rows[] = ['**Compilation**', "**{$bladeCompilation}**", "**{$blazeCompilation}**", "**{$compilationImprovement}**"]; + $allRows = collect([$headers, ...$rows]); $widths = collect($headers)->keys()->map( fn ($i) => $allRows->max(fn ($row) => mb_strlen($row[$i])) @@ -457,6 +516,9 @@ protected function outputJsonAttemptsResults(string $benchmarkName, array $allAt 'blade_ms' => $attempt['blade_ms'], 'blaze_ms' => $attempt['blaze_ms'], 'improvement' => $this->improvement($attempt), + 'blade_compilation_ms' => $attempt['blade_compilation_ms'], + 'blaze_compilation_ms' => $attempt['blaze_compilation_ms'], + 'compilation_improvement' => $this->improvement($attempt, 'blade_compilation_ms', 'blaze_compilation_ms'), 'outlier' => ! $keptIndices->contains($i), ])->values()->all(), 'benchmarks' => $results, @@ -472,6 +534,9 @@ protected function saveSnapshot(array $results): void 'blade_ms' => $result['blade_ms'], 'blaze_ms' => $result['blaze_ms'], 'improvement' => $this->improvement($result), + 'blade_compilation_ms' => $result['blade_compilation_ms'], + 'blaze_compilation_ms' => $result['blaze_compilation_ms'], + 'compilation_improvement' => $this->improvement($result, 'blade_compilation_ms', 'blaze_compilation_ms'), ])->all(), ]; @@ -501,10 +566,10 @@ protected function snapshotPath(): string return dirname(__DIR__, 4).'/benchmark-snapshot.json'; } - protected function improvement(array $result): float + protected function improvement(array $result, string $bladeKey = 'blade_ms', string $blazeKey = 'blaze_ms'): float { - return $result['blade_ms'] > 0 - ? round((1 - $result['blaze_ms'] / $result['blade_ms']) * 100, 1) + return $result[$bladeKey] > 0 + ? round((1 - $result[$blazeKey] / $result[$bladeKey]) * 100, 1) : 0; } @@ -577,6 +642,28 @@ protected function measureView(string $view): float return (hrtime(true) - $start) / 1_000_000; } + protected function measureCompilation(bool $blaze): float + { + $blaze ? Blaze::enable() : Blaze::disable(); + + Artisan::call('view:clear'); + + $compiler = app('blade.compiler'); + $views = File::allFiles(resource_path('views')); + + gc_collect_cycles(); + + $start = hrtime(true); + + foreach ($views as $view) { + if ($view->getExtension() === 'php' && str_ends_with($view->getFilename(), '.blade.php')) { + $compiler->compile($view->getPathname()); + } + } + + return (hrtime(true) - $start) / 1_000_000; + } + protected function formatTime(float $ms): string { return number_format($ms, 2).'ms'; From 955fdb5fc9ad4ee1e25b44f15608cf1db0d10a07 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sun, 2 Aug 2026 00:18:12 -0400 Subject: [PATCH 2/8] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index abafafc0..7005a5d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,7 @@ jobs: - name: Checkout base branch uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.base.sha }} + ref: v1.0.13 - name: Setup PHP uses: shivammathur/setup-php@v2 From b63c074fcf519e78123ec98a679d07aa6f89f979 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sun, 2 Aug 2026 00:22:56 -0400 Subject: [PATCH 3/8] Update ci.yml --- .github/workflows/ci.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7005a5d5..193b7b73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,11 +60,20 @@ jobs: runs-on: ubuntu-latest steps: + - name: Checkout PR benchmark harness + uses: actions/checkout@v4 + + - name: Save PR benchmark harness + run: cp workbench/app/Console/Commands/BenchmarkCommand.php ${{ runner.temp }}/BenchmarkCommand.php + - name: Checkout base branch uses: actions/checkout@v4 with: ref: v1.0.13 + - name: Restore PR benchmark harness + run: cp ${{ runner.temp }}/BenchmarkCommand.php workbench/app/Console/Commands/BenchmarkCommand.php + - name: Setup PHP uses: shivammathur/setup-php@v2 with: From 961022c5d771969bee06feefbcf25550a502bcb7 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sun, 2 Aug 2026 00:32:08 -0400 Subject: [PATCH 4/8] Update benchmark --- .github/workflows/benchmark-comment.yml | 2 +- .github/workflows/benchmark-on-demand.yml | 2 +- .../app/Console/Commands/BenchmarkCommand.php | 112 +++++++----------- 3 files changed, 44 insertions(+), 72 deletions(-) diff --git a/.github/workflows/benchmark-comment.yml b/.github/workflows/benchmark-comment.yml index 02766190..27bb084a 100644 --- a/.github/workflows/benchmark-comment.yml +++ b/.github/workflows/benchmark-comment.yml @@ -30,7 +30,7 @@ jobs: echo "heading=$(sed -n '2p' benchmark-result.md)" >> "$GITHUB_OUTPUT" tail -n +2 benchmark-result.md > benchmark-comment.md echo "" >> benchmark-comment.md - echo "To run a specific benchmark, comment /benchmark <name> where name is one of: attributes, aware, class, default, forwarding, merge, named-slots, no-attributes, slot" >> benchmark-comment.md + echo "To run a specific benchmark, comment /benchmark <name>
attributes, aware, class, compilation, default, forwarding, merge, named-slots, no-attributes, slot
" >> benchmark-comment.md - name: Find existing comment uses: peter-evans/find-comment@v3 diff --git a/.github/workflows/benchmark-on-demand.yml b/.github/workflows/benchmark-on-demand.yml index 83b62b59..fa776c79 100644 --- a/.github/workflows/benchmark-on-demand.yml +++ b/.github/workflows/benchmark-on-demand.yml @@ -18,7 +18,7 @@ jobs: COMMENT: ${{ github.event.comment.body }} run: | BENCHMARK=$(echo "$COMMENT" | awk '{print $2}') - VALID="attributes aware class default forwarding merge named-slots no-attributes slot" + VALID="attributes aware class compilation default forwarding merge named-slots no-attributes slot" if ! echo "$VALID" | grep -qw "$BENCHMARK"; then echo "::error::Unknown benchmark '$BENCHMARK'. Valid options: $VALID" exit 1 diff --git a/workbench/app/Console/Commands/BenchmarkCommand.php b/workbench/app/Console/Commands/BenchmarkCommand.php index dbad5f0e..25d619c6 100644 --- a/workbench/app/Console/Commands/BenchmarkCommand.php +++ b/workbench/app/Console/Commands/BenchmarkCommand.php @@ -78,6 +78,11 @@ public function handle(): int protected function runBenchmark(): array { $benchmarkName = $this->argument('benchmark'); + + if ($benchmarkName === 'compilation') { + return $this->runCompilationBenchmark(); + } + $bladeView = "bench.blade.{$benchmarkName}"; $blazeView = "bench.blaze.{$benchmarkName}"; $showProgress = ! $this->option('ci') && ! $this->option('json'); @@ -107,8 +112,6 @@ protected function runBenchmark(): array $bladeTimes = []; $blazeTimes = []; - $bladeCompilationTimes = []; - $blazeCompilationTimes = []; $bar?->setMessage('Benchmarking...'); @@ -124,20 +127,6 @@ protected function runBenchmark(): array $bar?->advance(); } - // Compilation clears the view cache, so measure it only after all render rounds. - $this->measureCompilation(false); - $this->measureCompilation(true); - - for ($r = 0; $r < $this->rounds; $r++) { - if ($r % 2 === 0) { - $bladeCompilationTimes[] = $this->measureCompilation(false); - $blazeCompilationTimes[] = $this->measureCompilation(true); - } else { - $blazeCompilationTimes[] = $this->measureCompilation(true); - $bladeCompilationTimes[] = $this->measureCompilation(false); - } - } - $bar?->setMessage('Done!'); $bar?->finish(); @@ -149,21 +138,46 @@ protected function runBenchmark(): array // if neither its blade nor blaze time is an outlier. $keptRounds = $this->nonOutlierIndices(collect($bladeTimes)) ->intersect($this->nonOutlierIndices(collect($blazeTimes))) - ->intersect($this->nonOutlierIndices(collect($bladeCompilationTimes))) - ->intersect($this->nonOutlierIndices(collect($blazeCompilationTimes))) ->values(); $this->filteredRounds = $this->rounds - $keptRounds->count(); $bladeTimes = $keptRounds->map(fn ($r) => $bladeTimes[$r])->all(); $blazeTimes = $keptRounds->map(fn ($r) => $blazeTimes[$r])->all(); - $bladeCompilationTimes = $keptRounds->map(fn ($r) => $bladeCompilationTimes[$r])->all(); - $blazeCompilationTimes = $keptRounds->map(fn ($r) => $blazeCompilationTimes[$r])->all(); return [ 'blade_ms' => round(collect($bladeTimes)->median(), 2), 'blaze_ms' => round(collect($blazeTimes)->median(), 2), - 'blade_compilation_ms' => round(collect($bladeCompilationTimes)->median(), 2), - 'blaze_compilation_ms' => round(collect($blazeCompilationTimes)->median(), 2), + ]; + } + + protected function runCompilationBenchmark(): array + { + for ($w = 0; $w < $this->warmupRounds; $w++) { + $this->measureCompilation(false); + $this->measureCompilation(true); + } + + $bladeTimes = []; + $blazeTimes = []; + + for ($r = 0; $r < $this->rounds; $r++) { + if ($r % 2 === 0) { + $bladeTimes[] = $this->measureCompilation(false); + $blazeTimes[] = $this->measureCompilation(true); + } else { + $blazeTimes[] = $this->measureCompilation(true); + $bladeTimes[] = $this->measureCompilation(false); + } + } + + $keptRounds = $this->nonOutlierIndices(collect($bladeTimes)) + ->intersect($this->nonOutlierIndices(collect($blazeTimes))) + ->values(); + $this->filteredRounds = $this->rounds - $keptRounds->count(); + + return [ + 'blade_ms' => round($keptRounds->map(fn ($r) => $bladeTimes[$r])->median(), 2), + 'blaze_ms' => round($keptRounds->map(fn ($r) => $blazeTimes[$r])->median(), 2), ]; } @@ -239,19 +253,13 @@ protected function runMultipleAttempts(int $attempts): int // Filter outlier attempts — if either blade or blaze is an outlier, drop the whole attempt. $bladeValues = collect($allAttempts)->map(fn ($r) => $r['blade_ms']); $blazeValues = collect($allAttempts)->map(fn ($r) => $r['blaze_ms']); - $bladeCompilationValues = collect($allAttempts)->map(fn ($r) => $r['blade_compilation_ms']); - $blazeCompilationValues = collect($allAttempts)->map(fn ($r) => $r['blaze_compilation_ms']); $keptIndices = $this->nonOutlierIndices($bladeValues) ->intersect($this->nonOutlierIndices($blazeValues)) - ->intersect($this->nonOutlierIndices($bladeCompilationValues)) - ->intersect($this->nonOutlierIndices($blazeCompilationValues)) ->values(); $medianResult = [ 'blade_ms' => round($keptIndices->map(fn ($i) => $allAttempts[$i]['blade_ms'])->median(), 2), 'blaze_ms' => round($keptIndices->map(fn ($i) => $allAttempts[$i]['blaze_ms'])->median(), 2), - 'blade_compilation_ms' => round($keptIndices->map(fn ($i) => $allAttempts[$i]['blade_compilation_ms'])->median(), 2), - 'blaze_compilation_ms' => round($keptIndices->map(fn ($i) => $allAttempts[$i]['blaze_compilation_ms'])->median(), 2), ]; $results = [$benchmarkName => $medianResult]; @@ -275,35 +283,20 @@ protected function buildTable(array $results): array { $snapshot = $this->option('snapshot') ? null : $this->loadSnapshot(); - $headers = ['Benchmark', 'Blade', 'Blaze', 'Improvement']; + $headers = ['Blade', 'Blaze', 'Improvement']; - $rows = collect($results)->flatMap(function ($result, $name) use ($snapshot) { + $rows = collect($results)->map(function ($result, $name) use ($snapshot) { $blade = $this->formatTime($result['blade_ms']); $blaze = $this->formatTime($result['blaze_ms']); $improvement = $this->improvement($result).'%'; - $bladeCompilation = $this->formatTime($result['blade_compilation_ms']); - $blazeCompilation = $this->formatTime($result['blaze_compilation_ms']); - $compilationImprovement = $this->improvement($result, 'blade_compilation_ms', 'blaze_compilation_ms').'%'; if ($prev = $snapshot['benchmarks'][$name] ?? null) { $blade .= ' '.$this->formatChange($prev['blade_ms'], $result['blade_ms']); $blaze .= ' '.$this->formatChange($prev['blaze_ms'], $result['blaze_ms']); $improvement .= ' '.$this->formatImprovementChange($prev['improvement'], $this->improvement($result)); - - if (isset($prev['blade_compilation_ms'], $prev['blaze_compilation_ms'])) { - $bladeCompilation .= ' '.$this->formatChange($prev['blade_compilation_ms'], $result['blade_compilation_ms']); - $blazeCompilation .= ' '.$this->formatChange($prev['blaze_compilation_ms'], $result['blaze_compilation_ms']); - $compilationImprovement .= ' '.$this->formatImprovementChange( - $prev['compilation_improvement'], - $this->improvement($result, 'blade_compilation_ms', 'blaze_compilation_ms') - ); - } } - return [ - ['Render', $blade, $blaze, $improvement], - ['Compilation', $bladeCompilation, $blazeCompilation, $compilationImprovement], - ]; + return [$blade, $blaze, $improvement]; })->values()->all(); return [$headers, $rows, $snapshot]; @@ -457,21 +450,6 @@ protected function outputMarkdownAttempts(array $allAttempts, array $results, Co $rows[] = ['**Result**', "**{$blade}**", "**{$blaze}**", "**{$improvement}**"]; - $bladeCompilation = $this->formatTime($medianResult['blade_compilation_ms']); - $blazeCompilation = $this->formatTime($medianResult['blaze_compilation_ms']); - $compilationImprovement = $this->improvement($medianResult, 'blade_compilation_ms', 'blaze_compilation_ms').'%'; - - if (isset($snapshotData['blade_compilation_ms'], $snapshotData['blaze_compilation_ms'])) { - $bladeCompilation .= ' '.$this->formatChange($snapshotData['blade_compilation_ms'], $medianResult['blade_compilation_ms']); - $blazeCompilation .= ' '.$this->formatChange($snapshotData['blaze_compilation_ms'], $medianResult['blaze_compilation_ms']); - $compilationImprovement .= ' '.$this->formatImprovementChange( - $snapshotData['compilation_improvement'], - $this->improvement($medianResult, 'blade_compilation_ms', 'blaze_compilation_ms') - ); - } - - $rows[] = ['**Compilation**', "**{$bladeCompilation}**", "**{$blazeCompilation}**", "**{$compilationImprovement}**"]; - $allRows = collect([$headers, ...$rows]); $widths = collect($headers)->keys()->map( fn ($i) => $allRows->max(fn ($row) => mb_strlen($row[$i])) @@ -516,9 +494,6 @@ protected function outputJsonAttemptsResults(string $benchmarkName, array $allAt 'blade_ms' => $attempt['blade_ms'], 'blaze_ms' => $attempt['blaze_ms'], 'improvement' => $this->improvement($attempt), - 'blade_compilation_ms' => $attempt['blade_compilation_ms'], - 'blaze_compilation_ms' => $attempt['blaze_compilation_ms'], - 'compilation_improvement' => $this->improvement($attempt, 'blade_compilation_ms', 'blaze_compilation_ms'), 'outlier' => ! $keptIndices->contains($i), ])->values()->all(), 'benchmarks' => $results, @@ -534,9 +509,6 @@ protected function saveSnapshot(array $results): void 'blade_ms' => $result['blade_ms'], 'blaze_ms' => $result['blaze_ms'], 'improvement' => $this->improvement($result), - 'blade_compilation_ms' => $result['blade_compilation_ms'], - 'blaze_compilation_ms' => $result['blaze_compilation_ms'], - 'compilation_improvement' => $this->improvement($result, 'blade_compilation_ms', 'blaze_compilation_ms'), ])->all(), ]; @@ -566,10 +538,10 @@ protected function snapshotPath(): string return dirname(__DIR__, 4).'/benchmark-snapshot.json'; } - protected function improvement(array $result, string $bladeKey = 'blade_ms', string $blazeKey = 'blaze_ms'): float + protected function improvement(array $result): float { - return $result[$bladeKey] > 0 - ? round((1 - $result[$blazeKey] / $result[$bladeKey]) * 100, 1) + return $result['blade_ms'] > 0 + ? round((1 - $result['blaze_ms'] / $result['blade_ms']) * 100, 1) : 0; } From 65d23b92e9086f7e62ee3b2030be0ef35a60bd85 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sun, 2 Aug 2026 00:32:50 -0400 Subject: [PATCH 5/8] Update ci.yml --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 193b7b73..939bf309 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,7 +86,7 @@ jobs: run: composer install --prefer-dist --no-progress --no-interaction - name: Generate baseline snapshot - run: vendor/bin/testbench benchmark default --ci --iterations=5000 --rounds=10 --attempts=10 --snapshot + run: vendor/bin/testbench benchmark compilation --ci --rounds=10 --attempts=10 --snapshot - name: Save baseline snapshot run: cp benchmark-snapshot.json ${{ runner.temp }}/benchmark-snapshot.json @@ -103,7 +103,7 @@ jobs: - name: Run benchmark run: | echo "${{ github.event.pull_request.number }}" > benchmark-result.md - vendor/bin/testbench benchmark default --ci --iterations=5000 --rounds=10 --attempts=10 >> benchmark-result.md + vendor/bin/testbench benchmark compilation --ci --rounds=10 --attempts=10 >> benchmark-result.md - name: Upload benchmark result uses: actions/upload-artifact@v4 From 0e8cc4b9d8d1c7f5a24b303026d501fdc3b1b854 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sun, 2 Aug 2026 00:35:13 -0400 Subject: [PATCH 6/8] Update benchmark-comment.yml --- .github/workflows/benchmark-comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/benchmark-comment.yml b/.github/workflows/benchmark-comment.yml index 27bb084a..9fde47f9 100644 --- a/.github/workflows/benchmark-comment.yml +++ b/.github/workflows/benchmark-comment.yml @@ -30,7 +30,7 @@ jobs: echo "heading=$(sed -n '2p' benchmark-result.md)" >> "$GITHUB_OUTPUT" tail -n +2 benchmark-result.md > benchmark-comment.md echo "" >> benchmark-comment.md - echo "To run a specific benchmark, comment /benchmark <name>
attributes, aware, class, compilation, default, forwarding, merge, named-slots, no-attributes, slot
" >> benchmark-comment.md + echo "To run a specific benchmark, comment /benchmark <name>
attributes, aware, class, default, forwarding, merge, named-slots, no-attributes, slot, compilation
" >> benchmark-comment.md - name: Find existing comment uses: peter-evans/find-comment@v3 From f2758fa89c340ae344bdf162e9e7864925d0f422 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sun, 2 Aug 2026 00:38:08 -0400 Subject: [PATCH 7/8] Refactor --- .../app/Console/Commands/BenchmarkCommand.php | 32 ++++++++--------- .../Commands/BenchmarkVarianceCommand.php | 34 +++++++++---------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/workbench/app/Console/Commands/BenchmarkCommand.php b/workbench/app/Console/Commands/BenchmarkCommand.php index 25d619c6..74d51ace 100644 --- a/workbench/app/Console/Commands/BenchmarkCommand.php +++ b/workbench/app/Console/Commands/BenchmarkCommand.php @@ -283,20 +283,20 @@ protected function buildTable(array $results): array { $snapshot = $this->option('snapshot') ? null : $this->loadSnapshot(); - $headers = ['Blade', 'Blaze', 'Improvement']; + $headers = ['Blade', 'Blaze', 'Change']; $rows = collect($results)->map(function ($result, $name) use ($snapshot) { $blade = $this->formatTime($result['blade_ms']); $blaze = $this->formatTime($result['blaze_ms']); - $improvement = $this->improvement($result).'%'; + $change = $this->change($result).'%'; if ($prev = $snapshot['benchmarks'][$name] ?? null) { $blade .= ' '.$this->formatChange($prev['blade_ms'], $result['blade_ms']); $blaze .= ' '.$this->formatChange($prev['blaze_ms'], $result['blaze_ms']); - $improvement .= ' '.$this->formatImprovementChange($prev['improvement'], $this->improvement($result)); + $change .= ' '.$this->formatChangeDelta($prev['change'], $this->change($result)); } - return [$blade, $blaze, $improvement]; + return [$blade, $blaze, $change]; })->values()->all(); return [$headers, $rows, $snapshot]; @@ -370,13 +370,13 @@ protected function displayAttemptsResults(array $allAttempts, array $results, Co foreach ($allAttempts as $i => $attempt) { $isOutlier = ! $keptIndices->contains($i); - $improvement = $this->improvement($attempt); + $change = $this->change($attempt); $line = sprintf( ' Attempt %d: Blade %s Blaze %s (%s%%)', $i + 1, $this->formatTime($attempt['blade_ms']), $this->formatTime($attempt['blaze_ms']), - $improvement + $change ); $isOutlier @@ -411,7 +411,7 @@ protected function outputMarkdownAttempts(array $allAttempts, array $results, Co $snapshot = $this->option('snapshot') ? null : $this->loadSnapshot(); $snapshotData = $snapshot['benchmarks'][$benchmarkName] ?? null; - $headers = ['Attempt', 'Blade', 'Blaze', 'Improvement']; + $headers = ['Attempt', 'Blade', 'Blaze', 'Change']; $rows = []; @@ -423,7 +423,7 @@ protected function outputMarkdownAttempts(array $allAttempts, array $results, Co '`#'.($i + 1).'`'.($isOutlier ? ' \*' : ''), $this->formatTime($attempt['blade_ms']), $this->formatTime($attempt['blaze_ms']), - $this->improvement($attempt).'%', + $this->change($attempt).'%', ]; } @@ -433,22 +433,22 @@ protected function outputMarkdownAttempts(array $allAttempts, array $results, Co 'Snapshot', $this->formatTime($snapshotData['blade_ms']), $this->formatTime($snapshotData['blaze_ms']), - $snapshotData['improvement'].'%', + $snapshotData['change'].'%', ]; } // Result row (median with comparison deltas when snapshot exists). $blade = $this->formatTime($medianResult['blade_ms']); $blaze = $this->formatTime($medianResult['blaze_ms']); - $improvement = $this->improvement($medianResult).'%'; + $change = $this->change($medianResult).'%'; if ($snapshotData) { $blade .= ' '.$this->formatChange($snapshotData['blade_ms'], $medianResult['blade_ms']); $blaze .= ' '.$this->formatChange($snapshotData['blaze_ms'], $medianResult['blaze_ms']); - $improvement .= ' '.$this->formatImprovementChange($snapshotData['improvement'], $this->improvement($medianResult)); + $change .= ' '.$this->formatChangeDelta($snapshotData['change'], $this->change($medianResult)); } - $rows[] = ['**Result**', "**{$blade}**", "**{$blaze}**", "**{$improvement}**"]; + $rows[] = ['**Result**', "**{$blade}**", "**{$blaze}**", "**{$change}**"]; $allRows = collect([$headers, ...$rows]); $widths = collect($headers)->keys()->map( @@ -493,7 +493,7 @@ protected function outputJsonAttemptsResults(string $benchmarkName, array $allAt 'attempts_detail' => collect($allAttempts)->map(fn ($attempt, $i) => [ 'blade_ms' => $attempt['blade_ms'], 'blaze_ms' => $attempt['blaze_ms'], - 'improvement' => $this->improvement($attempt), + 'change' => $this->change($attempt), 'outlier' => ! $keptIndices->contains($i), ])->values()->all(), 'benchmarks' => $results, @@ -508,7 +508,7 @@ protected function saveSnapshot(array $results): void 'benchmarks' => collect($results)->map(fn ($result) => [ 'blade_ms' => $result['blade_ms'], 'blaze_ms' => $result['blaze_ms'], - 'improvement' => $this->improvement($result), + 'change' => $this->change($result), ])->all(), ]; @@ -538,7 +538,7 @@ protected function snapshotPath(): string return dirname(__DIR__, 4).'/benchmark-snapshot.json'; } - protected function improvement(array $result): float + protected function change(array $result): float { return $result['blade_ms'] > 0 ? round((1 - $result['blaze_ms'] / $result['blade_ms']) * 100, 1) @@ -562,7 +562,7 @@ protected function formatChange(float $old, float $new, float $threshold = 2): s return "({$sign}".round($change, 1).'%)'; } - protected function formatImprovementChange(float $old, float $new, float $threshold = 0.2): string + protected function formatChangeDelta(float $old, float $new, float $threshold = 0.2): string { $delta = round($new - $old, 1); diff --git a/workbench/app/Console/Commands/BenchmarkVarianceCommand.php b/workbench/app/Console/Commands/BenchmarkVarianceCommand.php index 9bbcb458..b59aa5e0 100644 --- a/workbench/app/Console/Commands/BenchmarkVarianceCommand.php +++ b/workbench/app/Console/Commands/BenchmarkVarianceCommand.php @@ -128,31 +128,31 @@ protected function runBenchmarkInProcess(string $name): array protected function displayVarianceResults(array $snapshot, array $allRuns, float $avgRunDuration, float $totalDuration): void { - $snapshotImprovement = $this->improvement($snapshot); + $snapshotChange = $this->change($snapshot); $bladeChanges = collect($allRuns)->map(fn ($run) => $this->percentChange($snapshot['blade_ms'], $run['blade_ms'])); $blazeChanges = collect($allRuns)->map(fn ($run) => $this->percentChange($snapshot['blaze_ms'], $run['blaze_ms'])); - $improvementChanges = collect($allRuns)->map(fn ($run) => round($this->improvement($run) - $snapshotImprovement, 1)); + $changes = collect($allRuns)->map(fn ($run) => round($this->change($run) - $snapshotChange, 1)); - $headers = ['', 'Blade', 'Blaze', 'Improvement']; + $headers = ['', 'Blade', 'Blaze', 'Change']; $rows = [ [ 'Snapshot', $this->formatTime($snapshot['blade_ms']), $this->formatTime($snapshot['blaze_ms']), - $snapshotImprovement.'%', + $snapshotChange.'%', ], [ 'Variance', $this->formatVarianceRange($bladeChanges->min(), $bladeChanges->max()), $this->formatVarianceRange($blazeChanges->min(), $blazeChanges->max()), - $this->formatVarianceRange($improvementChanges->min(), $improvementChanges->max()), + $this->formatVarianceRange($changes->min(), $changes->max()), ], [ 'Std Dev', '±'.$this->stddev($bladeChanges).'%', '±'.$this->stddev($blazeChanges).'%', - '±'.$this->stddev($improvementChanges).'%', + '±'.$this->stddev($changes).'%', ], ]; @@ -168,31 +168,31 @@ protected function displayVarianceResults(array $snapshot, array $allRuns, float protected function outputVarianceMarkdown(array $snapshot, array $allRuns, float $avgRunDuration, float $totalDuration): void { - $snapshotImprovement = $this->improvement($snapshot); + $snapshotChange = $this->change($snapshot); $bladeChanges = collect($allRuns)->map(fn ($run) => $this->percentChange($snapshot['blade_ms'], $run['blade_ms'])); $blazeChanges = collect($allRuns)->map(fn ($run) => $this->percentChange($snapshot['blaze_ms'], $run['blaze_ms'])); - $improvementChanges = collect($allRuns)->map(fn ($run) => round($this->improvement($run) - $snapshotImprovement, 1)); + $changes = collect($allRuns)->map(fn ($run) => round($this->change($run) - $snapshotChange, 1)); - $headers = ['', 'Blade', 'Blaze', 'Improvement']; + $headers = ['', 'Blade', 'Blaze', 'Change']; $rows = [ [ 'Snapshot', $this->formatTime($snapshot['blade_ms']), $this->formatTime($snapshot['blaze_ms']), - $snapshotImprovement.'%', + $snapshotChange.'%', ], [ 'Variance', $this->formatVarianceRange($bladeChanges->min(), $bladeChanges->max()), $this->formatVarianceRange($blazeChanges->min(), $blazeChanges->max()), - $this->formatVarianceRange($improvementChanges->min(), $improvementChanges->max()), + $this->formatVarianceRange($changes->min(), $changes->max()), ], [ 'Std Dev', '±'.$this->stddev($bladeChanges).'%', '±'.$this->stddev($blazeChanges).'%', - '±'.$this->stddev($improvementChanges).'%', + '±'.$this->stddev($changes).'%', ], ]; @@ -230,7 +230,7 @@ protected function saveSnapshot(array $results): void 'benchmarks' => collect($results)->map(fn ($result) => [ 'blade_ms' => $result['blade_ms'], 'blaze_ms' => $result['blaze_ms'], - 'improvement' => $this->improvement($result), + 'change' => $this->change($result), ])->all(), ]; @@ -242,11 +242,11 @@ protected function saveSnapshot(array $results): void protected function outputJson(array $snapshot, array $allRuns, float $avgRunDuration, float $totalDuration): void { - $snapshotImprovement = $this->improvement($snapshot); + $snapshotChange = $this->change($snapshot); $bladeChanges = collect($allRuns)->map(fn ($run) => $this->percentChange($snapshot['blade_ms'], $run['blade_ms'])); $blazeChanges = collect($allRuns)->map(fn ($run) => $this->percentChange($snapshot['blaze_ms'], $run['blaze_ms'])); - $improvementChanges = collect($allRuns)->map(fn ($run) => round($this->improvement($run) - $snapshotImprovement, 1)); + $changes = collect($allRuns)->map(fn ($run) => round($this->change($run) - $snapshotChange, 1)); $this->output->writeln(json_encode([ 'iterations' => $this->iterations, @@ -258,12 +258,12 @@ protected function outputJson(array $snapshot, array $allRuns, float $avgRunDura 'snapshot' => [ 'blade_ms' => $snapshot['blade_ms'], 'blaze_ms' => $snapshot['blaze_ms'], - 'improvement' => $snapshotImprovement, + 'change' => $snapshotChange, ], 'variance' => [ 'blade' => ['min' => $bladeChanges->min(), 'max' => $bladeChanges->max(), 'stddev' => $this->stddev($bladeChanges)], 'blaze' => ['min' => $blazeChanges->min(), 'max' => $blazeChanges->max(), 'stddev' => $this->stddev($blazeChanges)], - 'improvement' => ['min' => $improvementChanges->min(), 'max' => $improvementChanges->max(), 'stddev' => $this->stddev($improvementChanges)], + 'change' => ['min' => $changes->min(), 'max' => $changes->max(), 'stddev' => $this->stddev($changes)], ], ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); } From b97d7e86c84d968dd2b43b48a507b86a145f9f27 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sun, 2 Aug 2026 00:38:46 -0400 Subject: [PATCH 8/8] Update ci.yml --- .github/workflows/ci.yml | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 939bf309..abafafc0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,19 +60,10 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout PR benchmark harness - uses: actions/checkout@v4 - - - name: Save PR benchmark harness - run: cp workbench/app/Console/Commands/BenchmarkCommand.php ${{ runner.temp }}/BenchmarkCommand.php - - name: Checkout base branch uses: actions/checkout@v4 with: - ref: v1.0.13 - - - name: Restore PR benchmark harness - run: cp ${{ runner.temp }}/BenchmarkCommand.php workbench/app/Console/Commands/BenchmarkCommand.php + ref: ${{ github.event.pull_request.base.sha }} - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -86,7 +77,7 @@ jobs: run: composer install --prefer-dist --no-progress --no-interaction - name: Generate baseline snapshot - run: vendor/bin/testbench benchmark compilation --ci --rounds=10 --attempts=10 --snapshot + run: vendor/bin/testbench benchmark default --ci --iterations=5000 --rounds=10 --attempts=10 --snapshot - name: Save baseline snapshot run: cp benchmark-snapshot.json ${{ runner.temp }}/benchmark-snapshot.json @@ -103,7 +94,7 @@ jobs: - name: Run benchmark run: | echo "${{ github.event.pull_request.number }}" > benchmark-result.md - vendor/bin/testbench benchmark compilation --ci --rounds=10 --attempts=10 >> benchmark-result.md + vendor/bin/testbench benchmark default --ci --iterations=5000 --rounds=10 --attempts=10 >> benchmark-result.md - name: Upload benchmark result uses: actions/upload-artifact@v4