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
2 changes: 1 addition & 1 deletion .github/workflows/benchmark-comment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<sub>To run a specific benchmark, comment <code>/benchmark &lt;name&gt;</code> where name is one of: <code>attributes</code>, <code>aware</code>, <code>class</code>, <code>default</code>, <code>forwarding</code>, <code>merge</code>, <code>named-slots</code>, <code>no-attributes</code>, <code>slot</code></sub>" >> benchmark-comment.md
echo "<sub>To run a specific benchmark, comment <code>/benchmark &lt;name&gt;</code><br><code>attributes</code>, <code>aware</code>, <code>class</code>, <code>default</code>, <code>forwarding</code>, <code>merge</code>, <code>named-slots</code>, <code>no-attributes</code>, <code>slot</code>, <code>compilation</code></sub>" >> benchmark-comment.md

- name: Find existing comment
uses: peter-evans/find-comment@v3
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/benchmark-on-demand.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
91 changes: 75 additions & 16 deletions workbench/app/Console/Commands/BenchmarkCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = [];

Expand All @@ -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).'%',
];
}

Expand All @@ -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(
Expand Down Expand Up @@ -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,
Expand All @@ -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(),
];

Expand Down Expand Up @@ -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)
Expand All @@ -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);

Expand Down Expand Up @@ -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';
Expand Down
34 changes: 17 additions & 17 deletions workbench/app/Console/Commands/BenchmarkVarianceCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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).'%',
],
];

Expand All @@ -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).'%',
],
];

Expand Down Expand Up @@ -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(),
];

Expand 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,
Expand All @@ -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));
}
Expand Down
Loading