diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 586d0fa..54c472d 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -291,6 +291,37 @@ export async function fetchCustomTrend(leaderboardId: string): Promise { + const res = await fetch(`/api/leaderboard/${leaderboardId}/fastest_trend`); + if (!res.ok) { + const json = await res.json(); + const message = json?.message || "Unknown error"; + throw new APIError(`Failed to fetch fastest trend: ${message}`, res.status); + } + const r = await res.json(); + return r.data; +} + export interface UserTrendResponse { leaderboard_id: number; user_ids: string[]; diff --git a/frontend/src/pages/leaderboard/components/UserTrendChart.tsx b/frontend/src/pages/leaderboard/components/UserTrendChart.tsx index 9065f30..1cad734 100644 --- a/frontend/src/pages/leaderboard/components/UserTrendChart.tsx +++ b/frontend/src/pages/leaderboard/components/UserTrendChart.tsx @@ -21,15 +21,20 @@ import { import { fetchUserTrend, fetchCustomTrend, + fetchFastestTrend, searchUsers, type UserTrendResponse, type CustomTrendResponse, + type FastestTrendResponse, type UserSearchResult, } from "../../../api/api"; // Display name prefix for custom (KernelAgent) entries const CUSTOM_ENTRY_PREFIX = "KernelAgent"; +// Display label for the fastest trend option +const FASTEST_TREND_LABEL = "⚡ Fastest (All Users)"; + // Simple option type - custom entries are identified by id starting with "custom_" to avoid collisions interface TrendOption { id: string; @@ -89,11 +94,12 @@ function toDailyBestSeries score for quick lookup - const submissionsByTime = sorted.map(p => ({ time: p.value[0], score: p.value[1] })); + // Build a map of timestamp -> data point for quick lookup + const submissionsByTime = sorted.map(p => ({ time: p.value[0], score: p.value[1], point: p })); const result: T[] = []; let runningMin = Infinity; + let currentBestPoint: T = sorted[0]; // Track the point that holds the current record let submissionIndex = 0; // Iterate through each day up to user's last submission @@ -103,15 +109,18 @@ function toDailyBestSeries userEndMidnight.getTime()) { result.push({ - ...sorted[0], + ...currentBestPoint, value: [globalEndMidnight.getTime(), runningMin] as [number, number], }); } @@ -137,6 +146,8 @@ function toDailyBestSeries(null); const [customData, setCustomData] = useState(null); + const [fastestTrendData, setFastestTrendData] = useState(null); + const [showFastestTrend, setShowFastestTrend] = useState(true); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [selectedGpuType, setSelectedGpuType] = useState(defaultGpuType || ""); @@ -223,6 +234,21 @@ export default function UserTrendChart({ leaderboardId, defaultUsers, defaultGpu loadCustomData(); }, [leaderboardId]); + // Fetch fastest trend data when checkbox is toggled on + useEffect(() => { + if (showFastestTrend && !fastestTrendData) { + const loadFastestTrend = async () => { + try { + const result = await fetchFastestTrend(leaderboardId); + setFastestTrendData(result); + } catch (err) { + console.error("Failed to load fastest trend data:", err); + } + }; + loadFastestTrend(); + } + }, [showFastestTrend, fastestTrendData, leaderboardId]); + // Build combined options: users + custom entries // Custom entries are identified by id starting with "custom_" // Sort all options alphabetically by label @@ -525,17 +551,30 @@ export default function UserTrendChart({ leaderboardId, defaultUsers, defaultGpu {resetting ? "Loading..." : " Load Top 5 "} )} - setClipOffscreen(e.target.checked)} - size="small" - /> - } - label="Clip offscreen" - sx={{ ml: 1 }} - /> + + setClipOffscreen(e.target.checked)} + size="small" + /> + } + label="Clip offscreen" + slotProps={{ typography: { variant: "body2" } }} + /> + setShowFastestTrend(e.target.checked)} + size="small" + /> + } + label="⚡ Fastest (All Users)" + slotProps={{ typography: { variant: "body2" } }} + /> + setDisplayMode(e.target.value as "all" | "best")} @@ -557,7 +596,7 @@ export default function UserTrendChart({ leaderboardId, defaultUsers, defaultGpu ); - if (selectedUsers.length === 0 && selectedCustomEntries.length === 0) { + if (selectedUsers.length === 0 && selectedCustomEntries.length === 0 && !showFastestTrend) { return ( {renderSearchInput()} @@ -611,7 +650,7 @@ export default function UserTrendChart({ leaderboardId, defaultUsers, defaultGpu const hasUserData = data?.time_series && Object.keys(data.time_series).length > 0; const hasCustomSelection = selectedCustomEntries.length > 0; - if (!hasUserData && !hasCustomSelection) { + if (!hasUserData && !hasCustomSelection && !showFastestTrend) { return ( {renderSearchInput()} @@ -629,11 +668,13 @@ export default function UserTrendChart({ leaderboardId, defaultUsers, defaultGpu ); } - // Use user data GPU type or fall back to first available AI GPU type - const effectiveGpuType = selectedGpuType || gpuTypes[0] || ""; + // Use user data GPU type or fall back to first available AI GPU type or fastest trend GPU type + const fastestTrendGpuTypes = fastestTrendData?.time_series ? Object.keys(fastestTrendData.time_series) : []; + const allGpuTypes = [...new Set([...gpuTypes, ...fastestTrendGpuTypes])]; + const effectiveGpuType = selectedGpuType || allGpuTypes[0] || ""; const gpuData = data?.time_series?.[effectiveGpuType] || {}; - if (Object.keys(gpuData).length === 0 && !hasCustomSelection) { + if (Object.keys(gpuData).length === 0 && !hasCustomSelection && !showFastestTrend) { return ( {renderSearchInput()} @@ -781,6 +822,56 @@ export default function UserTrendChart({ leaderboardId, defaultUsers, defaultGpu }); } + // Add fastest trend series if enabled + if (showFastestTrend && fastestTrendData?.time_series?.[effectiveGpuType]) { + const fastestGpuData = fastestTrendData.time_series[effectiveGpuType]; + const fastestDataPoints = fastestGpuData.fastest; + + if (fastestDataPoints && fastestDataPoints.length > 0) { + const sortedFastestData = [...fastestDataPoints].sort( + (a, b) => + new Date(a.submission_time).getTime() - + new Date(b.submission_time).getTime() + ); + + const displayName = FASTEST_TREND_LABEL; + const color = "#FFD700"; // Gold color for the fastest trend + + let chartData = sortedFastestData.map((point) => ({ + value: [ + new Date(point.submission_time).getTime(), + point.score, + ] as [number, number], + gpu_type: point.gpu_type, + user_name: point.user_name, + record_holder: point.user_name, + })); + + // Apply daily best series if display mode is "best" + if (displayMode === "best") { + chartData = toDailyBestSeries(chartData, globalEndDate); + } + + series.push({ + name: displayName, + type: "line", + data: chartData, + smooth: true, + symbol: "diamond", + symbolSize: 10, + lineStyle: { + width: 3, + color, + type: "solid", + }, + itemStyle: { + color, + }, + z: 10, // Ensure it's drawn on top + }); + } + } + const chartTitle = `Performance Trend (${selectedGpuType})`; const filterMode = clipOffscreen ? "filter" as const : "none" as const; diff --git a/kernelboard/api/leaderboard.py b/kernelboard/api/leaderboard.py index ecec172..b464a8d 100644 --- a/kernelboard/api/leaderboard.py +++ b/kernelboard/api/leaderboard.py @@ -418,6 +418,91 @@ def get_user_trend(leaderboard_id: int): }) +@leaderboard_bp.route("//fastest_trend", methods=["GET"]) +def get_fastest_trend(leaderboard_id: int): + """ + GET /leaderboard//fastest_trend + + Returns time series data showing the fastest submission across ALL users + over time for each GPU type. This creates a "world record" line showing + the best performance achieved at any point in time. + """ + total_start = time.perf_counter() + + conn = get_db_connection() + query_start = time.perf_counter() + + with conn.cursor() as cur: + sql = """ + SELECT + s.id AS submission_id, + s.user_id, + u.user_name, + s.file_name, + s.submission_time, + r.score, + r.runner AS gpu_type + FROM leaderboard.submission s + JOIN leaderboard.runs r ON r.submission_id = s.id + LEFT JOIN leaderboard.user_info u ON s.user_id = u.id + WHERE s.leaderboard_id = %s + AND r.score IS NOT NULL + AND r.passed = true + AND NOT r.secret + ORDER BY s.submission_time ASC + """ + cur.execute(sql, (leaderboard_id,)) + rows = cur.fetchall() + + query_time = (time.perf_counter() - query_start) * 1000 + + if not rows: + return http_success(data={ + "leaderboard_id": leaderboard_id, + "time_series": {}, + }) + + # Group by GPU type and compute running minimum + series_by_gpu = {} + running_min_by_gpu = {} + + for row in rows: + (submission_id, user_id, user_name, file_name, submission_time, + score, gpu_type) = row + + if not gpu_type or gpu_type == "unknown": + continue + + if gpu_type not in series_by_gpu: + series_by_gpu[gpu_type] = {"fastest": []} + running_min_by_gpu[gpu_type] = float("inf") + + # Only add a point if this submission beats the current record + if score < running_min_by_gpu[gpu_type]: + running_min_by_gpu[gpu_type] = score + series_by_gpu[gpu_type]["fastest"].append({ + "submission_time": ( + submission_time.isoformat() if submission_time else None + ), + "score": score, + "user_id": str(user_id) if user_id else None, + "user_name": user_name or str(user_id) if user_id else "Unknown", + "gpu_type": gpu_type, + "submission_id": submission_id, + }) + + total_time = (time.perf_counter() - total_start) * 1000 + logger.info( + "[Perf] fastest_trend leaderboard_id=%s | query=%.2fms | total=%.2fms", + leaderboard_id, query_time, total_time, + ) + + return http_success(data={ + "leaderboard_id": leaderboard_id, + "time_series": series_by_gpu, + }) + + @leaderboard_bp.route("//users", methods=["GET"]) def search_users(leaderboard_id: int): """