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
45 changes: 45 additions & 0 deletions packages/app/cypress/e2e/ttft-x-axis-toggle.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const interceptDerivedMetrics = () => {
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,
},
]),
),
Expand Down Expand Up @@ -144,6 +146,10 @@ 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 @@ -165,6 +171,21 @@ describe('X-Axis Mode Toggle (inference chart)', () => {
cy.get('[data-testid="chart-figure"] h2').should('contain.text', 'Interactivity');
});

it('shows the selected percentile in the Interactivity axis label', () => {
// Explicitly select the mode — do not rely on the agentic default mode.
cy.get('[data-testid="x-axis-mode-interactivity"]').click();
cy.get('[data-testid="x-axis-mode-interactivity"]').should(
'have.attr',
'aria-selected',
'true',
);
// Agentic plots percentile fields (p90_intvty), so the axis label carries it.
cy.get('[data-testid="chart-figure"] svg').should(
'contain.text',
'P90 Interactivity (tok/s/user)',
);
});

it('defaults to parallelism labels without line labels for the agentic view', () => {
cy.get('#scatter-parallelism-labels').should('have.attr', 'data-state', 'checked');
cy.get('#scatter-point-labels').should('have.attr', 'data-state', 'checked');
Expand Down Expand Up @@ -195,6 +216,7 @@ describe('X-Axis Mode Toggle (inference chart)', () => {
cy.get('[data-testid="x-axis-mode-e2e"]').click();
cy.get('[data-testid="x-axis-mode-e2e"]').should('have.attr', 'aria-selected', 'true');
cy.get('[data-testid="chart-figure"] h2').should('contain.text', 'End-to-end Latency');
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', () => {
Expand Down Expand Up @@ -231,6 +253,11 @@ describe('X-Axis Mode Toggle (inference chart)', () => {
'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="chart-figure"] svg').should(
'contain.text',
'P75 Interactivity (tok/s/user)',
);
});
});

Expand All @@ -246,6 +273,9 @@ describe('Default scenario', () => {
});
cy.get('[data-testid="scenario-selector"]').should('contain.text', '8K / 1K');
cy.get('[data-testid="chart-figure"]').should('have.length.at.least', 1);
// Fixed-seq plots the mean field — no percentile prefix on the axis label.
cy.get('[data-testid="chart-figure"] svg').should('contain.text', 'Interactivity (tok/s/user)');
cy.get('[data-testid="chart-figure"] svg').should('not.contain.text', 'P90 Interactivity');
});
});

Expand Down Expand Up @@ -340,6 +370,9 @@ 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 All @@ -352,10 +385,22 @@ describe('X-Axis Mode Toggle — overlay path (finding #8 regression guard)', ()
});

it('shows overlay (unofficial-run) watermark SVG when an overlay is loaded', () => {
// Explicitly select Interactivity — do not rely on the agentic default mode.
cy.get('[data-testid="x-axis-mode-interactivity"]').click();
cy.get('[data-testid="x-axis-mode-interactivity"]').should(
'have.attr',
'aria-selected',
'true',
);
// The unofficial-run pattern watermark appears when isUnofficialRun is true.
cy.get('[data-testid="inference-chart-display"] svg pattern[id^="unofficial-pattern-"]').should(
'exist',
);
// Overlay shares the chartDefinition label — the percentile prefix applies here too.
cy.get('[data-testid="chart-figure"] svg').should(
'contain.text',
'P90 Interactivity (tok/s/user)',
);
});

it('switches to ttft x-axis mode and renders SVG with overlay points', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, it, expect } from 'vitest';

import {
applyAgenticPercentileToXLabel,
buildComparisonDates,
dedupeRowsToLatestPerConfig,
filterByGPU,
Expand Down Expand Up @@ -139,6 +140,54 @@ describe('filterByGPU', () => {
});
});

describe('applyAgenticPercentileToXLabel', () => {
it('prefixes the percentile when the interactivity label has no statistic word', () => {
expect(applyAgenticPercentileToXLabel('Interactivity (tok/s/user)', 'P90')).toBe(
'P90 Interactivity (tok/s/user)',
);
});

it('prefixes the percentile when the e2e latency label has no statistic word', () => {
expect(applyAgenticPercentileToXLabel('End-to-end Latency (s)', 'P90')).toBe(
'P90 End-to-end Latency (s)',
);
});

it('follows the selected percentile (p75)', () => {
expect(applyAgenticPercentileToXLabel('Interactivity (tok/s/user)', 'P75')).toBe(
'P75 Interactivity (tok/s/user)',
);
});

it('replaces an existing percentile prefix instead of doubling it', () => {
expect(applyAgenticPercentileToXLabel('P90 Time To First Token (s)', 'P75')).toBe(
'P75 Time To First Token (s)',
);
});

it('replaces Median/Mean statistic prefixes', () => {
expect(applyAgenticPercentileToXLabel('Median Time To First Token (s)', 'P90')).toBe(
'P90 Time To First Token (s)',
);
expect(applyAgenticPercentileToXLabel('Mean Interactivity (tok/s/user)', 'P75')).toBe(
'P75 Interactivity (tok/s/user)',
);
});

it('is a no-op when the label already carries the selected percentile', () => {
expect(applyAgenticPercentileToXLabel('P90 Interactivity (tok/s/user)', 'P90')).toBe(
'P90 Interactivity (tok/s/user)',
);
});

it('does not touch mid-label statistic words', () => {
// Only a LEADING statistic word is a prefix; words later in the label are content.
expect(applyAgenticPercentileToXLabel('Normalized E2E @ 400 output tokens (s)', 'P90')).toBe(
'P90 Normalized E2E @ 400 output tokens (s)',
);
});
});

describe('flipRooflineDirection', () => {
it('flips left/right while preserving upper/lower', () => {
expect(flipRooflineDirection('upper_left')).toBe('upper_right');
Expand Down
27 changes: 24 additions & 3 deletions packages/app/src/components/inference/hooks/useChartData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,25 @@ export function flipRooflineDirection(dir: RooflineDirection): RooflineDirection
return FLIP_MAP[dir];
}

// Statistic words that may already prefix an x-axis label (from chart config
// or the TTFT override label). Trailing whitespace is consumed so a replace
// never doubles the separator space.
const X_LABEL_STAT_PREFIX_RE = /^(?:Median|Mean|P75|P90|P95|P99(?:\.9)?)\b\s*/iu;

/**
* Agentic sequences plot percentile fields (e.g. `p90_intvty`, `p75_e2el`),
* so the x-axis label must carry the selected percentile. Replaces an
* existing leading statistic word (e.g. the TTFT override's "P90 Time To
* First Token (s)") or prefixes the percentile when the configured label has
* none (e.g. "Interactivity (tok/s/user)" → "P90 Interactivity (tok/s/user)").
* Only call for agentic sequences — fixed-seq labels must stay untouched.
*/
export function applyAgenticPercentileToXLabel(label: string, pctlWord: string): string {
return X_LABEL_STAT_PREFIX_RE.test(label)
? label.replace(X_LABEL_STAT_PREFIX_RE, `${pctlWord} `)
: `${pctlWord} ${label}`;
}

/** The dedup key fields a chart series is identified by. */
interface DedupeRow {
hardware: string;
Expand Down Expand Up @@ -409,13 +428,15 @@ export function useChartData(

// Agentic: relabel to the chosen percentile (the resolver already
// rewrote the field) — xAxisLabel still carries the raw chartDef
// prefix. The chart heading ("vs. <latency>") is also rewritten so the
// title above the plot reflects what's drawn.
// prefix (or none: the base Interactivity / E2E Latency config labels
// have no statistic word, so the percentile is prefixed). The chart
// heading ("vs. <latency>") is also rewritten so the title above the
// plot reflects what's drawn.
const headingKey = `${selectedYAxisMetric}_heading` as keyof ChartDefinition;
let chartHeading = (chartDef[headingKey] as string) || chartDef.heading;
if (isAgentic) {
const pctlWord = selectedPercentile.toUpperCase();
xAxisLabel = xAxisLabel.replace(/^(?:Median|Mean|P75|P90|P95|P99(?:\.9)?)\b/iu, pctlWord);
xAxisLabel = applyAgenticPercentileToXLabel(xAxisLabel, pctlWord);
chartHeading = chartHeading.replace(
/^(?<vsPrefix>vs\.\s+)(?:(?:Median|Mean|P75|P90|P95|P99(?:\.9)?)\s+)?/iu,
`$1${pctlWord} `,
Expand Down