253 lines
No EOL
12 KiB
PHP
253 lines
No EOL
12 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">{{ __('Benchmark Results') }}</flux:heading>
|
|
<flux:subheading>{{ __('Every datapoint festbench captures — sortable.') }}</flux:subheading>
|
|
</div>
|
|
</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')" placeholder="{{ __('All suites') }}" size="sm" class="w-44">
|
|
@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')" placeholder="{{ __('All levels') }}" size="sm" class="w-36">
|
|
@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')" placeholder="{{ __('All models') }}" size="sm" class="w-56">
|
|
@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')" placeholder="{{ __('Any') }}" size="sm" class="w-32">
|
|
<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-right">{{ __('User tok/s') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'decode'" :direction="$sortDirection" wire:click="sort('decode')" class="text-right">{{ __('Decode') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'ttft'" :direction="$sortDirection" wire:click="sort('ttft')" class="text-right">{{ __('TTFT') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'wall'" :direction="$sortDirection" wire:click="sort('wall')" class="text-right">{{ __('Wall') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'total'" :direction="$sortDirection" wire:click="sort('total')" class="text-right">{{ __('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-right font-mono text-sm">{{ number_format($result->user_tps, 1) }}</flux:table.cell>
|
|
<flux:table.cell class="text-right font-mono text-sm">{{ number_format($result->decode, 1) }}</flux:table.cell>
|
|
<flux:table.cell class="text-right font-mono text-sm">{{ number_format($result->ttft, 2) }}</flux:table.cell>
|
|
<flux:table.cell class="text-right font-mono text-sm">{{ number_format($result->wall, 2) }}</flux:table.cell>
|
|
<flux:table.cell class="text-right 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>
|