Skip to content

Commit bc30638

Browse files
authored
fix(inference): official legend X works with overlay loaded + explicit hide/show affordances / 修复叠加层下图例移除按钮并明确隐藏/恢复操作提示 (#597)
* fix(inference): make official legend X work while an unofficial overlay is loaded With an unofficial-run overlay active, the chart reads official series visibility from `localOfficialOverride` (the unified overlay-mode selection), but the legend's "X" (remove) button routed straight to InferenceContext's `removeHwType`, which mutates `activeHwTypes` — a set the chart ignores in overlay mode. Clicking an official SKU's X therefore appeared to do nothing, while the same click worked on official-only charts. The legend row TOGGLE already had the overlay-aware split (`unifiedToggle` → commitUnifiedSelection); the X now shares it: `handleRemoveHwType` commits the removal through the unified selection when an overlay is loaded and delegates to the context's `removeHwType` otherwise. Context state stays untouched in overlay mode, so dismissing the overlay restores the pre-overlay official selection — same semantics as the toggle path. Also lifts the overlay e2e fixtures (real run-29682242847 values, intercepts, countVisible) into cypress/support/overlay-fixtures.ts, shared by the existing overlay-optimal-only spec and the new overlay-legend-remove regression spec (which fails without the fix). Assertions in both specs now use retryable `.should()` callbacks — the dots' 150ms opacity transition made one-shot `.then()` counts racy. 中文:修复加载非官方运行叠加层时,图例中官方 SKU 的"X"(移除)按钮无效的问题。 叠加层激活时图表从 `localOfficialOverride`(叠加模式的统一选择集)读取官方系列 可见性,而图例的 X 按钮直接调用 InferenceContext 的 `removeHwType` 修改 `activeHwTypes`——该集合在叠加模式下被图表忽略,因此点击官方 SKU 的 X 看似无效, 而在纯官方图表上正常。图例行的切换(toggle)已有叠加感知分支(`unifiedToggle`), 现在 X 与其共用:加载叠加层时 `handleRemoveHwType` 通过统一选择集提交移除,否则 委托给 context 的 `removeHwType`;叠加模式下不触碰 context 状态,关闭叠加层即恢复 之前的官方选择,与切换路径语义一致。同时将叠加层 e2e 测试夹具(fixtures)提取到 cypress/support/overlay-fixtures.ts,由既有 overlay-optimal-only 与新增的 overlay-legend-remove 回归测试(未修复时失败)共享;两个测试的断言改用可重试的 `.should()` 回调(数据点 150ms 的透明度过渡使一次性 `.then()` 计数存在竞态)。 * feat(legend): explicit hide/show affordances on legend rows Make the two legend actions visually explicit: hovering an ACTIVE row swaps the color dot for the "Hide <label>" X (now with a matching tooltip), and hovering an INACTIVE row swaps the dimmed dot for a "+" with a "Show <label>" tooltip — signaling that clicking the SKU name brings the series back. The "+" is decorative (aria-hidden): the row's label/checkbox remains the single click target, so behavior is unchanged; only the affordance is new. Legend-item strings now go through a component-local locale dict (en/zh) per the site's bilingual-UI rule; the English strings are byte-identical to the previous literals. e2e assertions added: active row exposes the Hide tooltip, removed row exposes the Show "+" indicator and no Hide X. 中文:让图例行的两种操作在视觉上更明确:悬停"激活"行时,颜色圆点切换为"隐藏 <label>"的 X(新增对应 tooltip);悬停"未激活"行时,变暗的圆点切换为"+"并带 "显示 <label>" tooltip——明确提示点击 SKU 名称即可恢复该系列。"+" 为装饰性元素 (aria-hidden):行的 label/checkbox 仍是唯一点击目标,行为不变,仅新增视觉提示。 图例项字符串按站点双语规则改经组件内 locale 字典(en/zh)处理,英文字符串与原 字面量逐字节一致。新增 e2e 断言:激活行暴露隐藏 tooltip,被移除的行暴露"+"恢复 指示且无隐藏 X。
1 parent c40c77b commit bc30638

5 files changed

Lines changed: 263 additions & 114 deletions

File tree

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* Clicking an official SKU's legend "X" must remove that series even while an
3+
* unofficial-run overlay is loaded.
4+
*
5+
* Regression: with an overlay active, the chart reads official visibility from
6+
* `localOfficialOverride` (the unified overlay-mode selection), but the legend
7+
* X routed straight to InferenceContext's `removeHwType`, which mutates
8+
* `activeHwTypes` — a set the chart ignores in overlay mode. The click
9+
* appeared to do nothing. The legend toggle already had the overlay-aware
10+
* split (`unifiedToggle`); the X now shares it (`handleRemoveHwType`).
11+
*/
12+
import { unlockAgenticGate } from '../support/e2e';
13+
import {
14+
countVisible,
15+
interceptOverlayRun,
16+
OVERLAY_RUN_ID,
17+
REAL_CONFIGS,
18+
} from '../support/overlay-fixtures';
19+
20+
describe('Official legend X works while an unofficial overlay is loaded', () => {
21+
before(() => {
22+
interceptOverlayRun();
23+
cy.visit(`/inference?unofficialrun=${OVERLAY_RUN_ID}&i_seq=agentic-traces&i_pctl=p90`, {
24+
onBeforeLoad(win) {
25+
win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now()));
26+
unlockAgenticGate(win);
27+
},
28+
});
29+
cy.wait('@unofficialRun');
30+
cy.get('[data-testid="chart-figure"]').should('have.length.at.least', 1);
31+
cy.get('[data-testid="inference-chart-display"] svg .unofficial-overlay-pt').should(
32+
'have.length',
33+
REAL_CONFIGS.length,
34+
);
35+
});
36+
37+
it('shows official points and an official legend entry initially', () => {
38+
cy.get('[data-testid="inference-chart-display"] svg .dot-group').should(($dots) => {
39+
expect(countVisible($dots), 'visible official points').to.be.greaterThan(0);
40+
});
41+
cy.get('[data-testid="chart-legend"]').contains('B300').should('exist');
42+
// Active row: the hover affordance is the "Hide" X with an explicit tooltip.
43+
cy.get('[data-testid="chart-legend"] [role="button"][aria-label^="Hide"][aria-label*="B300"]')
44+
.should('have.attr', 'title')
45+
.and('match', /^Hide B300/u);
46+
});
47+
48+
it('clicking the official SKU X hides its points but keeps the overlay', () => {
49+
// The X only becomes opaque on row hover (CSS group-hover), which Cypress
50+
// events don't trigger — force the click on the always-present element.
51+
// Target the OFFICIAL row's X: the overlay run row is listed first and has
52+
// its own (no-op) X, so `.first()` would hit the wrong one. The official
53+
// label is "B300 (SGLang)" — case-sensitive match excludes the overlay
54+
// row's lowercase branch name.
55+
cy.get('[data-testid="chart-legend"] [role="button"][aria-label^="Hide"][aria-label*="B300"]')
56+
.first()
57+
.click({ force: true });
58+
59+
// Every official point belongs to the removed B300 series → all hidden.
60+
cy.get('[data-testid="inference-chart-display"] svg .dot-group').should(($dots) => {
61+
expect(countVisible($dots), 'visible official points after remove').to.eq(0);
62+
});
63+
// The overlay series is untouched (Optimal Only default keeps 4 of 5).
64+
cy.get('[data-testid="inference-chart-display"] svg .unofficial-overlay-pt').should(($pts) => {
65+
expect(countVisible($pts), 'visible overlay X markers').to.eq(REAL_CONFIGS.length - 1);
66+
});
67+
// Inactive row: the hover affordance flips to the "+" restore indicator
68+
// (explicit "clicking the name brings it back"), and the Hide X is gone.
69+
cy.get('[data-testid="chart-legend"] [title^="Show B300"]').should('exist');
70+
cy.get(
71+
'[data-testid="chart-legend"] [role="button"][aria-label^="Hide"][aria-label*="B300"]',
72+
).should('not.exist');
73+
});
74+
75+
it('re-activating the SKU from the legend restores the official points', () => {
76+
cy.get('[data-testid="chart-legend"]').contains('B300').click();
77+
cy.get('[data-testid="inference-chart-display"] svg .dot-group').should(($dots) => {
78+
expect(countVisible($dots), 'visible official points after re-add').to.be.greaterThan(0);
79+
});
80+
});
81+
});

packages/app/cypress/e2e/overlay-optimal-only.cy.ts

Lines changed: 18 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -2,116 +2,25 @@
22
* Overlay (unofficial run) points must respect the "Optimal Only" toggle the
33
* same way official points do.
44
*
5-
* Regression: with Optimal Only ON, official non-pareto points are hidden via
6-
* `isPointVisible`, but overlay X markers rendered every point unconditionally.
7-
* On the agentic interactivity chart this made an e2e-dominated config (TP8
8-
* C=4 in the GLM5.2 B300 hicache run) look like a pareto point: its X marker
9-
* stayed visible sitting on the dashed roofline (the monotone spline between
10-
* C=8 and C=2 passes within ~0.5% of it) while the official twin was hidden.
11-
*
12-
* Fixture values are the real run-29682242847 numbers:
13-
* conc, p90_intvty, tput_per_gpu, p90_e2el
14-
* C=4 is dominated on e2e by C=8 (12874 tok/s @ 33.1s vs 9415 @ 48.0s), so
15-
* with Optimal Only ON exactly 4 of the 5 overlay X's must stay visible.
5+
* Regression: with Optimal Only ON (the default — `i_optimal !== '0'`),
6+
* official non-pareto points are hidden via `isPointVisible`, but overlay X
7+
* markers rendered every point unconditionally. On the agentic interactivity
8+
* chart this made an e2e-dominated config (TP8 C=4 in the GLM5.2 B300 hicache
9+
* run) look like a pareto point: its X marker stayed visible sitting on the
10+
* dashed roofline (the monotone spline between C=8 and C=2 passes within
11+
* ~0.5% of it) while the official twin was hidden.
1612
*/
1713
import { unlockAgenticGate } from '../support/e2e';
18-
19-
const DEFAULT_MODEL_DB_KEY = 'dsv4';
20-
const AGENTIC_DATE = '2026-07-19';
21-
const OVERLAY_RUN_ID = '29682242847';
22-
const OVERLAY_RUN_URL = `https://github.com/SemiAnalysisAI/InferenceX/actions/runs/${OVERLAY_RUN_ID}`;
23-
24-
const REAL_CONFIGS: [number, number, number, number][] = [
25-
[48, 10.6, 17199, 126.9],
26-
[8, 68.5, 12874, 33.1],
27-
[4, 88.3, 9415, 48], // e2e-dominated by C=8 → NOT optimal
28-
[2, 111.1, 5018, 30],
29-
[1, 130.2, 2600, 25.8],
30-
];
31-
32-
const metricsFor = (intvty: number, tput: number, e2el: number): Record<string, number> => ({
33-
median_itl: 1 / (intvty * 1.2),
34-
p90_itl: 1 / intvty,
35-
p99_itl: 1 / (intvty * 0.8),
36-
median_e2el: e2el * 0.8,
37-
p90_e2el: e2el,
38-
p99_e2el: e2el * 1.3,
39-
median_ttft: 0.5,
40-
p90_ttft: 1,
41-
p99_ttft: 2,
42-
tput_per_gpu: tput,
43-
output_tput_per_gpu: tput * 0.3,
44-
input_tput_per_gpu: tput * 0.7,
45-
});
46-
47-
let idCursor = 900000;
48-
const b300Rows = (runUrl: string | null) =>
49-
REAL_CONFIGS.map(([conc, intvty, tput, e2el]) => ({
50-
id: runUrl ? 0 : idCursor++,
51-
hardware: 'b300',
52-
framework: 'sglang',
53-
model: DEFAULT_MODEL_DB_KEY,
54-
precision: 'fp4',
55-
spec_method: 'none',
56-
disagg: false,
57-
is_multinode: false,
58-
prefill_tp: 8,
59-
decode_tp: 8,
60-
num_prefill_gpu: 8,
61-
num_decode_gpu: 8,
62-
isl: null,
63-
osl: null,
64-
conc,
65-
offload_mode: 'on',
66-
benchmark_type: 'agentic_traces',
67-
image: 'sglang:test',
68-
metrics: metricsFor(intvty, tput, e2el),
69-
workers: null,
70-
date: AGENTIC_DATE,
71-
run_url: runUrl,
72-
}));
73-
74-
const availability = [
75-
{
76-
model: DEFAULT_MODEL_DB_KEY,
77-
isl: null,
78-
osl: null,
79-
precision: 'fp4',
80-
hardware: 'b300',
81-
framework: 'sglang',
82-
spec_method: 'none',
83-
disagg: false,
84-
benchmark_type: 'agentic_traces',
85-
date: AGENTIC_DATE,
86-
},
87-
];
88-
89-
const countVisible = ($els: JQuery<HTMLElement>): number =>
90-
[...$els].filter((el) => getComputedStyle(el).opacity !== '0').length;
14+
import {
15+
countVisible,
16+
interceptOverlayRun,
17+
OVERLAY_RUN_ID,
18+
REAL_CONFIGS,
19+
} from '../support/overlay-fixtures';
9120

9221
describe('Overlay points respect Optimal Only (agentic interactivity)', () => {
9322
before(() => {
94-
cy.intercept('GET', '/api/v1/availability', { body: availability }).as('availability');
95-
cy.intercept('GET', '/api/v1/benchmarks*', { body: b300Rows(null) }).as('benchmarks');
96-
cy.intercept('GET', '/api/unofficial-run*', {
97-
body: {
98-
runInfos: [
99-
{
100-
id: OVERLAY_RUN_ID,
101-
name: 'add-glm5.2-b300-agentic-hicache',
102-
branch: 'add-glm5.2-b300-agentic-hicache',
103-
sha: 'abc000',
104-
createdAt: `${AGENTIC_DATE}T00:00:00Z`,
105-
url: OVERLAY_RUN_URL,
106-
conclusion: 'success',
107-
status: 'completed',
108-
isNonMainBranch: true,
109-
},
110-
],
111-
benchmarks: b300Rows(OVERLAY_RUN_URL),
112-
evaluations: [],
113-
},
114-
}).as('unofficialRun');
23+
interceptOverlayRun();
11524
cy.visit(`/inference?unofficialrun=${OVERLAY_RUN_ID}&i_seq=agentic-traces&i_pctl=p90`, {
11625
onBeforeLoad(win) {
11726
win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now()));
@@ -136,27 +45,27 @@ describe('Overlay points respect Optimal Only (agentic interactivity)', () => {
13645
it('hides the e2e-dominated overlay point in the default Optimal Only view', () => {
13746
cy.get('#scatter-hide-non-optimal').should('have.attr', 'data-state', 'checked');
13847
// Official parity check: 4 of 5 official dots visible.
139-
cy.get('[data-testid="inference-chart-display"] svg .dot-group').then(($dots) => {
48+
cy.get('[data-testid="inference-chart-display"] svg .dot-group').should(($dots) => {
14049
expect(countVisible($dots), 'visible official points').to.eq(REAL_CONFIGS.length - 1);
14150
});
14251
// The overlay must hide its C=4 too — 4 of 5 X markers visible.
143-
cy.get('[data-testid="inference-chart-display"] svg .unofficial-overlay-pt').then(($pts) => {
52+
cy.get('[data-testid="inference-chart-display"] svg .unofficial-overlay-pt').should(($pts) => {
14453
expect(countVisible($pts), 'visible overlay X markers').to.eq(REAL_CONFIGS.length - 1);
14554
});
14655
});
14756

14857
it('shows all overlay points when Optimal Only is turned off', () => {
14958
cy.get('#scatter-hide-non-optimal').click();
15059
cy.get('#scatter-hide-non-optimal').should('have.attr', 'data-state', 'unchecked');
151-
cy.get('[data-testid="inference-chart-display"] svg .unofficial-overlay-pt').then(($pts) => {
60+
cy.get('[data-testid="inference-chart-display"] svg .unofficial-overlay-pt').should(($pts) => {
15261
expect(countVisible($pts), 'visible overlay X markers').to.eq(REAL_CONFIGS.length);
15362
});
15463
});
15564

15665
it('re-hides the e2e-dominated overlay point when Optimal Only is re-enabled', () => {
15766
cy.get('#scatter-hide-non-optimal').click();
15867
cy.get('#scatter-hide-non-optimal').should('have.attr', 'data-state', 'checked');
159-
cy.get('[data-testid="inference-chart-display"] svg .unofficial-overlay-pt').then(($pts) => {
68+
cy.get('[data-testid="inference-chart-display"] svg .unofficial-overlay-pt').should(($pts) => {
16069
expect(countVisible($pts), 'visible overlay X markers').to.eq(REAL_CONFIGS.length - 1);
16170
});
16271
});
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* Shared fixtures for `?unofficialrun=` overlay e2e specs.
3+
*
4+
* The benchmark values are the real numbers from GitHub run 29682242847
5+
* (GLM5.2 B300 agentic hicache, offload=on rows):
6+
* conc, p90_intvty (tok/s/user), tput_per_gpu, p90_e2el (s)
7+
* C=4 is dominated on e2e by C=8 (12874 tok/s @ 33.1s vs 9415 @ 48.0s), which
8+
* makes the set a ready-made probe for the e2e-restricted frontier behaviors.
9+
*/
10+
export const DEFAULT_MODEL_DB_KEY = 'dsv4';
11+
export const AGENTIC_DATE = '2026-07-19';
12+
export const OVERLAY_RUN_ID = '29682242847';
13+
export const OVERLAY_RUN_BRANCH = 'add-glm5.2-b300-agentic-hicache';
14+
export const OVERLAY_RUN_URL = `https://github.com/SemiAnalysisAI/InferenceX/actions/runs/${OVERLAY_RUN_ID}`;
15+
16+
export const REAL_CONFIGS: [number, number, number, number][] = [
17+
[48, 10.6, 17199, 126.9],
18+
[8, 68.5, 12874, 33.1],
19+
[4, 88.3, 9415, 48], // e2e-dominated by C=8 → NOT optimal
20+
[2, 111.1, 5018, 30],
21+
[1, 130.2, 2600, 25.8],
22+
];
23+
24+
export const metricsFor = (intvty: number, tput: number, e2el: number): Record<string, number> => ({
25+
// intvty is ALWAYS derived as 1/itl by the agentic aliases — feed itl.
26+
median_itl: 1 / (intvty * 1.2),
27+
p90_itl: 1 / intvty,
28+
p99_itl: 1 / (intvty * 0.8),
29+
median_e2el: e2el * 0.8,
30+
p90_e2el: e2el,
31+
p99_e2el: e2el * 1.3,
32+
median_ttft: 0.5,
33+
p90_ttft: 1,
34+
p99_ttft: 2,
35+
tput_per_gpu: tput,
36+
output_tput_per_gpu: tput * 0.3,
37+
input_tput_per_gpu: tput * 0.7,
38+
});
39+
40+
let idCursor = 900000;
41+
export const b300Rows = (runUrl: string | null) =>
42+
REAL_CONFIGS.map(([conc, intvty, tput, e2el]) => ({
43+
id: runUrl ? 0 : idCursor++,
44+
hardware: 'b300',
45+
framework: 'sglang',
46+
model: DEFAULT_MODEL_DB_KEY,
47+
precision: 'fp4',
48+
spec_method: 'none',
49+
disagg: false,
50+
is_multinode: false,
51+
prefill_tp: 8,
52+
decode_tp: 8,
53+
num_prefill_gpu: 8,
54+
num_decode_gpu: 8,
55+
isl: null,
56+
osl: null,
57+
conc,
58+
offload_mode: 'on',
59+
benchmark_type: 'agentic_traces',
60+
image: 'sglang:test',
61+
metrics: metricsFor(intvty, tput, e2el),
62+
workers: null,
63+
date: AGENTIC_DATE,
64+
run_url: runUrl,
65+
}));
66+
67+
export const availability = [
68+
{
69+
model: DEFAULT_MODEL_DB_KEY,
70+
isl: null,
71+
osl: null,
72+
precision: 'fp4',
73+
hardware: 'b300',
74+
framework: 'sglang',
75+
spec_method: 'none',
76+
disagg: false,
77+
benchmark_type: 'agentic_traces',
78+
date: AGENTIC_DATE,
79+
},
80+
];
81+
82+
/** Intercept availability + benchmarks + unofficial-run with the B300 fixture. */
83+
export const interceptOverlayRun = () => {
84+
cy.intercept('GET', '/api/v1/availability', { body: availability }).as('availability');
85+
cy.intercept('GET', '/api/v1/benchmarks*', { body: b300Rows(null) }).as('benchmarks');
86+
cy.intercept('GET', '/api/unofficial-run*', {
87+
body: {
88+
runInfos: [
89+
{
90+
id: OVERLAY_RUN_ID,
91+
name: OVERLAY_RUN_BRANCH,
92+
branch: OVERLAY_RUN_BRANCH,
93+
sha: 'abc000',
94+
createdAt: `${AGENTIC_DATE}T00:00:00Z`,
95+
url: OVERLAY_RUN_URL,
96+
conclusion: 'success',
97+
status: 'completed',
98+
isNonMainBranch: true,
99+
},
100+
],
101+
benchmarks: b300Rows(OVERLAY_RUN_URL),
102+
evaluations: [],
103+
},
104+
}).as('unofficialRun');
105+
};
106+
107+
export const countVisible = ($els: JQuery<HTMLElement>): number =>
108+
[...$els].filter((el) => getComputedStyle(el).opacity !== '0').length;

packages/app/src/components/inference/ui/ScatterGraph.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,26 @@ const ScatterGraph = React.memo(
587587
[overlayData, unifiedToggle, toggleHwType],
588588
);
589589

590+
// Legend "X" (remove) — same overlay split as handleToggleHwType. With an
591+
// overlay loaded the chart reads localOfficialOverride, which the context's
592+
// removeHwType (activeHwTypes) never touches, so routing the X through it
593+
// left the official series visibly un-removed. Commit the removal through
594+
// the unified selection instead; context state stays untouched so
595+
// dismissing the overlay restores the pre-overlay official selection, same
596+
// as the toggle path.
597+
const handleRemoveHwType = useCallback(
598+
(key: string) => {
599+
if (!overlayData) {
600+
removeHwType(key);
601+
return;
602+
}
603+
const next = new Set(resolvedUnifiedSelection);
604+
next.delete(key);
605+
commitUnifiedSelection(next);
606+
},
607+
[overlayData, removeHwType, resolvedUnifiedSelection, commitUnifiedSelection],
608+
);
609+
590610
// --- Theme ---
591611
const hardwareConfig = hardwareConfigOverride || contextHardwareConfig;
592612
const activeHwKeys = useMemo(() => {
@@ -2820,7 +2840,7 @@ const ScatterGraph = React.memo(
28202840
variant="sidebar"
28212841
onItemHover={handleLegendHover}
28222842
onItemHoverEnd={handleLegendHoverEnd}
2823-
onItemRemove={showAllHardwareTypes ? undefined : removeHwType}
2843+
onItemRemove={showAllHardwareTypes ? undefined : handleRemoveHwType}
28242844
legendItems={[
28252845
// Overlay legend: one entry per loaded unofficial run that actually
28262846
// contributes points to this chart. Colored from the shared palette

0 commit comments

Comments
 (0)