Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions frontend/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,37 @@ export async function fetchCustomTrend(leaderboardId: string): Promise<CustomTre
return r.data;
}

export interface FastestTrendDataPoint {
score: number;
submission_id: number;
submission_time: string;
gpu_type: string;
user_id: string | null;
user_name: string;
}

export interface FastestTrendTimeSeries {
[gpuType: string]: {
fastest: FastestTrendDataPoint[];
};
}

export interface FastestTrendResponse {
leaderboard_id: number;
time_series: FastestTrendTimeSeries;
}

export async function fetchFastestTrend(leaderboardId: string): Promise<FastestTrendResponse> {
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[];
Expand Down
135 changes: 113 additions & 22 deletions frontend/src/pages/leaderboard/components/UserTrendChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -89,11 +94,12 @@ function toDailyBestSeries<T extends { value: [number, number]; gpu_type?: strin
const startMidnight = new Date(Date.UTC(firstDate.getUTCFullYear(), firstDate.getUTCMonth(), firstDate.getUTCDate()));
const userEndMidnight = new Date(Date.UTC(userLastDate.getUTCFullYear(), userLastDate.getUTCMonth(), userLastDate.getUTCDate()));

// Build a map of timestamp -> 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
Expand All @@ -103,15 +109,18 @@ function toDailyBestSeries<T extends { value: [number, number]; gpu_type?: strin

// Process all submissions up to this day's end
while (submissionIndex < submissionsByTime.length && submissionsByTime[submissionIndex].time < dayEnd) {
runningMin = Math.min(runningMin, submissionsByTime[submissionIndex].score);
if (submissionsByTime[submissionIndex].score < runningMin) {
runningMin = submissionsByTime[submissionIndex].score;
currentBestPoint = submissionsByTime[submissionIndex].point; // Update the record holder
}
submissionIndex++;
}

// Only add a point if we have at least one submission by this day
if (runningMin !== Infinity) {
// Use the template from the first point for metadata
// Use the current best point as template to preserve metadata (like user_name)
result.push({
...sorted[0],
...currentBestPoint,
value: [currentDate.getTime(), runningMin] as [number, number],
});
}
Expand All @@ -125,7 +134,7 @@ function toDailyBestSeries<T extends { value: [number, number]; gpu_type?: strin
const globalEndMidnight = new Date(Date.UTC(globalEndDate.getUTCFullYear(), globalEndDate.getUTCMonth(), globalEndDate.getUTCDate()));
if (globalEndMidnight.getTime() > userEndMidnight.getTime()) {
result.push({
...sorted[0],
...currentBestPoint,
value: [globalEndMidnight.getTime(), runningMin] as [number, number],
});
}
Expand All @@ -137,6 +146,8 @@ function toDailyBestSeries<T extends { value: [number, number]; gpu_type?: strin
export default function UserTrendChart({ leaderboardId, defaultUsers, defaultGpuType, rankings }: UserTrendChartProps) {
const [data, setData] = useState<UserTrendResponse | null>(null);
const [customData, setCustomData] = useState<CustomTrendResponse | null>(null);
const [fastestTrendData, setFastestTrendData] = useState<FastestTrendResponse | null>(null);
const [showFastestTrend, setShowFastestTrend] = useState(true);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedGpuType, setSelectedGpuType] = useState<string>(defaultGpuType || "");
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -525,17 +551,30 @@ export default function UserTrendChart({ leaderboardId, defaultUsers, defaultGpu
{resetting ? "Loading..." : " Load Top 5 "}
</Button>
)}
<FormControlLabel
control={
<Switch
checked={clipOffscreen}
onChange={(e) => setClipOffscreen(e.target.checked)}
size="small"
/>
}
label="Clip offscreen"
sx={{ ml: 1 }}
/>
<Box sx={{ display: "flex", flexDirection: "column", ml: 1 }}>
<FormControlLabel
control={
<Switch
checked={clipOffscreen}
onChange={(e) => setClipOffscreen(e.target.checked)}
size="small"
/>
}
label="Clip offscreen"
slotProps={{ typography: { variant: "body2" } }}
/>
<FormControlLabel
control={
<Switch
checked={showFastestTrend}
onChange={(e) => setShowFastestTrend(e.target.checked)}
size="small"
/>
}
label="⚡ Fastest (All Users)"
slotProps={{ typography: { variant: "body2" } }}
/>
</Box>
<RadioGroup
value={displayMode}
onChange={(e) => setDisplayMode(e.target.value as "all" | "best")}
Expand All @@ -557,7 +596,7 @@ export default function UserTrendChart({ leaderboardId, defaultUsers, defaultGpu
</Box>
);

if (selectedUsers.length === 0 && selectedCustomEntries.length === 0) {
if (selectedUsers.length === 0 && selectedCustomEntries.length === 0 && !showFastestTrend) {
return (
<Box>
{renderSearchInput()}
Expand Down Expand Up @@ -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 (
<Box>
{renderSearchInput()}
Expand All @@ -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 (
<Box>
{renderSearchInput()}
Expand Down Expand Up @@ -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;
Expand Down
85 changes: 85 additions & 0 deletions kernelboard/api/leaderboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,91 @@ def get_user_trend(leaderboard_id: int):
})


@leaderboard_bp.route("/<int:leaderboard_id>/fastest_trend", methods=["GET"])
def get_fastest_trend(leaderboard_id: int):
"""
GET /leaderboard/<leaderboard_id>/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("/<int:leaderboard_id>/users", methods=["GET"])
def search_users(leaderboard_id: int):
"""
Expand Down
Loading