Skip to content
Open
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
68 changes: 35 additions & 33 deletions src/BladeService.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,20 @@
use Livewire\Blaze\Compiler\DirectiveCompiler;
use Livewire\Blaze\Parser\Attribute;
use Livewire\Blaze\Support\LaravelRegex;
use ReflectionClass;

class BladeService
{
protected ComponentTagCompiler $tagCompiler;

protected \ReflectionMethod $storeRawBlockMethod;
protected \ReflectionMethod $restoreRawContentMethod;
protected ?\ReflectionMethod $restorePhpBlocksMethod = null;
protected \ReflectionMethod $compileCommentsMethod;
protected \ReflectionMethod $compileUseMethod;
protected \ReflectionMethod $compileAttributeEchosMethod;
protected \ReflectionMethod $guessAnonymousComponentUsingNamespacesMethod;
protected \ReflectionMethod $guessAnonymousComponentUsingPathsMethod;

public function __construct(
public BladeCompiler $compiler,
protected Factory $view,
Expand All @@ -25,6 +33,20 @@ public function __construct(
$compiler->getClassComponentNamespaces(),
$compiler,
);

$compilerReflection = new \ReflectionClass($compiler);
$this->storeRawBlockMethod = $compilerReflection->getMethod('storeRawBlock');
$this->restoreRawContentMethod = $compilerReflection->getMethod('restoreRawContent');
if ($compilerReflection->hasMethod('restorePhpBlocks')) {
$this->restorePhpBlocksMethod = $compilerReflection->getMethod('restorePhpBlocks');
}
$this->compileCommentsMethod = $compilerReflection->getMethod('compileComments');
$this->compileUseMethod = $compilerReflection->getMethod('compileUse');

$tagCompilerReflection = new \ReflectionClass($this->tagCompiler);
$this->compileAttributeEchosMethod = $tagCompilerReflection->getMethod('compileAttributeEchos');
$this->guessAnonymousComponentUsingNamespacesMethod = $tagCompilerReflection->getMethod('guessAnonymousComponentUsingNamespaces');
$this->guessAnonymousComponentUsingPathsMethod = $tagCompilerReflection->getMethod('guessAnonymousComponentUsingPaths');
}

/**
Expand Down Expand Up @@ -81,11 +103,8 @@ public function storePhpBlocks(string $input): string
*/
protected function storeRawBlock(string $pattern, string $content): string
{
$reflection = new \ReflectionClass($this->compiler);
$method = $reflection->getMethod('storeRawBlock');

return preg_replace_callback($pattern, function ($matches) use ($method) {
return $method->invoke($this->compiler, $matches[0]);
return preg_replace_callback($pattern, function ($matches) {
return $this->storeRawBlockMethod->invoke($this->compiler, $matches[0]);
}, $content);
}

Expand All @@ -94,32 +113,27 @@ protected function storeRawBlock(string $pattern, string $content): string
*/
public function restoreRawBlocks(string $input): string
{
$reflection = new \ReflectionClass($this->compiler);
$method = $reflection->getMethod('restoreRawContent');

return $method->invoke($this->compiler, $input);
return $this->restoreRawContentMethod->invoke($this->compiler, $input);
}

/**
* Restore raw block placeholders to their original content.
*/
public function restorePhpBlocks(string $input): string
{
$reflection = new \ReflectionClass($this->compiler);
$method = $reflection->getMethod('restorePhpBlocks');
if ($this->restorePhpBlocksMethod === null) {
return $input;
}

return $method->invoke($this->compiler, $input);
return $this->restorePhpBlocksMethod->invoke($this->compiler, $input);
}

/**
* Invoke the Blade compiler's compileComments via reflection.
*/
public function compileComments(string $input): string
{
$reflection = new \ReflectionClass($this->compiler);
$compileComments = $reflection->getMethod('compileComments');

return $compileComments->invoke($this->compiler, $input);
return $this->compileCommentsMethod->invoke($this->compiler, $input);
}

/**
Expand Down Expand Up @@ -152,10 +166,7 @@ public function preprocessAttributeString(string $attributeString): string
public function compileUseStatements(string $input): string
{
return DirectiveCompiler::make()->directive('use', function ($expression) {
$reflection = new \ReflectionClass($this->compiler);
$method = $reflection->getMethod('compileUse');

return $method->invoke($this->compiler, $expression);
return $this->compileUseMethod->invoke($this->compiler, $expression);
})->compile($input);
}

Expand Down Expand Up @@ -184,10 +195,7 @@ public function compileAttribute(Attribute $attribute, bool $escapeBound = false
*/
public function compileAttributeEchos(string $input): string
{
$reflection = new \ReflectionClass($this->tagCompiler);
$method = $reflection->getMethod('compileAttributeEchos');

return Str::unwrap("'".$method->invoke($this->tagCompiler, $input)."'", "''.", ".''");
return Str::unwrap("'".$this->compileAttributeEchosMethod->invoke($this->tagCompiler, $input)."'", "''.", ".''");
}

/**
Expand Down Expand Up @@ -273,17 +281,11 @@ protected function hasClassBasedComponent(string $name): bool

protected function guessAnonymousComponentUsingNamespaces(Factory $viewFactory, string $component): string|null
{
$reflection = new \ReflectionClass($this->tagCompiler);
$method = $reflection->getMethod('guessAnonymousComponentUsingNamespaces');

return $method->invoke($this->tagCompiler, $viewFactory, $component);
return $this->guessAnonymousComponentUsingNamespacesMethod->invoke($this->tagCompiler, $viewFactory, $component);
}

protected function guessAnonymousComponentUsingPaths(Factory $viewFactory, string $component): string|null
{
$reflection = new \ReflectionClass($this->tagCompiler);
$method = $reflection->getMethod('guessAnonymousComponentUsingPaths');

return $method->invoke($this->tagCompiler, $viewFactory, $component);
return $this->guessAnonymousComponentUsingPathsMethod->invoke($this->tagCompiler, $viewFactory, $component);
}
}
7 changes: 5 additions & 2 deletions src/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class Config

protected $compile = [];

protected array $realpathCache = [];

/**
* Alias for add(), used as Blaze::optimize()->in(...).
*/
Expand Down Expand Up @@ -62,7 +64,7 @@ public function shouldFold(string $file): bool
*/
protected function isEnabled(string $file, array $config): bool
{
$file = realpath($file);
$file = $this->realpathCache[$file] ??= realpath($file);

if ($file === false) {
return false;
Expand All @@ -73,7 +75,7 @@ protected function isEnabled(string $file, array $config): bool
$separator = DIRECTORY_SEPARATOR;

foreach ($paths as $path) {
$resolved = realpath($path);
$resolved = $this->realpathCache[$path] ??= realpath($path);

if ($resolved === false) {
continue;
Expand Down Expand Up @@ -117,6 +119,7 @@ public function clear(): self
$this->compile = [];
$this->memo = [];
$this->fold = [];
$this->realpathCache = [];

return $this;
}
Expand Down
13 changes: 10 additions & 3 deletions src/Support/Directives.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,16 @@ class Directives
public function __construct(string $content)
{
$this->content = $content;
$this->content = preg_replace(LaravelRegex::BLADE_COMMENT, '', $this->content);
$this->content = preg_replace(LaravelRegex::VERBATIM_BLOCK, '', $this->content);
$this->content = preg_replace(LaravelRegex::PHP_BLOCK, '', $this->content);

if (str_contains($content, '{{--')) {
$this->content = preg_replace(LaravelRegex::BLADE_COMMENT, '', $this->content);
}
if (str_contains($this->content, '@verbatim')) {
$this->content = preg_replace(LaravelRegex::VERBATIM_BLOCK, '', $this->content);
}
if (str_contains($this->content, '@php')) {
$this->content = preg_replace(LaravelRegex::PHP_BLOCK, '', $this->content);
}

$this->parsed = $this->parseKnownDirectives();
}
Expand Down
86 changes: 86 additions & 0 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\BlazeManager;

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,86 @@ protected function runBenchmark(): array
];
}

/**
* Run a compilation benchmark measuring BlazeManager::compile() throughput.
*
* Reports the same value for both blade_ms and blaze_ms so the existing
* snapshot format works for tracking compilation time over time.
*/
protected function runCompilationBenchmark(): array
{
$showProgress = ! $this->option('ci') && ! $this->option('json');

$templates = [
'<x-button type="submit" class="mt-4" />',
'<x-card class="mt-8"><x-slot:header>Title</x-slot:header>Body content</x-card>',
'<x-button type="submit" /><x-input type="text" :value="$name" /><x-button variant="secondary" />',
'<x-card><x-button type="submit" class="mt-4" /><x-slot:footer><x-button variant="link" /></x-slot:footer></x-card>',
];

$manager = app(BlazeManager::class);

if ($showProgress) {
$this->info("Running 'compilation' ({$this->iterations} iterations x {$this->rounds} rounds, ".count($templates).' templates)...');
$this->newLine();
}

$totalSteps = $this->warmupRounds + $this->rounds;
$bar = $showProgress ? $this->output->createProgressBar($totalSteps) : null;
$bar?->setFormat('[%bar%] %message%');
$bar?->setMessage('Warming up...');
$bar?->start();

for ($w = 0; $w < $this->warmupRounds; $w++) {
$this->measureCompilation($templates, $manager);
$bar?->advance();
}

$times = [];

$bar?->setMessage('Benchmarking...');

for ($r = 0; $r < $this->rounds; $r++) {
$times[] = $this->measureCompilation($templates, $manager);
$bar?->advance();
}

$bar?->setMessage('Done!');
$bar?->finish();

if ($showProgress) {
$this->newLine(2);
}

$keptRounds = $this->nonOutlierIndices(collect($times));
$this->filteredRounds = $this->rounds - $keptRounds->count();

$times = $keptRounds->map(fn ($r) => $times[$r])->all();
$median = round(collect($times)->median(), 2);

return [
'blade_ms' => $median,
'blaze_ms' => $median,
];
}

/**
* Measure one round of Blaze compilation.
*/
protected function measureCompilation(array $templates, BlazeManager $manager): float
{
gc_collect_cycles();

$start = hrtime(true);
for ($i = 0; $i < $this->iterations; $i++) {
foreach ($templates as $template) {
$manager->compile($template);
}
}

return (hrtime(true) - $start) / 1_000_000;
}

protected function runMultipleAttempts(int $attempts): int
{
$benchmarkName = $this->argument('benchmark');
Expand Down
Loading