diff --git a/packages/app/src/app/api/v1/benchmarks/route.test.ts b/packages/app/src/app/api/v1/benchmarks/route.test.ts index 6970358..4b7e574 100644 --- a/packages/app/src/app/api/v1/benchmarks/route.test.ts +++ b/packages/app/src/app/api/v1/benchmarks/route.test.ts @@ -52,7 +52,7 @@ describe('GET /api/v1/benchmarks', () => { expect(res.status).toBe(200); const body = await res.json(); expect(body).toEqual(mockRows); - expect(mockGetLatestBenchmarks).toHaveBeenCalledWith('mock-sql', 'dsr1', undefined); + expect(mockGetLatestBenchmarks).toHaveBeenCalledWith('mock-sql', 'dsr1', undefined, undefined); }); it('passes date param to query when provided', async () => { @@ -60,7 +60,22 @@ describe('GET /api/v1/benchmarks', () => { const res = await GET(req('/api/v1/benchmarks?model=DeepSeek-R1-0528&date=2026-03-01')); expect(res.status).toBe(200); - expect(mockGetLatestBenchmarks).toHaveBeenCalledWith('mock-sql', 'dsr1', '2026-03-01'); + expect(mockGetLatestBenchmarks).toHaveBeenCalledWith( + 'mock-sql', + 'dsr1', + '2026-03-01', + undefined, + ); + }); + + it('passes exact=true when query param set', async () => { + mockGetLatestBenchmarks.mockResolvedValueOnce([]); + + const res = await GET( + req('/api/v1/benchmarks?model=DeepSeek-R1-0528&date=2026-03-01&exact=true'), + ); + expect(res.status).toBe(200); + expect(mockGetLatestBenchmarks).toHaveBeenCalledWith('mock-sql', 'dsr1', '2026-03-01', true); }); it('returns 500 when query throws', async () => { diff --git a/packages/app/src/app/api/v1/benchmarks/route.ts b/packages/app/src/app/api/v1/benchmarks/route.ts index 50e5569..fb65640 100644 --- a/packages/app/src/app/api/v1/benchmarks/route.ts +++ b/packages/app/src/app/api/v1/benchmarks/route.ts @@ -9,9 +9,9 @@ import { cachedJson, cachedQuery } from '@/lib/api-cache'; export const dynamic = 'force-dynamic'; const getCachedBenchmarks = cachedQuery( - async (dbModelKey: string, date?: string) => { + async (dbModelKey: string, date?: string, exact?: boolean) => { const sql = getDb(); - return getLatestBenchmarks(sql, dbModelKey, date); + return getLatestBenchmarks(sql, dbModelKey, date, exact); }, 'benchmarks', { blobOnly: true }, @@ -21,13 +21,14 @@ export async function GET(request: NextRequest) { const params = request.nextUrl.searchParams; const model = params.get('model') ?? ''; const date = params.get('date') ?? undefined; + const exact = params.get('exact') === 'true'; const dbModelKey = DISPLAY_MODEL_TO_DB[model]; if (!dbModelKey) { return NextResponse.json({ error: 'Unknown model' }, { status: 400 }); } try { - const rows = await getCachedBenchmarks(dbModelKey, date); + const rows = await getCachedBenchmarks(dbModelKey, date, exact || undefined); return cachedJson(rows); } catch (error) { console.error('Error fetching benchmarks:', error); diff --git a/packages/app/src/components/inference/hooks/useChartData.ts b/packages/app/src/components/inference/hooks/useChartData.ts index 3c8854f..16d7717 100644 --- a/packages/app/src/components/inference/hooks/useChartData.ts +++ b/packages/app/src/components/inference/hooks/useChartData.ts @@ -100,7 +100,9 @@ export function useChartData( }, [selectedGPUs, selectedDates, selectedDateRange, selectedRunDate]); const comparisonQueries = useQueries({ - queries: comparisonDates.map((date) => benchmarkQueryOptions(selectedModel, date, enabled)), + queries: comparisonDates.map((date) => + benchmarkQueryOptions(selectedModel, date, enabled, true), + ), }); const comparisonLoading = comparisonQueries.some((q) => q.isLoading); diff --git a/packages/app/src/components/inference/ui/ChartControls.tsx b/packages/app/src/components/inference/ui/ChartControls.tsx index b027627..6726020 100644 --- a/packages/app/src/components/inference/ui/ChartControls.tsx +++ b/packages/app/src/components/inference/ui/ChartControls.tsx @@ -11,6 +11,7 @@ import { } from '@/components/ui/chart-selectors'; import { DateRangePicker } from '@/components/ui/date-range-picker'; import { LabelWithTooltip } from '@/components/ui/label-with-tooltip'; +import { MultiDatePicker } from '@/components/ui/multi-date-picker'; import { MultiSelect } from '@/components/ui/multi-select'; import { Select, @@ -68,9 +69,14 @@ const GROUPED_Y_AXIS_OPTIONS = (() => { interface ChartControlsProps { /** Hide GPU Config selector and related date pickers (used by Historical Trends tab) */ hideGpuComparison?: boolean; + /** Intermediate dates within the comparison range that have changelog entries */ + intermediateDates?: string[]; } -export default function ChartControls({ hideGpuComparison = false }: ChartControlsProps) { +export default function ChartControls({ + hideGpuComparison = false, + intermediateDates = [], +}: ChartControlsProps) { const { selectedModel, setSelectedModel, @@ -86,6 +92,8 @@ export default function ChartControls({ hideGpuComparison = false }: ChartContro availableGPUs, selectedDateRange, setSelectedDateRange, + selectedDates, + setSelectedDates, dateRangeAvailableDates, isCheckingAvailableDates, availablePrecisions, @@ -327,6 +335,32 @@ export default function ChartControls({ hideGpuComparison = false }: ChartContro /> )} + + {!hideGpuComparison && + selectedGPUs.length > 0 && + selectedDateRange.startDate && + selectedDateRange.endDate && + intermediateDates.length > 0 && ( +
+ + { + setSelectedDates(value); + track('inference_intermediate_dates_selected', { + dates: value.join(','), + }); + }} + availableDates={intermediateDates} + maxDates={2} + placeholder="Select intermediate dates" + /> +
+ )} diff --git a/packages/app/src/components/inference/ui/ChartDisplay.tsx b/packages/app/src/components/inference/ui/ChartDisplay.tsx index e768ce8..7450790 100644 --- a/packages/app/src/components/inference/ui/ChartDisplay.tsx +++ b/packages/app/src/components/inference/ui/ChartDisplay.tsx @@ -1,7 +1,7 @@ 'use client'; import { track } from '@/lib/analytics'; import Link from 'next/link'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useMemo, useState } from 'react'; import { ChevronDown, X } from 'lucide-react'; import { useInference } from '@/components/inference/InferenceContext'; @@ -37,7 +37,6 @@ import { Sequence, } from '@/lib/data-mappings'; import { useComparisonChangelogs } from '@/hooks/api/use-comparison-changelogs'; -import { configKeyMatchesHwKey } from '@/components/inference/utils/changelogFormatters'; import { useTrendData } from '@/components/inference/hooks/useTrendData'; import ChartControls from './ChartControls'; @@ -113,8 +112,6 @@ export default function ChartDisplay() { selectedGPUs, selectedPrecisions, selectedDateRange, - setSelectedDates, - availableDates, dateRangeAvailableDates, selectedModel, selectedSequence, @@ -130,49 +127,10 @@ export default function ChartDisplay() { const { changelogs, + intermediateDates, loading: changelogsLoading, totalDatesQueried, - } = useComparisonChangelogs(selectedGPUs, selectedDateRange, availableDates); - - // Auto-populate intermediate dates from changelog dates so they appear on the GPU graph - const prevChangelogDatesRef = useRef(''); - useEffect(() => { - if (!selectedDateRange.startDate || !selectedDateRange.endDate || selectedGPUs.length === 0) - return; - const gpuDatesSet = new Set(dateRangeAvailableDates); - const precSet = new Set(selectedPrecisions); - const changelogDates = changelogs - .filter((c) => - c.entries.some((entry) => - entry.config_keys.some((key) => { - const precision = key.split('-')[1]; - return ( - precSet.has(precision) && selectedGPUs.some((gpu) => configKeyMatchesHwKey(key, gpu)) - ); - }), - ), - ) - .map((c) => c.date) - .filter( - (d) => - d !== selectedDateRange.startDate && - d !== selectedDateRange.endDate && - gpuDatesSet.has(d), - ) - .sort(); - const key = changelogDates.join(','); - if (key !== prevChangelogDatesRef.current) { - prevChangelogDatesRef.current = key; - setSelectedDates(changelogDates); - } - }, [ - changelogs, - selectedDateRange, - selectedGPUs, - selectedPrecisions, - setSelectedDates, - dateRangeAvailableDates, - ]); + } = useComparisonChangelogs(selectedGPUs, selectedDateRange, dateRangeAvailableDates); const { unofficialRunInfo, getOverlayData, isUnofficialRun } = useUnofficialRun(); @@ -705,21 +663,18 @@ export default function ChartDisplay() { - + {selectedGPUs.length === 0 && } - {selectedGPUs.length > 0 && - selectedDateRange.startDate && - selectedDateRange.endDate && ( - - )} + {selectedGPUs.length > 0 && ( + + )} diff --git a/packages/app/src/components/inference/ui/ComparisonChangelog.tsx b/packages/app/src/components/inference/ui/ComparisonChangelog.tsx index 440409b..882b1ee 100644 --- a/packages/app/src/components/inference/ui/ComparisonChangelog.tsx +++ b/packages/app/src/components/inference/ui/ComparisonChangelog.tsx @@ -17,7 +17,6 @@ interface ComparisonChangelogProps { changelogs: ComparisonChangelogType[]; selectedGPUs: string[]; selectedPrecisions: string[]; - selectedDateRange: { startDate: string; endDate: string }; loading?: boolean; totalDatesQueried: number; } @@ -26,11 +25,10 @@ export default function ComparisonChangelog({ changelogs, selectedGPUs, selectedPrecisions, - selectedDateRange, loading, totalDatesQueried, }: ComparisonChangelogProps) { - const [isExpanded, setIsExpanded] = useState(false); + const [isExpanded, setIsExpanded] = useState(true); // Filter changelog entries to only show those matching selected GPUs and precisions const filteredChangelogs = useMemo(() => { @@ -48,24 +46,10 @@ export default function ComparisonChangelog({ }), ), })) - .filter((item) => item.entries.length > 0); + .filter((item) => item.entries.length > 0) + .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); }, [changelogs, selectedGPUs, selectedPrecisions]); - // Always include start/end dates, even if they have no changelog entries - const sortedChangelogs = useMemo(() => { - const byDate = new Map(filteredChangelogs.map((c) => [c.date, c])); - const { startDate, endDate } = selectedDateRange; - if (startDate && !byDate.has(startDate)) { - byDate.set(startDate, { date: startDate, entries: [] }); - } - if (endDate && !byDate.has(endDate)) { - byDate.set(endDate, { date: endDate, entries: [] }); - } - return [...byDate.values()].sort( - (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(), - ); - }, [filteredChangelogs, selectedDateRange]); - const handleToggle = () => { const newState = !isExpanded; setIsExpanded(newState); @@ -104,13 +88,13 @@ export default function ComparisonChangelog({ }`} >
- {sortedChangelogs.length === 0 ? ( + {filteredChangelogs.length === 0 ? (

No config changelog data matching the selected GPUs and precisions for this date range. Changelog tracking began Dec 30, 2025.

) : ( - sortedChangelogs.map((item) => ( + filteredChangelogs.map((item) => (
{item.date} diff --git a/packages/app/src/components/inference/utils/tooltipUtils.ts b/packages/app/src/components/inference/utils/tooltipUtils.ts index 2a39cf1..b50fecd 100644 --- a/packages/app/src/components/inference/utils/tooltipUtils.ts +++ b/packages/app/src/components/inference/utils/tooltipUtils.ts @@ -235,7 +235,7 @@ export const generateGPUGraphTooltipContent = (config: TooltipConfig): string =>
${isPinned ? '
Click elsewhere to dismiss
' : ''}
- Date: ${d.actualDate ?? d.date} + Date: ${d.date}${d.actualDate && d.actualDate !== d.date ? ` (data from ${d.actualDate})` : ''}
GPU Config: ${hardwareConfig[d.hwKey] ? getDisplayLabel(hardwareConfig[d.hwKey]) : d.hwKey} diff --git a/packages/app/src/hooks/api/use-benchmarks.test.ts b/packages/app/src/hooks/api/use-benchmarks.test.ts index 250113e..7329896 100644 --- a/packages/app/src/hooks/api/use-benchmarks.test.ts +++ b/packages/app/src/hooks/api/use-benchmarks.test.ts @@ -5,7 +5,12 @@ import { benchmarkQueryOptions } from '@/hooks/api/use-benchmarks'; describe('benchmarkQueryOptions', () => { it('builds query key from model and date', () => { const opts = benchmarkQueryOptions('DeepSeek-R1-0528', '2026-03-01'); - expect(opts.queryKey).toEqual(['benchmarks', 'DeepSeek-R1-0528', '2026-03-01']); + expect(opts.queryKey).toEqual(['benchmarks', 'DeepSeek-R1-0528', '2026-03-01', 'latest']); + }); + + it('builds exact query key when exact=true', () => { + const opts = benchmarkQueryOptions('DeepSeek-R1-0528', '2026-03-01', true, true); + expect(opts.queryKey).toEqual(['benchmarks', 'DeepSeek-R1-0528', '2026-03-01', 'exact']); }); it('produces distinct keys for different models', () => { diff --git a/packages/app/src/hooks/api/use-benchmarks.ts b/packages/app/src/hooks/api/use-benchmarks.ts index 1556bde..c20eda8 100644 --- a/packages/app/src/hooks/api/use-benchmarks.ts +++ b/packages/app/src/hooks/api/use-benchmarks.ts @@ -3,10 +3,15 @@ import { useQuery, keepPreviousData } from '@tanstack/react-query'; import { fetchBenchmarks } from '@/lib/api'; /** Shared query options — reused by useQueries for comparison dates. */ -export function benchmarkQueryOptions(model: string, date: string, enabled: boolean = true) { +export function benchmarkQueryOptions( + model: string, + date: string, + enabled: boolean = true, + exact?: boolean, +) { return { - queryKey: ['benchmarks', model, date] as const, - queryFn: () => fetchBenchmarks(model, date), + queryKey: ['benchmarks', model, date, exact ? 'exact' : 'latest'] as const, + queryFn: () => fetchBenchmarks(model, date, exact), enabled: enabled && Boolean(model), }; } diff --git a/packages/app/src/hooks/api/use-comparison-changelogs.ts b/packages/app/src/hooks/api/use-comparison-changelogs.ts index 2262818..48aec9f 100644 --- a/packages/app/src/hooks/api/use-comparison-changelogs.ts +++ b/packages/app/src/hooks/api/use-comparison-changelogs.ts @@ -19,40 +19,46 @@ export function useComparisonChangelogs( selectedDateRange: { startDate: string; endDate: string }, availableDates: string[], ) { - const isComparisonMode = - selectedGPUs.length > 0 && !!selectedDateRange.startDate && !!selectedDateRange.endDate; + const hasGPUs = selectedGPUs.length > 0; + const hasDateRange = !!selectedDateRange.startDate && !!selectedDateRange.endDate; - // Query all available dates within the selected range - const datesInRange = useMemo(() => { - if (!isComparisonMode) return []; + // When GPUs selected: fetch all available dates. When date range also set: limit to range. + const datesToQuery = useMemo(() => { + if (!hasGPUs) return []; + if (!hasDateRange) return availableDates; return availableDates.filter( (d) => d >= selectedDateRange.startDate && d <= selectedDateRange.endDate, ); - }, [isComparisonMode, availableDates, selectedDateRange.startDate, selectedDateRange.endDate]); + }, [ + hasGPUs, + hasDateRange, + availableDates, + selectedDateRange.startDate, + selectedDateRange.endDate, + ]); const queries = useQueries({ - queries: datesInRange.map((date) => ({ + queries: datesToQuery.map((date) => ({ queryKey: ['workflow-info', date], queryFn: () => fetchWorkflowInfo(date), - enabled: isComparisonMode, + enabled: hasGPUs, })), }); const changelogs = useMemo(() => { - if (!isComparisonMode) return []; + if (!hasGPUs) return []; const results: ComparisonChangelog[] = []; - for (let i = 0; i < datesInRange.length; i++) { + for (let i = 0; i < datesToQuery.length; i++) { const query = queries[i]; if (!query.data) continue; const data = query.data as WorkflowInfoResponse; if (!data.changelogs || data.changelogs.length === 0) continue; - // Include all changelogs for this date (across all runs) results.push({ - date: datesInRange[i], + date: datesToQuery[i], headRef: data.changelogs[data.changelogs.length - 1]?.head_ref, runUrl: data.runs[data.runs.length - 1]?.html_url ?? undefined, entries: data.changelogs.map((c: ChangelogRow) => ({ @@ -64,9 +70,18 @@ export function useComparisonChangelogs( } return results; - }, [isComparisonMode, datesInRange, queries]); + }, [hasGPUs, datesToQuery, queries]); + + // Intermediate dates with any changelog entries (excluding start/end when date range is set) + const intermediateDates = useMemo(() => { + if (!hasGPUs || !hasDateRange) return []; + return changelogs + .filter((c) => c.date !== selectedDateRange.startDate && c.date !== selectedDateRange.endDate) + .map((c) => c.date) + .sort(); + }, [hasGPUs, hasDateRange, changelogs, selectedDateRange.startDate, selectedDateRange.endDate]); const loading = queries.some((q) => q.isLoading); - return { changelogs, loading, totalDatesQueried: datesInRange.length }; + return { changelogs, intermediateDates, loading, totalDatesQueried: datesToQuery.length }; } diff --git a/packages/app/src/lib/api.ts b/packages/app/src/lib/api.ts index 107b774..63efd0e 100644 --- a/packages/app/src/lib/api.ts +++ b/packages/app/src/lib/api.ts @@ -94,9 +94,10 @@ async function fetchJson(url: string): Promise { return res.json(); } -export function fetchBenchmarks(model: string, date?: string) { +export function fetchBenchmarks(model: string, date?: string, exact?: boolean) { const params = new URLSearchParams({ model }); if (date) params.set('date', date); + if (exact) params.set('exact', 'true'); return fetchJson(`/api/v1/benchmarks?${params}`); } diff --git a/packages/db/src/queries/benchmarks.ts b/packages/db/src/queries/benchmarks.ts index edbeca0..59673c7 100644 --- a/packages/db/src/queries/benchmarks.ts +++ b/packages/db/src/queries/benchmarks.ts @@ -38,9 +38,13 @@ export async function getLatestBenchmarks( sql: NeonClient, modelKey: string, date?: string, + exact?: boolean, ): Promise { if (date) { // Date-filtered: use base table with DISTINCT ON (the view only has the absolute latest) + // exact=true: only return data from this exact date (for GPU comparison) + // exact=false (default): return latest data as of this date (for main chart) + const dateFilter = exact ? sql`br.date = ${date}::date` : sql`br.date <= ${date}::date`; const rows = await sql` SELECT DISTINCT ON (br.config_id, br.conc, br.isl, br.osl) c.hardware, @@ -71,7 +75,7 @@ export async function getLatestBenchmarks( JOIN latest_workflow_runs wr ON wr.id = br.workflow_run_id WHERE c.model = ${modelKey} AND br.error IS NULL - AND br.date <= ${date}::date + AND ${dateFilter} ORDER BY br.config_id, br.conc, br.isl, br.osl, br.date DESC `; return rows as unknown as BenchmarkRow[];