The headline totals were using a per-task geometric mean, which gave disproportionate weight to short responses with low effective user_tps (e.g. 2-token SoftBench answers at ~1.2 tok/s). This produced misleadingly low Devlogic totals (20.5 for asus_210w) when the displayed suite speeds were all in the 40-50 tok/s range. Replace geo-mean accumulators (ln_sum/ln_n) with token-weighted accumulators (rate_token_sum = sum(user_tps × tokens)). The headline total is now rate_token_sum / total_tokens, matching the methodology already used by each displayed suite row. For ScenarioBench, the suite aggregate gen_tps (sum(gen_tokens)/sum(wall)) is weighted by the suite's total generated tokens, keeping the headline total consistent with the displayed ScenarioBench row. Results for the 210W bands: asus_210w: devlogic 20.5 → 47.0, sysops 19.5 → 10.6 sapphire_210w: devlogic 19.6 → 43.7, sysops 8.3 → 8.8 |
||
|---|---|---|
| .github/workflows | ||
| app | ||
| bootstrap | ||
| config | ||
| database | ||
| docker | ||
| public | ||
| resources | ||
| routes | ||
| storage | ||
| tests | ||
| .dockerignore | ||
| .editorconfig | ||
| .env.example | ||
| .gitattributes | ||
| .gitignore | ||
| artisan | ||
| Caddyfile.dev | ||
| composer.json | ||
| composer.lock | ||
| dev.sh | ||
| Dockerfile | ||
| package-lock.json | ||
| package.json | ||
| phpunit.xml | ||
| README.md | ||
| vite.config.js | ||
festbench-web
Web viewer for FestBench results — LLM benchmark datapoints
submitted by the festbench CLI, displayed in a fast, sortable table.
Stack: Laravel 12 · Livewire 4 + Volt · Flux UI (Pro) · FrankenPHP · PostgreSQL 17
Architecture (v2)
One table per bench suite, monthly-partitioned on Postgres:
| Table | Suite-specific columns |
|---|---|
softbench_results |
section, tool_calls |
infrabench_results |
kind (recall/constraint) |
prosebench_results |
section, checks_passed/total, judge, prefill, prompt_tokens |
psychbench_results |
section, checks_passed/total |
scenario_runs |
scenario, outcome, turns, commands, submissions, rejected, t_pass, verdicts |
All share the core metric columns (ok, decode, user_tps, ttft, t_answer, wall,
reasoning, visible, total, finish) plus a payload JSONB preserving the
complete source row — future festbench fields need no migration.
Why: each suite has a different shape; partitioning keeps appends on a hot
partition; decommissioning a bench detaches whole partitions; and Postgres
inlines the all_results UNION view so the combined home query uses each
suite table's indexes directly.
Suite registry — decommissioning & adding benches
config/festbench.php is the single source of truth: suite → model, active
flag, in_all_results flag.
- Decommission a bench: flip
active => false, runphp artisan festbench:suites:refresh. The suite vanishes from the home table and suite filter; historical data stays queryable and old run-detail links keep rendering. - Add a bench: migration + model, registry entry, refresh.
Scale design (built lightly, ready for 10k submissions/day)
all_resultsview — combined home table; suite-filtered queries bypass it and hit the concrete table's indexes.home_snapshot— flat precomputed default view, refreshed every 5 minutes (festbench:snapshot). At high traffic the request path never runs a UNION.- TTL staleness caching (60–300s), never event-flush — at high submission rates event flushes would storm the cache; benchmark data a couple of minutes old is correct.
- Guest pages carry
Cache-Control: public, s-maxage=…, stale-while-revalidateso a CDN absorbs the bulk of views. Authenticated views areprivate, no-store(they embed the user's private runs). - Queue-based CLI ingestion + Redis driver: documented as the required shape for the future submit endpoint (auth session is the prerequisite).
- Retention: everything is kept forever.
- Monthly partitions exist for ±1 year at migration time; a scheduled partition-creation command is the follow-up when real submission volume approaches the horizon.
Visibility rules
Runs are public by default. Guests see public runs only; authenticated users additionally see their own private runs. Festbench-submitted metrics are immutable — enforced by a model guard and covered by tests; owners can only add notes and toggle visibility.
Local development
Requirements: frankenphp binary on PATH, Node 20+, Docker/Podman.
docker compose -f docker/develop.compose.yaml up -d # Postgres 17 on 127.0.0.1:5432
./dev.sh # frankenphp :8080 + vite HMR
Composer runs through frankenphp's embedded PHP (the host php.ini disables most extensions):
frankenphp php-cli $(pwd)/vendor/bin/composer install
frankenphp php-cli $(pwd)/vendor/bin/pest
frankenphp php-cli $(pwd)/vendor/bin/pint
frankenphp php-cli artisan <command>
Importing festbench data
# everything from a local benchmarks.db
php artisan festbench:import results/benchmarks.db --user=you@example.com
# one session, surgically, purging existing data first
php artisan festbench:import results/benchmarks.db \
--user=you@example.com --label="deterministic sampling, repeat 1" --fresh
--label filters by the run's label; --fresh deletes all existing benchmark
data first; rows route to per-suite tables via the registry; unknown suites
keep their run but warn about skipped rows.
Production (Coolify)
Build pack: Dockerfile. Single FrankenPHP process — no nginx, no php-fpm, no supervisor.
- 3 stages: composer (Flux Pro auth as a BuildKit secret), node assets,
final
dunglas/frankenphp:1.12-php8.5+install-php-extensions pdo_pgsql pgsql intl zip opcache. - Entrypoint:
migrate --force(retries) →festbench:suites:refresh→festbench:snapshot→frankenphp run. - Caddyfile listens on
{$SERVER_NAME::8080}; Coolify maps its port.
Secrets/env to set in Coolify:
| Variable | Notes |
|---|---|
APP_KEY |
generate once, keep stable |
DB_HOST / DB_PORT / DB_DATABASE / DB_USERNAME / DB_PASSWORD |
the nearby Postgres |
SESSION_DRIVER=database, CACHE_STORE=database |
no redis needed yet |
FESTBENCH_HOME_CACHE_TTL |
seconds; default 300 |
Flux Pro authentication: BuildKit secret composer_auth (contents = the
project's auth.json). The license key is never committed.
Health check: GET /up.
Submitting results (CLI integration — separate session)
The schema mirrors festbench's output exactly; festbench:import is the
reference implementation of the mapping. The authed ingestion endpoint for
festbench --submit is future work (queue-based, Redis-backed at volume).
Notes
- Scheduler must run in production for
festbench:snapshot(Coolify's scheduled task orschedule:workin a sidecar when needed). - Worker mode toggle:
FRANKENPHP_CONFIG="worker …"— documented option, off by default. - Tests run on in-memory SQLite (plain tables); partitioning is Postgres-only.