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 (
+
+ );
+}
+
+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'