| "use client"; |
|
|
| import { useState } from "react"; |
| import { runBenchmark } from "@/lib/actions"; |
|
|
| type BenchResult = { name: string; ms: number; rows?: number }; |
|
|
| const badge = (ms: number) => |
| ms < 1 ? "text-emerald-400" : ms < 5 ? "text-yellow-400" : ms < 20 ? "text-orange-400" : "text-red-400"; |
|
|
| const bar = (ms: number, max: number) => |
| Math.max(4, Math.min(100, (ms / max) * 100)); |
|
|
| export default function QueryBench() { |
| const [results, setResults] = useState<BenchResult[]>([]); |
| const [total, setTotal] = useState(0); |
| const [loading, setLoading] = useState(false); |
| const [runs, setRuns] = useState<number[]>([]); |
|
|
| const run = async () => { |
| setLoading(true); |
| try { |
| const r = await runBenchmark(); |
| setResults(r.results); |
| setTotal(r.totalMs); |
| setRuns((p) => [...p.slice(-9), r.totalMs]); |
| } finally { |
| setLoading(false); |
| } |
| }; |
|
|
| const max = results.length ? Math.max(...results.map((r) => r.ms), 0.1) : 1; |
| const avg = runs.length ? +(runs.reduce((a, b) => a + b, 0) / runs.length).toFixed(2) : 0; |
|
|
| return ( |
| <div className="space-y-4"> |
| <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3"> |
| <div> |
| <h2 className="text-lg font-bold">⚡ Query Benchmark</h2> |
| <p className="text-xs text-zinc-500">SQLite query performance — direct server-side via Prisma (no API roundtrip)</p> |
| </div> |
| <button |
| onClick={run} |
| disabled={loading} |
| className="bg-emerald-600 hover:bg-emerald-500 disabled:bg-zinc-700 px-5 py-2.5 rounded-lg text-sm font-medium transition shrink-0" |
| > |
| {loading ? "Running..." : "🚀 Run Benchmark"} |
| </button> |
| </div> |
| |
| {results.length > 0 && ( |
| <> |
| <div className="grid grid-cols-2 sm:grid-cols-4 gap-3"> |
| <Stat label="Total" value={`${total}ms`} /> |
| <Stat label="Queries" value={`${results.length}`} /> |
| <Stat label="Avg/query" value={`${(total / results.length).toFixed(2)}ms`} /> |
| <Stat label={`Avg (${runs.length} runs)`} value={`${avg}ms`} /> |
| </div> |
| |
| <div className="bg-zinc-900 border border-zinc-800 rounded-xl overflow-hidden overflow-x-auto"> |
| <div className="min-w-[480px]"> |
| <div className="grid grid-cols-[1fr_80px_60px_1fr] gap-2 px-4 py-2.5 border-b border-zinc-800 text-[11px] text-zinc-500 font-medium uppercase tracking-wider"> |
| <span>Query</span> |
| <span className="text-right">Time</span> |
| <span className="text-right">Rows</span> |
| <span>Speed</span> |
| </div> |
| {results.map((r, i) => ( |
| <div |
| key={i} |
| className="grid grid-cols-[1fr_80px_60px_1fr] gap-2 px-4 py-3 border-b border-zinc-800/50 hover:bg-zinc-800/30 transition text-sm items-center" |
| > |
| <span className="font-mono text-xs truncate">{r.name}</span> |
| <span className={`text-right font-mono text-xs font-bold ${badge(r.ms)}`}> |
| {r.ms}ms |
| </span> |
| <span className="text-right text-xs text-zinc-500"> |
| {r.rows !== undefined ? r.rows : "—"} |
| </span> |
| <div className="h-2 bg-zinc-800 rounded-full overflow-hidden"> |
| <div |
| className={`h-full rounded-full transition-all duration-500 ${r.ms < 1 ? "bg-emerald-500" : r.ms < 5 ? "bg-yellow-500" : r.ms < 20 ? "bg-orange-500" : "bg-red-500"}`} |
| style={{ width: `${bar(r.ms, max)}%` }} |
| /> |
| </div> |
| </div> |
| ))} |
| </div> |
| </div> |
| |
| <div className="flex flex-wrap gap-3 text-[11px] text-zinc-500"> |
| <span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-full bg-emerald-500" /><1ms</span> |
| <span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-full bg-yellow-500" />1-5ms</span> |
| <span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-full bg-orange-500" />5-20ms</span> |
| <span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-full bg-red-500" />>20ms</span> |
| </div> |
| |
| {runs.length > 1 && ( |
| <div className="bg-zinc-900 border border-zinc-800 rounded-xl p-4"> |
| <p className="text-xs text-zinc-500 mb-2">Run History (total ms)</p> |
| <div className="flex items-end gap-1 h-16"> |
| {runs.map((r, i) => { |
| const h = Math.max(8, (r / Math.max(...runs)) * 100); |
| return ( |
| <div key={i} className="flex-1 flex flex-col items-center gap-1"> |
| <span className="text-[9px] text-zinc-500">{r.toFixed(0)}</span> |
| <div |
| className={`w-full rounded-t ${r < 20 ? "bg-emerald-600" : r < 50 ? "bg-yellow-600" : "bg-red-600"}`} |
| style={{ height: `${h}%` }} |
| /> |
| </div> |
| ); |
| })} |
| </div> |
| </div> |
| )} |
| </> |
| )} |
| </div> |
| ); |
| } |
|
|
| function Stat({ label, value }: { label: string; value: string }) { |
| return ( |
| <div className="bg-zinc-900 border border-zinc-800 rounded-lg p-3 text-center"> |
| <p className="text-lg font-bold font-mono">{value}</p> |
| <p className="text-[10px] text-zinc-500 mt-0.5">{label}</p> |
| </div> |
| ); |
| } |
|
|