festbench-web/resources/views/livewire/results/index.blade.php

258 lines
No EOL
13 KiB
PHP

<?php
use App\Livewire\IncludesSorting;
use App\Models\BenchmarkRun;
use App\Support\Suites;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Computed;
use Livewire\Component;
use Livewire\WithPagination;
new class extends Component {
use IncludesSorting;
use WithPagination;
public array $filters = [
'search' => null,
'suite' => null,
'level' => null,
'model' => null,
'result' => null,
];
private const SORTABLE = [
'ran_at', 'model_id', 'suite', 'task', 'level', 'ok',
'decode', 'user_tps', 'ttft', 't_answer', 'wall', 'total',
];
public function resetFilters(): void
{
$this->filters = [
'search' => null,
'suite' => null,
'level' => null,
'model' => null,
'result' => null,
];
$this->resetPage();
}
#[Computed]
public function results(): LengthAwarePaginator
{
$ttl = (int) config('festbench.home_cache_ttl', 300);
$user = auth()->user();
// Guest + public-only views are cacheable; any logged-in view may
// embed private rows, so it gets a user-scoped key instead.
$cacheKey = 'home:'.($user?->id ?? 'guest').':'.md5(json_encode([$this->filters, $this->sortBy, $this->sortDirection, $this->getPage()]));
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)
->tap(fn ($q) => $this->applySorting($q))
->tap(fn ($q) => $this->applyFilters($q));
return $query->paginate(25);
});
}
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
* UNION plan for everything); suite filter set → the concrete suite
* table directly (full indexes, no union).
*/
private function baseQuery($user)
{
$suite = $this->filters['suite'];
$model = $suite !== null && $suite !== '' ? Suites::modelFor($suite) : null;
$source = $model !== null
? $model::query()->getModel()->getTable()
: 'all_results';
// The concrete suite tables have no suite column; promote it so the
// view renders identically for both query paths.
$select = $model !== null
? "{$source}.*, '{$suite}' AS suite"
: "{$source}.*";
return DB::table($source)
->join('benchmark_runs', 'benchmark_runs.id', '=', "{$source}.benchmark_run_id")
->selectRaw($select)
->addSelect(
'benchmark_runs.model_id',
'benchmark_runs.model_quant',
'benchmark_runs.engine',
'benchmark_runs.run_id',
'benchmark_runs.visibility',
'benchmark_runs.user_id as run_user_id')
->where(BenchmarkRun::visibleClause($user));
}
#[Computed]
public function suites(): array
{
return Cache::remember('home:suites', 300, fn () => Suites::active()->all());
}
#[Computed]
public function models(): array
{
return Cache::remember('home:models', 300, function () {
return BenchmarkRun::visibleTo(auth()->user())
->distinct()->orderBy('model_id')->pluck('model_id')->all();
});
}
private function applySorting($query)
{
$column = in_array($this->sortBy, self::SORTABLE, true) ? $this->sortBy : 'ran_at';
$source = $this->filters['suite'] !== null && $this->filters['suite'] !== '' ? Suites::modelFor($this->filters['suite'])::make()->getTable() : 'all_results';
return $query->orderBy($column, $this->sortDirection === 'asc' ? 'asc' : 'desc')
->orderBy("{$source}.id", 'desc');
}
private function applyFilters($query)
{
$source = $this->filters['suite'] !== null && $this->filters['suite'] !== '' ? Suites::modelFor($this->filters['suite'])::make()->getTable() : 'all_results';
return $query
->when($this->filters['search'], fn ($q, string $search) => $q->where(function ($q) use ($search, $source) {
$q->where("{$source}.task", 'like', "%{$search}%")
->orWhere('benchmark_runs.model_id', 'like', "%{$search}%")
->orWhere('benchmark_runs.label', 'like', "%{$search}%");
}))
->when($this->filters['level'], fn ($q, string $level) => $q->where("{$source}.level", $level))
->when($this->filters['model'], fn ($q, string $model) => $q->where('benchmark_runs.model_id', $model))
->when($this->filters['result'] !== null && $this->filters['result'] !== '', fn ($q) => $q->where("{$source}.ok", (bool) $this->filters['result']));
}
};
?>
<div>
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl">{{ __('All Task Rows') }}</flux:heading>
<flux:subheading>{{ __('Every datapoint festbench captures — sortable.') }}</flux:subheading>
</div>
<flux:button variant="ghost" icon="arrow-left" href="{{ route('home') }}" wire:navigate>{{ __('Session summary') }}</flux:button>
</div>
<div class="mt-6 flex flex-wrap items-end gap-3">
<flux:input
wire:model.live.debounce.300ms="filters.search"
:label="__('Search')"
:placeholder="__('Task, model or label…')"
size="sm"
class="w-64"
clearable
/>
<flux:select wire:model.live="filters.suite" :label="__('Suite')" size="sm" class="w-44">
<flux:select.option value="">{{ __('All suites') }}</flux:select.option>
@foreach ($this->suites as $suite)
<flux:select.option value="{{ $suite }}">{{ $suite }}</flux:select.option>
@endforeach
</flux:select>
<flux:select wire:model.live="filters.level" :label="__('Level')" size="sm" class="w-36">
<flux:select.option value="">{{ __('All levels') }}</flux:select.option>
@foreach (['off', 'low', 'medium', 'xhigh'] as $level)
<flux:select.option value="{{ $level }}">{{ $level }}</flux:select.option>
@endforeach
</flux:select>
<flux:select wire:model.live="filters.model" :label="__('Model')" size="sm" class="w-56">
<flux:select.option value="">{{ __('All models') }}</flux:select.option>
@foreach ($this->models as $model)
<flux:select.option value="{{ $model }}">{{ $model }}</flux:select.option>
@endforeach
</flux:select>
<flux:select wire:model.live="filters.result" :label="__('Result')" size="sm" class="w-32">
<flux:select.option value="">{{ __('Any') }}</flux:select.option>
<flux:select.option value="1">{{ __('Pass') }}</flux:select.option>
<flux:select.option value="0">{{ __('Fail') }}</flux:select.option>
</flux:select>
<flux:button size="sm" variant="ghost" wire:click="resetFilters" icon="x-mark">{{ __('Clear') }}</flux:button>
</div>
@if ($this->results->count() > 0)
<div class="mt-6">
<flux:table :paginate="$this->results">
<flux:table.columns>
<flux:table.column sortable :sorted="$sortBy === 'ran_at'" :direction="$sortDirection" wire:click="sort('ran_at')">{{ __('Date') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'model_id'" :direction="$sortDirection" wire:click="sort('model_id')">{{ __('Model') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'suite'" :direction="$sortDirection" wire:click="sort('suite')">{{ __('Suite') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'task'" :direction="$sortDirection" wire:click="sort('task')">{{ __('Task') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'level'" :direction="$sortDirection" wire:click="sort('level')">{{ __('Level') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'ok'" :direction="$sortDirection" wire:click="sort('ok')">{{ __('Pass') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'user_tps'" :direction="$sortDirection" wire:click="sort('user_tps')" class="text-center">{{ __('User tok/s') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'decode'" :direction="$sortDirection" wire:click="sort('decode')" class="text-center">{{ __('Decode') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'ttft'" :direction="$sortDirection" wire:click="sort('ttft')" class="text-center">{{ __('TTFT') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'wall'" :direction="$sortDirection" wire:click="sort('wall')" class="text-center">{{ __('Wall') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'total'" :direction="$sortDirection" wire:click="sort('total')" class="text-center">{{ __('Tokens') }}</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@foreach ($this->results as $result)
<flux:table.row :key="$result->suite.'-'.$result->id">
<flux:table.cell>{{ optional($result->ran_at)->format('Y-m-d') }}</flux:table.cell>
<flux:table.cell>
<a href="{{ route('runs.show', $result->run_id) }}" class="flex items-center hover:underline" wire:navigate>
<span class="truncate">{{ $result->model_id }}</span>
@if ($result->model_quant)
<flux:badge size="xs" class="ml-1" color="zinc">{{ $result->model_quant }}</flux:badge>
@endif
</a>
</flux:table.cell>
<flux:table.cell><flux:badge size="xs">{{ $result->suite }}</flux:badge></flux:table.cell>
<flux:table.cell>{{ $result->task }}</flux:table.cell>
<flux:table.cell>{{ $result->level ?? '—' }}</flux:table.cell>
<flux:table.cell>
@if ($result->ok)
<flux:icon name="check-circle" variant="solid" class="size-4 text-emerald-500" />
@elseif ($result->ok === null)
<span class="text-zinc-400">—</span>
@else
<flux:icon name="x-circle" variant="solid" class="size-4 text-red-500" />
@endif
</flux:table.cell>
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->user_tps, 1) }}</flux:table.cell>
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->decode, 1) }}</flux:table.cell>
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->ttft, 2) }}</flux:table.cell>
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->wall, 2) }}</flux:table.cell>
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->total) }}</flux:table.cell>
</flux:table.row>
@endforeach
</flux:table.rows>
</flux:table>
</div>
@else
<div class="mt-6">
<flux:callout variant="brand">
<flux:callout.heading>{{ __('No results') }}</flux:callout.heading>
<flux:callout.text>{{ __('No benchmark results match the current filters.') }}</flux:callout.text>
</flux:callout>
</div>
@endif
</div>