diff --git a/.github/workflows/benchmark-comment.yml b/.github/workflows/benchmark-comment.yml index 02766190..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> 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, default, forwarding, merge, named-slots, no-attributes, slot, compilation
" >> 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 930637a3..74d51ace 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 { @@ -77,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'); @@ -144,6 +150,37 @@ protected function runBenchmark(): array ]; } + 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), + ]; + } + protected function runMultipleAttempts(int $attempts): int { $benchmarkName = $this->argument('benchmark'); @@ -246,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]; @@ -333,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 @@ -374,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 = []; @@ -386,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).'%', ]; } @@ -396,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( @@ -456,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, @@ -471,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(), ]; @@ -501,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) @@ -525,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); @@ -577,6 +614,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'; 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)); }