2026-09-19 00:12:30 +00:00
|
|
|
|
<?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
|
2026-09-20 01:11:20 +00:00
|
|
|
|
{{-- 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>
|
2026-09-19 00:12:30 +00:00
|
|
|
|
<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>
|