When a suite filter is active, the per-band view now shows only the selected suite rows instead of all suites in the band. Category total strips (Devlogic/Sysops) are hidden during suite filtering since they aggregate over all suites and would be misleading for cross-band comparison. Clearing the suite filter restores all suite rows and totals.
390 lines
No EOL
22 KiB
PHP
390 lines
No EOL
22 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 = [
|
||
'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>
|