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

390 lines
22 KiB
PHP
Raw Normal View History

<?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 = [
'gpu' => [],
'model' => [],
'quant' => [],
'engine' => [],
'suite' => [],
];
public function updatedFilters(): void
{
$this->resetPage();
}
public function resetFilters(): void
{
$this->filters = [
'gpu' => [],
'model' => [],
'quant' => [],
'engine' => [],
'suite' => [],
];
$this->resetPage();
}
public function toggleFilter(string $field, string $value): void
{
if (! array_key_exists($field, $this->filters)) {
return;
}
$this->filters[$field] = array_values(array_unique(
in_array($value, $this->filters[$field], true)
? array_diff($this->filters[$field], [$value])
: array_merge($this->filters[$field], [$value])
));
$this->resetPage();
}
public function filterLabel(string $field): string
{
$count = count($this->filters[$field] ?? []);
return $count === 0 ? '' : ' ('.$count.')';
}
#[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['gpu'], fn ($q, array $gpus) => $q->whereIn('gpu', $gpus))
->when($this->filters['model'], fn ($q, array $models) => $q->whereIn('model_id', $models))
->when($this->filters['quant'], fn ($q, array $quants) => $q->whereIn('model_quant', $quants))
->when($this->filters['engine'], fn ($q, array $engines) => $q->whereIn('engine', $engines))
->when($this->filters['suite'], fn ($q, array $suites) => $q->whereExists(function ($sub) use ($suites) {
$sub->select(\Illuminate\Support\Facades\DB::raw(1))
->from('session_suite_rows')
->whereColumn('session_suite_rows.session_summary_id', 'session_summaries.id')
->whereIn('session_suite_rows.suite', $suites);
}))
->orderByDesc('ran_at')
->orderBy('sequence');
}
#[Computed]
public function gpus(): array
{
return Cache::remember('summary:gpus', 300, function () {
return \Illuminate\Support\Facades\DB::table('session_summaries')
->where('visibility', 'public')
->whereNotNull('gpu')
->distinct()->orderBy('gpu')->pluck('gpu')->all();
});
}
#[Computed]
public function models(): array
{
return Cache::remember('summary:models', 300, function () {
return \Illuminate\Support\Facades\DB::table('session_summaries')
->where('visibility', 'public')
->whereNotNull('model_id')
->distinct()->orderBy('model_id')->pluck('model_id')->all();
});
}
#[Computed]
public function quants(): array
{
return Cache::remember('summary:quants', 300, function () {
return \Illuminate\Support\Facades\DB::table('session_summaries')
->where('visibility', 'public')
->whereNotNull('model_quant')
->distinct()->orderBy('model_quant')->pluck('model_quant')->all();
});
}
#[Computed]
public function engines(): array
{
return Cache::remember('summary:engines', 300, function () {
return \Illuminate\Support\Facades\DB::table('session_summaries')
->where('visibility', 'public')
->whereNotNull('engine')
->distinct()->orderBy('engine')->pluck('engine')->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">{{ __('FestBench - Benchmarks') }}</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">
@php
$filterGroups = [
['field' => 'gpu', 'label' => __('GPU'), 'options' => $this->gpus],
['field' => 'model', 'label' => __('Model'), 'options' => $this->models],
['field' => 'quant', 'label' => __('Quant'), 'options' => $this->quants],
['field' => 'engine', 'label' => __('Inference engine'), 'options' => $this->engines],
['field' => 'suite', 'label' => __('Suite'), 'options' => $this->suites],
];
@endphp
@foreach ($filterGroups as $group)
<flux:dropdown position="bottom" align="start">
<flux:button size="sm" variant="outline" class="justify-between {{ count($this->filters[$group['field']]) > 0 ? 'border-sky-400 text-sky-600 dark:border-sky-500 dark:text-sky-400' : '' }}">
{{ $group['label'] }}{{ $this->filterLabel($group['field']) }}
<flux:icon.chevron-down variant="mini" />
</flux:button>
<flux:menu class="min-w-48 max-h-72 overflow-y-auto p-1">
@foreach ($group['options'] as $option)
@php $checked = in_array($option, $this->filters[$group['field']], true); @endphp
<flux:menu.item
wire:click="toggleFilter('{{ $group['field'] }}', '{{ addslashes($option) }}')"
class="cursor-pointer"
>
<span class="flex items-center gap-2">
@if ($checked)
<flux:icon.check variant="mini" class="size-4 text-sky-500" />
@else
<span class="size-4"></span>
@endif
<span class="text-sm">{{ $option }}</span>
</span>
</flux:menu.item>
@endforeach
</flux:menu>
</flux:dropdown>
@endforeach
<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->label)
<flux:badge size="xs" color="amber">{{ $session->band->label }}</flux:badge>
@endif
@if ($session->band->model_quant)
<flux:badge size="xs" color="zinc">{{ $session->band->model_quant }}</flux:badge>
@endif
@if ($session->band->engine)
<flux:badge size="xs" color="sky">{{ $session->band->engine }}</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>
@php
$devlogicSuites = ['softbench', 'prosebench', 'psychbench', 'depthsweep'];
$sysopsSuites = ['infrabench', 'scenariobench'];
$suiteFilterActive = ! empty($this->filters['suite']);
$visibleSuites = $suiteFilterActive
? $session->suites->filter(fn ($r) => in_array($r->suite, $this->filters['suite'], true))
: $session->suites;
$devlogicRows = $visibleSuites->filter(fn ($r) => in_array($r->suite, $devlogicSuites));
$sysopsRows = $visibleSuites->filter(fn ($r) => in_array($r->suite, $sysopsSuites));
@endphp
{{-- Devlogic section: total strip + column headers + suite rows --}}
@if ($devlogicRows->isNotEmpty() && ($suiteFilterActive || $session->band->devlogic_tps !== null))
@unless ($suiteFilterActive)
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 border-l-2 border-sky-400 bg-sky-50/50 px-4 py-2 dark:border-sky-500 dark:bg-sky-950/20">
<span class="text-xs font-semibold uppercase tracking-wider text-sky-600 dark:text-sky-400">{{ __('Devlogic total') }}</span>
<span class="text-sm">
<span class="font-mono font-semibold">{{ number_format($session->band->devlogic_tps, 1) }}</span>
<span class="text-xs text-zinc-400">{{ __('tok/s') }}</span>
</span>
<span class="text-sm">
<span class="font-mono font-semibold">{{ number_format($session->band->devlogic_tokens) }}</span>
<span class="text-xs text-zinc-400">{{ __('tokens') }}</span>
</span>
<span class="text-sm">
<flux:badge size="xs" color="emerald">✔ {{ $session->band->devlogic_passed }}</flux:badge>
<flux:badge size="xs" color="{{ $session->band->devlogic_failed > 0 ? 'rose' : 'zinc' }}">✘ {{ $session->band->devlogic_failed }}</flux:badge>
</span>
</div>
@endunless
<div class="hidden px-4 pt-2 md:grid md:grid-cols-[7.5rem_4.5rem_7rem_15rem_6rem_1fr] md:gap-x-4 md:items-center">
<span class="text-[10px] font-semibold uppercase tracking-wider text-zinc-400">{{ __('Benchmark') }}</span>
<span class="text-[10px] font-semibold uppercase tracking-wider text-zinc-400">{{ __('Thinking') }}</span>
<span class="text-[10px] font-semibold uppercase tracking-wider text-zinc-400">{{ __('Result') }}</span>
<span class="text-[10px] font-semibold uppercase tracking-wider text-zinc-400">{{ __('Speed (tok/s)') }} <span class="font-normal normal-case">{{ __('weighted average') }}</span></span>
<span class="text-[10px] font-semibold uppercase tracking-wider text-zinc-400">{{ __('Tokens') }}</span>
<span></span>
</div>
<div class="divide-y divide-zinc-100 dark:divide-zinc-800">
@foreach ($devlogicRows as $row)
@php
$detailUrl = $row->suite === 'scenariobench'
? route('sessions.scenarios', $session->band->id)
: route('runs.show', $row->run_id) . ($row->level && $row->suite === 'depthsweep' ? '?level=' . $row->level : '');
@endphp
<a href="{{ $detailUrl }}" 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 md:grid md:grid-cols-[7.5rem_4.5rem_7rem_15rem_6rem_1fr] md:gap-x-4 md:items-center">
<span><flux:badge size="xs">{{ $row->suite }}</flux:badge></span>
<span>
@if ($row->level)
<flux:badge size="xs" color="sky">{{ $row->level }}</flux:badge>
@endif
</span>
<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>
<span>
@if ($row->weighted_user_tps !== null)
<span class="font-mono text-sm">{{ number_format($row->weighted_user_tps, 1) }}</span>
@elseif ($row->gen_tps !== null)
<span class="font-mono text-sm">{{ number_format($row->gen_tps, 1) }} <span class="text-xs text-zinc-400">{{ __('gen') }}</span></span>
@endif
</span>
<span class="font-mono text-sm">{{ number_format($row->total_tokens) }}</span>
<span class="text-xs text-zinc-400 md:justify-self-end">{{ __('per-task detail') }} →</span>
</a>
@endforeach
</div>
@endif
{{-- Sysops section: total strip + column headers + suite rows --}}
@if ($sysopsRows->isNotEmpty() && ($suiteFilterActive || $session->band->sysops_tps !== null))
@unless ($suiteFilterActive)
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 border-l-2 border-amber-400 bg-amber-50/50 px-4 py-2 dark:border-amber-500 dark:bg-amber-950/20">
<span class="text-xs font-semibold uppercase tracking-wider text-amber-600 dark:text-amber-400">{{ __('Sysops total') }}</span>
<span class="text-sm">
<span class="font-mono font-semibold">{{ number_format($session->band->sysops_tps, 1) }}</span>
<span class="text-xs text-zinc-400">{{ __('tok/s') }}</span>
</span>
<span class="text-sm">
<span class="font-mono font-semibold">{{ number_format($session->band->sysops_tokens) }}</span>
<span class="text-xs text-zinc-400">{{ __('tokens') }}</span>
</span>
<span class="text-sm">
<flux:badge size="xs" color="emerald">✔ {{ $session->band->sysops_passed }}</flux:badge>
<flux:badge size="xs" color="{{ $session->band->sysops_failed > 0 ? 'rose' : 'zinc' }}">✘ {{ $session->band->sysops_failed }}</flux:badge>
</span>
</div>
@endunless
<div class="hidden px-4 pt-2 md:grid md:grid-cols-[7.5rem_4.5rem_7rem_15rem_6rem_1fr] md:gap-x-4 md:items-center">
<span class="text-[10px] font-semibold uppercase tracking-wider text-zinc-400">{{ __('Benchmark') }}</span>
<span class="text-[10px] font-semibold uppercase tracking-wider text-zinc-400">{{ __('Thinking') }}</span>
<span class="text-[10px] font-semibold uppercase tracking-wider text-zinc-400">{{ __('Result') }}</span>
<span class="text-[10px] font-semibold uppercase tracking-wider text-zinc-400">{{ __('Speed (tok/s)') }} <span class="font-normal normal-case">{{ __('weighted average') }}</span></span>
<span class="text-[10px] font-semibold uppercase tracking-wider text-zinc-400">{{ __('Tokens') }}</span>
<span></span>
</div>
<div class="divide-y divide-zinc-100 dark:divide-zinc-800">
@foreach ($sysopsRows as $row)
@php
$detailUrl = $row->suite === 'scenariobench'
? route('sessions.scenarios', $session->band->id)
: route('runs.show', $row->run_id) . ($row->level && $row->suite === 'depthsweep' ? '?level=' . $row->level : '');
@endphp
<a href="{{ $detailUrl }}" 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 md:grid md:grid-cols-[7.5rem_4.5rem_7rem_15rem_6rem_1fr] md:gap-x-4 md:items-center">
<span><flux:badge size="xs">{{ $row->suite }}</flux:badge></span>
<span>
@if ($row->level)
<flux:badge size="xs" color="sky">{{ $row->level }}</flux:badge>
@endif
</span>
<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>
<span>
@if ($row->weighted_user_tps !== null)
<span class="font-mono text-sm">{{ number_format($row->weighted_user_tps, 1) }}</span>
@elseif ($row->gen_tps !== null)
<span class="font-mono text-sm">{{ number_format($row->gen_tps, 1) }} <span class="text-xs text-zinc-400">{{ __('gen') }}</span></span>
@endif
</span>
<span class="font-mono text-sm">{{ number_format($row->total_tokens) }}</span>
<span class="text-xs text-zinc-400 md:justify-self-end">{{ __('per-task detail') }} →</span>
</a>
@endforeach
</div>
@endif
</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>