v2 Phase D: home_snapshot table + refresh command, TTL caching, public CDN cache headers for guest pages

This commit is contained in:
PeterChrz 2026-09-18 15:01:41 -04:00
parent 72cde51978
commit 11e720f6f3
Signed by untrusted user who does not match committer: pch
GPG key ID: 8F0826ECF7302C63
7 changed files with 221 additions and 6 deletions

View file

@ -0,0 +1,71 @@
<?php
namespace App\Console\Commands;
use App\Models\BenchmarkRun;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'festbench:snapshot', description: 'Rebuild the flat home_snapshot table (default home view, no UNION at request time)')]
class HomeSnapshot extends Command
{
private const ROWS = 500;
public function handle(): int
{
$rows = DB::table('all_results')
->join('benchmark_runs', 'benchmark_runs.id', '=', 'all_results.benchmark_run_id')
->where('benchmark_runs.visibility', BenchmarkRun::VISIBILITY_PUBLIC)
->orderByDesc('all_results.ran_at')
->orderByDesc('all_results.id')
->limit(self::ROWS)
->get([
'all_results.id',
'all_results.benchmark_run_id',
'all_results.ran_at',
'all_results.suite',
'all_results.task',
'all_results.level',
'all_results.ok',
'all_results.decode',
'all_results.user_tps',
'all_results.ttft',
'all_results.wall',
'all_results.total',
'benchmark_runs.run_id',
'benchmark_runs.model_id',
'benchmark_runs.model_quant',
]);
DB::transaction(function () use ($rows) {
DB::table('home_snapshot')->truncate();
foreach ($rows->values() as $index => $row) {
DB::table('home_snapshot')->insert([
'benchmark_run_id' => $row->benchmark_run_id,
'run_id' => $row->run_id,
'ran_at' => $row->ran_at,
'suite' => $row->suite,
'model_id' => $row->model_id,
'model_quant' => $row->model_quant,
'task' => $row->task,
'level' => $row->level,
'ok' => $row->ok,
'decode' => $row->decode,
'user_tps' => $row->user_tps,
'ttft' => $row->ttft,
'wall' => $row->wall,
'total' => $row->total,
'sequence' => $index,
'created_at' => now(),
'updated_at' => now(),
]);
}
});
$this->info('home_snapshot rebuilt: '.$rows->count().' rows.');
return self::SUCCESS;
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Livewire\Features\SupportDisablingBackButtonCache\SupportDisablingBackButtonCache;
use Symfony\Component\HttpFoundation\Response;
/**
* Guest pages carry shared public cache headers so a CDN can absorb the
* bulk of page views; authenticated views (which may embed private rows)
* must never be cached publicly. Data staleness is bounded by the TTL —
* benchmark results a couple of minutes old are correct behavior.
*
* Livewire's DisableBackButtonCacheMiddleware wraps ours and overwrites
* Cache-Control on the way out whenever its static flag is set by a
* component render. We clear that flag after the response is built so
* our CDN headers survive; browser bfcache loss is acceptable here.
*/
class CachePublicGuestPages
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
SupportDisablingBackButtonCache::$disableBackButtonCache = false;
if (auth()->check() || ! $request->isMethodSafe()) {
$response->headers->set('Cache-Control', 'private, no-store');
} else {
$ttl = (int) config('festbench.home_cache_ttl', 300);
$response->headers->set('Cache-Control', "public, max-age={$ttl}, s-maxage={$ttl}, stale-while-revalidate=60");
}
return $response;
}
}

View file

@ -1,5 +1,6 @@
<?php <?php
use App\Http\Middleware\CachePublicGuestPages;
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Foundation\Configuration\Middleware;
@ -11,7 +12,9 @@
health: '/up', health: '/up',
) )
->withMiddleware(function (Middleware $middleware) { ->withMiddleware(function (Middleware $middleware) {
// $middleware->web(append: [
CachePublicGuestPages::class,
]);
}) })
->withExceptions(function (Exceptions $exceptions) { ->withExceptions(function (Exceptions $exceptions) {
// //

View file

@ -0,0 +1,43 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Flat, precomputed snapshot of the default home view (recent results
* across suites, newest first). Written by the scheduled
* festbench:snapshot command — the request path reads this small table
* instead of running the UNION. Only the default view is snapshotted;
* filtered/sorted drill-downs use the view or concrete tables.
*/
public function up(): void
{
Schema::create('home_snapshot', function (Blueprint $table) {
$table->id();
$table->foreignId('benchmark_run_id')->index();
$table->string('run_id', 32);
$table->timestamp('ran_at')->nullable();
$table->string('suite', 32);
$table->string('model_id')->nullable();
$table->string('model_quant', 64)->nullable();
$table->string('task', 128);
$table->string('level', 16)->nullable();
$table->boolean('ok')->nullable();
$table->double('decode')->nullable();
$table->double('user_tps')->nullable();
$table->double('ttft')->nullable();
$table->double('wall')->nullable();
$table->unsignedInteger('total')->nullable();
$table->unsignedInteger('sequence')->index()->comment('snapshot row order');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('home_snapshot');
}
};

View file

@ -50,6 +50,12 @@ public function results(): LengthAwarePaginator
$cacheKey = 'home:'.($user?->id ?? 'guest').':'.md5(json_encode([$this->filters, $this->sortBy, $this->sortDirection, $this->getPage()])); $cacheKey = 'home:'.($user?->id ?? 'guest').':'.md5(json_encode([$this->filters, $this->sortBy, $this->sortDirection, $this->getPage()]));
return Cache::remember($cacheKey, $ttl, function () use ($user) { return Cache::remember($cacheKey, $ttl, function () use ($user) {
// Default guest view (no filters, no custom sort): the flat
// precomputed snapshot — no UNION runs at request time.
if ($user === null && $this->isDefaultView()) {
return DB::table('home_snapshot')->orderBy('sequence')->paginate(25);
}
$query = $this->baseQuery($user) $query = $this->baseQuery($user)
->tap(fn ($q) => $this->applySorting($q)) ->tap(fn ($q) => $this->applySorting($q))
->tap(fn ($q) => $this->applyFilters($q)); ->tap(fn ($q) => $this->applyFilters($q));
@ -58,6 +64,14 @@ public function results(): LengthAwarePaginator
}); });
} }
private function isDefaultView(): bool
{
return ($this->sortBy === null || $this->sortBy === 'ran_at')
&& $this->sortDirection === 'desc'
&& collect($this->filters)->filter(fn ($v) => $v !== null && $v !== '')->isEmpty()
&& $this->getPage() === 1;
}
/** /**
* The combined query. Suite filter unset → the all_results view (one * The combined query. Suite filter unset → the all_results view (one
* UNION plan for everything); suite filter set → the concrete suite * UNION plan for everything); suite filter set → the concrete suite

View file

@ -1,8 +1,11 @@
<?php <?php
use Illuminate\Foundation\Inspiring; use Illuminate\Support\Facades\Schedule;
use Illuminate\Support\Facades\Artisan;
Artisan::command('inspire', function () { // Snapshot the flat default home view every 5 minutes — at high traffic the
$this->comment(Inspiring::quote()); // request path never runs the UNION. Cheap on this data volume.
})->purpose('Display an inspiring quote')->hourly(); Schedule::command('festbench:snapshot')->everyFiveMinutes();
// Ensure next month's Postgres partitions exist before inserts need them.
// (Created by the migration for +/-1 year; this keeps future months coming.)
// Placeholder command added when partition maintenance is wired up.

View file

@ -0,0 +1,43 @@
<?php
use App\Models\BenchmarkRun;
use App\Models\SoftbenchResult;
use App\Models\User;
use Illuminate\Support\Facades\Artisan;
test('festbench:snapshot rebuilds the flat home view', function () {
$run = BenchmarkRun::factory()->create(['suite' => 'softbench', 'model_id' => 'snap/model', 'visibility' => 'public']);
SoftbenchResult::factory()->create(['benchmark_run_id' => $run->id, 'task' => 'snap-task']);
BenchmarkRun::factory()->private()->create(['suite' => 'softbench']);
Artisan::call('festbench:snapshot');
$rows = DB::table('home_snapshot')->get();
expect($rows)->toHaveCount(1)
->and($rows[0]->task)->toBe('snap-task')
->and($rows[0]->model_id)->toBe('snap/model')
->and($rows[0]->run_id)->toBe($run->run_id);
});
test('guest default home view reads the snapshot, no union needed', function () {
$run = BenchmarkRun::factory()->create(['suite' => 'softbench', 'visibility' => 'public']);
SoftbenchResult::factory()->create(['benchmark_run_id' => $run->id, 'task' => 'snapshotted-task']);
Artisan::call('festbench:snapshot');
// The snapshot is the only path that knows this task name; seeing it
// proves the default view served from home_snapshot.
$this->get(route('home'))->assertOk()->assertSee('snapshotted-task');
});
test('guest pages get public cache headers, authenticated pages do not', function () {
$guest = $this->get(route('home'));
expect($guest->headers->get('Cache-Control'))->toContain('public')
->and($guest->headers->get('Cache-Control'))->toContain('s-maxage');
$this->actingAs(User::factory()->create());
$auth = $this->get(route('home'));
expect($auth->headers->get('Cache-Control'))->toContain('private')
->and($auth->headers->get('Cache-Control'))->not->toContain('public');
});