Skip to content

Commit 37f643a

Browse files
Merge pull request #115 from TheHiddenObserver/agent/benchmark-dashboard-optimization-audit
perf: optimize benchmark dashboard bundle and collapsed DOM
2 parents 553f6b7 + 3d141c6 commit 37f643a

10 files changed

Lines changed: 268 additions & 125 deletions

File tree

docs/assets/benchmarks/assets/index-SG8QC8nm.js

Lines changed: 41 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/assets/benchmarks/assets/index-WHX9O2yE.js

Lines changed: 0 additions & 62 deletions
This file was deleted.

docs/assets/benchmarks/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* { margin: 0; padding: 0; box-sizing: border-box; }
99
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; }
1010
</style>
11-
<script type="module" crossorigin src="./assets/index-WHX9O2yE.js"></script>
11+
<script type="module" crossorigin src="./assets/index-SG8QC8nm.js"></script>
1212
<link rel="stylesheet" crossorigin href="./assets/index-y_ksZDKc.css">
1313
</head>
1414
<body>
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { test, expect, type Locator } from '@playwright/test';
2+
3+
async function announcedRowCount(details: Locator): Promise<number> {
4+
const text = (await details.locator('summary').textContent()) ?? '';
5+
const match = text.match(/\((\d+) rows\)$/);
6+
expect(match, `expected row count in disclosure summary: ${text}`).not.toBeNull();
7+
return Number(match![1]);
8+
}
9+
10+
test.describe('Deployed benchmark chart runtime', () => {
11+
test('completes tree-shaken ECharts initialization without async runtime errors', async ({ page }) => {
12+
const consoleErrors: string[] = [];
13+
const pageErrors: string[] = [];
14+
page.on('console', message => {
15+
if (message.type() === 'error') consoleErrors.push(message.text());
16+
});
17+
page.on('pageerror', error => pageErrors.push(error.message));
18+
19+
await page.goto('./');
20+
await expect(page.locator('.header')).toBeVisible({ timeout: 15000 });
21+
22+
// Chart rendering is intentionally deferred with requestAnimationFrame in
23+
// main.ts. Wait for both renderers to enter the real setOption path and
24+
// produce their CanvasRenderer surfaces before checking async errors.
25+
await page.waitForFunction(() => {
26+
const timing = document.querySelector<HTMLElement>('#timing-chart');
27+
const speedup = document.querySelector<HTMLElement>('#speedup-chart');
28+
return Boolean(
29+
timing?.dataset.timingDisplayed !== undefined &&
30+
speedup?.dataset.speedupDisplayed !== undefined &&
31+
timing.querySelector('canvas') &&
32+
speedup.querySelector('canvas'),
33+
);
34+
});
35+
await page.evaluate(
36+
() => new Promise<void>(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))),
37+
);
38+
39+
const runtime = await page.evaluate(() => {
40+
const timing = document.querySelector<HTMLElement>('#timing-chart')!;
41+
const speedup = document.querySelector<HTMLElement>('#speedup-chart')!;
42+
const timingCanvas = timing.querySelector<HTMLCanvasElement>('canvas')!;
43+
const speedupCanvas = speedup.querySelector<HTMLCanvasElement>('canvas')!;
44+
return {
45+
timingDisplayed: Number(timing.dataset.timingDisplayed ?? 0),
46+
speedupDisplayed: Number(speedup.dataset.speedupDisplayed ?? 0),
47+
timingCanvas: [timingCanvas.width, timingCanvas.height],
48+
speedupCanvas: [speedupCanvas.width, speedupCanvas.height],
49+
};
50+
});
51+
52+
expect(runtime.timingDisplayed).toBeGreaterThan(0);
53+
expect(runtime.speedupDisplayed).toBeGreaterThan(0);
54+
expect(runtime.timingCanvas[0]).toBeGreaterThan(0);
55+
expect(runtime.timingCanvas[1]).toBeGreaterThan(0);
56+
expect(runtime.speedupCanvas[0]).toBeGreaterThan(0);
57+
expect(runtime.speedupCanvas[1]).toBeGreaterThan(0);
58+
expect(pageErrors).toEqual([]);
59+
expect(consoleErrors).toEqual([]);
60+
});
61+
62+
test('materializes exactly the announced chart rows and resets them on rerender', async ({ page }) => {
63+
await page.goto('./');
64+
await expect(page.locator('.header')).toBeVisible({ timeout: 15000 });
65+
66+
const timing = page.locator('#timing-chart-data');
67+
const speedup = page.locator('#speedup-chart-data');
68+
const focusedTimingRows = await announcedRowCount(timing);
69+
const focusedSpeedupRows = await announcedRowCount(speedup);
70+
71+
await expect(timing.locator('table')).toHaveCount(0);
72+
await expect(speedup.locator('table')).toHaveCount(0);
73+
await timing.locator('summary').click();
74+
await speedup.locator('summary').click();
75+
await expect(timing.locator('tbody tr')).toHaveCount(focusedTimingRows);
76+
await expect(speedup.locator('tbody tr')).toHaveCount(focusedSpeedupRows);
77+
78+
// A filter/view update replaces the dashboard main content. The newly
79+
// selected disclosure must start unmaterialized and derive rows from the
80+
// new selection rather than retaining the previous table DOM.
81+
await page.locator('[data-chart-view="full"]').click();
82+
await expect(timing.locator('table')).toHaveCount(0);
83+
await expect(speedup.locator('table')).toHaveCount(0);
84+
85+
const fullTimingRows = await announcedRowCount(timing);
86+
const fullSpeedupRows = await announcedRowCount(speedup);
87+
expect(fullTimingRows).toBeGreaterThanOrEqual(focusedTimingRows);
88+
expect(fullSpeedupRows).toBeGreaterThanOrEqual(focusedSpeedupRows);
89+
90+
await timing.locator('summary').click();
91+
await speedup.locator('summary').click();
92+
await expect(timing.locator('tbody tr')).toHaveCount(fullTimingRows);
93+
await expect(speedup.locator('tbody tr')).toHaveCount(fullSpeedupRows);
94+
});
95+
});

frontend/e2e-production/production.spec.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ test.describe('Deployed benchmark dashboard', () => {
8080
await expect(page.locator('input[name="backend"][value="all"]')).toBeChecked();
8181
});
8282

83-
test('exposes keyboard sorting, disclosure controls, skip link, and chart tables', async ({ page }) => {
83+
test('exposes keyboard sorting, disclosure controls, skip link, and lazy chart tables', async ({ page }) => {
8484
await openProduction(page);
8585
const skip = page.getByRole('link', { name: 'Skip to benchmark results' });
8686
await skip.focus();
@@ -97,10 +97,19 @@ test.describe('Deployed benchmark dashboard', () => {
9797

9898
await expect(page.locator('#timing-chart-data')).toBeVisible();
9999
await expect(page.locator('#speedup-chart-data')).toBeVisible();
100+
await expect(page.locator('#timing-chart-data table')).toHaveCount(0);
101+
await expect(page.locator('#speedup-chart-data table')).toHaveCount(0);
102+
100103
await page.locator('#timing-chart-data summary').click();
101104
await expect(page.locator('#timing-chart-data table')).toBeVisible();
105+
await expect(page.locator('#timing-chart-data tbody tr').first()).toBeVisible();
102106
await expect(page.locator('#timing-chart-data caption')).toContainText('Full labels');
103107

108+
await page.locator('#speedup-chart-data summary').click();
109+
await expect(page.locator('#speedup-chart-data table')).toBeVisible();
110+
await expect(page.locator('#speedup-chart-data tbody tr').first()).toBeVisible();
111+
await expect(page.locator('#speedup-chart-data caption')).toContainText('Full labels');
112+
104113
const selects = page.locator('select');
105114
for (let i = 0; i < await selects.count(); i += 1) {
106115
const select = selects.nth(i);
@@ -174,4 +183,4 @@ test.describe('Deployed benchmark dashboard', () => {
174183
expect(ratios.header).toBeGreaterThanOrEqual(4.5);
175184
expect(ratios.muted).toBeGreaterThanOrEqual(4.5);
176185
});
177-
});
186+
});

frontend/src/charts/SpeedupChart.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as echarts from 'echarts';
1+
import { echarts, type ECharts } from '../echarts';
22
import type { Run } from '../schema';
33
import type { AppState } from '../state';
44
import { formatModelName } from '../utils/format';
@@ -196,7 +196,7 @@ export function renderSpeedupChart(
196196
el: HTMLElement,
197197
runs: Run[],
198198
state: AppState,
199-
chartInstances: echarts.ECharts[],
199+
chartInstances: ECharts[],
200200
): void {
201201
let chart = echarts.getInstanceByDom(el);
202202
if (!chart) {

frontend/src/charts/TimingChart.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as echarts from 'echarts';
1+
import { echarts, type ECharts } from '../echarts';
22
import type { Run } from '../schema';
33
import type { AppState } from '../state';
44
import { CHART_STYLE, COLORS } from '../utils/theme';
@@ -153,7 +153,7 @@ export function renderTimingChart(
153153
el: HTMLElement,
154154
runs: Run[],
155155
state: AppState,
156-
chartInstances: echarts.ECharts[],
156+
chartInstances: ECharts[],
157157
): void {
158158
let chart = echarts.getInstanceByDom(el);
159159
if (!chart) {

frontend/src/components/ChartDataFallback.ts

Lines changed: 80 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -20,52 +20,70 @@ function identityCells(run: Run): string[] {
2020
];
2121
}
2222

23+
interface TableValue {
24+
value: string;
25+
reference: string;
26+
}
27+
2328
function renderTable(
2429
id: string,
2530
caption: string,
2631
valueLabel: string,
27-
rows: Array<{ run: Run; value: string; reference: string }>,
32+
runs: Run[],
33+
valueForRun: (run: Run) => TableValue,
2834
): HTMLElement {
29-
const details = h('details', { class: 'chart-data-details', id });
30-
details.appendChild(h('summary', {}, `${caption} (${rows.length} rows)`));
31-
const wrapper = h('div', { class: 'chart-data-table-wrap' });
32-
const table = h('table', { class: 'chart-data-table' });
33-
table.appendChild(
34-
h(
35-
'caption',
36-
{},
37-
`${caption}. Full labels are shown here when chart labels are truncated.`,
38-
),
39-
);
35+
const details = h('details', { class: 'chart-data-details', id }) as HTMLDetailsElement;
36+
details.appendChild(h('summary', {}, `${caption} (${runs.length} rows)`));
37+
38+
let materialized = false;
39+
const materialize = (): void => {
40+
if (materialized) return;
41+
materialized = true;
42+
43+
const wrapper = h('div', { class: 'chart-data-table-wrap' });
44+
const table = h('table', { class: 'chart-data-table' });
45+
table.appendChild(
46+
h(
47+
'caption',
48+
{},
49+
`${caption}. Full labels are shown here when chart labels are truncated.`,
50+
),
51+
);
4052

41-
const header = h('tr');
42-
for (const label of [
43-
'Model',
44-
'Variant',
45-
'Penalty',
46-
'Solver',
47-
'Backend / reference',
48-
'Scale',
49-
valueLabel,
50-
'Reference',
51-
]) {
52-
header.appendChild(h('th', { scope: 'col' }, label));
53-
}
54-
const thead = h('thead');
55-
thead.appendChild(header);
56-
table.appendChild(thead);
53+
const header = h('tr');
54+
for (const label of [
55+
'Model',
56+
'Variant',
57+
'Penalty',
58+
'Solver',
59+
'Backend / reference',
60+
'Scale',
61+
valueLabel,
62+
'Reference',
63+
]) {
64+
header.appendChild(h('th', { scope: 'col' }, label));
65+
}
66+
const thead = h('thead');
67+
thead.appendChild(header);
68+
table.appendChild(thead);
5769

58-
const tbody = h('tbody');
59-
for (const { run, value, reference } of rows) {
60-
const tr = h('tr');
61-
for (const cell of [...identityCells(run), value, reference]) {
62-
tr.appendChild(h('td', {}, cell));
70+
const tbody = h('tbody');
71+
for (const run of runs) {
72+
const { value, reference } = valueForRun(run);
73+
const tr = h('tr');
74+
for (const cell of [...identityCells(run), value, reference]) {
75+
tr.appendChild(h('td', {}, cell));
76+
}
77+
tbody.appendChild(tr);
6378
}
64-
tbody.appendChild(tr);
65-
}
66-
table.appendChild(tbody);
67-
wrapper.appendChild(table);
68-
details.appendChild(wrapper);
79+
table.appendChild(tbody);
80+
wrapper.appendChild(table);
81+
details.appendChild(wrapper);
82+
};
83+
84+
details.addEventListener('toggle', () => {
85+
if (details.open) materialize();
86+
});
6987
return details;
7088
}
7189

@@ -87,20 +105,32 @@ export function renderChartDataFallback(
87105
),
88106
);
89107

90-
const timing = selectTimingRuns(timingSourceRuns, state).runs.map((run) => ({
91-
run,
92-
value: `${run.metrics.timing!.fit_time_ms.toFixed(3)} ms`,
93-
reference: run.metrics.timing!.quality,
94-
}));
95-
const speedup = selectSpeedupRuns(speedupSourceRuns, state).runs.map((run) => ({
96-
run,
97-
value: `${run.metrics.speedup!.value.toFixed(3)}×`,
98-
reference: `${run.metrics.speedup!.reference_framework} (${run.metrics.speedup!.reported_semantics})`,
99-
}));
108+
const timingRuns = selectTimingRuns(timingSourceRuns, state).runs;
109+
const speedupRuns = selectSpeedupRuns(speedupSourceRuns, state).runs;
100110

101-
section.appendChild(renderTable('timing-chart-data', 'Fit Time chart data', 'Time', timing));
102111
section.appendChild(
103-
renderTable('speedup-chart-data', 'Speedup chart data', 'Speedup', speedup),
112+
renderTable(
113+
'timing-chart-data',
114+
'Fit Time chart data',
115+
'Time',
116+
timingRuns,
117+
run => ({
118+
value: `${run.metrics.timing!.fit_time_ms.toFixed(3)} ms`,
119+
reference: run.metrics.timing!.quality,
120+
}),
121+
),
122+
);
123+
section.appendChild(
124+
renderTable(
125+
'speedup-chart-data',
126+
'Speedup chart data',
127+
'Speedup',
128+
speedupRuns,
129+
run => ({
130+
value: `${run.metrics.speedup!.value.toFixed(3)}×`,
131+
reference: `${run.metrics.speedup!.reference_framework} (${run.metrics.speedup!.reported_semantics})`,
132+
}),
133+
),
104134
);
105135
return section;
106136
}

frontend/src/echarts.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/** Tree-shakeable ECharts surface used by the benchmark dashboard. */
2+
import * as echarts from 'echarts/core';
3+
import { BarChart } from 'echarts/charts';
4+
import {
5+
DataZoomComponent,
6+
GridComponent,
7+
LegendComponent,
8+
MarkLineComponent,
9+
TitleComponent,
10+
TooltipComponent,
11+
} from 'echarts/components';
12+
import { CanvasRenderer } from 'echarts/renderers';
13+
14+
echarts.use([
15+
BarChart,
16+
DataZoomComponent,
17+
GridComponent,
18+
LegendComponent,
19+
MarkLineComponent,
20+
TitleComponent,
21+
TooltipComponent,
22+
CanvasRenderer,
23+
]);
24+
25+
export { echarts };
26+
export type { ECharts } from 'echarts/core';

frontend/src/main.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import './style.css';
22
import './metric-scope.css';
33

4-
import * as echarts from 'echarts';
4+
import { echarts, type ECharts } from './echarts';
55
import type { BenchmarkData, ParseReport, Run, SourceInventory } from './schema';
66
import { fetchBenchmarkData, fetchParseReport, fetchSourceInventory, filterRuns } from './data';
77
import { createDefaultState } from './state';
@@ -28,7 +28,7 @@ let sourceInventory: SourceInventory | null = null;
2828
let state: AppState | null = null;
2929

3030
/** Track ECharts instances for cleanup before re-render */
31-
const chartInstances: echarts.ECharts[] = [];
31+
const chartInstances: ECharts[] = [];
3232

3333
// ---------------------------------------------------------------------------
3434
// Layout
@@ -219,13 +219,17 @@ async function init(): Promise<void> {
219219
root.appendChild(emptyStateMessage('Loading benchmark data...'));
220220

221221
try {
222-
data = await fetchBenchmarkData();
223-
state = createDefaultState(data.environments, data.runs);
224-
// Non-critical metadata — fetch in parallel, failure doesn't block dashboard
225-
[parseReport, sourceInventory] = await Promise.all([
222+
// Start all production assets together. Data is required; metadata remains
223+
// non-critical, but it should not add a serial network round trip.
224+
const [loadedData, loadedReport, loadedInventory] = await Promise.all([
225+
fetchBenchmarkData(),
226226
fetchParseReport().catch(() => null),
227227
fetchSourceInventory().catch(() => null),
228228
]);
229+
data = loadedData;
230+
parseReport = loadedReport;
231+
sourceInventory = loadedInventory;
232+
state = createDefaultState(data.environments, data.runs);
229233

230234
// Cross-validate generation_id: discard metadata that doesn't match data
231235
if (parseReport && parseReport.generation_id !== data.meta.generation_id) {

0 commit comments

Comments
 (0)