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 .claude/skills/rfa-debug/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ When copying the SQLite file while the app is running, copy `database.sqlite`, `

## Cold-launch timeline

Every launch writes one line to `<storage>/logs/rfa-launch.jsonl` from the Electron main process (marks in ms since process creation: PHP spawn, listening, warm, splash, booted handshake, window open, load, renderer-ready, presented). The PHP side stamps its breadcrumbs in `rfa-diagnostics.jsonl` with the request start (a `request` object holding `started_at_ms` and `elapsed_ms`), and the renderer posts a `launch` browser sample with navigation timing and the settle sub-marks. The file rotates at 1MB.
Every launch writes one line to `<storage>/logs/rfa-launch.jsonl` from the Electron main process (marks in ms since process creation: PHP spawn, listening, warm, splash, booted handshake, window open, load, renderer-ready, presented, and after a version change the background cache rebuild as `php.optimize.started`/`finished`, which the flush waits for). The PHP side stamps its breadcrumbs in `rfa-diagnostics.jsonl` with the request start (a `request` object holding `started_at_ms` and `elapsed_ms`), and the renderer posts a `launch` browser sample with navigation timing and the settle sub-marks. The file rotates at 1MB.

Read the three as one timeline (defaults to the installed app's log directory, medians over the last launches):

Expand Down
84 changes: 84 additions & 0 deletions app/Actions/CompileViewsAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

declare(strict_types=1);

namespace App\Actions;

use Illuminate\Support\Collection;
use Illuminate\View\Compilers\BladeCompiler;
use Illuminate\View\Factory;
use Illuminate\View\FileViewFinder;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;

/**
* Compiles every Blade template into the compiled-views directory the
* running server reads from, without clearing it first.
*
* `view:cache` empties the directory before compiling, which a live
* request can trip over. Blade itself writes each compiled file through
* an atomic rename and skips templates whose output is unchanged, so
* compiling in place is safe next to a serving PHP process.
*/
final readonly class CompileViewsAction
{
public function __construct(
private Factory $views,
private BladeCompiler $compiler,
) {}

/** @return array{paths: int, compiled: int} */
public function handle(): array
{
$paths = $this->paths();

$compiled = $paths
->flatMap(fn (string $path): Collection => $this->bladeFilesIn($path))
->each(fn (SplFileInfo $file) => $this->compiler->compile($file->getRealPath()))
->count();

return ['paths' => $paths->count(), 'compiled' => $compiled];
}

/**
* The view roots and namespace hints, with roots nested inside another
* root dropped so no template is compiled twice.
*
* @return Collection<int, string>
*/
private function paths(): Collection
{
$finder = $this->views->getFinder();

if (! $finder instanceof FileViewFinder) {
return collect();
}

$paths = collect($finder->getPaths())
->merge(collect($finder->getHints())->flatten())
->filter(fn (mixed $path): bool => is_string($path))
->unique()
->values();

$directory = fn (string $path): string => rtrim(realpath($path) ?: $path, '/').'/';

return $paths
->reject(fn (string $path): bool => $paths->contains(
fn (string $existing): bool => $existing !== $path && str_starts_with($directory($path), $directory($existing)),
))
->values();
}

/** @return Collection<int, SplFileInfo> */
private function bladeFilesIn(string $path): Collection
{
if (! is_dir($path)) {
return collect();
}

return collect(iterator_to_array(
Finder::create()->in($path)->exclude('vendor')->name('*.blade.php')->files(),
false,
));
}
}
70 changes: 70 additions & 0 deletions app/Console/Commands/OptimizeCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

namespace App\Console\Commands;

use App\Actions\CompileViewsAction;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Context;
use Illuminate\Support\Facades\Log;
use Throwable;

/**
* Rebuilds the framework caches while the app is already serving.
*
* The Electron main process runs this in the background after a version
* change, with the config, route, and event cache paths pointed at a
* staging directory it renames into place afterwards. Views are compiled
* straight into the live directory without clearing it, which is the
* difference from `optimize`: `view:cache` empties the directory the
* running server reads from.
*/
class OptimizeCommand extends Command
{
protected $signature = 'rfa:optimize';

protected $description = 'Cache config, events, and routes, then compile every Blade view without clearing the compiled views';

public function handle(CompileViewsAction $compileViews): int
{
Context::flush();

$startedAt = microtime(true);
$outcome = 'completed';
$status = self::FAILURE;

try {
$status = $this->call('optimize', ['--except' => 'views']);

if ($status !== self::SUCCESS) {
$outcome = 'error';
Context::add('rfa.reason', 'optimize_failed');

return $status;
}

$views = $compileViews->handle();

Context::add('rfa.view_path_count', $views['paths']);
Context::add('rfa.view_count', $views['compiled']);

$this->components->info(sprintf('Compiled %d Blade templates from %d view roots.', $views['compiled'], $views['paths']));

return self::SUCCESS;
} catch (Throwable $e) {
$outcome = 'error';
$status = self::FAILURE;
Context::add('rfa.error_class', $e::class);
Context::add('rfa.reason', 'view_compile_failed');

throw $e;
} finally {
Context::add('rfa.optimize_status', $status);
Context::add('rfa.outcome', $outcome);
Context::add('rfa.duration_ms', (int) round((microtime(true) - $startedAt) * 1000));

Log::info('framework.optimized');
}
}
}
1 change: 1 addition & 0 deletions app/Services/LaunchTimelineService.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ final class LaunchTimelineService
public const PHASES = [
'electron: process -> bootstrap' => [null, 'bootstrap'],
'electron: bootstrap -> app ready' => ['bootstrap', 'app.ready'],
'php: optimize started -> finished (background)' => ['php.optimize.started', 'php.optimize.finished'],
'php: spawn -> listening' => ['php.spawning', 'php.listening'],
'php: listening -> warm request' => ['php.listening', 'php.warm.request'],
'php: warm request -> warmed' => ['php.warm.request', 'php.warmed'],
Expand Down
Loading