Skip to content

Commit 127259b

Browse files
committed
feat: add standalone grid performance test app
- Add tests/grid-perf-app/ standalone app for testing Grid performance without a Deephaven server, using MockGridModel - Support query params: rows, cols, a11y (to toggle accessibility layer) - Add grid-perf-app.spec.ts for accessibility layer performance comparison - Remove FPS threshold assertions - tests now just output results - Add npm run e2e:grid-performance script - Update README with Grid Performance Testing section
1 parent 232fb64 commit 127259b

12 files changed

Lines changed: 2014 additions & 118 deletions

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,48 @@ See [this guide](https://deephaven.io/core/docs/how-to-guides/authentication/aut
177177
- `npm run e2e:headed`: Runs end-to-end tests in headed debug mode. Also ignores snapshots since a test suite will stop once 1 snapshot comparison fails. Useful if you need to debug why a particular test isn't working. For example, to debug the `table.spec.ts` test directly, you could run `npm run e2e:headed -- ./tests/table.spec.ts`.
178178
- `npm run e2e:codegen`: Runs Playwright in codegen mode which can help with creating tests. See [Playwright Codegen](https://playwright.dev/docs/codegen/) for more details.
179179
- `npm run e2e:update-snapshots`: Updates the E2E snapshots for your local OS.
180+
- `npm run e2e:performance`: Runs grid performance benchmark tests against the main app (requires a Deephaven server). Skipped by default in CI due to resource constraints.
181+
182+
### Grid Performance Testing
183+
184+
For performance-sensitive changes to the Grid component, there are two ways to benchmark:
185+
186+
**1. Main App Tests** (`grid-performance.spec.ts`)
187+
188+
Tests scroll performance with real table data from a Deephaven server:
189+
190+
```bash
191+
npm run e2e:performance
192+
```
193+
194+
**2. Standalone Perf App** (`grid-perf-app.spec.ts`)
195+
196+
A lightweight test app in `tests/grid-perf-app/` that uses mock data. This is useful for:
197+
198+
- Testing without a Deephaven server
199+
- Comparing performance with different Grid props (e.g., accessibility layer on/off)
200+
- Iterating on Grid changes quickly
201+
202+
To use the perf app:
203+
204+
```bash
205+
# Install dependencies (one time)
206+
cd tests/grid-perf-app && npm install
207+
208+
# Start the app
209+
npm run dev
210+
211+
# In another terminal (from the repo root), run the perf app tests
212+
npm run e2e:grid-performance
213+
```
214+
215+
The perf app supports query params to configure the grid:
216+
217+
- `rows`: Number of rows (default: 1000000)
218+
- `cols`: Number of columns (default: 100)
219+
- `a11y`: Enable accessibility layer (default: true, set to "false" to disable)
220+
221+
Example: `http://localhost:4020/?rows=100000&cols=50&a11y=false`
180222

181223
### Docker
182224

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@
5050
"e2e": "playwright test",
5151
"e2e:codegen": "playwright codegen http://localhost:4000",
5252
"e2e:headed": "playwright test --project=chromium --debug --ignore-snapshots",
53+
"e2e:performance": "RUN_PERF_TESTS=1 playwright test grid-performance.spec.ts",
54+
"e2e:grid-performance": "RUN_PERF_TESTS=1 playwright test grid-perf-app.spec.ts",
5355
"e2e:update-snapshots": "playwright test --update-snapshots=changed",
5456
"e2e:docker": "./tests/docker-scripts/run.sh web-ui-tests",
5557
"e2e:update-ci-snapshots": "./tests/docker-scripts/run.sh web-ui-update-snapshots"

tests/grid-perf-app.spec.ts

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
import { test, expect, type Page } from '@playwright/test';
2+
3+
/**
4+
* Grid Performance Tests using the standalone perf app.
5+
*
6+
* These tests use the grid-perf-app which provides a standalone Grid component
7+
* with MockGridModel data, allowing proper testing of Grid props without needing
8+
* a Deephaven server.
9+
*
10+
* Prerequisites:
11+
* 1. Install the perf app: cd tests/grid-perf-app && npm install
12+
* 2. Start the perf app: cd tests/grid-perf-app && npm run dev
13+
* 3. Run tests: RUN_PERF_TESTS=1 npx playwright test grid-perf-app.spec.ts
14+
*
15+
* The perf app supports query params:
16+
* - rows: Number of rows (default: 1000000)
17+
* - cols: Number of columns (default: 100)
18+
* - a11y: Enable accessibility layer (default: true, set to "false" to disable)
19+
*/
20+
21+
const PERF_APP_URL = 'http://localhost:4020';
22+
23+
interface FPSResult {
24+
fps: number;
25+
avgFrameTime: number;
26+
minFrameTime: number;
27+
maxFrameTime: number;
28+
frameCount: number;
29+
droppedFrames: number;
30+
}
31+
32+
async function startFPSMeasurement(page: Page): Promise<void> {
33+
await page.evaluate(() => {
34+
(window as any).__frameTimings = [];
35+
(window as any).__fpsRunning = true;
36+
let lastTime = performance.now();
37+
38+
function measureFrame() {
39+
if (!(window as any).__fpsRunning) return;
40+
41+
const now = performance.now();
42+
(window as any).__frameTimings.push(now - lastTime);
43+
lastTime = now;
44+
requestAnimationFrame(measureFrame);
45+
}
46+
requestAnimationFrame(measureFrame);
47+
});
48+
}
49+
50+
async function stopFPSMeasurement(page: Page): Promise<FPSResult> {
51+
const timings = await page.evaluate(() => {
52+
(window as any).__fpsRunning = false;
53+
return (window as any).__frameTimings as number[];
54+
});
55+
56+
const validTimings = timings.filter(t => t < 500 && t > 0);
57+
58+
if (validTimings.length === 0) {
59+
return {
60+
fps: 0,
61+
avgFrameTime: 0,
62+
minFrameTime: 0,
63+
maxFrameTime: 0,
64+
frameCount: 0,
65+
droppedFrames: 0,
66+
};
67+
}
68+
69+
const avgFrameTime =
70+
validTimings.reduce((a, b) => a + b, 0) / validTimings.length;
71+
const fps = 1000 / avgFrameTime;
72+
const minFrameTime = Math.min(...validTimings);
73+
const maxFrameTime = Math.max(...validTimings);
74+
const droppedFrames = validTimings.filter(t => t > 33).length;
75+
76+
return {
77+
fps,
78+
avgFrameTime,
79+
minFrameTime,
80+
maxFrameTime,
81+
frameCount: validTimings.length,
82+
droppedFrames,
83+
};
84+
}
85+
86+
/**
87+
* Scrolls the grid in the perf app using mouse wheel events
88+
*/
89+
async function scrollPerfAppGrid(
90+
page: Page,
91+
totalDelta: number
92+
): Promise<void> {
93+
const canvas = page.locator('canvas').first();
94+
const box = await canvas.boundingBox();
95+
if (!box) throw new Error('Grid canvas not found');
96+
97+
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
98+
99+
const scrollStep = 100;
100+
const direction = Math.sign(totalDelta);
101+
let remaining = Math.abs(totalDelta);
102+
103+
while (remaining > 0) {
104+
const step = Math.min(scrollStep, remaining);
105+
await page.mouse.wheel(0, step * direction);
106+
remaining -= step;
107+
await page.waitForTimeout(16);
108+
}
109+
}
110+
111+
function logResults(
112+
testName: string,
113+
result: FPSResult,
114+
expected: { minFps: number }
115+
): void {
116+
console.log(`\n${testName}:`);
117+
console.log(` Average FPS: ${result.fps.toFixed(1)}`);
118+
console.log(` Avg frame time: ${result.avgFrameTime.toFixed(2)}ms`);
119+
console.log(
120+
` Frame time range: ${result.minFrameTime.toFixed(
121+
2
122+
)}ms - ${result.maxFrameTime.toFixed(2)}ms`
123+
);
124+
console.log(` Total frames: ${result.frameCount}`);
125+
console.log(
126+
` Dropped frames (>33ms): ${result.droppedFrames} (${(
127+
(result.droppedFrames / result.frameCount) *
128+
100
129+
).toFixed(1)}%)`
130+
);
131+
console.log(` Expected min FPS: ${expected.minFps}`);
132+
}
133+
134+
test.describe('grid perf app - accessibility layer comparison', () => {
135+
test.skip(
136+
!process.env.RUN_PERF_TESTS,
137+
'Performance tests skipped. Set RUN_PERF_TESTS=1 to run.'
138+
);
139+
140+
test.describe.configure({ mode: 'serial' });
141+
142+
test('compare scroll performance: accessibility layer ON vs OFF', async ({
143+
page,
144+
}) => {
145+
// --- TEST 1: WITH ACCESSIBILITY LAYER ---
146+
await page.goto(`${PERF_APP_URL}/?rows=1000000&cols=100&a11y=true`);
147+
await page.waitForSelector('canvas');
148+
149+
// Verify accessibility layer is present
150+
const layerCount = await page
151+
.locator('[data-testid="grid-accessibility-layer"]')
152+
.count();
153+
expect(layerCount).toBe(1);
154+
155+
await startFPSMeasurement(page);
156+
await scrollPerfAppGrid(page, 5000);
157+
await scrollPerfAppGrid(page, -3000);
158+
await scrollPerfAppGrid(page, 4000);
159+
await scrollPerfAppGrid(page, -5000);
160+
const withLayerResult = await stopFPSMeasurement(page);
161+
162+
// --- TEST 2: WITHOUT ACCESSIBILITY LAYER ---
163+
await page.goto(`${PERF_APP_URL}/?rows=1000000&cols=100&a11y=false`);
164+
await page.waitForSelector('canvas');
165+
166+
// Verify accessibility layer is NOT present
167+
const layerCountAfter = await page
168+
.locator('[data-testid="grid-accessibility-layer"]')
169+
.count();
170+
expect(layerCountAfter).toBe(0);
171+
172+
await startFPSMeasurement(page);
173+
await scrollPerfAppGrid(page, 5000);
174+
await scrollPerfAppGrid(page, -3000);
175+
await scrollPerfAppGrid(page, 4000);
176+
await scrollPerfAppGrid(page, -5000);
177+
const withoutLayerResult = await stopFPSMeasurement(page);
178+
179+
// --- COMPARISON REPORT ---
180+
console.log('\n========================================');
181+
console.log('ACCESSIBILITY LAYER PERFORMANCE COMPARISON');
182+
console.log('========================================\n');
183+
184+
logResults('WITH Accessibility Layer', withLayerResult, { minFps: 28 });
185+
logResults('WITHOUT Accessibility Layer', withoutLayerResult, {
186+
minFps: 28,
187+
});
188+
189+
const fpsDiff = withoutLayerResult.fps - withLayerResult.fps;
190+
const fpsPercentDiff = (fpsDiff / withoutLayerResult.fps) * 100;
191+
const frameTimeDiff =
192+
withLayerResult.avgFrameTime - withoutLayerResult.avgFrameTime;
193+
194+
console.log('\n--- COMPARISON SUMMARY ---');
195+
console.log(`FPS difference: ${fpsDiff.toFixed(2)} fps`);
196+
console.log(
197+
`Performance impact: ${fpsPercentDiff > 0 ? '-' : '+'}${Math.abs(
198+
fpsPercentDiff
199+
).toFixed(2)}%`
200+
);
201+
console.log(`Frame time overhead: ${frameTimeDiff.toFixed(3)}ms`);
202+
203+
if (Math.abs(fpsPercentDiff) < 5) {
204+
console.log('\n✓ Accessibility layer has NEGLIGIBLE performance impact');
205+
} else if (fpsPercentDiff > 0) {
206+
console.log(
207+
`\n⚠ Accessibility layer causes ${fpsPercentDiff.toFixed(
208+
1
209+
)}% performance decrease`
210+
);
211+
} else {
212+
console.log(
213+
`\n✓ Accessibility layer has no negative impact (${Math.abs(
214+
fpsPercentDiff
215+
).toFixed(1)}% faster)`
216+
);
217+
}
218+
});
219+
});
220+
221+
test.describe('grid perf app - stress tests', () => {
222+
test.skip(
223+
!process.env.RUN_PERF_TESTS,
224+
'Performance tests skipped. Set RUN_PERF_TESTS=1 to run.'
225+
);
226+
227+
test.describe.configure({ mode: 'serial' });
228+
229+
test('scroll performance - 1M rows', async ({ page }) => {
230+
await page.goto(`${PERF_APP_URL}/?rows=1000000&cols=100`);
231+
await page.waitForSelector('canvas');
232+
233+
await startFPSMeasurement(page);
234+
235+
await scrollPerfAppGrid(page, 5000);
236+
await scrollPerfAppGrid(page, -3000);
237+
await scrollPerfAppGrid(page, 4000);
238+
await scrollPerfAppGrid(page, -5000);
239+
240+
const result = await stopFPSMeasurement(page);
241+
logResults('1M Rows Scroll', result, { minFps: 30 });
242+
});
243+
244+
test('scroll performance - many columns', async ({ page }) => {
245+
await page.goto(`${PERF_APP_URL}/?rows=100000&cols=500`);
246+
await page.waitForSelector('canvas');
247+
248+
await startFPSMeasurement(page);
249+
250+
// Horizontal and vertical scrolling
251+
for (let i = 0; i < 20; i += 1) {
252+
await page.mouse.wheel(500, 500);
253+
await page.waitForTimeout(32);
254+
await page.mouse.wheel(-300, 300);
255+
await page.waitForTimeout(32);
256+
}
257+
258+
const result = await stopFPSMeasurement(page);
259+
logResults('500 Columns Scroll', result, { minFps: 28 });
260+
});
261+
262+
test('sustained scrolling - 3 seconds', async ({ page }) => {
263+
await page.goto(`${PERF_APP_URL}/?rows=1000000&cols=100`);
264+
await page.waitForSelector('canvas');
265+
266+
const canvas = page.locator('canvas').first();
267+
const box = await canvas.boundingBox();
268+
if (!box) throw new Error('Grid canvas not found');
269+
270+
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
271+
272+
await startFPSMeasurement(page);
273+
274+
const startTime = Date.now();
275+
const duration = 3000;
276+
let direction = 1;
277+
278+
while (Date.now() - startTime < duration) {
279+
await page.mouse.wheel(0, 300 * direction);
280+
await page.waitForTimeout(16);
281+
282+
if (Math.random() < 0.1) {
283+
direction *= -1;
284+
}
285+
}
286+
287+
const result = await stopFPSMeasurement(page);
288+
logResults('Sustained Scroll (3s)', result, { minFps: 30 });
289+
});
290+
});

tests/grid-perf-app/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules/
2+
dist/

tests/grid-perf-app/index.html

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Grid Performance Test</title>
7+
<style>
8+
* {
9+
box-sizing: border-box;
10+
}
11+
html,
12+
body,
13+
#root {
14+
margin: 0;
15+
padding: 0;
16+
width: 100%;
17+
height: 100%;
18+
overflow: hidden;
19+
}
20+
</style>
21+
</head>
22+
<body>
23+
<div id="root"></div>
24+
<script type="module" src="/src/main.tsx"></script>
25+
</body>
26+
</html>

0 commit comments

Comments
 (0)