From 8dccfdb8d397c313aa0acc26d05ae3fcc781d1f9 Mon Sep 17 00:00:00 2001 From: Julian Meyer Date: Tue, 29 Apr 2025 21:36:38 -0700 Subject: [PATCH] feat: improve filter options and add index filter Adds a filter to the index page to allow users to filter down the list of benchmarks. Also updates the benchmark page filter to only allow the user to select filters that result in actual data. This fixes a bug where for configs that are sparse, sometimes the user would select a filter that resulted in no data. --- .github/workflows/nodejs.yaml | 5 +- report/package-lock.json | 455 +++++++++++++++++++++++- report/package.json | 6 +- report/src/components/ChartSelector.tsx | 186 ++++------ report/src/filter.test.ts | 252 +++++++++++++ report/src/filter.ts | 95 +++++ report/src/hooks/useBenchmarkFilters.ts | 107 ++++++ report/src/pages/RunIndex.tsx | 83 ++++- report/src/types.ts | 21 -- 9 files changed, 1069 insertions(+), 141 deletions(-) create mode 100644 report/src/filter.test.ts create mode 100644 report/src/filter.ts create mode 100644 report/src/hooks/useBenchmarkFilters.ts diff --git a/.github/workflows/nodejs.yaml b/.github/workflows/nodejs.yaml index 36567cf9..e50476f4 100644 --- a/.github/workflows/nodejs.yaml +++ b/.github/workflows/nodejs.yaml @@ -28,5 +28,6 @@ jobs: - name: Install dependencies run: npm ci - name: Check formatting - run: | - npm run check + run: npm run check + - name: Run tests + run: npm test diff --git a/report/package-lock.json b/report/package-lock.json index 37f31c38..964ea075 100644 --- a/report/package-lock.json +++ b/report/package-lock.json @@ -34,7 +34,8 @@ "prettier": "^3.5.3", "typescript": "^5.3.3", "vite": "^5.1.4", - "vite-plugin-static-copy": "^2.3.1" + "vite-plugin-static-copy": "^2.3.1", + "vitest": "^3.1.2" } }, "node_modules/@ampproject/remapping": { @@ -2066,6 +2067,119 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, + "node_modules/@vitest/expect": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.2.tgz", + "integrity": "sha512-O8hJgr+zREopCAqWl3uCVaOdqJwZ9qaDwUP7vy3Xigad0phZe9APxKhPcDNqYYi0rX5oMvwJMSCAXY2afqeTSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.1.2", + "@vitest/utils": "3.1.2", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.2.tgz", + "integrity": "sha512-kOtd6K2lc7SQ0mBqYv/wdGedlqPdM/B38paPY+OwJ1XiNi44w3Fpog82UfOibmHaV9Wod18A09I9SCKLyDMqgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.1.2", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.2.tgz", + "integrity": "sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.2.tgz", + "integrity": "sha512-bhLib9l4xb4sUMPXnThbnhX2Yi8OutBMA8Yahxa7yavQsFDtwY/jrUZwpKp2XH9DhRFJIeytlyGpXCqZ65nR+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.1.2", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.2.tgz", + "integrity": "sha512-Q1qkpazSF/p4ApZg1vfZSQ5Yw6OCQxVMVrLjslbLFA1hMDrT2uxtqMaw8Tc/jy5DLka1sNs1Y7rBcftMiaSH/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.1.2", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.2.tgz", + "integrity": "sha512-OEc5fSXMws6sHVe4kOFyDSj/+4MSwst0ib4un0DlcYgQvRuYQ0+M2HyqGaauUMnjq87tmUaMNDxKQx7wNfVqPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.2.tgz", + "integrity": "sha512-5GGd0ytZ7BH3H6JTj9Kw7Prn1Nbg0wZVrIvou+UWxm54d+WoXXgAgjFJ8wn3LdagWLFSEfpPeyYrByZaGEZHLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.1.2", + "loupe": "^3.1.3", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", @@ -2311,6 +2425,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -2414,6 +2538,16 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -2495,6 +2629,23 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2512,6 +2663,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -3084,6 +3245,16 @@ } } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3316,6 +3487,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -3804,6 +3982,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3814,6 +4002,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", + "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5193,6 +5391,13 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -5203,6 +5408,16 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5573,6 +5788,23 @@ "dev": true, "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6198,6 +6430,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -6207,6 +6446,20 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -6402,6 +6655,95 @@ "dev": true, "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6723,6 +7065,29 @@ } } }, + "node_modules/vite-node": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.2.tgz", + "integrity": "sha512-/8iMryv46J3aK13iUXsei5G/A3CUlW4665THCPS+K8xAaqrVWiGB4RfXMQXCLjpK9P2eK//BczrVkn5JLAk6DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.0", + "es-module-lexer": "^1.6.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/vite-plugin-static-copy": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.3.1.tgz", @@ -6743,6 +7108,77 @@ "vite": "^5.0.0 || ^6.0.0" } }, + "node_modules/vitest": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.2.tgz", + "integrity": "sha512-WaxpJe092ID1C0mr+LH9MmNrhfzi8I65EX/NRU/Ld016KqQNRgxSOlGNP1hHN+a/F8L15Mh8klwaF77zR3GeDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "3.1.2", + "@vitest/mocker": "3.1.2", + "@vitest/pretty-format": "^3.1.2", + "@vitest/runner": "3.1.2", + "@vitest/snapshot": "3.1.2", + "@vitest/spy": "3.1.2", + "@vitest/utils": "3.1.2", + "chai": "^5.2.0", + "debug": "^4.4.0", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.13", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.1.2", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.1.2", + "@vitest/ui": "3.1.2", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6848,6 +7284,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/report/package.json b/report/package.json index e6d80858..b84e1152 100644 --- a/report/package.json +++ b/report/package.json @@ -8,7 +8,8 @@ "preview": "vite preview", "start": "vite build && vite preview", "check": "eslint . --ext .ts,.tsx --fix && prettier --check .", - "lint": "eslint . --ext .ts,.tsx --fix && prettier --write ." + "lint": "eslint . --ext .ts,.tsx --fix && prettier --write .", + "test": "vitest" }, "keywords": [], "author": "", @@ -40,6 +41,7 @@ "prettier": "^3.5.3", "typescript": "^5.3.3", "vite": "^5.1.4", - "vite-plugin-static-copy": "^2.3.1" + "vite-plugin-static-copy": "^2.3.1", + "vitest": "^3.1.2" } } diff --git a/report/src/components/ChartSelector.tsx b/report/src/components/ChartSelector.tsx index bf0063c0..dd57a11d 100644 --- a/report/src/components/ChartSelector.tsx +++ b/report/src/components/ChartSelector.tsx @@ -1,6 +1,5 @@ import { useEffect, useMemo, useRef } from "react"; -import { BenchmarkRun } from "../types"; -import { BenchmarkRuns, getBenchmarkVariables } from "../types"; +import { BenchmarkRuns } from "../types"; import { isEqual } from "lodash"; import { camelToTitleCase, @@ -8,7 +7,7 @@ import { formatLabel, } from "../utils/formatters"; import { interpolateWarm } from "d3"; -import { useSearchParamsState } from "../utils/useSearchParamsState"; +import { useBenchmarkFilters } from "../hooks/useBenchmarkFilters"; export interface DataFileRequest { outputDir: string; @@ -22,111 +21,83 @@ interface ChartSelectorProps { onChangeDataQuery: (data: DataFileRequest[]) => void; } -interface BenchmarkRunWithRole extends BenchmarkRun { - testConfig: BenchmarkRun["testConfig"] & { - role: string; - }; -} - const ChartSelector = ({ benchmarkRuns, onChangeDataQuery, }: ChartSelectorProps) => { - const variables = useMemo((): Record< - string, - (string | number | boolean)[] - > => { - return { - ...getBenchmarkVariables(benchmarkRuns.runs), - role: ["sequencer", "validator"], - }; - }, [benchmarkRuns]); - - const [filterSelections, setFilterSelections] = useSearchParamsState<{ - params: { [key: string]: string }; - byMetric: string; - }>("filters", { params: {}, byMetric: "role" }); - - const validFilterSelections = useMemo(() => { - return Object.fromEntries( - Object.keys(variables) - .filter((key) => { - return key !== filterSelections.byMetric; - }) - .map( - (key) => - [key, filterSelections.params[key] ?? variables[key][0]] as const, - ), - ); - }, [filterSelections.params, filterSelections.byMetric]); - - const matchedRuns = useMemo(() => { - return benchmarkRuns.runs - .flatMap((r): BenchmarkRunWithRole[] => [ - { - ...r, - testConfig: { - ...r.testConfig, - role: "sequencer", - }, - }, - { - ...r, - testConfig: { - ...r.testConfig, - role: "validator", - }, - }, - ]) - .filter((run) => { - return Object.entries(validFilterSelections).every(([key, value]) => { - return ( - `${(run.testConfig as Record)[key]}` === - `${value}` - ); - }); - }); - }, [validFilterSelections, benchmarkRuns.runs]); + const runsWithRoles = useMemo( + () => + benchmarkRuns.runs.flatMap((r) => [ + { ...r, testConfig: { ...r.testConfig, role: "sequencer" } }, + { ...r, testConfig: { ...r.testConfig, role: "validator" } }, + ]), + [benchmarkRuns.runs], + ); + + const { + variables, + filterOptions, + matchedRuns, + filterSelections, + setFilters, + setByMetric, + } = useBenchmarkFilters(runsWithRoles, "role"); const lastSentDataRef = useRef([]); useEffect(() => { let colorMap: ((val: number) => string) | undefined = undefined; - if (filterSelections.byMetric === "GasLimit") { - const min = matchedRuns.reduce((a, b) => { - return Math.min(a, Number(b.testConfig.GasLimit)); - }, 0); - const max = matchedRuns.reduce((a, b) => { - return Math.max(a, Number(b.testConfig.GasLimit)); - }, 0); + if (filterSelections.byMetric === "GasLimit" && matchedRuns.length > 0) { + const gasLimits = matchedRuns.map((r) => Number(r.testConfig.GasLimit)); + const min = Math.min(...gasLimits); + const max = Math.max(...gasLimits); colorMap = (val: number) => - interpolateWarm(1 - (max > 0 ? (val - min) / max : 0)); + interpolateWarm(max - min > 0 ? 1 - (val - min) / (max - min) : 0.5); } - const dataToSend: DataFileRequest[] = matchedRuns.map((run) => { - let seriesName = `${run.testConfig[filterSelections.byMetric ?? "role"]}`; - let color = undefined; - - if (filterSelections.byMetric === "GasLimit") { - seriesName = formatValue(Number(run.testConfig.GasLimit), "gas"); - color = colorMap?.(Number(run.testConfig.GasLimit)); - } - - return { - outputDir: run.outputDir, - role: run.testConfig.role, - name: seriesName, - color, - }; - }); + const dataToSend: DataFileRequest[] = matchedRuns + .map((run): DataFileRequest | null => { + if (!run.testConfig || !run.outputDir) { + console.warn("Skipping run with missing data:", run); + return null; + } + + let seriesName: string; + let color: string | undefined = undefined; + const byMetricValue = run.testConfig[filterSelections.byMetric]; + + if (filterSelections.byMetric === "GasLimit") { + const gasLimitNum = Number(byMetricValue); + seriesName = formatValue(gasLimitNum, "gas"); + color = colorMap?.(gasLimitNum); + } else { + seriesName = + byMetricValue !== undefined + ? formatLabel(String(byMetricValue)) + : "Unknown"; + } + + const role = run.testConfig.role ?? "unknown"; + + const request: DataFileRequest = { + outputDir: run.outputDir, + role: String(role), + name: seriesName, + }; + if (color !== undefined) { + request.color = color; + } + return request; + }) + .filter((item): item is DataFileRequest => item !== null); if (!isEqual(dataToSend, lastSentDataRef.current)) { lastSentDataRef.current = dataToSend; onChangeDataQuery(dataToSend); } - }, [filterSelections, matchedRuns, onChangeDataQuery]); + }, [matchedRuns, filterSelections.byMetric, onChangeDataQuery]); return (
@@ -134,47 +105,46 @@ const ChartSelector = ({
Show Line Per
- {Object.entries(variables) + {Object.entries(filterOptions) .sort((a, b) => a[0].localeCompare(b[0])) .filter(([k]) => k !== filterSelections.byMetric) - .map(([key, value]) => { + .map(([key, availableValues]) => { + const currentValue = + filterSelections.params[key] ?? availableValues[0]; return (
{camelToTitleCase(key)}
); })} + {matchedRuns.length === 0 && ( +
+ No benchmark runs match the current filter combination. +
+ )} ); }; diff --git a/report/src/filter.test.ts b/report/src/filter.test.ts new file mode 100644 index 00000000..44577085 --- /dev/null +++ b/report/src/filter.test.ts @@ -0,0 +1,252 @@ +import { describe, it, expect } from "vitest"; +import { getBenchmarkVariables } from "./filter"; +import { BenchmarkRun } from "./types"; + +// Sample data for testing - This data now aligns with the ActualBenchmarkRun type +const sampleRuns: BenchmarkRun[] = [ + { + sourceFile: "test1.json", + testName: "A", + testDescription: "Desc A", + outputDir: "/tmp/a", + testConfig: { NodeType: "geth", GasLimit: 100, ExtraParam: "true" }, // Use boolean true + result: { success: true }, + }, + { + sourceFile: "test2.json", + testName: "B", + testDescription: "Desc B", + outputDir: "/tmp/b", + testConfig: { NodeType: "reth", GasLimit: 100, ExtraParam: "true" }, // Use boolean true + result: { success: true }, + }, + { + sourceFile: "test3.json", + testName: "C", + testDescription: "Desc C", + outputDir: "/tmp/c", + testConfig: { NodeType: "geth", GasLimit: 200, ExtraParam: "true" }, // Use boolean true + result: { success: true }, + }, + { + sourceFile: "test4.json", + testName: "D", + testDescription: "Desc D", + outputDir: "/tmp/d", + testConfig: { NodeType: "reth", GasLimit: 200, ExtraParam: "false" }, // Use boolean false + result: { success: true }, + }, + { + sourceFile: "test5.json", + testName: "E", + testDescription: "Desc E", + outputDir: "/tmp/e", + testConfig: { NodeType: "geth", GasLimit: 200, ExtraParam: "false" }, // Use boolean false + result: { success: true }, + }, +]; + +describe("BenchmarkVariables", () => { + it("should update options and matched runs when a filter changes", () => { + const result = getBenchmarkVariables(sampleRuns, { + params: { GasLimit: 200, ExtraParam: "true" }, // Change GasLimit from default 100 to 200 + byMetric: "NodeType", + }); + + // Matched runs based on { GasLimit: 200, ExtraParam: true } + expect(result.matchedRuns).toHaveLength(1); + expect(result.matchedRuns[0].testName).toBe("C"); // geth, 200, true + + // Filter options available with GasLimit=200 + expect(result.filterOptions).toEqual({ + // GasLimit: [100, 200] + // Check GasLimit=100 with other filters { ExtraParam: true }: run 'A' matches. + // Check GasLimit=200 with other filters { ExtraParam: true }: run 'C' matches. + GasLimit: [100, 200], + // ExtraParam: [true, false] + // Check ExtraParam=true with other filters { GasLimit: 200 }: run 'C' matches. + // Check ExtraParam=false with other filters { GasLimit: 200 }: runs 'D', 'E' match. + ExtraParam: ["false", "true"], + }); + }); + + it("should provide options even if the current selection has no matches", () => { + const result = getBenchmarkVariables(sampleRuns, { + params: { GasLimit: 100, ExtraParam: false }, // This specific combo has no runs + byMetric: "NodeType", + }); + + // Active filters: { GasLimit: 100, ExtraParam: false } + + // Matched runs based on { GasLimit: 100, ExtraParam: false } + expect(result.matchedRuns).toHaveLength(0); + + // Filter options should still be calculated based on *other* filters + expect(result.filterOptions).toEqual({ + // GasLimit: [100, 200] + // Check GasLimit=100 with other filter { ExtraParam: false }: No match. + // Check GasLimit=200 with other filter { ExtraParam: false }: Runs 'D', 'E' match. + GasLimit: [200], + // ExtraParam: [true, false] + // Check ExtraParam=true with other filter { GasLimit: 100 }: Run 'A', 'B' matches. + // Check ExtraParam=false with other filter { GasLimit: 100 }: No match. + ExtraParam: ["true"], + }); + }); + + it("should handle byMetric correctly, excluding it from filters and options", () => { + const result = getBenchmarkVariables(sampleRuns, { + params: { GasLimit: 200, NodeType: "geth" }, + byMetric: "ExtraParam", // Group by ExtraParam this time + }); + + // Active filters: { NodeType: 'geth', GasLimit: 200 } (default for NodeType) + + // Matched runs based on { NodeType: 'geth', GasLimit: 200 } + expect(result.matchedRuns).toHaveLength(2); + expect(result.matchedRuns.map((r) => r.testName).sort()).toEqual([ + "C", + "E", + ]); // geth, 200, true AND geth, 200, false + + // Filter options available (excluding byMetric 'ExtraParam') + expect(result.filterOptions).toEqual({ + // NodeType: ['geth', 'reth'] + // Check NodeType='geth' with other { GasLimit: 200 }: Runs 'C', 'E' match. + // Check NodeType='reth' with other { GasLimit: 200 }: Run 'D' matches. + NodeType: ["geth", "reth"], + // GasLimit: [100, 200] + // Check GasLimit=100 with other { NodeType: 'geth' }: Run 'A' matches. + // Check GasLimit=200 with other { NodeType: 'geth' }: Runs 'C', 'E' match. + GasLimit: [100, 200], + }); + }); + + it("should handle scenario with only one variable", () => { + const singleVarRuns: BenchmarkRun[] = [ + { + sourceFile: "f1", + testName: "T1", + testDescription: "", + outputDir: "", + testConfig: { X: "a" }, + result: { success: true }, + }, + { + sourceFile: "f2", + testName: "T2", + testDescription: "", + outputDir: "", + testConfig: { X: "b" }, + result: { success: true }, + }, + ]; + const result = getBenchmarkVariables(singleVarRuns, { + params: {}, // No initial params + byMetric: "X", // Group by the only variable + }); + + expect(result.variables).toEqual({ X: ["a", "b"] }); + // No active filters because the only variable is the byMetric + expect(result.matchedRuns).toHaveLength(2); // All runs match initially + // No filter options because the only variable is the byMetric + expect(result.filterOptions).toEqual({}); + }); + + it("should correctly filter options based on other selected filters, even with sparse data", () => { + // Renamed test description for clarity + // Runs where combinations don't overlap well for filtering + const sparseRuns: BenchmarkRun[] = [ + { + sourceFile: "s1", + testName: "S1", + testDescription: "", + outputDir: "", + testConfig: { P1: "A", P2: "X" }, + result: { success: true }, + }, + { + sourceFile: "s2", + testName: "S2", + testDescription: "", + outputDir: "", + testConfig: { P1: "B", P2: "Y" }, + result: { success: true }, + }, + { + sourceFile: "s3", + testName: "S3", + testDescription: "", + outputDir: "", + testConfig: { P1: "A", P2: "Z" }, + result: { success: true }, + }, // Added A/Z + ]; + + // Scenario 1: Select P1='A', group by P2 + const result1 = getBenchmarkVariables(sparseRuns, { + params: { P1: "A" }, + byMetric: "P2", + }); + + expect(result1.variables).toEqual({ P1: ["A", "B"], P2: ["X", "Y", "Z"] }); + // Active filter: { P1: 'A' } + expect(result1.matchedRuns).toHaveLength(2); // S1 (A,X), S3 (A,Z) + expect(result1.matchedRuns.map((r) => r.testName).sort()).toEqual([ + "S1", + "S3", + ]); + // Filter Options: + // P1: Check P1='A' with other {}: Match S1, S3. Check P1='B' with other {}: Match S2. + expect(result1.filterOptions).toEqual({ P1: ["A", "B"] }); + + // Scenario 2: Select P1='B', group by P2 + const result2 = getBenchmarkVariables(sparseRuns, { + params: { P1: "B" }, + byMetric: "P2", + }); + // Active filter: { P1: 'B' } + expect(result2.matchedRuns).toHaveLength(1); // S2 (B, Y) + expect(result2.matchedRuns[0].testName).toBe("S2"); + // Filter Options: + // P1: Check P1='A' with other {}: Match S1, S3. Check P1='B' with other {}: Match S2. + expect(result2.filterOptions).toEqual({ P1: ["A", "B"] }); + }); +}); + +describe("getBenchmarkVariables", () => { + // Test for initial state (add this back if it was removed) + it("should correctly identify variables, initial options, and matched runs with defaults", () => { + const result = getBenchmarkVariables(sampleRuns, { + params: { + ExtraParam: "true", + GasLimit: 100, + }, + byMetric: "NodeType", // Group by NodeType + }); + + // Variables identified (more than one value) + expect(result.variables).toEqual({ + NodeType: ["geth", "reth"], + GasLimit: [100, 200], + ExtraParam: ["false", "true"], + }); + + // Matched runs based on initial active filters + expect(result.matchedRuns).toHaveLength(2); + expect(result.matchedRuns.map((r) => r.testName).sort()).toEqual([ + "A", + "B", + ]); // geth, 200, false AND reth, 200, false + + // Filter options available initially (excluding byMetric) + expect(result.filterOptions).toEqual({ + // GasLimit: Should check based on { ExtraParam: true } + // -> GasLimit=100 matches A, B. GasLimit=200 matches C. + GasLimit: [100, 200], + // ExtraParam: Should check based on { GasLimit: 100 } + // -> ExtraParam=true matches A, B. ExtraParam=false has no match with GasLimit=100. + ExtraParam: ["true"], // Only true should be available based on default GasLimit=100 + }); + }); +}); diff --git a/report/src/filter.ts b/report/src/filter.ts new file mode 100644 index 00000000..c7966aba --- /dev/null +++ b/report/src/filter.ts @@ -0,0 +1,95 @@ +import { BenchmarkRun } from "./types"; + +// Export this type for use elsewhere +export type FilterValue = string | number | boolean; +type FilterSelectionsParams = Record; + +/** + * Matches runs against a given set of filter criteria. + */ +function matchRuns( + runs: BenchmarkRun[], + filterSelections: FilterSelectionsParams, +): BenchmarkRun[] { + return runs.filter((run) => { + return Object.entries(filterSelections).every(([key, value]) => { + return `${run.testConfig[key]}` === `${value}`; + }); + }); +} + +/** + * Extracts variables, calculates available filter options, and filters runs based on selections. + * Ensures that filter options remain available even if the current selection yields no results. + */ +export function getBenchmarkVariables( + runs: BenchmarkRun[], + filterSelections: { + params: FilterSelectionsParams; + byMetric: string; + }, + // Add optional argument for pre-calculated variables + precalculatedVariables?: Record | undefined, + defaultBehavior: "first" | "any" = "first", // if a filter is not set, should it be any or first +) { + const variables = + precalculatedVariables ?? + (() => { + const allPossibleValues: Record> = {}; + for (const run of runs) { + for (const [key, value] of Object.entries(run.testConfig)) { + if (!allPossibleValues[key]) { + allPossibleValues[key] = new Set(); + } + allPossibleValues[key].add(value); + } + } + return Object.fromEntries( + Object.entries(allPossibleValues) + .filter(([, values]) => values.size > 1) + .map(([key, values]) => [key, [...values].sort()]), + ); + })(); // Immediately invoke the IIFE if needed + + const filterOptions: Record = {}; + for (const key of Object.keys(variables)) { + // Skip the metric used for grouping series + if (key === filterSelections.byMetric) { + continue; + } + + // Determine the base filters to use when checking options for `key` + // This includes all *other* active filters. + const otherActiveFilters = { ...filterSelections.params }; + delete otherActiveFilters[key]; + delete otherActiveFilters[filterSelections.byMetric]; + + // Check which values for `key` yield results when combined with `otherActiveFilters` + const validValuesForKey = variables[key].filter((value) => { + const potentialFilters = { ...otherActiveFilters, [key]: value }; + return matchRuns(runs, potentialFilters).length > 0; + }); + + if (validValuesForKey.length > 0) { + filterOptions[key] = validValuesForKey; + } + } + + // 3. Calculate active filters with defaults + const activeFilters: FilterSelectionsParams = {}; + for (const [key, values] of Object.entries(filterOptions)) { + if (key === filterSelections.byMetric) continue; + + const selectedValue = filterSelections.params[key]; + if (selectedValue !== undefined) { + activeFilters[key] = selectedValue; + } else if (defaultBehavior === "first" && values.length > 0) { + activeFilters[key] = values[0]; + } + } + + // 4. Calculate the final matched runs based on the current activeFilters + const matchedRuns = matchRuns(runs, activeFilters); + + return { variables, filterOptions, matchedRuns }; +} diff --git a/report/src/hooks/useBenchmarkFilters.ts b/report/src/hooks/useBenchmarkFilters.ts new file mode 100644 index 00000000..517b7d1b --- /dev/null +++ b/report/src/hooks/useBenchmarkFilters.ts @@ -0,0 +1,107 @@ +import { useCallback, useMemo } from "react"; +import { isEqual } from "lodash"; +import { type BenchmarkRun } from "../types"; +import { useSearchParamsState } from "../utils/useSearchParamsState"; +import { getBenchmarkVariables } from "../filter"; +type FilterValue = string | number | boolean; +type FilterSelectionsParams = Record; +type FilterSelections = { + params: FilterSelectionsParams; + byMetric: string; +}; + +/** + * Custom hook to manage benchmark filter selections and derived data. + * Encapsulates state logic and ensures filter consistency after updates. + * + * @param benchmarkRuns - The raw benchmark runs data. + * @param defaultMetric - The default metric to group by ('role' if not specified). + * @returns An object containing derived data and state management functions. + */ +export function useBenchmarkFilters( + runsWithRoles: BenchmarkRun[], + defaultMetric: string = "role", +) { + const [filterSelections, setRawFilterSelections] = + useSearchParamsState("filters", { + params: {}, + byMetric: defaultMetric, + }); + + // Memoize variables once, as they don't depend on selections + const variables = useMemo(() => { + const allPossibleValues: Record> = {}; + for (const run of runsWithRoles) { + for (const [key, value] of Object.entries(run.testConfig)) { + if (!allPossibleValues[key]) { + allPossibleValues[key] = new Set(); + } + allPossibleValues[key].add(value); + } + } + return Object.fromEntries( + Object.entries(allPossibleValues) + .filter(([, values]) => values.size > 1) + .map(([key, values]) => [key, [...values].sort()]), + ); + }, [runsWithRoles]); + + // Calculate current options and matched runs based on current selections + variables + const { filterOptions, matchedRuns } = useMemo(() => { + // Ensure a default byMetric if somehow cleared + const currentSelections = { + ...filterSelections, + byMetric: filterSelections.byMetric || defaultMetric, + }; + // Pass memoized variables to avoid recalculating them inside + return getBenchmarkVariables( + runsWithRoles, + currentSelections, + variables, + "first", + ); + }, [runsWithRoles, filterSelections, defaultMetric, variables]); + + // Define the setter function (simplified: no adjustment logic) + const setFilters = useCallback( + (name: string, value: FilterValue) => { + const prevState = filterSelections; + + const targetParams = { + ...prevState.params, + [name]: value, + }; + + const targetFilterSelections = { + ...prevState, + params: targetParams, + }; + + if (!isEqual(targetFilterSelections, prevState)) { + setRawFilterSelections(targetFilterSelections); + } + }, + [filterSelections, setRawFilterSelections], + ); + + const setByMetric = useCallback( + (metric: string) => { + // when by metric changes, reset all other filters + + setRawFilterSelections({ + params: {}, + byMetric: metric, + }); + }, + [setRawFilterSelections], + ); + + return { + variables, + filterOptions, + matchedRuns, + filterSelections, // Return current selections for UI binding + setFilters, // Return the simplified setter + setByMetric, + }; +} diff --git a/report/src/pages/RunIndex.tsx b/report/src/pages/RunIndex.tsx index 553cb89e..d248adde 100644 --- a/report/src/pages/RunIndex.tsx +++ b/report/src/pages/RunIndex.tsx @@ -5,21 +5,49 @@ import { formatValue, } from "../utils/formatters"; import { useTestMetadata } from "../utils/useDataSeries"; -import { useMemo } from "react"; +import { useMemo, useState } from "react"; +import { getBenchmarkVariables } from "../filter"; function RunIndex() { const { data: benchmarkRuns, isLoading: isLoadingBenchmarkRuns } = useTestMetadata(); + // State for filter selections + const [filterSelections, setFilterSelections] = useState< + Record + >({}); + + // Calculate filter options and filtered runs + const { filterOptions, matchedRuns } = useMemo(() => { + if (!benchmarkRuns) { + return { filterOptions: {}, matchedRuns: [] }; + } + + // Only include non-"any" filters in the params + const activeFilters = Object.fromEntries( + Object.entries(filterSelections).filter(([, value]) => value !== "any"), + ); + + return getBenchmarkVariables( + benchmarkRuns.runs, + { + params: activeFilters, + byMetric: "N/A", + }, + undefined, + "any", + ); + }, [benchmarkRuns, filterSelections]); + // Calculate only configs that differ across runs const diffConfigKeys = useMemo(() => { - if (!benchmarkRuns) { + if (!matchedRuns) { return []; } const configKeyToValues: Record> = {}; - benchmarkRuns.runs.forEach((run) => { + matchedRuns.forEach((run) => { const runConfig = run.testConfig || {}; Object.entries(runConfig).forEach(([key, value]) => { if (!configKeyToValues[key]) { @@ -30,16 +58,18 @@ function RunIndex() { }); const differingKeys = Object.entries(configKeyToValues) - .filter(([, values]) => values.size > 1) + .filter( + ([key, values]) => values.size > 1 || filterSelections[key] !== "any", + ) .map(([key]) => key); - return benchmarkRuns.runs.map((run) => { + return matchedRuns.map((run) => { const runConfig = run.testConfig || {}; return Object.entries(runConfig).filter(([key]) => differingKeys.includes(key), ); }); - }, [benchmarkRuns]); + }, [matchedRuns, filterSelections]); if (!benchmarkRuns || isLoadingBenchmarkRuns) { return
Loading...
; @@ -47,6 +77,45 @@ function RunIndex() { return (
+ {/* Filter Interface */} +
+ {Object.entries(filterOptions) + .sort((a, b) => a[0].localeCompare(b[0])) + .map(([key, availableValues]) => { + const currentValue = filterSelections[key] ?? "any"; + return ( +
+
+ {camelToTitleCase(key)} +
+ +
+ ); + })} +
+ @@ -113,7 +182,7 @@ function RunIndex() { - {benchmarkRuns?.runs.map((run, i) => ( + {matchedRuns.map((run, i) => (
{run.testName} diff --git a/report/src/types.ts b/report/src/types.ts index bc3b3b75..997c8ea7 100644 --- a/report/src/types.ts +++ b/report/src/types.ts @@ -64,24 +64,3 @@ export interface BenchmarkRuns { runs: BenchmarkRun[]; createdAt: string; } - -export function getBenchmarkVariables(runs: BenchmarkRun[]) { - const inferredConfig: Record> = {}; - - for (const run of runs) { - for (const [key, value] of Object.entries(run.testConfig)) { - if (!inferredConfig[key]) { - inferredConfig[key] = []; - } - inferredConfig[key].push(value); - } - } - - return Object.fromEntries( - Object.entries(inferredConfig) - .filter(([, values]) => values.length > 1) - .map(([key, values]) => { - return [key, [...new Set(values)]]; - }), - ); -}