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

414 lines
No EOL
23 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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' => [],
];
/** @var array<int, bool> session_summary_id => expanded */
public array $expanded = [];
public function updatedFilters(): void
{
$this->resetPage();
$this->expanded = [];
}
public function toggleExpand(int $sessionId): void
{
$this->expanded[$sessionId] = ! ($this->expanded[$sessionId] ?? false);
}
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 total to expand benchmark rows.') }}</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)
@php $isExpanded = $this->expanded[$session->band->id] ?? false; @endphp
<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));
$hasDevlogic = $devlogicRows->isNotEmpty() && ($suiteFilterActive || $session->band->devlogic_tps !== null);
$hasSysops = $sysopsRows->isNotEmpty() && ($suiteFilterActive || $session->band->sysops_tps !== null);
@endphp
{{-- Devlogic section: total strip (click to expand) + suite rows (when expanded) --}}
@if ($hasDevlogic)
@unless ($suiteFilterActive)
<button type="button"
wire:click="toggleExpand({{ $session->band->id }})"
class="flex w-full flex-wrap items-center gap-x-4 gap-y-1 border-l-2 border-sky-400 bg-sky-50/50 px-4 py-2 text-left transition hover:bg-sky-50 dark:border-sky-500 dark:bg-sky-950/20 dark:hover:bg-sky-950/40"
>
<flux:icon name="{{ $isExpanded ? 'chevron-down' : 'chevron-right' }}" variant="mini" class="size-4 text-sky-500" />
<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>
</button>
@endunless
@if ($isExpanded || $suiteFilterActive)
<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
@endif
{{-- Sysops section: total strip (click to expand) + suite rows (when expanded) --}}
@if ($hasSysops)
@unless ($suiteFilterActive)
<button type="button"
wire:click="toggleExpand({{ $session->band->id }})"
class="flex w-full flex-wrap items-center gap-x-4 gap-y-1 border-l-2 border-amber-400 bg-amber-50/50 px-4 py-2 text-left transition hover:bg-amber-50 dark:border-amber-500 dark:bg-amber-950/20 dark:hover:bg-amber-950/40"
>
<flux:icon name="{{ $isExpanded ? 'chevron-down' : 'chevron-right' }}" variant="mini" class="size-4 text-amber-500" />
<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>
</button>
@endunless
@if ($isExpanded || $suiteFilterActive)
<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
@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>