205 lines
No EOL
10 KiB
PHP
205 lines
No EOL
10 KiB
PHP
<?php
|
||
|
||
use App\Models\BenchmarkRun;
|
||
use Illuminate\Support\Facades\Cache;
|
||
use Livewire\Attributes\Computed;
|
||
use Livewire\Component;
|
||
use Livewire\WithPagination;
|
||
|
||
new class extends Component {
|
||
use WithPagination;
|
||
|
||
public array $filters = [
|
||
'search' => null,
|
||
'model' => null,
|
||
'suite' => null,
|
||
];
|
||
|
||
public function resetFilters(): void
|
||
{
|
||
$this->filters = ['search' => null, 'model' => null, 'suite' => null];
|
||
$this->resetPage();
|
||
}
|
||
|
||
#[Computed]
|
||
public function sessions()
|
||
{
|
||
$ttl = (int) config('festbench.home_cache_ttl', 300);
|
||
$user = auth()->user();
|
||
// Guest views are shared-cacheable; logged-in views may contain the
|
||
// user's private bands, so they get a user-scoped key.
|
||
$cacheKey = 'summary:'.($user?->id ?? 'guest').':'.md5(json_encode([$this->filters, $this->getPage()]));
|
||
|
||
return Cache::remember($cacheKey, $ttl, function () use ($user) {
|
||
$bands = $this->bandQuery($user)->paginate(10);
|
||
|
||
$runIds = $bands->getCollection()->map(fn ($b) => $b->id)->all();
|
||
$suiteRows = $runIds === []
|
||
? collect()
|
||
: \Illuminate\Support\Facades\DB::table('session_suite_rows')
|
||
->whereIn('session_summary_id', $runIds)
|
||
->orderBy('sequence')
|
||
->get()
|
||
->groupBy('session_summary_id');
|
||
|
||
$bands->getCollection()->transform(fn ($band) => (object) [
|
||
'band' => $band,
|
||
'suites' => $suiteRows->get($band->id, collect()),
|
||
]);
|
||
|
||
return $bands;
|
||
});
|
||
}
|
||
|
||
private function bandQuery($user)
|
||
{
|
||
return \Illuminate\Support\Facades\DB::table('session_summaries')
|
||
->where(function ($q) use ($user) {
|
||
$q->where('visibility', 'public');
|
||
if ($user !== null) {
|
||
$q->orWhere(function ($own) use ($user) {
|
||
$own->where('visibility', 'private')->where('user_id', $user->id);
|
||
});
|
||
}
|
||
})
|
||
->when($this->filters['search'], fn ($q, $search) => $q->where(function ($q) use ($search) {
|
||
$q->where('label', 'like', "%{$search}%")
|
||
->orWhere('model_id', 'like', "%{$search}%")
|
||
->orWhere('gpu', 'like', "%{$search}%")
|
||
->orWhere('runner_name', 'like', "%{$search}%");
|
||
}))
|
||
->when($this->filters['model'], fn ($q, $model) => $q->where('model_id', $model))
|
||
// Suite filter: keep bands containing the chosen suite
|
||
->when($this->filters['suite'], fn ($q, $suite) => $q->whereExists(function ($sub) use ($suite) {
|
||
$sub->select(\Illuminate\Support\Facades\DB::raw(1))
|
||
->from('session_suite_rows')
|
||
->whereColumn('session_suite_rows.session_summary_id', 'session_summaries.id')
|
||
->where('session_suite_rows.suite', $suite);
|
||
}))
|
||
->orderByDesc('ran_at')
|
||
->orderBy('sequence');
|
||
}
|
||
|
||
#[Computed]
|
||
public function models(): array
|
||
{
|
||
return Cache::remember('summary:models', 300, function () {
|
||
return \Illuminate\Support\Facades\DB::table('session_summaries')
|
||
->where('visibility', 'public')
|
||
->distinct()->orderBy('model_id')->pluck('model_id')->all();
|
||
});
|
||
}
|
||
|
||
#[Computed]
|
||
public function suites(): array
|
||
{
|
||
return Cache::remember('summary:suites', 300, function () {
|
||
return \Illuminate\Support\Facades\DB::table('session_suite_rows')
|
||
->join('session_summaries', 'session_summaries.id', '=', 'session_suite_rows.session_summary_id')
|
||
->where('session_summaries.visibility', 'public')
|
||
->distinct()->orderBy('suite')->pluck('suite')->all();
|
||
});
|
||
}
|
||
};
|
||
?>
|
||
|
||
<div>
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<flux:heading size="xl">{{ __('Benchmark Sessions') }}</flux:heading>
|
||
<flux:subheading>{{ __('GPU, model, suite scores — click a suite line for per-task detail.') }}</flux:subheading>
|
||
</div>
|
||
<flux:button variant="ghost" icon="table-cells" href="{{ route('results') }}" wire:navigate>{{ __('All task rows') }}</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="__('Session, model, GPU or runner…')"
|
||
size="sm"
|
||
class="w-64"
|
||
clearable
|
||
/>
|
||
|
||
<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.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:button size="sm" variant="ghost" wire:click="resetFilters" icon="x-mark">{{ __('Clear') }}</flux:button>
|
||
</div>
|
||
|
||
@if ($this->sessions->count() > 0)
|
||
<div class="mt-6 flex flex-col gap-4">
|
||
@foreach ($this->sessions as $session)
|
||
<flux:card class="!p-0">
|
||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 border-b border-zinc-200 px-4 py-3 dark:border-zinc-700">
|
||
<flux:icon name="cpu" class="size-4 text-zinc-500" />
|
||
<span class="text-sm font-semibold">{{ $session->band->gpu ?? 'unknown GPU' }}</span>
|
||
@if ($session->band->gpu_count)
|
||
<flux:badge size="xs" color="zinc">×{{ $session->band->gpu_count }}</flux:badge>
|
||
@endif
|
||
<span class="text-sm">{{ $session->band->model_id ?? 'unknown model' }}</span>
|
||
@if ($session->band->model_quant)
|
||
<flux:badge size="xs" color="zinc">{{ $session->band->model_quant }}</flux:badge>
|
||
@endif
|
||
<flux:spacer />
|
||
<span class="text-sm text-zinc-500">{{ $session->band->runner_name ?? 'anonymous' }}</span>
|
||
<span class="text-xs text-zinc-400">{{ optional($session->band->ran_at)->format('M j Y') }}</span>
|
||
</div>
|
||
@if ($session->band->label)
|
||
<div class="px-4 pt-2 text-xs text-zinc-500">{{ $session->band->label }}</div>
|
||
@endif
|
||
{{-- Column titles aligned over the suite rows (md+ only) --}}
|
||
<div class="hidden px-4 pt-2 md:flex flex-wrap items-center gap-x-4 gap-y-1">
|
||
<span class="w-24 text-[10px] font-semibold uppercase tracking-wider text-zinc-400">{{ __('Benchmark') }}</span>
|
||
<span class="w-14 text-[10px] font-semibold uppercase tracking-wider text-zinc-400">{{ __('Thinking') }}</span>
|
||
<span class="w-24 text-[10px] font-semibold uppercase tracking-wider text-zinc-400">{{ __('Result') }}</span>
|
||
<span class="w-40 text-[10px] font-semibold uppercase tracking-wider text-zinc-400">{{ __('Speed (tok/s)') }} <span class="font-normal normal-case">avg · wtd</span></span>
|
||
<span class="w-20 text-[10px] font-semibold uppercase tracking-wider text-zinc-400">{{ __('Tokens') }}</span>
|
||
</div>
|
||
<div class="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||
@foreach ($session->suites as $row)
|
||
<a href="{{ route('runs.show', $row->run_id) }}" wire:navigate
|
||
class="flex flex-wrap items-center gap-x-4 gap-y-1 px-4 py-2.5 hover:bg-zinc-50 dark:hover:bg-zinc-800/60">
|
||
<flux:badge size="xs">{{ $row->suite }}</flux:badge>
|
||
@if ($row->level)
|
||
<flux:badge size="xs" color="sky">{{ $row->level }}</flux:badge>
|
||
@endif
|
||
<span class="flex items-center gap-1.5">
|
||
<flux:badge size="xs" color="emerald">✔ {{ $row->passed }}</flux:badge>
|
||
<flux:badge size="xs" color="{{ $row->failed > 0 ? 'rose' : 'zinc' }}">✘ {{ $row->failed }}</flux:badge>
|
||
</span>
|
||
@if ($row->avg_user_tps !== null)
|
||
<span class="font-mono text-sm">{{ number_format($row->avg_user_tps, 1) }} <span class="text-xs text-zinc-400">avg</span></span>
|
||
<span class="font-mono text-sm">{{ number_format($row->weighted_user_tps, 1) }} <span class="text-xs text-zinc-400">wtd tok/s</span></span>
|
||
@endif
|
||
<span class="font-mono text-sm">{{ number_format($row->total_tokens) }} <span class="text-xs text-zinc-400">tokens</span></span>
|
||
<span class="ms-auto text-xs text-zinc-400">{{ __('per-task detail') }} →</span>
|
||
</a>
|
||
@endforeach
|
||
</div>
|
||
</flux:card>
|
||
@endforeach
|
||
</div>
|
||
|
||
<div class="mt-4">
|
||
{{ $this->sessions->links() }}
|
||
</div>
|
||
@else
|
||
<div class="mt-6">
|
||
<flux:callout variant="brand">
|
||
<flux:callout.heading>{{ __('No sessions') }}</flux:callout.heading>
|
||
<flux:callout.text>{{ __('No benchmark sessions match the current filters.') }}</flux:callout.text>
|
||
</flux:callout>
|
||
</div>
|
||
@endif
|
||
</div>
|