From 51dde2b22e5c18c218ac682ee5d0e8af5d8baba2 Mon Sep 17 00:00:00 2001 From: Mark Saroufim Date: Wed, 4 Nov 2026 08:23:20 -0800 Subject: [PATCH] Speed up uncached leaderboard reads and page loading --- frontend/src/App.tsx | 81 +++++++++++-------- frontend/src/api/api.ts | 1 + .../src/components/app-layout/NavBar.test.tsx | 56 ++++++------- frontend/src/components/app-layout/NavBar.tsx | 6 +- .../src/components/codeblock/CodeBlock.tsx | 6 +- frontend/src/components/codeblock/syntax.ts | 17 ++++ .../markdown-renderer/MarkdownRenderer.tsx | 5 +- frontend/src/pages/home/Home.tsx | 11 ++- .../src/pages/leaderboard/Leaderboard.tsx | 81 +++++-------------- .../leaderboard/components/UserTrendChart.tsx | 16 +++- kernelboard/__init__.py | 29 ++++++- kernelboard/api/leaderboard.py | 73 ++++++++--------- requirements.txt | 1 + tests/api/test_leaderboard_api.py | 14 ++++ tests/test_compression.py | 37 +++++++++ tests/test_hacked_submission_filters.py | 2 +- 16 files changed, 260 insertions(+), 176 deletions(-) create mode 100644 frontend/src/components/codeblock/syntax.ts create mode 100644 tests/test_compression.py diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e9175d79..d355a412 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,19 +5,25 @@ import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; import AppLayout from "./components/app-layout/AppLayout"; import { CssBaseline, ThemeProvider } from "@mui/material"; import { createAppTheme } from "./components/common/styles/theme"; -import Leaderboard from "./pages/leaderboard/Leaderboard"; -import LeaderboardEditor from "./pages/leaderboard/LeaderboardEditor"; import Home from "./pages/home/Home"; -import News from "./pages/news/News"; -import WorkingGroups from "./pages/working-groups/WorkingGroups"; -import Lectures from "./pages/lectures/Lectures"; -import Live from "./pages/live/Live"; import ErrorPage from "./pages/Error"; -import Login from "./pages/login/login"; -import Privacy from "./pages/privacy/Privacy"; +import Loading from "./components/common/loading"; import { useAuthStore } from "./lib/store/authStore"; import { useThemeStore } from "./lib/store/themeStore"; -import { useEffect, useMemo } from "react"; +import { lazy, Suspense, useEffect, useMemo } from "react"; + +const Leaderboard = lazy(() => import("./pages/leaderboard/Leaderboard")); +const LeaderboardEditor = lazy( + () => import("./pages/leaderboard/LeaderboardEditor"), +); +const News = lazy(() => import("./pages/news/News")); +const WorkingGroups = lazy( + () => import("./pages/working-groups/WorkingGroups"), +); +const Lectures = lazy(() => import("./pages/lectures/Lectures")); +const Live = lazy(() => import("./pages/live/Live")); +const Login = lazy(() => import("./pages/login/login")); +const Privacy = lazy(() => import("./pages/privacy/Privacy")); const errorRoutes = [ { @@ -67,34 +73,39 @@ function App() { - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - {/* error handling page */} - {errorRoutes.map(({ path, code, title, description }) => ( + }> + + } /> + } /> + } /> - } + path="/leaderboard/:id/editor" + element={} /> - ))} - + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + {/* error handling page */} + {errorRoutes.map(({ path, code, title, description }) => ( + + } + /> + ))} + + diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index fcb88a7d..c7ef9b6f 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -29,6 +29,7 @@ export interface LeaderboardDetail { rankings: Record< string, Array<{ + user_id: string; file_name: string; prev_score: number; rank: number; diff --git a/frontend/src/components/app-layout/NavBar.test.tsx b/frontend/src/components/app-layout/NavBar.test.tsx index a390d4bc..e95fdbee 100644 --- a/frontend/src/components/app-layout/NavBar.test.tsx +++ b/frontend/src/components/app-layout/NavBar.test.tsx @@ -1,37 +1,37 @@ -import { render, screen } from "@testing-library/react"; +import { fireEvent, screen } from "@testing-library/react"; import { describe, it, expect } from "vitest"; +import { useLocation } from "react-router-dom"; +import { renderWithRouter } from "../../tests/test-utils"; import NavBar from "./NavBar"; -describe("NavBar", () => { - it("renders Working Groups link with correct URL", () => { - render(); - - const workingGroupsLink = screen.getByRole("link", { - name: /working groups/i, - }); +function CurrentPath() { + return {useLocation().pathname}; +} - expect(workingGroupsLink).toBeInTheDocument(); - expect(workingGroupsLink).toHaveAttribute("href", "/working-groups"); - // Internal link should not have target="_blank" or rel="noopener" - expect(workingGroupsLink).not.toHaveAttribute("target", "_blank"); - expect(workingGroupsLink).not.toHaveAttribute("rel", "noopener"); +describe("NavBar", () => { + it("renders the site navigation links", () => { + renderWithRouter(); + for (const [name, href] of [ + ["News", "/news"], + ["Events", "/events"], + ["Projects", "/working-groups"], + ]) { + const link = screen.getByRole("link", { name }); + expect(link).toHaveAttribute("href", href); + expect(link).not.toHaveAttribute("target", "_blank"); + } }); - it("renders all expected navigation links in correct order", () => { - render(); - - const links = screen.getAllByRole("link"); - const navigationLinks = links.filter((link) => - ["News", "Working Groups", "Events", "Resources", "Docs"].includes( - link.textContent || "", - ), + it("navigates inside the app without reloading the document", () => { + renderWithRouter( + <> + + + , ); - - expect(navigationLinks).toHaveLength(5); - expect(navigationLinks[0]).toHaveTextContent("News"); - expect(navigationLinks[1]).toHaveTextContent("Working Groups"); - expect(navigationLinks[2]).toHaveTextContent("Events"); - expect(navigationLinks[3]).toHaveTextContent("Resources"); - expect(navigationLinks[4]).toHaveTextContent("Docs"); + fireEvent.click(screen.getByRole("link", { name: "News" })); + expect(screen.getByTestId("current-path")).toHaveTextContent("/news"); + fireEvent.click(screen.getByRole("link", { name: "GPU MODE" })); + expect(screen.getByTestId("current-path")).toHaveTextContent("/"); }); }); diff --git a/frontend/src/components/app-layout/NavBar.tsx b/frontend/src/components/app-layout/NavBar.tsx index 8f10edbf..b1f12c32 100644 --- a/frontend/src/components/app-layout/NavBar.tsx +++ b/frontend/src/components/app-layout/NavBar.tsx @@ -14,6 +14,7 @@ import { appBarStyle, brandStyle } from "./styles"; import { ConstrainedContainer } from "./ConstrainedContainer"; import NavUserProfile from "./NavUserProfile"; import { useThemeStore } from "../../lib/store/themeStore"; +import { Link as RouterLink } from "react-router-dom"; export interface NavLink { label: string; @@ -61,7 +62,7 @@ export default function NavBar() { const Brand = () => ( - + ( import("../../components/markdown-renderer/MarkdownRenderer")); + interface TopUser { rank: number; score: number; @@ -204,7 +205,11 @@ export default function Home() { > Submit Your First Kernel via cli tool! - + {isQuickStartOpen && ( + }> + + + )} diff --git a/frontend/src/pages/leaderboard/Leaderboard.tsx b/frontend/src/pages/leaderboard/Leaderboard.tsx index 699910e6..6ff8db1a 100644 --- a/frontend/src/pages/leaderboard/Leaderboard.tsx +++ b/frontend/src/pages/leaderboard/Leaderboard.tsx @@ -10,8 +10,8 @@ import { Button, } from "@mui/material"; import Grid from "@mui/material/Grid"; -import { memo, useCallback, useEffect, useState } from "react"; -import { fetchLeaderBoard, searchUsers } from "../../api/api"; +import { lazy, memo, Suspense, useCallback, useEffect, useMemo, useState } from "react"; +import { fetchLeaderBoard } from "../../api/api"; import { fetcherApiCallback } from "../../lib/hooks/useApi"; import { isExpired, toDateLocal } from "../../lib/date/utils"; import RankingsList from "./components/RankingLists"; @@ -23,7 +23,6 @@ import Loading from "../../components/common/loading"; import { ConstrainedContainer } from "../../components/app-layout/ConstrainedContainer"; import { useAuthStore } from "../../lib/store/authStore"; import SubmissionHistorySection from "./components/submission-history/SubmissionHistorySection"; -import UserTrendChart from "./components/UserTrendChart"; import { SubmissionSidebarProvider, useSubmissionSidebarState, @@ -41,6 +40,7 @@ import { } from "@mui/material"; import quickStartMarkdown from "../home/quick-start.md?raw"; +const UserTrendChart = lazy(() => import("./components/UserTrendChart")); const DEFAULT_SIDEBAR_WIDTH = 600; export const CardTitle = styled(Typography)(() => ({ @@ -89,12 +89,6 @@ const LeaderboardContent = memo(function LeaderboardContent() { const isAuthed = !!(me && me.authenticated); const userId = me?.user?.identity ?? null; - // State for top users (strongest submissions) and default GPU type - const [defaultUsers, setDefaultUsers] = useState< - Array<{ userId: string; username: string }> - >([]); - const [defaultGpuType, setDefaultGpuType] = useState(null); - // Sync tab state with query parameter const [searchParams, setSearchParams] = useSearchParams(); @@ -124,59 +118,22 @@ const LeaderboardContent = memo(function LeaderboardContent() { if (id) call(id); }, [id, call]); - // Fetch top users (strongest submissions) when rankings are available - // Select from the GPU with the most unique users - useEffect(() => { - const findTopUsers = async () => { - if (!id || !data?.rankings) return; - - const gpuTypes = Object.keys(data.rankings); - if (gpuTypes.length === 0) return; - - // Find the GPU type with the most unique users - const mostActiveGpu = gpuTypes.reduce((currentMax, gpuType) => { - const rankings = data.rankings[gpuType]; - const userCount = rankings ? rankings.length : 0; - const maxCount = data.rankings[currentMax]?.length ?? 0; - return userCount > maxCount ? gpuType : currentMax; - }, gpuTypes[0]); - - // Set the default GPU type to the one with most users - setDefaultGpuType(mostActiveGpu); - - const mostActiveGpuRankings = data.rankings[mostActiveGpu]; - if (!mostActiveGpuRankings || mostActiveGpuRankings.length === 0) return; - - // Get top 5 users (sorted by score ascending) - const topUserNames = mostActiveGpuRankings + // The ranking response already identifies users. Avoid five username-search + // requests (which can also pick the wrong account when names collide). + const { defaultUsers, defaultGpuType } = useMemo(() => { + const rankings = data?.rankings ?? {}; + const gpuTypes = Object.keys(rankings); + const gpuType = gpuTypes.reduce((best, gpu) => + best === null || rankings[gpu].length > rankings[best].length ? gpu : best, + null); + return { + defaultGpuType: gpuType, + defaultUsers: (gpuType ? rankings[gpuType] : []) .slice(0, 5) - .map((r) => r.user_name) - .filter(Boolean); - - if (topUserNames.length === 0) return; - - try { - // Search for each user by username to get their user_id - const userPromises = topUserNames.map((userName: string) => - searchUsers(id, userName, 1) - ); - const results = await Promise.all(userPromises); - - const foundUsers = results - .filter((result) => result.users && result.users.length > 0) - .map((result) => ({ - userId: result.users[0].user_id, - username: result.users[0].username, - })); - - setDefaultUsers(foundUsers); - } catch (err) { - console.error("Failed to fetch top users:", err); - } + .filter((entry) => entry.user_id && entry.user_name) + .map((entry) => ({ userId: entry.user_id, username: entry.user_name })), }; - - findTopUsers(); - }, [id, data?.rankings]); + }, [data?.rankings]); if (error) return ; if (loading || !data) return ; @@ -303,7 +260,9 @@ const LeaderboardContent = memo(function LeaderboardContent() { Performance Trend - + }> + + diff --git a/frontend/src/pages/leaderboard/components/UserTrendChart.tsx b/frontend/src/pages/leaderboard/components/UserTrendChart.tsx index 69328108..9c5522f6 100644 --- a/frontend/src/pages/leaderboard/components/UserTrendChart.tsx +++ b/frontend/src/pages/leaderboard/components/UserTrendChart.tsx @@ -1,5 +1,12 @@ import React, { useEffect, useState, useCallback, useRef, useMemo } from "react"; -import ReactECharts from "echarts-for-react"; +import ReactECharts from "echarts-for-react/lib/core"; +import * as echarts from "echarts/core"; +import { LineChart } from "echarts/charts"; +import { + DataZoomComponent, GridComponent, LegendComponent, TitleComponent, + ToolboxComponent, TooltipComponent, +} from "echarts/components"; +import { CanvasRenderer } from "echarts/renderers"; import { Box, Typography, @@ -37,6 +44,12 @@ import type { } from "./submissionTypes"; import { useSubmissionSidebarActions } from "./SubmissionSidebarContext"; +// Register only the chart features used here instead of the full ECharts bundle. +echarts.use([ + LineChart, DataZoomComponent, GridComponent, LegendComponent, + TitleComponent, ToolboxComponent, TooltipComponent, CanvasRenderer, +]); + // Display name prefix for custom (KernelAgent) entries const CUSTOM_ENTRY_PREFIX = "KernelAgent"; @@ -1165,6 +1178,7 @@ export default function UserTrendChart({ leaderboardId, defaultUsers, defaultGpu {renderSearchInput()} 'hacked' + AND NOT EXISTS ( + SELECT 1 FROM leaderboard.submission_job_status sjs + WHERE sjs.submission_id = s.id AND sjs.status = 'hacked' + ) ), - scoped_runs AS MATERIALIZED ( - SELECT r.* + + -- Total submission count per user per GPU type for this leaderboard. + submission_counts AS MATERIALIZED ( + SELECT s.user_id, r.runner, COUNT(*) AS submission_count FROM scoped_submissions s JOIN LATERAL ( - SELECT id, submission_id, runner, score, secret, passed - FROM leaderboard.runs + SELECT DISTINCT runner FROM leaderboard.runs WHERE submission_id = s.id OFFSET 0 ) r ON TRUE - ), - - -- Total submission count per user per GPU type for this leaderboard. - submission_counts AS MATERIALIZED ( - SELECT s.user_id, r.runner, COUNT(DISTINCT s.id) AS submission_count - FROM scoped_submissions s - JOIN scoped_runs r ON r.submission_id = s.id - WHERE s.leaderboard_id = %(leaderboard_id)s - AND s.status <> 'hacked' - AND NOT EXISTS ( - SELECT 1 - FROM leaderboard.submission_job_status sjs - WHERE sjs.submission_id = s.id - AND sjs.status = 'hacked' - ) GROUP BY s.user_id, r.runner ), @@ -278,6 +270,7 @@ def _get_query(): -- user's runs on that GPU type are ranked by score. ranked_runs AS ( SELECT r.runner AS runner, + s.user_id AS user_id, u.user_name AS user_name, r.score AS score, s.submission_time AS submission_time, @@ -288,22 +281,21 @@ def _get_query(): PARTITION BY r.runner, s.user_id ORDER BY r.score ASC, s.submission_time ASC, s.id ASC, r.id ASC ) AS rank - FROM scoped_runs r - JOIN scoped_submissions s ON r.submission_id = s.id + FROM scoped_submissions s + -- Read the covering public-score index, without fetching every + -- secret/failed run from the heap. Keep one candidate per GPU + -- and submission before ranking users. + JOIN LATERAL ( + SELECT DISTINCT ON (runner) id, submission_id, runner, score + FROM leaderboard.runs + WHERE submission_id = s.id + AND NOT secret AND score IS NOT NULL AND passed + ORDER BY runner, score, id + OFFSET 0 + ) r ON TRUE LEFT JOIN leaderboard.user_info u ON s.user_id = u.id LEFT JOIN submission_counts sc ON s.user_id = sc.user_id AND r.runner = sc.runner - WHERE NOT r.secret - AND r.score IS NOT NULL - AND r.passed - AND s.leaderboard_id = %(leaderboard_id)s - AND s.status <> 'hacked' - AND NOT EXISTS ( - SELECT 1 - FROM leaderboard.submission_job_status sjs - WHERE sjs.submission_id = s.id - AND sjs.status = 'hacked' - ) - AND EXISTS ( + WHERE EXISTS ( SELECT 1 FROM leaderboard.runs sr WHERE sr.submission_id = s.id @@ -311,6 +303,7 @@ def _get_query(): AND sr.runner = r.runner AND sr.mode = 'leaderboard' AND sr.passed + OFFSET 0 ) AND NOT EXISTS ( SELECT 1 @@ -319,6 +312,7 @@ def _get_query(): AND sr.secret AND sr.runner = r.runner AND sr.passed = FALSE + OFFSET 0 ) ), @@ -360,6 +354,7 @@ def _get_query(): 'rankings', (SELECT jsonb_object_agg(g.gpu_type, ( SELECT jsonb_agg( jsonb_build_object( + 'user_id', r.user_id, 'user_name', r.user_name, 'score', r.score, 'file_name', r.file_name, @@ -376,7 +371,7 @@ def _get_query(): ) ORDER BY r.score ASC ) - FROM top_runs r WHERE r.runner = g.gpu_type))), + FROM top_runs r WHERE r.runner = g.gpu_type)) FROM gpu_types g), 'leaderboard', (SELECT jsonb_build_object( 'name', name, @@ -388,7 +383,7 @@ def _get_query(): 'benchmarks', benchmarks, 'gpu_types', (SELECT jsonb_agg(gpu_type) FROM gpu_types) ) FROM leaderboard_info) - ) AS result FROM (SELECT gpu_type FROM gpu_types) g; + ) AS result WHERE EXISTS (SELECT 1 FROM gpu_types); """ return query diff --git a/requirements.txt b/requirements.txt index d6bfce0a..7ae236ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ click>=8.1.8,<8.2.0 coverage>=7.7.1,<7.8.0 dotenv>=0.9.9,<1.0.0 Flask>=3.1.0,<3.2.0 +Flask-Compress>=1.26,<2.0 Flask-Login>=0.6.3,<1.0.0 Flask-Session>=0.8.0,<1.0.0 flask-talisman>=1.1.0,<2.0.0 diff --git a/tests/api/test_leaderboard_api.py b/tests/api/test_leaderboard_api.py index c3edcaf3..e504a9f6 100644 --- a/tests/api/test_leaderboard_api.py +++ b/tests/api/test_leaderboard_api.py @@ -7,6 +7,20 @@ def test_leaderboard(client): assert b"conv2d" in response.data +def test_rankings_include_account_ids_for_trend_selection(client, app): + response = client.get("/api/leaderboard/339") + rankings = response.get_json()["data"]["rankings"] + with app.app_context(): + conn = get_db_connection() + with conn.cursor() as cur: + cur.execute("SELECT id, user_id FROM leaderboard.submission WHERE leaderboard_id = 339") + account_ids = dict(cur.fetchall()) + assert rankings + for entries in rankings.values(): + for entry in entries: + assert entry["user_id"] == account_ids[entry["submission_id"]] + + def test_concluded_leaderboard_includes_line_counts(client): response = client.get("/api/leaderboard/339") assert response.status_code == 200 diff --git a/tests/test_compression.py b/tests/test_compression.py new file mode 100644 index 00000000..5f4bd565 --- /dev/null +++ b/tests/test_compression.py @@ -0,0 +1,37 @@ +import gzip + +from flask import jsonify + + +def test_json_responses_are_compressed_without_changing_content(app, client): + payload = {"text": "kernelboard " * 500} + app.add_url_rule("/compression-test", view_func=lambda: jsonify(payload)) + + original = client.get("/compression-test") + compressed = client.get("/compression-test", headers={"Accept-Encoding": "gzip"}) + + assert compressed.status_code == 200 + assert compressed.headers["Content-Encoding"] == "gzip" + assert "Accept-Encoding" in compressed.headers["Vary"] + assert gzip.decompress(compressed.data) == original.data + assert len(compressed.data) < len(original.data) / 5 + + +def test_static_scripts_are_compressed_and_ranges_are_preserved(app, client, tmp_path): + assets = tmp_path / "app" / "assets" + assets.mkdir(parents=True) + script = b"console.log('kernelboard');\n" * 500 + (assets / "test.js").write_bytes(script) + app.static_folder = str(tmp_path) + + compressed = client.get("/assets/test.js", headers={"Accept-Encoding": "gzip"}) + assert compressed.headers["Content-Encoding"] == "gzip" + assert gzip.decompress(compressed.data) == script + + partial = client.get( + "/assets/test.js", + headers={"Accept-Encoding": "gzip", "Range": "bytes=0-9"}, + ) + assert partial.status_code == 206 + assert "Content-Encoding" not in partial.headers + assert partial.data == script[:10] diff --git a/tests/test_hacked_submission_filters.py b/tests/test_hacked_submission_filters.py index a03d73ff..5a1784dd 100644 --- a/tests/test_hacked_submission_filters.py +++ b/tests/test_hacked_submission_filters.py @@ -7,7 +7,7 @@ def test_leaderboard_queries_filter_hacked_submissions(): expected_counts = { - "kernelboard/api/leaderboard.py": 7, + "kernelboard/api/leaderboard.py": 6, "kernelboard/api/leaderboard_summaries.py": 2, "kernelboard/index.py": 1, "kernelboard/leaderboard.py": 1,