festbench-web/resources/views/livewire/sessions/scenarios.blade.php
PeterChrz 9bf17dba6f
Make label part of band identity; correct ASUS GPU attribution
Labels now define distinct sessions — different power-cap configs
(asus_210w, sapphire_210w, asus_uncapped) render as separate bands
instead of merging into one. The label badge displays alongside
quant/engine/config badges rather than replacing them.

Scenario drilldown filters by label to prevent cross-band mixing.

Data fix: runs 22-32 (asus_210w) and 43-46 (asus_uncapped) reassigned
from Sapphire (gpu_id=11) to ASUS (gpu_id=12) — they were run on the
ASUS card but imported with the Sapphire GPU identity.
2026-10-04 23:18:21 -04:00

169 lines
No EOL
8.7 KiB
PHP

<?php
use App\Livewire\IncludesSorting;
use App\Models\BenchmarkRun;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Layout;
use Livewire\Component;
new #[Layout('components.layouts.app')] class extends Component
{
use IncludesSorting;
private const SORTABLE = [
'scenario', 'level', 'ok', 'outcome',
'wall', 'turns', 'commands', 'submissions', 'rejected',
't_first_sub', 't_pass', 'gen_tokens', 'gen_tps',
];
public ?int $sessionId = null;
public function mount(int $session): void
{
$this->sessionId = $session;
}
#[Computed]
public function band(): object
{
return DB::table('session_summaries')
->where('id', $this->sessionId)
->where(function ($q) {
$q->where('visibility', 'public');
if (auth()->user() !== null) {
$q->orWhere(function ($own) {
$own->where('visibility', 'private')
->where('user_id', auth()->id());
});
}
})
->firstOrFail();
}
#[Computed]
public function runs()
{
$band = $this->band;
return BenchmarkRun::where('suite', 'scenariobench')
->where('model_id', $band->model_id)
->where('model_quant', $band->model_quant)
->where('gpu', $band->gpu)
->where('gpu_count', $band->gpu_count)
->where('user_id', $band->user_id)
->where('visibility', $band->visibility)
->where('engine', $band->engine)
->where(function ($q) use ($band) {
$q->whereNull('config_id')
->orWhere('config_id', $this->configIdFromName($band->config_name));
})
->where(function ($q) use ($band) {
if ($band->label !== null && $band->label !== '') {
$q->where('label', $band->label);
} else {
$q->where(function ($inner) {
$inner->whereNull('label')->orWhere('label', '');
});
}
})
->orderBy('ran_at')
->get();
}
private function configIdFromName(?string $name): ?int
{
if ($name === null) {
return null;
}
return DB::table('server_configs')->where('name', $name)->value('id');
}
#[Computed]
public function results()
{
$runIds = $this->runs->pluck('id');
$column = in_array($this->sortBy, self::SORTABLE, true) ? $this->sortBy : 'scenario';
$direction = $this->sortDirection === 'asc' ? 'asc' : 'desc';
$query = DB::table('scenario_runs')
->whereIn('benchmark_run_id', $runIds);
if ($column === 'gen_tps') {
$query->orderByRaw('gen_tokens / nullif(wall, 0) '.$direction);
} else {
$query->orderBy($column, $direction);
}
return $query->orderBy('id')->get();
}
};
?>
<div class="flex flex-col gap-6">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<flux:heading size="xl">{{ __('Scenario results') }}</flux:heading>
<flux:subheading>
@if ($this->band->label)
<flux:badge size="xs" color="amber">{{ $this->band->label }}</flux:badge>
@endif
<span class="text-sm">{{ $this->band->model_id ?? 'unknown model' }}</span>
@if ($this->band->model_quant)
<flux:badge size="xs" color="zinc">{{ $this->band->model_quant }}</flux:badge>
@endif
<span class="text-sm text-zinc-500">{{ $this->band->gpu ?? 'unknown GPU' }}</span>
</flux:subheading>
</div>
<flux:button variant="ghost" icon="arrow-left" href="{{ route('home') }}" wire:navigate>{{ __('Sessions') }}</flux:button>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column sortable :sorted="$sortBy === 'scenario'" :direction="$sortDirection" wire:click="sort('scenario')">{{ __('Scenario') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'level'" :direction="$sortDirection" wire:click="sort('level')">{{ __('Level') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'ok'" :direction="$sortDirection" wire:click="sort('ok')">{{ __('Pass') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'outcome'" :direction="$sortDirection" wire:click="sort('outcome')">{{ __('Outcome') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'wall'" :direction="$sortDirection" wire:click="sort('wall')" class="text-center">{{ __('Wall (s)') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'turns'" :direction="$sortDirection" wire:click="sort('turns')" class="text-center">{{ __('Turns') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'commands'" :direction="$sortDirection" wire:click="sort('commands')" class="text-center">{{ __('Commands') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'submissions'" :direction="$sortDirection" wire:click="sort('submissions')" class="text-center">{{ __('Submissions') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'rejected'" :direction="$sortDirection" wire:click="sort('rejected')" class="text-center">{{ __('Rejected') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 't_first_sub'" :direction="$sortDirection" wire:click="sort('t_first_sub')" class="text-center">{{ __('t first sub') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 't_pass'" :direction="$sortDirection" wire:click="sort('t_pass')" class="text-center">{{ __('t pass') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'gen_tokens'" :direction="$sortDirection" wire:click="sort('gen_tokens')" class="text-center">{{ __('Gen tokens') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'gen_tps'" :direction="$sortDirection" wire:click="sort('gen_tps')" class="text-center">{{ __('Gen tok/s') }}</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@foreach ($this->results as $result)
<flux:table.row :key="$result->id">
<flux:table.cell>{{ $result->scenario }}</flux:table.cell>
<flux:table.cell>{{ $result->level ?? '—' }}</flux:table.cell>
<flux:table.cell>
@if ($result->ok)
<flux:icon name="check-circle" variant="solid" class="size-4 text-emerald-500" />
@elseif ($result->ok === null)
<span class="text-zinc-400">—</span>
@else
<flux:icon name="x-circle" variant="solid" class="size-4 text-red-500" />
@endif
</flux:table.cell>
<flux:table.cell><flux:badge size="xs" color="{{ $result->outcome === 'passed' ? 'emerald' : 'rose' }}">{{ $result->outcome ?? '—' }}</flux:badge></flux:table.cell>
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->wall, 1) }}</flux:table.cell>
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->turns) }}</flux:table.cell>
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->commands) }}</flux:table.cell>
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->submissions) }}</flux:table.cell>
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->rejected) }}</flux:table.cell>
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->t_first_sub, 1) }}</flux:table.cell>
<flux:table.cell class="text-center font-mono text-sm">{{ $result->t_pass !== null ? number_format($result->t_pass, 1) : '—' }}</flux:table.cell>
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->gen_tokens) }}</flux:table.cell>
<flux:table.cell class="text-center font-mono text-sm">{{ $result->wall > 0 && $result->gen_tokens > 0 ? number_format($result->gen_tokens / $result->wall, 1) : '—' }}</flux:table.cell>
</flux:table.row>
@endforeach
</flux:table.rows>
</flux:table>
</div>