Skip to content
Open
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
68 changes: 68 additions & 0 deletions packages/app/cypress/e2e/reproduce-drawer.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* Tests for the Reproduce drawer — opens from the inference table row,
* scatter pinned tooltip, and GPU graph tooltip. Verifies drawer state is
* URL-safe (closing does not perturb chart zoom or query string).
*/
describe('Reproduce drawer', () => {
beforeEach(() => {
cy.window().then((win) => {
win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now()));
});
cy.visit('/inference');
cy.get('[data-testid="scatter-graph"]')
.first()
.find('svg .dot-group')
.should('have.length.greaterThan', 0);
});

it('opens from the inference table Reproduce button and shows the three tabs', () => {
cy.get('[data-testid="inference-table-view-btn"]').first().click();
cy.get('[data-testid="inference-results-table"]').should('be.visible');
cy.get('[data-testid="inference-table-reproduce-btn"]').first().click();

cy.get('[data-testid="reproduce-drawer"]').should('be.visible');
cy.contains('Reproduce this benchmark').should('be.visible');
cy.contains('button', 'Command').should('be.visible');
cy.contains('button', 'Config JSON').should('be.visible');
cy.contains('button', 'Environment').should('be.visible');
});

it('exposes a copy button on every tab', () => {
cy.get('[data-testid="inference-table-view-btn"]').first().click();
cy.get('[data-testid="inference-table-reproduce-btn"]').first().click();
cy.get('[data-testid="reproduce-drawer-copy"]').should('be.visible');
cy.contains('button', 'Config JSON').click();
cy.get('[data-testid="reproduce-drawer-copy"]').should('be.visible');
cy.contains('button', 'Environment').click();
cy.get('[data-testid="reproduce-drawer-copy"]').should('be.visible');
});

it('Esc closes the drawer without changing the URL hash', () => {
cy.get('[data-testid="inference-table-view-btn"]').first().click();
cy.url().then((before) => {
cy.get('[data-testid="inference-table-reproduce-btn"]').first().click();
cy.get('[data-testid="reproduce-drawer"]').should('be.visible');
cy.get('body').type('{esc}');
cy.get('[data-testid="reproduce-drawer"]').should('not.exist');
cy.url().should('eq', before);
});
});

it('renders correctly for an unofficial-run overlay row when one is loaded', () => {
// Re-visit with the overlay query param. We do NOT assert which row is
// rendered — we only assert the drawer can be opened from whatever points
// appear for the official path on top of the overlay. The wiring is the
// same code path: clicking a Reproduce control feeds the InferenceData
// through to the drawer regardless of where the row originated.
const candidateRunId = '15000000000';
cy.visit(`/inference?unofficialrun=${candidateRunId}`);
cy.get('[data-testid="scatter-graph"]')
.first()
.find('svg .dot-group')
.should('have.length.greaterThan', 0);
cy.get('[data-testid="inference-table-view-btn"]').first().click();
cy.get('[data-testid="inference-results-table"]').should('be.visible');
cy.get('[data-testid="inference-table-reproduce-btn"]').first().click();
cy.get('[data-testid="reproduce-drawer"]').should('be.visible');
});
});
3 changes: 3 additions & 0 deletions packages/app/cypress/support/mock-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,9 @@ export function createMockInferenceContext(
activePresetId: null,
setActivePresetId: namedStub('setActivePresetId'),
presetGuardRef: { current: false } as React.RefObject<boolean>,
reproducePoint: null,
openReproduceDrawer: namedStub('openReproduceDrawer'),
closeReproduceDrawer: namedStub('closeReproduceDrawer'),
...overrides,
};
}
Expand Down
23 changes: 23 additions & 0 deletions packages/app/src/components/inference/InferenceContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,23 @@ export function InferenceProvider({
// --- Tracked configs state ---
const [trackedConfigs, setTrackedConfigs] = useState<TrackedConfig[]>([]);

// --- Reproduce drawer state ---
// Local-only — we do NOT sync this to the URL because closing the drawer
// should not perturb chart zoom or share-link state.
const [reproducePoint, setReproducePoint] = useState<InferenceData | null>(null);
const openReproduceDrawer = useCallback((point: InferenceData, source: string) => {
setReproducePoint(point);
track('reproduce_drawer_open_clicked', {
source,
framework: point.framework,
hwKey: point.hwKey,
precision: point.precision,
tp: point.tp,
conc: point.conc,
});
}, []);
const closeReproduceDrawer = useCallback(() => setReproducePoint(null), []);

// --- Favorite presets state ---
const [pendingHwFilter, setPendingHwFilter] = useState<string[] | null>(null);
const [activePresetId, setActivePresetId] = useState<string | null>(null);
Expand Down Expand Up @@ -977,6 +994,9 @@ export function InferenceProvider({
activePresetId,
setActivePresetId,
presetGuardRef,
reproducePoint,
openReproduceDrawer,
closeReproduceDrawer,
}),
[
activeHwTypes,
Expand Down Expand Up @@ -1030,6 +1050,9 @@ export function InferenceProvider({
removeTrackedConfig,
clearTrackedConfigs,
activePresetId,
reproducePoint,
openReproduceDrawer,
closeReproduceDrawer,
],
);

Expand Down
6 changes: 6 additions & 0 deletions packages/app/src/components/inference/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,12 @@ export interface InferenceChartContextType {
activePresetId: string | null;
setActivePresetId: (id: string | null) => void;
presetGuardRef: React.RefObject<boolean>;
/** The point currently shown in the Reproduce drawer, or null when closed. */
reproducePoint: InferenceData | null;
/** Open the Reproduce drawer for a given chart point. */
openReproduceDrawer: (point: InferenceData, source: string) => void;
/** Close the Reproduce drawer. */
closeReproduceDrawer: () => void;
}
export interface CalculateUserCostsRequest {
model: string;
Expand Down
17 changes: 17 additions & 0 deletions packages/app/src/components/inference/ui/ChartDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,11 @@ import ComparisonChangelog from './ComparisonChangelog';
import CustomCosts from './CustomCosts';
import CustomPowers from './CustomPowers';
import GPUGraph from './GPUGraph';
import ReproduceDrawer from './ReproduceDrawer';
import TrendChart from './TrendChart';

import { sequenceToIslOsl } from '@semianalysisai/inferencex-constants';

const ModelArchitectureDiagram = dynamic(() => import('./ModelArchitectureDiagram'), {
ssr: false,
loading: () => <Skeleton className="h-40 w-full" />,
Expand Down Expand Up @@ -149,8 +152,15 @@ export default function ChartDisplay() {
activeHwTypes,
activeDates,
setSelectedE2eXAxisMetric,
reproducePoint,
closeReproduceDrawer,
} = useInference();

const reproduceSequence = useMemo(
() => (selectedSequence ? sequenceToIslOsl(selectedSequence) : null) ?? undefined,
[selectedSequence],
);

const {
changelogs,
loading: changelogsLoading,
Expand Down Expand Up @@ -684,6 +694,13 @@ export default function ChartDisplay() {
</div>
</DialogContent>
</Dialog>

<ReproduceDrawer
point={reproducePoint}
sequence={reproduceSequence}
model={selectedModel}
onClose={closeReproduceDrawer}
/>
</div>
);
}
13 changes: 13 additions & 0 deletions packages/app/src/components/inference/ui/GPUGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const GPUGraph = React.memo(
selectAllActiveDates,
showLineLabels,
setShowLineLabels,
openReproduceDrawer,
} = useInference();
const { resolvedTheme } = useTheme();
const chartRef = useRef<D3ChartHandle>(null);
Expand Down Expand Up @@ -692,6 +693,18 @@ const GPUGraph = React.memo(
sel.select('.visible-shape') as any,
getShapeKeyForPrecision(d.precision, selectedPrecisions),
),
onPointClick: (d: InferenceData) => {
const tooltipEl = chartRef.current?.getTooltipElement();
if (!tooltipEl) return;
const reproduceBtn = tooltipEl.querySelector('[data-action="reproduce"]');
if (!reproduceBtn) return;
reproduceBtn.addEventListener('click', (btnEvent) => {
btnEvent.stopPropagation();
openReproduceDrawer(d, 'gpu_graph_tooltip');
chartRef.current?.dismissTooltip();
chartRef.current?.hideTooltip();
});
},
attachToLayer: 1,
}}
onRender={(ctx: RenderContext) => {
Expand Down
32 changes: 31 additions & 1 deletion packages/app/src/components/inference/ui/InferenceTable.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
'use client';

import { useMemo } from 'react';
import { Wrench } from 'lucide-react';

import { useInference } from '@/components/inference/InferenceContext';
import type { ChartDefinition, InferenceData } from '@/components/inference/types';
import { type DataTableColumn, DataTable } from '@/components/ui/data-table';
import { track } from '@/lib/analytics';
import { getHardwareConfig } from '@/lib/constants';
import { getNestedYValue } from '@/lib/chart-utils';
import { type Precision, getPrecisionLabel } from '@/lib/data-mappings';
Expand All @@ -29,6 +32,7 @@ export default function InferenceTable({
chartDefinition,
selectedYAxisMetric,
}: InferenceTableProps) {
const { openReproduceDrawer } = useInference();
const yPath = chartDefinition[selectedYAxisMetric as keyof ChartDefinition] as string | undefined;
const yLabel = chartDefinition[`${selectedYAxisMetric}_label` as keyof ChartDefinition] as string;
const xLabel = chartDefinition.x_label;
Expand Down Expand Up @@ -110,8 +114,34 @@ export default function InferenceTable({
sortValue: (row) => row.median_intvty ?? 0,
className: 'tabular-nums',
},
{
header: '',
align: 'center',
cell: (row) => (
<button
type="button"
onClick={() => {
track('inference_table_reproduce_clicked', {
framework: row.framework,
hwKey: row.hwKey,
precision: row.precision,
tp: row.tp,
conc: row.conc,
});
openReproduceDrawer(row, 'inference_table');
}}
className="inline-flex items-center gap-1 rounded-md border border-border px-2 py-0.5 text-[11px] hover:bg-muted"
data-testid="inference-table-reproduce-btn"
aria-label="Reproduce this benchmark"
>
<Wrench className="size-3" aria-hidden="true" />
Reproduce
</button>
),
className: 'whitespace-nowrap',
},
],
[yPath, yLabel, xLabel],
[yPath, yLabel, xLabel, openReproduceDrawer],
);

return (
Expand Down
Loading
Loading