Skip to content
Open
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ See [this guide](https://deephaven.io/core/docs/how-to-guides/authentication/aut
- `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`.
- `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.
- `npm run e2e:update-snapshots`: Updates the E2E snapshots for your local OS.
- `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.

For benchmarking performance-sensitive Grid changes, see [Grid Performance Testing](./tests/grid-perf-app/README.md).

### Docker

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
"e2e": "playwright test",
"e2e:codegen": "playwright codegen http://localhost:4000",
"e2e:headed": "playwright test --project=chromium --debug --ignore-snapshots",
"e2e:performance": "cross-env RUN_PERF_TESTS=1 playwright test grid-performance.spec.ts --workers=1",
"e2e:grid-performance": "cross-env RUN_PERF_TESTS=1 playwright test grid-perf-app.spec.ts --workers=1",
"e2e:update-snapshots": "playwright test --update-snapshots=changed",
"e2e:docker": "./tests/docker-scripts/run.sh web-ui-tests",
"e2e:update-ci-snapshots": "./tests/docker-scripts/run.sh web-ui-update-snapshots",
Expand Down
34 changes: 19 additions & 15 deletions tests/docker-scripts/data/app.d/common_tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from deephaven.column import string_col, double_col

size = 20
big_size = 1_000_000
scale = 999

simple_table = empty_table(100).update(["x=i", "y=Math.sin(i)", "z=Math.cos(i)"])
Expand All @@ -24,21 +25,24 @@
]
)

all_types = empty_table(size).update(
[
"String=(i%11==0 ? null : `a` + (int)(scale*(i%2==0? i+1 : 1)))",
"Int=(i%12==0 ? null : (int)(scale*(i*2-1)))",
"Long=(i%13==0 ? null : (long)(scale*(i*2-1)))",
"Float=(float)(i%14==0 ? null : i%10==0 ? 1.0F/0.0F: i%5==0 ? -1.0F/0.0F : (float) scale*(i*2-1))",
"Double=(double)(i%16==0 ? null : i%10==0 ? 1.0D/0.0D: i%5==0 ? -1.0D/0.0D : (double) scale*(i*2-1))",
"Bool = (i%17==0 ? null : (int)(i)%2==0)",
"Char = (i%18==0 ? null : new Character((char) (((26+i*i)%26)+97)) )",
"Short=(short)(i%19==0 ? null : (int)(scale*(i*2-1)))",
"BigDec=(i%21==0 ? null : new java.math.BigDecimal(scale*(i*2-1)))",
"BigInt=(i%22==0 ? null : new java.math.BigInteger(Integer.toString((int)(scale*(i*2-1)))))",
"Byte=(Byte)(i%19==0 ? null : new Byte( Integer.toString((int)(i))))",
]
)
all_types_cols = [
"String=(i%11==0 ? null : `a` + (int)(scale*(i%2==0? i+1 : 1)))",
"Int=(i%12==0 ? null : (int)(scale*(i*2-1)))",
"Long=(i%13==0 ? null : (long)(scale*(i*2-1)))",
"Float=(float)(i%14==0 ? null : i%10==0 ? 1.0F/0.0F: i%5==0 ? -1.0F/0.0F : (float) scale*(i*2-1))",
"Double=(double)(i%16==0 ? null : i%10==0 ? 1.0D/0.0D: i%5==0 ? -1.0D/0.0D : (double) scale*(i*2-1))",
"Bool = (i%17==0 ? null : (int)(i)%2==0)",
"Char = (i%18==0 ? null : new Character((char) (((26+i*i)%26)+97)) )",
"Short=(short)(i%19==0 ? null : (int)(scale*(i*2-1)))",
"BigDec=(i%21==0 ? null : new java.math.BigDecimal(scale*(i*2-1)))",
"BigInt=(i%22==0 ? null : new java.math.BigInteger(Integer.toString((int)(scale*(i*2-1)))))",
"Byte=(Byte)(i%19==0 ? null : new Byte( Integer.toString((int)(i))))",
]

all_types = empty_table(size).update_view(all_types_cols)

# Large variant for scroll performance benchmarks; update_view keeps it lazy
all_types_big = empty_table(big_size).update_view(all_types_cols)

ordered_int_and_offset = empty_table(20).update(
[
Expand Down
202 changes: 202 additions & 0 deletions tests/grid-perf-app.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { test, type Page } from '@playwright/test';

/**
* Grid Performance Tests using the standalone perf app.
*
* These tests use the grid-perf-app which provides a standalone Grid component
* with MockGridModel data, allowing proper testing of Grid props without needing
* a Deephaven server.
*
* Prerequisites:
* 1. Install the perf app: cd tests/grid-perf-app && npm install
* 2. Start the perf app: cd tests/grid-perf-app && npm run dev
* 3. Run tests: RUN_PERF_TESTS=1 npx playwright test grid-perf-app.spec.ts
*
* The perf app supports query params:
* - rows: Number of rows (default: 1000000)
* - cols: Number of columns (default: 100)
*/

const PERF_APP_URL = 'http://localhost:4020';

interface FPSResult {
fps: number;
avgFrameTime: number;
minFrameTime: number;
maxFrameTime: number;
frameCount: number;
droppedFrames: number;
}

async function startFPSMeasurement(page: Page): Promise<void> {
await page.evaluate(() => {
(window as any).__frameTimings = [];
(window as any).__fpsRunning = true;
let lastTime = performance.now();

function measureFrame() {
if (!(window as any).__fpsRunning) return;

const now = performance.now();
(window as any).__frameTimings.push(now - lastTime);
lastTime = now;
requestAnimationFrame(measureFrame);
}
requestAnimationFrame(measureFrame);
});
}

async function stopFPSMeasurement(page: Page): Promise<FPSResult> {
const timings = await page.evaluate(() => {
(window as any).__fpsRunning = false;
return (window as any).__frameTimings as number[];
});

const validTimings = timings.filter(t => t > 0);

if (validTimings.length === 0) {
return {
fps: 0,
avgFrameTime: 0,
minFrameTime: 0,
maxFrameTime: 0,
frameCount: 0,
droppedFrames: 0,
};
}

const avgFrameTime =
validTimings.reduce((a, b) => a + b, 0) / validTimings.length;
const fps = 1000 / avgFrameTime;
const minFrameTime = Math.min(...validTimings);
const maxFrameTime = Math.max(...validTimings);
const droppedFrames = validTimings.filter(t => t > 33).length;

return {
fps,
avgFrameTime,
minFrameTime,
maxFrameTime,
frameCount: validTimings.length,
droppedFrames,
};
}

/**
* Scrolls the grid in the perf app using mouse wheel events
*/
async function scrollPerfAppGrid(
page: Page,
totalDelta: number
): Promise<void> {
const canvas = page.locator('canvas').first();
const box = await canvas.boundingBox();
if (!box) throw new Error('Grid canvas not found');

await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);

const scrollStep = 100;
const direction = Math.sign(totalDelta);
let remaining = Math.abs(totalDelta);

while (remaining > 0) {
const step = Math.min(scrollStep, remaining);
await page.mouse.wheel(0, step * direction);
remaining -= step;
await page.waitForTimeout(16);
}
}

function logResults(
testName: string,
result: FPSResult,
expected: { minFps: number }
): void {
console.log(`\n${testName}:`);
console.log(` Average FPS: ${result.fps.toFixed(1)}`);
console.log(` Avg frame time: ${result.avgFrameTime.toFixed(2)}ms`);
console.log(
` Frame time range: ${result.minFrameTime.toFixed(
2
)}ms - ${result.maxFrameTime.toFixed(2)}ms`
);
console.log(` Total frames: ${result.frameCount}`);
console.log(
` Dropped frames (>33ms): ${result.droppedFrames} (${(
(result.droppedFrames / result.frameCount) *
100
).toFixed(1)}%)`
);
console.log(` Expected min FPS: ${expected.minFps}`);
}

test.describe('grid perf app - stress tests', () => {
test.skip(
!process.env.RUN_PERF_TESTS,
'Performance tests skipped. Set RUN_PERF_TESTS=1 to run.'
);

test.describe.configure({ mode: 'serial' });

test('scroll performance - 1M rows', async ({ page }) => {
await page.goto(`${PERF_APP_URL}/?rows=1000000&cols=100`);
await page.waitForSelector('canvas');

await startFPSMeasurement(page);

await scrollPerfAppGrid(page, 5000);
await scrollPerfAppGrid(page, -3000);
await scrollPerfAppGrid(page, 4000);
await scrollPerfAppGrid(page, -5000);

const result = await stopFPSMeasurement(page);
logResults('1M Rows Scroll', result, { minFps: 30 });
});

test('scroll performance - many columns', async ({ page }) => {
await page.goto(`${PERF_APP_URL}/?rows=100000&cols=500`);
await page.waitForSelector('canvas');

await startFPSMeasurement(page);

// Horizontal and vertical scrolling
for (let i = 0; i < 20; i += 1) {
await page.mouse.wheel(500, 500);
await page.waitForTimeout(32);
await page.mouse.wheel(-300, 300);
await page.waitForTimeout(32);
}

const result = await stopFPSMeasurement(page);
logResults('500 Columns Scroll', result, { minFps: 28 });
});

test('sustained scrolling - 3 seconds', async ({ page }) => {
await page.goto(`${PERF_APP_URL}/?rows=1000000&cols=100`);
await page.waitForSelector('canvas');

const canvas = page.locator('canvas').first();
const box = await canvas.boundingBox();
if (!box) throw new Error('Grid canvas not found');

await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);

await startFPSMeasurement(page);

const startTime = Date.now();
const duration = 3000;
let direction = 1;

while (Date.now() - startTime < duration) {
await page.mouse.wheel(0, 300 * direction);
await page.waitForTimeout(16);

if (Math.random() < 0.1) {
direction *= -1;
}
}

const result = await stopFPSMeasurement(page);
logResults('Sustained Scroll (3s)', result, { minFps: 30 });
});
});
2 changes: 2 additions & 0 deletions tests/grid-perf-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
dist/
37 changes: 37 additions & 0 deletions tests/grid-perf-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Grid Performance Testing

For performance-sensitive changes to the Grid component, there are two ways to benchmark. Both suites are skipped unless `RUN_PERF_TESTS` is set, since frame timings are too resource sensitive for CI.

## Main app tests

[`tests/grid-performance.spec.ts`](../grid-performance.spec.ts) measures scroll FPS in the main app with real table data, so it requires a Deephaven server and the usual [E2E setup](../../README.md#e2e-tests).

```bash
npm run e2e:performance
```

## Standalone perf app

[`tests/grid-perf-app.spec.ts`](../grid-perf-app.spec.ts) drives this app, a lightweight Vite app that renders a `Grid` backed by `MockGridModel`. It is useful for:

- Testing without a Deephaven server
- Benchmarking row and column counts the test data does not reach
- Iterating on Grid changes quickly

```bash
# Install dependencies (one time)
cd tests/grid-perf-app && npm install

# Start the app
npm run dev

# In another terminal (from the repo root), run the perf app tests
npm run e2e:grid-performance
```

The app supports query params to configure the grid:

- `rows`: Number of rows (default: 1000000)
- `cols`: Number of columns (default: 100)

Example: `http://localhost:4020/?rows=100000&cols=50`
26 changes: 26 additions & 0 deletions tests/grid-perf-app/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Grid Performance Test</title>
<style>
* {
box-sizing: border-box;
}
html,
body,
#root {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Loading
Loading