Skip to content

Commit 8a96b57

Browse files
authored
Add Octane support (#113)
* Flush state * Fix debug mode * Fix performance regression
1 parent d86c32e commit 8a96b57

File tree

8 files changed

+200
-110
lines changed

8 files changed

+200
-110
lines changed

src/BlazeManager.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use Livewire\Blaze\Compiler\Wrapper;
99
use Livewire\Blaze\Compiler\Compiler;
1010
use Livewire\Blaze\Compiler\Profiler;
11+
use Livewire\Blaze\Memoizer\Memo;
12+
use Livewire\Blaze\Runtime\BlazeRuntime;
1113
use Livewire\Blaze\Directive\BlazeDirective;
1214
use Livewire\Blaze\Events\ComponentFolded;
1315
use Livewire\Blaze\Folder\Folder;
@@ -19,7 +21,6 @@
1921
use Livewire\Blaze\Support\Directives;
2022
use Livewire\Blaze\Support\ComponentSource;
2123
use Livewire\Blaze\Parser\Nodes\SlotNode;
22-
use Livewire\Blaze\Runtime\BlazeRuntime;
2324

2425
class BlazeManager
2526
{
@@ -369,7 +370,7 @@ public function stopFolding(): void
369370
*/
370371
public function isEnabled()
371372
{
372-
return $this->enabled ?? config('blaze.enabled', true);
373+
return $this->enabled ??= config('blaze.enabled', true);
373374
}
374375

375376
/**
@@ -393,7 +394,7 @@ public function shouldThrow()
393394
*/
394395
public function isDebugging()
395396
{
396-
return $this->debug ?? config('blaze.debug', false);
397+
return $this->debug ??= config('blaze.debug', false);
397398
}
398399

399400
/**
@@ -441,4 +442,13 @@ protected function hasAwareDescendant(ComponentNode|SlotNode $node): bool
441442

442443
return false;
443444
}
445+
446+
/**
447+
* Reset all per-request mutable state.
448+
*/
449+
public function flushState(): void
450+
{
451+
$this->foldedEvents = [];
452+
$this->expiredMemo = [];
453+
}
444454
}

src/BlazeServiceProvider.php

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
use Livewire\Blaze\Runtime\BlazeRuntime;
77
use Illuminate\Support\ServiceProvider;
88
use Illuminate\Support\Facades\Blade;
9+
use Illuminate\Support\Facades\Event;
910
use Illuminate\Support\Facades\View;
11+
use Livewire\Blaze\Memoizer\Memo;
1012

1113
class BlazeServiceProvider extends ServiceProvider
1214
{
@@ -45,6 +47,7 @@ public function boot(): void
4547
$this->registerBladeMacros();
4648
$this->interceptBladeCompilation();
4749
$this->registerDebuggerMiddleware();
50+
$this->registerOctaneListener();
4851
}
4952

5053
/**
@@ -54,8 +57,9 @@ protected function registerViewComposer(): void
5457
{
5558
$blaze = $this->app->make(BlazeManager::class);
5659
$runtime = $this->app->make(BlazeRuntime::class);
60+
$debugger = $this->app->make(Debugger::class);
5761

58-
View::composer('*', function (\Illuminate\View\View $view) use ($blaze, $runtime) {
62+
View::composer('*', function (\Illuminate\View\View $view) use ($blaze, $runtime, $debugger) {
5963
if ($blaze->isDisabled() && ! $blaze->isDebugging()) {
6064
return;
6165
}
@@ -68,6 +72,20 @@ protected function registerViewComposer(): void
6872
$view->getEngine()->getCompiler()->compile($view->getPath());
6973
}
7074

75+
if ($blaze->isDebugging()) {
76+
$debugger->injectRenderTimer($view);
77+
78+
if ($blaze->isDisabled()) {
79+
$name = $view->name();
80+
81+
if (str_contains($name, '::')) {
82+
$name = substr($name, strpos($name, '::') + 2);
83+
}
84+
85+
$debugger->incrementBladeComponents($name);
86+
}
87+
}
88+
7189
$view->with('__blaze', $runtime);
7290
});
7391
}
@@ -148,4 +166,27 @@ protected function registerDebuggerMiddleware(): void
148166
}
149167
});
150168
}
169+
170+
/**
171+
* Reset Blaze state between Octane requests.
172+
*/
173+
protected function registerOctaneListener(): void
174+
{
175+
Event::listen(\Laravel\Octane\Events\RequestReceived::class, function ($event) {
176+
$app = $event->sandbox;
177+
178+
$runtime = $app->make(BlazeRuntime::class);
179+
$manager = $app->make(BlazeManager::class);
180+
$debugger = $app->make(Debugger::class);
181+
182+
$runtime->setApplication($app);
183+
184+
$runtime->flushState();
185+
$manager->flushState();
186+
$debugger->flushState();
187+
188+
Unblaze::flushState();
189+
Memo::flushState();
190+
});
191+
}
151192
}

src/Compiler/Profiler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public function profile(Node $node, string $componentName, ?string $strategy = n
3535

3636
if ($strategy === null) {
3737
$isBlade = $node instanceof ComponentNode;
38-
$strategy = $isBlade ? 'blade' : $this->resolveStrategy($source);
38+
$strategy = $isBlade ? 'blade' : 'compiled';
3939
}
4040

4141
$file = $source->exists() ? $this->relativePath($source->path) : null;

src/Debugger.php

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Livewire\Blaze;
44

5+
use Illuminate\Support\Facades\File;
6+
57
class Debugger
68
{
79
protected ?int $renderStart = null;
@@ -22,13 +24,20 @@ class Debugger
2224

2325
protected bool $isColdRender = false;
2426

27+
protected bool $timerInjected = false;
28+
2529
// ── Profiler trace ───────────────────────────
2630
protected array $traceStack = [];
2731
protected array $traceEntries = [];
2832
protected ?float $traceOrigin = null;
2933
protected int $memoHits = 0;
3034
protected array $memoHitNames = [];
3135

36+
public function __construct(
37+
protected BladeService $blade,
38+
) {
39+
}
40+
3241
/**
3342
* Extract a human-readable component name from its file path.
3443
*
@@ -188,6 +197,80 @@ public function setTimerView(string $name): void
188197
$this->timerView = $name;
189198
}
190199

200+
/**
201+
* Inject start/stop timer calls into the compiled file of the first
202+
* view being rendered. This ensures we measure only view rendering
203+
* time, not the full request lifecycle.
204+
*
205+
* Called from the view composer on each composing view; only the
206+
* first successful injection per request takes effect.
207+
*/
208+
public function injectRenderTimer(\Illuminate\View\View $view): void
209+
{
210+
if ($this->timerInjected) {
211+
return;
212+
}
213+
214+
$path = $view->getPath();
215+
216+
// Some views (e.g. Livewire virtual views) may not have a real path.
217+
if (! $path || ! file_exists($path)) {
218+
return;
219+
}
220+
221+
// Ensure the view is compiled.
222+
if ($this->blade->compiler->isExpired($path)) {
223+
$this->blade->compiler->compile($path);
224+
}
225+
226+
$compiledPath = $this->blade->compiler->getCompiledPath($path);
227+
228+
if (! file_exists($compiledPath)) {
229+
return;
230+
}
231+
232+
$compiled = file_get_contents($compiledPath);
233+
234+
// Record which view was wrapped with the render timer.
235+
$this->setTimerView($this->resolveTimerViewName($view));
236+
237+
$this->timerInjected = true;
238+
239+
// Already injected (persisted from a previous request).
240+
if (str_contains($compiled, '__blaze_timer')) {
241+
return;
242+
}
243+
244+
$start = '<?php $__blaze->debugger->startRenderTimer(); /* __blaze_timer */ ?>';
245+
$stop = '<?php $__blaze->debugger->stopRenderTimer(); ?>';
246+
247+
File::replace($compiledPath, $start . $compiled . $stop);
248+
}
249+
250+
/**
251+
* Resolve a human-readable name for the view being timed.
252+
*
253+
* For Livewire SFCs the view path points to an extracted blade file
254+
* (e.g. storage/.../livewire/views/6ea59dbe.blade.php) which isn't
255+
* meaningful. In that case we pull the component name from Livewire's
256+
* shared view data instead.
257+
*/
258+
protected function resolveTimerViewName(\Illuminate\View\View $view): string
259+
{
260+
$path = $view->getPath();
261+
262+
// Livewire SFC extracted views live inside a "livewire/views" cache directory.
263+
if ($path && str_contains($path, '/livewire/views/')) {
264+
$livewire = app('view')->shared('__livewire');
265+
266+
if ($livewire && method_exists($livewire, 'getName')) {
267+
return $livewire->getName();
268+
}
269+
}
270+
271+
return $view->name();
272+
}
273+
191274
public function startRenderTimer(): void
192275
{
193276
$this->renderStart = hrtime(true);
@@ -296,6 +379,25 @@ protected function getData(): array
296379
'timerView' => $this->timerView,
297380
];
298381
}
382+
383+
public function flushState(): void
384+
{
385+
$this->renderStart = null;
386+
$this->renderTime = 0.0;
387+
$this->timerView = null;
388+
$this->components = [];
389+
$this->bladeComponentCount = 0;
390+
$this->bladeComponents = [];
391+
$this->blazeEnabled = false;
392+
$this->comparison = null;
393+
$this->isColdRender = false;
394+
$this->timerInjected = false;
395+
$this->traceStack = [];
396+
$this->traceEntries = [];
397+
$this->traceOrigin = null;
398+
$this->memoHits = 0;
399+
$this->memoHitNames = [];
400+
}
299401

300402
protected function formatMs(float $value): string
301403
{

src/DebuggerMiddleware.php

Lines changed: 1 addition & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
use Illuminate\Contracts\Http\Kernel;
77
use Illuminate\Http\Request;
88
use Illuminate\Support\Facades\Cache;
9-
use Illuminate\Support\Facades\Event;
109
use Illuminate\Support\Facades\Route;
1110
use Symfony\Component\HttpFoundation\Response;
1211

@@ -43,31 +42,9 @@ public function handle(Request $request, Closure $next): Response
4342

4443
$isBlaze = app('blaze')->isEnabled();
4544

46-
$debugger = app('blaze.runtime')->debugger;
45+
$debugger = app('blaze.debugger');
4746
$debugger->setBlazeEnabled($isBlaze);
4847

49-
// Inject render timer into the first view's compiled file so we
50-
// measure only actual view rendering, not the full request pipeline.
51-
// The composing event fires before the compiled file is included,
52-
// so modifying it here is safe.
53-
$timerInjected = false;
54-
55-
Event::listen('composing:*', function (string $event, array $data) use ($debugger, $isBlaze, &$timerInjected) {
56-
if (! $timerInjected) {
57-
$timerInjected = $this->injectRenderTimer($data[0]);
58-
}
59-
60-
if (! $isBlaze) {
61-
$name = str_replace('composing: ', '', $event);
62-
63-
if (str_contains($name, '::')) {
64-
$name = substr($name, strpos($name, '::') + 2);
65-
}
66-
67-
$debugger->incrementBladeComponents($name);
68-
}
69-
});
70-
7148
$response = $next($request);
7249

7350
$this->recordAndCompare($url, $debugger, $isBlaze);
@@ -152,75 +129,6 @@ protected function storeProfilerTrace(string $url, Debugger $debugger, bool $isB
152129
], 300); // 5 minutes
153130
}
154131

155-
/**
156-
* Inject start/stop timer calls into the compiled file of the first
157-
* view being rendered. This ensures we measure only view rendering
158-
* time, not the full request lifecycle.
159-
*/
160-
protected function injectRenderTimer(\Illuminate\View\View $view): bool
161-
{
162-
$bladeService = app(BladeService::class);
163-
$compiler = $bladeService->compiler;
164-
$path = $view->getPath();
165-
166-
// Some views (e.g. Livewire virtual views) may not have a real path.
167-
if (! $path || ! file_exists($path)) {
168-
return false;
169-
}
170-
171-
// Ensure the view is compiled.
172-
if ($compiler->isExpired($path)) {
173-
$compiler->compile($path);
174-
}
175-
176-
$compiledPath = $compiler->getCompiledPath($path);
177-
178-
if (! file_exists($compiledPath)) {
179-
return false;
180-
}
181-
182-
$compiled = file_get_contents($compiledPath);
183-
184-
// Record which view was wrapped with the render timer.
185-
app('blaze.runtime')->debugger->setTimerView($this->resolveViewName($view));
186-
187-
// Already injected (persisted from a previous request).
188-
if (str_contains($compiled, '__blaze_timer')) {
189-
return true;
190-
}
191-
192-
$start = '<?php ($__blaze ?? app(\'blaze.runtime\'))->debugger->startRenderTimer(); /* __blaze_timer */ ?>';
193-
$stop = '<?php ($__blaze ?? app(\'blaze.runtime\'))->debugger->stopRenderTimer(); ?>';
194-
195-
file_put_contents($compiledPath, $start . $compiled . $stop);
196-
197-
return true;
198-
}
199-
200-
/**
201-
* Resolve a human-readable name for the view being timed.
202-
*
203-
* For Livewire SFCs the view path points to an extracted blade file
204-
* (e.g. storage/.../livewire/views/6ea59dbe.blade.php) which isn't
205-
* meaningful. In that case we pull the component name from Livewire's
206-
* shared view data instead.
207-
*/
208-
protected function resolveViewName(\Illuminate\View\View $view): string
209-
{
210-
$path = $view->getPath();
211-
212-
// Livewire SFC extracted views live inside a "livewire/views" cache directory.
213-
if ($path && str_contains($path, '/livewire/views/')) {
214-
$livewire = app('view')->shared('__livewire');
215-
216-
if ($livewire && method_exists($livewire, 'getName')) {
217-
return $livewire->getName();
218-
}
219-
}
220-
221-
return $view->name();
222-
}
223-
224132
/**
225133
* Inject the debug bar HTML after the opening <body> tag.
226134
*/

0 commit comments

Comments
 (0)