diff --git a/README.md b/README.md index 2095a82889..4f7d90867e 100644 --- a/README.md +++ b/README.md @@ -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/README.md). ### Docker diff --git a/package.json b/package.json index 8bb6dc5e61..497d7b224d 100644 --- a/package.json +++ b/package.json @@ -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 tests/grid-perf/grid-performance.spec.ts --workers=1", + "e2e:grid-performance": "cross-env RUN_PERF_TESTS=1 playwright test tests/grid-perf/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", diff --git a/tests/docker-scripts/data/app.d/common_tables.py b/tests/docker-scripts/data/app.d/common_tables.py index 6802386515..5cc4f52c88 100644 --- a/tests/docker-scripts/data/app.d/common_tables.py +++ b/tests/docker-scripts/data/app.d/common_tables.py @@ -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)"]) @@ -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 % 128))))", +] + +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( [ diff --git a/tests/grid-perf/README.md b/tests/grid-perf/README.md new file mode 100644 index 0000000000..7b3cbe6b60 --- /dev/null +++ b/tests/grid-perf/README.md @@ -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-perf/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). It covers the application mode tables plus console generated tables of ~500 and ~25,000 character string cells, which exercise text measurement and truncation. + +```bash +npm run e2e:performance +``` + +## Standalone perf app + +[`tests/grid-perf/grid-perf-app.spec.ts`](./grid-perf-app.spec.ts) drives [`app`](./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` diff --git a/tests/grid-perf/app/.gitignore b/tests/grid-perf/app/.gitignore new file mode 100644 index 0000000000..b947077876 --- /dev/null +++ b/tests/grid-perf/app/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/tests/grid-perf/app/index.html b/tests/grid-perf/app/index.html new file mode 100644 index 0000000000..0ab6946666 --- /dev/null +++ b/tests/grid-perf/app/index.html @@ -0,0 +1,26 @@ + + + + + + Grid Performance Test + + + +
+ + + diff --git a/tests/grid-perf/app/package-lock.json b/tests/grid-perf/app/package-lock.json new file mode 100644 index 0000000000..5723492ec7 --- /dev/null +++ b/tests/grid-perf/app/package-lock.json @@ -0,0 +1,1540 @@ +{ + "name": "grid-perf-app", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "grid-perf-app", + "version": "0.0.0", + "dependencies": { + "@deephaven/grid": "file:../../../packages/grid", + "@deephaven/log": "file:../../../packages/log", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react-swc": "^3.7.0", + "typescript": "5.3.3", + "vite": "^6.0.11" + } + }, + "../../../packages/grid": { + "name": "@deephaven/grid", + "version": "1.25.0", + "license": "Apache-2.0", + "dependencies": { + "@deephaven/utils": "file:../utils", + "classnames": "^2.3.1", + "color-convert": "^2.0.1", + "event-target-shim": "^6.0.2", + "linkifyjs": "^4.1.0", + "lodash.clamp": "^4.0.3", + "memoize-one": "^5.1.1", + "memoizee": "^0.4.15" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "../../../packages/log": { + "name": "@deephaven/log", + "version": "1.8.0", + "license": "Apache-2.0", + "dependencies": { + "event-target-shim": "^6.0.2", + "jszip": "^3.10.1", + "safe-stable-stringify": "^2.5.0" + }, + "engines": { + "node": ">=16" + } + }, + "../../packages/grid": { + "name": "@deephaven/grid", + "version": "1.25.0", + "extraneous": true, + "license": "Apache-2.0", + "dependencies": { + "@deephaven/utils": "file:../utils", + "classnames": "^2.3.1", + "color-convert": "^2.0.1", + "event-target-shim": "^6.0.2", + "linkifyjs": "^4.1.0", + "lodash.clamp": "^4.0.3", + "memoize-one": "^5.1.1", + "memoizee": "^0.4.15" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "../../packages/log": { + "name": "@deephaven/log", + "version": "1.8.0", + "extraneous": true, + "license": "Apache-2.0", + "dependencies": { + "event-target-shim": "^6.0.2", + "jszip": "^3.10.1", + "safe-stable-stringify": "^2.5.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@deephaven/grid": { + "resolved": "../../../packages/grid", + "link": true + }, + "node_modules/@deephaven/log": { + "resolved": "../../../packages/log", + "link": true + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/core": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.18.tgz", + "integrity": "sha512-z87aF9GphWp//fnkRsqvtY+inMVPgYW3zSlXH1kJFvRT5H/wiAn+G32qW5l3oEk63KSF1x3Ov0BfHCObAmT8RA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.18", + "@swc/core-darwin-x64": "1.15.18", + "@swc/core-linux-arm-gnueabihf": "1.15.18", + "@swc/core-linux-arm64-gnu": "1.15.18", + "@swc/core-linux-arm64-musl": "1.15.18", + "@swc/core-linux-x64-gnu": "1.15.18", + "@swc/core-linux-x64-musl": "1.15.18", + "@swc/core-win32-arm64-msvc": "1.15.18", + "@swc/core-win32-ia32-msvc": "1.15.18", + "@swc/core-win32-x64-msvc": "1.15.18" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.18.tgz", + "integrity": "sha512-+mIv7uBuSaywN3C9LNuWaX1jJJ3SKfiJuE6Lr3bd+/1Iv8oMU7oLBjYMluX1UrEPzwN2qCdY6Io0yVicABoCwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.18.tgz", + "integrity": "sha512-wZle0eaQhnzxWX5V/2kEOI6Z9vl/lTFEC6V4EWcn+5pDjhemCpQv9e/TDJ0GIoiClX8EDWRvuZwh+Z3dhL1NAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.18.tgz", + "integrity": "sha512-ao61HGXVqrJFHAcPtF4/DegmwEkVCo4HApnotLU8ognfmU8x589z7+tcf3hU+qBiU1WOXV5fQX6W9Nzs6hjxDw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.18.tgz", + "integrity": "sha512-3xnctOBLIq3kj8PxOCgPrGjBLP/kNOddr6f5gukYt/1IZxsITQaU9TDyjeX6jG+FiCIHjCuWuffsyQDL5Ew1bg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.18.tgz", + "integrity": "sha512-0a+Lix+FSSHBSBOA0XznCcHo5/1nA6oLLjcnocvzXeqtdjnPb+SvchItHI+lfeiuj1sClYPDvPMLSLyXFaiIKw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.18.tgz", + "integrity": "sha512-wG9J8vReUlpaHz4KOD/5UE1AUgirimU4UFT9oZmupUDEofxJKYb1mTA/DrMj0s78bkBiNI+7Fo2EgPuvOJfuAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.18.tgz", + "integrity": "sha512-4nwbVvCphKzicwNWRmvD5iBaZj8JYsRGa4xOxJmOyHlMDpsvvJ2OR2cODlvWyGFH6BYL1MfIAK3qph3hp0Az6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.18.tgz", + "integrity": "sha512-zk0RYO+LjiBCat2RTMHzAWaMky0cra9loH4oRrLKLLNuL+jarxKLFDA8xTZWEkCPLjUTwlRN7d28eDLLMgtUcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.18.tgz", + "integrity": "sha512-yVuTrZ0RccD5+PEkpcLOBAuPbYBXS6rslENvIXfvJGXSdX5QGi1ehC4BjAMl5FkKLiam4kJECUI0l7Hq7T1vwg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.18.tgz", + "integrity": "sha512-7NRmE4hmUQNCbYU3Hn9Tz57mK9Qq4c97ZS+YlamlK6qG9Fb5g/BB3gPDe0iLlJkns/sYv2VWSkm8c3NmbEGjbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz", + "integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.27", + "@swc/core": "^1.12.11" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6 || ^7" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/tests/grid-perf/app/package.json b/tests/grid-perf/app/package.json new file mode 100644 index 0000000000..fcfeb03500 --- /dev/null +++ b/tests/grid-perf/app/package.json @@ -0,0 +1,24 @@ +{ + "name": "grid-perf-app", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@deephaven/grid": "file:../../../packages/grid", + "@deephaven/log": "file:../../../packages/log", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react-swc": "^3.7.0", + "typescript": "5.3.3", + "vite": "^6.0.11" + } +} diff --git a/tests/grid-perf/app/src/App.tsx b/tests/grid-perf/app/src/App.tsx new file mode 100644 index 0000000000..8b2f6cc929 --- /dev/null +++ b/tests/grid-perf/app/src/App.tsx @@ -0,0 +1,59 @@ +import { useMemo } from 'react'; +import { Grid, MockGridModel } from '@deephaven/grid'; + +/** + * Grid Performance Test App + * + * Use query params to configure the grid: + * - rows: Number of rows (default: 1000000) + * - cols: Number of columns (default: 100) + * + * Examples: + * http://localhost:4020/ + * http://localhost:4020/?rows=10000&cols=50 + * http://localhost:4020/?rows=100000&cols=200 + */ +function App(): JSX.Element { + const params = new URLSearchParams(window.location.search); + + const rowCount = parseInt(params.get('rows') ?? '1000000', 10); + const columnCount = parseInt(params.get('cols') ?? '100', 10); + + const model = useMemo( + () => new MockGridModel({ rowCount, columnCount }), + [rowCount, columnCount] + ); + + const configInfo = `Rows: ${rowCount.toLocaleString()}, Cols: ${columnCount.toLocaleString()}`; + + return ( +
+
+ {configInfo} +
+ +
+ ); +} + +export default App; diff --git a/tests/grid-perf/app/src/main.tsx b/tests/grid-perf/app/src/main.tsx new file mode 100644 index 0000000000..b31fb6bc32 --- /dev/null +++ b/tests/grid-perf/app/src/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/tests/grid-perf/app/tsconfig.json b/tests/grid-perf/app/tsconfig.json new file mode 100644 index 0000000000..a4c834a6ca --- /dev/null +++ b/tests/grid-perf/app/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/tests/grid-perf/app/vite.config.ts b/tests/grid-perf/app/vite.config.ts new file mode 100644 index 0000000000..afae791356 --- /dev/null +++ b/tests/grid-perf/app/vite.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react-swc'; +import path from 'path'; + +const packagesDir = path.resolve(__dirname, '../../../packages'); + +export default defineConfig({ + plugins: [react()], + server: { + port: 4020, + }, + preview: { + port: 4020, + }, + resolve: { + dedupe: ['react', 'react-dom'], + alias: { + // Resolve local packages to their source for development + '@deephaven/grid': path.resolve(packagesDir, 'grid/src'), + '@deephaven/log': path.resolve(packagesDir, 'log/src'), + '@deephaven/utils': path.resolve(packagesDir, 'utils/src'), + }, + }, +}); diff --git a/tests/grid-perf/grid-perf-app.spec.ts b/tests/grid-perf/grid-perf-app.spec.ts new file mode 100644 index 0000000000..769066e1a3 --- /dev/null +++ b/tests/grid-perf/grid-perf-app.spec.ts @@ -0,0 +1,98 @@ +import { test } from '@playwright/test'; +import { + logResults, + scrollGrid, + startFPSMeasurement, + stopFPSMeasurement, +} from './utils'; + +/** + * Grid Performance Tests using the standalone perf app. + * + * These tests use the perf app in `tests/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 (from the repo root): npm run e2e:grid-performance + * + * 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'; + +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); + + const canvas = page.locator('canvas').first(); + await scrollGrid(page, canvas, 5000); + await scrollGrid(page, canvas, -3000); + await scrollGrid(page, canvas, 4000); + await scrollGrid(page, canvas, -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 }); + }); +}); diff --git a/tests/grid-perf/grid-performance.spec.ts b/tests/grid-perf/grid-performance.spec.ts new file mode 100644 index 0000000000..d521d3d41d --- /dev/null +++ b/tests/grid-perf/grid-performance.spec.ts @@ -0,0 +1,360 @@ +import { test, type Locator, type Page } from '@playwright/test'; +import { + generateVarName, + gotoPage, + openTable, + pasteInMonaco, + waitForLoadingDone, +} from '../utils'; +import { + logResults, + scrollGrid, + startFPSMeasurement, + stopFPSMeasurement, +} from './utils'; + +/** + * Performance benchmark tests for the Grid component using the main app. + * Tests FPS during scrolling with real table data from a Deephaven server. + * + * These tests use existing tables from the test environment: + * - simple_table: Small table (100 rows, 2 columns) + * - all_types: Table with many column types (20 rows) + * - all_types_big: Same columns as all_types with 1,000,000 rows + * + * Long string benchmarks build their own tables through the console, since the + * application mode tables have no columns wide enough to force truncation. + * + * For benchmarks that need Grid props toggled or row and column counts the + * test data does not reach, see grid-perf-app.spec.ts which uses a standalone + * test app. + */ + +/** 123 characters */ +const LOREM = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. '; + +/** ~500 characters, already far wider than any column can render */ +const LONG_TEXT_REPEAT = 4; + +/** ~25,000 characters, where a single cell is more text than the whole grid */ +const HUGE_TEXT_REPEAT = 200; + +const MIN_VARIABLE_TEXT_LENGTH = 24; + +/** + * Locates the most recently opened grid if multiple exist + */ +function getGrid(page: Page): Locator { + return page.locator('.iris-grid-panel .iris-grid').last(); +} + +/** + * Builds a python command creating a table of long string cells. `update_view` + * keeps the strings formula based, so they are materialized as the viewport + * fetches them rather than being stored up front. + * @param tableName Name of the variable to assign the table to + * @param rowCount Number of rows in the table + * @param textRepeat How many times to repeat LOREM in each long string + * @returns The python command + */ +function makeLongStringTableCommand( + tableName: string, + rowCount: number, + textRepeat: number +): string { + const textName = `${tableName}_text`; + + // `Repeated` hits the text width cache on every row, `Unique` and `Variable` + // miss it, and `Variable` also varies the truncation point row to row + return `from deephaven import empty_table +${textName} = "${LOREM}" * ${textRepeat} +${tableName} = empty_table(${rowCount}).update_view([ + "Index = i", + "Short = \`Row \` + i", + "Repeated = ${textName}", + "Unique = ${textName} + i", + "Variable = ${textName}.substring(0, ${MIN_VARIABLE_TEXT_LENGTH} + (i % (${textName}.length() - ${MIN_VARIABLE_TEXT_LENGTH})))", +])`; +} + +/** + * Creates a table of long string cells in the console and opens it + * @param page + * @param textRepeat How many times to repeat LOREM in each long string + * @param rowCount Number of rows in the table + */ +async function openLongStringTable( + page: Page, + textRepeat: number, + rowCount = 1000000 +): Promise { + const tableName = generateVarName('long_strings'); + const consoleInput = page.locator('.console-input'); + + await pasteInMonaco( + consoleInput, + makeLongStringTableCommand(tableName, rowCount, textRepeat) + ); + await page.keyboard.press('Enter'); + + await page.getByRole('button', { name: tableName, exact: true }).click(); + await waitForLoadingDone(page); +} + +/** + * Performance tests are skipped by default as they can be flaky in CI due to + * resource constraints. To run these tests explicitly: + * + * npm run e2e:performance + */ +test.describe('grid scroll performance benchmarks', () => { + // Skip by default - these tests are flaky in CI due to resource constraints + test.skip( + !process.env.RUN_PERF_TESTS, + 'Performance tests skipped. Set RUN_PERF_TESTS=1 to run.' + ); + + // Run tests serially to avoid resource contention + test.describe.configure({ mode: 'serial' }); + + test.beforeEach(async ({ page }) => { + await gotoPage(page, ''); + }); + + test.describe('simple_table performance', () => { + test.beforeEach(async ({ page }) => { + // simple_table is a 100-row table with 2 columns + await openTable(page, 'simple_table'); + await waitForLoadingDone(page); + }); + + test('scroll performance - simple_table', async ({ page }) => { + await startFPSMeasurement(page); + + // Scroll down and back up + await scrollGrid(page, getGrid(page), 2000); + await scrollGrid(page, getGrid(page), -2000); + await scrollGrid(page, getGrid(page), 1500); + await scrollGrid(page, getGrid(page), -1000); + + const result = await stopFPSMeasurement(page); + logResults('Simple Table Scroll', result, { minFps: 30 }); + }); + }); + + test.describe('all_types_big table performance', () => { + test.beforeEach(async ({ page }) => { + // all_types_big has many different column types and 1,000,000 rows so + // scrolling actually exercises snapshot fetching and rendering + await openTable(page, 'all_types_big'); + await waitForLoadingDone(page); + }); + + test('scroll performance - all_types_big', async ({ page }) => { + await startFPSMeasurement(page); + + // Scroll down significantly and back + await scrollGrid(page, getGrid(page), 5000); + await scrollGrid(page, getGrid(page), -3000); + await scrollGrid(page, getGrid(page), 2000); + await scrollGrid(page, getGrid(page), -4000); + + const result = await stopFPSMeasurement(page); + logResults('All Types Big Table Scroll', result, { minFps: 30 }); + }); + + test('rapid scroll performance', async ({ page }) => { + await startFPSMeasurement(page); + + // Rapid small scrolls (simulates fast mouse wheel) + for (let i = 0; i < 50; i += 1) { + await page.mouse.wheel(0, 200); + await page.waitForTimeout(8); // ~120fps input rate + } + + const result = await stopFPSMeasurement(page); + logResults('Rapid Scroll', result, { minFps: 24 }); + }); + }); + + test.describe('long string table performance', () => { + test.beforeEach(async ({ page }) => { + await openLongStringTable(page, LONG_TEXT_REPEAT); + }); + + test('scroll performance - long strings', async ({ page }) => { + await startFPSMeasurement(page); + + await scrollGrid(page, getGrid(page), 5000); + await scrollGrid(page, getGrid(page), -3000); + await scrollGrid(page, getGrid(page), 2000); + await scrollGrid(page, getGrid(page), -4000); + + const result = await stopFPSMeasurement(page); + logResults('Long Strings Scroll', result, { minFps: 24 }); + }); + + test('horizontal scroll performance - long strings', async ({ page }) => { + const box = await getGrid(page).boundingBox(); + if (!box) throw new Error('Grid not found'); + + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + + await startFPSMeasurement(page); + + // Long string columns are wide, so horizontal scrolling changes which + // part of each cell is truncated on every frame + for (let i = 0; i < 20; i += 1) { + await page.mouse.wheel(600, 0); + await page.waitForTimeout(16); + } + for (let i = 0; i < 20; i += 1) { + await page.mouse.wheel(-600, 0); + await page.waitForTimeout(16); + } + + const result = await stopFPSMeasurement(page); + logResults('Long Strings Horizontal Scroll', result, { minFps: 24 }); + }); + + test('rapid scroll performance - long strings', async ({ page }) => { + const box = await getGrid(page).boundingBox(); + if (!box) throw new Error('Grid not found'); + + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + + await startFPSMeasurement(page); + + // Rapid small scrolls (simulates fast mouse wheel) + for (let i = 0; i < 50; i += 1) { + await page.mouse.wheel(0, 200); + await page.waitForTimeout(8); // ~120fps input rate + } + + const result = await stopFPSMeasurement(page); + logResults('Long Strings Rapid Scroll', result, { minFps: 20 }); + }); + }); + + test.describe('huge string table performance', () => { + test.beforeEach(async ({ page }) => { + await openLongStringTable(page, HUGE_TEXT_REPEAT); + }); + + test('scroll performance - huge strings', async ({ page }) => { + await startFPSMeasurement(page); + + await scrollGrid(page, getGrid(page), 5000); + await scrollGrid(page, getGrid(page), -3000); + await scrollGrid(page, getGrid(page), 2000); + await scrollGrid(page, getGrid(page), -4000); + + const result = await stopFPSMeasurement(page); + logResults('Huge Strings Scroll', result, { minFps: 20 }); + }); + + test('horizontal scroll performance - huge strings', async ({ page }) => { + const box = await getGrid(page).boundingBox(); + if (!box) throw new Error('Grid not found'); + + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + + await startFPSMeasurement(page); + + for (let i = 0; i < 20; i += 1) { + await page.mouse.wheel(600, 0); + await page.waitForTimeout(16); + } + for (let i = 0; i < 20; i += 1) { + await page.mouse.wheel(-600, 0); + await page.waitForTimeout(16); + } + + const result = await stopFPSMeasurement(page); + logResults('Huge Strings Horizontal Scroll', result, { minFps: 20 }); + }); + + test('rapid scroll performance - huge strings', async ({ page }) => { + const box = await getGrid(page).boundingBox(); + if (!box) throw new Error('Grid not found'); + + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + + await startFPSMeasurement(page); + + // Rapid small scrolls (simulates fast mouse wheel) + for (let i = 0; i < 50; i += 1) { + await page.mouse.wheel(0, 200); + await page.waitForTimeout(8); // ~120fps input rate + } + + const result = await stopFPSMeasurement(page); + logResults('Huge Strings Rapid Scroll', result, { minFps: 15 }); + }); + }); +}); + +test.describe('grid performance stress tests', () => { + // Skip by default - these tests are flaky in CI due to resource constraints + test.skip( + !process.env.RUN_PERF_TESTS, + 'Performance tests skipped. Set RUN_PERF_TESTS=1 to run.' + ); + + // Run tests serially to avoid resource contention + test.describe.configure({ mode: 'serial' }); + + test.beforeEach(async ({ page }) => { + await gotoPage(page, ''); + }); + + test('sustained scrolling performance', async ({ page }) => { + await openTable(page, 'simple_table'); + await waitForLoadingDone(page); + + await startFPSMeasurement(page); + + // Sustained scrolling for 3 seconds + 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); + + // Reverse direction occasionally + if (Math.random() < 0.1) { + direction *= -1; + } + } + + const result = await stopFPSMeasurement(page); + logResults('Sustained Scroll (3s)', result, { minFps: 30 }); + }); + + test('horizontal and vertical scroll combined', async ({ page }) => { + await openTable(page, 'all_types_big'); + await waitForLoadingDone(page); + + const grid = getGrid(page); + const box = await grid.boundingBox(); + if (!box) throw new Error('Grid not found'); + + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + + await startFPSMeasurement(page); + + // Combined 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('Combined H+V Scroll', result, { minFps: 28 }); + }); +}); diff --git a/tests/grid-perf/utils.ts b/tests/grid-perf/utils.ts new file mode 100644 index 0000000000..1615ec8fa5 --- /dev/null +++ b/tests/grid-perf/utils.ts @@ -0,0 +1,135 @@ +import { type Locator, type Page } from '@playwright/test'; + +export interface FPSResult { + fps: number; + avgFrameTime: number; + minFrameTime: number; + maxFrameTime: number; + frameCount: number; + droppedFrames: number; + stalledFrames: number; +} + +/** + * Injects an FPS counter into the page that measures frame timings + */ +export async function startFPSMeasurement(page: Page): Promise { + 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); + }); +} + +/** + * Stops FPS measurement and returns the results + */ +export async function stopFPSMeasurement(page: Page): Promise { + const timings = await page.evaluate(() => { + (window as any).__fpsRunning = false; + return (window as any).__frameTimings as number[]; + }); + + // Only drop non-positive/invalid timings; retain long frames (stalls) so + // that severe regressions are not hidden from the reported metrics. + const validTimings = timings.filter(t => t > 0); + + if (validTimings.length === 0) { + return { + fps: 0, + avgFrameTime: 0, + minFrameTime: 0, + maxFrameTime: 0, + frameCount: 0, + droppedFrames: 0, + stalledFrames: 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); + // Frames taking > 33ms (less than 30fps) are considered "dropped" + const droppedFrames = validTimings.filter(t => t > 33).length; + // Frames taking >= 500ms are likely severe stalls, reported separately + const stalledFrames = validTimings.filter(t => t >= 500).length; + + return { + fps, + avgFrameTime, + minFrameTime, + maxFrameTime, + frameCount: validTimings.length, + droppedFrames, + stalledFrames, + }; +} + +/** + * Scrolls the given element using mouse wheel events + */ +export async function scrollGrid( + page: Page, + grid: Locator, + totalDelta: number +): Promise { + const box = await grid.boundingBox(); + if (!box) throw new Error('Grid not found'); + + // Move mouse to center of grid + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + + // Scroll in increments + 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; + // Small delay to allow rendering + await page.waitForTimeout(16); + } +} + +export 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( + ` Stalled frames (>=500ms): ${result.stalledFrames} (${( + (result.stalledFrames / result.frameCount) * + 100 + ).toFixed(1)}%)` + ); + console.log(` Expected min FPS: ${expected.minFps}`); +} diff --git a/tests/utils.ts b/tests/utils.ts index 19cdf9fe11..afeac3d084 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -18,6 +18,7 @@ export enum TableTypes { type TableNames = | 'all_types' + | 'all_types_big' | 'simple_plot' | 'simple_table' | 'simple_table_header_group'