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
80 changes: 10 additions & 70 deletions packages/app/cypress/e2e/ttft-x-axis-toggle.cy.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,5 @@
import { unlockAgenticGate } from '../support/e2e';

const interceptDerivedMetrics = () => {
cy.intercept('GET', '/api/v1/derived-agentic-metrics*', (request) => {
const ids = new URL(request.url).searchParams.get('ids')?.split(',').filter(Boolean) ?? [];
request.reply({
body: Object.fromEntries(
ids.map((id, index) => [
id,
{
id: Number(id),
normalized_session_time_s: 60 + index,
p90_prefill_tps_per_user: 100 + index,
p75_normalized_e2e_400_s: 8 + index,
p90_normalized_e2e_400_s: 12 + index,
p75_osl_per_e2el: 40 + index,
p90_osl_per_e2el: 25 + index,
},
]),
),
});
}).as('derivedAgenticMetrics');
};

// This spec exercises the agentic x-axis modes, which only exist when the
// selected model resolves to the Agentic Traces scenario. The default e2e
// fixtures (cypress/fixtures/api/*.json) have NO agentic rows for any model, and
Expand Down Expand Up @@ -148,10 +126,6 @@ const interceptFixedSequenceData = () => {
describe('X-Axis Mode Toggle (inference chart)', () => {
before(() => {
interceptAgenticData();
// Stub derived metrics before the visit: if the agentic DEFAULT x-axis mode
// is (or becomes) a derived mode that fetches /derived-agentic-metrics on
// mount, the chart must not sit on its loading skeleton.
interceptDerivedMetrics();
cy.visit('/inference?i_seq=agentic-traces', {
onBeforeLoad(win) {
win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now()));
Expand All @@ -166,7 +140,6 @@ describe('X-Axis Mode Toggle (inference chart)', () => {
cy.get('[data-testid="scenario-selector"]').should('contain.text', 'Agentic Traces');
cy.get('[data-testid="x-axis-mode-ttft"]').should('be.visible');
cy.get('[data-testid="x-axis-mode-e2e"]').should('be.visible');
cy.get('[data-testid="x-axis-mode-normalized-e2e"]').should('be.visible');
cy.get('[data-testid="x-axis-mode-interactivity"]')
.should('be.visible')
.and('have.attr', 'aria-selected', 'true');
Expand Down Expand Up @@ -255,41 +228,26 @@ describe('X-Axis Mode Toggle (inference chart)', () => {
cy.get('[data-testid="chart-figure"] svg').should('contain.text', 'P90 End-to-end Latency (s)');
});

it('switches to request-level normalized E2E at 400 output tokens', () => {
Comment thread
cursor[bot] marked this conversation as resolved.
interceptDerivedMetrics();
cy.get('[data-testid="x-axis-mode-normalized-e2e"]').click();
cy.wait('@derivedAgenticMetrics');
cy.get('[data-testid="x-axis-mode-normalized-e2e"]').should(
it('switches back to Interactivity', () => {
cy.get('[data-testid="x-axis-mode-interactivity"]').click();
cy.get('[data-testid="x-axis-mode-interactivity"]').should(
'have.attr',
'aria-selected',
'true',
);
cy.get('[data-testid="chart-figure"] h2').should(
'contain.text',
'P90 Normalized E2E @ 400 output tokens',
);
cy.get('[data-testid="chart-figure"] h2').should('contain.text', 'Interactivity');
cy.get('[data-testid="chart-figure"] svg').should(
'contain.text',
'P90 Normalized E2E @ 400 output tokens (s)',
);

cy.get('[data-testid="percentile-selector"]').click();
cy.contains('[role="option"]', 'p75').click();
cy.get('[data-testid="chart-figure"] h2').should(
'contain.text',
'P75 Normalized E2E @ 400 output tokens',
'P90 Interactivity (tok/s/user)',
);
});

it('switches back to Interactivity', () => {
it('follows the percentile selector in the Interactivity axis label', () => {
// Select p75 here rather than inheriting it from another test — the axis
// label must track the selector on its own.
cy.get('[data-testid="x-axis-mode-interactivity"]').click();
cy.get('[data-testid="x-axis-mode-interactivity"]').should(
'have.attr',
'aria-selected',
'true',
);
cy.get('[data-testid="chart-figure"] h2').should('contain.text', 'Interactivity');
// Percentile was switched to p75 in the previous test — the axis label follows.
cy.get('[data-testid="percentile-selector"]').click();
cy.contains('[role="option"]', 'p75').click();
cy.get('[data-testid="chart-figure"] svg').should(
'contain.text',
'P75 Interactivity (tok/s/user)',
Expand Down Expand Up @@ -411,9 +369,6 @@ const interceptAgenticDataWithOverlay = () => {
describe('X-Axis Mode Toggle — overlay path (finding #8 regression guard)', () => {
before(() => {
interceptAgenticDataWithOverlay();
// Same as the main suite: keep the visit independent of the agentic default
// x-axis mode (a derived default fetches /derived-agentic-metrics on mount).
interceptDerivedMetrics();
cy.visit(`/inference?unofficialrun=${OVERLAY_RUN_ID}&i_seq=agentic-traces`, {
onBeforeLoad(win) {
win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now()));
Expand Down Expand Up @@ -462,19 +417,4 @@ describe('X-Axis Mode Toggle — overlay path (finding #8 regression guard)', ()
expect(total).to.be.greaterThan(0);
});
});

it('normalized-e2e mode shows suppression banner for unofficial-run overlays', () => {
interceptDerivedMetrics();
cy.get('[data-testid="x-axis-mode-normalized-e2e"]').click();
cy.get('[data-testid="x-axis-mode-normalized-e2e"]').should(
'have.attr',
'aria-selected',
'true',
);
// The suppression message appears because isUnofficialRun is true and the
// mode is 'normalized-e2e' (documented in ChartDisplay.tsx ~line 640).
cy.contains(
'Normalized E2E requires persisted per-request traces, so unofficial-run overlays are unavailable for this experimental view.',
).should('be.visible');
});
});
6 changes: 0 additions & 6 deletions packages/app/src/app/api/v1/agentic-cache-keys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,12 @@ import { STATS_VERSION } from '@semianalysisai/inferencex-db/queries/agentic-agg
import { REQUEST_TIMELINE_VERSION } from '@semianalysisai/inferencex-db/etl/compute-request-timeline';
import { TRACE_SERVER_METRICS_VERSION } from '@semianalysisai/inferencex-db/queries/trace-server-metrics';

import { CACHE_KEY_PREFIX as derivedAgenticMetricsKey } from './derived-agentic-metrics/route';
import { CACHE_KEY_PREFIX as agenticAggregatesKey } from './agentic-aggregates/route';
import { CACHE_KEY_PREFIX as requestTimelineKey } from './request-timeline/route';
import { CACHE_KEY_PREFIX as traceServerMetricsKey } from './trace-server-metrics/route';
import { CACHE_KEY_PREFIX as traceHistogramsKey } from './trace-histograms/route';

describe('agentic blob-cache keys are version-derived', () => {
it('derived-agentic-metrics key embeds STATS_VERSION', () => {
expect(derivedAgenticMetricsKey).toBe(`derived-agentic-metrics-v${STATS_VERSION}`);
});

it('agentic-aggregates key embeds STATS_VERSION', () => {
expect(agenticAggregatesKey).toBe(`agentic-aggregates-v${STATS_VERSION}`);
});
Expand All @@ -57,7 +52,6 @@ describe('agentic blob-cache keys are version-derived', () => {

it('every key actually contains a version segment (no unversioned literals)', () => {
for (const key of [
derivedAgenticMetricsKey,
agenticAggregatesKey,
requestTimelineKey,
traceServerMetricsKey,
Expand Down
51 changes: 0 additions & 51 deletions packages/app/src/app/api/v1/derived-agentic-metrics/route.ts

This file was deleted.

46 changes: 12 additions & 34 deletions packages/app/src/components/inference/InferenceContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,7 @@ import {
} from '@/lib/exclusion';
import { filterRunsByModel, getDisplayLabel } from '@/lib/utils';

import {
isAgenticOnlyXAxisMode,
useChartData,
X_AXIS_MODES,
type XAxisMode,
} from './hooks/useChartData';
import { useChartData, X_AXIS_MODES, type XAxisMode } from './hooks/useChartData';
import { resolveComparisonEntries } from './utils/comparisonEntry';
import {
comparisonDefaultGroup,
Expand Down Expand Up @@ -610,40 +605,23 @@ export function InferenceProvider({
}, [labelScenarioKind, sequenceResolved, getUrlParam]);

// Reconcile the x-axis mode with the scenario kind:
// - On mount with no `i_xmode` URL param: snap to the kind's natural default
// (interactivity for both agentic and fixed-sequence scenarios). The state was initialized
// to a SSR-stable constant so server and client render the same DOM; this
// effect fixes it up after hydration.
// - When the user later switches sequence kinds: snap to the new kind's
// natural default (the prior selection was for a different kind, so it
// doesn't carry over).
// - On mount with no `i_xmode` URL param: snap to the natural default
// (interactivity for both agentic and fixed-sequence scenarios). The state
// was initialized to a SSR-stable constant so server and client render the
// same DOM; this effect fixes it up after hydration.
// - When the user later switches sequence kinds: snap back to that default
// (the prior selection was for a different kind, so it doesn't carry over).
const lastSeqKindRef = useRef<ReturnType<typeof sequenceKind> | null>(null);
useEffect(() => {
const kind = sequenceKind(effectiveSequence);
const isInitialMount = lastSeqKindRef.current === null;
const isAgenticOnlyMode = isAgenticOnlyXAxisMode(selectedXAxisMode);
// On a stale render where kind hasn't changed, bail unless the current
// mode is agentic-only and we just landed on a fixed-seq scenario — in
// that case force the snap so the chart doesn't try to plot trace-derived
// metrics against rows that have no trace_replay.
if (!isInitialMount && lastSeqKindRef.current === kind) {
if (kind === 'fixed-seq' && isAgenticOnlyMode) {
handleSetXAxisMode('interactivity');
}
return;
}
// Stale render where the kind hasn't changed — nothing to reconcile.
if (!isInitialMount && lastSeqKindRef.current === kind) return;
lastSeqKindRef.current = kind;
if (
isInitialMount &&
xAxisModeFromUrlRef.current &&
!(kind === 'fixed-seq' && isAgenticOnlyMode)
) {
// URL-restored agentic-only mode on a fixed-seq sequence makes no sense
// — fall through to the default snap below.
return;
}
// A URL-restored mode wins on first mount; later kind switches reset it.
if (isInitialMount && xAxisModeFromUrlRef.current) return;
handleSetXAxisMode('interactivity');
}, [effectiveSequence, selectedXAxisMode, handleSetXAxisMode]);
}, [effectiveSequence, handleSetXAxisMode]);

// Reconcile selectedE2eXAxisMetric whenever the mode, sequence kind, or
// agentic percentile changes. For fixed-seq the JSONB only carries
Expand Down
39 changes: 8 additions & 31 deletions packages/app/src/components/inference/hooks/useChartData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,41 +39,18 @@ import {
/**
* Chart x-axis variant selected by the mode buttons above the plot. This is
* the single definition — InferenceContext (URL/state) and ChartDisplay
* (buttons, derived-metric remapping) import it from here.
* (buttons) import it from here.
*/
export type XAxisMode =
| 'ttft'
| 'e2e'
| 'normalized-e2e'
| 'interactivity'
| 'session-time'
| 'prefill-tps';

export const X_AXIS_MODES: readonly XAxisMode[] = [
'ttft',
'e2e',
'normalized-e2e',
'interactivity',
'session-time',
'prefill-tps',
];
export type XAxisMode = 'ttft' | 'e2e' | 'interactivity';

/**
* Modes whose x metric is derived from persisted per-request traces —
* these only exist for agentic scenarios (fixed-seq rows have no
* trace_replay blob to derive them from).
*/
export function isAgenticOnlyXAxisMode(mode: XAxisMode): boolean {
return mode === 'normalized-e2e' || mode === 'session-time' || mode === 'prefill-tps';
}
export const X_AXIS_MODES: readonly XAxisMode[] = ['ttft', 'e2e', 'interactivity'];

/**
* Compute the set of benchmark_results.id values that sit on the
* (e2e_latency, y) Pareto frontier within each (hwKey, precision, date)
* group. Used to restrict the non-e2e xmode charts (ttft, interactivity,
* session-time, prefill-tps) so they show *only* the points that win on
* end-to-end latency — preventing benchmark-hacking where a config tops
* one axis while tanking the other.
* group. Used to restrict the non-e2e xmode charts (ttft, interactivity) so
* they show *only* the points that win on end-to-end latency — preventing
* benchmark-hacking where a config tops one axis while tanking the other.
*
* Returns null when the y-metric has no roofline direction declared on
* the e2e chart (caller falls back to no filtering in that case).
Expand Down Expand Up @@ -220,7 +197,7 @@ export function useChartData(
/**
* Current x-axis mode. When set to anything other than 'e2e', the displayed
* data is filtered to the (e2e-latency, y) Pareto frontier so the ttft /
* interactivity / session-time / prefill-tps charts show only points that
* interactivity charts show only points that
* also win on end-to-end latency — preventing benchmark-hacking where a
* config tops one metric while tanking the other. The 'e2e' mode is the
* source of truth and keeps the full point set.
Expand Down Expand Up @@ -529,7 +506,7 @@ export function useChartData(
// non-optimal configs from view.
//
// Fixed-seq workloads keep the existing per-axis Pareto since
// there's no separate "session-time" notion of total latency —
// there's no separate session-level notion of total latency —
// their e2e IS the request latency, so a TTFT hack there reads
// honestly on e2e too. The anti-hack constraint is specifically
// about multi-turn agentic where TTFT measures a tiny fraction
Expand Down
17 changes: 3 additions & 14 deletions packages/app/src/components/inference/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ export interface InferenceData extends Partial<Omit<AggDataEntry, AggDataConflic
/**
* Whether this point sits on the (e2e_latency, y-metric) Pareto frontier.
* Set by useChartData when `selectedXAxisMode !== 'e2e'`. The TTFT /
* interactivity / session-time / prefill-tps charts use this flag to
* interactivity charts use this flag to
* restrict their roofline computation to e2e-Pareto winners — vendors
* can't benchmark-hack TTFT by tanking decode (or vice versa) and still
* appear on the frontier line — while keeping every point visible as
Expand Down Expand Up @@ -763,21 +763,10 @@ export interface InferenceChartContextType {
* at a time, picked by the big buttons above the chart.
* - 'ttft' → e2e chartType with x-axis forced to p90_ttft
* - 'e2e' → e2e chartType with the chart-config default x-axis (median_e2el / p90_e2el)
* - 'normalized-e2e'→ agentic-only; x = per-request E2E normalized to 400 output tokens
* - 'interactivity' → interactivity chartType (x = median_intvty / p90_intvty)
* - 'session-time' → agentic-only; x = mean-normalized session time (live-computed from trace blobs)
* - 'prefill-tps' → agentic-only; x = mean of P90 prefill TPS/user per session
*/
selectedXAxisMode:
| 'ttft'
| 'e2e'
| 'normalized-e2e'
| 'interactivity'
| 'session-time'
| 'prefill-tps';
setSelectedXAxisMode: (
mode: 'ttft' | 'e2e' | 'normalized-e2e' | 'interactivity' | 'session-time' | 'prefill-tps',
) => void;
selectedXAxisMode: 'ttft' | 'e2e' | 'interactivity';
setSelectedXAxisMode: (mode: 'ttft' | 'e2e' | 'interactivity') => void;
scaleType: 'auto' | 'linear' | 'log';
setScaleType: (type: 'auto' | 'linear' | 'log') => void;
/** Coarse vendor / framework / agg-disagg / mtp-stp filters applied to the chart point set. */
Expand Down
Loading